상세 컨텐츠

본문 제목

UNIX - Pipes system call [pipe], Blocking & Non-Blocking, Library [popen, pclose]

Computer Science/UNIX

by 2021. 11. 6. 10:57

본문

반응형

Inter-process communication using pipes

프로그램을 한 가지만 잘해야한다. 그리고 이 프로그램들이 함께 작동해야한다. 프로그램들은 text stream을 주고받아야한다 (IPC : Inter Process Communication). 이것이 UNIX 철학이다.

Pipes

가장 간단한 UNIX IPC 시스템이다. 

who는 시스템 내의 유저 이름을 출력하는 프로그램이다. 

temp파일에 who를 통해 user name을 입력한다.

wc는 word count로 단어 갯수를 세는 프로그램이다.

temp의 내용을 wc를 통해 user name 개수를 센다. 

위 명령어를 통해서 시스템 안에 유저 수를 센다. 

 

이러한 작업을 중간과정으로 temp파일을 이용했지만, temp를 이용하지 않고 하는 방법이 pipe이다. 

 

그림과 같이 who에서 pipe로 write(1)을, pipe에서 wc로 read(0)을 실행한다. 

shell이 pipe를 통해 who와 wc의 중간에서 서로를 연결시켜 communication하도록 해준다. 

who와 wd를 묶어주며 새로운 group을 생성하여 할당한다.

who의 write fd는 pipe write, wc의 read fd는 pipe read임을 확인할 수 있다.

 

 

The pipe(2) system call

#include <unistd.h>
int pipe(int filedes[2]);
// Returns: 0 if OK, -1 on error

filedes[0]이 read, filedes[1]이 write이다. 

pipe는 먼저 들어간 것을 먼저 읽는 FIFO로 작동한다.

pipe에서는 lseek가 작동하지 않는다. 

pipe는 half duplex(단방향)이며(full duplex 양방향으로 사용할 수 있으나 그러지 않음)

parent와 child 사이에서 사용된다. 즉 pipe변수를 공유할 수 있는 모든 프로세스 간에 사용된다. 

 

process가 pipe에서 read할 때

  • pipe가 비어있지 않으면, read를 즉시 반환한다. 
  • pipe가 비어있으면, pipe로 write가 진행 될 때 까지 read를 block한다.

process가 pipe에서 write할 때

  • pipie가 full이 아니면, write를 즉시 반환한다.
  • pipe가 full이면, 공간이 생길 때 까지 write를 block한다.

pipe 의 한쪽 끝이 닫혔을 경우

- write 쪽이 닫혔을 때

  • 모든 데이터가 read된 후 EOF을 나타내면 read는 0을 반환한다.

- read 쪽이 닫혔을 때

  • kernel에서 SIGPIPE signal을 전달
  • signal을 ignore하거나, catch하고 signal handler로 처리하지 않으면, write는 실패하여 -1을 return하고 errno로 EPIPE가 설정된다. 

 

example

char *msg1 = "hello, world #1";
char *msg2 = "hello, world #2";
char *msg3 = "hello, world #3";

main(){
   char inbuf[MSGSIZE];
   int p[2], j;

   /* 파이프를 개방한다 */
   pipe(p);

   /* 파이프에 쓴다 */
   write(p[1], msg1, MSGSIZE);
   write(p[1], msg2, MSGSIZE);
   write(p[1], msg3, MSGSIZE);

   /* 파이프로부터 읽는다. */
   for(j = 0; j < 3; j++){
      read (p[0], inbuf, MSGSIZE);
      printf ("%s\n", inbuf);
   }
   exit (0);
}

child를 만들지 않고 스스로가 pipe가 되어 아래 그림처럼 작동하는 코드이다. 

두 개의 pipe p를 만들고 pipe()로 개방한다. 

파이프에 쓰고 읽는 작업을 진행한다. 

 

example

