O_EXCL 的作用
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
标志能够成功创建指定的文件,说明它是该程序的唯一实例,可以继续执行;如果返回错误,说明该文件已经存在,进而说明系统中已经运行着一个该程序的其它实例,检测到错误的返回值后,该实例就可以退出了。
之所以能这么用的唯一理由是该操作是原子的。
之所以这么说,理由如下。假设同样语义的非原子的操作流程如下:
if( access(file, R_OK) == -1 ) /* 首先检查文件是否存在 */
open(file, O_RDWR | O_CREAT,0666); /* 如果不存在,那我创建一个这样的文件 */
... /* 继续执行任务 */
由于判断文件是否存在与创建文件是两个步骤,就会存在临界竞争的问题。试想下面的场景:
- 某程序的进程 A 判断文件不存在,因此 A 认为自己是此时系统中该程序唯一的实例,准备继续执行创建指定的文件。
- 操作系统的调度策略恰好在此时起作用,进程 A 暂停执行。此时指定的文件还没有创建。
- 该程序的另一个进程 B 开始执行,同样,由于指定的文件不存在,B 也认为自己是此时系统中该程序的唯一实例,准备继续执行创建指定的文件。
- 这样,进程 A 和进程 B 都能成功调用 open 并继续往下执行。
- 此时系统中就同时运行着该程序的两个实例,与仅运行一个进程的期望不符。
所以说,O_EXCL 与 O_CREATE 联合使用的前提就是该操作是原子的。
3. 非原子操作如何达到同样目的
假设现在不能使用 O_EXCL|O_CREATE
,或者假设用 O_EXCL|O_CREATE
调用 open 并不是原子的,该如何达到上面关于“一个程序只能运行一个实例”的要求呢?
可以用系统调用 link 来实现。
link的原型如下:
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
link 的作用是为 oldpath 指定的源文件创建一个 newpath 指定的链接文件(硬链接,hard link)。如果创建成功,则返回 0,如果 newpath 路径指定的目标文件在调用 link 前已经存在,则 link 会错误返回。
根据 link 的特点,可以达到上面的要求:
- 首先确保文件系统中已经有一个源文件 file1。
- 某程序的一个进程 A 开始执行,调用 link,试图创建一个 file1 的链接文件 file2。
- 如果 A 调用 link 成功,说明该程序此时只有进程A锁定了该文件,进程 A 可以继续往下执行。
- 由于进程 A 为 file1 创建了一个链接文件 file2,此时 file1 的链接数是 2(用 stat 可获取链接数)。
- 进程 B 调用 link,同样试图创建 file1 的链接文件 file2,但由于 file2 已经存在,link 错误返回。可进一步调用 stat 系统调用,查看 file1 的链接数,确定该链接数是 2。
- 进程 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.