Named Pipe

| 파이프를 이용해 명령들을 연결하여 사용하거나 명령, 프로세스 치환을 사용하면 명령 실행 중에 자동으로 pipe 가 생성되어 사용된 후 사라지게 되는데요. 이때 생성되는 파이프를 이름이 없다고 해서 unnamed pipe 또는 anonymous pipe 라고 합니다. 이에 반해 named pipe 는 직접 파이프를 파일로 만들어 사용합니다. shell 에서 IPC (Inter Process Communication) 이 필요할때 활용할 수 있습니다.

named pipe 는 파일과 동일하게 사용될 수 있는데 파일과 다른 점은 redirection 을 이용해 데이터를 출력했을 때 파일은 데이터를 저장하는 반면 pipe 는 저장하지 않는다는 점입니다. 그래서 만약에 디스크 용량이 부족한 상태에서 용량이 큰 파일을 다루고자 할 때 pipe 를 이용하면 프로세스 중간에 임시파일을 만들지 않아도 되므로 디스크 사용을 피할 수 있습니다. 다음은 gzip 으로 압축돼 있는 mysql 데이터 파일을 압축 해제하여 mypipe 로 출력하고 mysql 프롬프트 상에서 named pipe 를 이용해 테이블에 로드 하는 예입니다.

$ mkfifo /tmp/mypipe       # 또는 mknod /tmp/mypipe p
$ 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 는 korea.gnu.org 80 에 접속하는데 두 명령은 | 로 연결돼 있으므로 브라우저의 request 가 gnu.org 로 전달되게 됩니다. 이때 gnu.org 에서 받은 결과는 두번째 nc 명령의 stdout 으로 출력되는데 지금은 > mypipe 로 연결돼 있으므로 결과가 > mypipe 로 보내지고 다시 첫번째 nc 명령의 < mypipe 를 통해 입력으로 들어가서 브라우저까지 도달하게 됩니다.

$ mkfifo mypipe
$ nc -l localhost 8080 < mypipe | nc korea.gnu.org 80 > mypipe

$ nc -l localhost 8080 < mypipe | nc ftp.debian.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 line < mypipe; echo $line
111
$ read line < mypipe; echo $line
222
$ read line < mypipe; echo $line
333

$ exec 3>&-

Pipe 는 block 된다.

위의 FIFO 예에서 데이터를 mypipe 로 입력할때 exec 3<> mypipe 를 사용한 것을 볼 수 있습니다. 이것은 pipe 로 데이터를 전달할 때 데이터를 읽는 상대편이 없으면 block 되기 때문입니다. 앞서 이야기했지만 pipe 는 파일과 달리 데이터를 저장하지 않기 때문에 읽는 상대편이 없으면 작업이 중단됩니다. 또한 writer 가 없는 상태에서 읽기를 시도할 때도 block 됩니다. 다시 말해 pipe 는 writer 와 reader 가 서로 연결되어 있어야 작업이 진행될 수 있으며 writer 가 쓰기를 완료하고 종료하게 되면 reader 도 함께 종료하게 됩니다.

Broken pipe 에러

writer 와 reader 가 서로 연결된 상태에서 writer 가 먼저 종료하는 것은 오류가 되지 않지만 writer 가 지속적으로 pipe 에 쓰고 있는 상태에서 reader 가 먼저 종료하게 되면 writer 는 Broken pipe 오류로 종료하게 됩니다.

$ mkfifo mypipe

# 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

위와 동일한 상황이 다음과 같이 read 명령을 while 문에서 사용하는 것입니다. read line < mypipe 명령이 실행을 완료하면 mypipe 와의 연결이 끊기므로 writer 가 Broken pipe 에러로 종료됩니다. 따라서 두 번째와 같이 mypipe 를 while 문의 done 키워드에 연결해야 합니다.

$ for (( i=1; i<=10; i++ )) do echo $i; done > mypipe &
[1] 22564

$ while read line < mypipe; do echo "$line"; done
1
2
3
[1]+  Broken pipe             for ((i=1; i<=10; i++ ))    # writer 종료
--------------------------------------------------------------------

$ for (( i=1; i<=10; i++ )) do echo $i; done > mypipe &
[1] 22597

$ while read line; do echo "$line"; done < mypipe     # mypipe 를 done 키워드에 연결
1
2
3
4
5
6
7
8
9
10
[1]+  Done                    for ((i=1; i<=10; i++ ))

Reader 의 자동 연결 해제

writer 와 reader 가 서로 pipe 에 연결된 상태에서 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 는 읽을 데이터가 없으므로 파이프 연결이 해제되고 다음 프롬프트가 뜬다.

이와 같은 상황은 다음과 같이 main 프로세스가 while 문에서 파이프를 읽고 있는 상태에서 background 프로세스가 파이프에 메시지를 쓰는 경우입니다. 이럴 경우 처음 연결된 프로세스의 메시지가 전달된 후에는 reader 에 해당하는 main 프로세스가 종료되므로 나머지 background 프로세스들은 write block 상태에 놓이게 됩니다.

$ cat test.sh
#!/bin/bash

for ((i=1; i<=5; i++)) do        # 5 개의 background 프로세스 생성 (writer)
( 
    sleep 0.$(( RANDOM % 5 ))
    echo process $i .... > mypipe
) &
done

while read line; do              # main 프로세스 (reader)
    echo main read: $line
