![动态文件内容生成:通过“进程执行”满足“文件打开”](https://linux22.com/image/32027/%E5%8A%A8%E6%80%81%E6%96%87%E4%BB%B6%E5%86%85%E5%AE%B9%E7%94%9F%E6%88%90%EF%BC%9A%E9%80%9A%E8%BF%87%E2%80%9C%E8%BF%9B%E7%A8%8B%E6%89%A7%E8%A1%8C%E2%80%9D%E6%BB%A1%E8%B6%B3%E2%80%9C%E6%96%87%E4%BB%B6%E6%89%93%E5%BC%80%E2%80%9D.png)
我有一个只读文件,F
.
一个程序,P
需要阅读,但我不是其作者F
。
我希望 的内容F
来自另一个“生成器”程序,G
每当P
尝试读取F
(视为F
普通文件)而不是更早的时候。
我尝试执行以下操作:
$ mkfifo /well-known/path/to/F # line #1
$ G > /well-known/path/to/F # line #2
现在,当P
启动并尝试读取时F
,它似乎能够读取 生成的输出,G
就像我希望的那样。然而,它只能执行一次,因为 G 毕竟只能运行一次!因此,如果稍后P
需要F
在执行过程中再次读取,最终会阻塞 fifo!
我的问题是,除了在某种无限循环中将上面的第 2 行括起来之外,上述还有其他(优雅的)替代方案吗?
我希望的是,以某种方式将“钩子”程序注册到文件打开系统调用中,以便文件打开将导致钩子程序的启动和文件读取的读取钩子程序输出。显然,这里的假设是:读取将从文件开始到文件结尾顺序发生,而不是随机查找。
答案1
FUSE + 软链接(或绑定安装)是一种解决方案,尽管我不认为它“优雅”,但有很多包袱。在 *BSD 上你有更简单的选择门户文件系统,你可以用符号链接来解决这个问题——很多年前就有一个移植到 Linux 的版本,但它似乎已经被放弃了,可能是为了支持 FUSE。
您可以很容易地注入一个库来覆盖它所进行的所需的open()
/ open64()
libc 调用。例如:
#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <dlfcn.h>
#include <stdarg.h>
// gcc -Wall -rdynamic -fPIC -nostartfiles -shared -ldl -Wl,-soname,open64 \
// -o open64.so open64.c
#define DEBUG 1
#define dfprintf(fmt, ...) \
do { if (DEBUG) fprintf(stderr, "[%14s#%04d:%8s()] " fmt, \
__FILE__, __LINE__, __func__, __VA_ARGS__); } while (0)
typedef int open64_f(const char *pathname, int flags, ...);
typedef int close_f(int fd);
static open64_f *real_open64;
static close_f *real_close;
static FILE *mypipe=NULL;
static int mypipefd=-1;
//void __attribute__((constructor)) my_init()
void _init()
{
char **pprog=dlsym(RTLD_NEXT, "program_invocation_name");
dfprintf("It's alive! argv[0]=%s\n",*pprog);
real_open64 = dlsym(RTLD_NEXT, "open64");
dfprintf("Hook %p open64()\n",(void *)real_open64);
if (!real_open64) printf("error: %s\n",dlerror());
real_close = dlsym(RTLD_NEXT, "close");
dfprintf("Hook %p close()\n",(void *)real_close);
if (!real_close) printf("error: %s\n",dlerror());
}
int open64(const char *pathname, int flags, ...)
{
mode_t tmpmode=0;
va_list ap;
va_start(ap, flags);
if (flags & O_CREAT) tmpmode=va_arg(ap,mode_t);
va_end(ap);
dfprintf("open64(%s,%i,%o)\n",pathname,flags,tmpmode);
if (!strcmp(pathname,"/etc/passwd")) {
mypipe=popen("/usr/bin/uptime","r");
mypipefd=fileno(mypipe);
dfprintf(" popen()=%p fd=%i\n",mypipe,mypipefd);
return mypipefd;
} else {
return real_open64(pathname,flags,tmpmode);
}
}
int close(int fd)
{
int rc;
dfprintf("close(%i)\n",fd);
if (fd==mypipefd) {
rc=pclose(mypipe); // pclose() returns wait4() status
mypipe=NULL; mypipefd=-1;
return (rc==-1) ? -1 : 0;
} else {
return real_close(fd);
}
}
编译并运行:
$ gcc -Wall -rdynamic -fPIC -nostartfiles -shared -ldl -Wl,-soname,open64 \
-o open64.so open64.c
$ LD_PRELOAD=`pwd`/open64.so cat /etc/passwd
19:55:36 up 1110 days, 9:19, 55 users, load average: 0.53, 0.33, 0.29
根据应用程序的具体工作方式(libc 调用),您可能需要处理open()
or fopen()/fclose()
。上面的代码适用于cat
or head
,但不是sort
因为它调用了fopen()
(也可以直接将fopen()
/添加fclose()
到上面)。
您可能需要比上面更多的错误处理和健全性检查(特别是对于长时间运行的程序,以避免泄漏)。此代码无法正确处理并发打开。
由于管道和文件有明显的差异,因此存在程序发生故障的风险。
daemon -r -- socat -u EXEC:/usr/bin/uptime PIPE:/tmp/uptime
这有一个轻微的缺点(这里应该很明显):提供程序开始写入然后阻塞,因此您会看到旧的正常运行时间,而不是按需运行。您的提供商需要使用非阻塞 I/O 才能正确提供即时数据。 (unix 域套接字将允许更传统的客户端/服务器方法,但这与您可以直接插入的 FIFO/命名管道不同。)
更新另请参阅后面的这个问题,它涵盖了相同的主题,尽管概括为任意进程/读者而不是特定的进程/读者: 如何制作一个特殊文件,在读取时执行代码 (请注意,仅限 fifo 的答案不能可靠地推广到并发读取)
答案2
你可以做的是拥有一个守护进程(我们称它为守护进程,因为每次需要它时filld
它都会被填满)。F
P
当您启动它时,它会尝试打开 FIFO(并因没有读取器而阻塞)。每次读取器到来时,它都会向 FIFO 写入需要写入的内容(例如fork
-ing 和exec
-ing G
),关闭 FIFO 并重新打开它。每次成功打开时,都会写入F
.如果它收到 SIGPIPE,它会关闭所有内容,并再次在 FIFO 上阻塞自己。
答案3
造成无限循环的原因至少有两个。除了您注意到的那个之外:您希望每次访问时都更新文件,不是吗?否则,您可以简单地创建一个包含正确内容的常规文件。
open*() 的挂钩是可能的,但可能并不简单。保险丝是要走的路。如果你幸运的话,那么这个模块已经存在了。否则,您需要编写自己的模块来挂钩这一路径并传递其余路径。