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() 함수로 쓰기 전용 파이프 생성하기
<예제 10-1 관전 Point>
- 08행
- “w” 모드를 사용해 쓰기 전용 파이프를 생성하고 자식 프로세스는 wc -l 명령을 수행하도록 함
- wc -l은 입력되는 데이터의 행 수를 출력하는 명령
- 14~15행
- 부모 프로세스에서는 반복문을 사용해 문자열을 파이프로 출력
- 자식 프로세스는 파이프로 입력되는 문자열을 읽어서 wc -l 명령을 수행
◼ [예제 10-2] 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() 함수의 인자로는 파일 기술자를 저장할 배열을 지정
- if ( pipe(fd) == -1 ) {
- 17행
- switch ( pid = fork() ) {
- fork() 함수를 사용해 자식 프로세스를 생성
- switch ( pid = fork() ) {
- 22행 ~ 28행 (자식 프로세스)
- close(fd[1]);
- 자식 프로세스에서 파이프를 읽기용으로 사용하겠다는 의미
- len = read(fd[0], buf, 256);
- 파이프 입력 부분인 fd[0]에서 문자열을 읽어들이고
- write(1, buf, len);를 통해 화면에 출력
- close(fd[1]);
- 29행 ~ 34행 (부모 프로세스)
- close(fd[0]);
- 부모 프로세스에서 파이프를 출력용로 사용하겠다는 의미
- write(fd[1], "Test Message\n", 14);
- fd[1]로 문자열을 출력
- waitpid(pid, &status, 0);
- 자식 프로세스가 종료하기를 기다림
- close(fd[0]);
- 실행 결과
- 부모 프로세스가 출력한 문자열을 자식 프로세스가 받아 출력하고 있음을 알 수 있음
◼ [예제 10-4] pipe() 함수로 명령 실행하기
<예제 10-4 관전 Point>
- 10행
- if ( pipe(fd) == -1 ) {
- 파이프를 생성
- if ( pipe(fd) == -1 ) {
- 15행
- switch ( pid = fork() ) {
- fork() 함수를 사용해 자식 프로세스를 생성
- ps 명령의 결과는 기본으로 표준 출력으로 출력되고, grep 명령은 표준 입력을 통해 입력받음
- 따라서 부모 프로세스와 자식 프로 세스 간의 통신이 표준 입출력 대신 파이프를 통해 이루어지도록 만들어야 함
- switch ( pid = fork() ) {
- 21행
- close(fd[1]);
- 자식 프로세스가 할 일은 부모 프로세스가 파이프로 출력하는 ps -ef 명령의 결과를 받아 grep ssh 명령을 수행하는 것
- 따라서 파이프의 출력 부분이 필요 없으므로 fd[1]을 닫음
- close(fd[1]);
- 22행 ~ 25행
- if ( fd[0] != 0 ) {
- fd[0]의 값이 0이 아니면, 즉 표준 입력이 아니면
- dup2(fd[0], 0);
- fd[0]의 값을 표준 입력으로 복사 한 후
- close(fd[0]);
- fd[0]을 닫음
- 이제 자식 프로세스에서는 표준 입력을 fd[0]이 가리키는 파이프에서 읽음
- if ( fd[0] != 0 ) {
- 26행
- execlp("grep", "grep", "ssh", (char *)NULL);
- 자식 프로세스가 grep 명령을 exec() 함수로 호출
- 이렇게 하면 grep 명령은 표준 입력을 통해 데이터를 읽어들이려 함
- 이미 23행( dup2(fd[0], 0); )에서 표준 입력으로 파이프의 입력 파일 기술자를 복사했으므로 결과적으로 파이프를 통해 데이터를 읽어들임
- 30행
- close(fd[0]);
- 부모 프로세스에서는 파이프의 입력 부분이 필요 없으므로 닫음
- 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]이 가리키는 파이프에서 출력
- if ( fd[1] != 1 ) {
- 35행
- execlp("ps", "ps", "-ef", (char *)NULL);
- exec() 함수를 사용해 ps -ef 명령을 실행,
- ps -ef 명령은 기본으로 표준 출력으로 출력하므로 결과가 파이프로 출력
- 이 출력 결과를 자식 프로세스가 읽어들임
- execlp("ps", "ps", "-ef", (char *)NULL);
- 실행 결과
- ps –ef | grep ssh 명령이 실행
- 즉, 부모 프로세스에서 자식 프로세스로 ps –ef 명령의 실행 결과가 전달되었고 자식 프로세스는 여기서 grep ssh 명령을 실행
- execlp("grep", "grep", "ssh", (char *)NULL);
◼ [예제 10-5] 양방향 통신하기
<예제 10-5 관점 Point>
- 08행
- int fd1[2], fd2[2];
- 파이프 2개를 생성할 것이므로 파일 기술자 배열을 2개 선언
- int fd1[2], fd2[2];
- 13행
- if ( pipe(fd1) == -1 ) {
- 파이프를 하나 생성, 생성된 파일 기술자 fd1은 부모 프로세스에서 자식 프로세스로 데이터를 보낼 때 사용
- if ( pipe(fd1) == -1 ) {
- 18행
- if ( pipe(fd2) == -1 ) {
- 또 다른 파이프를 하나 생성, 생성된 파일 기술자 fd2은 자식 프로세스에서 부모 프로세스로 데이터를 보낼 때 사용
- if ( pipe(fd2) == -1 ) {
- 29 ~ 30행 (자식 프로세스)
- close(fd1[1]);
- 자식 프로세스는 부모 프로세스에서 오는 데이터를 읽는 데 사용할 fd1[0]을 남겨 두고 fd1[1]을 닫음
- close(fd2[0]);
- 또한 부모 프로세스로 데이터를 보내는 데 사용할 fd2[1]을 남겨두고 fd2[0]을 닫음
- close(fd1[1]);
- 39 ~ 40행 (부모 프로세스)
- close(fd1[0]);
- 자식 프로세스에 데이터를 보내는 데 사용할 fd1[1]을 남겨둠.
- close(fd2[1]);
- 자식 프로세스에서 오는 데이터를 읽는 데 사용할 fd2[0]을 남겨둠
- close(fd1[0]);
- 실행결과
- 부모 프로세스와 자식 프로세스가 데이터를 주고받았음을 알 수 있음
- 파일 기술자를 여닫는 과정이 다소 복잡하지만 이와 같이 파이프를 2개 생성하면 양방향 통신이 가능