프로세서에 전원을 처음 공급한 시점부터 전원을 끌 때까지 프로그램 카운터(실행할 기계어 코드 위치 지정)는 연속된 값들을 가정한다.
보통의 경우
→ 점진적인 순서로 두개의 메모리가 서로 나란히 있는 경우이다.
변화가 생기는 경우
→ jump, call 리턴 같은 프로그램 인스트럭션에 의해 발생된다. 이러한 명령어들은 프로그램 변수에 의해 상태 변화에 반응하도록 하기 위해 필요하다.
시스템들은 내부 프로그램 변수에 의해 표시되지 않음. 실행과 관련되어 있지 않은 시스템 상태에도 반응할 수 있어야함.
[ex]
→ 운영체제가 단순히 프로그램 실행 흐름만 따르게 아닌 하드웨어 이벤트나 시스템 상태 변화 등의 외부적인 요인에도 반응할 수 있어야한다.(인터럽트, 시그널, 블로킹/논블로킹 I/O)
예외적인 제어 흐름은 컴퓨터 모든 수준에서 발생한다.
[ex]
H / W: 하드웨어에서 발생되는 이벤트들은 예외 핸들러로 갑작스런 제어 이동을 발생.
OS 커널: 문맥전환을 통해서 사용자 프로세스에서 다른 프로세스로 제어가 이동.
응용: 프로세스는 시그널 핸들러(시그널을 수신하는 곳)로 제어를 급격히 이동하는 다른 프로세스로 시그널 보내기 가능.
개별 program: 일반적인 스택 운영 회피, 다른 함수 내 임의의 위치로 비지역성 점프로 에러 대응.
글의 흐름
앞으로 응용 프로그램과 운영체제가 어떻게 상호작용하는지 알아볼 것이다. 이러한 상호 작용은 모두 ECF를 중심으로 돌아간다.
글의 흐름은
시그널은 상위 수준의 소프트웨어 형태의 ECF이다. 프로세스와 커널이 다른 프로세스를 중단하도록할 수 있다.
각 시그널 타입은 특정 종류의 시스템 이벤트에 대응된다.
하위 수준 하드웨어 예외는 커널의 예외 핸들러에 의해 처리되고 정상적으로 사용자 프로세스에선 볼 수 없다.
시그널은 이러한 예외들을 사용자 프로세스에 노출해주는 메커니즘을 제공한다.
[ex]
어떤 프로세스가 0으로 나누려고함 → 커널이 SIGFPE 신호(8번)을 프로세스에 보냄
잘못된 인스트럭션을 실행 → SIGILL(4번) 보냄
잘못된 메모리를 참조 → SIGSEGV(11번) 보냄
다른 시그널들은 커널 내 or 다른 사용자 프로세스 내부의 상위수준 소프트웨어 이벤트에 대응.
[ex]
어떤 프로세스가 포그라운드에 돌아가고 있을 때, Ctral + C 입력 → 커널은 SUGINT(2번)을 포그라운드 프로세스 그룹에 속한 각 프로세서들에게 보냄.
어떤 프로세스는 다른 프로세스에 SIGKILL(9번)을 보내서 강제 종료도 가능.
자식 프로세스가 종료하거나 정지시 → 커널이 SIGCHILD(17번)을 부모에게 보냄.
밑에 사진은 시그널 종류이다.

시그널을 목적지 프로세스로 전달하는 것은 두 단계이다.
커널은 목적지 프로세스의 컨텍스트 내에 있는 일부 상태를 갱신해서 시그널을 목적지로 보낸다.
시그널이 두가지 이유 중의 하나로 배달된다.
1. 커널이 0으로 나누거나 자식 프로세스의 종료 같은 시스템 이벤트 감지.
2. 어떤 프로세스가 커널에 명시적으로 시그널을 목적지 프로세스에 보낼 것을 요구하기 위해서 kill 함수를 호출.(프로세스는 시그널을 자기 자신에게 보내기 가능)

