2

I am writing a program where the parent process needs to be able to communicate with a another child process, so i wrote a function that redirects the child's standard output and input to pipes.

int create_child_piped_io(int* pipe_in, int* pipe_out, const char* program, char* argv[], char* envp[])
{
    int stdin_pipe[2];
    int stdout_pipe[2];
    int child;


    if(pipe(stdin_pipe) < 0)
    {
        return -1;
    }

    if(pipe(stdout_pipe) < 0)
    {
        close(stdin_pipe[0]);
        close(stdin_pipe[1]);
    }
    
    child = fork();
    

    if(child == 0)
    {
        close(stdin_pipe[1]);
        close(stdout_pipe[0]);

        if(dup2(stdin_pipe[0], STDIN_FILENO) < 0)
        {
            close(stdin_pipe[0]);
            close(stdout_pipe[1]);

            exit(errno);
        }

        if(dup2(stdout_pipe[1], STDOUT_FILENO) < 0)
        {
            close(stdin_pipe[0]);
            close(stdout_pipe[1]);

            exit(errno);
        }

        close(stdin_pipe[0]);
        close(stdout_pipe[1]);

        execve(program, argv, envp);

        exit(1);
    }
    else if(child > 0)
    {
        close(stdin_pipe[0]);
        close(stdout_pipe[1]);

        *pipe_in = stdin_pipe[1];
        *pipe_out = stdout_pipe[0];

        return child;
    }
    else
    {
        close(stdin_pipe[0]);
        close(stdin_pipe[1]);
        close(stdout_pipe[0]);
        close(stdout_pipe[1]);


        exit(errno);
    }

    return child;

}


int main()
{
    int input, output;
    char* argv[] = {"program"};
    char* envp[] = {NULL};
    int cpid = create_child_piped_io(&input, &output, "/home/jaden/dev/c++/pipes/program", argv, envp);


    
    char c;


    while(read(output, &c, 1) == 1)
    {
        printf("%c", c);
    }
    printf("done\n");
    int status;
    waitpid(cpid, &status, 0);
    
    close(input);
    close(output);

}

this worked fine but i noticed when writing to the stdout from the child process it wouldn't get sent to the pipe immediately. this is the program the child process would run.

int main()
{
    // i < 1025 gets sent to pipe after sleep(10)
    // i > 1025 send first 1024 bytes to pipe immediately
    for(int i = 0; i < 1024; i++)
    {
        write(1, "a", 1);
    }
    
    sleep(10);
    return 0;
} 

the output would only get sent to the pipe after the child process ended. I tried different amounts of data to send. it turns out every time i write more than 1024 to stdout, it gets sent to the pipe. this leads me to believe that it is being buffered. I don't understand what the why this would be as the pipe is a buffer in memory as well, so it's just buffering the buffer. if this is standard behavior is there any way to turn it off, as i would like to get the data as soon as it is written.

i am using linux mint 20.3.

  • @CharlesDuffy but i dup2(stdout_pipe[1],STDOUT_FILENO) so isn't it no longer buffered? or does it keep the properties of stdout? – Jaden Amatuzzo May 01 '22 at 04:19
  • @CharlesDuffy also i already tried setvbuf, for me stdout has a buffer size of 4096 bytes, not 1024, only when using a pipe does it get set to 1024. – Jaden Amatuzzo May 01 '22 at 04:20
  • Ahh. _That's_ an important detail! Always tell us what you tried early in investigation -- it helps to rule out potential duplicates. (Also, it didn't quite sink in at first glance that you were going straight to the syscall layer instead of through the libc wrappers; that's my failure -- the buffering I was talking about before is a libc feature). – Charles Duffy May 01 '22 at 04:21
  • @DavidC.Rankin I've tested it and my pipe size is 64k. I start trying to read as soon as i create the child process, and it writes to the pupe when writing more than 1024 bytes to stdout, so i don't think it has anything to do with reading. man 7 pipe talks about bugs for before v4.9, i am using version 5.4.0. – Jaden Amatuzzo May 01 '22 at 04:45
  • I can duplicate your results. When up to 1024 bytes are written, the bytes are not written until `sleep()` completes and the program ends (flushing all butters and syncing all open file descriptors). When more than 1024 (and less than 2048) bytes) are written, the first 1024 are written immediately to the pipe, any additional bytes are not written until the writer program terminates. There is no doubt there is a 1024 byte buffer being written to by the writer that isn't available to the pipe until the writer terminates, flushes and syncs -- where and why is indeed a good question. – David C. Rankin May 01 '22 at 05:40
  • Writing "program" to take the number of bytes to write as it's first argument, and including that in the `argv` passed to the child process and called by `execve` makes it convenient to test. Also note your `argv` must have `NULL` as its final pointer (e.g. the array of pointers must be terminated by a `NULL`) See **man 2 execve**. – David C. Rankin May 01 '22 at 05:42

1 Answers1

2

Your problem is here:

    while(read(output, &c, 1) == 1)
    {
        printf("%c", c);
    }

While you are reading with a syscall, you are writing through glibc stdio using printf() connected to a terminal. The output is Line-Buffered. If you change your code to write with a syscall as well, e.g.

    while(read(output, &c, 1) == 1)
    {
        write (1, &c, 1);
    }

The mysterious buffering disappears.

As @ShadowRanger points out, if you need the formatting provided by the stdio.h functions, then you can use setvbuf() to change the normal line-buffering of stdout when connected to a terminal to no buffering with. e.g. setvbuf(stdout, NULL, _IONBF, 0); This will enable output through printf(), etc.. to be used in an unbuffered manner. See man 3 setbuf

David C. Rankin
  • 81,885
  • 6
  • 58
  • 85
  • 1
    Alternative solution is to use `setvbuf` (or one of the other functions in that family) to turn off the C stdio buffers, e.g. with `setvbuf(stdout, NULL, _IONBF, 0);`. Obviously, in the case of `printf("%c", ...);` it's not really doing anything for you that plain `write` wouldn't handle, but if you're doing more complicated stuff, giving up formatted I/O can be a pain, and `setvbuf` means you don't have to. You can also use it to explicitly switch to a smaller fixed size buffer, or line-buffering (for when you're not connected to a terminal but want to pretend you are), etc. – ShadowRanger May 01 '22 at 05:58
  • That's a very good point. Trying to use `write()` alone where field-widths and conversions are needed would become painful quickly. – David C. Rankin May 01 '22 at 06:01
  • One minor note for others: The child's `stdout` is likely block-buffered (because it's not connected to a tty, and most stdio libraries use line-buffering only when it's connected to a tty; block-buffering otherwise); the child process is likely manually flushing or modifying its buffering here so it works, but if it used the default buffering and produced output slowly, you might be stuck waiting for a while before you saw any input. If that's a problem, look at modifying the child with `setvbuf` if you can, or use the GNU coreutils `stdbuf` command line to wrap the invocation. – ShadowRanger May 03 '22 at 17:24