带有超时的linux fcntl文件锁定

时间:2019-07-15 21:38:47

标签: c linux multithreading file-locking

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

这是阻止锁的描述:


F_SETLKW

该命令应等效于F_SETLK,不同之处在于,如果共享锁或排他锁被其他锁阻塞,则线程应等待,直到请求可以满足为止。 如果在fcntl()等待某个区域时接收到要捕获的信号,则fcntl()将被中断。从信号处理程序返回后,fcntl()应以errno返回-1。设置为[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秒后中断,但是第二个进程刚刚终止。

>

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

2 个答案:

答案 0 :(得分: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锁定失败的情况下时间实际上已到期。

答案 1 :(得分:0)

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