[CS: APP] Chapter 8. Exceptional Control Flow

June 13, 2023, 7:49 p.m. · 11 min read · 🌐︎ ko

CS:APP system programming

컴퓨터를 켤 때부터 끌 때까지 PC가 $a_0, a_1, \cdots, a_{n-1}$의 sequence로 움직인다고 가정하자. 주소 $a_k$에 instruction $I_k$가 있는 식이다. 이때, PC가 $a_k$에서 $a_{k-1}$으로 옮겨가는 것을 control transfer라고 지칭하며, control transfer의 sequence인 ${\cdots, (a_{k-1}, a_k), (a_k, a_{k+1}), \cdots}$를 프로세서의 control flow라고 한다.

그런데, 프로그램 실행과 관련없는 시스템의 상태에 의해서도 smooth flow에 갑작스러운 변화가 생길 수 있다.

이러한 것들은 Exceptional Control Flow(ECF)를 통해 처리되며, HW, OS, application 등 모든 레벨이 관여한다.

8.1. Exceptions

Exception은 OS와 HW가 각각 부분적으로 구현한다. 그 정의는 프로세서 상태의 변화에 대응하여 control flow에 일어나는 갑작스러운 변화라 할 수 있다.

Exception Handling
Exception handling은 하드웨어와 소프트웨어의 복잡한 상호작용으로 구현된다.

Exception의 종류
Exception은 interrupt, trap, fault, abort의 네 가지로 분류된다.

  1. Interrupt: 프로세서 외부의 I/O 장치의 신호에 의해서 비동기적으로 일어남
  1. Trap: instruction이 의도적으로 발생시킨 exception
  1. Fault: handler가 수정할 가능성이 있는 exception
  1. Aborts: 복구가 불가능한 하드웨어 오류

Linux/x86-64 시스템에서의 exception

8.2. Process

Exception은 OS의 커널이 프로세스(process)라는 개념을 제공할 수 있도록 하는 building block이 된다.

이 절에서는 OS가 프로세스를 어떻게 구현하는지보다는 logical control flow와 private address space의 두 추상화에 대해 설명한다. 각각은 우리 프로그램이 {프로세서, 메모리} 자원을 독점하고 있다는 착각을 제공해준다.

Logical Control Flow: PC 값의 수열을 의미

Concurrent Flows: 다른 logical flow와 실행시간이 겹치는 control flow

유저 모드와 커널 모드

Context Switches

  1. 현 프로세스의 context를 저장
  2. 이전에 preempt된 프로세스의 context를 불러와 복원
  3. restored process에 컨트롤을 넘김

8.3. System Call Error Handling

system-level function에 에러가 발생하면,

  1. -1을 반환하고
  2. 전역변수 errno에 에러의 종류를 저장한다.

따라서 system-level function을 호출한 후에는 이를 항상 체크해줘야 하지만, 이는 매우 번거롭고 가독성을 떨어뜨린다. 따라서 CSAPP에서는 wrapper function들을 정의해 사용한다. 예시로 pid_t fork(void)에 대해서는 다음을 정의한다.

void unix_error(char * msg){
    fprintf(stderr, %s: %s\n, msg, stderror(errno));
}

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

따라서, fork()를 사용할 상황에 Fork()를 사용하면 의도한 효과를 가져오면서 에러 처리도 자동으로 되는 것이다. 앞으로 책에서는 csapp.h에 이들을 정의해두고 가져와 사용한다.

8.4. Process Control

Unix는 C 프로그램이 프로세스를 제어할 수 있도록 시스템 콜 형태로 다양한 인터페이스를 제공한다.

프로세스 ID 구하기

프로세스 생성과 종료

  1. Running: CPU상에서 실행중이거나, 스케줄되어 실행되기 위해 대기중인 상태
  2. Stopped: 프로세스가 중단된 상태

    • SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 등 시그널에 의해 발생
    • 중단된 후 다시 스케줄되지 않지만 SIGCONT 시그널을 받으면 다시 시작된다
  3. Terminated: 영구적인 stopped 상태

    • terminate를 명령하는 시그널을 받거나, main 함수가 반환했거나 exit()이 호출된 경우

프로세스의 생성과 종료에 관련된 system-level function은 다음이 있다.

자식 프로세스의 회수
프로세스가 종료(terminate)된 후에도 커널은 이를 시스템에서 바로 지우지 않고 부모 프로세스가 이를 회수(reap)할 때까지 terminated 상태를 유지한다. 이렇게 종료되었지만 회수되지 않은 프로세스를 zombie라고 한다.

    #include <unistd.h>
    pid_t waitpid(pid_t pid, int *statusp, int options)
    ```


    * default option(`options=0`)에서, `waitpid` waitset에 있는 자식 프로세스가 모두 종료될 때까지 실행을 멈추고 마지막으로 종료되는 자식의 PID를 반환
    * `options` 통해 행동을 바꿀  있음: 여러  적용시 `WNOHANG | WUNTRACED` 같이 bitwise OR 연산을 적용
        * `WNOHANG`: waitset 내에 종료된 자식이 하나도 없을  바로 반환시킴. 자식이 종료될 때까지  프로세스의 실행을 멈추지 않고 다른 작업을 하고 싶을  유용
        * `WUNTRACED`: 종료(terminated)뿐만 아니라 정지(stopped)되기만 해도 반환
        * `WCONTINUED`: waitset의 프로세스가 종료되거나 `SIGCONT` 받아 정지 상태에서 풀려났을 때까지 calling process를 정지시킴
    * `int *statusp` 상태 정보가 인코딩되는데, `wait.h` 정의된 매크로를 이용해 이를 해석 가능(p. 781 참고)
    * calling process에 자식이 하나도 없을  -1 반환하고 `errno` `ECHILD` 기록


* `wait`: `waitpid` 간략화된 버전으로 `wait(&status)` `waitpid(-1, &status, 0)` 동일하다.

**프로세스를 Sleep시키기**
```c
#include <unistd.h>
unsigned int sleep(unsigned int secs);
int pause(void);

프로그램의 로딩과 실행

forkexecve를 사용해 프로그램 실행하기 (pp. 790-792)
Unix 쉘과 웹 서버 등의 프로그램은 forkexecve 등의 함수를 매우 자주 사용한다.

8.5. Signals

Linux signal: Exceptional control flow의 고수준, SW 버전

Signal Terminology

Sending a Signal

Receiving a Signal

Blocking & Unblocking Signals
Signal을 block하기 위한 방법

Writing Signal Handlers
Signal handler를 작성하는 것은 다음의 여러 이유로 어려운 작업:

  1. main 프로그램과 concurrent하게 실행되면서 global variable을 공유함
  2. signal의 받기가 이루어지는 과정과 timing은 직관에서 벗어남
  3. 시스템마다 signal handling semantics가 각기 다름

Safe Signal Handling

Correct Signal Handling
동일 시그널이 처리중인 경우, 새로운 signal은 발생해도 queue되지 않음

Portable Signal Handling
시스템마다 signal-handling semantics가 달라서 발생하는 문제가 있음

Synchronizing Flows to avoid Nasty Concurrency Bugs

Explicitly Waiting for Signal

8.6. Nonlocal Jumps

Nonlocal jump는 C에서 제공하는 유저레벨의 exceptional control flow로, setjmplongjmp의 두 함수를 사용한다.(`setjmp.h에 위치)

즉, setjmp는 호출은 한번 되는데 반환은 여러 번(호출 당시에 한번, jump될 때마다 한 번씩) 되는 함수이다.

Nonlocal jump의 일반적인 활용은 다음이 있다: