使用带有 select 的非阻塞模式打开 fifo。

8
我有两个进程A和B。通信流程始终是A -> B,但我需要使用一个命名管道来实现,因为我必须在B进程中的select调用中使用管道文件描述符,并且写入管道的数据必须在任何一个或两个进程退出时保持存在。
管道在两端都以非阻塞模式打开。在进程A中:
int push_fifo_fd = open(FIFO_NAME, O_WRONLY | O_NONBLOCK | O_CREAT, 0644);

在B流程中:
int fd = open(FIFO_NAME, O_RDONLY | O_NONBLOCK | O_CREAT, 0644);

Q1. B进程使用curl的multi接口,因此我获取curl multi句柄的fd_sets,将“fd”描述符添加到read fd_set中,然后调用select函数获取可读和可写的文件描述符。每次调用select时,“fd”都包含在结果read fd_set中,但即使写端打开了,read函数也会返回0。这导致进程B使用100%的处理器时间。我提到我不知道管道两端打开的顺序。来自B的相关代码:

while (1)
{
    fd_set read_fds, write_fds, err_fds;

    FD_ZERO(&read_fds);
    FD_ZERO(&write_fds);
    FD_ZERO(&err_fds);

    FD_SET(fifo_fd, &read_fds);
    // some code
    ccode = curl_multi_fdset(curlm, &read_fds, &write_fds, &err_fds, &max_fd);
    max_fd = MAX(max_fd, fifo_fd);

    rc = select(max_fd + 1, &read_fds, &write_fds, &err_fds, &timeout);
    switch (rc)
    {
        case -1:
            WARN("select");
            continue;

        case 0:
        default:
            {
                if (FD_ISSET(fifo_fd, &read_fds))
                {
                    // read from the fifo_fd
                }

                /* Now look at the handles that need attention */
                int old_running_handles = running_handles;

                ccode = curl_multi_perform(curlm, &running_handles);
                if (ccode != CURLM_OK && ccode != CURLM_CALL_MULTI_PERFORM)
                {
                    WARN("curl_multi_perform error: %s", curl_multi_strerror(ccode));
                    continue;
                }

                if (running_handles != old_running_handles)
                {
                    CURLMsg *curl_msg;
                    int left_msgs = 0;
                    while ((curl_msg = curl_multi_info_read(curlm, &left_msgs)) != NULL)
                    {
                        // treat each easy handle
                    }
                }
            }
            break;
    }
}

Q2. 在 "man 7 fifo" 中提到:“一个进程可以以非阻塞模式打开FIFO。在这种情况下,只读打开将成功,即使没有任何人在写端打开,只写打开将失败,并显示ENXIO(没有这样的设备或地址),除非另一端已经被打开。”但是,即使读端没有被打开,进程A仍然可以成功以非阻塞模式打开管道的写端。为什么会这样?我测试的平台是Ubuntu服务器12.04.3,内核版本为3.8.0-29。


如果指定的 FIFO_NAME 文件确实是一个 FIFO,那么在打开它进行读写时使用 O_CREAT 是不可取的。如果 open() 必须创建它,则会将其创建为普通文件,这样做是适得其反的。 - John Bollinger
这个问题描述的行为似乎与相关系统接口的文档相矛盾。通常情况下,我会给提问者一个机会提供一个 MCVE,但考虑到这是一个五年前的死贴问题,似乎不太可能会有 MCVE 提供。因此,我直接投票关闭。 - John Bollinger
我在内核4.9.37上看到了Q1问题,但没有看到Q2问题。 - minghua
你必须单独处理 select() 返回零的情况,而且在这种情况下不要检查 FD_SETs。 - wildplasser
假设单独的读取器和写入器进程打开一个FIFO。在大多数系统上,如果写入器向FIFO写入并退出,而读取器也在未读取所写信息的情况下退出,则当最后一个具有打开文件描述符的进程关闭它(例如因为它退出)时,未读取的信息将被丢弃。因此,在大多数系统上使用FIFO无法满足“当任何一个或两个进程退出时,写入管道的数据必须持久存在”的要求。_ [ ...继续... ]_ - Jonathan Leffler
演示:mkfifo FIFO; (echo "Hello World" > FIFO) & (echo "Hi" < FIFO); (echo "Goodbye, Cruel World!" > FIFO) & cat FIFO; rm -f FIFO — 输出是 HiGoodbye, Cruel World!(加上作业控制信息)。信息 Hello World! 丢失了。将 echo "Hi" < FIFO 更改为 cat FIFO,则可以看到 Hello World!(而没有 "Hi",因为它没有被写入任何地方)。 - Jonathan Leffler
2个回答

2

Q1应该通过select()或者poll()来获取。参考相关问题。一个优雅的解决办法是在相同的FIFO上打开另一个文件描述符并关闭原始文件描述符。

我认为在某些内核版本中也期望Q2发生。 手册7 fifo中有一段关于此的话:

   Under Linux, opening a FIFO for read and write will succeed both in 
   blocking and nonblocking mode.  POSIX leaves this behavior undefined.
   This can be used to open a FIFO for writing while there are no
   readers available.

这段话似乎在说,正如原始作者在Q2中观察到的那样,您可以随时成功打开fifo的写端口。

尽管它似乎与前一段相矛盾,原始问题也引用自man 7 fifo页面,基本上是在说打开应该失败而不是成功:

   A process can open a FIFO in nonblocking mode.  In this case, opening
   for read-only succeeds even if no one has opened on the write side
   yet and opening for write-only fails with ENXIO (no such device or
   address) unless the other end has already been opened.

我发现在4.9.37内核上,当读端未打开时以非阻塞模式打开写端会失败。我猜这可能是从3.8版本到4.9版本的变化。


注意到之前的答案在Q2方面的观点是一致的。尽管在撰写这个答案之前我并没有理解它。 - minghua

0

原因是:

在Linux下,以读写方式打开FIFO管道无论是阻塞还是非阻塞模式都会成功。POSIX标准未定义此行为。这可以用于在没有读取器可用的情况下打开FIFO管道进行写入。使用连接的两端与自身通信的进程应非常小心以避免死锁。

来自http://linux.die.net/man/7/fifo


1
这个回答对于 OP 的问题两个分支都没有响应。它似乎意图回答第二部分“Q2”,但引用的内容描述的是不同于 OP 所询问的情况。 - John Bollinger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接