linux fcntl 文件锁定超时

linux fcntl file lock with timeout

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

阻塞锁的说明如下:


F_SETLKW

此命令等同于F_SETLK,除了如果共享锁或独占锁被其他锁阻塞,线程将等待直到请求得到满足。 如果在 fcntl() 等待区域时接收到要捕获的信号,则 fcntl() 将被中断。 来自信号处理程序的 return fcntl( ) 应 return -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;
}

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

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

更好的解决方案可能是使用 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;
}

So what kind of signal I need to use to indicate the lock to be interrupted?

最明显的信号选择是 SIGUSR1SIGUSR2。提供这些服务是为了 user-defined 目的。

还有 SIGALRM,如果您使用产生此类信号的计时器来进行计时,这将是很自然的,并且只要您是不用于其他目的。

And since there're multiple threads running in my process, I only want to interrupt this IO thread who is blokcing for the file lock, other threads should not be affected, but signal is process-level, I'm not sure how to handle this situation.

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

使用常规 kill(),您还可以选择让所有线程阻塞所选信号 (sigprocmask()),然后让进行锁定的线程立即尝试解除阻塞。 When the chosen signal is delivered to the process, a thread that is not presently blocking it will receive it, if any such thread is available.

示例实现

这假设已经设置了一个信号处理程序来处理所选信号(它不需要做任何事情),并且可以通过符号 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>

#if (__GLIBC__ < 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ < 30)
// glibc prior to 2.30 does not provide a wrapper 
// function for this syscall:    
static pid_t gettid(void) {
    return syscall(SYS_gettid);
}
#endif

/**
 * 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;
}

这符合我的预期。

备注:

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

我遇到了一些困难。终于搞定了。

// main1.cpp
#include <thread>
#include <chrono>
#include <iostream>

int main(int argc, char *argv[]) {
    int fd = open(argv[1],O_RDWR|O_CREAT,S_IRWXU | S_IRWXG | S_IRWXO);

    struct flock fd_lock;
    fd_lock.l_type = F_WRLCK;    /* read/write (exclusive) fd_lock_lock */
    fd_lock.l_whence = SEEK_SET; /* base for seek offsets */
    fd_lock.l_start = 0;         /* 1st byte in file */
    fd_lock.l_len = 0;           /* 0 here means 'until EOF' */
    fd_lock.l_pid = getpid();

    std::cout << "locked file\n";
    fcntl(fd, F_SETLKW, &fd_lock);

    std::cout << "file locked\n";
    std::this_thread::sleep_for(std::chrono::seconds(100));
}
// main2.cpp
#include <cstring>
#include <chrono>
#include <thread>
#include <iostream>

struct signal_trigger_thread_args { 
    int signum;
    pthread_t tid;
    unsigned int seconds;
};

void alarm_handler(int signum, siginfo_t *x, void *y) {
    // std::cout << "Alarm Handler!\n";
}

void *trigger_signal_after_time(void *arg) {
    struct signal_trigger_thread_args *_arg = (struct signal_trigger_thread_args*)arg; 

    std::this_thread::sleep_for(std::chrono::seconds(_arg->seconds));
    std::cout << "triggering signal!\n";
    pthread_kill(_arg->tid,_arg->signum);
    return NULL;
}

int fcntl_wait_for(int fd, int cmd, struct flock *_flock, int signum, unsigned int _seconds) {
    // Create a thread to trigger the signal.
    pthread_t signal_trigger_thread;

    struct signal_trigger_thread_args args;
    args.signum = signum;
    args.tid = pthread_self();
    args.seconds = _seconds;

    int return_value = pthread_create(&signal_trigger_thread, NULL, &trigger_signal_after_time,(void *)&args);

    if ( return_value ) {
        std::cout << "pthread creation failed\n";
        return -2;
    }

    return_value = fcntl(fd, cmd, _flock);

    if ( return_value == 0 ) { return 0; }

    if ( return_value = -1 && errno == EINTR ) {
        return 1;
    }
    return -1;
}

int main(int argc, char *argv[]) {
    // initialize_signal_handlers();
    static struct sigaction _sigact;

    memset(&_sigact,0,sizeof(_sigact));
    _sigact.sa_sigaction = alarm_handler;
    _sigact.sa_flags = SA_SIGINFO;

    sigaction(SIGUSR1,&_sigact,NULL);


    int fd = open(argv[1],O_RDWR|O_CREAT,S_IRWXU | S_IRWXG | S_IRWXO);

    struct flock fd_lock;
    fd_lock.l_type = F_WRLCK;    /* read/write (exclusive) fd_lock_lock */
    fd_lock.l_whence = SEEK_SET; /* base for seek offsets */
    fd_lock.l_start = 0;         /* 1st byte in file */
    fd_lock.l_len = 0;           /* 0 here means 'until EOF' */
    fd_lock.l_pid = getpid();

    std::cout << "waiting for file to be freed for 5 seconds\n";
    int return_value = fcntl_wait_for(fd, F_SETLKW, &fd_lock, SIGUSR1, 5);

    if ( return_value == 1 ) {
        std::cout << "fcntl was interrupted!\n";
    } else if ( return_value == 0 ) {
        std::cout << "fcntl obtained lock!\n";
    } else {
        std::cout << "fcntl failed!\n";
    }
}