커널이 시그널을 전달하면, 대상 프로세스는 그 시그널에 반응하도록 강제로 요청받게 됩니다.
프로세스는 해당 시그널에 대해 3가지 처리가 있다.
1. 시그널을 무시.
2. 시그널 때문에 종료.
3. 시그널 핸들러(signal handler)라고 불리는 사용자 정의 함수를 실행해서 시그널을 직접 처리.
위 사진이 그 과정을 그림으로 표현한 것이다.
보내졌으나 아직 받지 않은 시그널은 Pending Signal이라고 한다. 시간상 어느 시점에 특정 타입에 대해 최대 한 개의 펜딩 시그널이 존재할 수 있다.
타입 k의 펜딩 시그널을 가지고 있으면 다음 프로세스로 다음에 발생하는 k 타입의 시그널은 큐에 들어가지 않는다. 즉, 버려진다.
커널은 펜딩 내에 비트 k를 타입 k의 시그널이 배달될 때 마다 설정하며, 시그널 타입 k가 수신될 때마다 펜딩의 비트 k를 0으로 만든다.
펜딩 시그널: 최대 한 번만 수신된다. 각 프로세스에 대해, 커널은 펜딩 비트 벡터 내에 펜딩하고 있는 시그널의 집합을 관리하며, 블록 비트 벡터 내에서 블록된 시그널 집합을 관리한다.
Block: 프로세스는 선택적으로 어떤 시그널의 수신을 Block할 수 있다. 블록되면 시그널이 배달은 되지만 프로세스가 블록을 풀때까지 수신되지 않는다.
Unix 시스템은 시그널을 프로세스로 보내는 여러 가지 메커니즘을 제공한다. 모든 메커니즘은 프로세스 그룹 개념을 사용한다.
모든 프로세스는 한 개의 프로세스 그룹에 속한다. 양수 process group ID로 식별한다. getgrp함수는 현재 프로세스의 프로세스 그룹 ID를 리턴한다.
기본적으로, 자식 프로세스는 자신의 부모와 동일한 프로세스 그룹에 속한다.
프로세스는 자신의 프로세스 그룹 or 다른 프로세스 그룹을 setgrid 함수를 사용해서 변경 가능하다.
#include <unistd.h>
pid_t getpgrp(void);
setgrid 함수는 프로세스 pid의 프로세스 그룹을 pgrid(프로세스 묶음)로 변경한다. 만일 pid가 0이면 현재 프로세스의 PID가 사용된다. 만일 pgid가 0이면 pid 명시된 프로세스의 PID가 프로세스 그룹 ID로 사용된다.
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
linux> /bin/kill -9 15213
/bin/kill 프로그램은 다른 프로세스로 임의의 시그널을 보낸다.
SIGKILL(9번) 시그널을 다른 프로세스 15213에 보내면 음수 PID는 시그널이 프로세스 그룹 PID 내의 모든 프로세스(15213 내 모든 프로세스)로 보내지도록 한다.
위의 예시의 /bin/kill 처럼 완전한 경로를 사용하는 이유는 일부 Unix 쉘이 자신만의 내장 kill 명령어를 가지고 있기 때문이다.
유닉스 쉘은 job의 추상화를 통해 한 개의 명령줄을 해석한 결과로 만들어진 프로세스에 반영한다.
어떤 시점에서도 최대 한 개의 포그라운드 작업과 0 또는 그 이상의 백그라운드 작업이 존재한다.
linux> ls | sort
다음과 같이 작성하면 두 개의 프로세스가 Unix 파이프로 연결된 포그라운드를 만든다. 하나는 ls 프로그램을 실행하고, 다른 하나는 sort 프로그램을 실행한다.
쉘은 각 작업마다 별도의 프로세스 그룹을 만든다. 일반적으로 프로세스 그룹 ID는 작업 내의 부모 프로세스들 중 하나에서 가져온다.

