728x90
반응형

UNIX에서는 입출력장치와 상관없이 일관된 방법으로 IO operation을 수행할 수 있다.

앞서서 일반 text파일로 I/O 수행하는 것과 표준 입출력을 통한 I/O를 수행하는 법을 배웠었고

pipe를 통해 프로세스가 다른 프로세스에 정보를 보내거나 받을 수 있다. (프로세스 간의 통신)

가장 간단한 버전인 interprocess communication에서는 pipe를 어떻게 이용하는가. pipe를 통해 I/O 수행 -> open,read,write,clsoe를 한다.

 Pipe를 통해 프로세스간 통신을 어떻게 하고 pipe내에서 어떤 일이 벌어지는지 살펴보겠다.

 여기서는 client server interactions 즉, 한 컴퓨터 시스템내에서 process를 살펴볼것이다. 

 

Pipe

- 프로세스간의 통신하기 위해서는 OS의 도움을 받아야한다. 종류가 여러가지가 있다. 

가장 간단한 버전의 simples UNIX interprocess이 바로 pipe이다. pipe는 interprocess communication mechanisms이다. 프로세스가 같은 시스템에서 running되고 있을때 share information이 허용된다.

 

pipe도 file로 mapping이 된다. 그래서 file operation에서 썼던 read write 함수를 그대로 써서 수행할 수있다.

pipe라는 것은 통하는 관. 데이터가 흘러가는 관이라고 생각하면 된다. pipe로부터 데이터를 집어넣고 빼낼 수 있다. 

데이터 전달하고자 하는 프로세스가 데이터 집어넣고 받는 프로세스는 pipe로부터 데이터를 읽어올 수 있다. 

데이터 넣는 프로세스는 write함수 사용하고 받는 프로세스는 read함수를 사용할 것이다.

pipe를 사용하는 것은 당연히 같은 프로그램에서 사용하는 것이다.

 

pipe를 사용하려면 먼저 pipe 객체를 만들어야 한다.

#include <unistd.h>
int pipe(int fd[2]);

 

pipe( ) system call은 pipe라고 불리는 I/O mechanism을 생성하고, 2개의 file descriptor를 리턴한다.(fd[0], fd[1])

그래서 int형 배열 2개짜리 fd가 file descriptor 2개짜리 int형 배열 선언하고 그 배열을 파이프 함수의 parameter로 주게 되면 pipe객체를 생성해주고 pipe를 오픈해준다. pipe로 표현된 special file을 오픈까지 해준다. 이 때 open함수의 리턴값이 file descriptor parameter로 리턴된다. open된 파일 descriptor가 리턴된다.

 왜 2개이냐? 파이프의 mapping된 파일이 읽기용, 쓰기용이 구분이 되어있다. 파이프가 생긴 후에 fd배열 2개짜리 배열 index가 0인 것(fd[0])은 읽어들이는 용도로만 사용 fd[1]은 write용으로 사용한다. 만약에 실제 file descriptor를 잘못 지정하게 되면 오동작을 하게 된다. 

 - fd[0] is opened for reading

 - fd[1] is opened for writing

 pipe함수를 통해서 프로세스가 통신하기 위해서는 write할때 file descripotr [1]을 통해서 데이터를 write할 수 있다. 읽을때는 read(fd[0])을 해야 읽어들일 수 있다. 파이프 함수는 파이프 객체를 만들어주고 해당 객체를 오픈까지 해준다. 데이터를 읽을 때 쓸때 전용 file descriptor를 지정해야 한다.

 

 데이터가 들어오고 나가는 것은 FIFO방식 (first in first out) 나가는 것도 순서대로 나간다. 성공적으로 파이프 썼으면 성공의 의미로 0을 쓰게 된다. 에러가 나면 -1 리턴한다.

 

Characteristics of pipe

pipe는 외부의 이름이나 영구적인 이름이 없어서 pipe의 2개의 descriptor를 통해서만 접근할 수 있다. 그렇기 때문에 pipe는 pipe를 만든 process와 fork를 통해 상속된 자식들에 의해서만 사용될 수 있다. 

 

