linux fcntl文件锁定超时

Posted

技术标签:

【中文标题】linux fcntl文件锁定超时【英文标题】:linux fcntl file lock with timeout 【发布时间】:2019-11-24 14:45:09 【问题描述】:

标准的 linux fcntl 调用不提供超时选项。我正在考虑使用信号实现超时锁定。

这里是阻塞锁的描述:


F_SETLKW

这个命令应该等同于F_SETLK,除了如果共享或排他锁被其他锁阻塞,线程将等待直到请求被满足。 如果在 fcntl() 等待区域时接收到要捕获的信号,则 fcntl() 应被中断。 从信号处理程序返回时,fcntl() 应返回 -1 和 errno设置为[EINTR],不做锁操作。


那么我需要使用什么样的信号来指示要中断的锁呢?而且由于我的进程中有多个线程在运行,我只想中断这个阻塞文件锁的IO线程,其他线程不应该受到影响,但是信号是进程级别的,我不知道如何处理这种情况。

补充:

我用信号写了一个简单的实现。

int main(int argc, char **argv) 
  std::string lock_path = "a.lck";

  int fd = open(lock_path.c_str(), O_CREAT | O_RDWR, S_IRWXU | S_IRWXG | S_IRWXO);

  if (argc > 1) 
    signal(SIGALRM, [](int sig) );
    std::thread([](pthread_t tid, unsigned int seconds) 
      sleep(seconds);
      pthread_kill(tid, SIGALRM);
    , pthread_self(), 3).detach();
    int ret = file_rwlock(fd, F_SETLKW, F_WRLCK);

    if (ret == -1) std::cout << "FAIL to acquire lock after waiting 3s!" << std::endl;

   else 
    file_rwlock(fd, F_SETLKW, F_WRLCK);
    while (1);
  

  return 0;

通过运行./main,然后运行./main a,我希望第一个进程永远持有锁,第二个进程尝试获取锁并在 3 秒后中断,但第二个进程刚刚终止。

谁能告诉我我的代码有什么问题?

【问题讨论】:

您添加的代码是 C++,而不是 C。如果您实际上是在寻找有关 C++ 的答案,请适当地标记问题 - C++ 的答案可能与 C 的答案不同。跨度> 【参考方案1】:

那么我需要使用什么样的信号来指示锁定是 被打断了?

最明显的信号选择是SIGUSR1SIGUSR2。提供这些是为了满足用户定义的目的。

还有SIGALRM,如果您使用产生此类信号的计时器来进行计时,这将是很自然的,并且即使以编程方式生成也有意义,只要您不使用它其他用途。

而且由于我的服务器中有多个线程在运行 进程,我只想中断这个阻塞的 IO 线程 文件锁,其他线程不应该受到影响,但信号是 进程级别的,我不知道如何处理这种情况。

您可以通过pthread_kill() 函数向多线程进程中的选定线程传递信号。这也适用于多个线程同时等待锁的情况。

使用常规的kill(),您还可以选择让所有线程阻塞所选信号(sigprocmask()),然后让线程在尝试之前立即解除阻塞。当所选信号被传递到过程时,如果有任何此类线程可用,则不会显示它的线程将不会接收它。

示例实现

这假设已经设置了一个信号处理程序来处理选定的信号(它不需要做任何事情),并且要使用的信号编号可通过符号LOCK_TIMER_SIGNAL 获得。它提供了所需的超时行为作为围绕fcntl() 的包装函数,并使用问题中描述的命令F_SETLKW

#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE

#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/syscall.h>

// glibc does not provide a wrapper function for this syscall:    
static pid_t gettid(void) 
    return syscall(SYS_gettid);


/**
 * Attempt to acquire an fcntl() lock, with timeout
 *
 * fd: an open file descriptor identifying the file to lock
 * lock_info: a pointer to a struct flock describing the wanted lock operation
 * to_secs: a time_t representing the amount of time to wait before timing out
 */    
