파이프(pipe)
Tcl/Tk에서 외부 명령을 실행할 때, exec 명령뿐만 아니라 open 명령을 사용하여 파이프라인을 오픈할 수 있습니다. 이 경우, 파이프를 통해 실행된 외부 명령과 데이터의 주고받기가 가능합니다.
커맨드라인의 파이프
셸의 커맨드라인에서 사용하는 파이프는 한 프로그램의 표준 출력에서 출력된 데이터를 다른 프로그램의 표준 입력으로 전달하는 역할을 합니다. 데이터를 물의 흐름으로 생각하면, 실제로 파이프를 사용해 프로그램을 연결하여 데이터를 흘려보내는 것입니다. 파이프는 UNIX 계열 OS뿐만 아니라, MS-DOS에도 있는 기능입니다. 예를 들어, 다음과 같은 예를 보세요.
> sort file.dat | uniq
sort는 텍스트 파일을 정렬하는 명령이고, uniq는 중복된 행을 삭제하는 명령입니다. | 기호가 파이프를 나타내며, 이것으로 sort와 uniq가 파이프로 연결됩니다.
파이프의 제어는 셸(MS-DOS라면 command.com)이 담당합니다. 하지만 MS-DOS처럼 여러 프로그램을 동시에 실행할 수 없는 환경에서는 다음과 같이 파일을 경유하여 데이터가 전달됩니다.
sort file.dat > temp
uniq < temp
del temp
결국 MS-DOS의 경우, 파이프라고 해도 배치 처리와 동일해집니다.
하지만 UNIX 계열 OS나 Windows처럼 여러 프로그램을 동시에 실행할 수 있는 환경에서는 파일을 경유할 필요가 없습니다. 파이프는 "큐(queue)"와 같은 것이라고 생각하면 됩니다. 만약 파이프가 가득 차서 데이터를 쓸 수 없다면, OS에 의해 쓰기 측 프로그램은 휴면 상태에 들어갑니다. 그리고 파이프에서 데이터가 꺼내져 써도 되는 상태가 되면, OS가 휴면 중인 프로그램을 깨워 데이터를 파이프에 씁니다. 파이프가 비어있는 상태에서 데이터를 읽으려 한다면, 반대로 읽기 측 프로그램이 휴면 상태가 됩니다.
이러한 OS의 동작으로 인해, 실행 중인 프로그램 간에 파이프를 통해 데이터를 주고받을 수 있습니다. 이것을 "프로세스 간 통신"이라고 합니다. 프로세스는 실행 중인 프로그램이라고 생각하면 됩니다. 파이프를 통해 양방향으로 데이터를 주고받는 것도 가능합니다. 프로세스 간 통신에는 여러 가지 방법이 있지만, 그중에서도 파이프는 손쉽게 사용할 수 있는 방법입니다.
파이프 사용법
그럼 Tcl에서 파이프를 사용하는 방법을 설명하겠습니다. Tcl에서 파이프를 오픈하려면 open 명령을 사용합니다. open은 전달된 파일명의 첫 글자가 파이프 기호 |라면, 그 인수를 파일명이 아니라 명령으로 실행하여 파이프를 생성합니다. 다음 예를 보세요.
set f1 [open "|prog1" r]
set f2 [open "|prog2" w]
파일과 마찬가지로 파이프를 오픈할 때는 액세스 모드를 지정합니다. 그리고 open은 파이프에 액세스하기 위한 식별자(문자열)를 반환합니다.
첫 번째 예는 파이프를 읽기 오픈하는 경우입니다. 명령 prog1을 실행하고, 표준 출력으로 출력된 데이터가 파이프에 보내져 gets로 읽어올 수 있습니다. 다음 예는 파이프를 쓰기 오픈하는 경우입니다. 이 경우 puts로 표준 출력에 출력된 데이터는, 파이프를 통해 실행된 명령 prog2의 표준 입력으로 보내집니다. 마지막에는 close 명령으로 파이프를 닫는 것을 잊지 마세요.
그리고 Tcl에서는 파이프를 양방향(읽기/쓰기 모두)으로 오픈할 수 있습니다. 이 경우, 실행된 프로그램과 양방향으로 데이터를 주고받을 수 있습니다. 다음 예를 보세요.
set f3 [open "|prog3" r+]
액세스 모드 r이나 w 뒤에 +를 붙이면 갱신 모드가 되어, 입력과 출력을 모두 할 수 있게 됩니다. 파이프의 경우 r+와 w+ 어느 쪽도 상관없지만, 일반 파일에서 갱신 모드를 지정할 때는 r과 w의 차이에 주의하세요. r+는 파일이 존재하지 않으면 에러가 발생합니다. 또한 w+로 기존 파일을 오픈하면 길이를 0으로 잘라내기 때문에, 그 파일의 내용이 사라집니다.
또, 파이프를 사용할 때는 출력 데이터의 "버퍼링"에도 주의해야 합니다. 일반적으로 데이터 입출력은 1바이트 단위로 하면 효율이 나쁘기 때문에, 데이터를 모으는 버퍼를 준비하는 것이 보통입니다. 데이터를 출력할 때는 버퍼에 데이터를 모아두었다가,
버퍼가 가득 차면 그 내용을 비웁니다. C 언어의 표준 입출력 라이브러리에는 버퍼링 기능이 내장되어 있고, Tcl/Tk도 입출력은 버퍼링되어 있습니다. 이 때문에 puts로 데이터를 출력만 해서는 데이터를 프로그램에 보낼 수 없는 경우도 있습니다.
데이터를 프로그램에 보내려면, 버퍼의 내용을 비우는 명령 flush를 사용하세요. 혹은 fconfigure 명령으로 버퍼링 모드를 변경해도 됩니다.
fconfigure 식별자 -buffering 지정 -buffersize 바이트수
- full : 버퍼가 가득 차면 flush
- line : 줄바꿈 문자마다 flush
- none : 출력 명령이 실행될 때마다 flush
옵션 -buffering의 기본값은 full로 설정되어 있습니다. 버퍼 크기는 옵션 -buffersize로 변경할 수 있습니다.
줄바꿈 문자를 만났을 때 버퍼를 flush하는 동작을 "라인 버퍼링"이라고 합니다. 일반적으로 표준 출력의 동작은 라인 버퍼링이지만, 화면에 출력하지 않을 때, 예를 들어 파일로 리디렉션하거나 파이프에 연결되어 있을 때는 풀 버퍼링으로 바꾸는 프로그램도 있습니다. Tcl/Tk 측에서 데이터를 보내도, 상대 프로그램이 출력을 버퍼링하기 때문에 Tcl/Tk 측에서 데이터를 받을 수 없고, 프로그램이 동작하지 않을 수도 있습니다. 간단한 예를 들어보겠습니다.
set f [open "|pipetest" "r+"]
for {set i 0} {$i < 5} {incr i} {
puts "$i 를 보냅니다\n"
puts $f $i
flush $f
gets $f line
puts "$line 를 받았습니다"
after 1000
}
close $f
이 프로그램은 tclsh에서 동작합니다. 처음에 파이프를 양방향으로 오픈합니다. 실행하는 프로그램은 pipetest입니다. 이것은 C 언어로 작성합니다. 그 후 1초마다 puts로 숫자를 pipetest에 보내고, gets로 pipetest로부터 데이터를 받습니다. 데이터를 쓴 후에는 flush로 버퍼를 비우는 것에 주의하세요.
다음과 같이 pipetest.c를 작성합니다.
#include <stdio.h>
#include <stdlib.h>
#define SIZE 256
int main()
{
char buffer[SIZE];
while( fgets( buffer, SIZE, stdin ) != NULL ){
printf( "%d\n", atoi( buffer ) * 10 );
fflush( stdout );
}
return 0;
}
프로그램의 내용은 간단하며, 표준 입력으로부터 데이터를 받아 정수값으로 변환한 후, 그것을 10배로 하여 표준 출력으로 출력합니다. fflush()는 버퍼를 비우는 함수입니다.
이 프로그램을 DOS 창에서 실행하는 경우, fflush()가 없어도 정상적으로 동작합니다. 하지만 Tcl과 파이프를 사용해 데이터를 주고받을 때는, 표준 출력에 쓴 데이터가 버퍼링되기 때문에 fflush()로 버퍼를 비우지 않으면 정상적으로 동작하지 않습니다. 파이프를 사용할 때는 실행하는 프로그램의 동작에도 주의하세요.