유닉스의 프로세스 간 통신 방법 즉 IPC 통신에는 PIPE가 있다.
PIPE란 이름처럼 두 프로세스 간에 데이터가 오고 가는 관. 즉 영어로 파이프를 생성해서 두 프로세스가 데이터를 주고받는 방법이다.
이 파이프는 fifo의 성질을 띄고있으며 단방향 통신이라는 특징이 있다.
파이프는 read와 write 시스템 콜을 이용해서 데이터를 주고받으며, 기본적으로 blocking read/ blocking write를 사용한다.
파이프를 이용해서 프로세스간 동기화 작업을 수행할 수 있으며, 프로세스 간 서버와 클라이언트의 동작을 구현할 수 있다.
pipe 만들기
#include <unistd.h>
int pipe(int filedes[2]);
→ filedes[0] : 읽기용
→ filedes[1] : 쓰기용
→ 성공시 0, 실패 시 -1 return
→ process당 open file수, 시스템 내에서 동시에 오픈 가능한 file 수 제한이 있다. (파이프도 제한에 포함)
기본적으로 파일 디스크립터를 이용해서 파이프를 구현한다.
파이프를 생성하면 두 프로세스간 읽기/쓰기로 구분해서 열린 양방향 파이프가 만들어진다.
단방향 파이프를 만들기 위해서 사용자가 사용하지 않는 파이프는 close() 함수를 이용해서 닫아주는 처리를 해야 한다.
만양 양방향 파이프를 만들고 싶다면 단방향 파이프를 2개 만들어서 사용해야 한다.
그 이유는 파이프 하나에는 데이터를 쌓는 공간이 하나이기 때문이다.
pipe의 특성
→ FIFO 처리를 한다. (무조건!)
→ lseek는 작동 불가능 (파이프의 데이터는 읽으면 바로 사라진다. 따라서 lseek로 읽으면 바로 사라져서 값을 사용할 수 없다.)
→ pipe는 fork()에 의해 상속 가능하다.
pipe를 이용한 단방향 통신 (부모 → 자식)
1. pipe 생성
2. fork()에 의해 자식 생성 & pipe 복사
3. 부모는 읽기용(fd[0]), 자식은 쓰기용(fd[1]) pipe를 close
읽기 0, 쓰기 1은 read/write 순서로 외우면 쉽다.
부모 process와 자식 process의 단방향 파이프 라인 처리
- pipe를 생성한다.
- fork()로 자식 process를 생성한다.
- 자식 process의 fd[1] (쓰기용 file descriptor)를 close한다.
- 부모 process의 fd[0] (읽기용 file descriptor)를 close한다.
- 단반향 파이프 라인 생성완료
- read(), write() 명령을 이용해서 데이터를 주고받는다.
파이프의 장점
- 어디부터 읽어가야 하나 생각할 필요가 없다. 쓴 만큼 읽으면 바로 사라지기 때문에 바로 앞에서부터 읽어가면 된다.
- 파이프 라인을 이용하면 자연스럽게 동기화가 된다.
파이프를 이용한 양방향 통신
1. pipe 2개 생성
2. fork()에 의해 자식 생성 & pipe 2개 복사
3. pipe1 : 부모는 읽기용, 자식은 쓰기용 pipe를 close
4. pipe2 : 부모는 쓰기용, 자식은 읽기용 pipe를 close
blocking read / blocking write
→ read가 blocking 되는 경우 : pipe가 비어 있는 경우
→ write가 blocking 되는 경우 : pipe가 가득 찬 경우
기본적으로 파이프로 통신을 할 때 사용하는 read/write 함수는 blocking으로 작동한다.
이 사실은 프로세스 간 동기화를 고려할 때 중요하게 여겨진다.
다음과 같은 예시를 생각해 보자.
두 프로세스가 각각 리더와 라이터의 역할을 수행하려고 한다.
리더는 라이터가 쓴 글을 읽어야 하고, 없다면 쓸 때까지 기다려야 한다.
라이터는 글을 쓰는데, 글이 꽉 찼다면 리더가 읽어서 사라질 때까지 기다려야 한다.
이 두 프로세스를 파이프를 이용해서 어떻게 동기화할 수 있을까?
정답은 "파이프는 기본적으로 blocking read/ blockking write를 지원하므로 자연스럽게 동기화가 된다." 이다.
왜 그런지는 한 번 생각해보기 바란다.
또한 signal과 pause를 이용해서도 위의 문제를 해결할 수 있다. 이를 시그널로 해결하는 방법은 독자에게 숙제로 남겨두겠다.
파이프 닫기
→ 쓰기 전용 pipe 닫기 : 다른 writer가 없는 경우, read를 위해 기다리던 process들에게 0을 return (EOF와 같은 효과)
→ 읽기 전용 pipe 닫기 : 더 이상 reader가 없으면, writer들은 SIGPIPE signal을 받는다. Signal handling이 되지 않으면 process는 종료
signal handling이 되면, signal 처리 후 write는 -1을 return.
파이프를 닫을 때 read only pipe와 write only pipe가 다르게 동작한다는 것은 매우 중요한 사실이다.
우선, 쓰기 전용 파이프가 0을 리턴하는 이유는 read가 기본적으로 blocking이기 때문에 뭐라도 보내줘야지 리더의 block이 풀리기 때문이다. 이때 read의 리턴 값이 0이라는 의미는 아무것도 읽은 것이 없다는 것을 의미하고, 이는 파이프의 경우 반대편 프로세스에서 해당 파이프의 쓰기 전용 파이프를 닫았음을 의미한다.
읽기 전용 파이프가 signal을 받아서 처리하는 이유는 반대편 프로세스가 파이프가 닫혔음을 알릴 수 있는 방법이 시그널뿐이기 때문이다.
non-Blocking read / non-Blocking write
→ 여러 pipe를 차례로 polling 하는 경우 (다 대 일 통신)
사용법
#include <fcntl.h>
fcntl(filedes, F_SETFL, O_NONBLOCK);
// fcntl : 이미 열린 파일들의 속성을 변경하는 함수
// 두번째 인자로 F_SETFL을 넣으면 O_NONBLOCK으로 non-Blocking read/write로 바꿔줄 수 있음
→ filedes가 쓰기 전용이고, pipe가 차면 blocking 없이 즉시 -1을 return (읽기/쓰기 실패했다는 이야기)
→ 읽기 전용인 경우에는, pipe가 비어 있으면, 즉시 -1을 return
→ 이 경우, errno는 EAGAIN()
→ EAGAIN이란? : 사용 가능한 로컬 포트가 더 이상 없거나 라우팅 캐싱에 공간이 충분하지 않다.라는 의미
pipe를 이용한 client-server
→ Client는 하나의 pipe로 request를 write
→ Server는 여러 개의 pipe로부터 request를 read
→ no request from any client → server는 blocking
→ a request from any child → read the request
→ more than one request → read them in the order
select 시스템 호출
→ select system call
→ 지정된 file descriptor 집합 중 어느 것이 읽기/쓰기가 가능한지 표시
→ 읽기, 쓰기가 가능한 file descriptor가 없으면 blocking
→ 영구적 blocking을 막기 위해 time out을 인자로 지정해서 사용 가능하다.
사용법
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
→ nfds : server가 관심이 있는 file descriptor의 최대 번호 (예를 들어 7번째 bit가 내가 검사하려는 최대 비트의 인덱스이다.)
→ readers : 읽기 가능한 file descriptor
→ writers : 쓰기 가능한 file descriptor
→ errodfds : 오류 발생한 file descriptor를 Bit pattern으로 표현
→ timeout : timeout 값 설정
fd_set 관련 매크로
void FD_ZERO(fd_set *fdset);
// fdset 초기화
void FD_SET(int fd, fd_set *fdset);
// fdset의 fd bit를 1로 설정
void FD_ISSET(int fd, fd_set *fset);
// fdset의 fd bit가 1인지 검사
void FD_CLR(int fd, fd_set *fset);
// fdset의 fd bit를 0으로 설정
timeval의 구조
struct timeval {
long tv_sec; // 1 = 1초
long tv_usec; // 1 = 1밀리초
}
timeout이
- NULL이면, 해당 event가 발생할 때까지 blocking
- 0이면, non-blocking
- 0이 아닌 값이면, read/write가 없는 경우 정해진 시간 후에 return
select의 return 값은
- -1 : 오류 발생 시
- 0 : timeout 발생 시
- 0 보다 큰 정수 : 읽기/쓰기 가능한 file descriptor의 수 (데이터가 몇 개가 왔는지 알려줌!)
→ 주의 사항 : return 시 mask를 지우고, 재설정
readfds에 있는 socket의 연결이 끊어진다면 어떻게 되나?
- select()는 그 socket descriptor가 데이터를 읽을 준비가 되었다고 판단한다.
'CS(Computer Science) > UNIX' 카테고리의 다른 글
UNIX - [시스템 V의 프로세스간 통신 - Message queue] (0) | 2023.01.01 |
---|---|
UNIX - [PIPE - 2] (0) | 2022.12.31 |
UNIX - [MEMORY MAPPING] (0) | 2022.12.30 |
UNIX - [SIGNAL] (1) | 2022.12.29 |
UNIX - [소켓 프로그래밍 예제 - 2] (1) | 2022.12.27 |