done < mypipe
-------------------------------------------

$ ./test.sh 
main read: process 4 ....       # 4 번째 background 프로세스가 main 프로세스와 연결됨
                                # ( 메시지가 2 개가 보일수도 있습니다. 동시에 실행되므로 )

# writer 인 4 번째 background 프로세스가 종료되어 reader 인 main 프로세스도 종료되고
# 나머지 background 프로세스들은 write block 상태에 놓이게 됩니다.
$ ps jf | grep 'test.s[h]'
    1  7847  7840  4926 pts/15    7867 S     1000   0:00 /bin/bash ./test.sh
    1  7843  7840  4926 pts/15    7867 S     1000   0:00 /bin/bash ./test.sh
    1  7842  7840  4926 pts/15    7867 S     1000   0:00 /bin/bash ./test.sh
    1  7841  7840  4926 pts/15    7867 S     1000   0:00 /bin/bash ./test.sh

$ kill -- -7840   # background 프로세스 그룹 종료

FD 를 pipe 에 연결해서 사용

위에서 살펴본 바와 같이 파이프는 상대편의 연결 상태에 따라 broken pipe 오류로 writer 가 종료되거나 아니면 읽을 데이터가 없는 reader 는 파이프에서 자동으로 연결이 해제됩니다. 이와 같은 특성은 파이프에서 읽어들일 데이터가 없더라도 지속적으로 연결을 유지하고 있다가 데이터가 들어올 경우 처리하고자 할때나 또는 writer 가 지속적으로 데이터를 쓰고 있는 상태에서 reader 가 일시적으로 연결을 해제하고자 할때 장애가 됩니다. 이와 같은 경우에 FD 를 파이프에 연결하여 사용하면 문제를 해결할 수 있습니다.

# named pipe 는 시스템에서 모두 접근할 수 있는 파일이기 때문에 (file system namespace)
# 이 명령은 아무 프로세스에서 한번만 실행하면 됩니다.  
# 그리고 설정한 프로세스가 종료하면 연결도 해제됩니다.

$ mkfifo mypipe

$ 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
...

활용 예제 )

1.

다음은 named pipe 를 이용해 중단 없이 데이터 처리하는 예입니다.

2.

이번에는 fifo_data, fifo_even, fifo_odd 3개를 만들어서 프로세스 A 가 fifo_data 에 데이터를 입력하면 프로세스 B 는 입력받은 데이터를 짝수와 홀수로 분류해서 fifo_even, fifo_odd 로 각각 전달합니다. 프로세스 C 는 fifo_even 로부터 데이터를 입력받아 짝수를 처리하고 프로세스 D 는 fifo_odd 로부터 홀수를 처리합니다.

실행 중에 짝수를 처리하는 프로세스 C 를 종료하거나 또는 코드에 다음과 같이 sleep 을 추가하면 홀수를 처리하는 프로세스 D 의 출력이 어떤 식으로 변하는지 한번 테스트해보세요

$ awk '@load "time"; { gsub(/^|$/,"#"); print; sleep(0.01) }
    END { print "end..." }' fifo_even

Multiple writers and readers

파이프는 별다른 기능 없이 단순 byte stream 입니다. 따라서 복수개의 프로세스가 동시에 파이프로부터 읽기를 하거나 쓰기를 하면 전체 바이트 수는 올바르게 전달되지만 라인 단위로는 제대로 분배가 안됩니다. 단 하나 보장되는 것은 두 프로세스가 atomic write 에 해당하는 4096 바이트 내에서 라인 단위로 쓰기를 하면 두 라인이 겹치지는 않습니다.

다음은 multiple writers, single reader 의 예입니다. 2 개의 프로세스가 동시에 파이프로 쓰기를 하고 1 개의 프로세스가 파이프를 읽어서 파일로 저장합니다. 두 프로세스가 echo 명령을 이용해 라인 단위로 atomic write 을 하고 있기 때문에 라인이 겹치지 않고 올바로 전달되는 것을 볼 수 있습니다. 이때 파일로 저장되는 순서는 예측할 수 없습니다

$ mkfifo mypipe

# 2 개의 프로세스에서 10,000 라인씩 파이프로 쓰기 합니다.
$ for (( i=1; i<=10000; i++ )) do echo "$i"; done > mypipe & 
[1] 20134

$ for (( i=1; i<=10000; i++ )) do echo "--- $i"; done > mypipe &
[2] 20151

# single reader : 파이프를 읽어서 파일 x1 로 저장합니다.
$ while read v; do echo "$v"; done < mypipe > x1

$ wc -l x1     # 합계 20,000 라인
20000 x1

$ head x1      # 저장되는 순서는 예측할 수 없다.
1
--- 1
2
--- 2
3
4
--- 3
5
--- 4
--- 5

다음은 multiple readers, single writer 의 예인데 이 경우에는 정상적으로 라인 단위로 읽기가 되지 않습니다. writer 의 경우는 자기가 쓰는 내용을 알고 있기 때문에 라인 단위로 쓰는 게 가능하지만 reader 의 경우는 파이프에서 5 문자 뒤에 newline 이 올지 10 문자 뒤에 newline 이 올지 알 수 없기 때문에 라인 단위로 읽는다는 것은 meaningless 합니다.