char *msg1 = "hello, world #1";
char *msg2 = "hello, world #2";
char *msg3 = "hello, world #3";
main(){
   char inbuf[MSGSIZE]; /* MSGSIZE 16 */
   int p[2], j;
   pid_t pid;
   pipe(p);

   switch (pid = fork()){
   case -1: perror ("fork call");
   exit (2);
   case 0:  /* 자식일 경우 파이프에 쓴다. */
      write (p[1], msg1, MSGSIZE);
      write (p[1], msg2, MSGSIZE);
      write (p[1], msg3, MSGSIZE);
      break;
   default: /* 부모일 경우 파이프로부터 읽는다. */
      for (j = 0; j < 3; j++){
         read (p[0], inbuf, MSGSIZE);
         printf ("%s\n", inbuf);
      }
      wait (NULL);
  }
  exit (0);
}

p[0]이 pipe의 read, p[1]이 pipe의 write 역할로 child에서는 p[1]을 통해 write하고, parent는 p[0]을 통해 read한다.

위 코드의 경우 parent는 read만 하고, child는 write만 한다. parent도 child도 read write를 모두 할 수는 있지만, 그렇게 되면 confusion이 일어난다. 

따라서 각 process가 read만 혹은 write만 담당하도록 하고, 다른 필요하지 않은 쪽의 file descriptor는 close 한다.

 

만약 다른쪽을 close 하지 않는다면 deadlock이 발생할 수 있다.

child가 종료하였을 때, parent의 file descriptor [4] 즉 pipe write가 open되어있다면, parent는 무한정 block되고 데이터를 기다리게된다. 

 

 

char *msg1 = "hello, world #1";
char *msg2 = "hello, world #2";
char *msg3 = "hello, world #3";
main(){
   char inbuf[MSGSIZE]; /* MSGSIZE 16 */
   int p[2], j;
   pid_t pid;
   pipe(p);

   switch (pid = fork()){
   case -1: perror ("fork call");
   exit (2);
   case 0:  /* 자식일 경우 파이프에 쓴다. */
      write (p[1], msg1, MSGSIZE);
      write (p[1], msg2, MSGSIZE);
      write (p[1], msg3, MSGSIZE);
      break;
   default: /* 부모일 경우 파이프로부터 읽는다. */
      for (j = 0; j < 3; j++){
         read (p[0], inbuf, MSGSIZE);
         printf ("%s\n", inbuf);
      }
      wait (NULL);
  }
  exit (0);
}

코드를 다시 보자.

parent가 사용하지 않는 pipe write p[1]을 close하지 않았다고 하자.

child에서 write는 3번하지만 parent에서 read를 4번 한다고 하면,

child는 종료했지만 4번째로 오지 않는 데이터를 무한정 기다린다.

 

본래 write가 close되면 read는 return 0을 즉시하지만, child의 write가 닫혀있어도 parent 자신의 write가 열려있으므로 parent의 read는 끝없이 데이터를 기다리는 상황이 생긴다.

parent에서 write p[1]을 close하지 않으면 이와 같은 상황이 발생한다.

 

따라서 아래와 같이 close를 포함한 코드를 작성해야한다. 

main(){
   char inbuf[MSGSIZE];
   int p[2], j;
   pid_t pid;

   pipe(p);

   switch (pid = fork()){
   case -1: perror ("fork call"); exit (2);
   case 0:
      close (p[0]);
      write (p[1], msg1, MSGSIZE);
      write (p[1], msg2, MSGSIZE);
      write (p[1], msg3, MSGSIZE);
      break,
   default:
      close (p[1]);
      for (j = o; j < 3; j++){
         read (p[0], inbuf, MSGSIZE);
         printf ("%s\n", inbuf);
      }
      wait (NULL);
 }
 exit (0);
}

 

 

The size of a pipe

pipe size는 제한이 없지만, POSIX에서는 512 byte이고, 대부분의 시스템에서는 더 크다.

kernel의 pipe buffer size는 limit.h의 PIPE_BUF 상수에 size가 정의되어 있다. 

 

pipe가 full이 되면, 다른 process로부터 pipe를 read할 수 있는 공간이 생길 때까지 write가 block 된다. 

 

multiple process환경에서 PIPE_BUF보다 같거나 작은 크기로 write하면, 서로 번갈아가며 데이터가 write되는 interleaved가 발생하지 않는다. (interleaved되면 좋지 않음)

