본문 바로가기

Common Gateway Interface/Perl

[옛 강좌] 38. IPC의 기본적 이해

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

 이 게시물은 지금은 폐쇄되어 접속되지 않는 Kim Young Soo(http://hours.interpia98.net/~unisoo/)님의 웹 사이트에 2001년경 게시된 내용을 바탕으로 오늘날 웹 환경에 맞게 내용을 덧붙였습니다.

IPC의 기본적 이해

  • 글쓴이 : 이채광
  • 날짜 : 2000.01.05
  • 에디터 : 나모 웹에디터 3.0
  • 테스트 환경 : 평범한 펜티엄 PC, 리눅스(커널 2.2.5-22), perl5.00503
  • 연락처 : http://www.myperl.pe.kr/

0. 들어 가기 전에

 IPC(InterProcess Communication)의 기초적인 내용과 예제를 설명합니다. 혹시 IPC에 대해서 쉬운 부분만 나와 있어서 본인이 알고자 하는 내용이 없다면, 저는 좀 더 발전할 좋은 기회라 생각합니다. 저도 아직 부족한 부분이 너무도 많거든요. 아는 것도 별로 없지만 워낙 말빨이 약한지라...^^

IPC는 운영체제에 매우 민감한 부분입니다. 이 점을 염두에 두셔야 합니다. 저의 테스트 환경은 UNIX 호환 가능하며 POSIX를 따릅니다. 아래의 설명에서는 편의상 존칭을 생략하였습니다. 그리고 혹 저의 무지나 실수가 있거든 거침없는 지적을 부탁드립니다. 연락처의 email이나 게시판을 이용해 주세요.

1. 프로세스란

 process : 프로그램의 실행 단위. 태스크와는 달리 복수의 프로그램에 의한 병행 처리는 가능하나 하나의 프로그램 내에서 태스크가 병행 동작하는 듯한 처리는 불가능하다. 그 의미에서 태스크와는 다르다. - '97 컴퓨터 용어사전, 1997, 영진출판사

 다음은 책 - 레드햇 리눅스 Ver 5.2,1999, 사이버 출판사 - 의 7.18절(프로세스와 작업제어)을 정리한 내용이다.

IPC(InterProcess Communication)
커널 모드 차원의 통신을 사용해서 프로세스간 정보를 서로 전달하는 것
좀비 프로세스
자식 프로세스가 소멸 되었음에도 불구하고 IPC가 정상적으로 이루어지지 않아서 부모 프로세스가 그 사실을 알지 못하는 경우에 해당하며, 불완전한 소멸 상태의 프로세스이다. 사용하던 메모리는 운영체제에게 반납하지만 부모 프로세스의 프로세스 테이블에는 남아있는 상태.
고아 프로세스
부모 프로세스가 소멸되면 자식 프로세스도 소멸되는데, 소멸되지 않고 남아 있는 프로세스를 일컫는다.
fork()와 exec()이란 두 개의 시스템 콜
fork()는 기존의 프로세스를 복제하는 역할을 하며 exec()는 복제된 프로세스를 특정한 프로그램에 맞게 변형시키는 역할을 한다.
시스템 콜(system call)
일반 명령어와 같이 사용자가 로그인 세션에서 직접 실행시킬 수 있는 명령어가 아니라 커널이 제공하는 저수준의 시스템 명령어이다. Win32의 API와 비슷하다.
프로세스 아이디(PID)
모든 프로세스는 고유의 번호를 가지고 있으므로써 다른 프로세스와 구별될 수 있다.
작업(job)과 태스크(task)
프로그램이나 스크립트와 같이 사용자가 실행시킬 수 있는 일의 단위를 작업(job)이라고 하는데, 컴퓨터는 이를 내부적으로 태스크(task)라는 보다 세분된 단위로 분리해서 처리하게 된다. 따라서 멀티태스킹(multi-TASKing)이란 컴퓨터의 내부적인 작업 처리 단위인 태스크를 동시에 여러 개 수행할 수 있다는 뜻이 되므로, 결국 다수의 프로그램을 함께 실행시킬 수 있다는 확장된 의미가 된다. 태스크와 거의 같은 의미로 사용되는 프로세스(process)는 진행되고 있는 프로그램의 실행 단위를 의미한다. 파이프는 하나의 작업(job), 다수의 프로세스(process)로 이루어진다.

2. 간단한 프로세스 사용

 backtick과 system()를 사용하는 것이 가장 심플한 방법이므로, 이들의 사용법을 알아 본다. 추가로 exec()와 system()도 비교해 보자.

1) backtick 사용해 보기

$date = 'date';
print "$date";
print 'date';
						

 실행 결과 >

