不要使用flock()。如果锁定文件目录恰好是网络文件系统(例如 NFS)并且您使用的操作系统未使用 fcntl() 咨询记录锁定实现 flock(),则它无法可靠地工作。
(例如,在当前的 Linux 系统中,flock() 和 fcntl() 锁是分开的,不会对本地文件进行交互,但会对驻留在 NFS 文件系统上的文件进行交互。拥有/var/lock 并不奇怪在服务器集群中的 NFS 文件系统上,尤其是故障转移和 Web 服务器系统,所以在我看来,这是一个你应该考虑的真正问题。)
编辑添加:如果由于某些外部原因您被限制使用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>
/* Open and exclusive-lock file, creating it (-rw-------)
* if necessary. If fdptr is not NULL, the descriptor is
* saved there. The descriptor is never one of the standard
* descriptors STDIN_FILENO, STDOUT_FILENO, or STDERR_FILENO.
* If successful, the function returns 0.
* Otherwise, the function returns nonzero errno:
* EINVAL: Invalid lock file path
* EMFILE: Too many open files
* EALREADY: Already locked
* or one of the open(2)/creat(2) errors.
*/
static int lockfile(const char *const filepath, int *const fdptr)
{
struct flock lock;
int used = 0; /* Bits 0 to 2: stdin, stdout, stderr */
int fd;
/* In case the caller is interested in the descriptor,
* initialize it to -1 (invalid). */
if (fdptr)
*fdptr = -1;
/* Invalid path? */
if (filepath == NULL || *filepath == '\0')
return errno = EINVAL;
/* Open the file. */
do {
fd = open(filepath, O_RDWR | O_CREAT, 0600);
} while (fd == -1 && errno == EINTR);
if (fd == -1) {
if (errno == EALREADY)
errno = EIO;
return errno;
}
/* Move fd away from the standard descriptors. */
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;
/* Close the standard descriptors we temporarily used. */
if (used & 1)
close(STDIN_FILENO);
if (used & 2)
close(STDOUT_FILENO);
if (used & 4)
close(STDERR_FILENO);
/* Did we run out of descriptors? */
if (fd == -1)
return errno = EMFILE;
/* Exclusive lock, cover the entire file (regardless of size). */
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) {
/* Lock failed. Close file and report locking failure. */
close(fd);
return errno = EALREADY;
}
/* Save descriptor, if the caller wants it. */
if (fdptr)
*fdptr = fd;
return 0;
}
上面确保它不会意外重用标准描述符的原因是因为我在非常罕见的情况下被它咬过。 (我想在持有锁的同时执行用户指定的进程,但将标准输入和输出重定向到当前控制终端。)
使用很简单:
int result;
result = lockfile(YOUR_LOCKFILE_PATH, NULL);
if (result == 0) {
/* Have an exclusive lock on YOUR_LOCKFILE_PATH */
} else
if (result == EALREADY) {
/* YOUR_LOCKFILE_PATH is already locked by another process */
} else {
/* Cannot lock YOUR_LOCKFILE_PATH, see strerror(result). */
}
编辑添加:出于习惯,我对上述功能使用了内部链接 (static)。如果锁定文件是用户特定的,它应该使用~/.yourapplication/lockfile;如果它是系统范围的,它应该使用例如/var/lock/yourapplication/lockfile。我有保留与这种初始化相关的功能的习惯,包括定义/构建锁文件路径等以及自动插件注册功能(使用opendir()/readdir()/dlopen()/dlsym()/@ 987654350@),在同一个文件中; lockfile 函数倾向于在内部调用(由构建 lockfile 路径的函数),因此最终具有内部链接。
您可以随意使用、重用或修改该功能;我认为它属于公共领域,或在无法用于公共领域的情况下以CC0 许可。
描述符是故意“泄露”的,以便在进程退出时(而不是之前)由操作系统关闭(并释放其上的锁)。
如果您的进程进行了大量的工作后清理,在此期间您确实希望允许此进程的另一个副本,您可以保留描述符,并在您希望释放锁。