ex) 위 그림은 하나의 포그라운드 작업과 두 개의 백 그라운드 작업을 갖는 쉘이다.
포그라운드 작업의 부모 프로세스는 PID 20이고 프로세스 그룹 ID 20을 가진다. 부모 프로세스는 두개의 자식 프로세스를 만들수 있다.(동일한 그룹이므로 pgid는 20으로 같다)
키보드에서 Ctrl + C를 입력하면, SIGINT 시그널을 포그라운드 프로세스 그룹에 모두 보낸다.
→ 포그라운드를 종료시킨다.
비슷하게 Ctrl + Z 를 입력하면 커널은 포그라운드 프로세스 그룹에 속한 모든 프로세스에게 SIGSTP를 보낸다.
→ 포그라운드 작업을 정지시킨다.
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
프로세스는 kill 함수를 호출해서 시그널을 다른 프로세스로 보낸다.(자신 포함)
→ PID가 0보다 크면, kill 함수는 시그널 번호 sig → 프로세스 PID로 보낸다.
자기자신을 포함해서 자기 자신의 pgid 그룹을 모두 죽이겠다.
SIGALRM 로 alam 함수를 호출하여 자기 자신에게 보낼 수 있다.
#include <unistd.h>
unsigned int alarm(unsigned int secs);
위 함수는 커널이 초마다 프로세스로 SIGALRM을 보내도록 한다.
위의 secs 가 0 이면 새로운 알람이 스케줄되지 않는다.
alarm으로의 호출은 어떤 이벤트가 발생해도 대기하는 모든 알람을 취소하고 대기하던 알람이 전달되었어야 할 때까지 (취소 아닐시) 남은 시간을 초 단위 숫자로 반환하거나, 대기하고 있으면 0을 리턴한다.
커널이 프로세스 p를 커널 모드에서 사용자 모드로 전환 시, 커널은 프로세스 p에 대한 블록되지 않은 pending & ~ blocked의 집합을 체크한다.
만일 집합이 비었있으면 커널은 제어를 p의 논리 제어 흐름 내의 다음 인스트럭션(I)으로 전달한다.
→ 비어 있지 않다면, 커널은 집합 내 어떤 시그널 k를 선택해서 p가 시그널 k를 수신토록한다.
시그널을 받으면 프로세스는 동작을 개시하고 완료했다면, 제어는 p의 논리 제어 흐름 내의 다음 인스트럭션(I)으로 돌아간다.
시그널 타입은 다음과 같은사전에 정의된 기본동작을 가진다.

위의 사진처럼 여러 시그널이 있다.
SIGKILL: 수신한 프로세스 종료
SIGCHLD: 시그널 무시
프로세스는 위의 시그널과 연관된 기본 동작을 signal 함수를 사용해서 수정할 수 있다.
그러나! SIGSTOP과 SIGKILL은 예외적으로 기본 동작을 변경할 수 없다.
signal 함수는 signum과 연결된 동작을 밑의 3가지 방법 중 하나로 바꿀 수 있다.
핸들러 주소를 signal 함수로 넘겨주는 방법은 기본 동작을 변경하는 것: handling installing
핸들러 호출: catching the signal
핸들러 실행: handling the signal
타입 k의 시그널 잡을 시, 시그널 k를 위해 설치된 핸들러는 k에 설정된 한 개의 정수 인자를 사용해서 호출.
→ 정수 인자는 동일한 핸들러 함수가 서로 다른 종류의 시그널을 잡을 수 있게 해준다.
핸들러가 return 문장을 실행시, 제어는 보통 프로세스가 중단되었던 제어흐름 내 인스트럭션으로 다시 전달된다.(보통이라고 한 이유는 일부 시스템에서 중단된 콜들이 에러가 발생하면 즉시 리턴하기 때문이다)
사용자가 Ctrl + C를 예시로 보면, 입력마다 SIGINT를 잡고 프로세서를 종료 시킨다. 그러나 밑의 경우에는 기본 동작을 수정하여 시그널을 잡기 → 메시지 출력 → 프로세스 종료 순으로 진행되게 바뀐 것이다.
// 기본 동작 수정 예시
#include "csapp.h"
2
3 void sigint_handler(int sig) /* SIGINT handler **/
4 {
5 printf("Caught SIGINT!\n");
6 exit(0);
7 }
8
9 int main()
10 {
11 /* Install the SIGINT handler */
12 if (signal(SIGINT, sigint_handler) == SIG_ERR)
13 unix_error("signal error");
14
15 pause(); /** Wait for the receipt of a signal */
16
17 return 0;
18 }
시그널 핸들러는 다음 그림과 같이 다른 핸들러에 의해 중단 가능하다.
밑의 예제는 메인 프로그램은 메인 프로그램을 중단하고, 제어를 핸들러 S로 전송하는 시그널 s를 붙잡는다. S가 돌고 있는 동안 프로그램은 시그널 t를 붙잡는다. S를 중단하고, 제어를 핸들러 T로 전송한다.