Tue Jan 4 20:24:58 KST 2000
Tue Jan 4 20:24:58 KST 2000
						

 backtick은 date 프로그램을 실행시킨 후, 실행 결과를 리턴하므로 위의 두 경우는 같은 결과를 보인다. 위의 예에서 줄바꿈(\n)이 생략되었지만 date 명령의 결과 자체에 줄바꿈이 포함되어 있어서 줄을 바꾸게 된다. Perl process는 date 프로그램이 종료될 때까지 기다린 후에, 이후의 작업을 계속 진행해 간다. 좀비 프로세스는 펄이 자동으로 처리해 준다.

2) system() 사용해 보기

system ("date");
print system ("date");
						

 실행 결과>

Tue Jan 4 20:24:58 KST 2000
Tue Jan 4 20:24:58 KST 2000
0
						

 위의 backtick과는 달리 system()는 date 프로그램의 결과를 STDOUT로 보낸다. 그러므로 실행 결과를 변수에 저장할 수 없다. 대신 조금은 특별한 상태 값(status value)을 리턴한다. 프로그램이 제대로 실행되면, 0을 리턴하므로 주의해야 한다. 다음처럼...

system ("date") && die "cannot -- $!\n";

or나 ||가 아닌, && 이어야 한다.

system ("ls -l");
system ("ls", "-l");
						

 위의 코드는 똑같은 결과를 보인지만, 내부적으론 다르게 작동한다. 처음 것은(인자수가 1개인 경우) 셸을 통해서 'ls -l'이 실행된다. 두 번째 것은(인자수가 2개 이상인 겨우)는 셸을 경유하지 않으므로, 권장되는 방법이다. 셸을 경유하면 펄이 무척 고생을 할 뿐만 아니라, 안전하지 않다고 한다.

 날짜 팁 : 간편하게 날짜를 출력하는 경우. date 프로그램에서 KST가 생략되어 있다.

$date = localtime();
print "$date\n";
						

 실행 결과 >

Tue Jan 4 20:24:58 2000

3) exec() 사용해 보기

#!/usr/bin/perl -w
exec ("date");
print "after exec()\n"; 
						

 실행 결과>

 아래의 print문은 실행되지 않았음. (혹시 system()을 잘못 사용한 것 아닌가?)
Tue Jan 4  20:24:58 KST 2000
						

 경고 메시지를 볼 수 있다. 그리고 exec() 다음의 작업들은 실행되지 않았다. 왜냐하면 exec()는 date 프로그램을 실행시킨 후 곧바로 종료되기 때문이다. 위에서 print()를 삭제하면, 불평 메시지가 나타나지 않는 것을 볼 수 있다.

4) 주의 사항

 backtick에 의한 프로세스 생성은 명령을 완료한 후에 출력을 얻을 수 있고, system()은 출력을 얻을 수가 없다. exec()이 실패하면 원래의 프로세스가 계속 진행된다. 그러나 exec()이 성공하면, END블록과 객체 소멸자(destructor)가 자동적으로 실행되지 않는다. 다음에 나오는 open()와 함께, backtick과 system()는 좀비 프로세스를 자동 처리하며, 대부분의 non-UNIX에서도 작동한다. 그러나 fork(), exec(), pipe()는 그렇지 않다.

3. IPC 관련 함수

 이번에는 open(), pipe()와 추가로 times(), sleep()를 알아 보자.

1) open() 사용해 보기

 아래의 코드는 프로그램 이름이 double_string.pl이다. 표준 입력으로부터 하나의 문자열을 받아서 뻥튀긴다(?). open()에서 이 프로그램을 호출하고 표준 입력으로 문자열을 넘겨줄 것이다. 예를 들어, 표준 입력으로 문자열 'abc'를 입력하면 결과는 다음과 같다.