# 2 개의 프로세스가 동시에 하나의 파이프에서 읽기를 하여 각각 파일로 저장합니다. 
$ while read v; do echo "$v"; done < mypipe > x1 &        
[1] 20445

$ while read v; do echo "$v"; done < mypipe > x2 &
[2] 20460

# single writer : 전체 20,000 라인을 파이프로 쓰기합니다.
$ for (( i=1; i<=20000; i++ )) do echo "<$i>"; done > mypipe

$ wc -l x1 x2    # 합계 라인 개수는 올바르게 출력되지만 
  9783 x1
 10217 x2
 20000 total

$ less x1        # 파일을 열어보면 정상적으로 라인 단위로 읽기가 되지 않는다.
. . .
<1570>
<1572>
<1575>
5><1578>
<1580
<1583>
15<1586>
<1588><1589>
<151<1592>
14<1595>
<157
<1600>
<1603>
<1606>
. . .

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-`uname -r`/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 파일 사이즈가 4096 * 16 = 65,536 으로 나옴
$ ls -l outfile
-rw-rw-r-- 1 mug896 mug896 65536 08.03.2015 15:33 outfile

양방향 통신

파이프는 단방향이기 때문에 두 프로세스가 서로 대화를 나누기 위해서는 두개의 파이프가 필요합니다. 이와같은 방법을 이용하는 것이 keyword 명령중에 하나인 coproc 입니다. coproc 는 입, 출력 용으로 두개의 파이프를 만들어서 입력 파이프를 통해 외부 프로세스로부터 입력을 받고 연산결과를 출력 파이프를 통해 전달합니다.

bidirectional pipe

Socket

named pipe 의 경우 프로세스 A 가 파이프에 값을 쓰면 프로세스 B 가 값을 읽을 수 있습니다. 또한 프로세스 B 가 파이프에 값을 쓰면 프로세스 A 가 값을 읽을 수 있습니다. 이것은 양방향 통신이 가능한 것처럼 보이지만 문제는 프로세스 A 가 값을 쓴 후에 다시 자신이 파이프를 읽으면 값이 읽혀서 이후에 프로세스 B 는 값을 읽을 수 없게 됩니다. unix domain socket 은 named pipe 와 같이 디렉토리에 socket 파일을 만들어서 시스템 내의 프로세스와 통신을 하는데 이때 생성되는 socket 파일은 다른 프로세스가 접속할때 사용하는 ip 주소와 같은 역할을 합니다. 커널이 교통정리를 해주므로 socket 에 연결된 하나의 FD 만으로도 양방향 통신을 할 수 있습니다.

socket_diagram1

터미널에서 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 상태인데요. nc -l -U 로 실행한 서버 소켓은 처음에는 socket FD 가 하나였다가 ( listen ) 클라이언트가 접속을 하여 ( accept ) 하나가 추가되었습니다. 클라이언트 프로세스는 하나의 FD 만 사용하고 있는 것을 볼 수 있는데 이때 FD 에 연결된 socket 을 보면 뒤에 붙은 번호가 각각 틀립니다. 다시 말해서 프로세스 별로 다른 socket 을 사용하고 있는 것인데요. pipe 를 이용해 양방향 통신을 하는 coproc 의 FD 와 비교해 보시기 바랍니다.

socket fd

nc 명령은 동시에 여러개의 접속을 받지 못하는데 ncat 명령을 이용하면 테스트해볼 수 있습니다.
$ ncat -k -l -U mysock

unix domain socket 을 이용하는 대표적인 프로그램 중에 하나가 xwindows 서버인데요. /tmp/.X11-unix/ 디렉토리에 socket 파일이 위치합니다. 또한 ls /run 이나 lsof -U 명령을 실행하면 시스템 내에서 unix domain socket 을 사용하는 프로그램들을 볼 수 있습니다. ( pipe 를 이용하는 프로그램은 lsof | grep FIFO ).

unix domain socket 은 파일시스템에 socket 파일을 만들어 통신하기 때문에 같은 컴퓨터에서 실행되는 프로세스들에 한해서만 통신이 가능하다는 단점이 있습니다. internet domain socket 은 서로 떨어져 있는 컴퓨터에서 실행되는 프로세스들과도 통신을 할 수 있게 해줍니다. 이때는 데이터를 상자에 담아 송, 수신자 주소를 적어 보내는 packet 과 protocol 개념이 필요하게 됩니다 ( IPv4 or IPv6 ). 그러면 인터넷의 길목마다 위치해있는 router 라는 기계가 주소를 보고 해당 packet 을 목적지 까지 전달합니다. 도착한 packet 은 포트 번호에 의해 분류되어 해당 프로세스에게 전달됩니다.

socket_diagram2

nc 명령을 이용해 internet domain socket 도 테스트해볼 수 있습니다. 먼저 접속을 받는 computer1 에서 ifconfig 명령으로 ip 주소를 확인한 다음 nc -l 8080 명령을 실행하면 접속대기 상태가 됩니다. 이어 computer2 에서 computer1 의 ip 주소와 포트번호를 이용해 nc 12.34.56.78 8080 명령을 실행하면 두 프로세스가 연결되는데 이후부터 서로 메시지를 주고받을 수 있습니다.

  • computer1
# TCP                                    # UDP
$ nc -l 8080                             $ nc -l -u 8080
hello                                    hello
internet domain socket                   internet domain socket
  • computer2
