1. 原始语义

与 O_CREATE 标志组合起来调用 open,确保指定的文件由 open 的调用者创建,否则返回错误。即,如果进程 A 用 O_CREATE 和 O_EXCL 标志来调用 open,期望创建一个指定的文件 file1,如果 file1 不存在,则 open 成功返回且创建 file1,如果 file1 已经存在了(即不是由进程 A 创建的),那么 open 返回错误。

2. 使用场景

O_CREATE|O_EXCL 多用于确保一个一个程序只能执行单个进程,不能执行多个进程。原理如下,假设进程 A 是某程序的一个实例,如果它用 O_CREATE|O_EXCL 标志能够成功创建指定的文件,说明它是该程序的唯一实例,可以继续执行;如果返回错误,说明该文件已经存在,进而说明系统中已经运行着一个该程序的其它实例,检测到错误的返回值后,该实例就可以退出了。

之所以能这么用的唯一理由是该操作是原子的

之所以这么说,理由如下。假设同样语义的非原子的操作流程如下:

1
2
3
if( access(file, R_OK) == -1 )   /* 首先检查文件是否存在 */  
    open(file, O_RDWR | O_CREAT0666);  /* 如果不存在,那我创建一个这样的文件 */  
...  /* 继续执行任务 */  

由于判断文件是否存在与创建文件是两个步骤,就会存在临界竞争的问题。试想下面的场景:

  1. 某程序的进程 A 判断文件不存在,因此 A 认为自己是此时系统中该程序唯一的实例,准备继续执行创建指定的文件。
  2. 操作系统的调度策略恰好在此时起作用,进程 A 暂停执行。此时指定的文件还没有创建。
  3. 该程序的另一个进程 B 开始执行,同样,由于指定的文件不存在,B 也认为自己是此时系统中该程序的唯一实例,准备继续执行创建指定的文件。
  4. 这样,进程 A 和进程 B 都能成功调用 open 并继续往下执行。
  5. 此时系统中就同时运行着该程序的两个实例,与仅运行一个进程的期望不符。

所以说,O_EXCL 与 O_CREATE 联合使用的前提就是该操作是原子的。

3. 非原子操作如何达到同样目的

假设现在不能使用 O_EXCL|O_CREATE,或者假设用 O_EXCL|O_CREATE 调用 open 并不是原子的,该如何达到上面关于“一个程序只能运行一个实例”的要求呢?

可以用系统调用 link 来实现。

link的原型如下:

1
2
3
 #include <unistd.h>  
  
 int link(const char *oldpath, const char *newpath);  

link 的作用是为 oldpath 指定的源文件创建一个 newpath 指定的链接文件(硬链接,hard link)。如果创建成功,则返回 0,如果 newpath 路径指定的目标文件在调用 link 前已经存在,则 link 会错误返回。

根据 link 的特点,可以达到上面的要求:

  1. 首先确保文件系统中已经有一个源文件 file1。
  2. 某程序的一个进程 A 开始执行,调用 link,试图创建一个 file1 的链接文件 file2。
  3. 如果 A 调用 link 成功,说明该程序此时只有进程A锁定了该文件,进程 A 可以继续往下执行。
  4. 由于进程 A 为 file1 创建了一个链接文件 file2,此时 file1 的链接数是 2(用 stat 可获取链接数)。
  5. 进程 B 调用 link,同样试图创建 file1 的链接文件 file2,但由于 file2 已经存在,link 错误返回。可进一步调用 stat 系统调用,查看 file1 的链接数,确定该链接数是 2。
  6. 进程 B 退出。

4. O_EXCL|O_CREATE 确实有可能是非原子的

在 NFS 上,O_EXCL|O_CREATE 确实有可能是非原子的:

On NFS, O_EXCL is supported only when using NFSv3 or later on kernel 2.6 or later. In NFS environments where O_EXCL support is not provided, programs that rely on it for performing locking tasks will contain a race condition.

在这种情况下,上面讨论的使用 link 的方法就有了用武之地。如上引文所述,在最新的内核中, NFS 中并不存在该问题,用 O_EXCL|O_CREATE 仍然能满足要求。

5. 其它

O_EXCL 一般只能和 O_CREATE 一起使用,不能单独使用,但有一个例外:在 2.6 及以后的内核中,如果 open 指定的文件是一个块设备文件,O_EXCL 可以单独使用,此时,如果该块设备正在被使用(例如已经被挂载),那么 open 将失败返回,错误码是 EBUSY;除此之外,单独使用 O_EXCL 的后果无法预知:

In general, the behavior of O_EXCL is undefined if it is used without O_CREAT. There is one exception: on Linux 2.6 and later, O_EXCL can be used without O_CREAT if pathname refers to a block device. If the block device is in use by the system (e.g., mounted), open() fails with the error EBUSY.