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

How do I expose custom files similar to /procfs on Linux?

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

  1. 当有和更新时,读者不应该阅读partial/corrupted数据
  2. 文件在内存中应该是易失的,这样当编写器退出时,文件就消失了
  3. 文件内容大小可变
  4. 多个读者可以并行阅读文件,内容是否同步无关紧要,只要对每个客户端不偏不倚
  5. 如果使用 truncate 然后 write,客户端应该只读取完整的文件而不观察这样的部分操作

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

我想使用经典的 c Linux 文件 API 并默认在 /dev/shm 下创建一些东西,但我发现很难有效地实施第 1 点和第 5 点。 我怎么能公开这样的文件?

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

这样,进程要么看到旧的,要么看到新的,永远不会混合;这仅取决于他们打开文件的时间。

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


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

编写器会在运行覆盖和重写内容前获取write/exclusive锁,在读取内容前reader会获取read/shared锁。

但是这需要合作,并且编写者必须做好无法锁定的准备(否则抢到锁可能会花费不确定的时间)。


只有 Linux 的方案是使用原子替换(通过 rename/hardlinking)和文件租约。

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

基本上,写入进程创建一个空的状态文件,并在其上获得独占租约。每当 writer 收到 reader 想要访问状态文件的信号时,它会将当前状态写入文件,释放租约,在与状态相同的目录(相同的挂载就足够)中创建一个新的空文件文件,获得该文件的独占租约,并 renames/hardlinks 它覆盖状态文件。

如果状态文件内容不会一直变化,只是周期性变化,那么写进程创建一个空的状态文件,并获得独占租约。每当编写器收到 reader 想要访问(空)状态文件的信号时,它会将当前状态写入文件,并释放租约。然后,当 writer 进程的状态更新时,还没有租约,它会在状态文件目录中创建一个新的空文件,对其进行独占租约,并 renames/hardlinks 覆盖状态文件。

这样,状态文件总是在 reader 打开它之前更新,并且仅在那时更新。如果同时有多个reader,他们可以在写入者释放租约时不间断地打开状态文件。

重要的是要注意状态信息应该收集在一个单一的结构或类似的结构中,以便将其写入状态文件是有效的。如果没有足够快地释放租约,租约会自动中断(但至少有几秒钟的反应时间),并且租约是在 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] = '[=10=]';

    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")] = '[=10=]';
        if (line[0] == '[=10=]')
            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

然后,如果您使用另一个终端 window 检查该目录,您会看到它有一个名为 status 的文件(通常大小为零)。但是,cat status 向您展示了它的内容;就像 procfs/sysfs 伪文件一样。

请注意,状态文件仅在必要时更新,并且仅针对状态更改后的第一个 reader/accessor。这使得 writer/server 开销和 I/O 较低,即使状态经常变化也是如此。

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

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

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

我确实相信程序中的方法,但此实现中可能存在错误(甚至可能存在缺陷)。如果您发现任何内容,请发表评论或编辑。