Как открыть пользовательские файлы, подобные /procfs, в Linux?

У меня есть процесс писатель, который выводит свое состояние через равные промежутки времени в виде удобочитаемого фрагмента числа wchar_t. Мне нужно было бы обеспечить следующие свойства:

  1. При наличии и обновлении читатели не должны читать частичные/поврежденные данные.
  2. Файл должен находиться в памяти в энергозависимом состоянии, чтобы при выходе из программы записи файл исчезал.
  3. Размер содержимого файла является переменным
  4. Несколько читателей могут читать файл параллельно, не имеет значения, синхронизирован ли контент, если он не является частичным для каждого клиента.
  5. При использовании truncate, а затем write клиенты должны читать только полный файл и не наблюдать за такими частичными операциями.

Как я могу реализовать такой файл, подобный /procfs, вне файловой системы /procfs?

Я думал использовать классические файловые API c Linux и создать что-то под /dev/shm по умолчанию, но мне трудно эффективно реализовать пункты 1 и 5 больше всего . Как я могу открыть такой файл?


person Emanuele    schedule 09.06.2020    source источник
comment
вы можете создать драйвер файловой системы или исследовать его с помощью libfuse, что было бы проще   -  person dvhh    schedule 09.06.2020
comment
Не могу создать драйвер, он должен запускаться в пользовательском пространстве.   -  person Emanuele    schedule 09.06.2020
comment
вы также можете использовать канал (см. mkfifo)   -  person dvhh    schedule 09.06.2020
comment
С потенциально несколькими читателями? Как бы вы рекомендовали его использовать? Есть подробности?   -  person Emanuele    schedule 09.06.2020
comment
Итак, вы говорите, что вам нужен какой-то ресурс копирования при записи. Это похоже на процесс, предлагающий данные по каналу или сокету, отслеживая каждого клиента. В другом подходе, я не думаю, что вы могли бы сделать это с реальным файлом, но вы могли бы осуществить это с процессом записи, порождающим память mmapped. Тогда это становится проблемой времени и эффективности.   -  person Cheatah    schedule 09.06.2020
comment
@Cheatah Мне нужно что-то вроде /proc/self/maps, где другие процессы могут легко его прочитать, никогда не исчезают (пока процесс жив) и еще много чего. Я бы не хотел использовать сокеты/каналы или mmap, где у меня должен быть один или несколько потребителей, я действительно хотел бы сделать что-то вроде /procfs... просто вывести файл, и если есть одному или нескольким читателям, они могут просматривать его без усечения файла. Не волнует, если один или несколько читателей читают устаревшую информацию.   -  person Emanuele    schedule 09.06.2020


Ответы (1)


Типичное решение — создать новый файл в том же каталоге, а затем переименовать (жестко связать) его поверх старого.

Таким образом, процессы видят либо старый, либо новый, а не микс; и это зависит только от момента, когда они открывают файл.

Ядро 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
comment
Спасибо! Не беспокойтесь о примере, моей первой мыслью действительно было пойти по тому, что вы описываете как вариант 1, и создать файл tmp в /dev/shm (или любом другом каталоге), а затем переименовать в целевой файл. ... это должно быть как-то атомарно, я надеюсь... и читатели не увидят частичных данных... - person Emanuele; 09.06.2020
comment
@Emanuele: Да, переименование файла вместо другого в Linux атомарно. Открыватели видят либо старый файл, либо новый файл, а не смесь; и это не влияет на программы чтения, у которых открыт старый файл, они могут читать (и даже писать) в свой файл даже после этого. Это связано с тем, что фактическими метаданными и содержимым файла являются inode, а имена просто ссылаются на inodes. - person Guest; 09.06.2020
comment
@Emanuele: Однако я решил, что подход с арендой файлов является более жизнеспособным, поэтому я написал пример программы. Он использует вспомогательный поток для обработки сигналов прекращения аренды и для обновления содержимого всякий раз, когда читателю требуется новое содержимое состояния. (После того, как содержимое было записано, читатели могут получить к нему свободный доступ без какой-либо работы со стороны автора до следующего обновления статуса. Если статус меняется часто, а файл статуса просматривается реже, это более эффективно, чем просто сохранение каждого статуса!) - person Guest; 09.06.2020
comment
Спасибо - я пойду с простым подходом :-) - person Emanuele; 09.06.2020