我的用例如下:我有一个程序强制在任何给定时间只能运行它的一个实例,因此在启动时它总是试图抓住标准位置的锁文件,并终止如果文件已被锁定。这一切都运行良好,但现在我想用一个新的命令行选项来增强程序,当指定时,将导致程序只打印出程序的状态报告然后终止(在上面描述的主锁定保护之前),其将包括锁定文件是否已被锁定,正在运行的进程的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
我认为获得锁定然后立即释放它并不是一件大事,但我想知道是否有更好的解决方案,并没有涉及简短的不必要的锁定获取?
注意:已经有几个类似的问题,其标题可能会使它看起来与这个问题相同,但从这些问题的内容中可以清楚地看出,OP有兴趣实际写入获取锁定后的文件,所以这是一个独特的问题:
答案 0 :(得分:9)
你不能可靠。进程是异步的:当您无法获取锁定时,无法保证在您打印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");
}
答案 1 :(得分:5)
我没有看到锁定文件并立即释放它的方法有什么问题。在我看来,你就像我一样。
也就是说,Unix中还有另一个锁定API:fcntl
锁定。请参阅Linux上的man fcntl
。它有F_SETLK
获取或释放锁,F_GETLK
来测试是否可以放置锁。 fcntl
锁与flock
锁稍有不同:它们是放置在文件区域上的咨询记录锁,而不是整个文件。
还有第三个api:lockf(3)
。您可以使用F_LOCK
来锁定文件,并使用F_TEST
来测试是否可以锁定文件区域。 lockf(3)
API已作为Linux上fcntl(2)
锁定之上的包装器实现,但在其他操作系统上可能不是这样。
答案 2 :(得分:4)
请勿使用flock()
。如果锁定文件目录恰好是网络文件系统(例如,NFS)并且您使用的操作系统未使用flock()
建议记录锁定实现fcntl()
,则它无法可靠地工作。 / p>
(例如,在当前的Linux系统中,flock()
和fcntl()
锁是独立的,不在本地文件上交互,但在驻留在NFS文件系统上的文件上进行交互。这并不奇怪在服务器群集中的NFS文件系统上有/var/lock
,特别是故障转移和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
。我习惯保持与这种初始化相关的函数,包括定义/构建lockfile路径等以及自动插件注册功能(使用opendir()
/ readdir()
/ dlopen()
/ dlsym()
/ closedir()
),在同一个文件中;锁文件函数往往在内部调用(通过构建锁文件路径的函数),因此最终会有内部链接。
随意使用,重复使用或修改功能;我认为它属于公共领域,或者在CC0
下获得许可,但公共领域的奉献是不可能的。
描述符是&#34;泄露&#34;故意,以便在进程退出时,操作系统将关闭(并释放锁定),但不是之前。
如果您的流程有很多后期工作清理,在此期间您确实希望允许此流程的另一个副本,则可以保留描述符,并在您希望的位置close(thatfd)
释放锁。