# 8장 예외적인 제어흐름

제어 흐름의 양상은 크게 세 종류이다.

* 메모리에 연속적으로 할당되어 있는 명령어들을 순차적으로 실행 (기본적)
* 프로그램 상태의 변화에 반응하여 제어 흐름이 갑자기 바뀌는 경우 jump, call, return 등 실행
* 예외적인 제어 흐름(ECF)

예외적인 제어 흐름(ECF)은 시스템 상태의 갑작스러운 변화에 대응하여 프로그램의 실행 흐름이 변화하는 것을 의미한다.

ECF는 하드웨어, 운영체제, 응용 프로그램 수준에서 모두 발생할 수 있으므로 이에 대한 대응도 전부 준비되어 있어야 한다.

* 하드웨어 : 하드웨어에 의해서 검출되는 이벤트들은 예외 핸들러로 갑작스런 제어이동을 발생시킨다.
* 운영체제 : 문맥전환을 통해서 사용자 프로세스에서 다른 프로세스로 제어가 이동한다.
* 응용수준 : 프로세스는 시그널을 수신하는 곳에 있는 시그널 핸들러로 제어를 급격히 이동하는 다른 프로세스로 시그널을 보낼 수 있다.
* 개별 프로그램 : 일반적인 스택 운영을 회피하고 다른 함수 내 임의의 위치로 비지역성 점프를 하는 방법으로 에러에 대응할 수 있다.

#### **ECF를 이해해야 하는 이유**

* 시스템 개념 이해 : ECF는 운영체제가 입출력, 프로세스, 가상메모리를 구현하기 위해 사용하는 기본 메커니즘임
* 응용 프로그램과 운영체제의 상호작용 이해 : 응용 프로그램은 트랩(trap) 또는 시스템 콜(system call)이라고 알려진 ECF를 이용해서 운영체제로부터 서비스를 요청함. 기본 시스템 콜 메커니즘을 이해하면 데이터를 디스크에 쓰거나, 네트워크에서 데이터를 읽는 등의 서비스들이 어떻게 응용 프로그램에 제공되는지 이해하는 데 도움됨
* 재미있는 새로운 응용프로그램 작성 : 새로운 프로세스를 만들거나, 프로세스가 종료하기를 기다리거나, 다른 프로세서에게 시스템 내의 예외 이벤트를 알리거나, 이러한 이벤트를 감지하고 반응하는 등의 작업을 위한 ECF 메커니즘은 운영체제가 응용프로그램들에게 제공한다. ECF 메커니즘들을 이용해야 Unix 쉘과 웹 서버 같은 흥미로운 프로그램을 작성할 수 있다.
* 동시성 : ECF는 컴퓨터 시스템에서 동시성을 구현하는 기본 메커니즘임. 실행 시간이 중첩되는 프로세스, 쓰레드, 응용프로그램의 실행을 가로채는 예외처리 핸들러, 응용프로그램의 실행을 가로채는 시그널 핸들러 등...
* 소프트웨어적인 예외의 동작 이해 : C++과 자바 같은 언어는 try, catch, throw 문장을 통해서 소프트웨어 예외 메커니즘을 제공함. 소프트웨어 예외는 프로그램이 에러 발생 시에 비지역성(nonlocal) 점프(즉, 일반적인 call/return 스택 방식에 위배되는 점프)를 하도록 해준다. 비지역성 점프는 응용 수준의 ECF이며, C에서는 setjmp, longjmp 함수로 제공된다. 이러한 저수준 함수들을 이해하면 소프트웨어 수준의 고수준 예외를 이해할 수 있게됨.

앞 장까지는 응용이 하드웨어와 어떻게 상호작용하는지 배우는 거였고,\
이번 장은 응용이 운영체제와 어떻게 상호작용하는지 배우기 시작한다.\
이러한 상호작용들은 모두 ECF를 중심으로 돌아간다.

***

## 8.1 예외 (Exceptions)

예외 : 프로세서 상태의 변화에 대한 대응이며, 제어흐름의 갑작스런 변화.

프로세서가 어떤 명령어를 실행하고 있을 때 프로세서 상태에 변화가 일어나면(명령어와 관련 있을수도 있고 없을수도 있음) 예외 테이블이라는 점프 테이블을 통해서 이 특정 종류의 이벤트를 처리하기 위해 특별히 설계된 운영 체제 서브루틴(예외처리 핸들러)으로 간접 프로시저 콜을 하게 된다.

예외처리 핸들러가 처리를 끝마치면, 예외상황을 발생시킨 이벤트의 종류에 따라서 세 가지 중 하나가 발생한다.

1. 핸들러가 제어를 이벤트가 발생했을 때 실행되고 있던 명령어로 돌려준다.
2. 핸들러가 제어를 예외가 발생하지 않았다면 다음에 실행되었을 명령어로 돌려준다.
3. 핸들러가 중단된 프로그램을 종료한다.

