상세 컨텐츠

본문 제목

UNIX - FIFO, I/O Multiplexing, select system call

Computer Science/UNIX

by 2021. 11. 11. 22:06

본문

반응형

FIFO

named pipes라고 불리는 pipe의 종류이다. 

pipe는 공통 조상이 pipe를 생성했을 때 관련된 process 사이에서만 사용할 수 있지만, 
FIFO는 관련없는 process 사이에서 데이터 교환이 가능하다. 

FIFO는 owner가 있고, 관련 access permission이 필요하다. 

open, close할 수 있고, 다른 file처럼 delete도 할 수 있다.

read only 또는 write only로만 open해야한다. 마찬가지고 FIFO도 half-duplex이다. 

S_ISFIFO 매크로를 통해서 test 할 수 있다.

 

$ /etc/mknod channel p
$ ls –l channel
prw-rw-r–  ben  usr  0  Aug  1  21:05  channel

mknod 명령어로 channel이라는 FIFO를 생성하였다. FIFO도 pipe이므로 file type이  'p'임을 알 수 있다. 

 

pipe 와 작동하는 방식은 같다.

 

process가 pipe에서 read할 때

  • pipe가 not empty 이면, read를 즉시 반환한다. 
  • pipe가 empty 이면, pipe로 write가 진행 될 때 까지 read를 block한다.

process가 pipe에서 write할 때

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

pipe 의 한쪽 끝이 닫혔을 경우

- write가 close일 때

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

- read가 close 일 때

  • kernel에서 SIGPIPE signal을 전달
  • signal을 ignore하거나, catch하고 signal handler를 실행하게 되면, write는 실패하여 -1을 return하고 errno로 EPIPE가 설정된다. 

pipe와 마찬가지로 lseek를 사용할 수 없고, lseek 호출 시 ESPIPE error를 return한다. 

 

The mkfifo(2) system call

#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

// Returns: 0 if OK, -1 on error

pathname : FIFO file name

mode : permission mask

 

mkfifo ("/tmp/fifo", 0666);
.
fd = open("/tmp/fifo", O_WRONLY);
----------------------------------------------------------------------------
//if no process has the FIFO open for reading
if ((fd = open(“tmp/fifo”, O_WRONLY|O_NONBLOCK))==-1) 
   perror(“open on fifo”);
----------------------------------------------------------------------------
if ((fd = open(“tmp/fifo”, O_RDONLY|O_NONBLOCK))==-1)
   perror(“open on fifo”);

FIFO를 write only로 open하였을 때, open은 다른 프로세스가 FIFO를 read로 open할 때 까지 block 되어있다.

 

read로 open프로세스가 없더라도 write로 open할 때, nonblock flag로 open하면 block되지 않고 -1을 반환하여 error를 반환한다. 

만약 block되지도 않고 open에 실패하지도 않는다면, read없이 write만 open하였으므로 SIGPIPE signal이 전달되어 프로세스가 종료될 것이다. 

 

반대로 read only로 open하고 write로 open하지 않았다면, open이 block될 것이고, non block 세팅을 하여 open을 실행한 경우, 실패하지 않고 file descriptor를 반환한다. 

read의 경우에는 write가 없이 실행하더라도 pipe가 비어있으므로 0을 반환한다. 

 

  • open()에서의 Nonblocking flag(O_NONBLOCK)
    - O_NONBLOCK이 설정되지 않은 경우
    read only로 open : 다른 프로세스가 FIFO를 writing으로 열 때까지 block한다.
    write only로 open : 다른 프로세스가 FIFO를 reading으로 열 때까지 block한다.

    - O_NONBLOCK이 설정된 경우
    read only로 open : file descriptor를 즉시 return
    write only로 open : FIFO를 reading으로 open한 프로세스가 없는 경우, -1를 return하고 errno를 ENXIO로 설정

 

example - sendmessage

/* sendmessage --  FIFO를 통해 메시지를 보낸다. */
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#define MSGSIZ		63
char *fifo = "fifo";

main(int argc, char **argv){
  int fd, j, nwrite;     char msgbuf[MSGSIZ+1];

  if(argc < 2){
    fprintf (stderr, "Usage: sendmessage msg ... \n"); exit(1);
  }

  if ((fd = open(fifo, O_WRONLY|O_NONBLOCK))<0) fatal("fifo open failed");

  for ( j = 1; j < argc; j++){ /* send messages */
    if(strlen(argv[j]) > MSGSIZ){
      fprintf (stderr, "message too long %s\n", argv[j]);
      continue;
    }
    strcpy (msgbuf, argv[j]);
    if((nwrite=write(fd,msgbuf,MSGSIZ+1))==-1) fatal("message write failed");
  }
  exit (0);
}

