POSIX 신호 2
리눅스 프로세스 계층 구조
프로세스 계층 구조 개요
- 로그인 셸에서 파생된 자식 프로세스, 손자 프로세스 등 다양한 프로세스 계층 구조를 형성합니다.
- 데몬 프로세스는 백그라운드에서 실행되며, 예를 들어
httpd와 같은 웹 서버가 있습니다.
init 프로세스 (PID 1)는 시스템의 루트 프로세스입니다.
pstree 명령어를 통해 프로세스 계층 구조를 시각화할 수 있습니다.
셸 프로그램
셸의 역할
- 셸은 사용자를 대신하여 프로그램을 실행하는 애플리케이션 프로그램입니다.
- 대표적인 셸:
- sh: 오리지널 Unix 셸 (Stephen Bourne, AT&T Bell Labs, 1977)
- csh/tcsh: BSD Unix C 셸
- bash: "Bourne-Again" 셸 (기본 Linux 셸)
셸 프로그램 예시
int main()
{
char cmdline[MAXLINE];
while (1) {
printf("> ");
fgets(cmdline, MAXLINE, stdin);
if (feof(stdin))
exit(0);
eval(cmdline);
}
}
- 셸 프로그램은 읽기/평가(read/evaluate) 단계를 반복합니다.
간단한 셸의 eval 함수
eval 함수 예시
void eval(char *cmdline)
{
char *argv[MAXARGS];
char buf[MAXLINE];
int bg;
pid_t pid;
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL)
return;
if (!builtin_command(argv)) {
if ((pid = Fork()) == 0) {
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
if (!bg) {
int status;
if (waitpid(pid, &status, 0) < 0)
unix_error("waitfg: waitpid error");
}
else
printf("%d %s", pid, cmdline);
}
return;
}
간단한 셸의 문제점
백그라운드 작업 문제
- 포그라운드 작업을 올바르게 기다리고 회수(reap)하지만 백그라운드 작업은 문제가 됩니다.
- 백그라운드 작업이 종료되면 좀비(zombie) 상태가 되고, 셸이 종료되지 않으면 회수되지 않습니다.
- 이는 메모리 누수를 일으켜 커널의 메모리가 부족해질 수 있습니다.
예외적 제어 흐름 (ECF)
해결책: 예외적 제어 흐름
- 커널은 백그라운드 프로세스가 완료될 때 정규 처리를 중단하고 신호를 통해 알립니다.
- Unix에서 이 알림 메커니즘은 신호입니다.
신호 개념: 신호 수신
신호 수신 과정
- 커널에 의해 신호를 전달받으면 프로세스는 어떤 방식으로든 반응해야 합니다.
- 신호 무시
- 프로세스 종료 (선택적 코어 덤프와 함께)
- 신호 핸들러를 실행하여 신호 포착
- 이는 비동기 인터럽트에 대한 하드웨어 예외 처리기와 유사합니다.
신호 전송: 프로세스 그룹
프로세스 그룹
- 모든 프로세스는 정확히 하나의 프로세스 그룹에 속합니다.
getpgrp(): 현재 프로세스의 그룹을 반환합니다.
setpgid(): 프로세스 그룹을 변경합니다.
키보드로 신호 전송
- Ctrl-C (SIGINT) 또는 Ctrl-Z (SIGTSTP)를 입력하면 커널은 포그라운드 프로세스 그룹의 모든 작업에 신호를 보냅니다.
- SIGINT: 기본 동작은 각 프로세스를 종료하는 것입니다.
- SIGTSTP: 기본 동작은 각 프로세스를 중지(일시 중지)하는 것입니다.
신호 핸들러의 동시 실행 흐름
동시 실행 흐름으로서의 신호 핸들러
- 신호 핸들러는 별도의 논리적 흐름으로, 메인 프로그램과 동시에 실행됩니다.
- 신호가 전달되면 컨텍스트 스위치가 발생하여 신호 핸들러가 실행됩니다.
비지역 점프: setjmp/longjmp
비지역 점프 개념
- 사용자 수준에서 임의의 위치로 제어를 전달하는 강력하지만 위험한 메커니즘입니다.
- 프로시저 호출/반환 규율을 깨는 통제된 방법입니다.
- 오류 복구 및 신호 처리에 유용합니다.
setjmp 함수
int setjmp(jmp_buf j);
- longjmp 전에 호출되어야 합니다.
- 이후 longjmp를 위한 반환 지점을 식별합니다.
- 한 번 호출되며, 한 번 이상 반환됩니다.
longjmp 함수
void longjmp(jmp_buf j, int i);
- 의미: 기억된 setjmp로부터 다시 반환하지만, 이번에는 0 대신 i를 반환합니다.
- setjmp 호출 후 호출되며, 한 번 호출되지만 반환하지 않습니다.
setjmp/longjmp 예시
jmp_buf buf;
void foo(void)
{
if (error1)
longjmp(buf, 1);
bar();
}
void bar(void)
{
if (error2)
longjmp(buf, 2);
}
int main()
{
switch(setjmp(buf)) {
case 0:
foo();
break;
case 1:
printf("Detected an error1 condition in foo\n");
break;
case 2:
printf("Detected an error2 condition in foo\n");
break;
default:
printf("Unknown error condition in foo\n");
}
exit(0);
}
비지역 점프의 한계
- 스택 규율 내에서 작동합니다.
- 호출되었지만 아직 완료되지 않은 함수의 환경으로만 longjmp를 수행할 수 있습니다.
신호 처리 예시: Ctrl-C로 프로그램 재시작
프로그램 재시작 예제
sigjmp_buf buf;
void handler(int sig)
{
siglongjmp(buf, 1);
}
int main()
{
if (!sigsetjmp(buf, 1)) {
signal(SIGINT, handler);
puts("starting\n");
}
else
puts("restarting\n");
while(1) {
sleep(1);
puts("processing...\n");
}
exit(0);
}
- Ctrl-C 입력 시 프로그램을 재시작합니다.