본문 바로가기
카테고리 없음

[System Programming] 10장 파이프 (Unnamed PIPE)

by winter_sunshine 2023. 10. 30.

1.  파이프 (PIPE)

  • 파이프의 개요
    • 두 프로세스 사이에서 한 방향으로 통신할 수 있도록 지원하는 것
    • 쉘에서 "|"기호가  파이프를 의미
    • 쉘에서 파이프 기능한 명령의 표준 출력을 다음 명령에서 표준 입력으로 받아 수행하는 것을 의미
    • $ cat test.c | more
      • 앞에 있는 명령인 cat test.c의 표준 출력을 다음 명령인 more의 표준 입력으로 사용
      • 위 예를 실행하면 test.c를 화면 단위로 출력
      • 파이프는 이름 없는 파이프(익명 파이프)이름 있는 파이프로 구분

 

2.  이름 없는 파이프:  pipe

  • 특별한 수식어 없이 그냥 파이프라고 하면 일반적으로 이름 없는 파이프(익명 파이프)를 의미
  • 이름 없는 파이프는 부모-자식 프로세스 간에 통신을 할 수 있게 함
    • 부모 프로세스에서 fork() 함수를 통해 자식 프로세스를 생성하고, 부모 프로세스와 자식 프로세스 간에 통신하는 것
    • 따라서 ‘부모 프로세스 → 자식 프로세스’ 또는 ‘자식 프로세스 → 부모 프로세스’한 방향을 선택해야 함
    • 파이프를 이용해 양방향 통신을 원할 경우 파이프를 2개 생성해야 함

 

3.  이름 있는 파이프:  FIFO

  • 부모-자식 프로세스 관계가 아닌 독립적인 프로세스들이 파이프를 이용하려면 파일처럼 이름이 있어야 함
  • 이름 있는 파이프특수 파일의 한 종류로, FIFO라고도 함
  • 모든 프로세스가 이름 있는 파이프를 이용해 통신할 수 있음

 

4.  파이프 만들기 :  popen(3)

  • popen() 함수의 특징
    • 다른 프로세스와 통신하기 위해 파이프를 생성
    • 첫 번째 인자인 command에는 셸 명령을, 두 번째 인자인 type에는 “r”이나 “w”를 지정
    • “r”은 파이프를 읽기 전용으로, “w”쓰기 전용으로 실행
    • 내부적으로 fork() 함수를 실행자식 프로세스를 만들고, command에서 지정한 명령을 exec() 함수로 실행해 자식 프로세스가 수행하도록
    • popen() 함수는 자식 프로세스와 파이프를 만들고 mode의 값에 따라 표준 입출력을 연결
    • 리턴값은 파일 포인터로, 파일 입출력 함수에서 이 파일 포인터를 사용하면 파이프를 읽거나 쓸 수 있음
    • popen() 함수는 파이프 생성에 실패하면 널 포인터를 리턴

 

5.  파이프 닫기 :  pclose

  • pclose( ) 함수의 특징
    • pclose() 함수는 파일 입출력 함수처럼 인자로 지정한 파이프를 닫음
    • 관련된 waitpid() 함수를 수행하며 자식 프로세스들이 종료하기를 기다렸다가 리턴
    • pclose() 함수의 리턴 값은 자식 프로세스의 종료 상태
    • 이 함수는 파이프를 닫는 데 실패하면 –1을 리턴

 

◼  [예제 10-1]  popen() 함수로 쓰기 전용 파이프 생성하기

쓰기 전용 파이프 생성: popen()
실행 결과

<예제 10-1 관전 Point>

  • 08행
    • “w” 모드를 사용해 쓰기 전용 파이프를 생성하고 자식 프로세스wc -l 명령을 수행하도록 함
    • wc -l은  입력되는 데이터의 행 수를 출력하는 명령
  • 14~15행
    • 부모 프로세스에서는 반복문을 사용해 문자열을 파이프로 출력
    • 자식 프로세스파이프로 입력되는 문자열을 읽어서 wc -l 명령을 수행

 

◼  [예제 10-2]   popen() 함수로 읽기 전용 파이프 생성하기

