파이프 (Pipe)
- 가장 간단한 UNIX 프로세스 간 통신 방식은 파이프(pipe) [데이터가 흐르는 통로]이다. 이는 특별한 파일로 표현됨
- 파이프는 프로세스 간 통신 메커니즘으로, 동일한 시스템에서 실행 중인 프로세스들이 정보를 공유하여 협력할 수 있도록 해줍니다. (데이터를 저장할 때 write() 사용, 데이터를 읽어들일 때 read() 사용)
#include <unistd.h>
int pipe(int fd[2]);
- pipe() 시스템 호출은 파이프(pipe)라 불리는 입출력 메커니즘을 생성하며, 2 개의 파일 디스크립터인 fd[0]과 fd[1]을 반환한다. [읽기용, 쓰기용으로 구분되기에 배열이 2개인 것]
- fd[0]은 읽기(reading)를 위해 열리고, read[fd[1])
- fd[1]은 쓰기(writing)를 위해 열린다. write(fd[0])
- fd[1]에 쓰여진 데이터는 fd[0]에서 FIFO (선입선출) 방식으로 읽을 수 있다. (파이프에서 데이터 처리 방식) ex) a,b,c 순서로 들어오면 a,b,c 순으로 나가게 되는 것
- 성공하면 pipe는 0을 반환하고, 실패하면 -1을 반환한다.
=> 파이프는 이름이 있는 파이프 or 이름이 없는 파이프 이렇게 2가지가 존재함
파이프의 특성 I (Characteristics of pipe I) - 이름 없는 파이프
- 파이프는 외부적이거나 영구적인 이름을 가지지 않는다. => 일시적 (파이프를 사용하는 프로세스가 모두 종료되면, 자동으로 사용했던 파이프 객체도 같이 삭제됨)
- 프로그램은 오직 2 개의 파일 디스크립터를 통해서만 파이프에 접근할 수 있다.
- (1) 파이프를 생성한 프로세스와 (2) fork 시 디스크립터를 상속받는 자손 프로세스에 의해 사용된다.
- 프로그램은 오직 2 개의 파일 디스크립터를 통해서만 파이프에 접근할 수 있다.
- POSIX 표준은 프로세스가 fd[0]에 쓰기(write)를 시도하거나 fd[1]에서 읽기(read)를 시도할 경우 발생하는 상황 (바꿔서 사용하는 경우)에 대해 명시하지 않는다.
{fd[0] 호출하여 read 함수를 호출했을 때, pipe의 상태에 따라서 달라짐}
{ 파이프가 비어 있지 않은 경우 }
Case 1. 프로세스가 파이프에서 읽기(read) - fd[0])를 호출할 때,
- 파이프가 비어 있지 않다면 즉시 반환한다.
{ 파이프가 비어 있는 경우}
Case 2. 파이프가 비어 있고 다른 프로세스가 파이프를 쓰기 위해 열어둔 상태라면,
- 데이터가 파이프에 쓰일 때까지 읽기는 차단(blocks)되고 읽은 데이터 값이 버퍼에 쓰여짐
Case 3. 아무 프로세스도 파이프를 쓰기 위해 열어두지 않았다면,
- 비어 있는 파이프에서 읽기는 0을 반환하며, 이는 파일의 끝(end-of-file, pipe에 쓸 프로세스가 아무도 없을 경우) 조건(0을 반환)을 나타낸다.
=> 이 설명은 파이프에 대한 접근이 차단 I/O(blocking I/O)를 사용한다고 가정한다.
s를 string값으로 만들기 위해서 \0을 입력하여 끝 자리에 null값을 출력한 것
(위) 같은 프로세스가 파이프를 만들어 쓰기 및 읽기 작업을 수행한 간단한 예제
-
- 파이프 생성 및 파일 디스크립터의 역할
- pipe() 함수는 두 개의 파일 디스크립터(pipefd[0], pipefd[1])를 생성합니다.
- pipefd[1]은 쓰기 용도로, pipefd[0]은 읽기 용도로 사용됩니다.
- 즉, pipefd[1]에 데이터를 쓰면 pipefd[0]을 통해 그 데이터를 읽을 수 있습니다.
- 데이터 쓰기와 읽기의 흐름
- write(fd, buf, size) 함수가 호출되면, buf에 저장된 주소에서 시작하여 size만큼의 데이터가 운영 체제에 전달됩니다.
- 이때 fd는 운영 체제에 이 데이터가 어떻게 처리되어야 하는지를 알려주는 역할을 합니다. 파이프에서는 이 데이터가 파일처럼 일시적으로 저장되었다가 읽히게 됩니다.
- 파이프의 작동 방식
- 파이프는 데이터를 버퍼에 임시로 저장합니다. 만약 쓰기 프로세스가 파이프에 데이터를 썼는데, 아직 읽기 프로세스가 그 데이터를 읽지 않았다면, 데이터는 파이프의 버퍼에 남아 있게 됩니다.
- 그리고 파이프는 FIFO(First-In-First-Out, 선입선출) 구조이기 때문에, 먼저 쓰여진 데이터가 먼저 읽히게 됩니다.
- 파일의 끝 문자와 대기 상태
- 만약 프로세스가 파일의 끝 문자(예: Ctrl+D)를 파이프에 썼다면, 이 데이터가 파이프의 끝을 의미하게 되어 운영 체제는 이를 저장해 둡니다.
- read() 함수가 호출될 때까지 운영 체제는 이 데이터를 파이프 버퍼에 보관하고, read()가 실행될 때 비로소 이 데이터를 읽어서 가져갑니다.
- 파이프 생성 및 파일 디스크립터의 역할
- 파이프 생성
- pipe() 함수는 두 개의 파일 디스크립터, pipefd[0]과 pipefd[1]을 만듭니다.
- pipefd[1]은 쓰기 전용이고, pipefd[0]은 읽기 전용입니다.
- 데이터 쓰기 (write)
- write(pipefd[1], s2, strlen(s2)); 호출을 통해 "Rex Morgan MD"라는 문자열을 pipefd[1]에 씁니다.
- 이때 "Rex Morgan MD"는 파이프를 통해 전송되며, 파이프 내에 임시로 저장됩니다.
- 데이터 읽기 (read)
- read(pipefd[0], s, 1000); 호출을 통해 파이프에 저장된 "Rex Morgan MD" 문자열을 pipefd[0]을 통해 읽습니다.
- 읽은 데이터는 s 배열에 저장됩니다.
- 파이프의 동작 원리
- 이 과정을 통해 데이터가 pipefd[1]을 통해 파이프에 쓰이고, pipefd[0]을 통해 파이프로부터 읽혀집니다.
- 즉, write()는 데이터를 파이프에 전달하고, read()는 그 데이터를 가져가는 역할을 합니다.
파이프를 사용하는 단일 프로세스는 그리 유용하지 않다. 일반적으로 부모 프로세스가 파이프를 사용하여 자식 프로세스와 통신하는 데 사용된다.
부모 프로세스에서 파이프(Pipe) 객체를 먼저 생성하고,생성되면 2개의 파일 디스크립터 테이블이 생성이 되고 그 후 fork함수를 통하여 자식 프로세스를 생성하여 두 프로세스 간에 데이터를 주고받기 위한 준비 단계를 보여줍니다(자식과 부모가 pipe를 공유하는 상태). 부모 프로세스의 파일 디스크립터 테이블에 파이프의 읽기와 쓰기 끝이 등록되며, 자식 프로세스는 아직 생성되지 않은 상태로 표시됩니다.
- 파이프 생성: pipe(fd)를 호출하여 fd 배열에 읽기 끝 (fd[0])과 쓰기 끝 (fd[1])을 생성합니다. 만약 파이프 생성에 실패하면 오류 메시지를 출력하고 종료합니다.
- 버퍼 초기화: bufin과 bufout을 각각 "empty"와 "hello"로 초기화합니다. bufin은 자식 프로세스에서 데이터를 읽어들이는 데 사용될 버퍼입니다.
- 프로세스 분기: fork()를 호출하여 자식 프로세스를 생성합니다. childpid가 -1이면 프로세스 생성에 실패한 것이므로 오류를 출력하고 종료합니다.
- 부모와 자식 프로세스 코드 분기:
- 부모 프로세스는 fd[1]에 bufout의 내용을 write() 함수로 씁니다. 즉, "hello"라는 메시지를 파이프에 쓰는 역할을 합니다.
- 자식 프로세스는 fd[0]에서 read() 함수를 사용하여 데이터를 읽고, bufin에 저장합니다.
- 출력: 최종적으로 fprintf()를 사용하여 각 프로세스가 자신의 PID와 함께 bufin과 bufout의 내용을 출력합니다.
만약, parent가 "hello"를 쓰기 전에 child 프로세스가 먼저 읽을 경우에는 "hello"를 앞써 작성된 read 함수가 블록이 되고 "hello"가 쓰기 전까지 기다렸다가 읽어갈 거임!
질문 : 자식 프로세스는 항상 전체 문자열을 읽을 수 있는가?
- 부모의 bufin은 항상 문자열 "empty"를 포함한다.
- 자식의 bufin은 부모가 보내준 파이프로 전부 읽게 되면 대부분 문자열 "hello"를 포함할 가능성이 높다.
- 단일 read 호출이 단일 write 호출에 의해 쓰여진 모든 데이터를 실제로 가져올 것이라는 보장은 없다. (read가 모든 데이터를 읽지 못할 수 있음)
- read가 부분적인 결과만 가져오는 경우, 자식의 bufin은 "helty"와 같은 부분적인 내용을 포함할 수도 있다.
FIFO - 이름이 있는 파이프
- 파이프는 임시적이다. 프로세스가 파이프를 열어두지 않으면 파이프는 사라진다.
- POSIX는 모든 프로세스가 이를 닫은 후에도 지속되는 특별한 파일을 나타낸다.
- 이를 FIFO 또는 이름 있는 파이프(named pipe) 라고 한다.
- FIFO는 일반 파일처럼 영구적인 이름과 권한을 가지며, ls 명령어로 디렉토리 목록에서 확인할 수 있다.
- 적절한 권한을 가진 모든 프로세스는 FIFO에 접근할 수 있다. (파이프에 비해 사용범위가 넓어짐)
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
{FIFO를 생성하는 2가지 방법}
- 쉘에서 mkfifo 명령어 사용
- 프로그램에서 mkfifo 함수 호출
- mkfifo 함수는 (1) path로 지정된 경로에 새 FIFO 특별 파일을 생성한다.
- (2) mode 인수는 새로 생성된 FIFO의 권한을 지정한다.
{FIFO를 제거하는 2가지 방법}
- 일반 파일을 제거하는 것과 동일하다.
- 쉘에서 rm 명령어를 실행
- 프로그램에서 unlink 호출
FIFO를 사용하는 예제
- 현재 작업 디렉터리에 myfifo라는 이름의 FIFO를 생성한다.
- 모든 사용자가 읽을 수 있지만, 소유자만 쓸 수 있어야 한다. (권한 정보, 644(rwx, 소유자 -읽기 및 쓰기, 사용자 - 읽기))
아래 예제 문제
- 명령줄에서 지정된 경로를 사용하여 이름 있는 파이프(named pipe)를 생성한다.
- 그런 다음 자식 프로세스를 생성한다.
- 자식 프로세스는 이름 있는 파이프에 데이터를 쓴다.
- 부모 프로세스는 자식이 쓴 데이터를 읽는다.
dofifochild는 자식 프로세스가 부모 프로세스에게 FIFO를 통해서 메세지를 전달해주는 함수
dofifoparent는 부모 프로세스가 FIFO를 통해서 메세지를 읽는 함수
-> 2개의 함수는 별도의 함수로 구현이 되어 있음
mkfifo 함수를 호출하여 새로 만들 argv[1]을 2번째 command argument로 전달한 이름으로 새로 생성할 FIFO의 이름을 지정, FIFO의 권한은 소유자에게만 읽기와 쓰기 권한을 줌 -> FIFO_PERM
FIFO를 성공적으로 생성하고 부모가 fork를 하여 자식 프로세스를 만들고 fork의 반환값에 따라서 자식 프로세스는 다르게 작용 만약, fork의 반환값이 0이면 dofifochild 함수를 호출(this was written by the child 문자를 씀), 아닐 경우, dofifoparnet를 실행 (argv[1]은 fifo의 경로를 의미)
2번째 파라미터인 string값을 fifo에 쓰기 위해서 먼저 open()함수로 open을 하고 쓰기 전용으로 open함(자식은 쓸 거기 때문), snprinf함수를 통하여 자식 프로세스는 fifo에 쓸 내용이 포함된 버퍼를 먼저 준비하고 fifo에 쓸 거임("id%s]\n~~" 내용을 쓸 거임, 현재의 프로세스의 id값을 넣을 것!, idstring값으로는 this was written by child(위 예제 내용)을 작성할 거임, strsize로 "버퍼의 string의 길이 + 1" fifo에 작성할 것 -> r_write함수를 사용하여 fd에다가 buf의 내용을 작성하겠다(null character까지 포함한 내용을 작성하겠다는 것)
snprintf: 버퍼에 쓰는 함수
- 지정된 크기를 초과하여 쓰지 않으며, 추가한 문자열의 끝에 항상 null 문자를 추가하여 종료한다.
- 버퍼 주소
- 버퍼 크기
- 형식 문자열
자식이 넘겨진 fifo의 내용을 받아 출력을 할 것, 읽기 위해서 open을 할 때, fifoname으로 읽기 전용으로 open을 함, r_read함수로 open된 내용을 버퍼의 사이즈 만큼 읽은 내용을 버퍼에 쓸 것 -> 읽은 내용을 전부 출력하면 부모 프로세스는 "[-] : This was written by child" 내용이 출력을 함
질문
- 프로그램의 프로세스가 종료된 후 이름 있는 파이프(named pipe)는 어떻게 되는가?
- 두 프로세스 중 어느 것도 FIFO에 대해 unlink를 호출하지 않았기 때문에, FIFO는 여전히 존재하며 해당 경로의 디렉터리 목록에 나타난다.
'Computer Science > 시스템 프로그래밍' 카테고리의 다른 글
시스템 프로그래밍 chapter09. (타임과 타이머) (1) | 2024.11.19 |
---|---|
시스템 프로그래밍 chapter08. (신호) (0) | 2024.11.07 |
시스템 프로그래밍 chapter 05. (파일과 디렉토리) (1) | 2024.10.29 |
시스템 프로그래밍 chapter 04. (4) | 2024.10.09 |
시스템 프로그래밍 chapter 03. (1) | 2024.10.08 |