一、文件I/O概述
- 在前面的一系列文章中,我们介绍的是“标准I/O”,接下来的系列文章中我们介绍“文件I/O”——打开文件、读文件、写文件等
- 文件I/O在平常使用中一般只需用到5个函数:open()、read()、write()、lseek()、close()
- 文件I/O函数是不带缓冲的,这与“标准I/O”不同。不带缓冲指的是每个read和write都调用内核中的一个系统调用。这些不带缓冲区的I/O函数不是ISO C的组成部分。但是它们是POSIX.1和Single UNIX Specification的组成部门
二、文件描述符
- 对于内核而言,所有打开的文件都通过文件描述符引用。是一个非负整数。当打开一个现有文件或创建余个新文件时,内核向进程返回一个文件描述符
- 文件描述符的变化范围是0~OPEN_MAX-1(参阅:https://blog.csdn.net/qq_41453285/article/details/89959451)
标准输入、标准输出、标准错误
- 在UNIX系统shell中把:
- 文件描述符0:表示标准输入
- 文件描述符1:表示标准输出
- 文件描述符2:表示标准错误
- 在头文件中<unistd.h>种用常量表示了上面的描述符值:
- STDIN_FILENO:0
- STDOUT_FILENO:1
- STDERR_FILENO:2
三、open()、openat()
#include<fcntl.h>
int open(const char *pathname, int oflag, .../*, mode_t mode*/);
int openat(int fd, const char *pathname, int oflag, .../*, mode_t mode*/);
//返回值:若成功为文件描述符,若出错为-1
- 功能:调用这两个函数可以打开或创建一个文件
openat()函数的fd参数
- ①如果path参数指定的是绝对路径名,则fd参数会被忽略。openat函数相当于open
- ②path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取
- ③path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。这种情况下,路径名在当前工作目录中获取。openat()在操作上与open()函数类似
pathname参数
- 表示打开/创建的文件的名称
oflag参数
- 下面的选项在头文件<fcntl.h>中定义
- 下面这5个选项必须指定一个,并且只能指定一个:
O_RDONLY 只读打开 O_WRONLY 只写打开 O_RDWR 读、写打开 O_EXEC 只执行打开 O_SEARCH 只搜索打开(应用于目录)。
目的在于目录打开时验证它的搜索权限。对目录的文件描述符的后续操作就不需要再次检查该目录的搜索权限(目前还不支持这个选项)
- 下面的选项是可选的:
O_APPEND 每次写时都追加到文件的尾端 O_CLOEXEC 把FD_CLOEXEC常量设置为文件描述符标志 O_CREAT 若此文件不存在则创建它。使用此选择项时,需同时说明第三个参数mode,mode代表该新文件的权限 O_DIRECTORY 如果path引用的不是目录,则出错 O_EXCL 如果同时指定了O_ CREAT,而文件已经存在,则出错。这可测试一个文件是 否存在,如果不存在则创建此文件。使得测试和创建成为一个原子操作 O_NOCTTY 如果pathname引用的是终端设备,则不将该设备分配作为此进程的控制终端 O_NOFOLLOW 如果pathname引用的是一个符号链接,则出错 O_NONBLOCK 如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件, 则此选择项为此文件的本次打开操作和后续的I/O操作设置非阻塞方式
O_SYNC 使每次write都等到物理I/O操作完成(直至数据已写到磁盘上再返回),包括由该write操作引起的文件属性更新所需的I/O。数据库系统需要使用这个参数
通常write只将数据排入队列就返回了,而实际的写磁盘操作则可能在以后的某个时刻进行。如果使用O_SYNC,则直至数据已写到磁盘上write才返回,以免系统异常时产生数据丢失
O_SYNC标志会增加系统时间和时钟时间
O_TRUNC 如果此文件存在,而且为只读或只写成功打开,则将其长度截短为0 O_TTY_INIT 如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合Single UNIX Specification
- 下面的选项也是可选的。它们是Single UNIX Specification(以及POSIX.1)中同步输入和输出选项的一部分
O_DSYNC 使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需要等待文件属性被更新
O_RSYNC 使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成
mode参数如下
- openat()函数是POSIX.1最新版本新增的一类函数之一,希望解决两个问题:
- 第一:让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前的工作目录。在“线程处理”系列文章中将看到,同一进程中的所有线程共享相同的当前工作目录,因此很难让同一进程的多个不同线程在同一时间工作在不同的目录中
- 第二:
- 可以避免time-of-check-to-time-of-user(TOCTTOU)错误
- TOCTTOU错误的基本思想是:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序是脆弱的。因为两个调用并不是原子操作,在两个函数调用之间文件可能改变了,这样也就造成了第一个调用的结果就不再有效,使得程序最终的结果是错误的。文件系统命名空间中的TOCTTOU通常处理的就是那些颠覆文件系统权限的小把戏,这些小把戏通过骗取特权程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行。Wei和Pu在UNIX文件系统中讨论了TOCTTOU错误
文件名限制和路径名截断
- NAME_MAX:表示文件名的最大字节数
- 如果NAME_MAX是14,而我们却创建了一个文件名包含15个字符的新文件,那么此时会发生什么呢?
- 早期的System V版本(如SVR2)允许这种使用方法,但总是将文件名截断为14个字符,而且不给出任何信息
- BSD类的系统则返回出错状态,并将errno设置为ENAMETOOLONG
- _POSIX_NO_TRUNC:
- 表示当文件名长度超过NAME_MAX时,决定要决定截断过程的文件名或路径名,还是返回一个错误
- 如果_POSIX_NO_TRUNC有效,则超过NAME_MAX时,出错返回将errno设置为ENAMETOOLONG
四、creat()
#include<fcntl.h>
int creat(const char* path, mode_t mode);
//参数:1.文件路径名 2.创建文件的权限
//返回值:成功返回只写打开的文件描述符;失败的返回-1
- 功能:此函数创建一个新文件
- 此函数等效于:
open(pathname, O_WRONL|YO_CREAT|O_TRUNC, mode);
该函数已经淘汰了
- 在早期的UNIX系统中,open()的第二个参数只能是0、1、2,因此无法创建一个新文件,所以才使用creat来创建文件
- 现在open()函数提供了O_CREAT和O_TRUNC,因此这个函数就淘汰了
- 注意事项:
- 这个函数创建文件之后,直接把这个文件以只写的方式打开
- 因为这个函数只能以只写的方式打开所创建的文件,所以在open()新版本出来之前,你必须先调用creat()创建一个函数,然后再close()关闭文件,然后再调用老版本的open()打开文件进行读写。open()新版本出来之后,直接调用下面的代码就可以了
open(pathname, O_RDWR|YO_CREAT|O_TRUNC, mode);
五、close()
#include<unistd.h>
int close(int fd);
//返回值:成功返回0;出错,返回-1
//参数:文件描述符
- 功能:关闭一个打开的文件描述符
- 两点提示:
- 关闭一个文件还会释放该进程加在文件上的所有记录锁(请参阅:https://blog.csdn.net/qq_41453285/article/details/102630915)
- 当一个进程终止时,它所有的打开文件都由内核自动关闭。很多程序都使用这一功能而不显式地用close关闭打开的文件
六、lseek()
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
//返回:若成功为新的文件位移(长整型),若出错为-1
//参数2:相对于参数3的偏移量(正数表示正向偏移,负数表示负向偏移)。参数3:设定从文件的哪里开始偏移
参数3的取值
- SEEK_SET: 文件开头(0)
- SEEK_CUR: 当前位置(1)
- SEEK_END: 文件结尾(2)
- 如果文件描述符引用的是一个管道或FIFO,则l seek返回-1,并将errno设置为EPIPE
- 通常,文件的当前位移量应当是一个非负整数,但是,某些设备也可能允许负的位移量。 但对于普通文件,则其位移量必须是非负值。因为位移量可能是负值,所以在比较lseek的返回 值时应当谨慎,不要测试它是否小于0,而要测试它是否等于-1
- 注意:文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件, 并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被设为0
- 这些0还存储在文件中,但是查看时不会显示。使用vim编辑时会显示
- 文件中的空洞并不要求在磁盘上占用存储区。具体处理与文件系统的实现有关。但是新写入的数据需要磁盘数量存储
#include <fcntl.h> #include<stdlib.h> #include<stdli.h> char buf1[] = "abcdefghij"; char buf2[] = "ABCDEFGHIJ"; int main(void) { int fd; if ((fd = creat("file.hole", 0644)) < 0) perror("creat error"); if (write(fd, buf1, 10) != 10) perror("buf1 write error"); /* offset now = 10 */ if (lseek(fd,30, SEEK_SET) == -1) perror("lseek error"); /* offset now = 30*/ if (write(fd, buf2, 10) != 10) perror("buf2 write error"); /* offset now = 40*/ exit(0); }
- 查看文件的内容,以及文件存储在的内存
![]()
七、read()
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
- 功能:从打开的文件描述符中读取数据
- 参数:
- fd:打开的文件描述符
- buf:存放读取数据的缓冲区
- nbytes:读取的字节数
- 返回值:
- 成功:返回读到的字节数
- 已到文件尾:返回0
- 失败:返回-1
- 读取操作会使偏移量也向前移动
有多种情况可使实际读到的字节数少于要求读字节数
- 读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之 前还有3 0个字节,而要求读1 0 0个字节,则read返回3 0,下一次再调用read时,它将返回0 (文件尾端)
- 当从终端设备读时,通常一次最多读一行(“终端I/O”文章会介绍如何改变这一点)
- 当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数
- 某些面向记录的设备,例如磁带,一次最多返回一个记录
- 当一信号造成中断,而已经读了部分数据量时(详情参阅:https://blog.csdn.net/qq_41453285/article/details/89216990)
八、write()
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
- 功能:向打开的文件描述符中写入数据
- 参数:
- fd:打开的文件描述符
- buf:要写入的内容
- nbytes:写入的字节数
- 返回值:
- 成功:返回写入的字节数
- 失败:返回-1
- write出错的一个常见原因是:磁盘已写满,或者超过了对一个给定进程的文件长度限制
- 偏移量:
- 对于普通文件,写操作从文件的当前偏移量处开始
- 如果在打开该文件时指定了O_APPEND选项,则每次写操作之前,将文件偏移量设置在文件的当前尾部
- 在一些成功写之后,该文件的偏移量也会向后移动
九、演示案例
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#define BUFFSIZE 1024
int main()
{
int n;
char buff[BUFFSIZE];
while((n=read(STDIN_FILENO,buff,BUFFSIZE))>0)
if(write(STDOUT_FILENO,buff,n)!=n)
perror("write");
if(n<0)
perror("read");
exit(0);
}
- 效果如下: