一、原子操作和竞争条件
-
所有系统调用都是以原子操作方式执行的。之所以这么说,是指内核保证了某系统调用中的所有步骤会作为独立操作而一次性加以执行,其间不会为其他进程或线程所中断。
-
以独占方式创建一个文件
-
结合 O_CREAT 和 O_EXCL 标志来一次性地调用 open()可以防止这种情况,因
为这确保了检查文件和创建文件的步骤属于一个单一的原子(即不可中断的)操作。
-
向文件尾部追加数据
-
将文件偏移量的移动与数据写操作纳入同一原子操作。在打开文件时加入 O_APPEND 标志就可以保证这一点。
二、文件控制操作:fcntl()
-
fcntl()系统调用对一个打开的文件描述符执行一系列控制操作
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
2.fcntl中的cmd:
cmd指令 | 含义 | 示例 |
---|---|---|
F_GETFL | 获取其访问模式和状态标志 | int flags,accessMode;flags = fcntl(fd,F_GETFL);if(flags & O_SYNC) .... |
F_SETFL | 修改打开文件的某些状态标志 | 允许更改的标志有O_APPEND、 O_NONBLOCK、 O_NOATIME、O_ASYNC 和 O_DIRECT |
-
判定文件的访问模式有一点复杂,这是因为 O_RDONLY(0)、O_WRONLY(1)和 O_RDWR(2)这 3 个常量并不与打开文件状态标志中的单个比特位对应。因此,要判定访问模式,需使用掩码 O_ACCMODE 与 flag 相与,将结果与 3 个常量进行比对,示例代码如下:
accessMode = flags & O_ACCMODE;
if(accessMode == O_WRONLY || accessMode == O_RDWR)
//....
使用 fcntl()修改文件状态标志,尤其适用于如下场景。
-
文件不是由调用程序打开的,所以程序也无法使用 open()调用来控制文件的状态标志。
-
文件描述符的获取是通过 open()之外的系统调用。比如 pipe()调用,该调用创建一个管道,并返回两个文件描述符分别对应管道的两端。再比如 socket()调用,该调用创建一个套接字并返回指向该套接字的文件描述符。
三、文件描述符和打开文件之间的关系
-
要理解具体情况如何,需要查看由内核维护的 3 个数据结构。
-
进程级的文件描述符表。
-
系统级的打开文件表。
-
文件系统的 i-node 表。
-
针对每个进程,内核为其维护打开文件的描述符(open file descriptor)表。该表的每一条目都记录了单个文件描述符的相关信息,如下所示
-
控制文件描述符操作的一组标志。(目前,此类标志仅定义了一个,即 close-on-exec 标志)。
-
对打开文件句柄的引用。
-
内核对所有打开的文件维护有一个系统级的描述表格(open file description table)。有时,也称之为打开文件表(open file table),并将表中各条目称为打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息,如下所示:
-
当前文件偏移量(调用 read()和 write()时更新,或使用 lseek()直接修改)。
-
打开文件时所使用的状态标志(即,open()的 flags 参数)。
-
文件访问模式(如调用 open()时所设置的只读模式、只写模式或读写模式)。
-
与信号驱动 I/O 相关的设置。
-
对该文件 i-node 对象的引用。
-
每个文件系统都会为驻留其上的所有文件建立一个 i-node 表:
-
文件类型(例如,常规文件、套接字或 FIFO)和访问权限。
-
一个指针,指向该文件所持有的锁的列表。
-
文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳。
-
上图揭示出如下要点:
-
两个不同的文件描述符,若指向同一打开文件句柄,将共享同一文件偏移量。因此, 如果通过其中一个文件描述符来修改文件偏移量(由调用 read()、write()或 lseek() 所致),那么从另一文件描述符中也会观察到这一变化。无论这两个文件描述符分属于不同进程,还是同属于一个进程,情况都是如此。
-
要获取和修改打开的文件标志(例如,O_APPEND、O_NONBLOCK 和 O_ASYNC),可 执行 fcntl()的 F_GETFL 和 F_SETFL 操作,其对作用域的约束与上一条颇为类似。
-
相形之下,文件描述符标志(亦即,close-on-exec 标志)为进程和文件描述符所私有。 对这一标志的修改将不会影响同一进程或不同进程中的其他文件描述符。
四、复制文件描述符
1. dup()调用
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
2. dup()调用复制一个打开的文件描述符 oldfd,并返回一个新描述符,二者都指向同一打开 的文件句柄。系统会保证新描述符一定是编号值最低的未用文件描述符。
3. dup2()系统调用会为 oldfd 参数所指定的文件描述符创建副本,其编号由 newfd 参数指定。 如果由 newfd 参数所指定编号的文件描述符之前已经打开,那么 dup2()会首先将其关闭。
4. fcntl()的 F_DUPFD 操作是复制文件描述符的另一接口,更具灵活性
//将使用大于等于startfd的最小未用值作为描述符编号newfd = fcntl(oldfd,F_DUPFD,startfd); |
---|
-
该调用还能保证新描述符(newfd)编号落在特定的区间范围内。
-
文件描述符的正、副本之间共享同一打开文件句柄所含的文件偏移量和状 态标志。然而,新文件描述符有其自己的一套文件描述符标志,且其 close-on-exec 标志 (FD_CLOEXEC)总是处于关闭状态。
-
Linux 从 2.6.24 开始支持 fcntl()用于复制文件描述符的附加命令:F_DUPFD_CLOEXEC。 该标志不仅实现了与 F_DUPFD 相同的功能,还为新文件描述符设置 close-on-exec 标志。
5. dup3()系统调用完成的工作与 dup2()相同,只是新增了一个附加参数 flag,这是一个可以 修改系统调用行为的位掩码。
#define _GNU_SOURCE /* See feature_test_macros(7) */ #include <fcntl.h> /* Obtain O_* constant definitions */ #include <unistd.h> int dup3(int oldfd, int newfd, int flags);
-
目前,dup3()只支持一个标志 O_CLOEXEC,这将促使内核为新文件描述符设置 close-on-exec标志(FD_CLOEXEC)。
五、在文件特定偏移量处的I/O: pread()和pwrite()
-
pread()和pwrite()
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
六、分散输入和集中输出(Scatter-Gather I/O):readv() 和 writev()
-
readv()和 writev()系统调用分别实现了分散输入和集中输出的功能
-
#include <sys/uio.h> ssize_t readv(int fd, const struct iovec *iov, int iovcnt); ssize_t writev(int fd, const struct iovec *iov, int iovcnt); ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset); ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset); struct iovec { void *iov_base; /* Starting address */ size_t iov_len; /* Number of bytes to transfer */ };
-
readv()系统调用实现了分散输入的功能:从文件描述符 fd 所指代的文件中读取一片连续 的字节,然后将其散置(“分散放置”)于 iov 指定的缓冲区中。这一散置动作从 iov[0]开始, 依次填满每个缓冲区。
-
原子性是 readv()的重要属性。换言之,从调用进程的角度来看,当调用 readv()时,内核 在 fd 所指代的文件与用户内存之间一次性地完成了数据转移。这意味着,假设即使有另一进 程(或线程)与其共享同一文件偏移量,且在调用 readv()的同时企图修改文件偏移量,readv() 所读取的数据仍将是连续的。
-
调用 readv()成功将返回读取的字节数,若文件结束将返回 0。调用者必须对返回值进行 检查,以验证读取的字节数是否满足要求。若数据不足以填充所有缓冲区,则只会占用部分缓冲区,其中最后一个缓冲区可能只存有部分数据
struct iovec iov[3]; fd = open(argv[1], O_RDONLY); if (fd == -1) errExit("open"); totRequired = 0; iov[0].iov_base = &myStruct; iov[0].iov_len = sizeof(struct stat); totRequired += iov[0].iov_len; iov[1].iov_base = &x; iov[1].iov_len = sizeof(x); totRequired += iov[1].iov_len; iov[2].iov_base = str; iov[2].iov_len = STR_SIZE; totRequired += iov[2].iov_len; numRead = readv(fd, iov, 3); if (numRead == -1) errExit("readv"); if (numRead < totRequired)
printf("Read fewer bytes than requested\n");
七、截断文件:truncate()和ftruncate()系统调用
#include <unistd.h> #include <sys/types.h> //若文件当前长度大于参数 length,调用将丢弃超出部分, //若小于参数 length,调用将在文件尾部添加一系列空字节或是一个文件空洞。 int truncate(const char *path, off_t length); int ftruncate(int fd, off_t length);
八、非阻塞I/O
-
在打开文件时指定 O_NONBLOCK 标志,目的有二:
-
若 open()调用未能立即打开文件,则返回错误,而非陷入阻塞。有一种情况属于例外, 调用 open()操作 FIFO 可能会陷入阻塞。
-
调用 open()成功后,后续的 I/O 操作也是非阻塞的。若 I/O 系统调用未能立即完成,则可能会只传输部分数据,或者系统调用失败,并返回 EAGAIN或EWOULDBLOCK 错误。具体返回何种错误将依赖于系统调用。Linux 系统与许多 UNIX 实现一样,将两 个错误常量视为同义。
九、大文件 I/O
-
应用程序可使用如下两种方式之一以获得 LFS 功能。:
-
在编译应用程序时,将宏_FILE_OFFSET_BITS 的值定义为 64。这一方法更为可取,因 为符合 SUS 规范的应用程序无需修改任何源码即可获得 LFS 功能。
十、/dev/fd 目录
-
对于每个进程,内核都提供有一个特殊的虚拟目录/dev/fd。该目录中包含“/dev/fd/n”形 式的文件名,其中 n 是与进程中的打开文件描述符相对应的编号。因此,例如, /dev/fd/0 就对应于进程的标准输入。
-
打开/dev/fd 目录中的一个文件等同于复制相应的文件描述符,所以下列两行代码是等价的:
fd = open("/dev/fs/1",O_WRONLY); fd = dup(1);