PIPE_BUF보다 큰 사이즈로 write할 경우, 다른 writer와 데이터가 interleaved된다. 

 

example

int count;
void alrm_action (int signo) {
 printf ("write blocked after %d characters\n", count);
 exit (0);
}
main(){
  int p[2];
  int pipe_size;
  char c = 'x';
  static struct sigaction act;

  act.sa_handler = alrm_action;
  sigfillset (&(act.sa_mask));
  pipe(p);
  pipe_size = fpathconf (p[0], _PC_PIPE_BUF);
  printf ("Maximum size of write to pipe: %d bytes\n", pipe_size);
  sigaction (SIGALRM, &act, NULL);

  while (1) {
    alarm (20);
    write(p[1], &c, 1);
    alarm(0);
    if ((++count % 1024) == 0) printf ("%d characters in pipe\n", count);
  }

SIGALRM을 받았을 때, alarm action으로 count 값을 출력하는 코드이다. 

최대 pipe size인 _PC_PIPE_BUF를 출력한다. 

20초마다 1byte씩 pipe에 write하고 1024byte단위로 출력한다.

write를 진행하다 pipe의 공간이 꽉찼을 경우 write가 block되어 alarm 시간이 만료되어서 SIGALRM을 전달하여 alrm_action이 실행된다. 

 

실행 결과는 다음과 같다.

Maximum size of write to pipe: 32768 bytes
1024 characters in pipe
2048 characters in pipe
3072 characters in pipe
4096 characters in pipe
5120 characters in pipe
.
.
.
31744 characters in pipe
32768 characters in pipe
write blocked after 32768 characters

 

 

Blocking & Non-Blocking

read는 읽어올 데이터가 없을 경우에 block 된다. (pipes, terminal devices, network devices 등에서)

write는 접근하여 가져올 데이터가 없을 경우 block된다. (pipe에 공간이 없을 때, network flow control 등)

FIFO를 open할 때 특정 조건에서는 block되는 경우도 있다. 

 

위와 같은 상황에서도 nonblocking I/O를 지정하는 2가지 방법은 다음과 같다.

  • file descriptor를 open하는 경우에도 O_NONBLOCK flag를 설정하면 block되지 않는다. 
  • 이미 open된 file descriptor에 대해서 read, write시 block이 될 수 있는데, 
    file fcntl로 O_NONBLOCK flag를 설정해주면 block되지 않는다.

 

Non-blocking reads and writes

fcntl로 다음과 같이 O_NONBLOCK flag를 설정하면, 

if(fcntl(p[1], F_SETFL, O_NONBLOCK)==-1)
    perror(“fcntl”);

write에서 read가 open되어있고, pipe가 꽉 차있더라도 block되지 않고 return 한다.

 

pipe가 full일 때 

fcntl(p[1], F_SETFL, O_NONBLOCK)

위와 같이 설정하면 이후의 write는 block되지 않고, -1을 return하며, errno로 EAGAIN을 설정한다.

 

pipe가 empty일 때

fcntl(p[0], F_SETFL, O_NONBLOCK)

위와 같이 설정하면 read는 block되지 않고, -1을 return하며, errno로 EAGAIN을 설정한다. 

 

example

#include <fcntl.h>
#include <errno.h>
#define MSGSIZE 6
 
int parent (int *);
int child (int *);

char *msg1 = "hello";
char *msg2 = "bye!!";

main(){
 int pfd[2];

 if(pipe (pfd) == -1)
  fatal ("pipe call");

 if(fcntl(pfd[0],F_SETFL, O_NONBLOCK)==-1)
  fatal ("fcntl call");

 switch(fork()){
 case -1: /* 오류 */
  fatal("fork call");
 case 0:	 /* 자식 */
 child(pfd);
 default: /* 부모 */
 parent (pfd);
 }
}

int parent (int p[2]){
  int nread;  char buf[MSGSIZE]; 
  close (p[1]);
 
 for(;;){
 switch (nread=read(p[0],buf,MSGSIZE)){
    case -1:
      if (errno == EAGAIN){ 
        printf ("(pipe empty)\n");
        sleep (1);
        break;
        } else fatal ("read call");
    case 0: 
      printf ("End of conversation\n");
      exit (0);
    default: printf ("MSG=%s\n", buf);
    }
  }
}

int child(int p[2]){
  int count;
  close (p[0]);
  for (count= 0; count < 3; count++){
    write (p[1], msg1, MSGSIZE);
    sleep(3);
  }
  write (p[1], msg2, MSGSIZE); exit (0);
}

child function은 pipe에 write하고 3초간 sleep하는 과정을 3번 반복하고 write를 한 번 더 진행한다. 

parent function은 pipe에서 read를 반복해서 실행하는데,

main에서 read에 대해서는 O_NONBLOCK설정을 해주었다. 

 

read를 통해 block이 발생하지 않고 -1을 반환하므로 pipe empty를 출력하고 다시 반복하며, 

return 0이 나오는 경우는 write가 close되었다는 의미이다. 

write가 open되어있는 상태임에도 pipe가 empty라면 return -1을 하는 것이다. 

 

결과는 다음과 같다

MSG=hello
(pipe empty)
(pipe empty)
(pipe empty)
MSG=hello
(pipe empty)
(pipe empty)
(pipe empty)
MSG=hello
(pipe empty)
(pipe empty)
(pipe empty)
MSG=bye!!
End of conversation

child에서 3초 간격으로 write할 때 read로 데이터를 읽으므로 MSG=hello가 출력되고, 

나머지 1초 간격으로는 write시 pipe가 비어있으므로 pipe empty를 출력한다. 

마지막으로 MSG=bye!!메시지를 child가 write한 후 child는 종료하므로 parent에서 read했을 때 write가 close이기 때문에 End of conversation이 출력된다. 

 

 

The popen(3) & pclose(3)

#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
// Returns: file pointer if OK, NULL on error

int pclose(FILE *fp);
// Returns: termination status of cmdstring, or -1 on error

위 두 function이 다음과 같은 복잡한 과정을 대신 처리해준다.

 

- popen

  • creating a pipe,
  • forking a child,
  • closing the unused ends of the pipe,
    사용하지 않는 pipe close
  • executing a shell to run the command,
    child가 command 실행하도록

- pclose

  • waiting for the command to terminate.
    child가 종료될 때 까지 기다리는 역할

 

popen 라이브러리는 fork와 exec가 cmdstring을 실행하도록 해준다. 

또한 두 번째 매개변수 type이 'r'인 경우 pipe에 read로, 'w'인 경우 pipe에 write로 연결해준다.

 

example

int main(){
    FILE *read_fp;
    char buffer[BUFSIZ + 1];
    int chars_read;
    memset(buffer, '\0', sizeof(buffer));
    read_fp = popen("uname -a", "r");
    if (read_fp != NULL) {
        chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        if (chars_read > 0) {
            printf("Output was:-\n%s\n", buffer);
        }
        pclose(read_fp);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}

popen으로 'r'설정하여 실행하면, parent에게 pipe read를 연결해주고, child가 pipe write로 보낼 수 있도록 연결해준다. 

read_fp가 실패하면 NULL이 저장된다. (라이브러리이므로 실패시 -1이 아닌 NULL저장)

fread를 통해서 read_fp의 내용을 읽어 buffer에 저장하는데, fread는 sizeof(char)로 데이터의 기본 단위를 지정하고, BUFSIZ로 크기를 설정한다. 총 byte수는 기본 단위와 BUFSIZ의 곱으로 결정된다. 

 

fread로 읽은 데이터의 수가 chars_read에 저장된다. 

popen으로 데이터를 읽는 것은 child의 내용을 pipe를 통해 전달받아 read_fp로 읽은 것이다.

pclose를 통해 child가 exit할 때까지 기다리고 나서 exit를 수행한다. 

 

결과는 다음과 같다. 

$./popen
Output was:-
Linux sloven 2.6.18-8.el5PAE #1 SMP Tue Jun 5 23:39:57 EDT 2007 i686 i686 i38
6 GNU/Linux

uname -a 실행에 대한 결과가 출력된다.

반응형

관련글 더보기