Pourquoi semble-t-il que je perds des données en utilisant cette construction de tuyau bash?

11

J'essaye de combiner quelques programmes comme ça (veuillez ignorer les inclusions supplémentaires, c'est un travail en cours):

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

Où la source du programme répété se présente comme suit:

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

J'ai remarqué ceci:

  • Quand j'utilise le tuyau ./repeat, tout fonctionne comme prévu.
  • Lorsque j'utilise simplement la substitution de processus, tout fonctionne comme prévu.
  • Lorsque j'encapsule pv en utilisant la substitution de processus, tout fonctionne comme prévu.
  • Cependant, lorsque j'utilise la construction spécifique, je semble perdre des données (caractères individuels) de stdin!

J'ai essayé ce qui suit:

  • J'ai essayé de désactiver la mise en mémoire tampon sur le canal entre pvet l' ./repeatutilisation stdbuf -i0 -o0 -e0de tous les processus, mais cela ne semble pas fonctionner.
  • J'ai troqué epoll pour sondage, ça ne marche pas.
  • Lorsque je regarde le flux entre pvet ./repeatavec tee stream.csv, cela semble correct.
  • J'avais l'habitude stracede voir ce qui se passait, et je vois beaucoup de lectures à un octet (comme prévu) et elles montrent également que des données manquent.

Je me demande ce qui se passe? Ou que puis-je faire pour enquêter davantage?

Roel Baardman
la source

Réponses:

16

Parce que la nccommande à l'intérieur <(...)lira également depuis stdin.

Exemple plus simple:

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

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

Où est-il textallé? Par le netcat.

$ cat /tmp/foo
text

Votre programme et ncconcourir pour le même stdin, et en ncobtient une partie.

mosvy
la source
Vous avez raison! Merci! Pouvez-vous suggérer un moyen propre de déconnecter stdin dans le <(...)? Y a-t-il une meilleure façon que <( 0<&- ...)?
Roel Baardman
5
<(... </dev/null). ne pas utiliser 0<&-: cela fera open(2)revenir le premier en 0tant que nouveau fd. Si vous le ncsupportez, vous pouvez également utiliser l' -doption.
mosvy
3

epoll () ou poll () renvoyant avec E / POLLIN ne vous diront qu'un seul read () ne peut pas bloquer.

Ce n'est pas que vous puissiez faire beaucoup de lecture d'un octet jusqu'à une nouvelle ligne, comme vous le faites.

Je dis mai car un read () après epoll () retourné avec E / POLLIN peut toujours bloquer.

Votre code essaiera également de lire au-delà de l'EOF et ignorera complètement les erreurs de lecture ().

pizdelect
la source
Bien que ce ne soit pas une solution directe à mon problème, merci d'avoir fait le commentaire. Je me rends compte que ce code a des défauts, et la détection EOF est présente dans une version moins allégée (grâce à l'utilisation de POLLHUP / POLLNVAL). J'ai du mal à trouver un moyen sans tampon de lire les lignes de plusieurs descripteurs de fichiers. Mon repeatprogramme traite essentiellement des données NMEA (basées sur des lignes et sans indicateur de longueur) provenant de plusieurs sources. Étant donné que je combine des données provenant de plusieurs sources en direct, j'aimerais que ma solution soit sans tampon. Pouvez-vous suggérer un moyen plus efficace de procéder?
Roel Baardman
fwiw, effectuer un appel système (lecture) pour chaque octet est le moyen le moins efficace possible. La vérification EOF peut être effectuée en vérifiant simplement la valeur de retour de read, pas besoin de POLLHUP (et POLLNVAL ne sera retourné que lorsque vous lui passerez un faux fd, pas sur EOF). Mais de toute façon, restez à l'écoute. J'ai l'idée d'un ypeeutilitaire qui lit à partir de plusieurs fds et les mélange dans un autre fd, tout en préservant les enregistrements (en gardant les lignes intactes).
pizdelect
J'ai remarqué que cette construction bash devrait faire cela, mais je ne sais pas comment y combiner stdin: le { cmd1 & cmd2 & cmd3; } > filefichier contiendra ce que vous décrivez. Cependant, dans mon cas, j'exécute tout depuis tcpserver (3), donc je veux également inclure stdin (qui contient les données client). Je ne sais pas comment faire ça.
Roel Baardman
1
Cela dépend de ce que sont cmd1, cmd2, .... S'ils sont nc ou cat et que vos données sont orientées ligne, la sortie peut être mal formée - vous obtiendrez des lignes composées du début d'une ligne imprimée par cmd1 et de la fin d'une ligne imprimée par cmd2.
pizdelect