### 8.1.1 예외처리 (Exception Handling)

예외 처리 : 예외가 발생했을 때 운영체제와 하드웨어가 이를 처리하는 과정

예외 번호 : 가능한 예외 상황들에 중복되지 않는 양의 정수를 예외 번호로 할당함

* 프로세서 설계자가 부여한 것 : divide by zero, 페이지 오류, 메모리 접근 위반, breakpoint, 산술연산 오버플로우 등...
* 운영체제 커널 설계자가 할당한 것 : 시스템 콜, 외부 I/O 디바이스로부터의 시그널 등...

#### **예외 처리 과정**

1. 현재 명령어 완료 : 현재 실행중인 명령어가 완료됨
2. 예외 발생 감지 : 예외가 발생했음을 감지
3. 상태 저장 : 현재 프로세스의 상태(프로세서 레지스터 값, 프로그램 카운터 등)를 저장
4. 예외 조회 : 예외 테이블을 참조하여 해당 예외의 예외 핸들러를 찾음
5. 예외 처리기 실행 : 예외 처리기를 실행하여 예외 처리
6. 상태 복구 및 복귀 : 예외 처리기가 완료되면, 저장된 상태를 복구하여 원래의 프로그램 흐름으로 복귀하거나 프로그램을 종료함.

#### **일반적인 함수의 호출과 예외 핸들러의 차이점**

* 프로세서는 프로시저 콜을 사용해서 핸들러로 분기하기 전에 스택에 리턴 주소를 저장해둠. 예외의 종류에 따라 리턴 주소는 현재 명령어이거나 다음 명령어가 됨.
* 프로세서는 핸들러가 리턴할 때 중단된 프로그램을 다시 시작하기 위해 필요하게 될 추가적인 프로세서 상태를 스택에 푸시함.
* 제어가 사용자 프로그램에서 커널로 전환하고 있을 때, 모든 정보들은 사용자 스택 위가 아니라 커널 스택 상에 푸시됨.
* 예외 핸들러는 커널 모드에서 돌아가는데, 이것은 이들이 모든 시스템 자원에 완전히 접근할 수 있는 것을 의미함.

### 8.1.2 예외의 종류 (Types of Exceptions)

발생 원인과 처리 방식에 따라 분류한 예외 종류 (책마다 정의 다름)

#### **인터럽트 (Interrupts)**

* 비동기적으로 발생하며, 외부 장치(ex. 키보드, 마우스, 타이머)에서 발생함.
* 현재 명령어가 완료된 후 처리됨.
* 주로 하드웨어 장치에서 발생하며, 프로세서에게 즉시 주의를 끌어야 하는 중요한 이벤트를 알림.
  * 하드웨어 인터럽트: 키보드 입력, 마우스 움직임, 타이머 이벤트, 네트워크 패킷 수신 등과 같은 외부 장치의 요청에 의해 발생함.
  * 타이머 인터럽트: 운영체제의 스케줄러가 프로세스의 실행 시간을 관리하기 위해 주기적으로 발생시
  * I/O 인터럽트: 디스크, 네트워크 인터페이스 등 I/O 장치에서 데이터 전송이 완료되었음을 알리기 위해 발생함.

#### **트랩 (Traps)**

* 동기적으로 발생하며, 주로 소프트웨어 조건(ex. 시스템 호출, 디버그 이벤트)에서 발생함.
* 명령어가 완료된 후 발생하며, 예외 처리 후 원래의 명령어 흐름으로 복귀함.
  * 시스템 콜 : 프로그램이 운영체제의 커널 서비스를 요청할 때 발생함. (ex. 파일 입출력, 메모리 할당, 프로세스 제어 등의 작업)
  * 디버그 트랩 : 디버거가 프로그램의 실행을 제어하기 위해 브레이크포인트를 설정하면 발생.

#### **폴트 (Faults)**

* 동기적으로 발생하며, 주로 오류 조건(ex. 페이지 폴트, 정수 연산 오버플로)에서 발생함.
* 문제가 해결되면 원래 명령어로 복귀하여 다시 실행을 시도함.
  * 페이지 폴트 : 프로그램이 접근하려는 메모리 페이지가 현재 메모리에 로드되지 않았을 때 발생. 커널은 페이지 폴트 처리기를 호출하여 필요한 페이지를 메모리에 로드함.
  * 보호 오류 (Protection Faults): 프로그램이 접근할 수 없는 메모리 영역에 접근하려고 할 때 발생. 예를 들어, 읽기 전용 메모리에 쓰기를 시도하는 경우.
  * 정수 연산 오버플로 : 정수 연산 중 오버플로가 발생할 때 발생함.

