Por que pareço perder dados usando essa construção de tubo bash?

11

Estou tentando combinar alguns programas como esse (ignore todas as inclusões extras, esse é um trabalho pesado em andamento):

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

Onde a fonte do programa de repetição é a seguinte:

#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;
}

Eu notei isso:

  • Quando apenas uso o cachimbo ./repeat, tudo funciona como pretendido.
  • Quando apenas uso a substituição do processo, tudo funciona como pretendido.
  • Quando encapsulo o pv usando substituição de processo, tudo funciona como pretendido.
  • No entanto, quando uso a construção específica, pareço perder dados (caracteres individuais) do stdin!

Eu tentei o seguinte:

  • Eu tentei desativar o buffer no canal entre pve ./repeatusando stdbuf -i0 -o0 -e0em todos os processos, mas isso não parece funcionar.
  • Troquei epoll por pesquisa, não funciona.
  • Quando olho para o fluxo entre pve ./repeatcom tee stream.csv, isso parece correto.
  • Eu costumava stracever o que estava acontecendo, e vejo muitas leituras de byte único (conforme o esperado) e elas também mostram que os dados estão faltando.

Me pergunto o que está acontencedo? Ou o que posso fazer para investigar mais?

Roel Baardman
fonte

Respostas:

16

Porque o nccomando dentro <(...)também lerá de stdin.

Exemplo mais simples:

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

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

Para onde foi text? Através do netcat.

$ cat /tmp/foo
text

Seu programa e ncconcorra pelo mesmo estilo, e ncobtém parte dele.

mosvy
fonte
Você está certo! Obrigado! Você pode sugerir uma maneira limpa de desconectar o stdin no <(...)? Existe uma maneira melhor do que <( 0<&- ...)?
Roel Baardman
5
<(... </dev/null). não use 0<&-: fará com que o primeiro open(2)retorne 0como o novo fd. Se o seu ncsuporte, você também pode usar a -dopção
mosvy
3

epoll () ou poll () retornando com E / POLLIN apenas informará que uma única leitura () pode não ser bloqueada.

Não que você consiga executar muitos bytes de leitura () até uma nova linha, como você faz.

Digo maio porque um read () após epoll () retornado com E / POLLIN ainda pode bloquear.

Seu código também tentará ler o EOF passado e ignora completamente todos os erros de leitura ().

pizdelect
fonte
Apesar de não ser uma solução direta para o meu problema, obrigado por fazer o comentário. Percebo que esse código possui falhas e a detecção EOF está presente em uma versão menos simplificada (através do uso de POLLHUP / POLLNVAL). Eu luto para encontrar uma maneira sem buffer de ler linhas de vários descritores de arquivos. Meu repeatprograma está processando essencialmente dados NMEA (baseados em linhas e sem indicadores de comprimento) de várias fontes. Como estou combinando dados de várias fontes ativas, gostaria que minha solução fosse sem buffer. Você pode sugerir uma maneira mais eficiente de fazer isso?
Roel Baardman
fwiw, fazer uma chamada do sistema (leitura) para cada byte é a maneira menos eficiente possível. A verificação do EOF pode ser feita apenas verificando o valor de retorno da leitura, sem necessidade de POLLHUP (e POLLNVAL será retornado apenas quando você passar um fd falso, não no EOF). Mas de qualquer maneira, fique atento. Eu tenho a idéia de um ypeeutilitário que lê vários fds e os mistura em outro fd, preservando registros (mantendo as linhas intactas).
Pizdelect
Notei que essa construção do bash deve fazer isso, mas não sei como combinar o stdin: o { cmd1 & cmd2 & cmd3; } > filearquivo conterá o que você descreve. No entanto, no meu caso, estou executando tudo a partir do tcpserver (3), então quero incluir o stdin (que contém os dados do cliente) também. Não sei bem como fazer isso.
Roel Baardman 12/06/19
1
Depende do que cmd1, cmd2, ... são. Se eles são nc ou cat e seus dados são orientados a linhas, a saída pode estar malformada - você obterá linhas consistindo no início de uma linha impressa por cmd1 e no final de uma linha impressa em cmd2.
pizdelect