我如何在Linux上公开类似于/ procfs的自定义文件?

问题描述 投票:0回答:1

我有一个writer进程,该进程以wchar_t的可读chunck定期输出其状态。我需要确保以下属性:

  1. 有更新时,读者不应读取部分/损坏的数据
  2. 该文件在内存中应该是易失的,以便在退出编写器时该文件消失了
  3. 文件内容大小可变
  4. 多个阅读器可以并行读取文件,内容是否同步都无所谓,只要每个客户端都是非局部的
  5. 如果使用truncate,然后使用write,则客户端应仅读取完整文件,而不能观察到此类部分操作

如何在/ procfs文件系统之外实现类似/ procfs的文件?

我曾考虑使用经典的c Linux文件API,并默认情况下在/ dev / shm下创建内容,但是我发现很难有效地实现第1点和第5点。我如何公开这样的文件?

c linux in-memory procfs
1个回答
4
投票

典型的解决方案是在同一目录中创建一个新文件,然后在旧文件上重命名(硬链接)。

这样,进程看到的是旧的或新的,永远不会混合;并且仅取决于打开文件的时间。

Linux内核负责缓存,因此,如果经常访问文件,它将位于RAM(页面缓存)中。但是,编写者必须记得退出文件时将其删除。


一种更好的方法是使用基于fcntl()的咨询记录锁(通常在整个文件上,即.l_whence = SEEK_SET.l_start = 0.l_len = 0)。

作者将在截断和重写内容之前获取写/独占锁,而读者在读取内容之前将拥有读/共享锁。

但是,这需要合作,并且编写者必须准备好不能锁定(否则,抓住该锁定可能要花费不确定的时间)。


仅Linux方案将使用原子替换(通过重命名/硬链接)和文件租用。

((当writer进程对打开的文件拥有独占租约时,只要另一个进程要打开同一文件(inode,而不是文件名),它都会收到信号。它至少有几秒钟的时间来降级或释放租约,此时打开程序即可访问其中的内容。)

[基本上,编写器进程创建一个空的状态文件,并在其上获得排他租约。编写器每当收到表示读取器要访问状态文件的信号时,就会将当前状态写入文件,释放租约,在与状态文件相同的目录中创建一个新的空文件(安装相同),获得一个独占租约,并在状态文件上对其重命名/硬链接。

如果状态文件的内容不是一直都在变化,只是周期性地变化,那么编写器进程将创建一个空的状态文件,并在其上获取排他租约。编写器每当收到表示读取器要访问(空)状态文件的信号时,就会将当前状态写入文件,并释放租约。然后,当写入器进程的状态被更新且尚无租约时,它将在状态文件目录中创建一个新的空文件,对其进行排他租约,并在状态文件上重命名/硬链接。

这样,状态文件总是在读者打开文件之前才更新。如果同时有多个读取器,则在写入器释放租约时,他们可以不中断地打开状态文件。

重要的是,应将状态信息收集到单个结构或类似结构中,这样才能将其写到状态文件中是有效的。如果没有足够快地释放租约,租约会自动中断(但至少有几秒钟的时间才能做出反应),并且租约位于inode上-文件内容-而不是文件名,因此我们仍然需要原子替换。

这是一个简单的示例实现:

#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伪文件一样。

请注意,状态文件仅在必要时更新,并且仅在状态更改后才更新给第一个读取器/访问器。即使状态经常更改,也可以保持编写器/服务器的开销和I / O较低。

上面的示例程序使用工作线程来捕获租约中断信号。这是因为无法在信号处理程序中安全锁定或释放pthread互斥锁(pthread_mutex_lock()等不是异步信号安全)。辅助线程保持其cancelability,以便在持有互斥对象时不会被取消;如果在此期间取消,它将在释放互斥锁后被取消。这样要小心。

此外,临时替换文件不是随机的,它只是状态文件名,其末尾附加有.new。在同一安装架上的任何地方都可以正常工作。

只要其他线程也阻止了租约中断信号,这在多线程程序中也可以正常工作。 (如果在工作线程之后创建其他线程,则它们将从主线程继承正确的信号掩码; start_status_worker()设置调用线程的信号掩码。)

我确实相信程序中的方法,但是此实现中可能会有错误(甚至可能是思想家)。如果找到任何内容,请评论或编辑。

© www.soinside.com 2019 - 2024. All rights reserved.