읽기 전용 파이프 생성: popen()
실행 결과

<예제 10-2 관전 Point>

  • 08행
    • 자식 프로세스에서는 date 명령을 수행
  • 14행
    • 부모 프로세스에서는 자식 프로세스가 기록한 데이터를 읽고 저장한다.
    • =>  if ( fgets(buf, sizeof(buf), fp) == NULL ) { 
  • 19행
    • fgets(buf, sizeof(buf), fp)에서 저장한 데이터를 출력한다.

 

6.  파이프 만들기 :  pipe(2)

  • pipe( ) 함수의 특징
    • pipe() 함수는 인자크기가 2인 정수형 배열을 받음
    • 이 배열에 파일 기술자 2개를 저장
    • pipefd[0] 읽기 전용으로 열고 pipefd[1]쓰기 전용으로 실행
    • pipe() 함수는 파이프를 생성하는 데 성공하면 0을, 실패하면  –1을 리턴

 

[ pipe() 함수로 통신하는 과정 ]

 

①  pipe() 함수를 호출해 파이프에 사용할 파일 기술자를 얻음

  • 파이프파일의 일종이므로 파일(파이프)을 읽고 쓸 수 있는 파일 기술자가 필요한데, 이를 pipe() 함수가 생성

 

fork() 함수를 수행해 자식 프로세스를 생성

  • 이때 pipe() 함수에서 생성한 파일 기술자자식 프로세스로 복사
  • 같은 파일 기술자부모 프로세스자식 프로세스모두 가지고 있음

 

파이프단방향 통신이므로 통신 방향을 결정

  • 부모 프로세스에서 자식 프로세스통신할 수 있도록 파일 기술자들을 정리
  • 부모 프로세스fd[1]에 쓴 내용을 자식 프로세스 fd[0]에서 읽음
  • 만약 파이프의 쓰기 부분이 닫혀 있다면 파이프에서 읽으려고 할 때 0이나 EOF가 리턴
  • 파이프의 읽기 부분이 닫혀 있다면 파이프에 쓰려고 할 때 SIGPIPE 시그널이 발생

 

◼  [예제 10-3]  pipe() 함수로 통신하기

 

<예제 10-3 관전 Point>

  • 12행
    • if ( pipe(fd) == -1 ) {
      • pipe() 함수를 사용해 파이프를 생성
      • pipe() 함수의 인자로는 파일 기술자를 저장할 배열을 지정
  • 17행
    • switch ( pid = fork() ) {
      • fork() 함수를 사용해 자식 프로세스를 생성
  • 22행 ~ 28행 (자식 프로세스)
    • close(fd[1]);
      • 자식 프로세스에서 파이프를 읽기용으로 사용하겠다는 의미
    • len = read(fd[0], buf, 256);
      • 파이프 입력 부분인 fd[0]에서 문자열을 읽어들이고
      • write(1, buf, len);를 통해 화면에 출력
  • 29행 ~ 34행 (부모 프로세스)
    • close(fd[0]);
      • 부모 프로세스에서 파이프를 출력용로 사용하겠다는 의미
    • write(fd[1], "Test Message\n", 14);
      • fd[1]로 문자열을 출력
    • waitpid(pid, &status, 0);
      • 자식 프로세스가 종료하기를 기다림
  • 실행 결과
    • 부모 프로세스가 출력한 문자열을 자식 프로세스가 받아 출력하고 있음을 알 수 있음

 

◼  [예제 10-4]  pipe() 함수로 명령 실행하기

 

<예제 10-4 관전 Point>

  • 10행
    • if ( pipe(fd) == -1 ) {
      • 파이프를 생성
  • 15행
    • switch ( pid = fork() ) {
      • fork() 함수를 사용해 자식 프로세스를 생성
    • ps 명령의 결과는 기본으로 표준 출력으로 출력되고, grep 명령은 표준 입력을 통해 입력받음
    • 따라서 부모 프로세스와 자식 프로 세스 간의 통신이 표준 입출력 대신 파이프를 통해 이루어지도록 만들어야 함
  • 21행
    • close(fd[1]);
      • 자식 프로세스가 할 일은 부모 프로세스가 파이프로 출력하는 ps -ef 명령의 결과를 받아 grep ssh 명령을 수행하는 것
      • 따라서 파이프의 출력 부분이 필요 없으므로 fd[1]을 닫음
  • 22행 ~ 25행
    • if ( fd[0] != 0 ) {
      • fd[0]의 값이 0이 아니면, 즉 표준 입력이 아니면
    • dup2(fd[0], 0);
      • fd[0]의 값을 표준 입력으로 복사 한 후
    • close(fd[0]);
      • fd[0]을 닫음
      • 이제 자식 프로세스에서는 표준 입력을 fd[0]이 가리키는 파이프에서 읽음
  • 26행
    • execlp("grep", "grep", "ssh", (char *)NULL);
      • 자식 프로세스가 grep 명령을 exec() 함수로 호출
      • 이렇게 하면 grep 명령은 표준 입력을 통해 데이터를 읽어들이려 함
      • 이미 23행( dup2(fd[0], 0); )에서 표준 입력으로 파이프의 입력 파일 기술자를 복사했으므로 결과적으로 파이프를 통해 데이터를 읽어들임
    • 30행
      • close(fd[0]);
        •  부모 프로세스에서는 파이프의 입력 부분이 필요 없으므로 닫음
    • 31 ~ 34행
      • if ( fd[1] != 1 ) {
        • fd[1]의 값이 1이 아니면, 즉 표준 출력이 아니면
      • dup2(fd[1], 1);
        • fd[1]의 값을 표준 출력으로 복사 한 후
      • close(fd[1]);
        • fd[1]을 닫음
        • 이제 부모 프로세스에서는 표준 출력을 fd[1]이 가리키는 파이프에서 출력
    • 35행
      • execlp("ps", "ps", "-ef", (char *)NULL);
        • exec() 함수를 사용해 ps -ef 명령을 실행,
        • ps -ef 명령은 기본으로 표준 출력으로 출력하므로 결과가 파이프로 출력
        • 이 출력 결과를 자식 프로세스가 읽어들임
    • 실행 결과
      • ps –ef | grep ssh 명령이 실행
      • 즉, 부모 프로세스에서 자식 프로세스로 ps –ef 명령의 실행 결과가 전달되었고 자식 프로세스는 여기서 grep ssh 명령을 실행

 

◼  [예제 10-5]  양방향 통신하기

 

<예제 10-5 관점 Point>

  • 08행
    • int fd1[2], fd2[2];
      • 파이프 2개를 생성할 것이므로 파일 기술자 배열을 2개 선언
  • 13행
    • if ( pipe(fd1) == -1 ) {
      • 파이프를 하나 생성, 생성된 파일 기술자 fd1부모 프로세스에서 자식 프로세스로 데이터를 보낼 때 사용
  • 18행
    • if ( pipe(fd2) == -1 ) {
      • 또 다른 파이프를 하나 생성, 생성된 파일 기술자 fd2자식 프로세스에서 부모 프로세스로 데이터를 보낼 때 사용
  • 29 ~ 30행 (자식 프로세스)
    • close(fd1[1]);
      • 자식 프로세스는 부모 프로세스에서 오는 데이터를 읽는 데 사용할 fd1[0]을 남겨 두고  fd1[1]을 닫음
    • close(fd2[0]);
      • 또한 부모 프로세스로 데이터를 보내는 데 사용할 fd2[1]을 남겨두고  fd2[0]을 닫음
  • 39 ~ 40행 (부모 프로세스)
    • close(fd1[0]);
      • 자식 프로세스에 데이터를 보내는 데 사용할 fd1[1]을 남겨둠.
    • close(fd2[1]);
      • 자식 프로세스에서 오는 데이터를 읽는 데 사용할 fd2[0]을 남겨둠
  • 실행결과
    • 부모 프로세스와 자식 프로세스가 데이터를 주고받았음을 알 수 있음
    • 파일 기술자를 여닫는 과정이 다소 복잡하지만 이와 같이 파이프를 2개 생성하면 양방향 통신이 가능