T가 리턴할 때, S는 중단한 위치에서 이어서 실행한다.
→ S는 리턴하고, 제어를 다시 메인 프로그램으로 전달하며, 처음에 떠난 자리에서 실행을 다시한다.
리눅스는 시그널을 블록하기 위해 묵시적이고 명시적인 방법을 제공한다.
기본적으로 커널은 핸들러에 의해 처리되고 있는 유형의 모든 대기 시그널들의 처리를 막는다.
ex) 바로 위의 그림에서 메인 프로그램에서 시그널 s를 잡아 핸들러 S를 실행하고 있다. 다른 시그널 s가 보내졌다면 대기하게 되지만, 핸들러 S가 리턴되기 전까진 수신 불가하다.
응용 프로그램들은 sigprocmask 함수와 이들의 도움 함수를 이용해서 시그널들을 명시적으로 블록하거나 해제할 수 있다. 해당 함수는 현재 블록된 시그널의 집합을 변경한다. 특정 동작은 how값에 따라 달라진다.
SIG_BLOCK: set에 있는 시그널들을 blocked에 추가
SIG_UNBLOCK: set에 있는 시그널들을 blocked에서 제거
SIG_SETMASK: blocked = set
oldset이 널이 아니면, blocked 비트 벡터의 이전 값이 oldset에 저장된다. set같은 시그널 집합들은 여러 함수를 사용해서 조작한다. (자세한건 책)
시그널 처리가 리눅스 수준 프로그래밍에서 가장 까다로운 부분이다.
시그널의 특성은 다음과 같다.
1. 핸들러는 메인 프로그램과 동시적으로 돌아가고, 같은 전역변수를 공유한다. 그래서 메인의 다른 핸들러들과 섞일 수 있다.
2. 어떻게 언제 시그널을 수신될 수 있는지 직관적이지 않다.
3. 다른 시스템들은 다른 시그널 처리 방식을 갖는다.
시그널 핸들러는 다른 핸들러들과 함께 돌아갈 수 있기 때문에 처리하기 어렵다. 만약, 핸들러와 메인 프로그램이 동시에 같은 전역 데이터 구조에 접근한다면, 결과는 예측할 수 없게 되고 치명적인 결과를 낼 수 있다.
그래서 안전한 핸들러를 작성하는 보수적인 지침을 알아보겠다. 지침을 무시하면, 동시성 에러를 낳아 위험해 질 수 있다. 이러한 에러가 있으면 대부분 정확하게 동작하지만, 실패한다면 예측할 수 없고 디버그하기 어려운 재현할 수 없는 방식으로 실패한다.
3번까지는 내용을 작성하였으나 그외의 자세한 내용은 책을 참고하자.