FIFO를 write only & nonblock으로 open한다. nonblock으로 open했으므로 받는 쪽에서 read로 open하지 않으면, -1을 return하고 fatal을 통해 종료한다. 

 

message의 개수만큼 반복문을 도는데, message의 길이를 확인하고, message를 copy하여 FIFO에게 write한다. 

write가 실패할 경우, -1을 반환하여 fatal로 종료한다. 

 

example - receive message

/* rcvmessage -- fifo를 통해 메시지를 받는다. */
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#define MSGSIZ		63
char *fifo = "fifo";

main (int argc, char **argv){
  int fd;
  char msgbuf[MSGSIZ+1];

  if (mkfifo(fifo, 0666) == -1){
    if (errno != EEXIST) fatal ("receiver: mkfifo");
  }

  if ((fd = open(fifo, O_RDWR)) < 0) fatal ("fifo open failed");

  for(;;){
    if (read(fd, msgbuf, MSGSIZ+1) <0) fatal ("message read failed");

 /*
  * 메시지를 프린트한다 ; 실제로는 보다 흥미 있는 일이 수행된다.
  */
    printf ("message received:%s\n", msgbuf);
  }
}

receive에서는 먼저 FIFO를 만든다. FIFO생성시 이미 존재하는 경우 error가 발생한다. (errno = EEXIST)

 

FIFO를 open할 때 read only혹은 write only로 해야하는데 위 코드의 경우 read write로 open하였다. 

이에 대해서 read only로 하였다고 가정하자. 

 

상대편이 write only로 open하여 sendmessage를 하지 않은 상태에서는 read를 실행해도 0을 return한다.

0을 return하는 상태로 for문에서 무한루프에 빠지게 되는데, 

 

read write로 open하게되면, write가 있는데 FIFO가 empty인 상태이므로 read하였을 때, send message가 올 때까지 block되어 대기하다가 send message를 보내면 msgbuf로 read한다. 

 

FIFO를 이용하기 때문에 서로 연관련 프로세스가 아니라도 데이터를 주고받을 수 있다. 

 

 

I/O Multiplexing

FIFO에 여러 클라이언트가 send를 하는 상황이라고 하자. receive server에 연결되는 여러 채널 중 하나에서 block이 일어나면 다른 채널들에 대한 확인을 하지 못한다. 이러한 상황에서 처리하는 방법을 알아보자. 

 

한 descriptor 에서 read하고 다른 곳으로 write하는 프로세스가 있을 때, read loop에서 blocking I/O를 다음과 같이 사용할 수 있다.

while ((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0) 
		if (write(STDOUT_FILENO, buf, n) != n) 
			err_sys("write error");

 

 

만약 다음 그림과 같이 두 개의 descriptor에서 read를 해야한다면 어떻게 해야할까?

server에서 한 쪽으로 block되면 다른 쪽에 대해서는 처리할 수 없는 상황이 생긴다.

 

 

Non blocking I/O Model의 경우

loop문을 반복해서 돌며 확인하므로 CPU 점유율이 높고, multitasking system에서는 피해야하는 방식이다.

따라서 Multiplexting I/O Model을 사용해야한다. 

 

The select(2) system call

#include <sys/time.h>
int select(int nfds , 
		fd_set *readfds, 
		fd_set *writefds,
		fd_set *exceptfds,
		struct timeval *timeout);
// Returns: count of ready descriptors, 0 on timeout, -1 on error

nfds : 할당된 descriptor의 개수를 의미한다. 만약 file descriptor 3,4 가 open되어있다면, 5로 설정 되어야 한다. 

readfds : read하는 file descriptor의 array

writefds : write하는 file descriptor의 array

exceptfds : exception 처리를 위한 file descriptor

timeout : 대기시간 설정

 

timeout의 timeval struct는 다음과 같은 구조이다.

struct timeval { 
	long tv_sec;  /* seconds */ 
	long tv_usec; /* and microseconds */ 
};

timeout의 값에 따라 다음과 같이 행동한다. 

timeout == NULL wait forever. Return is made when one of the specified descriptors is ready or when a signal is caught.
대기하다가 특정 descriptor가 I/O할 준비가 되거나, signal이 catch되었을 때 return 한다. 
timeout->tv_sec==0 && 
timeout->tv_usec==0
Don’t wait all. All the specified descriptors are tested, and return is made immediately.
전혀 기다리지 않고, 모든 지정된 descriptor가 테스트되고, 즉시 return한다. 
timeout->tv_sec!=0 ||
timeout->
tv_usec!=0
Wait the specified number of seconds and microseconds. Return is made when one of the specified descriptors is ready or when the timeout value expires. If the timeout expires before any of the descriptors is ready, the return value is 0.

주어진 시간동안 대기하고, 지정된 descriptor 중 하나가 ready 되거나,  또는 timeout이 만료되면 return한다. 
만약 timeout되기 전에 어느 하나 descriptor가 ready되면, 0을 return한다. 

 

readfds, writefds, execptfds은 위 그림과 같이 fd set을 의미한다. 

 

#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset);
// Returns: nonzero if fd is in set, 0 otherwise

void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);

