【问题标题】:Using std::cin to read from pipe after dup2()在 dup2() 之后使用 std::cin 从管道中读取
【发布时间】:2015-04-03 01:05:41
【问题描述】:

我只是想编写一个简单的环回函数,该函数在设置管道并使用 dup2() 复制到标准输入和标准输出后在子进程中运行。但是环回在它试图从管道中读取时挂起。管道的写入端——在父进程中——是一个使用 fputs() 的 C 函数。我知道父进程可以工作,因为如果将子环回函数替换为在 C 中使用 read() 的另一个函数,它就可以工作。

一旦我得到这个工作,我就可以用 exec() 替换环回函数,我希望它可以与用 C++ 编写的程序一起工作。

有很多类似的问题,但是像调用 setvbuf() 这样的解决方案对我不起作用(你可以看到我确实在父环回函数中调用它)。其他提问者直接在管道文件描述符上使用 read() (当我这样做时有效——但我想用 C++ 中的 std::cin 测试它)。

所以主函数看起来是这样的:-

int pipeIn[2];  // To be read by child process
int pipeOut[2]; // To be written by child process

#define PARENT_TO_CHILD_READ_END  pipeIn[0]
#define PARENT_TO_CHILD_WRITE_END pipeIn[1]
#define CHILD_TO_PARENT_READ_END  pipeOut[0]
#define CHILD_TO_PARENT_WRITE_END pipeOut[1]

int main(int argc, char** argv) {
    pipe(pipeIn);
    pipe(pipeOut);

    pid_t hijo = fork();

    if (hijo == 0) {
        // CHILD
        dup2(PARENT_TO_CHILD_READ_END, STDIN_FILENO);
        dup2(CHILD_TO_PARENT_WRITE_END, STDOUT_FILENO);
        close(PARENT_TO_CHILD_READ_END);
        close(CHILD_TO_PARENT_WRITE_END);
        close(PARENT_TO_CHILD_WRITE_END);
        close(CHILD_TO_PARENT_READ_END);

        Child_plusplus_Loopback();

    } else if (hijo == -1) {
        perror("fork");
        exit(EXIT_FAILURE);

    } else {
        // PARENT
        close(PARENT_TO_CHILD_READ_END);
        close(CHILD_TO_PARENT_WRITE_END);

        Parent_FILE_Loopback(
                PARENT_TO_CHILD_WRITE_END,
                CHILD_TO_PARENT_READ_END);

        close(PARENT_TO_CHILD_WRITE_END);
        close(CHILD_TO_PARENT_READ_END);
        wait(NULL);
    }

    return 0;
}

回送函数如下所示:-

void
Parent_FILE_Loopback(const int outPipe, const int inPipe) {
    FILE * toChild   = fdopen(outPipe, "w");
    FILE * fromChild = fdopen(inPipe, "r");
    setvbuf(toChild, NULL, _IONBF, 0);

    fputs("Hello", toChild);

    const size_t bufferSize(256);
    char         buffer[ bufferSize ];

    fgets(buffer, bufferSize, fromChild);

    printf("PARENT : %s\n\n", buffer);
}

void
Child_plusplus_Loopback(void) {
    string buffer;

    cin >> buffer; // this hangs

    string message("CHILD : ");
    message += buffer;

    cout << message;
}

strace -f 的输出如下所示:-