#### **어보트 (Aborts)**

* 비동기적으로 발생하며, 치명적인 하드웨어 오류나 시스템 오류에서 발생함.
* 어보트는 복구할 수 없는 상황을 나타내며, 프로그램을 즉시 종료함.
  * 하드웨어 오류 : 메모리 오류, 버스 오류 등 치명적인 하드웨어 오류가 발생할 때 발생함.
  * 시스템 불안정 : 시스템이 안정적으로 운영될 수 없을 때 발생함.

***

## 8.2 프로세스 (Process)

프로세스는 실행 중인 프로그램의 인스턴스를 의미함.\
운영체제는 여러 프로세스를 동시에 실행시켜 사용자에게 여러 작업이 동시에 수행되는 것처럼 보이게 함.

각각의 프로세스는 특정 문맥에서 실행됨.\
**문맥(Context) : 프로그램이 올바르게 실행되기 위해 필요한 상태 정보들의 집합**

사용자가 실행 목적파일의 이름을 쉘에 입력해서 프로그램을 돌릴 때마다 쉘은 새로운 프로세스를 생성하고, 실행 목적파일을 이 새로운 프로세스의 문맥에서 실행함.

프로세스는 두 가지 중요한 추상화를 제공함.

독립적인 논리적 제어 흐름 (Independent Logical Control Flow)\
: 프로세서는 프로그램 내의 명령어들을 차례대로 중단 없이 실행하는 것처럼 보이고,

개인 주소 공간 (Private Address Space)\
: 프로그램의 코드와 데이터는 시스템 메모리 상의 유일한 객체인 것처럼 보임.

### 8.2.1 논리적인 제어흐름 (Logical Control Flow)

**프로세스는 시스템에 서로 다른 여러 프로그램들이 동시적으로 동작하고 있지만, 프로세서를 혼자서 사용한다는 착각을 느끼게 한다.**

디버거 써보면 명령어들에 PC (프로그램 카운터) 값들이 대응된다는 걸 알 수 있음.\
이러한 PC 값들의 배열을 논리적 제어흐름 또는 간단히 논리흐름이라고 부름.

하나의 프로세서에는 여러 프로세스들이 교대로 돌아감.\
각 프로세스는 자신의 흐름의 일부를 실행한 후 다른 프로세스들이 실행되는 동안 일시적으로 정지된다.

### 8.2.2 동시성 흐름 (Concurrent Flow)

논리흐름은 컴퓨터 시스템 내에서 여러 가지 다른 형태를 가짐.\
예외 핸들러, 프로세스, 시그널 핸들러, 쓰레드, 자바 프로세스 등...

동시성 흐름 : 자신의 실행시간이 다른 흐름과 겹치는 논리흐름. 이 두 흐름은 동시에 실행한다고 표현함.\
동시성 : 여러 개의 논리적 흐름이 동시적으로 실행되는 것\
멀티태스킹 : 프로세스가 다른 프로세스들과 교대로 실행되는 것. 타임 슬라이싱이라고도 함.\
타임 슬라이스 : 한 프로세스가 자신의 흐름 일부를 실행하는 매 시간 주기

동시적 흐름은 프로세서 코어나 컴퓨터 개수와는 무관함. 두 흐름이 시간상으로 중첩되면, 이들이 동일한 프로세서에서 돌아가고 있더라도 이들은 동시적. 두 개의 흐름이 서로 다른 프로세서 코어나 컴퓨터에서 동시에 돌아가고 있다면 이건 병렬 흐름.

### 8.2.3 사적 주소공간 (Private Address Space)

**프로세스는 각 프로그램에 사적 주소 공간을 제공함으로써 자신이 시스템의 주소공간을 혼자서 사용한다는 착각을 불러일으킨다.**

사적 주소 공간의 특정 주소에 연결된 메모리의 한 개의 바이트가 일반적으로 다른 프로세스에 의해서 읽히거나 쓰일 수 없기 때문에, 이 공간은 사적(Private)이다.

각각의 사적 주소공간에 저장된 내용은 서로 다르더라도, 구조는 동일하다.

### 8.2.4 사용자 및 커널 모드 (User and Kernel Mode)

프로세서는 응용프로그램이 실행할 수 있는 명령어들을 제한해야 한다.\
프로세서는 이를 위해 프로세스가 현재 가지고 있는 특권(Privilege)을 저장하는 일부 제어 레지스터로 **모드 비트**를 사용한다.

모드 비트가 설정되면 프로세스는 커널 모드로 동작한다. 커널 모드에서 돌고 있는 프로세스는 모든 명령어를 사용할 수 있고, 시스템 내의 어떤 메모리 위치도 접근할 수 있다.

