Типичное решение — создать новый файл в том же каталоге, а затем переименовать (жестко связать) его поверх старого.
Таким образом, процессы видят либо старый, либо новый, а не микс; и это зависит только от момента, когда они открывают файл.
Ядро Linux позаботится о кэшировании, поэтому, если к файлу часто обращаются, он будет находиться в ОЗУ (кэш страницы). Однако автор должен не забыть удалить файл при выходе.
Лучшим подходом является использование рекомендаций на основе fcntl(). блокировки записи (обычно по всему файлу, т. е. .l_whence = SEEK_SET
, .l_start = 0
, .l_len = 0
).
Модуль записи захватит блокировку записи/монопольного доступа перед усечением и перезаписью содержимого, а считыватель получит блокировку чтения/разделяемого доступа перед чтением содержимого.
Это требует сотрудничества, однако, и писатель должен быть готов к тому, что не сможет заблокировать (или получение блокировки может занять неопределенное количество времени).
Схема только для Linux будет заключаться в использовании атомарной замены (путем переименования/жесткой ссылки) и аренды файлов.
(Когда процесс записи имеет исключительную аренду открытого файла, он получает сигнал всякий раз, когда другой процесс хочет открыть тот же файл (инод, а не имя файла). У него есть по крайней мере несколько секунд, чтобы понизить или освободить аренду, в в этот момент открыватель получает доступ к содержимому.)
По сути, процесс записи создает пустой файл состояния и получает на него исключительную аренду. Всякий раз, когда модуль записи получает сигнал о том, что читатель хочет получить доступ к файлу статуса, он записывает текущий статус в файл, освобождает аренду, создает новый пустой файл в том же каталоге (достаточно того же монтирования), что и файл статуса, получает эксклюзивная аренда на этот и переименовывает/связывает его с файлом состояния.
Если содержимое файла состояния не меняется постоянно, а только периодически, то процесс записи создает пустой файл состояния и получает на него исключительную аренду. Всякий раз, когда модуль записи получает сигнал о том, что читатель хочет получить доступ к (пустому) файлу состояния, он записывает текущий статус в файл и освобождает аренду. Затем, когда состояние записывающего процесса обновляется, а аренды еще нет, он создает новый пустой файл в каталоге файла состояния, берет на него монопольную аренду и переименовывает/жестко связывает файл состояния.
Таким образом, файл состояния всегда обновляется непосредственно перед тем, как читатель откроет его, и только после этого. Если есть несколько читателей одновременно, они могут открыть файл состояния без перерыва, когда писатель освобождает аренду.
Важно отметить, что информация о состоянии должна собираться в единой структуре или аналогичной, чтобы ее запись в файл состояния была эффективной. Аренда автоматически разрывается, если не освобождается достаточно быстро (но есть хотя бы несколько секунд, чтобы отреагировать), и аренда находится в индексном узле — содержимом файла, а не в имени файла, поэтому нам все еще нужна атомарная замена.
Вот грубый пример реализации:
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdarg.h>
#include <inttypes.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <limits.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#define LEASE_SIGNAL (SIGRTMIN+0)
static pthread_mutex_t status_lock = PTHREAD_MUTEX_INITIALIZER;
static int status_changed = 0;
static size_t status_len = 0;
static char *status = NULL;
static pthread_t status_thread;
static char *status_newpath = NULL;
static char *status_path = NULL;
static int status_fd = -1;
static int status_errno = 0;
char *join2(const char *src1, const char *src2)
{
const size_t len1 = (src1) ? strlen(src1) : 0;
const size_t len2 = (src2) ? strlen(src2) : 0;
char *dst;
dst = malloc(len1 + len2 + 1);
if (!dst) {
errno = ENOMEM;
return NULL;
}
if (len1 > 0)
memcpy(dst, src1, len1);
if (len2 > 0)
memcpy(dst+len1, src2, len2);
dst[len1+len2] = '\0';
return dst;
}
static void *status_worker(void *payload __attribute__((unused)))
{
siginfo_t info;
sigset_t mask;
int err, num;
/* This thread blocks all signals except LEASE_SIGNAL. */
sigfillset(&mask);
sigdelset(&mask, LEASE_SIGNAL);
err = pthread_sigmask(SIG_BLOCK, &mask, NULL);
if (err)
return (void *)(intptr_t)err;
/* Mask for LEASE_SIGNAL. */
sigemptyset(&mask);
sigaddset(&mask, LEASE_SIGNAL);
/* This thread can be canceled at any cancellation point. */
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
while (1) {
num = sigwaitinfo(&mask, &info);
if (num == -1 && errno != EINTR)
return (void *)(intptr_t)errno;
/* Ignore all but the lease signals related to the status file. */
if (num != LEASE_SIGNAL || info.si_signo != LEASE_SIGNAL || info.si_fd != status_fd)
continue;
/* We can be canceled at this point safely. */
pthread_testcancel();
/* Block cancelability for a sec, so that we maintain the mutex correctly. */
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
pthread_mutex_lock(&status_lock);
status_changed = 0;
/* Write the new status to the file. */
if (status && status_len > 0) {
const char *ptr = status;
const char *const end = status + status_len;
ssize_t n;
while (ptr < end) {
n = write(status_fd, ptr, (size_t)(end - ptr));
if (n > 0) {
ptr += n;
} else
if (n != -1) {
if (!status_errno)
status_errno = EIO;
break;
} else
if (errno != EINTR) {
if (!status_errno)
status_errno = errno;
break;
}
}
}
/* Close and release lease. */
close(status_fd);
status_fd = -1;
/* After we release the mutex, we can be safely canceled again. */
pthread_mutex_unlock(&status_lock);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
pthread_testcancel();
}
}
static int start_status_worker(void)
{
sigset_t mask;
int result;
pthread_attr_t attrs;
/* This thread should block LEASE_SIGNAL signals. */
sigemptyset(&mask);
sigaddset(&mask, LEASE_SIGNAL);
result = pthread_sigmask(SIG_BLOCK, &mask, NULL);
if (result)
return errno = result;
/* Create the worker thread. */
pthread_attr_init(&attrs);
pthread_attr_setstacksize(&attrs, 2*PTHREAD_STACK_MIN);
result = pthread_create(&status_thread, &attrs, status_worker, NULL);
pthread_attr_destroy(&attrs);
/* Ready. */
return 0;
}
int set_status(const char *format, ...)
{
va_list args;
char *new_status = NULL;
int len;
if (!format)
return errno = EINVAL;
va_start(args, format);
len = vasprintf(&new_status, format, args);
va_end(args);
if (len < 0)
return errno = EINVAL;
pthread_mutex_lock(&status_lock);
free(status);
status = new_status;
status_len = len;
status_changed++;
/* Do we already have a status file prepared? */
if (status_fd != -1 || !status_newpath) {
pthread_mutex_unlock(&status_lock);
return 0;
}
/* Prepare the status file. */
do {
status_fd = open(status_newpath, O_WRONLY | O_CREAT | O_CLOEXEC, 0666);
} while (status_fd == -1 && errno == EINTR);
if (status_fd == -1) {
pthread_mutex_unlock(&status_lock);
return 0;
}
/* In case of failure, do cleanup. */
do {
/* Set lease signal. */
if (fcntl(status_fd, F_SETSIG, LEASE_SIGNAL) == -1)
break;
/* Get exclusive lease on the status file. */
if (fcntl(status_fd, F_SETLEASE, F_WRLCK) == -1)
break;
/* Replace status file with the new, leased one. */
if (rename(status_newpath, status_path) == -1)
break;
/* Success. */
pthread_mutex_unlock(&status_lock);
return 0;
} while (0);
if (status_fd != -1) {
close(status_fd);
status_fd = -1;
}
unlink(status_newpath);
pthread_mutex_unlock(&status_lock);
return 0;
}
int main(int argc, char *argv[])
{
char *line = NULL;
size_t size = 0;
ssize_t len;
if (argc != 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
const char *argv0 = (argc > 0 && argv[0]) ? argv[0] : "(this)";
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv0);
fprintf(stderr, " %s STATUS-FILE\n", argv0);
fprintf(stderr, "\n");
fprintf(stderr, "This program maintains a pseudofile-like status file,\n");
fprintf(stderr, "using the contents from standard input.\n");
fprintf(stderr, "Supply an empty line to exit.\n");
fprintf(stderr, "\n");
return EXIT_FAILURE;
}
status_path = join2(argv[1], "");
status_newpath = join2(argv[1], ".new");
unlink(status_path);
unlink(status_newpath);
if (start_status_worker()) {
fprintf(stderr, "Cannot start status worker thread: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
if (set_status("Empty\n")) {
fprintf(stderr, "Cannot create initial empty status: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
while (1) {
len = getline(&line, &size, stdin);
if (len < 1)
break;
line[strcspn(line, "\n")] = '\0';
if (line[0] == '\0')
break;
set_status("%s\n", line);
}
pthread_cancel(status_thread);
pthread_join(status_thread, NULL);
if (status_fd != -1)
close(status_fd);
unlink(status_path);
unlink(status_newpath);
return EXIT_SUCCESS;
}
Сохраните приведенное выше как server.c
, затем скомпилируйте, используя, например.
gcc -Wall -Wextra -O2 server.c -lpthread -o server
Это реализует сервер состояния, сохраняя каждую строку из стандартного ввода в файл состояния, если это необходимо. Укажите пустую строку для выхода. Например, чтобы использовать файл status
в текущем каталоге, просто запустите
./server status
Затем, если вы используете другое окно терминала для просмотра каталога, вы увидите, что в нем есть файл с именем status
(обычно с нулевым размером). Но cat status
показывает вам его содержимое; точно так же, как псевдофайлы procfs/sysfs.
Обратите внимание, что файл состояния обновляется только в случае необходимости и только для первого читателя/доступа после изменения состояния. Это позволяет снизить накладные расходы писателя/сервера и операций ввода-вывода, даже если состояние изменяется очень часто.
В приведенном выше примере программы используется рабочий поток для перехвата сигналов разрыва аренды. Это связано с тем, что мьютексы pthread не могут быть безопасно заблокированы или освобождены в обработчике сигналов (pthread_mutex_lock()
и т. д. не являются безопасными для асинхронных сигналов). Рабочий поток сохраняет свою отменяемость, так что он не будет отменен, когда он удерживает мьютекс; если он будет отменен в течение этого времени, он будет отменен после освобождения мьютекса. Так осторожно.
Кроме того, файл временной замены не является случайным, это просто имя файла состояния с добавлением .new
в конце. В любом месте на том же креплении будет работать нормально.
Пока другие потоки также блокируют сигнал разрыва аренды, это прекрасно работает и в многопоточных программах. (Если вы создадите другие потоки после рабочего потока, они наследуют правильную маску сигнала от основного потока; start_status_worker()
устанавливает маску сигнала для вызывающего потока.)
Я доверяю подходу в программе, но в этой реализации могут быть ошибки (и, возможно, даже thinkos). Если найдете, прокомментируйте или отредактируйте.
person
Guest
schedule
09.06.2020
mkfifo
) - person dvhh   schedule 09.06.2020