clone(Process 6989 attached (waiting for parent)
Process 6989 resumed (parent 6988 ready)
child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb74e5768) = 6989
[pid  6989] dup2(3, 0 <unfinished ...>
[pid  6988] close(3)                    = 0
[pid  6989] <... dup2 resumed> )        = 0
[pid  6988] close(6 <unfinished ...>
[pid  6989] dup2(6, 1 <unfinished ...>
[pid  6988] <... close resumed> )       = 0
[pid  6989] <... dup2 resumed> )        = 1
[pid  6988] fcntl64(4, F_GETFL <unfinished ...>
[pid  6989] close(3 <unfinished ...>
[pid  6988] <... fcntl64 resumed> )     = 0x1 (flags O_WRONLY)
[pid  6989] <... close resumed> )       = 0
[pid  6989] close(6 <unfinished ...>
[pid  6988] brk(0 <unfinished ...>
[pid  6989] <... close resumed> )       = 0
[pid  6988] <... brk resumed> )         = 0x848f000
[pid  6989] close(4 <unfinished ...>
[pid  6988] brk(0x84b0000 <unfinished ...>
[pid  6989] <... close resumed> )       = 0
[pid  6988] <... brk resumed> )         = 0x84b0000
[pid  6989] close(5)                    = 0
[pid  6988] fstat64(4,  <unfinished ...>
[pid  6989] fstat64(0,  <unfinished ...>
[pid  6988] <... fstat64 resumed> {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
[pid  6989] <... fstat64 resumed> {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
[pid  6988] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0 <unfinished ...>
[pid  6989] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0 <unfinished ...>
[pid  6988] <... mmap2 resumed> )       = 0xb77d3000
[pid  6989] <... mmap2 resumed> )       = 0xb77d3000
[pid  6988] _llseek(4, 0,  <unfinished ...>
[pid  6989] read(0,  <unfinished ...>
[pid  6988] <... _llseek resumed> 0xbfefde40, SEEK_CUR) = -1 ESPIPE (Illegal seek)
[pid  6988] fcntl64(5, F_GETFL)         = 0 (flags O_RDONLY)
[pid  6988] fstat64(5, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
[pid  6988] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77d2000
[pid  6988] _llseek(5, 0, 0xbfefde40, SEEK_CUR) = -1 ESPIPE (Illegal seek)
[pid  6988] munmap(0xb77d3000, 4096)    = 0
[pid  6988] write(4, "Hello", 5)        = 5
[pid  6989] <... read resumed> "Hello", 4096) = 5
[pid  6988] read(5,  <unfinished ...>
[pid  6989] brk(0)                      = 0x848f000
[pid  6989] brk(0x84b0000)              = 0x84b0000
[pid  6989] read(0, 0xb77d3000, 4096)   = ? ERESTARTSYS (To be restarted)
[pid  6988] <... read resumed> 0xb77d2000, 4096) = ? ERESTARTSYS (To be restarted)
[pid  6989] --- SIGWINCH (Window changed) @ 0 (0) ---
[pid  6988] --- SIGWINCH (Window changed) @ 0 (0) ---
[pid  6989] read(0,  <unfinished ...>
[pid  6988] read(5, 

【问题讨论】:

    标签: c++ c linux pipe


    【解决方案1】:

    std::cin 读取通常会被 缓冲。这意味着&gt;&gt; 运算符在读取换行符或到达流的末尾之前不会返回。即使 std::cin 在您的情况下碰巧没有被缓冲,流也必须继续尝试读取,直到它看到字符串的结尾,这发生在空格或流的结尾(不一定在当前可用字节的末尾)。在任何情况下,您都无需担心底层read() 调用的细节。这是 C++ 库实现的责任。

    您的服务器将“Hello”的五个字符写入管道,但fputs() 不会自动在它们后面加上换行符(与puts() 不同)或其他任何字符。如果你想发送一个换行符——与读取端的行缓冲很好地互操作——那么你必须明确地发送它:

    fputs("Hello\n", toChild);
    

    fputs("Hello", toChild);
    fputc('\n', toChild);
    

    即使读取端没有缓冲,您也需要至少发送一个空格或制表符,以便读取器能够识别字符串的结尾。只要你需要这样做,你最好使用换行符。

    无论如何,如果输出流被缓冲,那么您可能希望使用fflush(toChild) 跟进,但由于您明确将其设为无缓冲(不一定是明智的选择),以上内容应该足以使客户端的读取返回。

    请注意,类似的考虑适用于孩子发回给父母的消息:fgets() 读取到换行符或 EOF,并且看起来孩子没有用换行符终止其回复消息.或冲洗。

    【讨论】:

    • 谢谢你的作品。 '\n' 成功了。如果我删除 setvbuf() 行,那么我需要你提到的 fflush()。
    • iostreams 上没有行缓冲,至少不是根据 C++ 标准。 (某些实现确实转发到FILE* 函数,而这些函数又可能是行缓冲的。这是支持sync_with_stdio 的最简单方法。但是,我不会指望它。)
    • @JamesKanze:很公平。我稍微修改了我的答案。特别是,我已经强调了我现在意识到的基本问题,这不是行缓冲本身,而是在字符串之后有某种空格,以便流识别它的结尾。
    • 是的。在输入时,您需要一些东西来使&lt;&lt; 返回。在输出时,您需要冲洗。
    【解决方案2】:

    来自std::cin(或FILE*)的任何读取通常都会被缓冲, 这意味着对read() 的调用将请求大量 字节。系统安排从控制台读取甚至返回 如果读取的字节数较少,但这不适用于从 管道;为填充缓冲区而调用的read 只会在以下情况下返回 缓冲区已满或管道的写入端已关闭。

    您可以通过调用std::cin.setbuf 来控制缓冲, 但这可能很棘手;通常,它应该在第一次之前完成 来自std::cin 的输入。如果您使用&gt;&gt; 输入字符串, std::cin 将继续调用 read 直到它看到空白(或 文件结束)。

    编辑:

    目前还不清楚您要做什么。如果你需要一个 基于消息的协议,那么你需要的不仅仅是管道 和 iostream;这两个都是面向流的,而不是面向消息的。

    我过去处理这个问题的常用方法是写下 消息(比如一个四字节整数),然后是消息。这 如果管道上只有一个读取器和一个写入器,则相当简单; 有了更多的作家,你必须确保每次写入的长度加上 消息是原子的,如果有几个则不能真正完成 阅读器——您需要为每个阅读器使用单独的管道。对于文本 格式化,可以使用std::ostringstreamstd::istringstream; 写入,将std::ostringstream 中的数据转换为字符串,然后 那么:

    void
    writeOneMessage( int fd, std::string const& message )
    {
        std::size_t size = message.size();
        char sizeBuffer[4] = 
        {
            (size >> 24) & 0xFF,
            (size >> 16) & 0xFF,
            (size >>  8) & 0xFF,
            (size      ) & 0xFF
        }
        write( fd, sizeBuffer, 4 );
        write( fd, message.data(), message.size() );
    }
    

    阅读要复杂得多,因为有许多 您必须检查的其他错误情况:

    std::string
    readOneMessage( int fd )
    {
        char sizeBuffer[4];
        if ( read( fd, sizeBuffer, 4 ) != 4 ) {
            //  Really too simple: if you read 0, it's end of file
            //  if you read anythong other than 0 or 4, it's a serious
            //  error.
        }
        size_t size = ((sizeBuffer[0] & 0xFF) << 24)
                    | ((sizeBuffer[1] & 0xFF) << 16)
                    | ((sizeBuffer[2] & 0xFF) <<  8)
                    | ((sizeBuffer[3] & 0xFF)      );
        std::string message( size );
        if ( read( fd, &message[0], size ) != size ) {
            //  Can only be a format error...
        }
        return message;
    }
    

    同样,一旦您阅读了该消息,您就可以使用它来构建一个 std::istringstream,并按照您的意愿进行解析。

    这确实是您可以可靠地实现基于消息的唯一方法 带有管道的协议;另一种方法是写一个'\0' 每条消息的终止字符串,并逐字节读取,直到找到 '\0'。 (实际上,使用FILE*,将所有流设置为 行缓冲,通常大部分时间都可以工作,前提是消息 足够小,但并不能真正保证,也不可靠。)

    【讨论】:

    • 这是否意味着管道方法完全错误?我不想每次写东西时都关闭管道,因为它需要再次创建。如果这些管道不是办法,你能建议别的吗,我会试试的。
    • 您对 read(2) 行为的描述似乎与其规范不符。当在未设置为非阻塞模式的文件描述符上调用该函数时,需要阻塞直到至少传输一个字节或检测到 EOF,但绝不需要读取之前指定的全部字节数返回。
    • 好吧,在这种情况下,我的 read() 解决方案是不可移植的。然而,即使它是偶然工作的,它也确实(幸运地)显示了父环回函数工作。而且,事实上,这就是它的全部目的。我真的很想专注于 std::cin。我认为对 pubsetbuf(nullptr,0) 的调用——我刚刚在 Josuttis 中找到它——不会在对 exec() 的调用中存活下来,这是对的吗?
    • @Alex,我现在正在写一个答案,可能会澄清一些事情。您所做的事情本身并没有什么不可移植性——只是不太正确。
    • @JohnBollinger 有一个通用规范read,非常开放,可以支持各种设备,还有pipe的要求。写入pipe 到一定大小必须是原子的,实际上,如果有一个该大小的read 打开,它也将被原子地服务。但是你说得对,一般来说,管道(也不是 TCP 套接字)不直接支持基于消息的协议。
    【解决方案3】:

    正确设置文件描述符后,您需要为通信定义一个协议。

    使用 std::cout/std::cin: 的管道:

    #include <iostream>
    #include <cstdio>
    #include <unistd.h>
    #include <sys/wait.h>
    
    
    int parent_pipe[2];
    int child_pipe[2];
    
    void parent_loop() {
        std::cout << "Hello Child" << ' ' << "Disconnect" << std::endl;
        std::string receive("Parent Receives: ");
        while(true) {
            std::string buffer;
            if( ! (std::cin >> buffer) || buffer == "Disconnect")
                break;
            receive += buffer;
        }
        std::cout << "Disconnect";
        std::cerr << receive << std::endl;
    }
    
    void child_loop() {
        std::string receive(" Child Receives: ");
        while( true) {
            std::string buffer;
            if( ! (std::cin >> buffer) || buffer == "Disconnect")
                break;
            receive += buffer;
        }
        std::cout << "Hello Parent" << std::endl;
        std::cout << "Disconnect" << std::endl;
        std::cerr << receive << std::endl;
    }
    
    
    int main(int argc, char** argv) {
        pipe(parent_pipe);
        pipe(child_pipe);
    
        pid_t pid = fork();
    
        if (pid == -1) {
            perror("fork");
            return EXIT_FAILURE;
        }
        else if (pid) {
            // Parent
            if((dup2(child_pipe[0], STDIN_FILENO) == -1)
            || (dup2(parent_pipe[1], STDOUT_FILENO) == -1))
            {
                std::cerr << "Setup Failure\n";
                return EXIT_FAILURE;
            }
            close(child_pipe[1]);
            close(parent_pipe[0]);
    
            parent_loop();
    
            close(child_pipe[0]);
            close(parent_pipe[1]);
        }
        else {
            // Child
            if((dup2(parent_pipe[0], STDIN_FILENO) == -1)
            || (dup2(child_pipe[1], STDOUT_FILENO) == -1))
            {
                std::cerr << "Setup Failure\n";
                return EXIT_FAILURE;
            }
            close(parent_pipe[1]);
            close(child_pipe[0]);
    
            child_loop();
    
            close(parent_pipe[0]);
            close(child_pipe[1]);
        }
        return 0;
    }
    

    请注意使用std::endl 将分隔符放入流中并刷新流。 如果你想要未格式化的 IO,你可以使用读写函数,而不是(一个孩子可能会读取父级写入的块,直到 EOF)。 注意:日志和错误消息转到 std::cerr (STDERR_FILENO)

    输出:

     Child Receives: HelloChild
    Parent Receives: HelloParent
    

    【讨论】:

    • 谢谢。我应该更多地关注传输端的消息描述以及 std::cin 查找的内容。
    猜你喜欢
    • 1970-01-01
    • 2010-11-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-31
    • 2021-02-28
    • 2013-09-04
    相关资源
    最近更新 更多