FD_ISSET을 통해 fdset에 fd가 set되어있는지를 알 수 있다.

FD_CLR를 통해 fdset에서 fd를 0으로 설정할 수 있다.

FD_SET을 통해 fdset에서 fd를 1로 설정할 수 있다.

FD_ZERO로 fdset의 모든 file descriptor를 0으로 설정한다. 

 

fd_set rset; 
int fd; 

FD_ZERO(&rset); 
FD_SET(fd, &rset);
FD_SET(STDIN_FILENO, &rset); 


if (FD_ISSET(fd, &rset)) {
       ...
}

위와 같이 사용할 수 있다. 

 

fd_set readset, writeset;

FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_SET(0, &readset);
FD_SET(3, &readset);
FD_SET(1, &writeset);
FD_SET(2, &writeset);
select(4, &readset, &writeset, NULL, NULL);

FD_ZERO를 통해 readset, writeset의 모든 file descriptor를 0으로 초기화하고,

FD_SET을 통해 readset에서는 0,3 descriptor를, writeset에서는 1,2 descriptor를 세팅한다.

select system call을 통해 설정한 set들을 kernel로 보낸다. 이때 timeout이 NULL로 설정되었으므로 특정 I/O가 ready되거나 signal이 올 때까지 대기한다. 

 

위 코드의 결과는 다음과 같다.

 

select의 return value에 대해서 알아보자.

  • Return -1
    - error 발생. signal을 catch한 경우 
    - 어떤 descriptor set도 변경되지 않은 경우 (ready되지 않은 경우)
  • Return 0
    - timeout될 때까지 어떤 descriptor도 ready되지 않은 경우
    - 모든 descriptor set이 0이 되는 경우
  • Return positive value
    - ready된 descriptor의 개수 반환
    세 개의 sets(readfds, writefds, exceptfds)에서 ready된 descriptor들의 총 개수 
    같은 desciptor의 ready가 겹치면 중복되어 count된다. 
    - ready된 descriptor는 1로, ready되지 않은 descriptor는 0로 clear된다. 

 

example

#include <sys/time.h>
#include <sys/wait.h>
#define MSGSIZE   6

char *msg1 = "hello";
char *msg2 = "bye!!";
void parent(int [][]);
int child(int[]);

main(){
  int pip[3][2];
  int i;

  for (i = 0; i < 3; i++){
    if (pipe(pip[i]) == -1) fatal ("pipe call");
    switch (fork()){
    case -1:  /* 오류 */
      fatal ("fork call");
    case 0:   /* 자식 */
      child (pip[i]);
    }
  }
  parent (pip);
  exit (0);
}

 

위 코드에 대한 그림으로 다음과 같은 상태이다.

 

int child(int p[2]){
  int count;
  close (p[0]);

  for (count = 0; count < 2; count++)
  {
    write (p[1], msg1, MSGSIZE);
    sleep (getpid() % 4);
  }

  write (p[1], msg2, MSGSIZE);
  exit (0);
}

child 함수에서는 pipe를 받아 2번의 랜덤 간격을 둔 write후 한 번의 write를 실행한다.

 

void parent(int p[3][2]){
  char buf[MSGSIZE], ch; fd_set set, master;   int i;
  for (i = 0; i < 3; i++)  close (p[i][1]);

  FD_ZERO (&master);
  FD_SET (0, &master);
  for (i = 0; i <3; i++) FD_SET (p[i][0], &master);

  while (set = master, select (p[2][0]+1, &set, NULL, NULL, NULL) > 0){
    if (FD_ISSET(0, &set)){
      printf ("From standard input...");
      read (0, &ch, 1);
      printf ("%c\n", ch);
    }

    for (i = 0; i < 3; i++){
      if (FD_ISSET(p[i][0], & set)){
        if (read(p[i][0], buf MSGSIZE)>0){
          printf ("Message from child%d\n", i);
          printf ("MSG=%s\n",buf);
          }
      }
    }
    if (waitpid (-1, NULL,WNOHANG) == -1) return;
  }
}