# TCP                                    # UDP
$ nc 12.34.56.78 8080                    $ nc -u 12.34.56.78 8080
hello                                    hello
internet domain socket                   internet domain socket

다음은 socket 을 이용해서 tar 파일을 전송하는 예입니다.

  • computer1
# 먼저 tar 파일을 받는 컴퓨터에서 다음 명령을 실행합니다.
$ nc -l 8080 | tar xvz
  • computer2
# dir1 디렉토리를 tar 하고 파이프를 통해 nc 명령으로 전달합니다.
$ tar cvz ./dir1 | nc -N 12.34.56.78 8080
./dir1/
./dir1/socat
./dir1/ncat
./dir1/websock.sh
. . .
----------------------------------------------------
# 또는 bash 에서 제공하는 socket 연결 기능을 사용
$ tar cvz ./dir1 > /dev/tcp/12.34.56.78/8080     # 호스트 주소에 로컬호스트를 사용하려면
./dir1/                                          # localhost 또는 0.0.0.0
./dir1/socat
./dir1/ncat
./dir1/websock.sh
. . .

Bash 에서 제공하는 socket 연결 기능

bash 에서는 스크립트에서 직접 socket 에 연결할 수 있는 기능을 다음과 같은 형식을 통해 제공합니다. protocol 은 tcp 또는 udp 를 사용할 수 있습니다.

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.google.com/80

$ ls -l /dev/fd/
total 0
lrwx------ 1 mug896 mug896 64 2020-02-26 17:42 0 -> /dev/pts/13
lrwx------ 1 mug896 mug896 64 2020-02-26 17:42 1 -> /dev/pts/13
lrwx------ 1 mug896 mug896 64 2020-02-26 17:42 2 -> /dev/pts/13
lrwx------ 1 mug896 mug896 64 2020-02-26 17:42 3 -> socket:[4419777]    # socket
lr-x------ 1 mug896 mug896 64 2020-02-26 17:42 4 -> /proc/8058/fd/

$ echo hello >& 3
$ cat <& 3
HTTP/1.0 400 Bad Request
Content-Type: text/html; charset=UTF-8
Referrer-Policy: no-referrer
...
...
$ 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=`mktemp -u`
mkfifo $mypipe
exec {fd1}<> $mypipe    # fd1 를 named pipe 에 연결

num_of_jobs=10

do_job() {
    echo start job $i...
    sleep $((RANDOM % 5)) 
    echo ...done job $i
    exit $((RANDOM % num_of_jobs))
}

for i in `seq $num_of_jobs`; do
( 
    trap "echo job$i ---------- exit status: \$? >& $fd1" EXIT
    do_job 
) &
done

i=1
while read -r res; do
    echo "$res"
    [ $i -eq $num_of_jobs ] && break 
    let ++i
done <& $fd1

echo $i jobs done !!!

###########  output  ###########

start job 1...
start job 2...
start job 3...
start job 4...
start job 5...
...done job 4
job4 ---------- exit status: 0
...done job 1
...done job 5
job1 ---------- exit status: 3
job5 ---------- exit status: 0
...done job 3
job3 ---------- exit status: 4
...done job 2
job2 ---------- exit status: 0
5 jobs done !!!
  1. >& FD<& FD 대신에 > mypipe< mypipe 를 사용해도 되지만 mypipe 를 이용하는 것은 매번 open, close 과정을 거치게 되므로 FD 를 사용하는 것이 좋습니다.
  2. while 문에서 FD 를 done 키워드에 연결하지 않고 read -r res <& FD 와 같이 사용할 수도 있습니다. 생성한 FD 는 read 명령의 실행이 종료되어도 close 되지 않습니다.

다음 예제는 파이프에서 읽을 데이터가 없을 경우 프로세스가 블록 된다는 점을 이용해서 일종의 process pool 을 만드는 것입니다. multiple processes 가 동시에 하나의 파이프를 읽기 하는 것은 문제가 있지만 1 byte 씩은 괜찮습니다. fifo_work 파이프는 main 프로세스가 worker 프로세스들에게 1 byte 씩 전달하여 깨어나게 하는 용도로 사용되고 fifo_exit 파이프는 worker 프로세스가 main 프로세스에게 종료 상태 값을 전달하는 용도로 사용됩니다.

사용방법 )

  1. process pool 에 작업할당 ( x 이외의 문자사용 ) : 11 ( 2개 할당 ) 111 ( 3개 할당 ) ...
  2. worker 프로세스 종료 : x ( 1개 종료 ) xx ( 2개 종료 ) ...
  3. q : 전체 프로그램 종료
#!/bin/bash

fifo_work=`mktemp -u`   # main 프로세스가 worker 프로세스들에게 1 byte 씩 전달하는 용도
mkfifo $fifo_work
exec {fd_work}<> $fifo_work

fifo_exit=`mktemp -u`   # worker 프로세스가 exit status 를 main 프로세스에게 전달하는 용도
mkfifo $fifo_exit
exec {fd_exit}<> $fifo_exit

# named pipe 파일은 외부에서 다른 프로세스가 read, write 할 수 있기 때문에
# FD 를 named pipe 에 연결한 후에는 삭제합니다.
rm -f $fifo_work $fifo_exit

num_of_process=4               # process pool 프로세스 개수

do_job() (
    echo process $i running...
    sleep $((RANDOM % 5 + 2)) 
    echo ...process $i done.
    exit $((RANDOM % 10))
)

