1. Signal
Signal(시그널)은 간단한 메시지를 특정 프로세스에게 보내는 것을 의미한다. 시그널을 보내는 주체는 kernel이고, 프로세스가 다른 프로세스에게 보낼 때도 kernel을 통하게 된다. 시그널은 정수 ID 값으로 구별한다. 예를 들어, 시그널 중 $\verb|SIGKILL|$ 의 ID는 9번으로, 프로그램을 강제 종료시키는 역할을 한다.
커널이 시그널을 보내면 목표 프로세스 안에 자신의 state 정보를 저장하는 자료구조에 시그널이 도착했다는 표시를 남긴다. 나중에 CPU를 할당받았을 때 자신한테 도착한 시그널을 보고 해당 핸들러들을 호출하게 된다. 커널이 시그널을 보내게 되는 계기는 다음과 같다.
- 커널이 스스로 판단하여 시그널을 보내는 경우: ex) 프로그램 실행 중 0으로 나누는 연산이 생기면 $\verb|SIGFPE|$ 을 보냄 / 자식이 종료되면 $\verb|SIGCHLD|$를 보냄
- $\verb|kill|$ 명령어: 그냥 사용하면 프로세스를 죽이는 명령어이지만, 명령어 창에서 어떤 시그널을 프로세스에게 보내라는 명령어로 쓸 수도 있다.
- $\verb|kill()|$ 시스템 콜로 시그널 호출
시그널을 받은 목표 프로세스는 시그널을 무시하거나, 프로세스를 종료하거나, 프로그래머가 정의한 시그널 핸들러를 호출하게 된다.
시그널의 종류는 아래와 같이 총 64가지가 존재하고, 각각의 시그널마다 Default Action과 발생하는 이벤트가 존재한다.
자주 사용하는 시그널 정리
$\texttt{SIGINT}$ : $\texttt{ctrl C}$ 입력 시 발생 - 프로세스 종료
$\texttt{SIGTERM}$ : $\texttt{kill}$ 명령어 사용 시 디폴트로 호출되는 시그널.
$\texttt{SIGKILL}$ : 프로세스 종료
$\texttt{SIGTSTP}$ : $\texttt{ctrl Z}$ 입력 시 프로세스 일시 정지 - fg 로 재시작
$\texttt{SIGCHLD}$ : 자식이 종료될 때 커널이 스스로 보냄. (디폴트 액션은 Ignore)
2. Signal 동작 원리
커널이 시그널을 보냈다고 해서 바로 시그널 핸들러가 실행되는 것은 아니다. 시그널을 보낸 후 pending 되는 상태가 존재한다. 이 때 비트 벡터를 사용하여 정보를 저장하므로 단 하나의 pending 시그널만 존재할 수 있기 때문에, 시그널이 여러번 도착했다고 해서 몇 번 도착했는지에 대한 정보를 알 수 없다. (즉, 시그널이 Queueing 되지 않는다.) 또한 시그널을 block 시킬 수도 있는데, block상태가 된 시그널은 unblock될 때까지 시그널 핸들러를 실행하지 않는다. 따라서 커널은 pending 비트 벡터와 blocked 비트 벡터의 비트 연산을 통해서 시그널을 처리할지 말지를 결정한다. 이때 처리되는 순서는 시그널 번호의 내림차순이다.
3. kill, kill()로 Signal 보내기
PID와 PGID
PID: 프로세스의 ID. $\verb|getpid()|$ 를 통해 현재 프로세스의 PID를 가져올 수 있다.
PGID: 프로세스 그룹의 ID: $\verb|getpgrp()|$ 를 통해 현재 프로세스가 속한 그룹의 PGID를 가져올 수 있다.
kill 명령어의 사용, kill() 시스템 콜의 사용
- kill -9 20: PID가 20번인 프로세스에게 9번 시그널 (SIGKILL) 을 보내라.
- kill -9 -20: PGID가 20번인 프로세스에게 9번 시그널 (SIGKILL) 을 보내라.
- $\verb|ctrl C|$: 프로세스를 완전 중지 시킴 (SIGKILL)
- $\verb|ctrl Z|$: 프로세스를 일시 중지 시킴 (SIGTSTP) - fg 입력 시 재시작
- $\verb|int kill(pid_t pid, int sig)|$ : pid 프로세스에게 sig 시그널을 보내라.
4. signal()로 시그널 핸들러 정의하기
$$\verb|sighandler_t signal(int signum, sighandler_t handler)|$$
$\verb|siganl()|$ 은 signum 번호의 시그널이 취하는 액션(terminate, dump, ignore 등)을 사용자가 지정한 handler로 바꾼다. 하지만, $\verb|SIGKILL, SIGSTOP|$ 처럼 시그널 핸들러를 바꿀 수 없는 시그널도 존재한다. 이 시스템 콜을 사용하려면 헤더파일 $\verb|<signal.h>|$가 필요하다.
handler에 올 수 있는 값으로 미리 정의된 값들은 $\verb|SIG_IGN|$ (signum에 해당하는 시그널을 무시), $\verb|SIG_DFL|$ (signum의 기존 핸들러로 복구) 등이 있다. 이외에는 사용자가 직접 정의한 함수의 주소를 넣어주면 된다. 시그널 핸들러가 끝나고나서는 시그널 핸들러가 불려지기 직전에 프로세스가 실행했던 상태로 돌아가게 된다. 즉, 시그널 핸들러는 기존 프로세스와 병렬로 실행되는 것이 아니라 하던 일을 잠시 멈추고 핸들러가 시키는 일을 하고 오는 것이다.
$\verb|sigaction|$은 POSIX 에서 정의하고 있는 시스템 콜로, $\verb|siganl()|$과 기능은 동일하지만 가능하다면 $\verb|sigaction|$을 사용하는 것이 더 유용하다.
아래는 SIGINT에 대한 signal 핸들러 예제로, int_handler는 시그널을 받았을 때 해당 프로세스의 번호를 출력하는 함수이다.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void int_handler(int sig)
{
printf("Process %d received signal %d\n", getpid(), sig);
exit(0);
}
void fork13()
{
pid_t pid[N];
int i;
int child_status;
signal(SIGINT, int_handler);
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0)
while(1);
for (i = 0; i < N; i++) {
printf("Killing process %d\n", pid[i]);
kill(pid[i], SIGINT);
}
for (i = 0; i < N; i++) {
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminated abnormally\n", wpid);
}
}
5. pause()
시그널을 받기 전까지 프로세스를 정지 (sleep) 시키는 시스템 콜이다.
'학부 수업 정리 > 시스템프로그래밍 (21-2)' 카테고리의 다른 글
[시스템] 6. IPC (Inter Process Communication) (0) | 2021.12.28 |
---|---|
[시스템] 5. Race Condition & Signal Blocking (0) | 2021.12.28 |
[시스템] 4. 어셈블리 기초 (0) | 2021.12.28 |
[시스템] 2. 멀티 프로세스 프로그래밍 (0) | 2021.12.28 |
[시스템] 1. Processes - 생성과 삭제 (0) | 2021.12.28 |