위 함수들은 리눅스가 안전하다고 보장하는 시스템 수준 함수들이다. 자주 사용하는 printf, sprintf, malloc, exit 등은 리스트에 없다.
시그널 핸들러에서 출력을 생성하는 유일한 안전한 방법은 write 함수를 사용하는 것이다. 특히나 printf나 sprintf 호출하면 안전하지 않다. 이런 제한사항을 피해가기 위해 우리는 Safe I/O 패키지라고 부르는 안전 함수를 개발했다. 이걸 시그널 핸들러에서 간단한 메시지를 출력하기 위해 이용 가능하다. (예시는 책을 확인해보자.)
우리가 제시한 지침들은 항상 반드시 필요한 것은 아니라는 의미에서 보수적인 것이다. 위 사항을 지키지 않고 유효할 때도 있다. 그러나 그러한 케이스는 찾기 어렵다. 그러므로 위의 접근법을 취하는 것이 좋다. (자세한 내용은 책)
시그널에 대한 직관적이지 않은 측면은 대기하는 시그널들이 큐에 들어가지 않는다는 점이다.
→ pending 비트 벡터가 각 시그널 유형에 대해 정확히 한 개의 비트만을 포함하기 때문이다.
만약 같은 유형 k의 두 개의 시그널이 목적지 프로세스에 보내지면, 두 번째 시그널은 버려진다.(큐에 안들어간다)
기본적인 구조는 부모 프로세스가 한동안 독립적으로 동작하는 몇 개의 자식들을 생성하고 종료한다. 부모는 좀비를 없애기 위해 자식들을 소거한다. 그러나 자식들이 돌고 있는 동안 부모가 다른 작업을 하길 원하므로 SIGCHLD 핸들러로 자식을 소거한다.(자식 종료 시 커널이 부모에게 SIGCHLD 보냄)
(예시는 책의 741 ~ 743쪽을 확인해보자.)
유닉스 시그널 핸들링의 또 다른 지저분한 측면은 서로 다른 시스템들이 서로 다른 시그널 처리 방식을 갖는다는 점이다.
위 이슈를 다루기 위해 Posix 표준은 sigaction함수를 정의하여 핸들러 설치시 사용자가 원하는 시그널 처리 개념을 명확히 명시하도록 한다.
#include <signal.h>
int sigaction(int signum, struct sigaction *act,
struct sigaction *oldact);
그러나 위함수는 사용자가 복잡한 구조의 항목들을 설정하도록 허용하기 때문에 불편하다. 그래서 Signal이라고 부르는 래퍼 함수를 정의하는 것이 좋다. 우리를 대신해서 sigaction을 호출한다.
밑에와 같은 처리 개념으로 시그널 핸들러를 설치한다.
우리의 모든 코드에서 Signal 래퍼를 이용한다.
같은 저장장치의 위치에서 읽고 쓰는 동시성 흐름을 프로그래밍하는 방법의 문제가 있다. 일반적으로, 다수의 잠재적인 제어의 중첩은 인스트럭션 수에 있어 기하급수적이다. 그래서 주요 문제는 동시성 흐름을 동기화해서 각각의 가능한 중첩들이 정확한 답을 만들게 되는 가장 큰 중첩들의 집합을 갖도록하는 것이다.
동시성 프로그래밍 자체가 어렵지만, 밑의 프로그램 예시를 살펴보겠다.
1 /* WARNING: This code is buggy! **/
2 void handler(int sig)
3 {
4 int olderrno = errno;
5 sigset_t mask_all, prev_all;
6 pid_t pid;
7
8 Sigfillset(&mask_all);
9 while ((pid = waitpid(-1, NULL, 0)) > 0) { /** Reap a zombie child **/
10 Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
11 deletejob(pid); /** Delete the child from the job list */
12 Sigprocmask(SIG_SETMASK, &prev_all, NULL);
13 }
14 if (errno != ECHILD)
15 Sio_error("waitpid error");
16 errno = olderrno;
17 }
18
19 int main(int argc, char **argv)
20 {
21 int pid;
22 sigset_t mask_all, prev_all;
23
24 Sigfillset(&mask_all);
25 Signal(SIGCHLD, handler);
26 initjobs(); /* Initialize the job list */
27
28 while (1) {
29 if ((pid = Fork()) == 0) { /* Child process */
30 Execve("/bin/date", argv, NULL);
31 }
32 33 Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); /* Parent process */
addjob(pid); /** Add the child to the job list */
34 Sigprocmask(SIG_SETMASK, &prev_all, NULL);
35 }
36 exit(0);
37 }
위 프로그램에서 addjob과 deletejob 함수는 작업 리스트로부터 추가하고 제거된다. 부모가 새로운 자식 프로세스를 생성한 후에 그 자식을 작업 리스트에 추가한다. 부모가 SIGCHLD 시그널 핸들러에서 종료한 자식(좀비)를 청소할 때, 자식으리 작업 리스트에서 삭제한다. 대충보면 정상적인 것 같지만, 5가지 순서에서 문제가 발생한다.
→ 부모의 메인 루틴과 시그널 핸들링 흐름들의 일부가 겹칠 때 addjob 전에 deletejob 호출 가능성이 있다. 존재하지 않은 작업으로 잘못된 엔트리가 작업 리스트에 존해하고 삭제할 수 없다. 반면에 이벤트들이 정확한 순서로 발생 할 수도 있다.
이게 race라고 알려진 고전적인 동기화 에러 예시다. 루틴 내의 addjob 호출과 핸들러 내 deletejob으로의 호출 간에 존재한다. addjob이 경주에서 이긴하면 답은 틀린다.
→ 디버깅하기 매우어려운데, 수백번 수행하다가 한 번 중첩상황이 경쟁을 유발할 수 있기 때문이다.
그래서 SIGCHLD를 fork 호출 전 블록하고 addjob을 호출한 후만 블록을 해제하면 자식이 작업 리스트에 추가된 후에 청소되는 것을 보장하게 된다.
밑의 그림은 대략적으로 나타낸 흐름도이다.

