不要使用
flock()
。如果锁文件目录恰好是网络文件系统(例如NFS),并且您正在使用的操作系统没有使用
fcntl()
建议性记录锁来实现
flock()
,则它不会可靠地工作。
(例如,在当前Linux系统中,
flock()
和
fcntl()
锁是分开的,在本地文件上不互动,但在驻留在NFS文件系统上的文件上互动。在服务器集群中,特别是故障转移和Web服务器系统中,将
/var/lock
放在NFS文件系统上并不奇怪,因此我认为这是一个您应该考虑的真正问题。)
编辑以添加:如果由于某些外部原因而限制您使用
flock()
,则可以使用
flock(fd,LOCK_EX|LOCK_NB)
尝试获取独占锁定。此调用永远不会阻塞(等待锁被释放),但如果文件已被锁定,则会失败并显示-1和
errno == EWOULDBLOCK
。与下面详细解释的
fcntl()
锁定方案类似,您尝试获取独占锁定(不阻塞);如果成功,则保持锁定文件描述符打开,并让操作系统在进程退出时自动释放锁定。如果非阻塞锁定失败,则必须选择是否中止或继续执行。
您可以使用POSIX.1函数和
fcntl()
建议性记录锁(覆盖整个文件)来实现您的目标。语义在所有POSIXy系统上都是标准的,因此这种方法适用于所有POSIXy和类Unix系统。
fcntl()
锁的特点很简单,但不直观。当引用锁文件的任何描述符关闭时,该文件上的建议锁将被释放。进程退出时,所有打开文件上的建议锁会自动释放。锁通过
exec*()
维护。锁不通过
fork()
继承,也不在父级中释放(即使标记为close-on-exec)。 (如果描述符是close-on-exec,则它们将在子进程中自动关闭。否则,子进程将有一个对文件的打开描述符,但没有任何
fcntl()
锁定。在子进程中关闭描述符不会影响父进程对文件的锁定。)
因此,正确的策略非常简单:仅打开锁定文件一次,并使用
fcntl(fd,F_SETLK,&lock)
来放置一个排他的全文件咨询锁定而不会阻塞:如果存在冲突锁定,则会立即失败,而不是阻塞直到可以获取锁定。保持描述符打开,并让操作系统在进程退出时自动释放锁定。
例如:
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
static int lockfile(const char *const filepath, int *const fdptr)
{
struct flock lock;
int used = 0;
int fd;
if (fdptr)
*fdptr = -1;
if (filepath == NULL || *filepath == '\0')
return errno = EINVAL;
do {
fd = open(filepath, O_RDWR | O_CREAT, 0600);
} while (fd == -1 && errno == EINTR);
if (fd == -1) {
if (errno == EALREADY)
errno = EIO;
return errno;
}
while (1)
if (fd == STDIN_FILENO) {
used |= 1;
fd = dup(fd);
} else
if (fd == STDOUT_FILENO) {
used |= 2;
fd = dup(fd);
} else
if (fd == STDERR_FILENO) {
used |= 4;
fd = dup(fd);
} else
break;
if (used & 1)
close(STDIN_FILENO);
if (used & 2)
close(STDOUT_FILENO);
if (used & 4)
close(STDERR_FILENO);
if (fd == -1)
return errno = EMFILE;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
if (fcntl(fd, F_SETLK, &lock) == -1) {
close(fd);
return errno = EALREADY;
}
if (fdptr)
*fdptr = fd;
return 0;
}
以上代码中确保不会意外重用标准描述符的原因是,我曾经在一个非常罕见的情况下受到了它的影响。(我想要在持有锁的同时执行用户指定的进程,但将标准输入和输出重定向到当前控制终端。)
使用方法非常简单:
int result;
result = lockfile(YOUR_LOCKFILE_PATH, NULL);
if (result == 0) {
} else
if (result == EALREADY) {
} else {
}
编辑补充:上述函数中我使用了内部链接(static
)只是出于习惯。如果锁文件是特定于用户的,则应使用~/.yourapplication/lockfile
;如果它是系统范围内的,则应使用例如/var/lock/yourapplication/lockfile
。我有一个习惯,即将与此类初始化相关的函数(包括定义/构建锁文件路径等以及自动插件注册函数(使用opendir()
/readdir()
/dlopen()
/dlsym()
/closedir()
)放在同一个文件中;锁文件函数往往是由内部调用的(由构建锁文件路径的函数),因此最终具有内部链接。
请随意使用、重复使用或修改该函数;我认为它属于公共领域,或者在不可能进行公共领域捐赠的情况下根据CC0
许可证授权。
描述符被“故意泄漏”,以便进程退出时操作系统将其关闭(并释放其上的锁),但在此之前不会关闭。
如果您的进程有很多后续清理工作,在此期间您确实希望允许另一个副本该进程,您可以保留描述符,并在希望释放锁的点上只需close(thatfd)
即可。