process가 pipe를 read하는 것을 call하면 read는 pipe가 비어있지 않으면 즉시 리턴한다. 만약 pipe가 비어있고 다른 프로세스가 pipe에 write하기 위해 open하였다면 read는 파이프가 write할때까지 block이 된다.

아무 프로세스도 wirte를 위해 open하지 않았다면 empty pipe를 읽으면 0을 return하고 이것은 end of file condition을 의미한다. 

pipe에 접근하기 위해서는 blocking I/O를 사용한다.

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>

int main()
{
	int pipefd[2];
	int i;
	char s[1000];
	char *s2;

	if (pipe(pipefd) < 0)
	{
		perror("pipe");
		_exit(1);
	}
	s2 = "Taeheon is the king";
	write(pipefd[1], s2, strlen(s2));
	i = read(pipefd[0], s, 1000);
	s[i] = '\0';

	printf("Read %d bytes from the piep: %s'\n", i, s);
}

pipe에는 이름이 있는 pipe와 이름이 없는 pipe가 있다. 이름이 없다는 것은 ls 명령어를 입력해서 디렉토리 entry에 나오는게 없다는 뜻이다. 파이프는 temporary한 객체이다. pipe를 만든 프로세스가 있는데 모두 종료하게 되면 pipe객체도 자동으로 소멸이 된다.  반면 이름있는 파이프는 open하고 close해도 파일이 삭제되는 것은 아니고 계속 남아있다.

 

위의 설명을 자세히 보겠다.

이름이 없는 파이프는 temporary하다. file descriptor를 사용하는 process가 하나도 없게 되면 pipe는 자동으로 같이 삭제가 된다. 이름이 없기 때문에 access하기 위해서는 process는 file descriptor 정보를 알고 있어야 한다. 알고있는 process만 access 할 수 있다.

 

 그러면 이 파이프는 어떤 프로세스가 사용할 수 있는 것일까? pipe를 생성한 process이던가 부모 자식 관계의 process만 사용할 수 있다. 부모 자식관계가 사용할 수 있는 이유는 fork를 하면 프로세스의 file descriptor table이 상속이 되는데 그 안에 file descriptor table을 받기때문에 2개의 entry가 추가된다. 자식 프로세스도 부모 프로세스가 open한것을 상속받게 되기 때문에 사용할 수 있게된다.

 

 fd[0]와 fd[1]를 잘못 사용할 경우 어떤 것을 해야될지 POSIX에서는 규정하지 않기 때문에 무슨 일이 벌어질지 모른다. 

write하는 것은 write함수를 쓰면 되는데 read하는 경우에는 pipe의 상태에 따라 달라진다. pipe를 통해 read할때 fd[0]을 통해 읽겠다 하면 즉각적으로 리턴이 되는 경우는 파이프가 비어있지 않은 경우이다. 즉, 읽을 데이터가 있다면 생기는 경우이다. pipe를 read하면 실제 읽은 바이트 수가 리턴된다.

 

 그런데 read 호출했는데 비어있는 경우. 읽을 데이터가 없다는 의미이다. 파이프가 비어있는게 2가지 케이스가 있다. 지금 당장 비어있지만 나중에 write함수를 사용해 써줄 수 있던가, 아니면 파이프에 앞으로 쓰여질 가능성이 없는 경우 두 가지 경우에서 read함수가 다르게 작동한다. OS는 이 두가지 경우에서 다르게 작동한다.

 파이프가 비어있고 만약에 어떤 프로세스가 pipe에 wirte용으로 open을 해놓은 상황이다라고 한다면 write용 descriptor를 가진 process가 있으면 그 얘기는 언젠가 write를 수행하기 위해 파이프를 여전히 오픈해놓고 있는것이다. os는 언제가 write해서 쓸 수 있겠구나 라고 판단해서 read함수는 block이 된다. 즉, read 함수는 기다린다.

 