for i in `seq $num_of_process`; do
( 
    while read -n 1 val; do            # fd_work 으로 부터 1 byte 를 읽어들임
        if [ "$val" = "x" ]; then 
            echo process $i exit !!! 
            exit                       # 값이 "x" 이면 exit 합니다.
        else 
            do_job    # 작업이 완료되면 종료 상태 값을 fd_exit 으로 전달
            echo ">>>process $i exit status: $?" >& $fd_exit
        fi
    done <& $fd_work 
) &
done

while read -p "enter : " val; do
    [ "$val" = "q" ] && kill 0             # "q" : 전체 프로그램 종료
    num=$( echo -n "$val" | wc -c )
    echo -n "$val" >& $fd_work             # 입력받은 문자를 fd_work 으로 전달

    i=1
    if [[ $num -gt 0 && $val != *x* ]]; then    # 값이 "x" 가 아닐 경우 
        while read -r res; do                   # fd_exit 으로 부터
            echo "$res"                         # 종료 상태 값을 읽어들임
            [ "$i" -eq "$num" ] && break 
            let ++i
        done <& $fd_exit
    fi
done

Go 언어의 channel 은 파이프와 비슷하다.

멀티코어 CPU 를 좀더 쉽게 활용하기 위한 프로그래밍 기법과 언어들이 만들어지고 있는데요. 여기에 Go 언어는 channel 을 사용합니다. 그런데 이 channel 이 위에서 설명한 pipe 와 비슷하게 동작합니다 ( 파이프 업그레이드 버전이라고 보면 됨. 파이프 버퍼 사이즈도 사용자가 마음대로 설정할 수 있고, Multiple Readers, Writers 를 해도 문제가 없습니다 ). 다음은 web crawler 에서 이전에 방문했던 url 을 map 에 저장해서 재방문 하는것을 방지하는 것인데요. 여기서 map 은 여러 스레드가 동시에 사용하는 공유자원이 되는데 기존에는 lock 을 사용해 보호했다면 Go 에서는 channel 을 이용하면 lock 이 필요없게 됩니다.

// 두 번째 인수는 string 을 key 로 하고 bool 을 value 로 하는 map 을 전달하는 channel.
// 채널 이름 왼쪽에 붙는 <- 는 read 를 의미하고 오른쪽에 붙는 <- 는 write 을 의미합니다.
func Crawl (url string, visited_ch chan map[string]bool, ...기타인수) {
    visited := <-visited_ch      // channel 에서 map 을 하나 읽어들여 visited 는 map 이된다.
    found := visited[url]        // 이전에 url 을 설정하지 않았다면 found 는 false 가 된다.
    visited[url] = true          // map 에 url 을 key 로 하고 value 는 true 로 설정.
    visited_ch <- visited        // 설정이 완료되었으면 다시 map 을 channel 로 전달.
                                 // 첫 번째 라인에서 <-visited_ch (read 블록상태) 에 있던
    if found { return }          // 다른 스레드가 map 을 전달받게 되고 실행을 재개합니다.
    . . .                        // 따라서 lock 없이도 공유자원을 보호할 수 있게 됩니다.
}

File I/O 는 왜 동기적으로 처리될까?

비동기 프레임워크로 유명한 nodejs 에서도 file I/O 만큼은 따로 thread pool 을 두어서 동기적으로 처리하는데요. 이것은 실제 디스크에서 데이터를 가져오거나 쓰는데 시간이 걸리더라도 항상 ok 가 되기 때문입니다. linux 는 버전 5.1 부터 io_uring API 가 제공되는데요. 이것은 epoll 에서처럼 readiness model ( 준비가 됐을때 이벤트 발생 ) 이 아니라 completion model ( 완료가 됐을때 이벤트 발생 ) 을 사용합니다. 따라서 block devices 나 file I/O 에서도 비동기 처리가 가능합니다.

io_uring 도 내부적으로는 커널 스레드를 이용합니다. 그러니까 사용자 프로그램이 직접 thread pool 을 운용할 필요가 없어지는 거죠. nodejs 가 이용하고 있는 libuv 라이브러리 에서 io_uring 사용했을때 기존에 비해 8배 까지 throughput 이 증가했다고 합니다. (링크)

Quiz

named pipe 에 FD 를 연결한 후에 파일을 삭제하면 어떻게 될까요?

아래 테스트에서 보면 알 수 있듯이 파일이 삭제되어도 FD 는 정상적으로 사용할 수 있습니다.

1 . named pipe 생성 후 FD 연결

$ pipe=$(mktemp -u)      

$ mkfifo $pipe

$ ls -l $pipe
prw-rw-r-- 1 mug896 mug896 0 2020-03-16 21:30 /tmp/tmp.w1NNYBmni9|

$ file $pipe
/tmp/tmp.w1NNYBmni9: fifo (named pipe)

$ exec 3<> $pipe

$ ls -l /proc/$$/fd
total 0
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 0 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 1 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 2 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 255 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:15 3 -> /tmp/tmp.w1NNYBmni9|

$ echo 111 >&3
$ echo 222 >&3

$ read line <&3; echo $line
111
$ read line <&3; echo $line
222

2 . named pipe 파일 삭제