종종 메인 프로그램은 특정 시그널 핸들러가 동작하기를 명시적으로 기다려야할 필요가 있다. 위의 상황과 같다.
예를 들어, 어떤 리눅스 쉘이 전면작업을 생성할 때, 커널은 이 작업이 종료되고, SIGCHLD 핸들러에 의해 삭제될 때까지 기다려야한다.
스핀루프: CPU를 계속 점유하면서 아무 일도 하지 않고 짧은 루프를 반복 수행하는 것.
이러한 흐름으로 가는 것이 정확하나, 스핀루프는 프로세서 자원을 낭비한다. 그래서 이 이하의 글은 자원 낭비를 줄이기위한 3가지 방법에 대해 설명한다.
while (!pid) /*race*/
pause();
OR
sleep(1);
pause가 한개 이상의 SIGINT 시그널을 수신한 경우 중단될 수 있기 때문에 아직 루프를 사용한다. 그러나 SIGCHLD가 while 시험이 끝나고, pause 실행 전에 수신되면, pause는 여전히 잠을 잔다.
pause를 sleep으로 대체하는 옵션도 있다.
그러나 해당 코드는 느리다. 시그널이 while 이후, sleep 이전에 수신된다면, 이 프로그램은 루프의 종료조건을 다시 체크할 수 있을 때까지 긴 시간을 기다려야 한다. nanosleep과 같이 높은 정밀도의 sleep 함수를 사용하는 것도 안된다. 잠을 자야하는 길이를 결정하는 좋은 규칙이 없기 때문이다. 너무 작으면 루프가 낭비되고, 너무 크게하면 프로그램이 너무 느려진다. 적절한 해결책은 sigsuspend를 이용하는 것이다.
sigsuspend함수는 현재 블록된 집합들을 일시적으로 mask로 교체하고, 자신의 동작이 핸들러를 실행 또는 프로세스를 종료하는 시그널을 수신할 때까지 프로세스를 유예한다.
시그널의 동작이 종료하는 것이라면, sigsuspend로부터 리턴하지 않고 종료한다.
동작이 핸들러를 돌리는 것이라면, sigsuspend는 핸들러가 리턴한 후에 리턴해서 블록된 집합을 sigsuspend가 호출되었을 때의 상태로 복원한다.
→ sigprocmask와 pause로의 호출은 중단되는 일 없이 함께 일어나는 것을 보장해준다. 이건 어떤 시그널이 sigprocmask로 호출 이후, pause로의 호출 전에 수신되는 잠재적인 경쟁상태를 제거한다.
SIGCHLD는 sigsuspend로 호출하기 전에 블록된다. sigsuspend는 임시로 SIGCHLD를 언블록하고 부모가 시그널을 한 개 붙잡을 때까지 잠을 잔다. 리턴하기 전에 본래의 블록된 집합을 복원해서 SIGCHLD를 다시 블록한다.
여기서 두가지 케이스가 있다.
해당 지점에서 SIGCHLD는 블록되어 있고, 우리는 선택적으로 SIGCHLD를 언블록한다. 이것은 소거되어야하는 후면작업을 갖는 실제 쉘에서 유용할 수 있다.
sigsuspend 버전은 최초의 스핀 루프보다 덜 낭비적이어서 pause로 인해 생긴 경쟁 상태를 회피하고 sleep보다 더 효율적이다.
C에서는 비지역성 점프라고 불리는 사용자 수준의 예외적 흐름 제어를 제공한다.
이것은 보통의 콜-리턴 순서를 통할 필요 없이 하나의 함수에서 현재 실행하고 있는 다른 함수로 제어를 이동한다.
C언어에서 비지역성 점프는 setjmp, longjmp 함수가 있다.
현재의 실행 위치와 상태(=환경)를 env라는 버퍼에 저장합니다. 그리고 처음 호출될 때는 항상 0을 반환합니다.
하지만 이게 끝이 아니다. 이 함수는 longjmp와의 연계 때문에 한 번만 호출되지만, 두 번 이상 반환할 수 있습니다.
→ 현재 상태를 저장해라
longjmp(env, retval)는 이전에 setjmp로 저장해둔 그 위치로 프로그램의 흐름을 되돌립니다.
그렇게 되돌아가면, setjmp는 다시 돌아온 것처럼 동작하면서 이번에는 0이 아닌 retval 값을 반환하게 됩니다.이렇게 하면 멀리 떨어져 있는 코드 위치로 "점프"해서 돌아갈 수 있는 겁니다.
→ setjmp로 점프해 넘어가라
위의 두개의 함수가 유용한 이유가 무엇일까?
→ 예를 들어, 어떤 프로그램에서 여러 단계로 함수가 깊게 호출되다가, 깊숙한 곳에서 에러가 발생했다고 해보겠습니다. 보통은 에러 처리를 위해 하나하나 돌아가며 복귀해야 합니다.
그러나 setjmp와 longjmp를 사용하면, 에러 발생 즉시 미리 지정한 위치(예: 메인 함수)로 바로 점프해서 돌아올 수 있습니다. 이를 통해 복잡한 함수 호출 체인을 다 거치지 않고, 한 번에 에러 처리 코드로 돌아올 수 있다는 장점이 있습니다.
그러면 장점만 있을까요?
당연히 강력한만큼 위험성도 따릅니다. 예를 들어 함수 안에서 malloc 같은 동적 메모리 할당을 하고 나중에 free로 해제하려 했는데, longjmp로 갑자기 점프해버리면 그 해제 코드가 실행되지 않아 메모리 누수가 발생할 수 있습니다.
앞에서 설명한 것처럼 프로그램 실행 중에 신호(signal)가 도착하면, 신호 처리 함수(핸들러)가 실행되고, 그 처리가 끝나면 인터럽트되었던 위치로 되돌아갑니다.
하지만 어떤 경우에는 그렇게 되돌아가는 대신, 프로그램의 특정 지점으로 점프하여 흐름을 재설정(soft restart)하고 싶을 수 있습니다. 예를 들어, 사용자가 키보드에서 Ctrl+C (SIGINT)를 누르면, 원래 하던 일을 멈추고 처음부터 다시 시작하게 할 수 있습니다.
→ 이럴 때 사용하는 것이 바로 sigsetjmp와 siglongjmp입니다.
이 함수들은 각각 setjmp, longjmp의 신호 처리 전용 버전으로, 단순한 호출 상태뿐 아니라 신호 마스크(signal mask) 등도 함께 저장하고 복원할 수 있습니다.
sigsetjmp를 호출하여 현재 상태를 저장합니다.siglongjmp를 호출하여 프로그램을 다시 초기 지점으로 점프시킵니다.이로써 프로그램은 마치 다시 시작한 것처럼 동작하게 됩니다.
위의 두개의 시그널은 2개의 주의점이 있습니다.
sigsetjmp 이후에 설정해야 합니다sigsetjmp를 호출하기 전에 신호 핸들러를 등록해버리면, 아주 드물게 Ctrl + C가 먼저 발생해서 핸들러가 실행될 수 있습니다.siglongjmp가 참조할 저장된 환경이 아직 준비되지 않은 상태이므로, 예기치 않은 동작이 발생할 수 있습니다.sigsetjmp를 먼저 호출한 다음, 신호 핸들러를 등록해야 안전합니다.siglongjmp로 점프한 이후 실행되는 함수들은 반드시 비동기-신호 안전 함수(async-signal-safe)여야 합니다.exit 같은 함수는 신호 처리 후 코드에서 호출되지 않기 때문에 도달 불가능(unreachable)하므로 안전합니다.리눅스 시스템에서는 프로세스를 관찰하고 조직하기 위해 도구를 제공한다.
| 도구 이름 | 주요 기능 |
|---|---|
strace | 실행 중인 프로그램이 호출하는 시스템 호출(system call)을 모두 추적해서 출력. 프로그램의 동작 원리를 이해하는 데 아주 유용함. 특히 -static 옵션으로 정적 링크된 바이너리를 만들면 출력이 깔끔함. |
ps | 현재 실행 중인 프로세스들의 목록을 보여줌. 좀비(zombie) 프로세스도 포함됨. |
top | 실시간으로 시스템 자원(CPU, 메모리 등)을 누가 얼마나 쓰고 있는지 보여줌. |
pmap | 특정 프로세스의 메모리 맵을 보여줌. 각 메모리 영역이 어떤 용도로 쓰이는지 확인 가능. |
/proc 파일시스템 | 가상 파일시스템으로 커널 내부의 다양한 데이터를 텍스트 형식으로 제공. (예: /proc/loadavg는 현재 시스템의 평균 부하를 보여줌.) |