다른 케이스는 read함수를 호출했는데 pipe가 비어있는데 write 용으로 오픈한 것도 없는 경우이다. read함수 호출하면 마냥 기다려도 pipe가 쓸 경우가 없다. read함수는 바로 리턴해버린다. 이 때는 0을 리턴한다. 0이 리턴이 되엇다는것은 end of file을 의미한다. 

파이프에서는 파일의 끝이다 라는 경우는 쓸 프로세스가 아무도 없을 경우. 지금도 없고 앞으로도 없다. 라고 판단. 파이프에대해서 read 호출했을때 2가지 선택지에 대해서 os가 판단해 다르게 작동한다. read함수가 pipe상태에 따라 다르게 작동한다. pipe에 대해서 i/o 사용한다는 것은 blocking된다 라고 생각. pipe는 기본적으로 blocking i/o를 사용한다.

 

예제는 같은 프로세스가 pipe에 쓰고 읽는 간단한 예제이다. pipe호출하면 pipe객체만들어지고 open까지 됨. 그래서 바로 write쓸 수 있다. read호출해서 바로 읽어준다. 읽은 데이터를 s버퍼에 쓰겠다. s에는 Rex Morgan MD\(null)까지 추가됨. 키보드로 ctrl +D하면 end of file. 파일의 끝이다 라는것을 알려준다. single process의 pipe는 유용하지 않다. 보통 부모와 자식간의 communication을 위해 pipe를 한다.

 

Two steps to make PIPE 

부모 프로세스 - 자식 프로세스

먼저 pipe객체를 만든다. 두개의 파일 descriptor가 open되고 자식도 fdt를 알게 되는것이다.

전달될 메시지는 hello란 메시지를 전달해주고 싶다. 부모 프로세스가 파이프를 만들것이고 write를 통해 hello를 전달하고 싶다. 자식은 부모를 통해 읽어서 pipe함수 호출해서 pipe일단 만들고 return되는 fdt. 2개의 entry추가. fork를 해서 자식프로세스를 만든다. if 리턴값이 0이 아니면 부모프로세스가 수행. 자식 프로세스가 else구문 실행. 부모는 파이프에 쓰겟다. -> hello를 쓰겠다. 자식프로세스는 read를 하겠다. fd를 통해 읽겠다. 

 

parentwritepipe.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFSIZE 10

int main(void) {
   char bufin[BUFSIZE] = "empty";
   char bufout[] = "hello";
   int bytesin;
   pid_t childpid;
   int fd[2];

   if (pipe(fd) == -1) {
      perror("Failed to create the pipe");
      return 1;
   }
   bytesin = strlen(bufin);
   printf("bufin size: %d\n", bytesin);
   childpid = fork();
   if (childpid == -1) {
      perror("Failed to fork");
      return 1;
   }
   if (childpid)                                       /* parent code */
      write(fd[1], bufout, strlen(bufout)+1);
   else                                                 /* child code */
      bytesin = read(fd[0], bufin, BUFSIZE);
   fprintf(stderr, "[%ld]:my bufin is {%.*s}, my bufout is {%s}\n",
              (long)getpid(), bytesin, bufin, bufout);
   return 0;
}

cf> %.*s 의 의미는 bytesin의 길이만큼 bufin을 출력하겠다는 뜻이다.

 

Q> child는 항상 full string전부를 읽을까?

- 보장이 안된다. 일부만 읽은 상태에서 중단이 되는 경우가 생긴다.

- child의 bufin은 hello메시지를 전부 읽게 되면 정상인데 100퍼센트 read할 수 없다. 일부만 읽고 리턴이 될 수도 있다. bufin 하는 경우. helty. empty -> hel만 읽음 -> bufin에 처음 3바이트만 써서 helty가 될 수도 있다. temp를 수행 못한것이 아니다. 한바이트만 읽어도 성공한 것임. 보통은 hello를 다 읽는다.

 

child 프로세스가 부모가 쓰기전에 먼저 읽으면?? read함수가 블럭이 될것이다(위에 말했던 것처럼)write용으로 open한 프로세스가 있기 때문에. fd[1]을 가지고 있기 때문에. 누가 먼저 실행이 될 것이냐는 걱정할 필요가 없다.

 