모드 비트가 없으면 프로세스는 사용자 모드에서 돌아간다. 사용자 모드의 프로세스는 프로세서를 멈추거나, 모드 비트를 변경하거나, 입출력 연산을 초기화하는 등의 특수 인스트럭션을 실행할 수 없다. 이러한 시도를 하면 보호 오류가 발생한다. 그 대신 **사용자 프로그램은 시스템 콜을 통해서 커널 코드와 데이터에 간접적으로 접근**해야 한다.

프로세스가 사용자 모드에서 커널 모드로 진입하는 유일한 방법은 인터럽트, 오류같은 예외를 통해서다. 예외 핸들러에서 돌아오면 다시 유저 모드가 된다.

### 8.2.5 문맥 전환 (Context Switch)

<figure><img src="/files/a7YyW8B6eCLEeaxACrzS" alt=""><figcaption></figcaption></figure>

운영체제 커널은 문맥 전환이라는 ECF의 상위수준 형태를 사용해서 멀티태스킹(프로세스가 다른 프로세스들과 교대로 실행되는 것)을 구현하고 있다. 문맥 전환은 ECF에 해당하는 예외 매커니즘을 기반으로 구현된다.

* 문맥 : **커널이 중단됐던 프로세스를 다시 시작하기 위해서 필요로 하는 상태 정보들**이다. 범용 레지스터, 프로그램 카운터, 상태 레지스터, 유저 스택, 커널 스택 등의 정보들로 구성된다.
* 스케줄링 : 프로세스가 실행되는 동안 어떤 시점에 현재 프로세스를 정지하고 다른 프로세스를 다시 시작할지 결정하는 것. **스케줄러**라고 불리는 커널 내부의 코드에 의해 처리됨. 커널이 실행할 새 프로세스를 선택하면 그 프로세스는 커널에 의해 **스케줄**된 것.
* 문맥 전환 : **커널이 실행할 새 프로세스를 스케줄한 후에 현재 프로세스를 정지하는 것**

문맥 전환은 커널이 사용자를 대신해서 시스템 콜을 실행하고 있을 때 일어날 수 있다. 만약 어떤 시스템 콜이 특정 이벤트의 발생을 기다리고 있다면, 커널은 현재 프로세스를 sleep시키고 다른 프로세스로 문맥 전환한다.

문맥 전환은 인터럽트에 의해 발생할 수도 있다. 예를 들어, 모든 시스템은 대개 1 ms 또는 10 ms마다 주기적인 타이머 인터럽트를 생성하는 어떤 메커니즘을 갖고 있다. 타이머 인터럽트가 일어날 때마다 커널은 현재 프로세스가 충분히 오래 실행됐다고 판단하고 문맥 전환한다.

***

## 8.3 시스템 콜의 에러 처리 (System Call Error Handling)

시스템 콜 : 운영 체제의 커널에서 제공하는 서비스를 사용자 프로그램이 사용할 수 있도록 하는 메커니즘

* 프로그램이 파일을 읽거나 씀
* 새로운 프로세스를 생성함
* 프로그램을 종료함

시스템 콜 쓸 때 오류가 발생하면 **반드시** 에러 체크를 해줘야되는데,\
프로그래머들은 코드가 길어져서 에러 체크를 생략하는 경향이 있음.\
이럴 땐 함수로 묶어주면 됨.

#### **Unix 스타일의 에러 처리**

```
if ((pid = fork()) < 0) {
    fprintf(stderr, "fork error: %s\n", strerror(errno));
    exit(0);
}
```

Linux `fork` 함수를 호출할 때 오류를 확인하는 코드인데, 너무 김

```
void unix_error(char *msg) {
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(0);
}
```

이렇게 함수를 정의해놓으면

```
if ((pid = fork()) < 0)
    unix_error("fork error");
```

에러 보고 함수를 짧게 줄일 수 있음

#### **에러 처리 래퍼 (Error-Handling Wrappers)**

```
pid_t Fork(void) {
    pid_t pid;
    if ((pid = fork()) < 0)
        unix_error("Fork error");
    return pid;
}
```

에러 처리 함수를 아예 통으로 만들어버릴 수도 있음

```
pid = Fork();
```

그럼 해당 함수만 호출하면 에러 체크 가능

#### **세 가지 스타일의 에러 처리**

* Unix 스타일: 함수의 반환값을 통해 오류 코드와 유용한 결과를 모두 전달함. 예를 들어, wait 함수는 오류가 발생하면 -1을 반환하고, errno에 오류 코드를 설정함.
* Posix 스타일: 성공(0) 또는 실패(비 0)를 반환값으로 나타내고, 유용한 결과는 참조에 의한 함수 인수로 반환함. 예를 들어, pthread\_create 함수는 스레드의 ID를 첫 번째 인수로 반환함.
* GAI 스타일: 성공 시 0을 반환하고, 실패 시 비 0 값을 반환함. 예를 들어, getaddrinfo 함수는 성공 시 0을 반환하고, 실패 시 오류 코드를 반환함.

