检查 `read` 是否会 return EOF 而不会消耗任何数据

Check if a `read` would return EOF without consuming any data

我有一个 C 程序,它通过 (Linux) 管道从另一个程序接收数据。如果管道在写入任何数据之前关闭,我希望程序的行为有所不同。

执行此操作的自然方法是尝试从管道读取并检查我是否得到 EOF,但是如果有任何可用数据,它会消耗管道中的一些数据,并且(据我所知知道)无法将数据“放回”管道中。

程序中我要检查管道是否为空的部分离我处理数据的地方很远,所以我宁愿在那之前不必处理保存第一次读取的数据.

有什么方法可以检查管道是否为空(read 会 return EOF),如果它不为空则不消耗任何数据?

注意:我确实希望它在管道尚未写入或关闭时阻塞。

不,没有办法按照您描述的进行操作。确定是否已到达 non-seekable 文件(如管道)末尾的方法是尝试从中读取。这不仅仅是自然的方式,它是 的方式。

but that consumes some data from the pipe if there is any available,

是的。

and (as far as I know) there's no way to put data "back" in a pipe.

这取决于。如果你是用 POSIX read() 阅读,那么不会。如果您将管端包裹在 FILE 中并使用 stdio 函数读取它,那么就会有 ungetc().

然而,这:

The part of the program where I want to check if the pipe is empty is pretty far away from where I process the data

似乎是设计问题。在您实际 确实 获取数据或看到 EOF 之前,您无法知道您是否会获取数据。管道写入端的进程可以在对管道进行任何操作之前延迟任意时间,即使该进程是由您提供的,您也无法完全控制其行为的这一方面。因此,从某种意义上说,在您准备好使用数据之前尝试检查 EOF 没有多大意义,因为您不能指望在不阻塞的情况下获得答案。

, so I'd rather not have to deal with saving the data from my first read until then.

我想你一定想避免在没有数据要处理的情况下执行某种重量级的初始化。好的,但我不明白有什么大不了的。无论如何,您都需要提供用于读取数据的存储空间。这样的事情有什么问题:

void consume_pipe_data(int fd) {
    char buffer[BUFFER_SIZE];
    ssize_t count;

    count = read(fd, buffer, BUFFER_SIZE);
    if (count == 0) {
        handle_no_data();
        return;
    } else if (count > 0) {
        perform_expensive_initialization();
    }
    do {
        if (count == -1) {
            handle_error();
            return;
        }
        consume_data(buffer);
        count = read(fd, buffer, BUFFER_SIZE);
    } while (count);
}

关键不在于这一定是适合您的程序的结构,而是可以构建程序以便存储数据(如果有的话)来自初始读取的数据非常干净自然。

如果您使用 Unix 域流套接字而不是管道 – 这意味着您将 pipe(fds) 调用替换为 socketpair(AF_UNIX, SOCK_STREAM, 0, fds) –, you could use recv(fd, dummybuffer, 1, MSG_PEEK) 到 read/receive 一个字节的数据,而不会将其从接收缓冲区中删除.

如果不想阻塞,可以将 MSG_PEEKMSG_DONTWAIT 结合使用,如果想要阻塞,则可以与 MSG_WAITALL 结合使用,直到整个缓冲区都被填满。

Unix 域流套接字和管道之间的区别很小。流套接字是双向的,但您可以使用 shutdown(fd, SHUT_WR)(或 SHUT_RD)关闭“写入端”(resp。“读取端”),这意味着如果另一端尝试从套接字读取,他们将立即获得 end-of-stream(read()recv() 等 return 0)。 (关闭“读端”意味着当另一端尝试写入套接字时,他们将得到 EPIPE。)

现在,我什至想不出为什么使用管道的程序不能使用 Unix 域流套接字对的原因。

如果您使用命名管道,您需要将 mkfifo()open() 更改为 socket(AF_UNIX, SOCK_STREAM, 0),然后将 bind() 更改为套接字地址。 read()write(),甚至 higher-level 标准 I/O 设施在 Unix 域流套接字之上工作得很好(使用 fdopen() 来转换套接字描述符到 FILE 句柄)。

如果你不能修改 readers,你可以创建一个插入 openat() 的最小动态库(这是当前 C 库在 fopen() 下面使用的),调用原始 openat() 对于除了套接字路径之外的所有内容,比如在环境变量中命名,而是创建一个套接字并绑定到该套接字路径。执行 reader 二进制文件时,只需设置 LD_PRELOAD 指向此插入库。

换句话说,我相信从管道切换到 Unix 域流套接字没有真正的障碍。

您不能将 recv() 与管道一起使用,因为管道是在 Linux 中使用特殊文件系统而不是套接字实现的。