为什么使用这种bash管道结构会丢失数据?


11

我正在尝试合并一些这样的程序(请忽略任何额外的包含,这是繁重的工作):

pv -q -l -L 1  < input.csv | ./repeat <(nc "host" 1234)

重复程序的来源如下所示:

#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <iostream>
#include <string>

inline std::string readline(int fd, const size_t len, const char delim = '\n')
{
    std::string result;
    char c = 0;
    for(size_t i=0; i < len; i++)
    {
        const int read_result = read(fd, &c, sizeof(c));
        if(read_result != sizeof(c))
            break;
        else
        {
            result += c;
            if(c == delim)
                break;
        }
    }
    return result;
}

int main(int argc, char ** argv)
{
    constexpr int max_events = 10;

    const int fd_stdin = fileno(stdin);
    if (fd_stdin < 0)
    {
        std::cerr << "#Failed to setup standard input" << std::endl;
        return -1;
    }


    /* General poll setup */
    int epoll_fd = epoll_create1(0);
    if(epoll_fd == -1) perror("epoll_create1: ");
    {
        struct epoll_event event;
        event.events = EPOLLIN;
        event.data.fd = fd_stdin;
        const int result = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd_stdin, &event);
        if(result == -1) std::cerr << "epoll_ctl add for fd " << fd_stdin << " failed: " << strerror(errno) << std::endl;
    }

    if (argc > 1)
    {
        for (int i = 1; i < argc; i++)
        {
            const char * filename = argv[i];
            const int fd = open(filename, O_RDONLY);
            if (fd < 0)
                std::cerr << "#Error opening file " << filename << ": error #" << errno << ": " << strerror(errno) << std::endl;
            else
            {
                struct epoll_event event;
                event.events = EPOLLIN;
                event.data.fd = fd;
                const int result = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);
                if(result == -1) std::cerr << "epoll_ctl add for fd " << fd << "(" << filename << ") failed: " << strerror(errno) << std::endl;
                else std::cerr << "Added fd " << fd << " (" << filename << ") to epoll!" << std::endl;
            }
        }
    }

    struct epoll_event events[max_events];
    while(int event_count = epoll_wait(epoll_fd, events, max_events, -1))
    {
        for (int i = 0; i < event_count; i++)
        {
            const std::string line = readline(events[i].data.fd, 512);                      
            if(line.length() > 0)
                std::cout << line << std::endl;
        }
    }
    return 0;
}

我注意到了这一点:

  • 当我仅使用到的管道时./repeat,一切都按预期工作。
  • 当我只使用流程替换时,一切都会按预期进行。
  • 当我使用进程替换封装pv时,一切都会按预期进行。
  • 但是,当我使用特定的结构时,我似乎会丢失stdin的数据(单个字符)!

我尝试了以下方法:

  • 我试图禁用所有进程之间pv以及所有进程之间./repeat使用的管道上的缓冲stdbuf -i0 -o0 -e0,但这似乎不起作用。
  • 我已经将epoll换成民意调查,但是没有用。
  • 当我查看pv和之间的流./repeattee stream.csv,这看起来是正确的。
  • 我曾经strace看过发生了什么,并且看到了很多单字节读取(如预期的那样),并且它们还表明数据正在丢失。

我不知道这是怎么回事?还是我可以做进一步的调查?

Answers:


16

因为nc里面的命令<(...)也会从stdin中读取。

比较简单的例子:

$ nc -l 9999 >/tmp/foo &
[1] 5659

$ echo text | cat <(nc -N localhost 9999) -
[1]+  Done                    nc -l 9999 > /tmp/foo

text去哪儿了?通过netcat。

$ cat /tmp/foo
text

您的程序nc将与同一个标准输入竞争,并nc获得其中的一些。


你是对的!谢谢!您能提出一种干净的方法来断开stdin中的连接<(...)吗?有没有比这更好的方法<( 0<&- ...)
Roel Baardman '19

5
<(... </dev/null)。不要使用0<&-:它将导致第一个open(2)返回0新的fd。如果您nc支持,也可以使用该-d选项。
mosvy

3

E / POLLIN返回的epoll()或poll()只会告诉您单个 read()可能不会阻塞。

并不是像您一样,您将能够执行多达一个字节的read()直到换行符。

我说可能是因为()的epoll后读()为E返回/ POLLIN可能仍然阻挡。

您的代码还将尝试读取过去的EOF,并完全忽略任何read()错误。


尽管这不是我的问题的直接解决方案,但感谢您的评论。我意识到此代码存在缺陷,并且在简化程度较低的版本中(通过使用POLLHUP / POLLNVAL)提供了EOF检测。我确实在努力寻找一种从多个文件描述符读取行的无缓冲方式。我的repeat程序实际上是在处理来自多个来源的NMEA数据(基于行且没有长度指示符)。由于我要合并来自多个实时资源的数据,因此我希望我的解决方案不受缓冲。您能建议一种更有效的方法吗?
Roel Baardman '19

首先,对每个字节进行系统调用(读取)是最不高效的方法。只需检查read的返回值即可完成EOF检查,而不需要POLLHUP(并且POLLNVAL仅在将其传递给假fd时才返回,而不是在EOF上返回)。但是无论如何,请继续关注。我有一个ypee实用程序的想法,该实用程序可以从多个fds中读取数据并将它们混合到另一个fd中,同时保留记录(保持行完整)。
pizdelect

我注意到这种bash构造应该做到这一点,但是我不知道如何在其中组合stdin:{ cmd1 & cmd2 & cmd3; } > file文件将包含您所描述的内容。但是,就我而言,我正在运行tcpserver(3)中的所有内容,因此我也想包括stdin(其中包含客户端数据)。我不确定该怎么做。
Roel Baardman,

1
这取决于cmd1,cmd2,...是什么。如果它们是nc或cat并且您的数据是面向行的,则输出可能格式错误-您将得到由cmd1打印的行的开头和cmd2打印的行的结尾组成的行。
pizdelect
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.