***

## 8.4 프로세스의 제어 (Process Control)

운영체제와 프로그램이 프로세스를 생성하고, 종료하고, 관리하는 방법들 설명

### 8.4.1 프로세스 ID 얻기

각 프로세스는 고유한 프로세스 ID(PID)를 가짐.

리눅스 시스템에서\
`getpid` 함수 : 현재 프로세스의 PID 얻을 수 있음\
`getppid` 함수 : 부모 프로세스의 PID 얻을 수 있음

### 8.4.2 프로세스의 생성과 종료

프로그래머 관점에서 프로세스는 다음의 세 가지 상태 중 하나로 생각할 수 있음

* 실행중 : 프로세스는 CPU에서 실행하고 있거나 실행을 기다리고 있으며, 궁극적으로 커널에 의해서 스케줄될 것.
* 정지 : 프로세스가 정지된 상태이고 스케줄되지 않음. 프로세스는 SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 시그널을 받게되면 그 결과로 정지하고, SIGCONT 시그널을 받으면 다시 실행 시작함.
* 프로세스가 영구적으로 정지됨. 세 가지 이유 중 하나로 인해 종료됨. (1) 프로세스를 종료하는 시그널을 받았을 때, (2) 메인 루틴에서 리턴할 때, (3) exit 함수를 호출할 때

#### **fork 함수**

새로운 프로세스를 생성하는 기본적인 방법은 `fork` 시스템 콜을 사용하는 것.\
부모 프로세스는 fork 함수로 자식 프로세스를 생성할 수 있음.\
새로 만들어진 자식 프로세스는 부모 프로세스의 복사본이 됨.\
부모 프로세스와 자식 프로세스는 동일한 프로그램 코드를 실행하지만, 각 프로세스는 독립적인 주소 공간을 가짐. (PID도 다름)\
`fork`는 두 번 반환됨. 부모 프로세스는 자식의 PID를 반환받고, 자식 프로세스는 0을 반환받음.

#### **exit 함수**

프로세스는 `exit` 함수를 호출하여 종료할 수 있음.\
`exit` 함수는 프로세스의 종료 상태를 전달하며,\
부모 프로세스는 `wait` 함수를 사용하여 자식 프로세스의 종료 상태를 청소할 수 있음.

### 8.4.3 자식 프로세스의 청소 (Reaping Child Processes)

프로세스가 종료될 때, 커널은 시스템에서 프로세스를 즉시 제거하지 않음.\
프로세스는 부모가 청소할 때까지 종료된 상태로 남아있음.\
이렇게 남아있는 프로세스를 **좀비**라고 함.

부모 프로세스가 종료할 때, 커널은 고아가 된 자식들을 init 프로세스에 입양시킴.

\**init 프로세스 \**

* PID가 1번
* 시스템 초기화 과정에서 커널에 의해 생성됨
* 절대 종료되지 않음
* 모든 프로세스의 조상임

적당히 살다가 죽는 프로세스들은 자식들을 청소 안 해도 init 프로세스가 청소해주는데,\
쉘이나 서버같이 오랫동안 실행하는 프로그램들은 항상 자신의 좀비들을 소거해야 함.\
좀비들이 실행되고 있지 않더라도 이들은 여전히 시스템 메모리 자원을 잡아먹기 때문임.

부모가 종료된 자식을 청소할 때 커널은 자식의 exit 상태를 부모에게 전달하고 종료된 프로세스를 없앰.

#### **waitpid 함수**

프로세스는 `wait` 또는 `waitpid` 함수를 사용하여 종료된 자식 프로세스를 청소할 수 있음.

waitpid 함수는 기본적으로 (options = 0일 때) wait set 내의 자식 프로세스 하나가 종료할 때까지\
호출한 프로세스의 실행을 정지시킴.\
만일 wait set 내의 프로세스가 호출 시에 이미 종료한 상태라면, waitpid는 즉시 리턴함.\
어떤 경우든 waitpid 함수는 종료된 자식의 PID를 리턴함.

### 8.4.4 프로세스 재우기 (Putting Processes to Sleep)

sleep 함수는 지정된 시간 동안 프로세스를 정지시킴.\
pause 함수는 시그널이 수신될 때까지 함수를 정지시켜놓음.

### 8.4.5 프로그램의 로딩과 실행 (Loading and Running Programs)

`exec` 함수 계열은 현재 프로세스를 새로운 프로그램으로 대체함.\
`execve` 함수는 현재 프로그램의 문맥(context) 내에서 새로운 프로그램을 로드하고 실행함.

#### 프로그램 vs 프로세스