#!/usr/bin/perl -w
chomp ($stdin = <STDIN>);
foreach (split //, $stdin) { print; print; }
print "\n";
						

 실행 결과>

aabbcc

 아래 스크립트의 첫 번째 문단은 open(FH, "| program") 형태이고, 두 번째는 open(FH, "program |") 형태이다.

open(FH, "| program") 형태는 FH을 program의 표준 입력(STDIN)과 연결해 준다(! ARGV가 아님). 아래에서 WRITE 핸들에 문자열 'abc'를 출력하면, 이는 위에서 본 double_string.pl의 표준 입력으로 보내게 된다. 물론, 그 전에 double_string.pl 프로그램을 실행시킨다.

open(FH, "program |") 형태는 program의 출력 결과를 READ 핸들로 연결해 준다. 그러므로 READ로부터 무언가를 읽을 수 있는 것이다.

#!/usr/bin/perl -w
$pid = open (WRITE, "| ./double_string.pl") or die;
print WRITE "abc";
close (WRITE) or die;
print "pid=> $pid\n";
$pid = open (READ, "who|") or die;
while(<READ>) { print }
close (READ) or die;
print "pid=> $pid\n";
						

 실행 결과>

aabbccpid=>870
root tty1 Jan 4 20:39
second tty Jan 4 20:40
pid=>871
						

위의 실행 결과는 컴퓨터 환경에 따라 다르게 보인다.

2) 특별한 open() 사용해 보기

 아래의 스크립트는 입력 데이터에 줄 번호를 삽입해 주는 기능을 한다. | 다음에 program이 아니라 -(빼기 기호)가 나왔다. 이는 implicitly(암묵적으로)하게 pipe()를 실행시킨다. pipe()은 곧 설명할 것이다.

 program을 실행시키고 이를 Perl process와 연결했던 위의 예에서와는 달리, 본 프로세스를 두 개로 만든 후(forking) 이들을 서로 연결시킨다. 그러면 아래의 open (STDOUT, "|-") 대신에 open (STDIN, "-|")로 하면 어떻게 될까? 출력이 자식 프로세스의 입력이 된다.

 여기서는 이렇게만 알아두자! '4) 시그널 사용'에서 fork()에 대해서 다룰 것이다.

#!/usr/bin/perl -w
sub number
{
	my $kidpid;
	return if $kidpid = open (STDOUT, "|-");
	die "cannot fork: $!" unless defined $kidpid;
	while(<STDIN>)
	{
		printf "%d: %s", $., $_
	}
	exit;
}

number();

while(<>) {print}
close STDOUT;
						

3) pipe() 사용해 보기

 간단하게 말하면, 파이프는 두 개의 연결된 파일 핸들이다. 파이프를 만든 후에 이를 복제하므로(forking), 두 개의 프로세스 모두가 쓰기 핸들과 읽기 핸들도 똑같이 가지게 된다.

 그런데 이렇게 생성된 파일 핸들은 양방향 통신을 할 수 없다. 그래서 쓰고자 하는 프로세스는 읽기 핸들을 먼저 닫고 쓰기 핸들에 쓴 후에, 쓰기 핸들을 닫는다. 읽고자 하는 프로세스는 쓰기 핸들을 먼저 닫고 읽기 핸들에서 읽은 후에, 읽기 핸들을 닫아준다. 사용 절차가 이렇게 번거로운 이유는, 읽기 프로세스에서는 쓰기 핸들이 열려 있으면 더 읽을 데이터가 있는 줄 알고 계속해서 이를 기다린다. 운영체제가 이를 알려주지 않기 때문이다.

 여기서 사용된 IO::Handle.pm은 버퍼를 방지하기 위해 사용되었다. select()를 이용해서 직접 이 기능을 구현해도 된다.

 if 블록은 부모 프로세스가, else 블록은 자식 프로세스가 실행하게 된다. 자식 프로세스는 exit()를 통해서 삶을 마감하게 되며, 부모 프로세서는 waitpid()를 통해서 자식 프로세스가 사라질 때까지 기다린다.

#!/usr/bin/perl -w

use IO::Handle;

pipe (READ, WRITE);

WRITE->autoflush(1);

if($kidpid = fork())
{
	close READ;
	print WRITE "I am sending\n";
	close WRITE;
	waitpid ($kidpid, 0);
}
else
{
	die "cannot fork -- $!\n" unless defined ($kidpid);
	close WRITE;
	chomp ($input = <READ>);
	print "Input=> $input\n";
	close READ;
	exit;
}
						

 실행 결과 >

Input=> I am sending

 pipe()는 사용 방법이 불편하므로, open()의 "|-"와 "-|"를 많이 사용한다.

4) times()와 sleep() 사용해 보기

 IPC 관련 함수에 이들 함수가 있다. 여기서 times()는 실행 시간을(CPU) 측정하는데 사용하였고, sleep()는 잠시 하던 작업을 쉬게 하는데 사용되었다.

#!/usr/bin/perl -w

#@array = times;
#print "@array\n\n";

$cpu = (times)[0];
for ($i=0; $i<1000000; $i++) { }
$cpu = (times)[0] - $cpu;
printf ("CPU time : %.2f sec\n", $cpu);
print "before sleep\n"; 
sleep 3;
print "after sleep\n";
						

 실행 결과 >

0CPU time : 1.01 sec
before sleep
after sleep
						

 실행 결과에서, 두 번째 줄을 출력한 후 약 3초정도 지난 후에 세 번째 줄을 출력한다. sleep()은 매우 정밀하고 정확한 시간을 필요로 하는 곳에는 사용하지 않는게 좋다. 실행 시간을 측정하고자 하는 코드 전에 times[0]를, 코드 후에 times[0]를 넣는다. 그리고 이들의 차이를 구하면, 사용된 CPU 시간을 측정할 수 있다.

 스크립트의 3, 4번째 라인에 주석을 풀고 실행시켜 보라!

