【TLPI读书笔记】 五、深入探究文件I/O
原子操作和竞争条件
原子操作:
- 将某一系统调用所要完成的各个动作为不可中断的操作,一次性加以执行。
- 所有系统调用都是以原子操作方式执行的。
- 原子性是某些操作得以圆满成功的关键所在。特别是它规避了竞争状态(如情形:操作共享的资源的两个进程或线程,其结果取决于一个无法预期的顺序,即这些进程获得CPU使用权的先后相对顺序)
以独占方式创建一个文件
当同时指定O_EXCL
与O_CREAT
作为open()
的标志位时,如果要打开的文件已然存在,则open()
将返回一个一个错误,这提供了一种机制:保证进程是打开文件的创建者。对文件是否存在的检查和创建文件属于同一原子操作。
向文件尾部追加数据
多个进程同时向一个文件尾部追加数据,为避免进程间互相覆盖数据,应在打开文件时加入O_APPEND
标志可以保证原子操作。
文件控制操作:fcntl()
fcntl()
系统调用对一个打开的文件描述符执行一系列控制操作。
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
Return on success depends on cmd, or -1 on error
参数
fd
:文件描述符cmd
:cmd参数所支持的操作范围很广,下面将介绍
返回值
- 成功:依据
cmd
返回相应的值 - 失败:返回
-1
,并将errno
置为相应的错误标志
打开文件的状态标志
fcntl()
的用途之一是针对一个打开的文件,获取或修改其访问模式和状态标志(这些值是通过open()
调用的flag
参数来设置的)。
要获取这些设置应该将
fcntl()
的cmd
参数设置为F_GETFL
。判断访问模式:需使用掩码
O_ACCMODE
与flag
相与accessMode = flag & O_ACCMODE; if (accessMode == O_WRONLY || accessMode == O_RDWR) //判断是否可写
判断状态标志:和需要判断的标志作按位与运算即可
flags = fcntl(fd, F_GETFL); if (flags & O_SYNC) //判断是否以同步方式打开
可以使用
fcntl()
的F_SETFL
命令来修改打开文件的某些状态标志。允许更改的标志有:O_APPEND
、O_NONBLOCK
、O_NOATIME
、O_ASYNC
和O_DIRECT
。系统将忽略对其他标志的修改操作。flags = fcntl(fd, F_GETFL); flags |= O_APPEND; //添加 O_APPEND 标志 if (fcntl(fd, F_SETFL, flags) == -1)
文件描述符和打开文件之间的关系
内核维护了3个数据结构:
进程级的文件描述符表
- 控制文件描述符操作的一组标志
- 对打开文件句柄的引用
系统级的打开文件表
- 当前文件偏移量
- 打开文件时所使用的状态标志
- 文件访问模式
- 与信号驱动I/O相关的设置
- 对该文件
i-node
的引用
文件系统的
i-node
表- 文件类型
- 一个指针,指向该文件所持有的锁的列表
- 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳
即从上至下的引用关系,所以就有了两个文件描述符指向同一个打开文件句柄即可共享文件偏移量的机制。
复制文件描述符
dup()
调用复制一个打开的文件描述符oldfd
,并返回一个新描述符,二者都指向同一打开的文件句柄。系统会保证新描述符一定是编号值最低的未用文件描述符。
#include <unistd.h>
ssize_t dup(int oldfd);
// Returns (new)file descriptor on success, or -1 on error
参数
oldfd
:文件描述符
返回值
- 成功:返回文件描述符
- 失败:返回
-1
,并将errno
置为相应的错误标志
如果想获得所期望的文件描述符,可以使用dup2()
#include <unistd.h>
ssize_t dup2(int oldfd, int newfd);
// Returns (new)file descriptor on success, or -1 on error
dup2()
系统调用会为oldfd
参数所指定的文件描述符创建副本,其编号由newfd
参数所指定。如果newfd
参数所指定的文件描述符之前已经打开,那么dup2()
会首先将其关闭
参数
oldfd
:文件描述符newfd
:期望的新文件描述符
返回值
- 成功:返回文件描述符
- 失败:返回
-1
,并将errno
置为相应的错误标志
dup3()
系统调用完成的工作与dup2()
相同,只是新增了一个附加参数flags
,这是一个可以修改系统调用行为的位掩码
#include <unistd.h>
ssize_t dup3(int oldfd, int newfd, int flags);
// Returns (new)file descriptor on success, or -1 on error
参数
oldfd
:文件描述符newfd
:期望的新文件描述符flags
:标志;目前,dup3()
只支持一个标志O_CLOEXEC
,这将促使内核为新文件描述符设置close-on-exec
标志
返回值
- 成功:返回文件描述符
- 失败:返回
-1
,并将errno
置为相应的错误标志
在文件特定偏移量处的I/O:pread()
和pwrite()
系统调用pread()
和pwrite()
完成与read()
和write()
相类似的工作,只是前两者会在offset
参数所指定的位置进行文件I/O操作,而非始于文件的当前偏移量处,且他们不会改变文件的当前偏移量
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
// Returns number of bytes read, 0 on EOF, or -1 on error
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
// Returns number of bytes written, or -1 on error
这两系统调用等同于将读或写操作和偏移量设置(lseek()
)纳入同一个原子操作
参数
fd
:文件描述符buf
:缓冲区地址count
:读或写操作的最大字节数offset
:偏移量
返回值
- 成功:返回读或写操作的字节数
- 失败:返回
-1
,并将errno
置为相应的错误标志
分散输入和集中输出(Scatter-Gather I/O):readv()
和writev()
readv()
和writev()
系统调用分别实现了分散输入和集中输出的功能
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
// Returns number of bytes read, 0 on EOF, or -1 on error
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
// Returns number of bytes written, or -1 on error
这些系统调用并非只对单个缓冲区进行读写,而是一次即可传输多个缓冲区的数据。
参数
fd
:文件描述符iov
:定义了一组用来传输数据的缓冲区,其中每个成员都是如下形式的数据结构struct iovec{ void *iov_base; size_t iov_len; }
iovcnt
:指定了iov
的成员个数
返回值
- 成功:返回读或写操作的字节数
- 失败:返回
-1
,并将errno
置为相应的错误标志
截断文件:truncate()
和ftruncate()
系统调用
truncate()
和ftruncate()
系统调用将文件大小设置为length
参数所指定的值
#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
// Both Return 0 on success, or -1 on error
参数
pathname
/fd
:文件路径/文件描述符length
:长度,若文件长度长于该参数将丢弃超出的部分,若小于该参数,将在尾部添加一系列空字节或文件空洞
返回值
- 成功:返回
0
- 失败:返回
-1
,并将errno
置为相应的错误标志
非阻塞I/O
在打开文件时指定O_NONBLOCK
标志,目的有二
- 若
open()
调用未能立即打开文件,则返回错误,而非陷入阻塞 - 调用
open()
成功后,后续的I/O操作也是非阻塞的
大文件I/O
通常将存放文件偏移量的数据类型off_t
实现为一个有符号的长整型(以-1
表示错误),在32位系统架构中这将文件大小置于2GB的限制之下。
为了针对大文件支持,始于内核版本2.4,32位Linux系统开始提供对LFS的支持(glibc版本必须为2.2或更高),此外相应的文件系统也必须支持大文件操作。
应用程序可使用如下两种方式之一获得LFS功能
- 使用支持大文件操作的备选API。此类函数通常尾部以64以示区别,其中包括:
fopen64()
、open64()
、lseek64()
、truncate64()
、stat64()
、mmap64()
和setrlimit64()
- 在编译应用程序时,将宏
_FILE_OFFSET_BITS
的值定义为64。推荐此方式,因为不用修改代码
/dev/fd
目录
对于每个进程,内核都提供有一个特殊的虚拟目录/dev/fd
。该目录中包含“/dev/fd/n
”形式的文件名,其中n
是与进程中的打开文件描述符相对应的编号。
打开/dev/fd
目录中的一个文件等同于复制相应的文件描述符,在为open
调用设置flags
参数时,需要注意将其设置为与原描述符相同的访问模式
创建临时文件
有些程序需要创建一些临时文件,仅供其在运行期间使用,程序终止后即行删除。GNU C函数库为此提供了一些列库函数,这里介绍其中两个:mkstemp()和tmpfile()。
#include <stdlib.h>
int mkstemp(char *template);
// Returns file descriptor on success, or -1 on error
该函数打开文件时使用了O_EXCL
标志,以保证调用者以独占方式访问文件;该函数不会自动删除临时文件,需要手动调用unlink()
参数
template
:模板文件路径名,其中最后6个字符必须为XXXXXX
。这6个字符将被替换,以保证文件名的唯一性,且修改后的字符串将通过该参数传回
返回值
- 成功:返回
0
- 失败:返回
-1
,并将errno
置为相应的错误标志
tmpfile()
函数会创建一个名称唯一的临时文件,并以读写方式将其打开(使用了O_EXCL
)
#include <stdlib.h>
FILE tmpfile(void);
// Returns file descriptor on success, or NULL on error
该函数会在内部自动删除临时文件
返回值
- 成功:返回文件流,供stdio库函数使用
- 失败:返回
NULL
总结
本章主要讲解了原子操作和竞争条件,这是本章的核心内容;然后讲解了文件描述符和打开文件之间的关系,这也是重点概念;其次讲解了一些相关的系统调用和库函数