프로그램

* 코드 + 데이터
* 디스크 상에 목적파일이나 주소공간에 세그먼트로 존재할 수 있음
* 항상 어떤 프로세스의 문맥 내에서 돌아감

프로세스

* 실행 중에 있는 프로그램의 특정 사례

프로그램이 클래스면 프로세스는 인스턴스라고 비유할 수 있을듯?

***

## 8.5 시그널

#### **시그널**

* 시스템에서 특정 이벤트가 발생했음을 프로세스에 알리는 작은 메시지
* 리눅스 시스템은 총 30개의 다양한 시그널 유형을 지원함
* 각 시그널은 특정 시스템 이벤트와 연관되어 있으며, 기본 동작과 대응 이벤트가 지정되어 있음

### 8.5.1 시그널 용어

#### **시그널의 전달**

1. 시그널 보내기 : 커널이 특정 이벤트를 감지하거나 프로세스가 `kill` 함수를 호출하여 다른 프로세스에 시그널을 보낼 수 있음. 커널은 시그널을 대상 프로세스의 문맥 상태를 업데이트함으로써 보냄.
2. 시그널 받기 : 프로세스가 시그널을 받으면 커널은 해당 프로세스가 시그널에 반응하도록 강제한다. 프로세스는 시그널을 무시하거나, 종료하거나, 사용자 정의 시그널 핸들러를 실행하여 시그널을 처리할 수 있다.

보내졌지만 아직 받지 않은 시그널은 pending 시그널이라고 함.\
특정 타입에 대해 동시에 여러 개의 pending 시그널이 존재할 순 없음. 한 번에 하나만 존재함.\
같은 거 여러 개 발생하면 버려짐.

### 8.5.2 시그널 보내기

프로세스에 시그널을 보낼 때 프로세스 그룹의 개념을 사용함.

모든 프로세스는 정확히 한 개의 프로세스 그룹에 속하며, process group ID로 식별함.\
`getpgrp` 함수는 현재 프로세스의 프로세스 그룹 ID를 리턴함.\
`setpgid` 함수는 프로세스의 프로세스 그룹을 변경함.

* /bin/kill 프로그램: 이 프로그램은 다른 프로세스에 시그널을 보냄. 예를 들어, kill -9 15213 명령은 프로세스 15213에 SIGKILL 시그널을 보냄.
* 키보드에서 시그널 보내기: Ctrl+C를 누르면 커널은 포그라운드 프로세스 그룹의 각 프로세스에 SIGINT 시그널을 보냄. Ctrl+Z는 SIGTSTP 시그널을 보냄.
* kill 함수: 프로세스는 kill 함수를 사용하여 다른 프로세스에 시그널을 보낼 수 있음. kill(pid\_t pid, int sig) 형태로 사용됨.
* alarm 함수: 프로세스는 alarm 함수를 호출하여 자신에게 SIGALRM 시그널을 보낼 수 있음.

### 8.5.3 시그널 받기

커널이 프로세스를 커널 모드에서 사용자 모드로 전환할 때, (시스템 콜에서 리턴하거나 문맥 전환을 끝마치거나 할 때) 커널은 프로세스의 block되지 않은 pending 시그널의 집합을 체크함. 집합이 비어있으면 다음 명령으로 제어가 전달되지만, 비어 있지 않으면 커널은 시그널을 강제로 받게 함. 시그널을 수신하면 프로세스는 특정한 동작을 수행한 뒤 다음 명령어로 진행함.

각 시그널 타입의 기본 동작 예시

* 프로세스가 종료함
* 프로세스를 종료하고 코어를 덤프함
* 프로세스가 SIGCONT 시그널에 의해 재시작될 때까지 정지함
* 프로세스가 시그널을 무시함

코어 덤프 : 컴퓨터 프로그램이 비정상적으로 종료될 때, 프로세스의 메모리 상태를 파일로 저장하는 것. 이를 통해 개발자는 프로그램이 왜 비정상 종료됐는지 분석할 수 있음

### 8.5.4 시그널 블록하기와 블록 해제하기

프로세스는 특정 시그널의 수신을 차단할 수 있음. 시그널이 차단되면, 해당 시그널이 대기 상태로 남아 있다가 프로세스가 시그널 차단을 해제할 때까지 수신되지 않음. 커널은 각 프로세스에 대해 대기 중인 시그널 집합과 차단된 시그널 집합을 유지 관리함.

리눅스는 시그널을 차단하기 위해 묵시적인 방법과 명시적인 방법을 제공함.

* 묵시적 블록 방법 : 기본적으로, 커널은 핸들러에 의해 처리되고 있는 유형의 모든 대기 시그널들의 처리를 막음.
* 명시적 블록 방법 : 응용 프로그램들은 `sigprocmask` 함수와 이들의 도움함수를 이용해서 시그널들을 명시적으로 블록하거나 블록 해제할 수 있음

