Блокировка файла linux fcntl с тайм-аутом

стандартный вызов Linux fcntl не предоставляет опцию тайм-аута. Я рассматриваю возможность реализации блокировки тайм-аута с сигналом.

Вот описание блокировки блокировки:


F_SETLKW

Эта команда должна быть эквивалентна F_SETLK, за исключением того, что если общая или монопольная блокировка блокируется другими блокировками, поток должен ожидать, пока запрос не будет удовлетворен. Если сигнал, который должен быть перехвачен, получен, пока функция fcntl() ожидает область, функция fcntl() должна быть прервана. По возвращении из обработчика сигнала функция fcntl() должна вернуть -1 с errno, установленным в [EINTR], и операция блокировки не должна выполняться.


Итак, какой сигнал мне нужно использовать, чтобы указать, что блокировка должна быть прервана? И поскольку в моем процессе запущено несколько потоков, я хочу прервать только этот поток ввода-вывода, который блокируется для блокировки файла, другие потоки не должны быть затронуты, но сигнал находится на уровне процесса, я не уверен, как справиться эта ситуация.

Добавлен:

Я написал простую реализацию с использованием сигнала.

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.

John Bollinger 16.07.2019 13:53
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
1
2 105
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Лучшим решением может быть использование Выбрать():

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() ждать блокировки записи?

mosvy 16.07.2019 01:53

Даже если бы select() позволял ждать блокировки записи (а я не думаю, что это так), это не достигло бы цели OP блокировка файла. И это не просто вопрос вызова fcntl() после того, как все конфликтующие блокировки очищены, потому что этому присуще состояние гонки - какой-то другой процесс может снять конфликтующую блокировку между select() и fcntl(), а затем именно то, что пытается OP избежать происходит в любом случае.

John Bollinger 16.07.2019 18:26
Ответ принят как подходящий

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

Наиболее очевидным выбором сигнала будет SIGUSR1 или SIGUSR2. Они предоставляются для определенных пользователем целей.

Также есть 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()), а затем сделать так, чтобы поток пытался разблокировать его непосредственно перед этим. Когда выбранный сигнал доставляется процессу, поток, который в данный момент не блокирует его, получит его, если такой поток доступен.

Пример реализации

Это предполагает, что обработчик сигнала уже настроен для обработки выбранного сигнала (он не должен ничего делать), и что номер сигнала для использования доступен через символ 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_*.
  • Вышеупомянутое работает вокруг того факта, что struct sigevent Glibc не соответствует его собственной документации (по крайней мере, в относительно старой версии 2.17). В документах утверждается, что у struct sigevent есть член sigev_notify_thread_id, но на самом деле его нет. Вместо этого у него есть недокументированное объединение, содержащее соответствующий член, и он предоставляет макрос для устранения разницы, но этот макрос не работает как указатель члена в назначенном инициализаторе.
  • fcntl блокировки работают для каждого процесса. Таким образом, разные потоки одного и того же процесса не могут исключать друг друга с помощью такой блокировки. Более того, разные потоки одного и того же процесса могут изменять fcntl() блокировки, полученные через другие потоки, без каких-либо особых усилий или уведомления любого из потоков.
  • Вы можете рассмотреть возможность создания и поддержки статического таймера для каждого потока для этой цели вместо создания и последующего уничтожения нового при каждом вызове.
  • Имейте в виду, что fcntl() вернет EINTR, если будет прерван сигналом Любые, который не завершает поток. Поэтому вы можете захотеть использовать обработчик сигнала, который устанавливает утвердительный флаг для каждого потока, с помощью которого вы можете убедиться, что фактический сигнал таймера был получен, чтобы повторить попытку блокировки, если она была прервана другим сигналом.
  • Вам решать, убедиться, что поток не получит выбранный сигнал по какой-либо другой причине, или подтвердить каким-либо другим способом, что время действительно истекло в случае, если блокировка не удалась с помощью EINTR.

спасибо за ответ, я написал простую реализацию, основанную на вашей идее, но она работает не так, как я ожидал. Не могли бы вы взглянуть на мой добавленный код и посмотреть, что происходит не так? Спасибо!

Ziqi Liu 16.07.2019 02:45

@ZiqiLiu, мне не очевидно, что не так с вашей попыткой, и я не могу исключить, что проблема в функции file_rwlock, которую вы не представили. Но пример реализации, который я добавил к этому ответу, работает так, как я ожидаю, в тесте, построенном по тем же принципам, что и ваш.

John Bollinger 16.07.2019 18:20

Другие вопросы по теме