펄 프로세스 간 통신
펄 프로세스 간 통신 (perlipc)
Perl의 프로세스 간 통신(IPC) 기본 기능은 옛날 좋은 Unix 신호(signal), 네임드 파이프, 파이프 open, 버클리 소켓 루틴, SysV IPC 호출 위에 구축되어 있어요. 각각은 조금씩 다른 상황에서 쓰인답니다.
신호 (Signals)
Perl은 단순한 신호 처리 모델을 사용해요: %SIG 해시가 사용자가 설치한 신호 처리기의 이름이나 참조를 담아요. 이 처리기들은 그것을 촉발한 신호의 이름인 인자 하나와 함께 호출돼요. 신호는 control-C나 control-Z 같은 특정 키보드 시퀀스에서 의도적으로 생성되거나, 다른 프로세스에서 보내지거나, 자식 프로세스의 종료, 자신의 프로세스가 스택 공간을 소진, 프로세스 파일 크기 한계 도달 같은 특별한 사건이 일어날 때 커널이 자동으로 촉발할 수 있어요.
예를 들어 인터럽트 신호를 잡으려면 다음과 같이 처리기를 설정하세요:
our $shucks;
sub catch_zap {
my $signame = shift;
$shucks++;
die "Somebody sent me a SIG$signame";
}
$SIG{INT} = __PACKAGE__ . "::catch_zap";
$SIG{INT} = \&catch_zap; # best strategy
Perl 5.8.0 이전에는 처리기에서 가능한 한 조금만 하는 것이 필요했어요. 우리가 전역 변수를 설정하고 예외를 일으키는 것밖에 안 하는 것에 주목하세요. 그 이유는 대부분의 시스템에서 라이브러리가 재진입(re-entrant)이 아니기 때문이에요. 특히 메모리 할당과 I/O 루틴이 그렇죠. 즉 처리기에서 거의 무엇이든 하는 것이 이론상 메모리 폴트와 그에 따른 core dump를 촉발할 수 있다는 뜻이었어요. 아래의 "연기된 신호 (안전한 신호)"를 참고하세요.
신호의 이름은 시스템에서 kill -l이 나열하는 것들이에요. 또는 CPAN 모듈 IPC::Signal로 가져올 수 있어요.
처리기로 문자열 "IGNORE"나 "DEFAULT"를 할당할 수도 있는데, 그 경우 Perl은 신호를 버리거나 기본 동작을 하려 할 거예요.
대부분의 Unix 플랫폼에서 CHLD(가끔 CLD라고도 함) 신호는 "IGNORE" 값에 대해 특별한 동작을 해요. 그런 플랫폼에서 $SIG{CHLD}를 "IGNORE"로 설정하면 부모 프로세스가 자식 프로세스에서 wait()를 하지 못했을 때 좀비 프로세스를 만들지 않는 효과가 있어요 (즉 자식 프로세스가 자동으로 거둬들여져요). 그런 플랫폼에서 $SIG{CHLD}가 "IGNORE"로 설정된 상태로 wait()을 호출하면 보통 -1을 반환해요.
KILL과 STOP(TSTP는 아님)처럼 잡을 수도 무시할 수도 없는 신호도 있어요. 신호를 무시하면 신호가 사라진다는 점을 참고하세요. 잃어버리지 않고 일시적으로만 차단하고 싶다면 POSIX 모듈의 sigprocmask를 사용해야 해요.
음수 프로세스 ID로 신호를 보내면 전체 Unix 프로세스 그룹으로 신호를 보내는 뜻이에요. 이 코드는 현재 프로세스 그룹의 모든 프로세스에 hang-up 신호를 보내고, 스스로 죽지 않게 $SIG{HUP}도 "IGNORE"로 설정해요:
# block scope for local
{
local $SIG{HUP} = "IGNORE";
kill HUP => -getpgrp();
# snazzy writing of: kill("HUP", -getpgrp())
}
보내기 흥미로운 또 다른 신호는 신호 번호 0이에요. 이는 실제로 자식 프로세스에 영향을 주지 않고, 살아 있는지 또는 UID를 바꿨는지 확인만 해요.
unless (kill 0 => $kid_pid) {
warn "something wicked happened to $kid_pid";
}
신호 번호 0은 프로세스가 살아 있어도, 그 프로세스의 실효/저장 UID가 보내는 프로세스의 실효/실제 UID와 다를 때는 보낼 권한이 없어 실패할 수 있어요. $!나 %!로 실패 원인을 알아낼 수 있을 거예요.
unless (kill(0 => $pid)||$!{EPERM}) {
warn "$pid looks dead";
}
간단한 신호 처리기에는 익명 함수를 쓰고 싶을 수도 있어요:
$SIG{INT} = sub { die "\nOutta here!\n" };
SIGCHLD 처리기에는 특별한 주의가 필요해요. 첫 번째 죽음으로 인한 신호 처리기 안에서 두 번째 자식이 죽으면, 우리는 또 다른 신호를 받지 못해요. 그래서 여기서 루프를 돌지 않으면 거둬들여지지 않은 자식을 좀비로 남겨둘 거예요. 그리고 다음에 두 자식이 죽으면 또 좀비가 생기고, 계속 그렇겠죠.
use POSIX ":sys_wait_h";
$SIG{CHLD} = sub {
while ((my $child = waitpid(-1, WNOHANG)) > 0) {
$Kid_Status{$child} = $?;
}
};
# do something that forks...
주의하세요: qx(), system(), 그리고 외부 명령을 호출하는 일부 모듈은 fork() 다음에 결과를 위해 wait()을 해요. 따라서 여러분의 신호 처리기가 호출될 거예요. system()이나 qx()가 이미 wait()을 호출했으므로, 신호 처리기의 wait()은 더 이상 좀비를 보지 못하고 따라서 블록될 거예요.
이 문제를 막는 가장 좋은 방법은 다음 예처럼 waitpid()를 사용하는 거예요:
use POSIX ":sys_wait_h"; # for nonblocking read
my %children;
$SIG{CHLD} = sub {
# don't change $! and $? outside handler
local ($!, $?);
while ( (my $pid = waitpid(-1, WNOHANG)) > 0 ) {
delete $children{$pid};
cleanup_child($pid, $?);
}
};
while (1) {
my $pid = fork();
die "cannot fork" unless defined $pid;
if ($pid == 0) {
# ...
exit 0;
} else {
$children{$pid}=1;
# ...
system($command);
# ...
}
}
신호 처리는 Unix에서 타임아웃에도 사용돼요. eval{} 블록 안에서 안전하게 보호하며, alarm 신호를 잡는 신호 처리기를 설정하고 몇 초 후에 배달되도록 예약해요. 그런 다음 블로킹 연산을 시도하고, eval{} 블록을 나가기 전이 아닌 끝났을 때 alarm을 지워요. 발동하면 die()로 블록에서 뛰어나와요.
예:
my $ALARM_EXCEPTION = "alarm clock restart";
eval {
local $SIG{ALRM} = sub { die $ALARM_EXCEPTION };
alarm 10;
flock($fh, 2) # blocking write lock
|| die "cannot flock: $!";
alarm 0;
};
if ($@ && $@ !~ quotemeta($ALARM_EXCEPTION)) { die }
타임아웃되는 작업이 system()이나 qx()라면 이 기법은 좀비를 생성하기 쉬워요. 이것이 중요하다면 스스로 fork()와 exec()를 하고 잘못된 자식 프로세스를 죽여야 해요.
더 복잡한 신호 처리를 위해 표준 POSIX 모듈을 볼 수 있어요. 유감스럽게도 이것은 거의 전적으로 문서화되어 있지 않지만, Perl 소스 배포의 ext/POSIX/t/sigaction.t 파일에 몇 가지 예가 있어요.
데몬에서 SIGHUP 신호 처리하기 (Handling the SIGHUP Signal in Daemons)
시스템이 부팅될 때 시작하고 종료될 때 꺼지는 프로세스를 데몬(daemon)이라고 해요. 데몬 프로세스에 시작된 후 수정된 설정 파일이 있다면, 프로세스를 멈추지 않고 설정 파일을 다시 읽으라고 말할 방법이 있어야 해요. 많은 데몬이 SIGHUP 신호 처리기로 이 메커니즘을 제공해요. 데몬에게 파일을 다시 읽으라고 하고 싶다면 SIGHUP 신호를 보내기만 하면 돼요.
다음 예는 SIGHUP 신호를 받을 때마다 스스로 재시작하는 간단한 데몬을 구현해요. 실제 코드는 code() 서브루틴에 있는데, 그냥 동작한다는 디버깅 정보를 출력하기만 해요. 실제 코드로 바꿔야 해요.
#!/usr/bin/perl
use v5.36;
use POSIX ();
use FindBin ();
use File::Basename ();
use File::Spec::Functions qw(catfile);
$| = 1;
# make the daemon cross-platform, so exec always calls the script
# itself with the right path, no matter how the script was invoked.
my $script = File::Basename::basename($0);
my $SELF = catfile($FindBin::Bin, $script);
$SIG{HUP} = sub {
print "got SIGHUP\n";
exec($SELF, @ARGV) || die "$0: couldn't restart: $!";
};
# Make sure SIGHUP isn't blocked (as it normally would be, due to the
# handler function calling exec rather than returning)
POSIX::sigprocmask(&POSIX::SIG_UNBLOCK,
POSIX::SigSet->new(&POSIX::SIGHUP));
code();
sub code {
print "PID: $$\n";
print "ARGV: @ARGV\n";
my $count = 0;
while (1) {
sleep 2;
print ++$count, "\n";
}
}
연기된 신호 (안전한 신호) (Deferred Signals (Safe Signals))
Perl 5.8.0 이전에는 신호를 다루는 Perl 코드를 설치하면 두 가지로부터 위험에 노출됐어요. 첫째, 소수의 시스템 라이브러리 함수만 재진입이에요. Perl이 한 함수(malloc(3)이나 printf(3) 같은)를 실행하는 동안 신호가 인터럽트하고, 신호 처리기가 같은 함수를 다시 호출하면 예측 불가능한 동작이 생길 수 있어요 — 종종 core dump가 되죠. 둘째, Perl 자체도 가장 낮은 수준에서 재진입이 아니에요. Perl이 자기 내부 데이터 구조를 바꾸는 동안 신호가 인터럽트하면, 마찬가지로 예측 불가능한 동작이 생길 수 있어요.
이를 알면 할 수 있는 일이 두 가지 있었어요: 편집증적으로 하거나 실용적으로 하거나. 편집증적인 접근은 신호 처리기에서 가능한 한 조금만 하는 거였어요. 이미 값이 있는 정수 변수를 설정하고 반환하죠. 하지만 느린 시스템 호출 안에 있다면 이건 도움이 안 되고, 그건 그냥 재시작될 거예요. 그렇다면 처리기에서 longjmp(3)로 나가기 위해 die를 해야 해요. 진짜 편집증 환자에게는 이것조차 조금 경솔해요. 시스템이 당신을 노리기 때문에 처리기에서 die를 피하죠. 실용적인 접근은 "위험을 알지만 편의를 선호한다"고 말하고, 신호 처리기에서 원하는 무엇이든 하고, 이따금 core dump를 치울 준비를 하는 거였어요.
Perl 5.8.0 이후는 신호를 "연기"함으로써 이 문제들을 피해요. 즉 시스템이 신호를 프로세스에(Perl을 구현하는 C 코드에) 배달하면 플래그가 설정되고 처리기가 즉시 반환해요. 그런 다음 Perl 인터프리터의 전략적인 "안전한" 지점(예: 새 opcode를 실행하려 할 때)에서 플래그가 확인되고 %SIG의 Perl 레벨 처리기가 실행돼요. "연기" 방식은 Perl 인터프리터가 안전한 상태에 있고 처리기가 호출될 때 시스템 라이브러리 함수 안에 있지 않다는 것을 알기 때문에, 신호 처리기의 코딩에 훨씬 더 많은 유연성을 허용해요. 하지만 구현은 이전 Perl과 다음과 같은 점에서 다릅니다:
오래 실행되는 opcode (Long-running opcodes)
Perl 인터프리터가 신호 플래그를 새 opcode를 실행하려 할 때만 보므로, 오래 실행되는 opcode(예: 매우 큰 문자열에 대한 정규식 연산) 동안 도착한 신호는 현재 opcode가 끝날 때까지 보이지 않아요.
어떤 주어진 유형의 신호가 opcode 동안 여러 번 발생하면(미세한 타이머처럼), 그 신호의 처리기는 opcode가 끝난 후 한 번만 호출되고, 다른 모든 인스턴스는 버려져요. 또한, 시스템의 신호 큐가 넘쳐서 opcode가 완료될 때 발생했지만 아직 잡히지(그리고 따라서 연기되지) 않은 신호가 있다면, 그 신호들은 후속 opcode 동안 잡혀서 연기될 수 있고, 때로 놀라운 결과를 내요. 예를 들어 alarm(0)은 alarm 발생을 멈추지만 이미 발생했지만 아직 잡히지 않은 alarm의 배달을 취소하지는 않으므로, alarm(0) 호출 후에도 alarm이 배달되는 것을 볼 수 있어요. 이 문단에 설명된 동작에 의존하지 마세요. 이는 현재 구현의 부작용이고 미래의 Perl 버전에서 바뀔 수 있어요.
IO 인터럽트 (Interrupting IO)
신호가 배달되면(예: control-C의 SIGINT) 운영체제는 Perl의 readline() 함수, 즉 <> 연산자를 구현하는 데 쓰이는 read(2) 같은 IO 연산에 끼어들어요. 이전 Perl에서는 처리기가 즉시 호출됐어요 (read가 "안전하지" 않으므로 잘 동작했죠). "연기" 방식에서는 처리가 즉시 호출되지 않고, Perl이 시스템의 stdio 라이브러리를 사용 중이라면 그 라이브러리가 Perl로 돌아와 %SIG 처리기를 호출할 기회를 주지 않은 채 read를 재시작할 수도 있어요. 시스템에서 이렇게 된다면 해결책은 IO에 :perlio 레이어를 사용하는 거예요 — 적어도 신호로 끼어들고 싶은 핸들에는 말이죠. (:perlio 레이어는 신호 플래그를 확인하고 IO 연산을 재개하기 전에 %SIG 처리기를 호출해요.)
Perl 5.8.0 이후의 기본값은 :perlio 레이어를 자동으로 사용하는 거예요.
신호가 같은 핸들의 I/O 연산을 인터럽트한 상태에서 신호 처리기 안에서 그 파일 핸들에 접근하는 것은 권장하지 않음을 참고하세요. Perl이 적어도 크래시하지 않도록 애쓰겠지만 데이터 무결성은 보장되지 않아요. 예를 들어 일부 데이터가 떨어지거나 두 번 쓰일 수 있어요.
gethostbyname() 같은 일부 네트워킹 라이브러리 함수는 자체 타임아웃 구현을 가져서 여러분의 타임아웃과 충돌할 수 있는 것으로 알려져 있어요. 그런 함수에 문제가 있다면 SigAction 객체와 함께 POSIX sigaction() 함수를 시도해 보세요. 이것은 safe 플래그를 명시적으로 설정하지 않는 한 Perl 안전 신호를 우회해요. 위에서 설명한 대로 이는 메모리 손상의 가능성에 노출시킬 수 있음을 경고해요.
$SIG{ALRM}을 설정하는 대신:
local $SIG{ALRM} = sub { die "alarm" };
다음과 같은 것을 시도하세요:
use POSIX qw(SIGALRM);
POSIX::sigaction(SIGALRM,
POSIX::SigAction->new(sub { die "alarm" }))
|| die "Error setting SIGALRM handler: $!\n";
안전 신호 동작을 로컬에서 비활성화하는 또 다른 방법은 CPAN의 Perl::Unsafe::Signals 모듈을 사용하는 것인데, 이는 모든 신호에 영향을 줘요.
재시작 가능한 시스템 호출 (Restartable system calls)
지원하는 시스템에서 이전 버전의 Perl은 %SIG 처리기를 설치할 때 SA_RESTART 플래그를 사용했어요. 이는 재시작 가능한 시스템 호출이 신호가 도착했을 때 반환하는 대신 계속된다는 뜻이에요. 연기된 신호를 신속히 배달하려고 Perl 5.8.0 이후는 SA_RESTART를 사용하지 않아요. 결과적으로 재시작 가능한 시스템 호출은 이전에는 성공했을 곳에서 ($!가 EINTR로 설정되어) 실패할 수 있어요.
기본 :perlio 레이어는 위에서 설명한 대로 read, write, close를 재시도해요. 인터럽트된 wait와 waitpid 호출은 항상 재시도돼요.
신호를 "결함"으로 (Signals as "faults")
SEGV, ILL, BUS, FPE 같은 특정 신호는 가상 메모리 주소 오류와 비슷한 "결함"에 의해 생성돼요. 이들은 보통 치명적이에요: Perl 레벨 처리기가 이들로 할 수 있는 일은 거의 없어요. 그래서 Perl은 연기하려 하지 않고 즉시 배달해요.
이들을 %SIG 처리기로 잡을 수는 있지만(perlvar 참고), "안전하지 않은" 신호의 평범한 문제에 더해 신호가 처리기 반환 직후 즉시 다시 던져질 가능성이 높으므로, 그런 처리기는 die나 exit를 해야 해요.
운영체제 상태로 촉발된 신호 (Signals triggered by operating system state)
일부 운영체제에서 특정 신호 처리기는 반환 전에 "무언가 해야" 하는 것으로 여겨져요. 한 예가 자식 프로세스가 완료됐음을 나타내는 CHLD나 CLD예요. 일부 운영체제에서 신호 처리기는 완료된 자식 프로세스에 대해 wait해야 한다고 기대돼요. 그런 시스템에서 연기된 신호 방식은 그 신호들에 대해 동작하지 않아요: wait을 하지 않으니까요. 운영체제가 아직 wait되지 않은 완료된 자식 프로세스가 있으므로 신호를 재발행하므로, 그 실패는 다시 루프처럼 보일 거예요.
메모리 손상 가능성에도 불구하고 옛 신호 동작을 되찾고 싶다면 환경 변수 PERL_SIGNALS을 "unsafe"로 설정하세요. 이 기능은 Perl 5.8.1에서 처음 등장했어요.
네임드 파이프 (Named Pipes)
네임드 파이프(흔히 FIFO라고 함)는 같은 머신에서 통신하는 프로세스들을 위한 옛 Unix IPC 메커니즘이에요. 일반 익명 파이프처럼 동작하지만, 프로세스들이 파일 이름으로 만나고 서로 관련될 필요가 없어요.
네임드 파이프를 만들려면 POSIX::mkfifo() 함수를 사용하세요.
use POSIX qw(mkfifo);
mkfifo($path, 0700) || die "mkfifo $path failed: $!";
Unix 명령 mknod(1)나 일부 시스템에서는 mkfifo(1)를 쓸 수도 있어요. 하지만 이들은 일반 경로에 없을 수도 있어요.
# system return val is backwards, so && not ||
#
$ENV{PATH} .= ":/etc:/usr/etc";
if ( system("mknod", $path, "p")
&& system("mkfifo", $path) )
{
die "mk{nod,fifo} $path failed";
}
FIFO는 프로세스를 관련 없는 프로세스와 연결하고 싶을 때 편리해요. FIFO를 열면 다른 쪽 끝에 무언가가 생길 때까지 프로그램이 블록돼요.
예를 들어 .signature 파일이 다른 쪽 끝에 Perl 프로그램이 있는 네임드 파이프가 되길 원한다고 해 보죠. 이제 어떤 프로그램(메일러, 뉴스 리더, finger 프로그램 등)이든 그 파일을 읽으려고 할 때마다, 읽는 프로그램이 여러분 프로그램에서 새 서명을 읽을 거예요. 파이프 확인 파일 테스트 연산자인 -p를 사용해 누군가(또는 무언가)가 실수로 FIFO를 제거했는지 알아낼 거예요.
chdir(); # go home
my $FIFO = ".signature";
while (1) {
unless (-p $FIFO) {
unlink $FIFO; # discard any failure, will catch later
require POSIX; # delayed loading of heavy module
POSIX::mkfifo($FIFO, 0700)
|| die "can't mkfifo $FIFO: $!";
}
# next line blocks till there's a reader
open (my $fh, ">", $FIFO) || die "can't open $FIFO: $!";
print $fh "John Smith (smith\@host.org)\n", `fortune -s`;
close($fh) || die "can't close $FIFO: $!";
sleep 2; # to avoid dup signals
}
IPC에 open() 사용하기 (Using open() for IPC)
Perl의 기본 open() 문은 open 모드를 |-나 -|로 지정하여 단방향 프로세스 간 통신에도 쓸 수 있어요. 쓰려는 자식 프로세스에서 무언가를 시작하는 방법:
open(my $spooler, "|-", "cat -v | lpr -h 2>/dev/null")
|| die "can't fork: $!";
local $SIG{PIPE} = sub { die "spooler pipe broke" };
print $spooler "stuff\n";
close $spooler || die "bad spool: $! $?";
읽으려는 자식 프로세스를 시작하는 방법:
open(my $status, "-|", "netstat -an 2>&1")
|| die "can't fork: $!";
while (<$status>) {
next if /^(tcp|udp)/;
print;
}
close $status || die "bad netstat: $! $?";
이 연산들은 완전한 Unix fork라는 점을 알아 두세요. 즉 일부 이기종 시스템에서는 올바르게 구현되지 않을 수 있어요. 이식성 세부 사항은 perlport의 "open"을 참고하세요.
open()의 두 인자 형태에서 파이프 open은 두 번째 인자에 파이프 기호를 덧붙이거나 앞에 붙여서 이룰 수 있어요:
open(my $spooler, "| cat -v | lpr -h 2>/dev/null")
|| die "can't fork: $!";
open(my $status, "netstat -an 2>&1 |")
|| die "can't fork: $!";
이것은 포킹을 지원하지 않는 시스템에서도 쓸 수 있지만, 파일을 읽으려는 코드가 예기치 않게 프로그램을 실행하게 할 수 있어요. 특정 프로그램이 open()의 두 인자 형태나 <> 연산자를 사용해 @ARGV의 파일 이름을 기대하는 Perl 스크립트라고 확신할 수 있다면, 영리한 프로그래머는 다음과 같이 쓸 수 있어요:
% program f1 "cmd1|" - f2 "cmd2|" f3 < tmpfile
어떤 종류의 셸에서 호출하든 Perl 프로그램은 f1 파일, cmd1 프로세스, 표준 입력(이 경우 tmpfile), f2 파일, cmd2 명령, 그리고 마지막으로 f3 파일에서 읽을 거예요. 꽤 멋지죠?
읽기용 파이프를 여는 것과 거의 같은 효과에 백틱을 쓸 수 있음을 알아차릴 수도 있어요:
print grep { !/^(tcp|udp)/ } `netstat -an 2>&1`;
die "bad netstatus ($?)" if $?;
표면적으로는 사실이지만, 파일을 한 번에 한 줄 또는 한 레코드씩 처리하는 것이 훨씬 더 효율적이에요. 그러면 전체를 한 번에 메모리로 읽지 않아도 되거든요. 또한 전체 프로세스를 더 세밀하게 제어할 수 있어 원하면 자식 프로세스를 일찍 죽일 수도 있어요.
open()과 close()의 반환 값을 모두 확인하도록 주의하세요. 파이프에 쓰고 있다면 SIGPIPE도 잡아야 해요. 그렇지 않으면 존재하지 않는 명령으로 파이프를 시작할 때 어떻게 될지 생각해 보세요: open()은 모든 가능성에서 성공하고(fork()의 성공만 반영하니까요), 하지만 그런 다음 출력이 실패할 거예요 — 장관으로요. Perl은 명령이 동작했는지 알 수 없어요. 여러분의 명령이 실제로 exec()가 실패했을 수 있는 별도 프로세스에서 실행 중이기 때문이에요. 따라서 가짜 명령의 읽는 쪽은 빠른 EOF만 반환하지만, 가짜 명령에 쓰는 쪽은 신호를 맞게 되며, 그것을 처리할 준비를 하는 게 가장 좋아요. 생각해 보세요:
open(my $fh, "|-", "bogus") || die "can't fork: $!";
print $fh "bang\n"; # neither necessary nor sufficient
# to check print retval!
close($fh) || die "can't close: $!";
print()의 반환 값을 확인하지 않는 이유는 파이프 버퍼링 때문이에요. 물리적 쓰기는 지연돼요. close 때까지 터지지 않고, SIGPIPE로 터질 거예요. 잡으려면 이렇게 할 수 있어요:
$SIG{PIPE} = "IGNORE";
open(my $fh, "|-", "bogus") || die "can't fork: $!";
print $fh "bang\n";
close($fh) || die "can't close: status=$?";
파일 핸들 (Filehandles)
메인 프로세스와 그것이 fork한 자식 프로세스들은 모두 같은 STDIN, STDOUT, STDERR 파일 핸들을 공유해요. 두 프로세스가 한 번에 그것들에 접근하려 하면 이상한 일이 생길 수 있어요. 자식을 위해 파일 핸들을 닫거나 다시 열 수도 있어요. open()으로 파이프를 열어 이 문제를 피할 수 있지만, 일부 시스템에서는 자식 프로세스가 부모보다 오래 살 수 없다는 뜻이에요.
백그라운드 프로세스 (Background Processes)
다음으로 명령을 백그라운드로 실행할 수 있어요:
system("cmd &");
명령의 STDOUT과 STDERR(셸에 따라 STDIN도)은 부모의 것과 같을 거예요. 이중 fork가 일어나기 때문에 SIGCHLD를 잡을 필요가 없어요. 자세한 내용은 아래를 참고하세요.
자식을 부모로부터 완전히 분리하기 (Complete Dissociation of Child from Parent)
어떤 경우(예: 서버 프로세스 시작)에는 자식 프로세스를 부모로부터 완전히 분리하고 싶을 거예요. 이를 흔히 데몬화(daemonization)라고 해요. 잘 행동하는 데몬은 또한 chdir()로 루트 디렉터리로 가서 자신이 시작된 디렉터리를 담은 파일시스템의 언마운트를 막지 않고, 표준 파일 서술자를 /dev/null로부터 그리고 로 리다이렉트해서 임의 출력이 사용자의 터미널에 나타나지 않게 해요.
use POSIX "setsid";
sub daemonize {
chdir("/") || die "can't chdir to /: $!";
open(STDIN, "<", "/dev/null") || die "can't read /dev/null: $!";
open(STDOUT, ">", "/dev/null") || die "can't write /dev/null: $!";
defined(my $pid = fork()) || die "can't fork: $!";
exit if $pid; # non-zero now means I am the parent
(setsid() != -1) || die "Can't start a new session: $!";
open(STDERR, ">&", STDOUT) || die "can't dup stdout: $!";
}
fork()는 프로세스 그룹 리더가 아니도록 보장하기 위해 setsid()보다 먼저 와야 해요. 프로세스 그룹 리더라면 setsid()는 실패할 거예요. 시스템에 setsid() 함수가 없다면 /dev/tty를 열고 그것에 TIOCNOTTY ioctl()을 사용하세요. 자세한 내용은 tty(4)를 참고하세요.
비Unix 사용자는 다른 가능한 해결책을 위해 *Your_OS*::Process 모듈을 확인하세요.
안전한 파이프 open (Safe Pipe Opens)
IPC에 대한 또 다른 흥미로운 접근은 단일 프로그램을 다중 프로세스로 만들어 그들 사이에서 — 또는 서로 간에 — 통신하는 거예요. open() 함수의 두 인자 형태는 "-|"나 "|-"의 파일 인자를 받아 아주 흥미로운 일을 해요: 여러분이 연 파일 핸들에 연결된 자식을 fork해요. 자식은 부모와 같은 프로그램을 실행해요. 이는 예를 들어 가정된 UID나 GID 아래에서 실행할 때 파일을 안전하게 여는 데 유용해요. minus에 로 파이프를 열면, 연 파일 핸들에 쓸 수 있고 자식은 자기 STDIN에서 그것을 찾을 거예요. minus에서 부터 파이프를 열면 자식이 자기 STDOUT에 쓰는 무엇이든 연 파일 핸들에서 읽을 수 있어요.
my $PRECIOUS = "/path/to/some/safe/file";
my $sleep_count;
my $pid;
my $kid_to_write;
do {
$pid = open($kid_to_write, "|-");
unless (defined $pid) {
warn "cannot fork: $!";
die "bailing out" if $sleep_count++ > 6;
sleep 10;
}
} until defined $pid;
if ($pid) { # I am the parent
print $kid_to_write @some_data;
close($kid_to_write) || warn "kid exited $?";
} else { # I am the child
# drop permissions in setuid and/or setgid programs:
($>, $)) = ($<, $();
open (my $outfile, ">", $PRECIOUS)
|| die "can't open $PRECIOUS: $!";
while (<STDIN>) {
print $outfile; # child STDIN is parent $kid_to_write
}
close($outfile) || die "can't close $PRECIOUS: $!";
exit(0); # don't forget this!!
}
이 구문의 또 다른 일반적인 용도는 셸의 간섭 없이 무언가를 실행해야 할 때예요. system()에서는 간단하지만, 파이프 open이나 백틱은 안전하게 쓸 수 없어요. 셸이 인자를 건드리는 것을 막을 방법이 없기 때문이에요. 대신 더 낮은 수준의 제어를 사용해 exec()를 직접 호출하세요.
안전한 읽기용 백틱 또는 파이프 open:
my $pid = open(my $kid_to_read, "-|");
defined($pid) || die "can't fork: $!";
if ($pid) { # parent
while (<$kid_to_read>) {
# do something interesting
}
close($kid_to_read) || warn "kid exited $?";
} else { # child
($>, $)) = ($<, $(); # suid only
exec($program, @options, @args)
|| die "can't exec program: $!";
# NOTREACHED
}
안전한 쓰기용 파이프 open:
my $pid = open(my $kid_to_write, "|-");
defined($pid) || die "can't fork: $!";
$SIG{PIPE} = sub { die "whoops, $program pipe broke" };
if ($pid) { # parent
print $kid_to_write @data;
close($kid_to_write) || warn "kid exited $?";
} else { # child
($>, $)) = ($<, $();
exec($program, @options, @args)
|| die "can't exec program: $!";
# NOTREACHED
}
이 open() 형태나, 실제로 다중 서브프로세스와 함께 pipe()를 쓰는 것은 프로세스를 데드락시키기 매우 쉽습니다. 위의 예는 단순하고 exec()를 호출하기 때문에 "안전"해요. 일반 안전 원칙은 "파이프 데드락 피하기"를 참고하세요. 하지만 Safe Pipe Open에는 추가 함정이 있어요.
특히 open $fh, "|-"로 파이프를 열었다면, 부모 프로세스에서 close()를 사용해 원하지 않는 작성자를 간단히 닫을 수 없어요. 이 코드를 생각해 보세요:
my $pid = open(my $writer, "|-"); # fork open a kid
defined($pid) || die "first fork failed: $!";
if ($pid) {
if (my $sub_pid = fork()) {
defined($sub_pid) || die "second fork failed: $!";
close($writer) || die "couldn't close writer: $!";
# now do something else...
}
else {
# first write to $writer
# ...
# then when finished
close($writer) || die "couldn't close writer: $!";
exit(0);
}
}
else {
# first do something with STDIN, then
exit(0);
}
위 예에서 진짜 부모는 $writer 파일 핸들에 쓰고 싶지 않으므로 닫아요. 하지만 $writer가 open $fh, "|-"로 열렸기 때문에 특별한 동작이 있어요: 닫으면 waitpid()를 호출하고(perlfunc의 "waitpid" 참고), 이는 서브프로세스가 종료되길 기다려요. 자식 프로세스가 "do something else"라고 표시된 섹션에서 일어나는 무언가를 기다리며 끝난다면 데드락이 생겨요.
이것은 더 복잡한 코드의 중간 서브프로세스에서도 문제가 될 수 있는데, 전역 소멸 중 모든 열린 파일 핸들에 대해 — 예측 가능한 순서 없이 — waitpid()를 호출하기 때문이에요.
이것을 해결하려면 아래처럼 pipe(), fork(), 그리고 한 파일 서술자를 다른 파일 서술자로 설정하는 open() 형태를 수동으로 사용해야 해요:
pipe(my $reader, my $writer) || die "pipe failed: $!";
my $pid = fork();
defined($pid) || die "first fork failed: $!";
if ($pid) {
close $reader;
if (my $sub_pid = fork()) {
defined($sub_pid) || die "first fork failed: $!";
close($writer) || die "can't close writer: $!";
}
else {
# write to $writer...
# ...
# then when finished
close($writer) || die "can't close writer: $!";
exit(0);
}
# write to $writer...
}
else {
open(STDIN, "<&", $reader) || die "can't reopen STDIN: $!";
close($writer) || die "can't close writer: $!";
# do something...
exit(0);
}
Perl 5.8.0부터 파이프에 open의 리스트 형태를 쓸 수도 있어요. 명령 문자열에 있을 수 있는 메타문자를 셸이 해석하는 것을 피하고 싶을 때 선호돼요.
예를 들어 다음을 쓰는 대신:
open(my $ps_pipe, "-|", "ps aux") || die "can't open ps pipe: $!";
이 둘 중 하나를 쓸 거예요:
open(my $ps_pipe, "-|", "ps", "aux")
|| die "can't open ps pipe: $!";
my @ps_args = qw[ ps aux ];
open(my $ps_pipe, "-|", @ps_args)
|| die "can't open @ps_args|: $!";
open()에 3개보다 많은 인자가 있으므로, 셸을 띄우지 않고 ps(1) 명령을 fork하고 $ps_pipe 파일 핸들을 통해 그 표준 출력을 읽어요. 명령 파이프에 쓰는 대응 문법은 "-|" 대신 "|-"를 사용하는 거예요.
솔직히 이 예는 좀 어리석어요. 내용이 완전히 안전한 문자열 리터럴을 쓰고 있으니까요. 따라서 읽기 어려운 다인자 형태의 파이프 open()에 의존할 이유가 없어요. 하지만 프로그램 인자가 셸 메타문자에서 자유롭다고 보장할 수 없을 때마다 더 화려한 open() 형태를 써야 해요. 예를 들어:
my @grep_args = ("egrep", "-i", $some_pattern, @many_files);
open(my $grep_pipe, "-|", @grep_args)
|| die "can't open @grep_args|: $!";
여기서 다인자 형태의 파이프 open()이 선호되는 이유는 패턴과 파일 이름 자신조차 메타문자를 담을 수 있기 때문이에요.
파이프 데드락 피하기 (Avoiding Pipe Deadlocks)
둘 이상의 서브프로세스가 있다면, 각각이 프로세스 간 통신을 위해 만든 어떤 파이프든 자신이 사용하지 않는 절반을 닫도록 주의해야 해요. 자식 프로세스가 파이프에서 읽고 EOF를 기대하면 결코 받지 못하므로 결코 종료하지 않기 때문이에요. 단일 프로세스가 파이프를 닫는 것만으로는 충분하지 않아요. 파이프를 연 마지막 프로세스가 EOF를 읽을 수 있게 닫아야 해요.
특정 내장 Unix 기능이 대부분의 경우 이를 막는 데 도움돼요. 예를 들어 파일 핸들에는 "close on exec" 플래그가 있고, 이는 $^F 변수의 제어 아래 일제히 설정돼요. 이는 자식 프로그램의 STDIN, STDOUT, STDERR로 명시적으로 라우팅하지 않은 파일 핸들은 자동으로 닫히게 하려는 거예요.
그 프로세스가 실제로 파이프에 쓰지 않는 한, 항상 파이프의 쓰기 가능한 끝에 명시적으로 그리고 즉시 close()를 호출하세요. 명시적으로 close()를 호출하지 않아도 Perl은 전역 소멸 중 모든 파일 핸들을 여전히 close()할 거예요. 앞서 논의했듯이 그 파일 핸들이 Safe Pipe Open으로 열렸다면 이는 waitpid()를 호출하게 되고, 다시 데드락할 수 있어요.
다른 프로세스와의 양방향 통신 (Bidirectional Communication with Another Process)
단방향 통신에는 꽤 잘 동작하지만, 양방향 통신은 어떨까요? 가장 명백한 접근은 동작하지 않아요:
# THIS DOES NOT WORK!!
open(my $prog_for_reading_and_writing, "| some program |")
use warnings를 잊으면 유용한 진단 메시지를 완전히 놓칠 거예요:
Can't do bidirectional pipe at -e line 1.
정말 원한다면 IPC::Open2 모듈의 표준 open2()를 사용해 양쪽 끝을 모두 잡을 수 있어요. 삼방향 I/O를 위한 IPC::Open3의 open3()도 있어서 자식의 STDERR도 잡을 수 있지만, 그러려면 어색한 select() 루프가 필요하고 정상적인 Perl 입력 연산을 쓸 수 없어요.
그 소스를 보면 open2()가 모든 연결을 만들기 위해 pipe()와 exec() syscall 같은 낮은 수준의 원시 요소를 사용하는 걸 볼 수 있어요. socketpair()를 쓰면 더 효율적이었겠지만, 이미 이식 가능하지 않은 것을 더욱 덜 이식 가능하게 만들었을 거예요. open2()와 open3() 함수는 Unix 시스템에서나, 아니면 최소한 POSIX 준수를 주장하는 곳에서만 동작할 가능성이 높아요.
open2() 사용 예:
use IPC::Open2;
my $pid = open2(my $reader, my $writer, "cat -un");
print $writer "stuff\n";
my $got = <$reader>;
waitpid $pid, 0;
여기의 문제는 버퍼링이 정말 여러분의 하루를 망친다는 거예요. $writer 파일 핸들이 자동 플러시되어 다른 쪽 끝의 프로세스가 제때 데이터를 받더라도, 그 프로세스가 비슷하게 빠르게 여러분에게 데이터를 주도록 강제할 수는 보통 없어요. 이 특별한 경우에는 실제로 그렇게 할 수 있는데, cat에 -u 플래그를 줘서 버퍼링 없게 만들었기 때문이에요. 하지만 파이프 위에서 동작하도록 설계된 명령은 거의 없어서, 양끝 파이프의 다른 쪽 끝에 있는 프로그램을 여러분 자신이 썼지 않는 한 이것은 거의 동작하지 않아요.
해결책은 프로그램이 더 합리적으로 행동하게 만들기 위해 pseudotty를 사용하는 라이브러리를 쓰는 거예요. 이렇게 하면 사용하는 프로그램의 소스 코드를 제어할 필요가 없어요. CPAN의 Expect 모듈도 이런 종류의 것을 다뤄요. 이 모듈은 CPAN의 다른 두 모듈 IO::Pty와 IO::Stty를 필요로 해요. 터미널 장치 드라이버와 대화하기를 고집하는 프로그램과 상호작용하기 위해 가상 터미널을 설정해요. 시스템이 지원된다면 이것이 최선의 선택일 수 있어요.
자기 자신과의 양방향 통신 (Bidirectional Communication with Yourself)
원한다면 낮은 수준의 pipe()와 fork() syscall을 사용해 손으로 이것을 엮을 수 있어요. 이 예는 자기 자신하고만 대화하지만, 적절한 핸들을 STDIN과 STDOUT으로 다시 열고 다른 프로세스를 호출할 수 있어요. (다음 예는 적절한 오류 검사가 없어요.)
#!/usr/bin/perl
# pipe1 - bidirectional communication using two pipe pairs
# designed for the socketpair-challenged
use v5.36;
use IO::Handle; # enable autoflush method before Perl 5.14
pipe(my $parent_rdr, my $child_wtr); # XXX: check failure?
pipe(my $child_rdr, my $parent_wtr); # XXX: check failure?
$child_wtr->autoflush(1);
$parent_wtr->autoflush(1);
if ($pid = fork()) {
close $parent_rdr;
close $parent_wtr;
print $child_wtr "Parent Pid $$ is sending this\n";
chomp(my $line = <$child_rdr>);
print "Parent Pid $$ just read this: '$line'\n";
close $child_rdr; close $child_wtr;
waitpid($pid, 0);
} else {
die "cannot fork: $!" unless defined $pid;
close $child_rdr;
close $child_wtr;
chomp(my $line = <$parent_rdr>);
print "Child Pid $$ just read this: '$line'\n";
print $parent_wtr "Child Pid $$ is sending this\n";
close $parent_rdr;
close $parent_wtr;
exit(0);
}
하지만 실제로 두 개의 pipe 호출을 할 필요는 없어요. socketpair() 시스템 호출이 있다면 이걸 전부 해줘요.
#!/usr/bin/perl
# pipe2 - bidirectional communication using socketpair
# "the best ones always go both ways"
use v5.36;
use Socket;
use IO::Handle; # enable autoflush method before Perl 5.14
# We say AF_UNIX because although *_LOCAL is the
# POSIX 1003.1g form of the constant, many machines
# still don't have it.
socketpair(my $child, my $parent, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
|| die "socketpair: $!";
$child->autoflush(1);
$parent->autoflush(1);
if ($pid = fork()) {
close $parent;
print $child "Parent Pid $$ is sending this\n";
chomp(my $line = <$child>);
print "Parent Pid $$ just read this: '$line'\n";
close $child;
waitpid($pid, 0);
} else {
die "cannot fork: $!" unless defined $pid;
close $child;
chomp(my $line = <$parent>);
print "Child Pid $$ just read this: '$line'\n";
print $parent "Child Pid $$ is sending this\n";
close $parent;
exit(0);
}
소켓: 클라이언트/서버 통신 (Sockets: Client/Server Communication)
전적으로 Unix 계열 운영체제에만 국한되지는 않지만(예: PC의 WinSock, 일부 VMS 라이브러리가 소켓 지원을 제공해요), 시스템에 소켓이 없을 수도 있고 그 경우 이 섹션은 별로 도움이 안 될 거예요. 소켓으로 TCP 스트림 같은 가상 회로와 UDP 패킷 같은 데이터그램, 둘 다 할 수 있어요. 시스템에 따라 더 많은 것도 할 수 있어요.
소켓을 다루는 Perl 함수는 C의 대응 시스템 호출과 같은 이름을 가지지만, 두 가지 이유로 인자가 다르기 마련이에요. 첫째, Perl 파일 핸들은 C 파일 서술자와 다르게 동작해요. 둘째, Perl은 이미 자기 문자열의 길이를 알므로 그 정보를 전달할 필요가 없어요.
Perl의 고대의, 밀레니엄 이전 소켓 코드의 주요 문제 중 하나는 상수 중 일부에 하드코딩된 값을 사용해서 이식성을 심각하게 해쳤다는 거예요. $AF_INET = 2를 명시적으로 설정하는 코드를 본다면 큰 문제에 빠진 것임을 알 수 있어요. 헤아릴 수 없이 우월한 접근은 필요한 다양한 상수와 함수에 더 확실히 접근할 수 있게 해주는 Socket 모듈을 사용하는 거예요.
NNTP나 SMTP 같은 기존 프로토콜의 서버/클라이언트를 작성하는 것이 아니라면, 서버가 클라이언트가 말을 마쳤다는 것을 그 반대도 마찬가지로 어떻게 알지 생각해 봐야 해요. 대부분의 프로토콜은 한 줄 메시지와 응답(그래서 한쪽이 "\n"을 받으면 다른 쪽이 마쳤다는 것을 알아요) 또는 빈 줄의 마침표(".\n"이 메시지/응답을 종료)로 끝나는 여러 줄 메시지와 응답에 기반해요.
인터넷 줄 종결자 (Internet Line Terminators)
인터넷 줄 종결자는 "\015\012"예요. ASCII 변종의 Unix 아래에서는 보통 "\r\n"으로 쓸 수 있지만, 다른 시스템에서는 "\r\n"이 때로 "\015\015\012", "\012\012\015", 또는 완전히 다른 것일 수 있어요. 표준은 "\015\012"를 쓰는 것을 (제공하는 것에 엄격하라) 규정하지만, 입력에서 단독 "\012"를 받아들이는 것도 (요구하는 것에 관대하라) 권장해요. 이 manpage의 코드에서 항상 그렇게 잘해 온 것은 아니지만, Unix 이전 암흑기에 오래된 Mac이 아니라면 아마 괜찮을 거예요.
인터넷 TCP 클라이언트와 서버 (Internet TCP Clients and Servers)
자신의 시스템 밖의 머신까지 확장될 수 있는 클라이언트-서버 통신을 원할 때 인터넷 도메인 소켓을 사용하세요.
인터넷 도메인 소켓을 사용하는 샘플 TCP 클라이언트:
#!/usr/bin/perl
use v5.36;
use Socket;
my $remote = shift || "localhost";
my $port = shift || 2345; # random port
if ($port =~ /\D/) { $port = getservbyname($port, "tcp") }
die "No port" unless $port;
my $iaddr = inet_aton($remote) || die "no host: $remote";
my $paddr = sockaddr_in($port, $iaddr);
my $proto = getprotobyname("tcp");
socket(my $sock, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
connect($sock, $paddr) || die "connect: $!";
while (my $line = <$sock>) {
print $line;
}
close ($sock) || die "close: $!";
exit(0);
그리고 그것과 함께 가는 대응 서버. 주소를 INADDR_ANY로 남겨둘 거예요. 그래야 커널이 멀티홈 호스트에서 적절한 인터페이스를 고를 수 있으니까요. 특정 인터페이스(게이트웨이나 방화벽 머신의 외부 측 같은)에 앉고 싶다면 이것을 실제 주소로 채우세요.
#!/usr/bin/perl -T
use v5.36;
BEGIN { $ENV{PATH} = "/usr/bin:/bin" }
use Socket;
use Carp;
my $EOL = "\015\012";
sub logmsg { print "$0 $$: @_ at ", scalar localtime(), "\n" }
my $port = shift || 2345;
die "invalid port" unless $port =~ /^ \d+ $/x;
my $proto = getprotobyname("tcp");
socket(my $server, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
setsockopt($server, SOL_SOCKET, SO_REUSEADDR, pack("l", 1))
|| die "setsockopt: $!";
bind($server, sockaddr_in($port, INADDR_ANY)) || die "bind: $!";
listen($server, SOMAXCONN) || die "listen: $!";
logmsg "server started on port $port";
for (my $paddr; $paddr = accept(my $client, $server); close $client) {
my($port, $iaddr) = sockaddr_in($paddr);
my $name = gethostbyaddr($iaddr, AF_INET);
logmsg "connection from $name [",
inet_ntoa($iaddr), "]"
" at port $port";
print $client "Hello there, $name, it's now ",
scalar localtime(), $EOL;
}
그리고 여기 멀티태스킹 버전이 있어요. 대부분의 전형적인 서버처럼 들어오는 클라이언트 요청을 처리하기 위해 자식 서버를 (fork()로) 생성해서 마스터 서버가 새 클라이언트를 서비스하러 빨리 돌아갈 수 있다는 점에서 멀티태스킹이에요.
#!/usr/bin/perl -T
use v5.36;
BEGIN { $ENV{PATH} = "/usr/bin:/bin" }
use Socket;
use Carp;
my $EOL = "\015\012";
sub spawn; # forward declaration
sub logmsg { print "$0 $$: @_ at ", scalar localtime(), "\n" }
my $port = shift || 2345;
die "invalid port" unless $port =~ /^ \d+ $/x;
my $proto = getprotobyname("tcp");
socket(my $server, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
setsockopt($server, SOL_SOCKET, SO_REUSEADDR, pack("l", 1))
|| die "setsockopt: $!";
bind($server, sockaddr_in($port, INADDR_ANY)) || die "bind: $!";
listen($server, SOMAXCONN) || die "listen: $!";
logmsg "server started on port $port";
my $waitedpid = 0;
use POSIX ":sys_wait_h";
use Errno;
sub REAPER {
local $!; # don't let waitpid() overwrite current error
while ((my $pid = waitpid(-1, WNOHANG)) > 0 && WIFEXITED($?)) {
logmsg "reaped $waitedpid" . ($? ? " with exit $?" : "");
}
$SIG{CHLD} = \&REAPER; # loathe SysV
}
$SIG{CHLD} = \&REAPER;
while (1) {
my $paddr = accept(my $client, $server) || do {
# try again if accept() returned because got a signal
next if $!{EINTR};
die "accept: $!";
};
my ($port, $iaddr) = sockaddr_in($paddr);
my $name = gethostbyaddr($iaddr, AF_INET);
logmsg "connection from $name [",
inet_ntoa($iaddr),
"] at port $port";
spawn $client, sub {
$| = 1;
print "Hello there, $name, it's now ",
scalar localtime(),
$EOL;
exec "/usr/games/fortune" # XXX: "wrong" line terminators
or confess "can't exec fortune: $!";
};
close $client;
}
sub spawn {
my $client = shift;
my $coderef = shift;
unless (@_ == 0 && $coderef && ref($coderef) eq "CODE") {
confess "usage: spawn CLIENT CODEREF";
}
my $pid;
unless (defined($pid = fork())) {
logmsg "cannot fork: $!";
return;
}
elsif ($pid) {
logmsg "begat $pid";
return; # I'm the parent
}
# else I'm the child -- go spawn
open(STDIN, "<&", $client) || die "can't dup client to stdin";
open(STDOUT, ">&", $client) || die "can't dup client to stdout";
## open(STDERR, ">&", STDOUT) || die "can't dup stdout to stderr";
exit($coderef->());
}
이 서버는 각 들어오는 요청에 대해 fork()를 통해 자식 버전을 복제하는 수고를 해요. 그렇게 해서 한 번에 많은 요청을 처리할 수 있지만, 항상 원하는 건 아닐 수 있어요. fork()를 하지 않아도 listen()이 그만큼 많은 대기 연결을 허용해요. 포킹 서버는 죽은 자식(Unix 용어로 "좀비")을 정리하는 데 특히 조심해야 해요. 그렇지 않으면 프로세스 테이블을 빨리 채울 테니까요. 여기 REAPER 서브루틴이 끝난 자식 프로세스에 대해 waitpid()를 호출해, 그들이 깨끗하게 종료하고 살아 있는 죽은 자의 대열에 합류하지 않도록 보장해요.
while 루프 안에서 accept()를 호출하고 거짓 값을 반환하는지 확인해요. 이는 보통 보고해야 할 시스템 오류를 나타내요. 하지만 Perl 5.8.0에 안전 신호(위의 "연기된 신호 (안전한 신호)" 참고)가 도입되면서, 프로세스가 신호를 받으면 accept()도 인터럽트될 수 있게 됐어요. 이는 보통 fork된 서브프로세스 중 하나가 종료하고 CHLD 신호로 부모 프로세스에 알릴 때 생겨요.
accept()가 신호로 인터럽트되면 $!는 EINTR로 설정돼요. 이렇게 되면 안전하게 루프의 다음 반복과 accept()의 또 다른 호출로 계속할 수 있어요. 신호 처리 코드가 $!의 값을 수정하지 않는 것이 중요해요. 그렇지 않으면 이 테스트가 아마 실패할 거예요. REAPER 서브루틴에서 waitpid()를 호출하기 전에 $!의 로컬 버전을 만들어요. waitpid()가 더 이상 기다릴 자식이 없을 때 어쩔 수 없이 $!를 ECHILD로 설정하면, 로컬 복사본을 갱신하고 원본은 그대로 두어요.
setuid나 setgid로 실행하지 않더라도 -T 플래그를 사용해 taint 검사를 활성화해야 해요 (perlsec 참고). 이것은 서버나 다른 누군가를 대신해 실행되는 어떤 프로그램(CGI 스크립트처럼)에도 항상 좋은 생각이에요. 외부 사람들이 시스템을 손상시킬 가능성을 줄여주니까요. perl은 taint 지원 없이 빌드될 수 있음을 참고하세요. 두 가지 모드가 있어요: 하나는 -T가 조용히 아무것도 안 하는 것이고, 다른 모드는 -T가 치명적 오류를 내는 거예요.
또 다른 TCP 클라이언트를 살펴보죠. 이것은 여러 다른 머신의 TCP "time" 서비스에 연결해 그것들의 시계가 실행 중인 시스템과 얼마나 다른지 보여줘요:
#!/usr/bin/perl
use v5.36;
use Socket;
my $SECS_OF_70_YEARS = 2208988800;
sub ctime { scalar localtime(shift() || time()) }
my $iaddr = gethostbyname("localhost");
my $proto = getprotobyname("tcp");
my $port = getservbyname("time", "tcp");
my $paddr = sockaddr_in(0, $iaddr);
$| = 1;
printf "%-24s %8s %s\n", "localhost", 0, ctime();
foreach my $host (@ARGV) {
printf "%-24s ", $host;
my $hisiaddr = inet_aton($host) || die "unknown host";
my $hispaddr = sockaddr_in($port, $hisiaddr);
socket(my $socket, PF_INET, SOCK_STREAM, $proto)
|| die "socket: $!";
connect($socket, $hispaddr) || die "connect: $!";
my $rtime = pack("C4", ());
read($socket, $rtime, 4);
close($socket);
my $histime = unpack("N", $rtime) - $SECS_OF_70_YEARS;
printf "%8d %s\n", $histime - time(), ctime($histime);
}
Unix 도메인 TCP 클라이언트와 서버 (Unix-Domain TCP Clients and Servers)
인터넷 도메인 클라이언트와 서버에는 그게 좋지만, 로컬 통신은 어떨까요? 같은 설정을 쓸 수 있지만 때로는 원하지 않아요. Unix 도메인 소켓은 현재 호스트에 로컬이고, 파이프를 구현하기 위해 내부적으로 자주 사용돼요. 인터넷 도메인 소켓과 달리 Unix 도메인 소켓은 ls(1) 목록으로 파일시스템에 나타날 수 있어요.
% ls -l /dev/log
srw-rw-rw- 1 root 0 Oct 31 07:23 /dev/log
Perl의 -S 파일 테스트로 이것들을 테스트할 수 있어요:
unless (-S "/dev/log") {
die "something's wicked with the log system";
}
샘플 Unix 도메인 클라이언트:
#!/usr/bin/perl
use v5.36;
use Socket;
my $rendezvous = shift || "catsock";
socket(my $sock, PF_UNIX, SOCK_STREAM, 0) || die "socket: $!";
connect($sock, sockaddr_un($rendezvous)) || die "connect: $!";
while (defined(my $line = <$sock>)) {
print $line;
}
exit(0);
그리고 대응 서버. 여기서는 머저리 같은 네트워크 종결자에 대해 걱정할 필요가 없어요. Unix 도메인 소켓은 localhost에 있다고 보장되기 때문에 모든 것이 제대로 동작해요.
#!/usr/bin/perl -T
use v5.36;
use Socket;
use Carp;
BEGIN { $ENV{PATH} = "/usr/bin:/bin" }
sub spawn; # forward declaration
sub logmsg { print "$0 $$: @_ at ", scalar localtime(), "\n" }
my $NAME = "catsock";
my $uaddr = sockaddr_un($NAME);
my $proto = getprotobyname("tcp");
socket(my $server, PF_UNIX, SOCK_STREAM, 0) || die "socket: $!";
unlink($NAME);
bind ($server, $uaddr) || die "bind: $!";
listen($server, SOMAXCONN) || die "listen: $!";
logmsg "server started on $NAME";
my $waitedpid;
use POSIX ":sys_wait_h";
sub REAPER {
my $child;
while (($waitedpid = waitpid(-1, WNOHANG)) > 0) {
logmsg "reaped $waitedpid" . ($? ? " with exit $?" : "");
}
$SIG{CHLD} = \&REAPER; # loathe SysV
}
$SIG{CHLD} = \&REAPER;
for ( $waitedpid = 0;
accept(my $client, $server) || $waitedpid;
$waitedpid = 0, close $client)
{
next if $waitedpid;
logmsg "connection on $NAME";
spawn $client, sub {
print "Hello there, it's now ", scalar localtime(), "\n";
exec("/usr/games/fortune") || die "can't exec fortune: $!";
};
}
sub spawn {
my $client = shift();
my $coderef = shift();
unless (@_ == 0 && $coderef && ref($coderef) eq "CODE") {
confess "usage: spawn CLIENT CODEREF";
}
my $pid;
unless (defined($pid = fork())) {
logmsg "cannot fork: $!";
return;
}
elsif ($pid) {
logmsg "begat $pid";
return; # I'm the parent
}
else {
# I'm the child -- go spawn
}
open(STDIN, "<&", $client)
|| die "can't dup client to stdin";
open(STDOUT, ">&", $client)
|| die "can't dup client to stdout";
## open(STDERR, ">&", STDOUT)
## || die "can't dup stdout to stderr";
exit($coderef->());
}
보시다시피 인터넷 도메인 TCP 서버와 놀랍도록 비슷해요. 사실 너무 비슷해서 중복된 함수 몇 개 — spawn(), logmsg(), ctime(), REAPER() — 는 다른 서버와 같으므로 생략했어요.
그럼 왜 더 단순한 네임드 파이프 대신 Unix 도메인 소켓을 쓰고 싶을까요? 네임드 파이프는 세션을 주지 않기 때문이에요. 한 프로세스의 데이터를 다른 프로세스의 데이터와 구분할 수 없어요. 소켓 프로그래밍으로는 클라이언트마다 별도 세션을 얻어요. 그래서 accept()가 두 인자를 받는 거예요.
예를 들어, 사람들이 웹에서 접근하길 원하지만 CGI 인터페이스를 거치는 경우에만 접근하길 원하는 오래 실행되는 데이터베이스 서버 데몬이 있다고 해 보죠. 여러분이 원하는 검사와 로깅을 하고 Unix 도메인 클라이언트처럼 행동하며 개인 서버에 연결하는 작고 단순한 CGI 프로그램이 있을 거예요.
IO::Socket을 사용한 TCP 클라이언트 (TCP Clients with IO::Socket)
소켓 프로그래밍에 더 높은 수준의 인터페이스를 선호하는 분들을 위해 IO::Socket 모듈이 객체지향 접근을 제공해요. 어떤 이유로 이 모듈이 없다면 CPAN에서 IO::Socket을 가져올 수 있고, 거기서 다음 시스템들에 대한 쉬운 인터페이스를 제공하는 모듈도 찾을 수 있어요: DNS, FTP, Ident (RFC 931), NIS와 NISPlus, NNTP, Ping, POP3, SMTP, SNMP, SSLeay, Telnet, Time — 몇 가지만 들자면요.
간단한 클라이언트 (A Simple Client)
여기 호스트 이름 "localhost"의 포트 13에서 "daytime" 서비스에 TCP 연결을 만들고 거기 서버가 제공하는 모든 것을 출력하는 클라이언트가 있어요.
#!/usr/bin/perl
use v5.36;
use IO::Socket;
my $remote = IO::Socket::INET->new(
Proto => "tcp",
PeerAddr => "localhost",
PeerPort => "daytime(13)",
)
|| die "can't connect to daytime service on localhost";
while (<$remote>) { print }
이 프로그램을 실행하면 다음과 같은 것을 받아야 해요:
Wed May 14 08:40:46 MDT 1997
new() 생성자의 매개변수들이 무슨 뜻인지:
Proto
어떤 프로토콜을 쓸지예요. 이 경우 반환된 소켓 핸들은 TCP 소켓에 연결될 거예요. 스트림 지향 연결을 원하기 때문이죠. 즉 평범한 옛 파일처럼 행동하는 연결이에요. 모든 소켓이 이 유형은 아니에요. 예를 들어 UDP 프로토콜은 메시지 전달에 쓰이는 데이터그램 소켓을 만드는 데 쓸 수 있어요.
PeerAddr
서버가 실행 중인 원격 호스트의 이름이나 인터넷 주소예요. "www.perl.com" 같은 더 긴 이름이나 "207.171.7.72" 같은 주소를 지정할 수도 있었어요. 데모 목적으로 항상 현재 실행 중인 머신을 뜻해야 하는 특별한 호스트 이름 "localhost"를 사용했어요. localhost의 대응 인터넷 주소는 "127.0.0.1"이고, 그것을 쓰고 싶다면 그렇게 해도 돼요.
PeerPort
연결하고 싶은 서비스 이름이나 포트 번호예요. 잘 구성된 시스템 서비스 파일이 있는 시스템에서는 "daytime"만 써도 됐겠지만, 여기서는 괄호 안에 포트 번호(13)를 지정했어요. [FOOTNOTE: 시스템 서비스 파일은 Unix 계열 시스템에서 /etc/services에 있어요.] 번호만 써도 동작했겠지만, 숫자 리터럴은 꼼꼼한 프로그래머를 긴장하게 해요.
Webget 클라이언트 (A Webget Client)
문서를 가져올 원격 호스트를 받고, 그 호스트에서 가져올 파일 목록을 받는 간단한 클라이언트가 있어요. 이것은 서버의 응답을 가져오기 전에 먼저 서버에 무언가를 보내기 때문에 이전 것보다 더 흥미로운 클라이언트예요.
#!/usr/bin/perl
use v5.36;
use IO::Socket;
unless (@ARGV > 1) { die "usage: $0 host url ..." }
my $host = shift(@ARGV);
my $EOL = "\015\012";
my $BLANK = $EOL x 2;
for my $document (@ARGV) {
my $remote = IO::Socket::INET->new( Proto => "tcp",
PeerAddr => $host,
PeerPort => "http(80)",
) || die "cannot connect to httpd on $host";
$remote->autoflush(1);
print $remote "GET $document HTTP/1.0" . $BLANK;
while ( <$remote> ) { print }
close $remote;
}
HTTP 서비스를 처리하는 웹 서버는 표준 포트 80에 있다고 가정돼요. 연결하려는 서버가 1080이나 8080 같은 다른 포트에 있다면 명명된 매개변수 쌍 PeerPort => 8080으로 지정해야 해요. autoflush 메서드가 소켓에 쓰이는 이유는 그렇지 않으면 시스템이 우리가 보낸 출력을 버퍼링할 테니까예요. (선사시대 Mac이라면 네트워크로 데이터를 보내는 코드의 모든 "\n"을 "\015\012"로 바꿔야 할 수도 있어요.)
서버 연결은 과정의 첫 부분일 뿐이에요. 연결을 얻고 나면 서버의 언어를 사용해야 해요. 네트워크의 각 서버는 입력으로 기대하는 자기만의 작은 명령 언어를 가져요. "GET"으로 시작하는 서버에 보내는 문자열은 HTTP 문법이에요. 이 경우 각 지정된 문서를 단순히 요청해요. 네, 같은 호스트라도 각 문서마다 새 연결을 만드는 거예요. 항상 그렇게 HTTP로 말해야 했거든요. 최근 웹 브라우저 버전은 원격 서버가 연결을 잠시 열어두라고 요청할 수 있지만, 서버가 그런 요청을 들어줄 필요는 없어요.
webget이라 부르는 그 프로그램을 실행하는 예:
% webget www.perl.com /guanaco.html
HTTP/1.1 404 File Not Found
Date: Thu, 08 May 1997 18:02:32 GMT
Server: Apache/1.2b6
Connection: close
Content-type: text/html
<HEAD><TITLE>404 File Not Found</TITLE></HEAD>
<BODY><H1>File Not Found</H1>
The requested URL /guanaco.html was not found on this server.<P>
</BODY>
그 특정 문서를 찾지 못했으니 그리 흥미롭진 않네요. 하지만 긴 응답은 이 페이지에 안 들어갔을 거예요.
이 프로그램의 더 많은 기능을 갖춘 버전을 위해 CPAN의 LWP 모듈에 포함된 lwp-request 프로그램을 살펴보시길 권해요.
IO::Socket을 사용한 인터랙티브 클라이언트 (Interactive Client with IO::Socket)
한 명령을 보내고 한 답을 받고 싶다면 그게 다 좋아요. 하지만 telnet이 동작하는 것처럼 완전히 인터랙티브한 것을 설정하는 건 어떨까요? 그렇게 하면 한 줄을 입력하고 답을 얻고, 한 줄 입력하고 답 얻고, 계속 그런 식이에요.
이 클라이언트는 지금까지 한 두 개보다 더 복잡하지만, 강력한 fork 호출을 지원하는 시스템에 있다면 해결책은 그리 힘들지 않아요. 대화하고 싶은 서비스에 연결하고 나면 fork를 호출해 프로세스를 복제해요. 이 두 동일 프로세스 각각은 아주 간단한 일을 해요: 부모는 소켓에서 표준 출력으로 모든 것을 복사하고, 자식은 동시에 표준 입력에서 소켓으로 모든 것을 복사해요. 단일 프로세스로 같은 것을 하려면 훨씬 더 어려워요. 두 프로세스가 한 가지 일을 하도록 코딩하는 것이 한 프로세스가 두 가지 일을 하도록 코딩하는 것보다 쉽거든요. (이 keep-it-simple 원칙은 Unix 철학의 초석이자 좋은 소프트웨어 공학이기도 해서, 아마 그래서 다른 시스템으로 퍼졌어요.)
코드:
#!/usr/bin/perl
use v5.36;
use IO::Socket;
unless (@ARGV == 2) { die "usage: $0 host port" }
my ($host, $port) = @ARGV;
# create a tcp connection to the specified host and port
my $handle = IO::Socket::INET->new(Proto => "tcp",
PeerAddr => $host,
PeerPort => $port)
|| die "can't connect to port $port on $host: $!";
$handle->autoflush(1); # so output gets there right away
print STDERR "[Connected to $host:$port]\n";
# split the program into two processes, identical twins
die "can't fork: $!" unless defined(my $kidpid = fork());
# the if{} block runs only in the parent process
if ($kidpid) {
# copy the socket to standard output
while (defined (my $line = <$handle>)) {
print STDOUT $line;
}
kill("TERM", $kidpid); # send SIGTERM to child
}
# the else{} block runs only in the child process
else {
# copy standard input to the socket
while (defined (my $line = <STDIN>)) {
print $handle $line;
}
exit(0); # just in case
}
부모의 if 블록에 있는 kill 함수는 원격 서버가 연결의 자기 쪽 끝을 닫는 즉시, 현재 else 블록에서 실행 중인 자식 프로세스에 신호를 보내기 위한 거예요.
원격 서버가 데이터를 바이트 단위로 보내고, 줄바꿈을 기다리지 않고(일어나지 않을 수도 있으니) 그 데이터를 즉시 필요로 한다면, 부모의 while 루프를 다음으로 바꾸고 싶을지도 몰라요:
my $byte;
while (sysread($handle, $byte, 1) == 1) {
print STDOUT $byte;
}
읽고 싶은 각 바이트마다 시스템 호출을 하는 것은 (완곡하게 말해) 그리 효율적이지 않지만, 설명하기 가장 단순하고 합리적으로 잘 동작해요.
IO::Socket을 사용한 TCP 서버 (TCP Servers with IO::Socket)
항상 그렇듯이 서버 설정은 클라이언트 실행보다 조금 더 복잡해요. 모델은 서버가 특정 포트에서 들어오는 연결을 듣는 일만 하는 특별한 종류의 소켓을 만드는 거예요. 클라이언트보다 약간 다른 인자로 IO::Socket::INET->new() 메서드를 호출함으로써 이걸 해요.
Proto
어떤 프로토콜을 쓸지예요. 클라이언트처럼 여기서도 여전히 "tcp"를 지정할 거예요.
LocalPort
클라이언트에서는 하지 않았던 LocalPort 인자에 로컬 포트를 지정해요. 여러분이 서버가 되고 싶은 서비스 이름이나 포트 번호예요. (Unix에서 1024 미만 포트는 슈퍼유저로 제한돼요.) 샘플에서는 포트 9000을 쓸 거지만, 시스템에서 현재 사용 중이지 않은 어떤 포트든 쓸 수 있어요. 이미 사용 중인 것을 쓰려고 하면 "Address already in use" 메시지를 받을 거예요. Unix에서 netstat -a 명령이 어떤 서비스에 현재 서버가 있는지 보여줄 거예요.
Listen
Listen 매개변수는 들어오는 클라이언트를 거절하기 전에 받아들일 수 있는 보류 중인 연결의 최대 수로 설정돼요. 전화의 통화 대기 큐라고 생각하면 돼요. 낮은 수준의 Socket 모듈에는 시스템 최대값을 위한 특별한 심볼이 있는데, 그것이 SOMAXCONN이에요.
Reuse
Reuse 매개변수는 시스템 버퍼가 비워지길 몇 분 기다리지 않고 서버를 수동으로 재시작할 수 있도록 필요해요.
위 매개변수로 일반 서버 소켓을 만들고 나면 서버는 새 클라이언트가 연결하길 기다려요. 서버는 accept 메서드에서 블록하고, 결국 원격 클라이언트로부터의 양방향 연결을 받아들여요. (버퍼링을 피하려면 이 핸들을 autoflush해야 해요.)
사용자 친화성을 더하기 위해 서버가 사용자에게 명령을 프롬프트해요. 대부분의 서버는 이렇게 안 해요. 줄바꿈 없는 프롬프트 때문에 위 인터랙티브 클라이언트의 sysread 변형을 써야 할 거예요.
이 서버는 다섯 가지 명령 중 하나를 받아들이고, 클라이언트에 출력을 다시 보내요. 대부분의 네트워크 서버와 달리 이 서버는 한 번에 하나의 들어오는 클라이언트만 처리해요. 멀티태스킹 서버는 Camel의 16장에 다뤄져 있어요.
코드:
#!/usr/bin/perl
use v5.36;
use IO::Socket;
use Net::hostent; # for OOish version of gethostbyaddr
my $PORT = 9000; # pick something not in use
my $server = IO::Socket::INET->new( Proto => "tcp",
LocalPort => $PORT,
Listen => SOMAXCONN,
Reuse => 1);
die "can't setup server" unless $server;
print "[Server $0 accepting clients]\n";
while (my $client = $server->accept()) {
$client->autoflush(1);
print $client "Welcome to $0; type help for command list.\n";
my $hostinfo = gethostbyaddr($client->peeraddr);
printf "[Connect from %s]\n",
$hostinfo ? $hostinfo->name : $client->peerhost;
print $client "Command? ";
while ( <$client>) {
next unless /\S/; # blank line
if (/quit|exit/i) { last }
elsif (/date|time/i) { printf $client "%s\n", scalar localtime() }
elsif (/who/i ) { print $client `who 2>&1` }
elsif (/cookie/i ) { print $client `/usr/games/fortune 2>&1` }
elsif (/motd/i ) { print $client `cat /etc/motd 2>&1` }
else {
print $client "Commands: quit date who cookie motd\n";
}
} continue {
print $client "Command? ";
}
close $client;
}
UDP: 메시지 전달 (UDP: Message Passing)
연결이 아니라 메시지를 사용하는 또 다른 종류의 클라이언트-서버 설정이 있어요. UDP 통신은 훨씬 더 낮은 오버헤드를 수반하지만 신뢰성도 덜해요. 메시지가 도착한다는 보장이 없고, 순서대로 무손상으로 도착한다는 보장은 더더욱 없으니까요. 그래도 UDP는 TCP보다 몇 가지 이점을 제공해요. 한 번에 많은 대상 호스트(보통 로컬 서브넷)에 "broadcast" 또는 "multicast"할 수 있는 능력을 포함해서요. 신뢰성에 대해 지나치게 걱정하기 시작하고 메시지 시스템에 검사를 만들기 시작한다면, 아마 처음부터 TCP만 써야 할 거예요.
UDP 데이터그램은 바이트스트림이 아니고 그렇게 취급되어선 안 돼요. 이는 stdio(즉 print()와 친구들) 같은 내부 버퍼링이 있는 I/O 메커니즘을 쓰는 것을 특히 번거롭게 만들어요. 아래 예처럼 syswrite()를 쓰거나 더 나은 send()를 사용하세요.
앞서 준 인터넷 TCP 클라이언트 샘플과 비슷한 UDP 프로그램이에요. 하지만 한 번에 한 호스트를 확인하는 대신, UDP 버전은 멀티캐스트를 시뮬레이션하고 select()를 사용해 시간 제한이 있는 I/O 대기를 함으로써 그것들을 비동기적으로 많이 확인해요. TCP로 비슷한 것을 하려면 호스트마다 다른 소켓 핸들을 써야 했을 거예요.
#!/usr/bin/perl
use v5.36;
use Socket;
use Sys::Hostname;
my $SECS_OF_70_YEARS = 2_208_988_800;
my $iaddr = gethostbyname(hostname());
my $proto = getprotobyname("udp");
my $port = getservbyname("time", "udp");
my $paddr = sockaddr_in(0, $iaddr); # 0 means let kernel pick
socket(my $socket, PF_INET, SOCK_DGRAM, $proto) || die "socket: $!";
bind($socket, $paddr) || die "bind: $!";
$| = 1;
printf "%-12s %8s %s\n", "localhost", 0, scalar localtime();
my $count = 0;
for my $host (@ARGV) {
$count++;
my $hisiaddr = inet_aton($host) || die "unknown host";
my $hispaddr = sockaddr_in($port, $hisiaddr);
defined(send($socket, 0, 0, $hispaddr)) || die "send $host: $!";
}
my $rout = my $rin = "";
vec($rin, fileno($socket), 1) = 1;
# timeout after 10.0 seconds
while ($count && select($rout = $rin, undef, undef, 10.0)) {
my $rtime = "";
my $hispaddr = recv($socket, $rtime, 4, 0) || die "recv: $!";
my ($port, $hisiaddr) = sockaddr_in($hispaddr);
my $host = gethostbyaddr($hisiaddr, AF_INET);
my $histime = unpack("N", $rtime) - $SECS_OF_70_YEARS;
printf "%-12s ", $host;
printf "%8d %s\n", $histime - time(), scalar localtime($histime);
$count--;
}
이 예는 재시도를 포함하지 않아 도달 가능한 호스트에 연락하지 못할 수 있어요. 가장 두드러진 이유는 연락할 호스트 수가 충분히 많을 때 보내는 호스트의 큐 혼잡이에요.
SysV IPC
System V IPC는 소켓만큼 널리 쓰이진 않지만 여전히 흥미로운 용도가 있어요. 하지만 SysV IPC나 버클리 mmap()으로 여러 프로세스 사이에서 공유되는 변수를 가질 수는 없어요. Perl이 원하지 않을 때 문자열을 재할당할 테니까요. 그 용도로는 IPC::Shareable이나 threads::shared 모듈을 살펴보세요.
공유 메모리 사용을 보여주는 작은 예:
use IPC::SysV qw(IPC_PRIVATE IPC_RMID S_IRUSR S_IWUSR);
my $size = 2000;
my $id = shmget(IPC_PRIVATE, $size, S_IRUSR | S_IWUSR);
defined($id) || die "shmget: $!";
print "shm key $id\n";
my $message = "Message #1";
shmwrite($id, $message, 0, 60) || die "shmwrite: $!";
print "wrote: '$message'\n";
shmread($id, my $buff, 0, 60) || die "shmread: $!";
print "read : '$buff'\n";
# the buffer of shmread is zero-character end-padded.
substr($buff, index($buff, "\0")) = "";
print "un" unless $buff eq $message;
print "swell\n";
print "deleting shm $id\n";
shmctl($id, IPC_RMID, 0) || die "shmctl: $!";
세마포어 예:
use IPC::SysV qw(IPC_CREAT);
my $IPC_KEY = 1234;
my $id = semget($IPC_KEY, 10, 0666 | IPC_CREAT);
defined($id) || die "semget: $!";
print "sem id $id\n";
이 코드를 여러 프로세스에서 실행하기 위해 별도 파일에 넣으세요. 파일을 take라고 부르세요:
# create a semaphore
my $IPC_KEY = 1234;
my $id = semget($IPC_KEY, 0, 0);
defined($id) || die "semget: $!";
my $semnum = 0;
my $semflag = 0;
# "take" semaphore
# wait for semaphore to be zero
my $semop = 0;
my $opstring1 = pack("s!s!s!", $semnum, $semop, $semflag);
# Increment the semaphore count
$semop = 1;
my $opstring2 = pack("s!s!s!", $semnum, $semop, $semflag);
my $opstring = $opstring1 . $opstring2;
semop($id, $opstring) || die "semop: $!";
이 코드를 여러 프로세스에서 실행하기 위해 별도 파일에 넣으세요. 이 파일을 give라고 부르세요:
# "give" the semaphore
# run this in the original process and you will see
# that the second process continues
my $IPC_KEY = 1234;
my $id = semget($IPC_KEY, 0, 0);
die unless defined($id);
my $semnum = 0;
my $semflag = 0;
# Decrement the semaphore count
my $semop = -1;
my $opstring = pack("s!s!s!", $semnum, $semop, $semflag);
semop($id, $opstring) || die "semop: $!";
위의 SysV IPC 코드는 아주 오래 전에 작성됐고 확실히 투박해 보여요. 더 현대적인 모습은 IPC::SysV 모듈을 참고하세요.
SysV 메시지 큐를 보여주는 작은 예:
use IPC::SysV qw(IPC_PRIVATE IPC_RMID IPC_CREAT S_IRUSR S_IWUSR);
my $id = msgget(IPC_PRIVATE, IPC_CREAT | S_IRUSR | S_IWUSR);
defined($id) || die "msgget failed: $!";
my $sent = "message";
my $type_sent = 1234;
msgsnd($id, pack("l! a*", $type_sent, $sent), 0)
|| die "msgsnd failed: $!";
msgrcv($id, my $rcvd_buf, 60, 0, 0)
|| die "msgrcv failed: $!";
my($type_rcvd, $rcvd) = unpack("l! a*", $rcvd_buf);
if ($rcvd eq $sent) {
print "okay\n";
} else {
print "not okay\n";
}
msgctl($id, IPC_RMID, 0) || die "msgctl failed: $!\n";
참고 (NOTES)
이 루틴들 대부분은 실패할 때 잡히지 않은 예외로 인해 프로그램을 바로 죽이는 대신 조용히 그리고 예의 바르게 undef를 반환해요. (실제로 새 Socket 변환 함수 중 일부는 잘못된 인자에 croak()해요.) 따라서 이 함수들의 반환 값을 확인하는 것이 필수적이에요. 소켓 프로그램을 항상 이렇게 시작해 최적의 성공을 얻고, 서버의 #! 줄에 -T taint 검사 플래그를 추가하는 것을 잊지 마세요:
#!/usr/bin/perl -T
use v5.36;
use sigtrap;
use Socket;
버그 (BUGS)
이 루틴들은 모두 시스템별 이식성 문제를 만들어요. 다른 곳에서 언급했듯이, Perl은 시스템 행동의 많은 부분에서 여러분의 C 라이브러리에 달려 있어요. 신호에 대해 깨진 SysV 의미론을 가정하고 단순한 TCP와 UDP 소켓 연산에 머무르는 것이 아마 가장 안전할 거예요. 예를 들어, 코드가 이식 가능할 기회를 원한다면 로컬 UDP 데이터그램 소켓 위로 열린 파일 서술자를 넘기려 하지 마세요.
저자 (AUTHOR)
Tom Christiansen, Larry Wall의 원본 버전의 잔재와 Perl Porters의 제안이 가끔 포함됨.
더 보기 (SEE ALSO)
네트워킹에는 이보다 훨씬 더 많지만, 이걸로 시작할 수 있어요.
대담한 프로그래머를 위해 없어서는 안 될 교과서는 W. Richard Stevens의 Unix Network Programming, 2nd Edition, Volume 1(Prentice-Hall 출판)이에요. 네트워킹 책 대부분은 C 프로그래머의 관점에서 주제를 다뤄요. Perl로의 번역은 독자의 연습문제로 남겨져 있어요.
IO::Socket(3) manpage는 객체 라이브러리를, Socket(3) manpage는 소켓의 낮은 수준 인터페이스를 설명해요. perlfunc의 명백한 함수들 외에도 가까운 CPAN 사이트의 modules 파일을 확인해야 해요. 특히 https://www.cpan.org/modules/00modlist.long.html#ID5_Networking_를요. perlmodlib를 참고하거나, 이전 링크가 동작하지 않으면 CPAN이 무엇이고 어디서 얻는지에 대한 최상의 설명인 Perl FAQ를 참고하세요.
CPAN의 modules 파일의 섹션 5는 "Networking, Device Control (modems), and Interprocess Communication"에 바쳐졌고, 수많은 번들되지 않은 모듈을 담고 있어요 — Networking, Chat과 Expect 연산, CGI 프로그래밍, DCE, FTP, IPC, NNTP, Proxy, Ptty, RPC, SNMP, SMTP, Telnet, Threads, ToolTalk — 몇 가지만 들자면요.