파일을 삭제한 후에 ls -l /proc/$$/fd 를 출력해 보면 here document 와 같이 deleted 로 나오는 것을 볼 수 있습니다. 파일이 삭제되어도 생성한 FD 는 정상적으로 사용할 수 있습니다.

$ rm -f $pipe    # named pipe 파일 삭제

$ ls -l /proc/$$/fd
total 0
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 0 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 1 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 2 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 255 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:15 3 -> /tmp/tmp.w1NNYBmni9 (deleted)

$ echo 111 >&3           
$ echo 222 >&3

$ read line <&3; echo $line
111
$ read line <&3; echo $line
222

3 . FD 삭제

/tmp/tmp.w1NNYBmni9 (deleted) 항목이 제거 되었습니다.

$ exec 3>&-

$ ls -l /proc/$$/fd
total 0
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 0 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 1 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 2 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 255 -> /dev/pts/14

이것은 unix domain socket 파일도 마찬가지입니다. 두 프로세스가 연결된 후엔 socket 파일을 삭제해도 문제가 없습니다.

2.

shell 에서 anonymous pipe 만들어 보기

$ cat | cat &

$ echo $!       # 두번째 cat pid
9218

$ jobs -p %+    # 첫번째 cat pid
9217

$ ls -l /proc/{9217,9218}/fd
/proc/9217/fd:
total 0
lrwx------ 1 mug896 mug896 64 2020-03-16 21:15 0 -> /dev/pts/14
l-wx------ 1 mug896 mug896 64 2020-03-16 21:15 1 -> pipe:[23509697]
lrwx------ 1 mug896 mug896 64 2020-03-16 21:15 2 -> /dev/pts/14

/proc/9218/fd:
total 0
lr-x------ 1 mug896 mug896 64 2020-03-16 21:15 0 -> pipe:[23509697]
lrwx------ 1 mug896 mug896 64 2020-03-16 21:15 1 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:15 2 -> /dev/pts/14

# FD 3 번을 두번째 cat 명령의 stdin 에 입력으로 연결
# FD 4 번을 첫번째 cat 명령의 stdout 에 출력으로 연결
$ exec 3< /proc/9218/fd/0 4> /proc/9217/fd/1

# cat 프로세스 모두 종료
$ kill %1
[1]+  Terminated              cat | cat

$ ls -l /proc/$$/fd            # anonymouse pipe 가 생성되었다!  :)
total 0
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 0 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 1 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 2 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 255 -> /dev/pts/14
lr-x------ 1 mug896 mug896 64 2020-03-16 21:15 3 -> pipe:[23509697]
l-wx------ 1 mug896 mug896 64 2020-03-16 21:15 4 -> pipe:[23509697]

$ echo 111 >&4
$ echo 222 >&4

$ read line <&3; echo $line
111
$ read line <&3; echo $line
222

$ exec 3>&- 4>&-

$ ls -l /proc/$$/fd
total 0
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 0 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 1 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 2 -> /dev/pts/14
lrwx------ 1 mug896 mug896 64 2020-03-16 21:13 255 -> /dev/pts/14

3 .

unix domain socket 이 IP 주소체계를 사용하지 않고 연결에 파일을 사용한다고 해서 동시에 여러 clients 와 통신할 수 없는 것은 아닙니다. 또한 SOCK_SEQPACKET 타입을 이용하면 메시지 바운더리가 유지되는 데이터그램을 사용할 수도 있습니다. 다음은 unix domain socket 을 이용해 multiple clients 로부터 접속을 받고 동시에 서버에서 multiple clients 로 메시지를 전송하는 예입니다.

man socket

SOCK_STREAMProvides sequenced, reliable, two-way, connection-based byte streams.
SOCK_DGRAMSupports datagrams (connectionless, unreliable messages of a fixed maximum length.
SOCK_SEQPACKETProvides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length
$ cat unix_server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <pthread.h>
#include <sys/epoll.h>

#define MAX_CONN 100
int clist[MAX_CONN] = {0};
int CUR_MAX = 0;
void error_exit(char *msg) { perror(msg); exit(1); }

void* runfunc(void *arg) 
{
    const int epollfd = (intptr_t)arg;
    struct epoll_event events[1];
    char buf[1024];
    puts("epoll start...");
    while (1) 
    {
        epoll_wait(epollfd, events, 1, -1);
        const int clientfd = events[0].data.fd;

        if (events[0].events & EPOLLHUP) {     // client 가 연결이 close 됐을경우
            epoll_ctl(epollfd, EPOLL_CTL_DEL, clientfd, NULL);
            clist[clientfd] = 0;
            close(clientfd);
            printf("DELE <<< sockfd %d\n", clientfd);
        } 
        else if (events[0].events & EPOLLIN) {     // client 로부터 입력이 있을경우
            int sc = sprintf(buf, "[ sockfd %d ] ", clientfd);
            int rc = read(clientfd, buf + sc, sizeof(buf) - sc - 1);
            rc += sc;
            buf[rc] = '\0';
            fputs(buf, stdout);
            for (int fd = 5; fd <= CUR_MAX; fd++)
                if (clist[fd] == 1) write(fd, buf, rc);
        } 
    }
    return NULL;
}

int main() 
{
    const int sockfd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
    struct sockaddr_un addr_un = {
        .sun_family  = AF_UNIX,
        .sun_path    = "server.sock",
    };
    if (bind(sockfd, (struct sockaddr *)&addr_un, sizeof(addr_un)) == -1)
        error_exit("bind()");
    listen(sockfd, 10);

    const int epollfd = epoll_create(1);
    struct epoll_event ev = { .events = EPOLLIN | EPOLLHUP };

    pthread_t thread;
    pthread_create(&thread, NULL, runfunc, (void *)(intptr_t)epollfd);
    pthread_detach(thread);

    while (1) {
        const int conn_sock = accept(sockfd, NULL, NULL);    // 새로 client 가 접속하면
        clist[conn_sock] = 1;
        for (int i = MAX_CONN - 1; i > 4; i--) 
            if (clist[i] == 1) { CUR_MAX = i; break; }
        ev.data.fd = conn_sock;
        epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev);   // epoll 에 등록
        printf("CONN >>> sockfd %d\n", conn_sock);
    }
}
$ cat unix_client.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/epoll.h>

