CS학습5 min read
Pipe: 프로세스의 출력을 다른 프로세스의 입력으로 잇는 단방향 바이트 스트림
파이프(pipe)는 한 프로세스의 표준 출력(stdout)을 다른 프로세스의 표준 입력(stdin)으로 연결하는 커널 내부의 단방향 바이트 버퍼다. 셸의 `|` 연산자로 작은 명령을 조합해 복잡한 처리를 만들 수 있다. 이 글에서는 파일 디스크립터, 버퍼, 블로킹 동작을 중심으로 원리를 설명하고, 셸과 Python 예제로 확인한다.
By 송민성
1. 개념
파이프(pipe)는 커널이 관리하는 메모리 버퍼를 통해 한 프로세스가 쓴 바이트를 다른 프로세스가 읽게 해 주는 단방향 통신 통로다. 프로세스 간 통신(IPC, Inter-Process Communication) 방식 중 가장 단순한 형태다.
- 한쪽 끝은 쓰기 전용, 반대쪽 끝은 읽기 전용이다.
- 데이터는 선입선출(FIFO) 순서로 전달된다.
- 데이터에는 메시지 경계가 없고, 바이트 스트림(byte stream)으로만 취급된다.
- 파이프는 크게 두 종류다.
- 익명 파이프(anonymous pipe): 셸의 |, pipe() 시스템 콜로 만들며 파일 시스템에 이름이 없다. - 이름 있는 파이프(named pipe, FIFO): mkfifo로 만들며 파일 시스템에 경로가 존재한다.
2. 왜 사용하는가
- 조합성:
grep,sort,wc처럼 한 가지 일만 하는 도구를 이어 붙여 새로운 기능을 만든다. 유닉스 철학의 핵심이다. - 임시 파일 제거: 중간 결과를 디스크에 저장하지 않아도 된다.
- 동시 실행: 파이프라인의 각 프로세스가 동시에 실행되므로 앞 단계가 끝나기 전에도 뒤 단계가 데이터를 처리하기 시작한다.
- 메모리 절약: 전체 데이터를 한꺼번에 올리지 않고 흘려보내며 처리할 수 있다.
3. 동작 원리
파일 디스크립터
리눅스 프로세스는 기본적으로 세 개의 파일 디스크립터(file descriptor)를 가진다.
| 번호 | 이름 | 용도 |
|---|---|---|
| 0 | stdin | 표준 입력 |
| 1 | stdout | 표준 출력 |
| 2 | stderr | 표준 에러 |
셸이 a | b를 실행하는 과정
- 셸이
pipe()시스템 콜로 읽기 끝(fd)과 쓰기 끝(fd) 한 쌍을 만든다. - 각 명령을 위해
fork()로 자식 프로세스를 만든다. a의 자식은dup2()로 자신의 stdout(1번)을 파이프의 쓰기 끝으로 교체한다.b의 자식은dup2()로 자신의 stdin(0번)을 파이프의 읽기 끝으로 교체한다.- 각자
exec()로 해당 프로그램을 실행한다. 프로그램은 파이프인지 모른 채 stdin/stdout만 사용한다.
버퍼와 블로킹
- 파이프 버퍼가 가득 차면 쓰는 쪽은 블로킹(blocking)되어 읽는 쪽이 비울 때까지 기다린다.
- 버퍼가 비어 있으면 읽는 쪽은 데이터가 올 때까지 기다린다.
- 쓰기 끝이 모두 닫히면 읽는 쪽은 EOF(read가 0 반환)를 받는다.
- 읽기 끝이 모두 닫힌 파이프에 쓰면 쓰는 프로세스는
SIGPIPE시그널을 받으며, 기본 동작은 프로세스 종료다. - 리눅스의 기본 파이프 용량은 64KiB(65536바이트)다.
fcntl의F_SETPIPE_SZ로 조정할 수 있다.
4. 코드 예제
셸 파이프라인
shell
# 가장 많이 등장한 단어 상위 3개
printf 'apple\nbanana\napple\ncherry\nbanana\napple\n' \
| sort \
| uniq -c \
| sort -rn \
| head -n 3이름 있는 파이프 (FIFO)
shell
mkfifo /tmp/demo_fifo
# 백그라운드에서 읽기 대기
cat /tmp/demo_fifo &
# 쓰기 (읽는 쪽이 있어야 open이 진행됨)
echo "hello fifo" > /tmp/demo_fifo
wait
rm /tmp/demo_fifoPython으로 pipe() 직접 사용
python
import os
read_fd, write_fd = os.pipe()
pid = os.fork()
if pid == 0:
# 자식: 읽기만 하므로 쓰기 끝을 닫는다
os.close(write_fd)
with os.fdopen(read_fd) as r:
print("자식이 받음:", r.read())
os._exit(0)
else:
# 부모: 쓰기만 하므로 읽기 끝을 닫는다
os.close(read_fd)
with os.fdopen(write_fd, "w") as w:
w.write("부모가 보낸 메시지")
os.waitpid(pid, 0)Python subprocess로 파이프라인 구성
python
import subprocess
p1 = subprocess.Popen(["printf", "b\\na\\nc\\na\\n"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["sort"], stdin=p1.stdout, stdout=subprocess.PIPE)
p1.stdout.close() # p2만 읽도록 부모의 복사본을 닫는다
out, _ = p2.communicate()
print(out.decode())5. 시간 복잡도 또는 성능 특성
- 파이프를 통한 전송은 데이터 크기 n에 대해 복사 비용이 O(n)이다. 쓰는 쪽에서 커널 버퍼로, 커널 버퍼에서 읽는 쪽으로 복사가 일어난다.
- 디스크를 거치지 않으므로 임시 파일 방식보다 일반적으로 I/O 부담이 적다.
- 버퍼 크기(기본 64KiB)보다 많은 데이터를 보내면 읽는 쪽 속도에 맞춰 쓰는 쪽이 대기한다. 즉 파이프라인 전체 처리량은 가장 느린 단계에 의해 제한된다.
- 시스템 콜(
read,write) 호출 횟수가 많아지면 오버헤드가 생기므로, 작은 조각을 자주 보내기보다 적당한 크기로 묶어 보내는 편이 유리하다.
6. 실무 사용 사례
shell
# 로그에서 5xx 에러 수 세기
grep ' 5[0-9][0-9] ' access.log | wc -l
# 실행 중인 프로세스에서 특정 이름 찾기
ps aux | grep nginx
# 큰 파일을 압축하며 원격으로 전송
tar -cf - ./project | gzip | ssh user@host 'cat > project.tar.gz'
# JSON 응답에서 필드 추출
curl -s https://api.example.com/users | jq '.[].name'- 로그 분석, 배포 스크립트, CI 파이프라인에서 명령 조합에 널리 쓰인다.
- 도커 컨테이너 로그를
docker logs app 2>&1 | grep ERROR처럼 필터링할 때도 쓴다.
7. 주의할 점
- stderr는 기본적으로 파이프를 타지 않는다.
|는 stdout만 연결한다. 에러까지 넘기려면2>&1 |또는 bash의|&를 쓴다. - 종료 코드는 마지막 명령 기준이다. 중간 명령이 실패해도
$?는 마지막 명령의 값이다. bash에서는set -o pipefail을 설정하면 파이프라인 중 실패한 명령의 코드가 반영된다.
shell
set -o pipefail
false | true
echo $? # pipefail 적용 시 1- 파이프 안의 명령은 서브셸에서 실행된다. bash에서
echo hi | read x처럼 쓰면x가 현재 셸에 남지 않는다. - 교착 상태(deadlock) 위험: 부모가 쓰기 끝이나 읽기 끝을 닫지 않으면 EOF가 도착하지 않아 상대가 영원히 대기할 수 있다. 사용하지 않는 fd는 반드시 닫는다.
- SIGPIPE:
yes | head -n 1처럼 읽는 쪽이 먼저 종료하면 쓰는 쪽은 SIGPIPE로 종료된다. 이는 정상 동작이다. - 불필요한 `cat`:
cat file | grep x는grep x file로 충분하다. - 단방향이므로 양방향 통신이 필요하면 파이프를 두 개 만들거나 소켓을 사용한다.
8. 핵심 정리
- 파이프는 커널 버퍼로 연결된 단방향 FIFO 바이트 스트림이다.
- 셸의
a | b는pipe(),fork(),dup2(),exec()의 조합으로 동작한다. - 버퍼가 가득 차면 쓰는 쪽이, 비면 읽는 쪽이 블로킹된다. 리눅스 기본 용량은 64KiB다.
- 쓰기 끝이 닫히면 EOF, 읽기 끝이 닫히면 SIGPIPE가 발생한다.
|는 stdout만 연결하며, 종료 코드 확인에는set -o pipefail을 함께 사용한다.- 사용하지 않는 파이프 끝은 반드시 닫아야 교착을 피할 수 있다.