FIFO

: 이름이 있는 pipe이다. 동작은 동일한데 사용방법, 특징이 unnamed pipe와 다른점이 있다. 

파이프는 temporary하다. FIFO객체를 새로만들 때 일반 파일만들 때 했던것처럼 access모드도 지정하지만 권한 정보도 주었던 것처럼 FIFO만들때도 permission을 주어야 한다. 각 파일별로 읽기,쓰기, 이름도 지정을 한다. 이름이 있기 때문에 ls해서 보면 만든 FIFO를 볼 수가 있다. FIFO객체는 이름이 있기 때문에 이름을 아는 다른 프로세스와 통신이 가능하다. 즉, 파이프에 비해서 사용범위가 넓어진다. access권한이 있으면 누구든지 오픈해서 통신해서 사용할 수 있다. 

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

첫번째 파라미터로 경로를 주고 권한을 두번째 파라미터로 준다. FIFO는 2가지 방법으로 만들 수 있는데 먼저, mkfifo 명령어를 통해서 만들 수 있다. 또는, mkfifo함수로 새로운 FIFO객체가 생성이 된다.

path는 FIFO의 경로, mode는 FIFO의 permission을 의미한다.

 FIFO는 mkfifo객체를 생성해준다. 읽기든 쓰기를 할려면 따로 open함수 써서 open해줘야 한다.

FIFO를 삭제하려면 수동으로 삭제해야한다. shell상에서 rm명령어로 삭제할수있고 unlink로 사용해서 삭제할 수 있다. 

(file을 지우는 방식과 같다)

 

Example

- FIFO(my fifo) ./myfifo

- 모두가 FIFO의 내용을 읽을 수 있는데 owner만 쓸 수 있게만들어 보자.

owner에게는 읽기와 쓰기 -> 6권한

others에게는 100 권한 아니면 symbolic name으로 mkfifo의 두번째 파라미터로 주면 된다.

리턴값은 성공하면 0 실패하면 -1.

삭제하고 싶다-> unlink해서 경로지정하면 된다. 

#define FIFO_PERMS(S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
if(mkfifo("myfifo", FIFO_PERMS) == -1)
   perror("Falied to create myfifo");

현재 working directory 에서 생성된 파일을 지우고 싶다.

if(unlink("myfifo") == -1)
  perror("Falied to remove myfifo");

Example

1. named pipe를 특정 path에 만든다

2. child를 fork한다

3. child process는 pipe에 write한다.

4. parent는 child가 write한 것을 read한다.

parentchildfifo.c

#include <errno.h>
#include <fcntl.h>  
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#define BUFSIZE 256
#define FIFO_PERM  (S_IRUSR | S_IWUSR)

int dofifochild(const char *fifoname, const char *idstring);
int dofifoparent(const char *fifoname);

int main (int argc, char *argv[]) {
   pid_t childpid; 

   if (argc != 2) {                           /* command line has pipe name */
      fprintf(stderr, "Usage: %s pipename\n", argv[0]);
      return 1; 
   }
   if (mkfifo(argv[1], FIFO_PERM) == -1) {           /* create a named pipe */
      if (errno != EEXIST) {
         fprintf(stderr, "[%ld]:failed to create named pipe %s: %s\n", 
              (long)getpid(), argv[1], strerror(errno));
         return 1; 
      }
   }
   if ((childpid = fork()) == -1){
      perror("Failed to fork");
      return 1;
   } 
   if (childpid == 0)                                   /* The child writes */
      return dofifochild(argv[1], "this was written by the child");
   else
      return dofifoparent(argv[1]);
}

dofifochild.c 는 자식프로세스가 부모에게 전달하는 프로그램

dofifochild.c

#include <errno.h>
#include <fcntl.h>  
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include "restart.h"
#define BUFSIZE 256

