我的用例如下:我有一个程序强制在任何给定时间只能运行它的一个实例,所以在启动时它总是试图抓住标准位置的锁文件,并在文件终止时终止已被锁定.这一切都运行正常,但现在我想用一个新的命令行选项增强程序,当指定时,将导致程序打印出程序的状态报告然后终止(在主锁定保护之前)上面),其中包括锁文件是否已被锁定,正在运行的进程的pid是什么(如果存在),以及从数据库查询的某些程序状态.
正如您所看到的,当在"状态报告"模式下调用时,如果可用,我的程序实际上不应该获取锁.我只是想知道文件是否已经锁定,所以我可以通知用户状态报告的一部分.
从我的搜索来看,似乎没有办法做到这一点.相反,唯一可能的解决方案似乎是flock()使用非阻塞标志调用,然后,如果您实际获得了锁定,则可以立即释放它.像这样的东西:
if (flock(fileno(lockFile), LOCK_EX|LOCK_NB ) == -1) {
if (errno == EWOULDBLOCK) {
printf("lock file is locked\n");
} else {
// error
} // end if
} else {
flock(fileno(lockFile), LOCK_UN );
printf("lock file is unlocked\n");
} // end if
Run Code Online (Sandbox Code Playgroud)
我认为获取锁定并立即释放它并不是什么大问题,但我想知道是否有更好的解决方案,不涉及短暂和不必要的锁定获取?
注意:已经有几个类似的问题,其标题可能会使它看起来与这个问题完全相同,但从这些问题的内容可以清楚地看出,OP在获取锁定后有兴趣实际写入文件,所以这是一个截然不同的问题:
你不能可靠地做到这一点.进程是异步的:当您无法获取锁定时,无法保证在您打印locked状态时文件仍会被锁定.同样,如果你设法获得锁,那么你立即释放它,所以当你打印unlocked状态时,我的文件被另一个进程锁定了.如果有很多竞争者试图锁定此文件,则状态消息不同步的可能性很高.攻击者可以利用这种近似来穿透系统.
如果你依靠这个检查脚本来执行任何类型的并发工作,那么所有的赌注都会被取消.如果它只是产生信息状态,您应该在状态消息中使用过去时态:
if (flock(fileno(lockFile), LOCK_EX|LOCK_NB) == -1) {
if (errno == EWOULDBLOCK) {
printf("lock file was locked\n");
} else {
// error
}
} else {
flock(fileno(lockFile), LOCK_UN);
printf("lock file was unlocked\n");
}
Run Code Online (Sandbox Code Playgroud)
我看不到在文件上放置锁并立即释放它的方法有什么问题。我认为,您正在按照我的意愿去做。
也就是说,Unix中还有另一个锁定API:fcntl锁。man fcntl在Linux上查看。它必须F_SETLK获取或释放锁,并F_GETLK测试是否可以放置锁。该fcntl锁是略有不同flock的锁:他们是放置在文件的一个区域,而不是整个文件的咨询记录锁定。
还有第三个API也:lockf(3)。您可以F_LOCK用来锁定文件,并F_TEST测试文件区域是否可以锁定。该lockf(3)API已fcntl(2)在Linux 上作为锁的基础上的包装器实现,但在其他操作系统上可能并非如此。
不要使用flock(). 如果锁定文件目录恰好是网络文件系统(例如 NFS)并且您使用的操作系统没有flock()使用fcntl()建议记录锁定来实现,则它无法可靠地工作。
(例如,在目前的Linux系统,flock()以及fcntl()锁是独立的,不会互相影响的本地文件,但做交互上驻留在NFS文件系统中的文件。这并不是说怪有/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>
/* 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;
}
Run Code Online (Sandbox Code Playgroud)
上面确保它不会意外重用标准描述符的原因是因为我在非常罕见的情况下被它咬了。(我想在持有锁的同时执行用户指定的进程,但将标准输入和输出重定向到当前控制终端。)
使用非常简单:
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). */
}
Run Code Online (Sandbox Code Playgroud)
编辑补充:static出于习惯,我对上述函数使用了内部链接()。如果锁定文件是特定于用户的,则应使用~/.yourapplication/lockfile; 如果是系统范围的,则应使用例如/var/lock/yourapplication/lockfile. 我习惯将与此类初始化相关的功能,包括定义/构建锁定文件路径等以及自动插件注册功能(使用opendir()/ readdir()/ dlopen()/ dlsym()/ closedir())保存在同一文件中;lockfile 函数倾向于在内部调用(由构建锁文件路径的函数),因此最终具有内部链接。
随意使用、重用或修改该功能;我认为它属于公共领域,或者在CC0不可能进行公共领域奉献的情况下获得许可。
描述符是有意“泄露”的,以便在进程退出时操作系统将关闭(并释放对它的锁),但不会在此之前关闭。
如果您的进程进行了大量的后期工作清理,在此期间您确实希望允许此进程的另一个副本,您可以保留描述符,并且就close(thatfd)在您希望释放锁的地方。