📌 매뉴얼 (Linux)
NAME
pipe - create pipe
: 파이프 만들기
SYNOPSIS
#include <unistd.h>
int pipe(int pipefd[2]);
DESCRIPTION
pipe() creates a pipe, a unidirectional data channel that can be used for interprocess commu‐
nication. The array pipefd is used to return two file descriptors referring to the ends of
the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of
the pipe. Data written to the write end of the pipe is buffered by the kernel until it is
read from the read end of the pipe. For further details, see pipe(7).
If flags is 0, then pipe2() is the same as pipe(). The following values can be bitwise ORed
in flags to obtain different behavior:
O_CLOEXEC
Set the close-on-exec (FD_CLOEXEC) flag on the two new file descriptors. See the de‐
scription of the same flag in open(2) for reasons why this may be useful.
O_DIRECT (since Linux 3.4)
Create a pipe that performs I/O in "packet" mode. Each write(2) to the pipe is dealt
with as a separate packet, and read(2)s from the pipe will read one packet at a time.
Note the following points:
* Writes of greater than PIPE_BUF bytes (see pipe(7)) will be split into multiple
packets. The constant PIPE_BUF is defined in <limits.h>.
* If a read(2) specifies a buffer size that is smaller than the next packet, then the
requested number of bytes are read, and the excess bytes in the packet are dis‐
carded. Specifying a buffer size of PIPE_BUF will be sufficient to read the largest
possible packets (see the previous point).
* Zero-length packets are not supported. (A read(2) that specifies a buffer size of
zero is a no-op, and returns 0.)
Older kernels that do not support this flag will indicate this via an EINVAL error.
Since Linux 4.5, it is possible to change the O_DIRECT setting of a pipe file descrip‐
tor using fcntl(2).
O_NONBLOCK
Set the O_NONBLOCK file status flag on the open file descriptions referred to by the
new file descriptors. Using this flag saves extra calls to fcntl(2) to achieve the
same result.
: pipe()는 프로세스 간 통신에 사용될 수 있는 단방향 데이터 채널인 파이프를 생성합니다.
배열 pipefd는 파이프의 끝을 참조하는 두 개의 파일 디스크립터를 반환하는 데 사용됩니다.
pipefd[0]은 파이프의 읽기 끝을 나타냅니다. pipefd[1]은 파이프의 쓰기 끝을 나타냅니다.
파이프의 쓰기 끝에 쓰여진 데이터는 파이프의 읽기 끝에서 읽을 때까지 커널에 의해 버퍼링됩니다.
자세한 내용은 파이프(7)를 참조하십시오.
플래그가 0이면 pipe2()는 pipe()와 동일합니다.
다음 값은 플래그에서 비트 단위로 ORed되어 서로 다른 동작을 얻을 수 있습니다:
O_CLOEXEC
: 두 개의 새 파일 디스크립터에 FD_CLOEXEC(Close on exec) 플래그를 설정합니다.
이것이 유용한 이유는 open(2)의 동일한 플래그에 대한 설명을 참조하십시오.
O_DIRECT(Linux 3.4 이후)
: "패킷" 모드에서 I/O를 수행하는 파이프를 만듭니다. 파이프에 대한 각 쓰기(2)는 별도의 패킷으로
처리되며, 파이프의 읽기(2)는 한 번에 하나의 패킷을 읽습니다.
다음 사항에 주의하십시오:
* PIPE_BUF 바이트보다 큰 쓰기(PIPE(7))는 여러 패킷으로 분할됩니다.
상수 PIPE_BUF는 <limits.h>에 정의됩니다.
* read(2)가 다음 패킷보다 작은 버퍼 크기를 지정하면 요청된 바이트 수가 읽혀지고
패킷의 초과 바이트는 버려집니다. PIPE_BUF의 버퍼 크기를 지정하면 가능한
가장 큰 패킷을 읽을 수 있습니다(앞의 설명 참조).
* 길이가 0인 패킷은 지원되지 않습니다. (버퍼 크기를 0으로 지정하는 읽기(2)는 no-op이고 0을 반환합니다.)
이 플래그를 지원하지 않는 오래된 커널은 EINVAL 오류를 통해 이 플래그를 나타냅니다.
Linux 4.5 이후 fcntl(2)을 사용하여 파이프 파일 디스크립터의 O_DIRECT 설정을 변경할 수 있습니다.
O_NONBLOCK
: 새 파일 디스크립터가 참조하는 열린 파일 설명에 O_NONBLOCK 파일 상태 플래그를 설정합니다.
이 플래그를 사용하면 fcntl(2)에 추가 호출이 저장되어 동일한 결과를 얻을 수 있습니다.
RETURN VALUE
On success, zero is returned. On error, -1 is returned, errno is set appropriately, and
pipefd is left unchanged.
On Linux (and other systems), pipe() does not modify pipefd on failure. A requirement stan‐
dardizing this behavior was added in POSIX.1-2008 TC2.
: 성공 시 0이 반환되고 오류 시 -1이 반환되고 errno가 적절하게 설정되며
pipefd가 변경되지 않은 상태로 유지됩니다.
Linux(및 기타 시스템)에서 pipe()는 pipefd on failure를 수정하지 않습니다.
이 동작을 표준화하는 요구 사항이 POSIX.1-2008 TC2에 추가되었습니다.
📌 함수 설명
pipe 함수는 프로세스 간 통신을 할때 사용하는 커뮤니케이션의 한 방법으로, 모든 유닉스 시스템이 제공한다.
프로세스 간 통신은 IPC(Inter Process Communication)이라 하며, 커널이 제공하는 IPC 설비를 통해 프로세스들은
서로 통신을 하게 된다. 지금 다루게 될 것은 가장 오래된 IPC인 pipe이다.
[ 기본 특징 ]
- Uni-directional byte stream (단방향) -> pipe는 단방향으로 동작한다.
- name or ID 없음. 그리하여 두 프로세스 간에 관계가 항상 있어야 함. 즉, related process 간에 사용 가능한데,
fork 를 통해 만들어진 부모-자식 관계의 프로세스일 때 사용 가능하다. (또는 부모를 통해 만들어진 자식 간 통신)
- 위 그림에서처럼 fd[1]을 통해 pipe가 앞의 프로세스로부터 write를 받고, fd[0]을 통해 read 시켜준다.
[ 1. pipe 생성 이후 fork 한 상황 ]
- 위 그림과 같이 pipe를 생성한 뒤, fork() 함수를 통해 부모-자식 프로세스를 만들어준다.
여기서 fork 함수는 기존에 프로세스가 가지고 있던 파일 디스크립터를 복제해서 그대로 공유하게 되는데,
기존 Parent가 write 하여 pipe에 fd[1]로 전달되던 것을 child도 똑같이 fd[1]로 전달하며, read 과정도 마찬가지다.
하지만, 이 상황에는 문제점 한가지가 존재하는데,
Parent가 Child 에게 어떠한 데이터를 작성하여 Child 가 받는 동작을 원할 때,
Parent가 write 하고 Child가 read 하는 이상적인 동작을 생각할 수 있으나
fd가 지금 중복되어 사용되고 있으므로, Parent가 write한 것이 다시 Parent로 read 될 수 있는 상태이다.
그렇다면 이 문제를 해결하기 위해서는 어떻게 하면 좋을까?
[ 2. 사용하지 않는 fd들을 닫기 ]
Parent와 Child를 단방향으로 이어주기 위해서는 Parent의 read 부분과, Child의 write 부분을 닫아준다.
이렇게 변경되면 Parent가 write하는 데이터는 Child만 read로 수신할 수 있게 된다.
이 방법으로 fork를 이용해 pipe로 Parent-Child간 통신을 구현할 수 있게 된다.
이제, pipe() 함수의 인자들에 대해서 알아보도록 하겠다.
int pipe(int pipefd[2]); 라는 모양인데, 인자가 독특하게 2개의 데이터를 담는 int 배열이 된다.
이는 '2개의 파일 디스크립터를 저장할 배열'이고 각각 읽고 쓰는 역할의 파일 디스크립터가 된다.
이는 위의 그림들에서 나온 fd[0], fd[1] 이라는 것을 간접적으로 알 수 있다.
pipefd[2] : 생성될 pipe fd[2]를 저장할 버퍼
-> pipefd[0] = reader-side fd (read를 사용할 fd 부분)
-> pipefd[1] = writer-side fd (write를 사용할 fd 부분)
추가적으로, pipe도 하나의 '데이터를 담는 버퍼 공간'이므로, 이 공간의 크기를 넘게 되면 full 이 된다.
그럼 pipe가 full이 되면 write 시도 시 더 이상 넣지 못하도록 blocking이 되어 기다린다.
반대로, 이 버퍼 공간에 아무것도 없으면 read 시도 시 blocking 되어 기다린다.
그리고, 정해진 PIPE_BUF라는 특정 사이즈가 있는데 (4KB),
write size가 이 PIPE_BUF보다 작으면 atomic, 즉, 버퍼에 담겨 정상적으로 write 되며,
write size가 이 PIPE_BUF보다 크면 interleaved, 즉, 버퍼에 다 담기지 못하고 쪼개져서 write 될 수 있다.
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_BUF 1024
int main(){
int fd[2];
pid_t pid;
char buf[MAX_BUF];
if(pipe(fd) < 0){
printf("pipe error\n");
exit(1);
}
if((pid=fork()) < 0){
printf("fork error\n");
exit(1);
}
if(pid > 0){
close(fd[0]);
strcpy(buf, "hello from parent");
write(fd[1], buf, strlen(buf));
}
else {
close(fd[1]);
read(fd[0], buf, MAX_BUF);
printf("child received message : %s\n", buf);
}
return (0);
}
코드를 통해 가볍게 pipe의 동작을 따라가보도록 하겠다.
pipe 함수는 성공하면 0을 반환하고, 실패하면 -1을 반환하므로, if문으로 pipe의 error를 잡아준다.
다음으로, fork를 통해 pid에 넣어주는 과정을 따르는데, parent process는 child process의 pid를,
child process는 0을 pid에 넣게 된다. fork가 실패할 경우 -1을 반환하므로 마찬가지로 if문으로 fork error를 잡아준다.
그리고, fork를 했으므로 pid가 각각 다른 parent process, child process가 생기고, 하위 코드를 각 프로세스가 실행한다.
이 상황에서는 parent의 fd를 그대로 child가 받아 같은 read, write fd를 가지게 된다.
우선, pid > 0 이 if문의 조건인데 이는 parent process가 실행할 코드가 되고,
else 부분이 child process의 실행 코드가 될 것이다.
parent process는 read 부분을 닫아주어 write할 내용이 child에게만 가도록 해준다.
그리고 문자열 하나를 만들어, buf에 담고 이를 fd[1]로 write 해주면, child에서는 read 과정으로 fd[0]에서 받으면
해당 문자열을 받을 수 있게 된다.
child process로 가면, write 부분을 닫아준다. 그리고, read함수로 fd[0]으로 읽어오면 pipe를 통해
parent process로부터 입력받은 내용이 담기게 되고, buf를 prinf로 출력하면 그 내용이 나오게 된다.
출력 - std_output)
child received message : hello from parent |
추가적으로, parent 와 child가 둘 다 읽기 쓰기가 가능하도록 해주려면,
파이프를 하나 더 추가하여 두개의 파이프를 사용하면 된다.
A파이프는 parent의 read 부분인 fd[0]를 닫아주고, child의 write 부분인 fd[1]을 닫아준다.
그리고 B파이프는 parent의 write 부분인 fd[1]을 닫아주고, child의 read 부분인 fd[0]을 닫아준다.
그럼 A 파이프를 통해 parent가 쓴 데이터를 child가 받게 되고,
B 파이프를 통해서는 child가 쓴 데이터를 parent가 받게 되는 것이다.
'프로그래밍 > C' 카테고리의 다른 글
[function] wait 함수 알아보기 (0) | 2024.06.09 |
---|---|
[function] unlink 함수 알아보기 (2) | 2024.06.07 |
[function] fork 함수 알아보기 (2) | 2024.06.04 |
[function] exit 함수 알아보기 (0) | 2024.05.31 |
[function] execve 함수 알아보기 (0) | 2024.05.30 |