### 8.5.5 시그널 핸들러 작성하기

#### **핸들러가 어려운 이유**

1. 핸들러는 메인 프로그램과 동시적으로 돌아가고, 같은 전역변수를 공유함. 그래서 메인 프로그램이나 다른 핸들러들과 뒤섞일 수 있음.
2. 어떻게 그리고 언제 시그널들이 수신될 수 있는지는 종종 직관적이지 않음.
3. 시스템이 다르면 시그널 처리 방식도 다름.

그래서 핸들러는 안전하게 작성해야 함.

#### **핸들러 기본 작성 지침**

1. **핸들러를 가능한 한 간단하게 유지** : 그냥 전역 플래그 한 개를 설정하고 즉시 리턴해도 됨.
2. **비동기-시그널-안전 함수만 호출** : p.738에 정리돼있음. 시그널이 발생하는 동안 호출되더라도 안전하게 동작하는 함수들임. 시그널 핸들러는 언제든지 실행될 수 있기 때문에, 재진입 문제(reentrancy issue)가 발생할 수 있음. 시그널 핸들러가 실행되는 동안 다른 함수가 동일한 자원에 접근하면 오류가 발생할 수 있기 때문임. 비동기-시그널-안전 함수 쓰면 이런 문제 방지 가능.
3. **errno를 저장하고 복원** : 많은 리눅스 비동기-시그널-안전 함수들은 에러를 갖고 리턴할 때 errno를 설정함. 이런 함수들을 핸들러 내에서 호출하면 errno에 의존하는 프로그램 내의 다른 부분들과 혼선이 생길 수 있음. 핸들러 진입 전에 errno를 지역 변수에 저장해놨다가 리턴하기 전에 복원하면 해결됨.
4. **모든 시그널을 차단하여 공유 데이터 구조에 접근** : 시그널 핸들러가 실행되는 동안 다른 시그널이 발생하면 공유 데이터 구조를 안전하게 접근하는 데 문제가 생길 수 있음. 이를 방지하기 위해 시그널 핸들러가 실행되는 동안 모든 시그널을 차단할 수 있음. 이는 시그널 핸들러가 실행되는 동안 다른 시그널이 발생하지 않도록 하여 공유 데이터 구조를 안전하게 보호하는 방법임.
5. **전역 변수를 volatile로 선언** : C 언어에서 volatile 키워드는 컴파일러에게 해당 변수의 값을 최적화하지 말고 항상 메모리에서 읽고 쓰도록 지시함. 이는 시그널 핸들러와 같이 비동기적으로 변경될 수 있는 변수에 유용함. 시그널 핸들러는 프로그램의 다른 부분과는 독립적으로 실행될 수 있기 때문에, 컴파일러는 volatile로 선언된 변수를 매번 메모리에서 읽도록 해야 함.
6. **플래그를 sig\_atomic\_t로 선언** : sig\_atomic\_t는 시그널 핸들러와 프로그램의 다른 부분 간에 원자적으로 접근할 수 있는 변수를 선언할 때 사용하는 데이터 타입임. 이는 시그널 핸들러가 해당 변수를 변경하는 동안 프로그램의 다른 부분에서 중간 상태를 볼 수 없도록 보장함. 즉, 변수의 읽기와 쓰기가 중단되지 않고 한 번에 완료됨.

원자적 접근 : 특정 연산이 다른 연산으로부터 중단되지 않고 한 번에 완료된다는 것

#### **정확한 시그널 처리**

pending 비트 벡터는 각 시그널 유형에 대해 정확히 한 개의 비트만을 포함하기 때문에 어떤 특정 유형의 대기 시그널은 최대 한 개만 존재할 수 있다. 즉, 보냈는데 버려지는 시그널이 생긴다는 뜻. 이렇게 되면 좀비 프로세스가 남을 가능성이 생긴다. 특히 SIGCHLD 시그널이 제대로 처리되지 않으면 좀비 프로세스가 남게 된다.

해결 방법들

1. 시그널 핸들러에서 wait 또는 waitpid 사용\
   시그널 핸들러에서 SIGCHLD 시그널을 수신할 때 자식 프로세스의 종료 상태를 수집함. waitpid를 루프에서 호출하여 모든 종료된 자식 프로세스의 상태를 수집할 수 있음.
2. SIGCHLD 시그널을 무시\
   SIGCHLD 시그널을 무시하도록 설정하면 자식 프로세스가 종료될 때 자동으로 그 종료 상태가 커널에 의해 수집되어 좀비 프로세스가 남지 않음. 하지만 자식 프로세스의 종료 상태를 부모가 확인할 수 없게 됨.