4. 시스널 사용

 시그널은 말 그대로 신호이다. 커널이나 키보드로부터 시그널이 오기도 하고, 프로그램에서도 시그널을 보낼 수 있다. 펄을 이용해서 사용 가능한 시그널을 보는 방법은 아래와 같다(5.004이상에서만).

foreach $sig(keys %SIG) { print "$sig\n" }

 우선 시스널에 대해서 단순하게 생각해 보자.

신호를 보냄->신호 접수->신호 처리자(핸들러)에게 신호를 넘겨줌->핸들러가 신호처리

1) 신호 보내기

 어떤 신호를 누구에게 보낼 것인가? 신호를 보내는데 사용되는 함수는 kill()이다.

kill ('USR1', $pid);

 SIGUSR1을 보내고자 하면, 귀찮은 부분은 펄이 처리해 주므로 앞의 SIG는 생략한다. 또는 시그널 번호로도 가능한데, 시스템에서는 실제로 시그널 번호로 처리되기 때문이다.

 그럼 누구에게 시그널을 보낼 것인가? 프로세스 고유의 번호(PID)면 된다.

2) 신호 접수

 %SIG을 이용한다. SIGUSR1 신호를 접수하고자 하면, $SIG{'USR1'}이라 적어주면 된다.

3) 핸들러에게 신호를 넘겨줌

 신호를 신호 처리자에게 넘겨주기 위해서는 다음과 같이 하면 된다.

$SIG{'USR1'} = 'subroutine';
$SIG{'USR1'} = \&subroutine;
$SIG{'USR1'} = sub { ... };
						

위의 예에서는 두 번째가 권장된다.

4) 신호 처리

 핸들러는 서브루틴이다. 여기에는 시그널을 받은 후에 보이는 반응이 들어 있다. 여기서는 $sigusr1을 1에서 0으로 바꾸었다. 0과 1은 엄청난 차이 아닌가?

 아래의 예제에서는 자식 프로세스가 생성된다. 이는 출력을 규칙적으로 하게 되는데 잠시 후 부모 프로세스는 시그널(SIGUSR1)을 보내서 자식 프로세스를 제거하게 된다.

 시그널 SIGUSR1은 시스템에 의해서는 생성되지 않는 시그널이므로, 프로그램에서는 안전하게 사용할 수 있다. 이런 이유로 예제에서 SIGUSR1을 사용했다.

#!/usr/bin/perl -w

use strict;

my $sigusr1 = 1;
my $kidpid;

sub sigusr1_handler
{
	$sigusr1 = 0;
}

print "Kid is playing outside......\n";
print "mum : \"Come in, my dear!\"\n";

if ($kidpid=fork())
{
	## I am parent
	sleep 4;
	kill ('USR1', $kidpid);
}
else
{
	die "cannot fork -- $!\n" unless defined ($kidpid);
	## I am child
	print "kid : \"I wanna play more!\"\n";
	$SIG{'USR1'} = \&sigusr1_handler;
	## $SIG{'USR1'} = 'sigusr1_handler';
	
	while ($sigusr1)
	{
		sleep 1;
		print "kid : \"lalala land ~~ \"\n";
	}
	
	print "After ten minutes, kid comes back\n";
	
	exit;
}

waitpid ($kidpid, 0);

## wait;

print "mum : \"Take dinner\"\n";
print "kidpid=> $kidpid\n";

 실행 결과 >

Kid is playing outside......
mum : "Come in, my dear!"
kid : "I wanna play more!"
kid : "lalala land ~~"
kid : "lalala land ~~"
kid : "lalala land ~~"
kid : "lalala land ~~"
After ten minutes, kid comes back
mum : "Take dinner"
kidpid=> 894
						

 fork()는 프로세스를 복제하는 기능을 한다. 부모 프로세스에게는 자식 프로세스의 ID($kidpid)를, 자식 프로세스에게는 0을 돌려 준다. 만약 fork()의 실패로 복제가 안 되면 $kidpid가 정의되지 않는다. 위에서는 die()를 이용해 프로그램이 종료된다.

 위의 waitpid()wait()를 번갈아 주석처리 하고 실행시켜 보라. 그리고 둘 다 주석처리 한 후 실행시켜 보라!

  • wait() : 부모 프로세스로 하여금 자식 프로세스가 완전히 종료할 때까지 기다리게 한다.
  • waitpid() : 부모 프로세스로 하여금 특정한 자식 프로세스가 완전히 종료할 때까지 기다리게 한다.

5. 앞으로의 과제

  • 다양한 프로세스 관련 함수 사용해 보기
  • 좀비 프로세스 방지 하기
  • 모듈(IPC::Open2, IPC::Open3)
  • 다양한 시그널의 이해