void error_exit(char *msg) { perror(msg); exit(1); }

int main() 
{
    const int sockfd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
    struct sockaddr_un addr_un = {
        .sun_family  = AF_UNIX,
        .sun_path    = "server.sock",
    };
    if (connect(sockfd, (struct sockaddr *)&addr_un, sizeof(addr_un)) == -1)
        error_exit("connect()");

    const int epollfd = epoll_create(1);              // 입력을 stdin 과 socket
    struct epoll_event ev = { .events = EPOLLIN };    // 두 군데서 받아야 하기 때문에
    ev.data.fd = STDIN_FILENO;                        // I/O multiplexer epoll 을 사용
    epoll_ctl(epollfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
    ev.data.fd = sockfd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
    struct epoll_event events[1];

    char buf[1024];
    while (1) 
    {
        epoll_wait(epollfd, events, 1, -1);

        if (events[0].data.fd == STDIN_FILENO) {
            int rc = read(STDIN_FILENO, buf, sizeof(buf));
            if (rc == 0 || buf[rc - 1] != '\n') exit(0);    // Ctrl-d 를 눌렀을 경우 
            write(sockfd, buf, rc);                         // (Ctrl-d 는 signal 이 아님)
        } 
        else if (events[0].data.fd == sockfd) {
            int rc = read(sockfd, buf, sizeof(buf));
            if (rc == 0) break;
            write(STDOUT_FILENO, buf, rc);
        }
    }
}
$ gcc unix_server.c -pthread -o server

$ gcc unix_client.c -o client

4 .

이번에는 위와 같은 기능을 connectionless 한 datagram 을 이용해서 만들어보겠습니다. datagram 이라고 해서 internet domain socket 에서처럼 UDP 가 사용되는 것은 아닙니다. unix domain socket 이므로 datagram 이라도 reliable 하고 전송 order 가 유지됩니다. 코드를 작성할 때 한가지 생각해야될 점은 위에서처럼 server 만 소켓파일을 만들어서는 안됩니다 ( 그러면 client --> server 로만 메시지가 전달됩니다 ). datagram 은 TCP 와 달리 connectionless 하기 때문에 server 가 메시지를 다시 client 에게 전송하려면 각 client 들도 소켓파일 ( ip 주소 ) 를 가지고 있어야 합니다.

TCP 같은 경우는 connection 개념이 있어서 client 가 3-way handshake 을 거치면 연결된 것으로 간주하고 4-way handshake 을 거쳐 종료하면 연결이 끊긴것으로 간주합니다. 따라서 서버쪽 프로세스의 FD 를 조회해 보면 접속한 client 의 socket 이 생겼다 없어졌다 하는것을 볼 수 있지만 datagram 의 경우는 connection 개념이 없기 때문에 client 의 socket 이 생성되지 않고 연결 맺음과 끊긴 상황을 직접 처리해 주어야 합니다.

$ cat datagram_server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <signal.h>
#include <string.h>
#define sizeof_field(t, f) (sizeof(((t*)0)->f))

const char *sock_file = "server.sock";    // server 소켓파일 
void exit_handler() { unlink(sock_file); }
void INT_handler() { exit(2); }
void error_exit(char *msg) { perror(msg); exit(1); }

typedef struct client_list {
    char sun_path[ sizeof_field(struct sockaddr_un, sun_path) ];
    struct client_list *next;
} client_list_t;

client_list_t *clist;    // client 목록을 담고있는 단방향 linked list.

void add_client(const char *sun_path)    // 새로 client 가 접속하면 리스트에 추가.
{
    client_list_t *newc = malloc(sizeof(client_list_t));
    strcpy(newc->sun_path, sun_path);
    newc->next = clist->next;
    clist->next = newc;
    printf("CONN >>> %s\n", sun_path);
}

client_list_t* del_client(const char *sun_path)    // 리스트에서 client 삭제.
{
    client_list_t *cur = clist, *prev;
    while (strcmp(cur->sun_path, sun_path) != 0) {
        prev = cur;
        cur = cur->next;
        if (cur == NULL) return NULL;
    }
    prev->next = cur->next;
    printf("DELE <<< %s\n", cur->sun_path);
    free(cur);
    return prev;
}

int main(int argc, char *argv[])
{
    signal(SIGINT, &INT_handler);
    atexit(&exit_handler);
    clist = malloc(sizeof(client_list_t));
    clist->next = NULL;

    const int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);

    struct sockaddr_un serveraddr = { .sun_family = AF_UNIX };
    strcpy(serveraddr.sun_path, sock_file);

    // bind() 로 server 소켓파일 생성.
    if (bind(sockfd, (struct sockaddr *) &serveraddr, sizeof(serveraddr)) == -1)
        error_exit("bind()");

    struct sockaddr_un clientaddr = { .sun_family = AF_UNIX };
    char sbuf[1024 - sizeof_field(struct sockaddr_un, sun_path) - 2];
    char cbuf[1024];
    while (1) 
    {
        socklen_t addrlen = sizeof(serveraddr);
        int rc = recvfrom(sockfd, sbuf, sizeof(sbuf), 0, (struct sockaddr *) &serveraddr, &addrlen);
        sbuf[rc] = '\0';
        if (strcmp(sbuf, "\a\a\a") == 0) {    // "\a\a\a" 는 접속 시작을 나타내는 스트링.
            add_client(serveraddr.sun_path);
            continue;
        } else if (strcmp(sbuf, "\b\b\b") == 0) {    // "\b\b\b" 는 접속 종료를 나타냄.
            del_client(serveraddr.sun_path);
            continue;
        } else {
            printf("%s: %s", serveraddr.sun_path, sbuf);
        }

        client_list_t *head = clist;
        while (head->next != NULL) {
            head = head->next;
            strcpy(clientaddr.sun_path, head->sun_path);
            addrlen = sizeof(clientaddr);
            sprintf(cbuf, "%s: %s", serveraddr.sun_path, sbuf);
            rc = sendto(sockfd, cbuf, strlen(cbuf), 0, (struct sockaddr *) &clientaddr, addrlen);
            if (rc == -1)                            // client 로 전송이 실패하였다는 것은
                head = del_client(head->sun_path);   // client 소켓파일이 삭제되었다는
        }                                            // 의미이므로 리스트에서 client 삭제
    }
}
$ cat datagram_client.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <signal.h>
#include <sys/epoll.h>
#include <setjmp.h>