parent 함수를 보면, 모든 pipe를 매개변수로 받아온다.

read역할만 하므로 write역할을 하는 p[i][1]를 close한다. 

master라는 set을 할당하여 0번만 세팅한 후, p[i][0]에 해당되는 것들을 세팅한다. 

즉 master set에서는 0, 3, 5, 7 file descriptor가 1로 세팅된 것이다. 

master를 set에 저장하고, set을 select의 readfds값으로 넣는다. 

nfds의 값으로는 p[2][0] + 1 = 7+ 1 = 8을 넣었으므로, 7번 file descriptor까지 사용한다는 의미이다. 

 

select에서 set의 어느 하나라도 ready가 되었을 때 양수를 반환한다.

set에서 ready된 fd가 0번인지 FD_ISSET으로 확인하고, 0번이라면 키보드 입력으로 read를 실행한다.

0번이 아니라면, 어떤 fd가 set에서 ready되었는지 반복문을 통해 찾아내고, 해당 fd를 통해서 read를 실행한다. 

 

waitpid 의 매개변수가 -1이므로 모든 child에 대해서 대기하는데, WNOHANG flag로 인해 즉시 return한다.

waitpid의 return값이 -1인 경우 child가 모두 exit되었으므로 종료하고,

child가 남아있는 경우 return값이 0이므로 while문을 반복한다. 

 

 

example

int join (char *com1[], char *com2[]){
  int p[2], status;
  
  switch (fork()){
  case -1: fatal ("1st fork call in join");
  case 0: break;
  default: wait(&status); return(status);
  }
  if (pipe(p) == -1) fatal ("pipe call in join");

  switch (fork()){
  case -1: fatal ("2nd fork call in join");
  case 0:
    dup2 (p[1],1); /*표준 출력이 파이프로 가게 한다*/
    close (p[0]);  /*화일 기술자를 절약한다. */
    close (p[1]);
    execvp (com1[0], com1); /* com1: ls */
    fatal("1st execvp call in join");
  default:
    dup2(p[0], 0); /* 표준 입력이 파이프로부터 오게 한다 */
    close (p[0]);
    close (p[1]);
    execvp (com2[0], com2); /* com2: grep */
    fatal ("2nd execvp call in join");
  }
}

위 코드는 pipe를 실제로 shell에서 어떻게 처리하는 가에 대한 것을 보여준다. 

#include <stdio.h>

main()
{
  char *one[4] = {"ls", "-l", "/usr/lib", NULL};
  char *two[3] = {"grep", "∧d", NULL};
  int ret;

  ret = join (one, two);
  printf ("join returned %d\n", ret);
  exit (0);
}

one, two는 명령어를 배열로 만든 것이다. 

 

int join (char *com1[], char *com2[]){
  int p[2], status;
  
  switch (fork()){
  case -1: fatal ("1st fork call in join");
  case 0: break;
  default: wait(&status); return(status);
  }
  if (pipe(p) == -1) fatal ("pipe call in join");

  switch (fork()){
  case -1: fatal ("2nd fork call in join");
  case 0:
    dup2 (p[1],1); /*표준 출력이 파이프로 가게 한다*/
    close (p[0]);  /*화일 기술자를 절약한다. */
    close (p[1]);
    execvp (com1[0], com1); /* com1: ls */
    fatal("1st execvp call in join");
  default:
    dup2(p[0], 0); /* 표준 입력이 파이프로부터 오게 한다 */
    close (p[0]);
    close (p[1]);
    execvp (com2[0], com2); /* com2: grep */
    fatal ("2nd execvp call in join");
  }
}

main으로부터 one, two를 매개변수로 전달받아, com1 = "ls -l /usr/lib", com2 = "grep ^d" 이다.

com1에 대한 표준 출력이 pipe를 통해 com2의 표준 입력으로 들어간다. 

fork로 child 프로세스를 만들면 child 프로세스는 pipe를 생성한다. 

 

또 fork를 통해 child 프로세스를 만들고 child, parent 각각의 코드를 수행한다.

dup2를 이용하여 child는 p[1]을 std out에 copy한다.

사용하지 않는 p[0]를 close하고, fd 1이 p[1]을 가지고 있는 상태이므로 p[1]도 close한다. 

exec를 통해 child 프로세스가 com1을 실행한다. 

 

com1으로부터 넘어온 명령어를 com2에 입력한다. 

parent는 p[0]을 std in에 copy한다. 

사용하지 않는 p[0], p[1] (descriptor 3,4)을 close한다.

exec로 com2를 실행한다. 

 

위와 같은 과정으로 pipe를 구현한다.

 

반응형

관련글 더보기