Named Pipe
|
파이프를 이용해 명령들을 연결하여 사용하거나 명령,프로세스 치환을 사용하면 명령 실행 중에 pipe 가 자동으로 생성되어 사용된 후 사라지게 되는데요. 이때 생성되는 파이프를 이름이 없다고 해서 unnamed pipe 또는 anonymous pipe 라고 합니다. 이에 반해 named pipe 는 직접 파이프를 파일로 만들어 사용합니다.
named pipe 는 파일과 동일하게 사용될 수 있는데 파일과 다른 점은 redirection 을 이용해 데이터를 출력했을 때 파일은 데이터를 저장하는 반면 pipe 는 저장하지 않는다는 점입니다. 그래서 만약에 디스크 용량이 부족한 상태에서 용량이 큰 파일을 다루고자 할 때 pipe 를 이용하면 프로세스 중간에 임시파일을 만들지 않아도 되므로 디스크 사용을 피할 수 있습니다. 다음은 gzip 으로 압축돼 있는 mysql 데이터 파일을 압축 해제하여 mypipe 로 출력하고 mysql 프롬프트 상에서 named pipe 를 이용해 테이블에 로드 하는 예입니다.
$ mkfifo /tmp/mypipe
$ gzip --stdout -d dbfile.gz > /tmp/mypipe
# 다음은 mysql 프롬프트 상에서 실행하는 명령입니다.
mysql> LOAD DATA INFILE '/tmp/mypipe' INTO TABLE tableName;
또 한 가지 pipe 는 데이터를 저장하지 않기 때문에 파일 내용을 random access 할 수 없습니다. 그러므로 위에서처럼 파일을 open 한 후에는 처음부터 끝까지 한 번에 읽거나 써야 합니다.
다음은 named pipe 를 이용해서 일종의 프록시를 만드는 예인데요.
명령 라인에서 첫번째 nc 명령은 localhost 8080 포트를 리스닝하고 있다가 브라우저가 접속하면 받은 request 를 그대로 stdout 으로 출력합니다.
두번째 nc 는 news.naver.com 80 에 접속하는데 두 명령은 |
로 연결돼 있으므로 브라우저의 request 가 naver 로 전달되게 됩니다.
이때 naver 에서 받은 결과는 두번째 nc 명령의 stdout 으로 출력되는데 지금은 > mypipe
로 연결돼 있습니다.
그러므로 naver 에서 받은 결과는 > mypipe
로 보내지고 다시 첫번째 nc 명령의 < mypipe
를 통해 입력으로 들어가서 브라우저까지 도달하게 됩니다.
$ mkfifo mypipe
$ nc -l localhost 8080 < mypipe | nc news.naver.com 80 > mypipe
# 오류 페이지가 전달되기는 하지만 mypipe 를 통해 브라우저에 전달되는 것을 볼 수 있습니다.
$ nc -l localhost 8080 < mypipe | nc news.zum.com 80 > mypipe
$ nc -l localhost 8080 < mypipe | nc www.google.com 80 > mypipe
이렇게 named pipe 를 이용하면 기존의 |
파이프를 통해서 하지 못하는 작업들을 할 수 있습니다.
Pipe 는 FIFO
Pipe 는 FIFO ( First In First Out ) 형식으로 데이터가 전달됩니다. 그러니까 제일 먼저 pipe 로 들어간 데이터가 pipe 를 읽게 되면 제일 먼저 값으로 나오게 됩니다.
$ mkfifo mypipe
# FD 를 연결하면 버퍼가 차기 전까지 block 되지 않는다.
$ exec 3<> mypipe
$ echo 111 > mypipe
$ echo 222 > mypipe
$ echo 333 > mypipe
$ read var < mypipe; echo $var
111
$ read var < mypipe; echo $var
222
$ read var < mypipe; echo $var
333
$ exec 3>&-
Pipe 는 block 된다.
위의 FIFO 예에서 데이터를 mypipe 로 입력할때 exec 3<> mypipe
를 사용한걸 볼 수 있습니다. 이것은 pipe 로 데이터를 전달할 때 데이터를 읽는 상대편이 없으면 block 되기 때문입니다. 앞서 이야기했지만 pipe 는 파일과 달리 데이터를 저장하지 않기 때문에 읽는 상대편이 없으면 작업이 중단됩니다. 또한 writer 가 없는 상태에서 읽기를 시도할 때도 block 됩니다. 다시 말해 pipe 는 writer 와 reader 가 서로 연결되어 있어야 작업이 진행될 수 있으며 writer 가 쓰기를 완료하고 종료하게 되면 reader 도 함께 종료하게 됩니다.
Broken pipe 에러
writer 와 reader 가 서로 pipe 에 연결되어 writer 가 지속적으로 데이터를 쓰고 있는 상태에서 reader 가 종료 하게 되면 Broken pipe 에러로 writer 가 종료됩니다.
# writer 쓰기시작, 하지만 reader 가 읽기 전까지 block 된다.
$ while :; do echo $(( i++ )); sleep 1; done > mypipe &
[1] 17103
$ cat mypipe # reader 읽기 시작
0
1
2
3
4
^C # ctrl-c 로 강제 종료
$ # Broken pipe 에러로 writer 종료됨
[1]+ Broken pipe while :; do
echo $(( i++ )); sleep 1;
done > mypipe
Reader 의 자동 연결 해제
writer 와 reader 가 서로 pipe 에 연결되어 reader 가 지속적으로 데이터를 읽고 있는 상태에서 writer 가 종료하면 에러는 발생하지 않지만 읽을 데이터가 없는 reader 는 파이프로부터 연결이 해제됩니다.
# terminal 1
# writer 시작. 현재 reader 가 없기 때문에 block
$ while :; do echo $(( i++ )); sleep 1; done > mypipe
# terminal 2
$ cat mypipe # reader 실행
0
1
2
3
...
# terminal 1
# 현재 실행되고 있는 writer 를 ctrl-c 로 강제 종료
# terminal 2
# reader 는 읽을 데이터가 없으므로 파이프 연결이 해제되고 다음 프롬프트가 뜬다.
FD 를 pipe 에 연결해서 사용
위에서 살펴본 바와 같이 파이프는 상대편의 연결 상태에 따라 broken pipe 오류로 writer 가 종료되거나 아니면 읽을 데이터가 없는 reader 는 파이프에서 자동으로 연결이 해제됩니다. 이와 같은 파이프의 특성은 파이프에서 읽어들일 데이터가 없더라도 지속적으로 연결을 유지하고 있다가 데이터가 들어올 경우 처리하고자 할때나 또는 writer 가 지속적으로 데이터를 쓰고 있는 상태에서 reader 가 일시적으로 연결을 해제하고자 할때 장애가 됩니다. 이와 같은 경우에 FD 를 pipe 에 연결하여 사용하면 문제를 해결할 수 있습니다.
# FD 3 번을 mypipe 에 연결
# named pipe 는 외부에서 모두 사용할 수 있는 파일이기 때문에
# 이 명령은 아무 프로세스에서 한번만 실행하면 됩니다.
# 그리고 설정한 프로세스가 종료하면 연결도 해제됩니다.
$ exec 3<> mypipe
writer 가 지속적으로 쓰고 있는 상태에서 reader 를 강제 종료하기
$ while :; do echo $(( i++ )); sleep 1; done > mypipe &
[1] 21951
$ cat mypipe
0
1
2
3
4
^C # ctrl-c 강제 종료
# reader 를 강제 종료 시겼지만 writer 는 broken pipe 로 종료되지 않는다.
# 그리고 다시 파이프를 읽으면 다음 데이터가 이어진다.
$ cat mypipe
5
6
7
8
^C
reader 가 지속적으로 읽고 있는 상태에서 writer 를 종료하기
# terminal 1
$ while :; do echo $(( i++ )); sleep 1; done > mypipe # writer 실행
# terminal 2
$ cat mypipe # reader 실행
0
1
2
3
...
# terminal 1
# ctrl-c 로 writer 강제 종료
# terminal 2
# writer 가 종료되어 읽어들일 데이터가 없지만 파이프에 연결을 지속하고 있음.
# terminal 1
# 다시 writer 연결
$ while :; do echo $(( i++ )); sleep 1; done > mypipe
# terminal 2
# writer 가 다시 연결하여 쓴 데이터가 연이어서 읽혀진다.
4
5
6
7
...
Pipe buffer size
Reader 가 없는 상태에서 writer 가 파이프에 쓰기를 하면 커널에서 사용하는 pipe buffer size 를 알아볼 수 있습니다. ulimit -a
명령으로 조회해 보면 pipe size 가 8 * 512 bytes = 4096 로 나오지만 리눅스의 경우 16 개까지 버퍼를 할당해 사용하므로 4096 * 16 = 65,536 까지 사용할 수 있게 됩니다. 여기서 4096 은 여러 프로세스가 동시에 같은 파일에 write 할때 데이터가 서로 겹치지 않고 atomic 하게 쓸 수 있는 크기에 해당합니다.
$ grep PIPE_BUF /usr/include/linux/limits.h
#define PIPE_BUF 4096 /* # bytes in atomic write to a pipe */
----------------------------------------
$ grep PIPE_DEF_BUFFERS /usr/src/linux-headers-4.8.0-39/include/linux/pipe_fs_i.h
#define PIPE_DEF_BUFFERS 16
----------------------------------------
$ mkfifo mypipe
$ exec 3<> mypipe
# ctrl-c 로 종료
$ while :; do echo -n '1'; done > mypipe
^Cbash: echo: write error: Interrupted system call
# outfile 로 버퍼 내용을 출력후 ctrl-c 로 종료
$ cat mypipe > outfile
^C
# outfile 파일 사이즈가 65,536 으로 나옴
$ ls -l outfile
-rw-rw-r-- 1 mug896 mug896 65536 08.03.2015 15:33 outfile
Multiple writers and readers
Pipe 는 데이터 손실 없이 multiple writers, multiple readers 를 가질 수 있습니다. 이때 입력되는 순서, 출력되는 순서는 예측할 수 없습니다. 다음은 multiple writers, single reader 의 예입니다.
$ while :; do echo $(( i++ )); sleep 1; done > mypipe &
[1] 23084
$ while :; do echo --- $(( i++ )); sleep 1; done > mypipe &
[2] 23097
$ cat mypipe
0
1
2
--- 0
3
--- 1
4
--- 2
5
--- 3
6
--- 4
7
--- 5
...
양방향 통신
파이프는 단방향이기 때문에 두 프로세스가 서로 대화를 나누기 위해서는 두개의 파이프가 필요합니다. 이와같은 방법을 이용하는 것이 keyword 명령중에 하나인 coproc 입니다. coproc 는 입, 출력 용으로 두개의 파이프를 만들어서 입력 파이프를 통해 외부 프로세스로부터 입력을 받고 연산결과를 출력 파이프를 통해 전달합니다.
Socket
named pipe 의 양방향 통신과 비슷한 것이 unix domain socket 입니다. unix domain socket 은 named pipe 와 같이 디렉토리에 socket 파일을 만들어서 시스템 내의 프로세스와 통신을 하는데 이때 생성되는 socket 파일은 다른 프로세스가 접속할때 사용하는 ip 주소와 같은 역할을 합니다. 커널이 교통정리를 해주므로 socket 에 연결된 하나의 FD 만으로도 양방향 통신을 할 수 있습니다.
터미널에서 nc
명령을 이용하여 직접 테스트해볼 수 있습니다.
먼저 터미널에 입, 출력 메시지가 함께 표시되므로 2개의 터미널이 필요합니다.
terminal 1 에서 nc -lU mysocket
명령을 실행하면 디렉토리에 mysocket 파일이 생성되고 접속대기 상태가 됩니다.
이어 terminal 2 에서 nc -U mysocket
명령을 실행하면 두 프로세스가 연결되는데 이후에 서로 메시지를
주고받을 수 있습니다.
- terminal 1
$ nc -l -U mysocket
hello
unix domain socket
- terminal 2
$ nc -U mysocket
hello
unix domain socket
다음은 두 프로세스가 연결된 후 FD 상태인데요. 각 프로세스가 하나의 FD 만 사용하고 있는 것을 볼 수 있습니다. 이때 FD 에 연결된 socket 을 보면 뒤에 붙은 번호가 각각 틀립니다. 다시 말해 프로세스 별로 다른 socket 을 사용하고 있는 것인데요. pipe 를 이용해 양방향 통신을 하는 coproc 의 FD 와 비교해 보시기 바랍니다.
unix domain socket 을 이용하는 대표적인 프로그램 중에 하나가 xwindows 서버 인데요 /tmp/.X11-unix/
디렉토리에 socket 파일이 위치합니다. 또한 lsof -U
명령을 실행하면 시스템 내에서 unix domain socket 을 사용하는 프로그램들을 볼수있습니다. ( pipe 를 이용하는 프로그램은 lsof | grep FIFO
로 볼수있습니다. )
unix domain socket 은 파일시스템에 socket 파일을 만들어 통신하기 때문에 같은 컴퓨터에서 실행되는 프로세스들에 한해서만 통신이 가능하다는 단점이 있습니다. internet domain socket 은 서로 떨어져 있는 컴퓨터에서 실행되는 프로세스들과도 통신을 할 수 있게 해줍니다. 이때는 데이터를 상자에 담아 송, 수신자 주소를 적어 보내는 packet 과 protocol 개념이 필요하게 됩니다. 그러면 인터넷의 길목마다 위치해있는 router 라는 기계가 주소를 보고 해당 packet 을 목적지 까지 전달합니다. 도착한 packet 은 포트 번호에 의해 분류되어 해당 프로세스에게 전달됩니다.
nc
명령을 이용해 internet domain socket 도 테스트해볼 수 있습니다.
먼저 접속을 받는 computer1 에서 ifconfig
명령으로 ip 주소를 확인한 다음
nc -l 8080
명령을 실행하면 접속대기 상태가 됩니다.
이어 computer2 에서 computer1 의 ip 주소와 포트번호를 이용해 nc 12.34.56.78 8080
명령을 실행하면
두 프로세스가 연결되는데 이후부터 서로 메시지를 주고받을 수 있습니다.
- computer1
$ nc -l 8080
hello
internet domain socket
- computer2
$ nc 12.34.56.78 8080
hello
internet domain socket
다음은 socket 을 이용해서 tar 파일을 전송하는 예입니다.
- computer1
# 먼저 tar 파일을 받는 컴퓨터에서 다음 명령을 실행합니다.
$ nc -l 8080 | tar -xvz
- computer2
# 현재 디렉토리를 tar 하고 파이프를 통해 nc 명령으로 전달하기 위해
# '-f -' 옵션을 사용합니다. 여기서 '-' 는 stdout 을 나타냅니다.
$ tar -cvz -f - . | nc -N 12.34.56.78 8080
./
./socat
./ncat
./websock.sh
...
Bash 에서 제공하는 socket 연결 기능
bash 에서는 스크립트에서 직접 socket 에 연결할 수 있는 기능을 다음과 같은 형식을 통해 제공합니다.
exec {file-descriptor}<> /dev/{protocol}/{host}/{port}
command > /dev/{protocol}/{host}/{port}
실제 /dev/protocal/host/port 디렉토리가 존재하는 것은 아닙니다.
그냥 bash 에서 socket 연결 기능을 제공하기 위해 사용하는 주소 형식이라고 생각하면 됩니다.
리스닝을 위한 서버 socket 은 nc -l {port}
명령을 사용할 수 있습니다.
$ cat < /dev/tcp/time.nist.gov/13
57752 16-12-30 15:11:41 00 1 0 602.7 UTC(NIST) *
$ exec 3<> /dev/tcp/www.naver.com/80
$ echo hello >& 3
$ cat <& 3
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML ...
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
...
...
$ exec 3>&-
사용예 )
Job Control 메뉴에 보면 wait 명령과 파일을 이용해 background job 들의 exit 값을 구하는 예가 있습니다. 그걸 named pipe 를 이용해 바꾼 것인데요. 여기서 눈여겨볼 점은 FD 를 named pipe 에 연결해 사용하지 않으면 while read -r res
문에서 읽을 데이터가 없을경우 바로 종료가 됩니다. 이 방법의 장점은 종료상태 값을 구하기 위해 10 개의 프로세스가 모두 종료될 때까지 기다리지 않아도 된다는 것입니다.
#!/bin/bash
trap 'rm -f $mypipe' EXIT
mypipe=/tmp/mypipe_$$
mkfifo $mypipe
exec {FD}<> $mypipe # FD 를 named pipe 에 연결
number_of_jobs=10
do_job() {
echo start job $i...
sleep $((RANDOM % 5))
echo ...end job $i
exit $((RANDOM % number_of_jobs))
}
for i in $( seq $number_of_jobs )
do
( trap "echo job$i ---------- exit: \$? > $mypipe" EXIT
do_job ) &
done
i=1
while read -r res; do
echo "$res"
[ $i -eq $number_of_jobs ] && break
let i++
done < $mypipe
echo $i jobs done !!!
> $mypipe
와< $mypipe
대신에>& $FD
와<& $FD
를 사용해도 됩니다.
Quiz
named pipe 에 FD 를 연결한 후 파일을 삭제하면 어떻게 될까요?
아래 테스트에서 보면 알 수 있듯이 파일이 삭제되어도 FD 는 정상적으로 사용할 수 있습니다.
1 . named pipe 생성 후 FD 연결
$ pipe=$(mktemp -u)
$ mkfifo $pipe
$ ls -l $pipe
prw-rw-r-- 1 mug896 mug896 0 2018-05-25 07:13 /tmp/tmp.dlgHS1PEgV|
$ file $pipe
/tmp/tmp.dlgHS1PEgV: fifo (named pipe)
$ exec 3<> $pipe
$ ls -l /dev/fd/
total 0
lrwx------ 1 mug896 mug896 64 2018-05-25 07:14 0 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 07:14 1 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 07:14 2 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 07:14 3 -> /tmp/tmp.dlgHS1PEgV|
lr-x------ 1 mug896 mug896 64 2018-05-25 07:14 4 -> /proc/4910/fd/
$ echo 111 >&3
$ echo 222 >&3
$ echo 333 >&3
$ read var <&3; echo $var
111
$ read var <&3; echo $var
222
$ read var <&3; echo $var
333
2 . named pipe 파일 삭제
파일이 삭제되어도 FD 는 정상적으로 사용할 수 있다.ls -l /dev/fd
출력을 보면 here document 와 같은 것을 알 수 있습니다.
$ rm -f $pipe # named pipe 파일 삭제
$ ls -l /dev/fd/
total 0
lrwx------ 1 mug896 mug896 64 2018-05-25 07:17 0 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 07:17 1 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 07:17 2 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 07:17 3 -> /tmp/tmp.dlgHS1PEgV (deleted)
lr-x------ 1 mug896 mug896 64 2018-05-25 07:17 4 -> /proc/5110/fd/
$ echo 111 >&3
$ echo 222 >&3
$ echo 333 >&3
$ read var <&3; echo $var
111
$ read var <&3; echo $var
222
$ read var <&3; echo $var
333
3 . FD 삭제
/tmp/tmp.dlgHS1PEgV (deleted)
항목이 제거 되었습니다.
$ exec 3>&-
$ ls -l /dev/fd/
total 0
lrwx------ 1 mug896 mug896 64 2018-05-25 07:18 0 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 07:18 1 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 07:18 2 -> /dev/pts/7
lr-x------ 1 mug896 mug896 64 2018-05-25 07:18 3 -> /proc/5241/fd/
2.
anonymous pipe 만들어 보기
$ cat | cat &
[1] 8703
[1]+ Stopped cat | cat
$ echo $! # 두번째 cat pid
8703
$ jobs -p %+ # 첫번째 cat pid
8702
# 두번째 cat 명령의 stdin 을 FD 3 번에 입력으로 연결
# 첫번째 cat 명령의 stdout 을 FD 4 번에 출력으로 연결
$ exec 3< /proc/8703/fd/0 4> /proc/8702/fd/1
# cat 프로세스 모두 종료
$ kill %1
[1]+ Terminated cat | cat
$ ls -l /dev/fd/ # anonymouse pipe 가 생성되었다! :)
total 0
lrwx------ 1 mug896 mug896 64 2018-05-25 08:52 0 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 08:52 1 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 08:52 2 -> /dev/pts/7
lr-x------ 1 mug896 mug896 64 2018-05-25 08:52 3 -> pipe:[1121731]
l-wx------ 1 mug896 mug896 64 2018-05-25 08:52 4 -> pipe:[1121731]
lr-x------ 1 mug896 mug896 64 2018-05-25 08:52 5 -> /proc/8845/fd/
$ echo 111 >&4
$ echo 222 >&4
$ echo 333 >&4
$ read var <&3; echo $var
111
$ read var <&3; echo $var
222
$ read var <&3; echo $var
333
$ exec 3>&- 4>&-
$ ls -al /dev/fd/
total 0
dr-x------ 2 mug896 mug896 0 2018-05-25 08:55 ./
dr-xr-xr-x 9 mug896 mug896 0 2018-05-25 08:55 ../
lrwx------ 1 mug896 mug896 64 2018-05-25 08:55 0 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 08:55 1 -> /dev/pts/7
lrwx------ 1 mug896 mug896 64 2018-05-25 08:55 2 -> /dev/pts/7
lr-x------ 1 mug896 mug896 64 2018-05-25 08:55 3 -> /proc/8990/fd/