const char *server_sock = "server.sock";     // server 소켓파일
char client_sock[25];                        // client 소켓파일
jmp_buf env;

void exit_handler() { unlink(client_sock); }
void INT_handler() { longjmp(env, 3); }      // Ctrl-c 로 종료시 longjmp
void USR1_handler() { longjmp(env, 1); }     // Ctrl-d 로 종료시
void error_exit(char *msg) { perror(msg); exit(1); }

int main(int argc, char *argv[])
{
    signal(SIGINT, &INT_handler);
    signal(SIGUSR1, &USR1_handler);
    atexit(&exit_handler);
    sprintf(client_sock, "%s%u%s", "client_", getpid(), ".sock");

    // client --> server 접속을 위한 주소
    struct sockaddr_un serveraddr = { .sun_family = AF_UNIX };
    strcpy(serveraddr.sun_path, server_sock);

    // server --> client 접속을 위한 주소
    struct sockaddr_un clientaddr = { .sun_family = AF_UNIX };
    strcpy(clientaddr.sun_path, client_sock);

    const int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);

    // bind() 를 이용해 client 소켓파일을 만들어주어야 한다.
    if (bind(sockfd, (struct sockaddr *) &clientaddr, sizeof(clientaddr)) == -1)
        error_exit("bind()");

    // 여기서 connect() 는 꼭 필요한 것은 아니지만 connect() 를 하지 않으면 아래서
    // write(), read() 대신에 serveraddr 와함께 sendto(), recvfrom() 을 사용해야 합니다.
    if (connect(sockfd, (struct sockaddr *) &serveraddr, sizeof(serveraddr)) == -1)
        error_exit("connect()");

    char buf[1024] = "\a\a\a";     // 서버로 "\a\a\a" 스트링을 보내 접속을 알림
    write(sockfd, buf, 3);

    int ret = setjmp(env);
    if (ret != 0) {
        strcpy(buf, "\b\b\b");     // Ctrl-c 로 종료할경우 "\b\b\b" 를 보내 종료를 알림
        write(sockfd, buf, 3);
        exit(ret - 1);
    }
    const int epollfd = epoll_create(1);
    struct epoll_event ev = { .events = EPOLLIN };
    ev.data.fd = STDIN_FILENO;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
    ev.data.fd = sockfd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
    struct epoll_event events[1];
    while (1) 
    {
        epoll_wait(epollfd, events, 1, -1);

        if (events[0].data.fd == STDIN_FILENO) {
            int rc = read(STDIN_FILENO, buf, sizeof(buf));
            if (rc == 0 || buf[rc - 1] != '\n') raise(SIGUSR1);
            if (write(sockfd, buf, rc) == -1)    // 서버가 종료됐을때 쓰기는 오류가 된다
                error_exit("write()");
        } 
        else if (events[0].data.fd == sockfd) {
            int rc = read(sockfd, buf, sizeof(buf));
            write(STDOUT_FILENO, buf, rc);
        }
    }
}
$ gcc datagram_server.c -o server       # 실행을 해보면 server 와 client 들 각각 현재
                                        # 디렉토리에 소켓파일이 생성되는 것을 볼 수 있습니다.
$ gcc datagram_client.c -o client

datagram 이라 message boundary 가 유지되므로 사용자가 데이터를 입력하면 헤더를 붙여보내고 받는 쪽 서버에서는 헤더를 읽어들여 처리하는 식으로 할수도 있겠죠