Buffering
대부분의 명령들이 출력을 할 때 버퍼를 사용합니다. 왜냐하면 작은 양의 데이터를 받는 즉시 매번 출력을 하는 것보다는 버퍼에 데이터가 찼을 때 출력하는 것이 효율적이기 때문입니다. buffering 은 shell 에서 명령을 사용할때 뿐만 아니라 python, java 같은 언어에서 입,출력을 할 때도 적용되는 개념입니다.
데이터를 입력받고 연산결과를 출력하는데 stream 을 이용하는 명령들은 내부적으로 버퍼를 이용합니다 ( grep, sed, awk ...). 명령 실행이 바로 종료되면 버퍼에 있던 내용도 모두 출력되므로 문제가 없지만 프로세스가 종료되지 않은 상태에서 출력을 지속한다면 버퍼와 관련해서 문제가 생길 수 있습니다.
다음은 프로세스 A 가 logfile 에 로그를 append 하면 tail 명령으로 실시간으로 데이터를 추출해서 프린트하는 명령인데 실행해보면 정상적으로 동작하지 않습니다. 프로세스 A 가 ERR 로그를 logfile 에 append 했으므로 파일에는 로그가 존재함에도 불구하고 grep, awk 명령을 거치면서 출력이 되지 않고 있는 것입니다. 나중에 로그가 쌓여서 출력할 데이터가 4096 bytes 가되면 그때 한번에 출력이 됩니다.
$ tail -f logfile | grep ERR | awk '{ print $3 }'
이와 같은 상황을 그림으로 나타내면 다음과 같습니다. 먼저 kernel 쪽 버퍼는 kernel 에서 사용하는 것으로 사용자가 컨트롤할 수 있는 부분이 아니고 버퍼링과 관련해서 문제가 되지는 않는다고 합니다. 사용자가 컨트롤할 수 있는 부분은 프로그램이 사용하는 버퍼인데 tail 명령에서는 -f
옵션을 주어 출력이 버퍼링 되지 않지만 grep 명령에서 출력이 버퍼링 되고 있는 것을 볼 수 있습니다.
[출처] http://www.pixelbeat.org/programming/stdio_buffering/
Buffering modes
Buffering modes 는 다음 3 가지로 구분해 볼 수 있습니다.
- line buffered : newline 을 만나면 출력합니다.
- full buffered : 버퍼가 차면 출력합니다.
- unbuffered : 버퍼링을 하지 않고 바로 출력합니다.
명령이 실행되면 자동으로 stdin, stdout, stderr 세 개의 stream 이 생성되는데 버퍼와 관련해서 각각 다음과 같은 특징이 있습니다. ( 기본적으로 새로 open 되는 stream 은 full buffered 입니다. )
- stdin : 터미널에서 입력을 받으면 line buffered or unbuffered 이고 그외는 full buffered 입니다.
- stdout : 터미널에 연결되어 있으면 line buffered 이고 그외는 full buffered 입니다.
- stderr : 항상 unbuffered 입니다. 그러므로 stderr 로 메시지를 출력하면 바로 표시가 됩니다.
위 예제의 경우를 적용해보면 tail 명령은 -f 옵션을 주었으므로 unbuffered 에 해당되고 grep 명령은 출력이 파이프에 연결되어 있으므로 full buffered 가되며 awk 는 터미널에 연결되어 있으므로 line buffered 가 됩니다. 그러므로 문제를 해결하기 위해서는 grep 명령의 출력 버퍼링을 수정해 주어야 합니다.
문제 해결 하기
버퍼를 이용하는 대부분의 명령들은 버퍼링으로 인해 발생할 수 있는 문제를 해결하기 위해 별도의 옵션이나, 함수를 제공합니다. 위에서 예로든 grep, sed, awk 명령 외에도 perl, python, java 같은 언어들도 모두 동일하게 적용됩니다. 이런 언어들은 보통 내부적으로 버퍼링을 컨트롤할 수 있는 함수를 제공합니다.
- tail : -f
- grep : --line-buffered
- sed : -u,--unbuffered
- gawk : fflush(), close()
- perl : flush(), close(), $|
- tcpdump : -l
- python : -u , flush(), close()
- java : flush(), close()
그러므로 위의 문제를 해결하기 위해서는 다음과 같이 작성하면 됩니다.
$ tail -f logfile | grep --line-buffered ERR | awk '{ print $3 }'
버퍼와 관련된 옵션을 제공하지 않는 경우
모든 명령이 버퍼와 관련된 옵션이나 함수를 제공하는 것은 아닙니다. 다음의 경우 cut, tr 명령은 별도로 버퍼를 컨트롤할 수 있는 옵션을 제공하지 않아 문제를 해결할 수 없습니다.
$ tail -f access.log | cut -d ' ' -f1 | tr A-Z a-z | uniq
이럴 때에는 gnu coreutils 명령 중에 하나인 stdbuf 명령을 이용하여 해결할 수 있습니다.
$ tail -f access.log | stdbuf -oL cut -d ' ' -f1 | stdbuf -oL tr A-Z a-z | uniq
stdbuf
Usage: stdbuf OPTION... COMMAND
Options
-i, --input=MODE : stdin stream buffering 을 조정
-o, --output=MODE : stdout stream buffering 을 조정
-e, --error=MODE : stderr stream buffering 을 조정Modes
mode L : line buffered
mode 0 : unbuffered
full buffered 는 KB 1000, K 1024, MB 1000*1000, M 1024*1024 ... G, T, P, E, Z, Y 단위를 이용해서 버퍼 사이즈를 설정할 수 있습니다.
tee 명령같이 자체에서 버퍼링을 설정하는 경우나 dd, cat 명령같이 i/o 에 stream 을 사용하지 않는 경우에는 적용되지 않습니다.
sed, awk 명령의 출력 버퍼링 문제
다음은 coprocess 를 사용하는 예인데 sed, awk 명령이 background 로 실행을 지속하면서 stdout 이 파이프에 연결되어 출력이 버퍼링 되고 있습니다. 그러므로 read 명령으로 연산 결과를 읽을 수가 없습니다.
$ coproc sed 's/^/foo/'
[1] 5907
$ echo bar >& ${COPROC[1]}
$ read -r res <& ${COPROC[0]}
^C
--------------------------------
$ coproc awk '{ print $1 + $2 }'
[1] 7999
$ echo 1 2 >& ${COPROC[1]}
$ read -r res <& ${COPROC[0]}
^C
다음과 같이 -u | --unbuffered
옵션, fflush()
함수를 사용하여 해결할 수 있습니다.
$ coproc sed --unbuffered 's/^/foo/'
[1] 5993
$ echo bar >& ${COPROC[1]}
$ read -r res <& ${COPROC[0]}
$ echo $res
foobar
------------------------------------------
$ coproc awk '{ print $1 + $2; fflush() }'
[1] 8064
$ echo 1 2 >& ${COPROC[1]}
$ read -r res <& ${COPROC[0]}
$ echo $res
3
sed 명령의 입력 버퍼링 문제
sed 1q
는 입력받은 데이터에서 첫번째 라인을 출력하고 종료하는 명령입니다.
echo 명령으로 3 개의 라인을 파이프를 통해 전달하였으므로 각 sed 명령에서 한 라인씩 프린트할것 같지만 실제 결과는 첫번째 라인만 출력되고 있습니다.
원인은 첫번째 sed 명령의 input 버퍼로 3 개의 라인이 모두 저장되었기 때문입니다.
$ echo -e "ONE\nTWO\nTHREE" | { sed 1q ; sed 1q ; sed 1q ;}
ONE
다음과 같이 -u | --unbuffered
옵션을 사용하면 해결할 수 있습니다.
# 첫번째 sed 명령에 -u 옵션사용
$ echo -e "ONE\nTWO\nTHREE" | { sed -u 1q ; sed 1q ; sed 1q ;}
ONE
TWO
# 첫번째, 두번째 sed 명령에 -u 옵션사용
$ echo -e "ONE\nTWO\nTHREE" | { sed -u 1q ; sed -u 1q ; sed 1q ;}
ONE
TWO
THREE
redirection 의 경우에는 -u 옵션을 사용하지 않아도 됩니다.
$ cat file
ONE
TWO
THREE
$ { sed 1q; sed 1q; sed 1q ;} < file
ONE
TWO
THREE
$ { sed 1q; sed 1q; sed 1q ;} <<< $'ONE\nTWO\nTHREE'
ONE
TWO
THREE
sort 명령
sort, column, wc 같은 명령은 이전 명령 실행이 바로 종료되지 않는 경우에는 사용할 수 없습니다.
Buffer sizes
Default Buffer sizes
- ulimit -p ( 8 * 512 = 4096 )
- stdin, stdout 이 터미널에 연결되어 있으면 default size = 1024, 그외는 4096
버퍼가 자동으로 flush 되는 경우
- 버퍼가 full 되었을때
- stream 이 close 될때
- 프로그램이 종료될때
- line buffered 모드에서 newline 을 만났을때
- input stream 에서 파일로부터 데이터를 읽어들일때
strace 를 이용해 버퍼링 모드 확인해보기
strace 명령을 이용하면 프로세스 간에 데이터가 전달될 때 실제 사이즈를 알아볼 수 있습니다. 테스트에는 sed 명령을 이용하였는데요. 좌측에는 노란색으로 read, write 시스템 콜 함수가 표시되고 우측에는 녹색으로 실제 전달되는 바이트수가 표시됩니다.
첫 번째 예는 sed 명령의 stdin, stdout 이 모두 파이프에 연결되어 있는 경우입니다. read, write 모두 4096 바이트 크기의 full buffered 모드로 데이터가 전달되고 있는것을 볼 수 있습니다.
두 번째 예는 sed 명령이 명령 파이프라인의 마지막에 위치하여 stdout 이 터미널에 연결되어 있는 경우입니다. read 는 이전과 같이 파이프에 연결되어 있으므로 full buffered 이지만 write 할 때는 라인 단위로 하는 것을 볼 수 있습니다.
마지막으로 세 번째는 sed 명령의 --unbuffered
옵션을 사용한 경우입니다.
stdin, stdout 이 모두 파이프에 연결되어 있지만 read 는 1 바이트씩 읽고 있고
write 또한 full buffered 가 아닌 것을 알 수 있습니다.
왜 버퍼링을 사용하나?
상대적으로 속도가 느린 장치에 접근 횟수를 줄일 수 있습니다.
데이터를 읽어들일때 매번 속도가 느린 디스크에 접속해서 읽어들이는 것보다는 한번 메모리 버퍼에 읽어들인 후에 버퍼에서 값을 읽어오는게 효율적입니다. 또한 쓸 때도 마찬가지로 매번 데이터를 쓸 때마다 속도가 느린 디스크에 접속해서 쓰는 것보다는 우선 메모리 버퍼에 쓰기를 한후에 나중에 디스크에 접속해서 한번에 쓰는것이 효율적입니다.
처리 속도가 다른 프로세스 간에 block 되는 횟수를 줄일 수 있습니다.
두 프로세스 간에 데이터를 주고 받을때 한쪽이 처리속도가 느리면 다른쪽 프로세스는 더이상 진행하지 못하고 block 됩니다. 하지만 버퍼를 사용하면 이렇게 block 되는 횟수를 줄일 수 있습니다.
System call 횟수를 줄일 수 있습니다.
외부 장치에서 데이터를 입, 출력할 때는 사용자 프로그램이 단독으로 할 수 없고 kernel 에서 제공하는 system call 함수를 이용해야 합니다. system call 을 하면 사용자 프로그램이 실행되는 user mode 에서 kernel mode 로 switching 이 되는데 이것은 일반 함수 호출에 비해 오버헤드가 큰 작업입니다. 버퍼를 이용하면 system call 횟수를 줄일 수 있습니다.
Stream 에 대해서
변수에 값을 대입할때 사용하는 데이터나 함수를 호출할때 전달하는 데이터는 유한한 (finite) 값입니다. 그에 반해 stream 은 ( 예를를면 byte stream 같은 ) 기본적으로 unlimited 한 데이터를 말합니다. 그래서 처리하는 방식이 일반 데이터와 다릅니다. 보통 내부적으로 버퍼를 두어 처리하는데 파일을 읽고 쓸때, 네트워크 socket 을 이용해 통신할 때도 추상화된 stream 이 사용됩니다. I/O 디바이스 같은 경우도 무한정으로 데이터를 주고받기 때문에 stream 으로 볼 수 있습니다. stdin, stdout, stderr 도 standard streams 이라고 하며 명령의 실행 결과로 나오는 unlimited 한 데이터가 전달되어 표시됩니다. 또한 stream 은 pipe 를 통해서 다른 명령의 입력으로 전달됩니다.
C 프로그래밍 언어에서 input, output 을 하는 방법을 두 가지로 나누어 볼 수 있습니다.
하나는 직접 file descriptor 를 이용하는 low-level I/O 이고
다른 하나는 내부적으로 버퍼링을 제공하는 FILE *
구조체를 이용하는 것입니다.
두 번째가 high-level I/O 에 해당하는 stream 입니다.
stream 에 데이터 입, 출력이 일어나면 character-by-character 로 바로 전달되는 것이 아니라
내부 버퍼에 저장되었다가 block 단위로 전달됩니다.
fopen, fread, fwrite, fclose ... 와 같은 함수들이 stream 을 이용하는 함수에 해당합니다.
low-level I/O 는 가령 바이너리 파일을 large chunks 로 읽어 들인다거나
nonblocking, polling 같은 특수 모드를 사용할때 특정 디바이스를 control 할때 사용됩니다.
참고로 TCP socket 같은 경우 byte stream 이라고는 하지만 실제로 네트워크는 packet 단위로 전송이 됩니다. 중간에 버퍼가 있어서 writer 는 무한정 byte stream 을 쓰는 것처럼 보이고 reader 는 byte stream 을 읽는 것처럼 보이는 것뿐입니다. 버퍼가 차면 OS 는 잠시 프로세스를 중단시켰다가 버퍼가 비워지면 다시 writer 프로세스를 진행시킵니다. 또한 TCP 같은 경우 packet 을 전송하고 나서 바로 버리지 않고 acknowledgement 를 받을 때까지 가지고 있는데 (못 받을 경우 재전송하기 위해) 이것을 가지고 TCP 가 reliable 하다고 합니다. UDP 같은 경우는 그런게 없죠