Pipe Gotchas

다음 소개하는 코드는 동작하지 않습니다. child가 파이프로부터 한번에 1 바이트를 읽고 출력합니다. 하지만 메세지를 볼 수 없습니다. 왜 그럴까요?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int main() {
    int fd[2];
    pipe(fd);
    // You must read from fd[0] and write from fd[1]
    printf("Reading from %d, writing to %d\n", fd[0], fd[1]);

    pid_t p = fork();
    if (p > 0) {
        /* I have a child therefore I am the parent*/
        write(fd[1],"Hi Child!",9);

        /*don't forget your child*/
        wait(NULL);
    } else {
        char buf;
        int bytesread;
        // read one byte at a time.
        while ((bytesread = read(fd[0], &buf, 1)) > 0) {
            putchar(buf);
        }
    }
    return 0;
}


부모는 파이프로 H,i,(space),C....등의 바이트를 보냅니다(파이프가 가득 차면 block됩니다). child는 파이프로부터 한 번에 한 바이트씩 읽기 시작합니다. 위의 경우에서 child process는 하나의 character를 읽고 출력할 것입니다. 하지만 while loop를 벗어날 방법이 없습니다! 더 이상 읽을 바이트가 남아있지 않을 때 단순히 block되어 다음을 기다립니다.


putchar 호출은 character를 쓰지만 stdout buffer를 flush하지 않습니다. 예를 들어 하나의 프로세스에서 다른 프로세스로 전송했을 때 아직 출력되지 않습니다. 메세지를 보기 위해서는 fflush(stdout)같이 버퍼를 비워주어야 (또는 printf("\n")을 호출) 합니다. 더 좋은 방법은 아래와 같이 마지막 메세지 마커를 체크해 루프를 벗어나는 것입니다.

        while ((bytesread = read(fd[0], &buf, 1)) > 0) {
            putchar(buf);
            if (buf == '!') break; /* End of message */
        }

이렇게 한다면 메세지는 child process가 종료될 때 버퍼가 비워집니다.



Want to use pipes with printf and scanf? Use fdopen!

POSIX의 file descriptor는 단순히 정수로 표시됩니다. C library level에서 C는 버퍼와 printf나 scanf와 같은 유용한 함수로 래핑해 쉽게 출력하거나 정수나 문자열로 나눌 수 있습니다. 만약 이미 file descriptor가 존재한다면 fdopen을 통해 FILE pointer로 래핑해 사용할 수 있습니다.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
    char *name = "Fred";
    int score = 123;
    int filedes = open("mydata.txt", "w", O_CREAT, S_IWUSR | S_IRUSR);

    FILE *f = fdopen(filedes, "w");
    fprintf(f, "Name:%s Score:%d\n", name, score);
    fclose(f);


파일에 쓰기 위해서 이렇게 하는 것은 불필요합니다. pipe를 통해 이미 file descriptor를 가지고 있으므로 file descriptor를 사용하는 fopen을 사용하면 됩니다. fdopen을 사용하면 file descriptor를 file pointer로 변환할 수 있습니다.


아래는 파이프를 사용해 거의 동작하는 예시입니다. 에러를 찾을 수 있을까요?(힌트: 부모는 아무것도 출력하지 않습니다!)


#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main() {
    int fh[2];
    pipe(fh);
    FILE *reader = fdopen(fh[0], "r");
    FILE *writer = fdopen(fh[1], "w");
    pid_t p = fork();
    if (p > 0) {
        int score;
        fscanf(reader, "Score %d", &score);
        printf("The child says the score is %d\n", score);
    } else {
        fprintf(writer, "Score %d", 10 + 10);
        fflush(writer);
    }
    return 0;
}


자식과 부모 모두 종료되면 파이프 리소스가 사라진다는 것에 주목합시다. 위의 예에서 child는 byte를 전송하고 parent는 파이프에서 받습니다. 하지만, 줄이 끝나는 character가 보내지지 않으므로 fscanf는 계속 다음 바이트를 요구할 것입니다. 이러한 문제를 보장하기 위해서 아래와 같이 new line을 삽입해 주면 해결됩니다.

change:   fprintf(writer, "Score %d", 10 + 10);
to:       fprintf(writer, "Score %d\n", 10 + 10);



So do we need to fflush too?

