一、打开文件的内核数据结构
内核使用3种数据结构表示打开文件:进程表项、文件表项、V节点表项。
如图1所示:
图1. 打开文件的内核数据结构
-
进程表项(process table entry)
每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表。
文件描述符表由文件描述符(fd)索引,对于内核而言,所有打开文件都通过文件描述符引用。
按照惯例,文件描述符0与进程的标准输入关联,文件描述符1与进程的标准输出关联,文件描述符2与进程的标准错误关联。
-
文件描述符
每个文件描述符,索引文件描述符表中的一个矢量。
文件描述符表中的每一项包括:文件描述符标志(fd flags)、指向文件表项的指针(file pointer)。
-
文件描述符标志
文件描述符标志包括FD_CLOEXEC(close-on-exec)标志:
如果设置FD_CLOEXEC标志,那么对应的fd在执行exec后时会失效。
一般fork后的子进程和父进程共用文件描述符表,但在exec后,子进程就不能使用设置了FD_CLOEXEC标志的fd(close-on-exec)。
-
文件表项(file table entry)
文件表项包括:文件状态标志、当前文件偏移量、指向该文件V节点表项的指针。
-
文件状态标志(file status flags)
文件状态标志如图2所示,其中前5个互斥,后半部分可以附选。
图2. 文件状态标志
-
当前文件偏移量(current file offset)
当前文件偏移量,记录对该文件操作的具体位置,如从哪个位置开始读,从哪个位置开始写。
如O_APPEND就标志在文件结尾进行操作。
-
v节点表项(v-node table entry)
v节点包含文件类型和对此文件进行各种操作函数的指针,还包含了该文件的i节点。
i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。
-
共享文件
如果两个进程各自打开了同一个文件,则数据结构关系如图3所示。
图3. 共享文件
每个进程都有自己的文件表项,这是因为每个进程都有独特的对该文件的当前偏移量。
二、Open和Openat
图4. open和openat函数
path是要打开或创建文件的名字、oflag参数说明此函数的多个选项、...表明余下的参数及其类型是可变的(仅在创建新文件时,才用到这个参数)。
返回文件描述符fd。
-
oflag标志
图2的文件状态标志,在这里都可以设置,设置的文件状态标志(file status flags)。
其余:
O_CLOEXEC,设置文件描述符标志(fd flags)->FD_CLOEXEC(close-on-exec)标志。
O_CREAT,文件不存在则创建文件,需要指定mode。
O_EXCL,如果指定了O_CREA,而文件已经存在,则出错。
-
open和openat的区别
fd参数把open和openat函数区分开,共有3中可能性。
(1)path参数是绝对路径,fd参数被忽略,openat和open功能一致
(2)path参数是相对路径,fd参数标识开始地址。
(3)path参数是相对路径,fd是特殊值AT_FDCWD,open和openat功能类似
三、creat
图5. creat函数
creat函数创建一个新的文件,此函数等效于:open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
creat的不足是它只能以只写的方式打开创建的文件,在提供open的新版本前,如果要创建一个临时文件,并要先写后读该文件,
则必须先调用creat、close、open。
四、close
关闭一个打开文件:
图5. close函数
当一个进程终止时,内核自动关闭它所有的打开文件。很多程序利用这个特性,而不显式地用close关闭打开文件。
五、lseek
图6. lseek函数
lseek函数更改的是图1文件表项中的当前文件偏移量。
空洞:
文件偏移量可以大于文件的当前长度,在这种情况下对文件的下一次写将加长该文件,并在文件中构成一个空洞。
但是文件空洞,不占用磁盘存储区。
大多数平台提供两组接口以处理文件偏移量,一组用32位文件偏移量,一组用64位文件偏移量,可以用sysconf函数确定支持哪种环境。
六、read、write、I/O效率
图7. read函数
图8. write函数
预读技术改善性能, Buffer Size大到一定程度,再增加缓冲区长度则I/O效率提升不明显。
七、dup和dup2
图9. dup和dup2函数
dup和dup2都可用来复制一个现有的文件描述符。
dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。
dup2可以用fd2制定新文件描述符的值,如果fd2已经打开,则先关闭。如果fd=fd2,则直接返回,不关闭fd2。
效果如下:
图10. dup后的内核数据结构
dup2等效于
close(fd2);
fcntl(fd, F_DUPFD, fd2);
但是dup2是原子操作,原子操作指的是由多步组成的一个操作,要么一步也不执行,要么执行完所有步骤,不可能执行所有步骤的一个子集。
八、sync、fsync、fdatasync
图11. sync、fsync、fdatasync
sync将所有修改过的块缓冲区排入写队列,不等待实际写磁盘操作结束。
fsync只对fd指定的文件起作用,并且等待磁盘写操作结束后才返回。
fdatasync与fsync类似,但只影响文件的数据部分,而fsync还会同步文件的属性。
九、fcntl
图12. fcntl函数
fcntl函数有5种功能。
(1)复制一个已有的fd(cmd = F_DUPFD 或 F_DUPFD_CLOEXEC)
与dup、dup2等类似。
(2)获取/设置文件描述符标志(cmd = F_GETFD 或 F_SETFD)
文件描述符标志FD_CLOEXEC
(3)获取/设置文件状态标志(cmd = F_GETFL 或 F_SETFL)
即更改图1文件表项中的文件状态标志
(4)获取/设置异步I/O所有权(cmd = F_GETOWN 或 F_SETOWN)
设置/获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID
(5)获取/设置记录锁(cmd = F_GETLK、F_SETLK或F_SETLKW)
十、ioctl
ioctl函数是I/O操作的杂物箱,不能用之前的函数表示的I/O操作通常都能用ioctl表示。
图13. ioctl函数