3. waitpid를 사용하는 부모 프로세스\
   부모 프로세스가 주기적으로 waitpid를 호출하여 종료된 자식 프로세스의 상태를 수집함. 이를 통해 시그널이 누락되더라도 좀비 프로세스를 수집할 수 있음.

### 8.5.6 치명적인 동시성 버그를 피하기 위해서 흐름을 동기화하기 (Synchronizing Flows to Avoid Nasty Concurrency Bugs)

동시적으로 실행되는 여러 흐름이 동일한 저장 위치를 읽고 쓰는 문제는 매우 복잡함.\
이를 해결하기 위해 흐름을 동기화하여 올바른 결과를 생성하도록 해야함.

#### **고전적인 동기화 에러 : race**

1. 부모가 fork 함수를 실행하고, 자식이 부모 대신 실행됨
2. 부모가 깨어나기 전에 자식이 종료됨. SIGCHLD 시그널을 보냄.
3. 커널이 SIGCHLD를 발견하고 부모의 시그널 핸들러를 실행함. (부모는 실행되기 전임)
4. 시그널 핸들러는 종료된 자식을 청소해야 하지만, 부모가 자식을 리스트에 아직 추가를 안 해서 아무 일도 안 일어남.
5. 부모가 깨어나서 존재하지도 않는 자식을 작업 리스트에 추가함.

해결책 : fork 호출이 리턴할 때 커널이 자식 대신 부모를 실행하도록 스케줄하면,\
부모는 자식이 종료하고 시그널 핸들러가 청소 작업을 하기 전에 자식을 작업 리스트에 추가할 것.

### 8.5.7 명시적으로 시그널 대기하기

종종 메인 프로그램은 특정 시그널 핸들러가 동작하기를 명시적으로 기다려야 할 필요가 있음.\
예를 들어, 리눅스 쉘이 전면 작업을 생성할 때, 커널은 전면 작업이 종료되고 최종적으로 삭제될 때까지 기다려야 됨.\
while문으로 pid 계속 기다리는 건 프로세서 자원 너무 낭비함. 중간에 sleep 넣더라도 얼마나 자야 하는지 결정하는게 곤란함.\
(전면 작업 : 쉘 환경에서 실행되는 프로세스 중 사용자가 직접 상호작용하는 작업)

* `pause` 함수 : 시그널이 도착할 때까지 프로세스를 중단시킴
* `sigwait` 함수 : 주어진 시그널 집합 중 하나가 발생할 때까지 기다림
* `sigsuspend` 함수 : 시그널이 도착할 때까지 시그널 마스크를 변경하여 대기
* 시그널 마스크 : 현재 프로세스가 수신할 수 있는 시그널의 집합을 나타내는 비트 벡터. 프로세스는 이 마스크를 사용하여 특정 시그널을 차단하거나 허용할 수 있음.

***

## 8.6 비지역성 점프

비지역성 점프는 현재 실행 중인 함수의 컨텍스트를 벗어나서, 호출 스택의 상위에 있는 다른 함수로 제어를 직접 이동시키는 메커니즘임. 이는 C 언어의 표준 라이브러리에서 제공하는 setjmp와 longjmp 함수로 구현됨.

* setjmp: 현재 함수의 실행 상태를 저장
* longjmp: 이전에 setjmp에 의해 저장된 실행 상태로 제어를 이동시킴

용도

1. 오류 처리: 여러 함수 호출이 중첩된 깊은 호출 스택에서 오류가 발생했을 때, 비지역성 점프를 사용하여 상위 함수로 제어를 빠르게 이동시킬 수 있음. 이는 특히 리소스 할당과 해제를 관리해야 하는 상황에서 유용함.
2. 복잡한 제어 흐름: 비지역성 점프를 사용하면 복잡한 제어 흐름을 단순화할 수 있음. 예를 들어, 특정 조건이 충족되면 즉시 함수의 실행을 중단하고 상위 함수로 돌아가야 하는 경우에 사용할 수 있음.

***

### 8.7 프로세스 조작을 위한 도구

리눅스 시스템은 프로세스를 관찰하고 조작하기 위한 여러 가지 유용한 도구를 제공함.

* STRACE : 돌고 있는 프로그램과 자식들이 호출한 각 시스템 콜의 경로를 인쇄함.
* PS : 현재 시스템 내의 프로세스들(좀비 포함)을 출력
* TOP : 현재 프로세스의 자원 사용에 관한 정보를 출력
* PMAP : 프로세스의 메모리 맵을 보여줌
* /proc : 여러 가지 커널 자료구조의 내용을 사용자 프로그램이 읽을 수 있는 ASCII 문자 형태로 내보내는 가상 파일 시스템.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lazyartisan.gitbook.io/note/main-page/books/computer-systems-a-programmers-perspective/8.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
