原子操作和竞争条件

所有系统调用都是以原子操作方式执行的。指内核保证了某系统调用中的所有步骤会作为独立操作而一次性加以执行,其间不会为其他进程或线程所中断。

以独占的方式创建文件

当同时指定 O_EXCL 与 O_CREAT 作为 open()的标志位时,如果要打开的文件已然存在,则 open()将返回一个错误。由此可以使用一次 open 系统调用创建新文件并打开(以检查再创建的方式需要进行同步)。

向文件尾部追加数据

如果使用 lseek 和 write 组合的方式向文件末尾写入数据,那么存在的同步问题同上,在打开文件时加入 O_APPEND 标志就可以保证这一点。

fcntl() 文件控制操作

int fcntl(int fd, int cmd, ...)
Return on success depends on cmd, or -1 on errer.

对一个文件执行一个原子性的操作,... 标识操作的参数。

打开文件的状态标志

可以使用 fcntl() 配合 cmd 宏 GETFL 和 SETFL 来获取和设置文件的状态标志位,允许更改的标志有 O_APPEND、O_NONBLOCK、O_NOATIME、O_ASYNC 和 O_DIRECT。系统将忽略对其他标志的修改操作(有些其他的 UNIX 实现允许 fcntl() 修改其他标志,如 O_SYNC)。

需要以 fcntl() 而非 open 来访问文件状态的场景有:

  • 文件不是由调用程序打开的。
  • 文件是由非 open() 系统调用打开的。比如 pipe() 创建一个管道,返回两个文件描述符对应管道的两端;socket()调用,该调用创建一个套接字并返回指向该套接字的文件描述符。

文件描述符和打开文件之间的关系

文件描述符和打开的文件之间并非一对一的关系,可能存在多个文件描述符指向同一个打开的文件的场景(在不同的进程中打开)。

内核中维护的和打开文件相关的数据结构:

  • 进程级的文件描述符表。
    针对每个进程,内核为其维护打开文件的描述符(open file descriptor)表。该表的每一条目都记录了单个文件描述符的相关信息:
    * 控制文件描述符操作的一组标志;
    * 对打开文件句柄的引用。
  • 系统级的打开文件表。
    内核对所有打开的文件维护有一个系统级的描述表格(open file description table)。有时,也称之为打开文件表(open file table)并将表中各条目称为打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息:
    * 当前文件偏移量(调用 read()和 write()时更新,或使用 lseek()直接修改);
    * 打开文件时所使用的状态标志(即 open() 的 flags 参数);
    * 文件访问模式(如调用 open()时所设置的只读模式、只写模式或读写模式);
    * 与信号驱动 I/O 相关的设置;
    * 对该文件 i-node 对象的引用。
  • 文件系统的 i-node 表。
    每个文件系统都会为驻留其上的所有文件建立一个 i-node 表。出每个文件的 i-node 信息:
    * 文件类型(例如,常规文件、套接字或 FIFO)和访问权限;
    * 一个指针,指向该文件所持有的锁的列表;
    * 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳。
    磁盘和内存中保存的 i-node 记录不同,磁盘上的 i-node 记录了文件的固有属性,诸如:文件类型、访问权限和时间戳。访问一个文件时,会在内存中为 i-node 创建一个副本,其中记录了引用该 i-node 的打开文件句柄数量以及该 i-node 所在设备的主、从设备号,还包括一些打开文件时与文件相关的临时属性,例如:文件锁。

一些说明:

  • 两个不同的文件描述符,若指向同一打开文件句柄,将共享同一文件偏移量。因此, 如果通过其中一个文件描述符来修改文件偏移量(由调用 read()、write() 或 lseek() 所致),那么从另一文件描述符中也会观察到这一变化。无论这两个文件描述符分属于不同进程,还是同属于一个进程,情况都是如此。
  • 要获取和修改打开的文件标志(例如,O_APPEND、O_NONBLOCK 和 O_ASYNC),可执行 fcntl()的 F_GETFL 和 F_SETFL 操作,其对作用域的约束与上一条颇为类似。
  • 相形之下,文件描述符标志(亦即,close-on-exec 标志)为进程和文件描述符所私有。 对这一标志的修改将不会影响同一进程或不同进程中的其他文件描述符。

复制文件描述符

Bourne shell 的 I/O 重定向语法 2>&1,意在通知 shell 把标准错误(文件描述符 2)重定向到标准输出(文件描述符 1)。

shell 通过复制文件描述符 2 实现了标准错误的重定向操作,因此文件描述符 2 与文件描述符 1 指向同一个打开文件句柄,可以通过调用 dup() 和 dup2() 来实现此功能。

要满足 shell 的这一要求,仅仅简单地打开 results.log 文件两次是远远不够的,首先两个文件描述符不能共享相同的文件偏移量指针,因此有可能导致相互覆盖彼此的输出。再者打开的文件不一定就是磁盘文件。

dup()

int dup(int oldfd)
Returns new file descriptor on success, or -1 on error.

dup() 调用复制一个打开的文件描述符 oldfd,并返回一个新描述符,二者都指向同一打开的文件句柄。

int dup2(int oldfd, int newfd)
Returns new file descriptor on success, or -1 on error.

dup2()

dup2()系统调用会为 oldfd 参数所指定的文件描述符创建副本,其编号由 newfd 参数指定。如果由 newfd 参数所指定编号的文件描述符之前已经打开,那么 dup2()会首先将其关闭。(dup2()调用会默然忽略 newfd 关闭期间出现的任何错误。故此,编码时更为安全的做法是:在调用dup2()之前,若 newfd 已经打开,则应显式调用 close() 将其关闭。)

如果 oldfd 并非有效的文件描述符,那么 dup2() 调用将失败并返回错误 EBADF,且不关 闭 newfd。如果 oldfd 有效,且与 newfd 值相等,那么 dup2() 将什么也不做,不关闭 newfd, 并将其作为调用结果返回。

fcntl()

newfd = fcntl(oldfd, F_DUPFD, startfd) 为 oldfd 创建一个副本,且将使用大于等于 startfd 的最小未用值作为描述符编号。 该调用还能保证新描述符(newfd)编号落在特定的区间范围内。

文件描述符的正、副本之间共享同一打开文件句柄所含的文件偏移量和状态标志。然而,新文件描述符有其自己的一套文件描述符标志,且其 close-on-exec 标志(FD_CLOEXEC)总是处于关闭状态。

dup3()

int dup3(int oldfd, int newlf, int flags)
Returns (new) file descriptor on success, or -1 on error.

dup3() 系统调用完成的工作与 dup2() 相同,只是新增了一个附加参数 flag,这是一个可以修改系统调用行为的位掩码。目前,dup3()只支持一个标志 O_CLOEXEC,这将促使内核为新文件描述符设置 close-on-exec 标志(FD_CLOEXEC)。

在文件特定偏移量处的 I/O:pread()和 pwrite()