바이트를 즉시 파이프로 보내고 싶다면 fflush가 필요합니다! file stream이 line buffered된다고 가정하여 C library는 new line character를 만났을 때 flush됩니다.  사실 터미널 스트림에 대해서만 참이 됩니다. 다른 파일스트림에서는 성능을 향상시키기 위해 내부 버퍼가 가득 차거나 파일이 닫힐 때만 flush됩니다.

When do I need two pipes?

두 개의 파이프가 필요한 경우는 child와 데이터를 보내고 받는 것을 비동기적으로 동작하도록 할 때 입니다. 이 때 각각의 방향으로 두 파이프가 필요합니다. 그렇지 않다면 child가 parent에게 갈 데이터를 child가 읽거나 parent가 child에게 갈 데이터를 parent가 읽는 일이 발생합니다.

Closing pipes gotchas

pipe(2) man page를 보면 어떤 프로세스도 listening을 하지 않으면 프로세스들은 SIGPIPE 신호를 받습니다.

If all file descriptors referring to the read end of a pipe have been closed,
 then a write(2) will cause a SIGPIPE signal to be generated for the calling process. 

오직 writer만(독자가 아닌) 이 신호를 사용할 수 있습니다. writer가 파이프를 닫는다고 reader에게 알려주기 위해서는 특별한 바이트를 쓰거나 "Bye"라는 메세지를 써야합니다.


아래의 예는 동작하지 않는 signal catching의 예입니다. 왜 동작하지 않을까요?


#include <stdio.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void no_one_listening(int signal) {
    write(1, "No one is listening!\n", 21);
}

int main() {
    signal(SIGPIPE, no_one_listening);
    int filedes[2];
    
    pipe(filedes);
    pid_t child = fork();
    if (child > 0) { 
        /* I must be the parent. Close the listening end of the pipe */
        /* I'm not listening anymore!*/
        close(filedes[0]);
    } else {
        /* Child writes messages to the pipe */
        write(filedes[1], "One", 3);
        sleep(2);
        // Will this write generate SIGPIPE ?
        write(filedes[1], "Two", 3);
        write(1, "Done\n", 5);
    }
    return 0;
}



위 코드에서 실수는 파이프에 여전히 reader가 있다는 것입니다. child는 여전히 첫 번째 file descriptor를 열어 상세를 기억하고 있습니다. 모든 reader는 closed되어야합니다.

  fork를 할 때 일반적으로 사용하지 않는 각각의 파이프를 close합니다. 예를 들어 parent가 reading end를 닫고 child가 writing end를 닫습니다. (2개의 파이프가 있다면 반대로 똑같이 해줍니다.)


What is filling up the pipe? What happens when the pipe becomes full?

파이프가 가득 차는 경우는 reader가 읽지 않는데 witer가 파이프에 너무 많이 쓰는 경우 일어납니다. 파이프가 가득 차면, read가 일어나기 전까지 write는 fail이 발생합니다. 만약 파이프에 조금의 공간만 남아 전체 메세지를 담기에 부족하면 write는 부분적으로 실패합니다.

일반적으로 이러한 일을 방지하기 위해서는 2가지 방법이 존재합니다. 파이프의 사이즈를 증가시키거나, 더 일반적으로는 프로그램 디자인을 고쳐 파이프가 계속 읽혀지도록 하는 것입니다.

Are pipes process safe?

파이프는 process safe입니다. 즉, 만약 두 프로세스가 같은 파이프에 쓰려고 시도한다면 커널은 파이프에 내부 mutex를 가지고 있어서 lock이 될 것이고, 쓰기가 완료된 다음에 return될 것입니다. 실수하기 쉬운 부분은 파이프가 가득 차려고 할 때, 두 프로세스가 파이프에 쓰려고 시도하려고 할 때 발생합니다. 이 때 파이프가 부분적으로 쓰는 것이 가능할 때 파이프에 쓰는 것은 atomic하게 동작하지 않습니다. 이 점을 주의하세요.

The lifetime of pipes

이름이 지정되지 않은 파이프는 메모리에 존재하고 단순히 데이터나 메시지 전송에 효과적인 IPC입니다. 일단 모든 프로세스가 종료되면, 파이프 리소스도 free됩니다.

unamed pipe의 대안은 mkfifo를 이용해 만들어지는 named pipe입니다.


Posted by 몰랑&봉봉
,