int try_lock(int fd, struct flock *lock_info, time_t to_secs) 
    int result;
    timer_t timer;

    result = timer_create(CLOCK_MONOTONIC,
            & (struct sigevent) 
                .sigev_notify = SIGEV_THREAD_ID,
                ._sigev_un =  ._tid = gettid() ,
                // note: gettid() conceivably can fail
                .sigev_signo = LOCK_TIMER_SIGNAL ,
            &timer);
    // detect and handle errors ...

    result = timer_settime(timer, 0,
            & (struct itimerspec)  .it_value =  .tv_sec = to_secs  ,
            NULL);

    result = fcntl(fd, F_SETLKW, lock_info);
    // detect and handle errors (other than EINTR) ...
    // on EINTR, may want to check that the timer in fact expired

    result = timer_delete(timer);
    // detect and handle errors ...

    return result;

这符合我的预期。

注意事项:

信号处置是进程范围的属性,而不是每个线程的属性,因此您需要在整个程序中协调您对信号的使用。在这种情况下,try_lock 函数本身修改其所选信号的处置是没有用的(而且可能很危险)。 timer_* 接口提供 POSIX 间隔计时器,但指定特定线程从此类计时器接收信号的规定是 Linux 特定的。 在 Linux 上,您需要与 -lrt 链接以使用 timer_* 函数。 以上内容解决了 Glibc 的 struct sigevent 不符合其自己的文档(至少在相对较旧的 2.17 版本中)这一事实。文档声称struct sigevent 有一个成员sigev_notify_thread_id,但实际上它没有。相反,它有一个包含相应成员的未记录联合,并且它提供了一个宏来修补差异 - 但该宏不能作为指定初始化程序中的成员指示符。 fcntl 锁在每个进程的基础上运行。因此,同一进程的不同线程不能通过这种锁相互排斥。此外,同一进程的不同线程可以修改通过其他线程获得的fcntl() 锁,而无需任何特别努力或通知任一线程。 您可以考虑为此目的创建和维护每个线程的静态计时器,而不是在每次调用时创建然后销毁一个新计时器。 注意fcntl() 如果被不终止线程的任何 信号中断,将返回EINTR。因此,您可能希望使用一个信号处理程序来设置一个肯定的每线程标志,您可以通过该标志来验证是否收到了实际的计时器信号,以便在它被不同的信号中断时重试锁定。 由您来确保线程不会因其他原因接收到所选信号,或者通过其他方式确认在锁定失败并显示 EINTR 的情况下时间实际上已过期。

【讨论】:

感谢您的回答,我根据您的想法编写了一个简单的实现,但它并没有按我预期的方式工作。你能看看我添加的代码,看看有什么问题吗?谢谢! @ZiqiLiu,你的尝试有什么问题对我来说并不明显,我不能排除问题出在函数file_rwlock 上,而你没有出现。但是我添加到这个答案中的示例实现正如我所期望的那样在一个与你的相同行构建的测试中工作。【参考方案2】:

更好的解决方案可能是使用select():

https://www.gnu.org/software/libc/manual/html_node/Waiting-for-I_002fO.html

#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>

int
input_timeout (int filedes, unsigned int seconds)

  fd_set set;
  struct timeval timeout;

  /* Initialize the file descriptor set. */
  FD_ZERO (&set);
  FD_SET (filedes, &set);

  /* Initialize the timeout data structure. */
  timeout.tv_sec = seconds;
  timeout.tv_usec = 0;

  /* select returns 0 if timeout, 1 if input available, -1 if error. */
  return TEMP_FAILURE_RETRY (select (FD_SETSIZE,
                                     &set, NULL, NULL,
                                     &timeout));


int
main (void)

  fprintf (stderr, "select returned %d.\n",
           input_timeout (STDIN_FILENO, 5));
  return 0;

【讨论】:

select() 允许等待记录锁吗? 即使select() 确实允许等待记录锁(我认为不会),这也无法实现 OP 的锁定文件的目标.这不仅仅是在清除所有冲突锁之后调用fcntl() 的问题,因为其中存在固有的竞争条件——其他一些进程可能会在select()fcntl() 之间取出一个冲突锁,然后完全正确OP 试图避免的事情无论如何都会发生。

以上是关于linux fcntl文件锁定超时的主要内容,如果未能解决你的问题,请参考以下文章

Linux文件锁学习-flock, lockf, fcntl

Linux 文件锁 - fcntl

Linux 下三种文件锁 —— fcntl/lockf、flock

我可以让 fcntl 和 Perl 警报合作吗?

使用fcntl在Python中锁定文件

记录锁