int dofifochild(const char *fifoname, const char *idstring) {
   char buf[BUFSIZE];
   int fd;
   int rval;
   ssize_t strsize;

   fprintf(stderr, "[%ld]:(child) about to open FIFO %s...\n",
          (long)getpid(), fifoname);
   while (((fd = open(fifoname, O_WRONLY)) == -1) && (errno == EINTR)) ; 
   if (fd == -1) {
      fprintf(stderr, "[%ld]:failed to open named pipe %s for write: %s\n", 
             (long)getpid(), fifoname, strerror(errno));
      return 1; 
   } 
   rval = snprintf(buf, BUFSIZE, "[%ld]:%s\n", (long)getpid(), idstring);
   if (rval < 0) {
      fprintf(stderr, "[%ld]:failed to make the string:\n", (long)getpid());
      return 1; 
   } 
   strsize = strlen(buf) + 1;
   fprintf(stderr, "[%ld]:about to write...\n", (long)getpid());
   rval = r_write(fd, buf, strsize);
   if (rval != strsize) {
      fprintf(stderr, "[%ld]:failed to write to pipe: %s\n",
             (long)getpid(), strerror(errno));
      return 1;
   }
   fprintf(stderr, "[%ld]:finishing...\n", (long)getpid());
   return 0;
}

dofifoparent.c는 부모프로세스가 fifo를 통해서 읽는 함수

dofifoparent.c

#include <errno.h>
#include <fcntl.h>  
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include "restart.h"
#define BUFSIZE 256
#define FIFO_MODES O_RDONLY

int dofifoparent(const char *fifoname) {
   char buf[BUFSIZE];
   int fd;
   int rval;

   fprintf(stderr, "[%ld]:(parent) about to open FIFO %s...\n",
                       (long)getpid(), fifoname);
   while (((fd = open(fifoname, FIFO_MODES)) == -1) && (errno == EINTR))  ; 
   if (fd == -1) {
      fprintf(stderr, "[%ld]:failed to open named pipe %s for read: %s\n",
             (long)getpid(), fifoname, strerror(errno));    
      return 1; 
   }    
   fprintf(stderr, "[%ld]:about to read...\n", (long)getpid());
   rval = r_read(fd, buf, BUFSIZE);
   if (rval == -1) {
      fprintf(stderr, "[%ld]:failed to read from pipe: %s\n",
             (long)getpid(), strerror(errno));    
      return 1; 
   }    
   fprintf(stderr, "[%ld]:read %.*s\n", (long)getpid(), rval, buf);
   return 0; 
}

argv[1] 넘겨주는 값을 통해 읽고 쓸것임

open을 써서 open을 하고 자식은 fifo에 써야되기 때문에 쓰기 전용으로 오픈.
그다음에 snprintf -> 버퍼에 쓰는 함수이다. 뒤에나오는 내용을 출력을 하겠다.

buffer를 준비하고 쓰겠다. buffer에는 buf[BUFSIZE] buf안에 snprintf 두번째 파라미터는 크기. 그리고 (%ld) %s를 버퍼에 담겠다. 

idstring -> 파이프에 쓸 내용으로 buf에 준비를 먼저한다.(snprintf 사용) 끝에 항상 null character를 출력해준다. 

sn은 정해진 크기만큼 쓰겠다. 

buf가 string이 된것이다. 

strsize = strlen(buf)+1 // buffer의 길이 +1 크기만큼에 +1 null character 포함한 것을 알아냄

write함수를 써서 fd에 버퍼안에있는 내용을 null character까지 쓰겠다. 

open할때 파라미터를 가지고 open하고, r_read함수 써서 FIFO로부터 bufsize만큼 읽어서 읽은내용 쓰겠다. 

Q> process들이 모두 종료하고 namedpipe는 디렉토리안에 남아있게된다. 

 

728x90
반응형

'CS > 시스템 프로그래밍' 카테고리의 다른 글

POSIX Threads  (0) 2021.11.23
Times and Timers  (0) 2021.11.16
Files and Directories  (0) 2021.10.15
UNIX I/O  (0) 2021.10.15
System Call 함수 [Fork() 함수, wait() 함수]  (0) 2021.10.04

+ Recent posts