컴퓨터를 켤 때부터 끌 때까지 PC가 의 sequence로 움직인다고 가정하자. 주소 에 instruction 가 있는 식이다. 이때, PC가 에서 으로 옮겨가는 것을 control transfer라고 지칭하며, control transfer의 sequence인 를 프로세서의 control flow라고 한다.
jmp
, call
, ret
등의 프로그램 내부 요인에 의해 갑작스럽게 변화가 생기기도 한다.그런데, 프로그램 실행과 관련없는 시스템의 상태에 의해서도 smooth flow에 갑작스러운 변화가 생길 수 있다.
이러한 것들은 Exceptional Control Flow(ECF)를 통해 처리되며, HW, OS, application 등 모든 레벨이 관여한다.
Exception은 OS와 HW가 각각 부분적으로 구현한다. 그 정의는 프로세서 상태의 변화에 대응하여 control flow에 일어나는 갑작스러운 변화라 할 수 있다.
Exception Handling
Exception handling은 하드웨어와 소프트웨어의 복잡한 상호작용으로 구현된다.
EFLAG
등의 프로세서 상태를 스택에 함께 푸쉬Exception의 종류
Exception은 interrupt, trap, fault, abort의 네 가지로 분류된다.
syscall n
의 instruction을 실행함으로써 파일 읽기(read
), 프로세스 생성(fork
), 프로그램 로드(execve
), exit
과 같은 작업들이 가능하다.Linux/x86-64 시스템에서의 exception
read
, write
, execve
, kill
등이 존재syscall n
으로 호출하는 것도 가능하지만 보통 wrapper 함수(system-level function이라 부름)를 활용syscall
instruction 실행시에는 %rax
가 syscall number를, %rdi, %rsi, %r10, %r9, %r8
등이 1~6번째 argument를 전달하는 식. 반환값은 %rax
에 덮어쓰기됨Exception은 OS의 커널이 프로세스(process)라는 개념을 제공할 수 있도록 하는 building block이 된다.
이 절에서는 OS가 프로세스를 어떻게 구현하는지보다는 logical control flow와 private address space의 두 추상화에 대해 설명한다. 각각은 우리 프로그램이 {프로세서, 메모리} 자원을 독점하고 있다는 착각을 제공해준다.
Logical Control Flow: PC 값의 수열을 의미
Concurrent Flows: 다른 logical flow와 실행시간이 겹치는 control flow
유저 모드와 커널 모드
/proc
filesystem에 텍스트 파일들을 export함으로써 유저 프로그램이 커널 데이터구조에 접근할 수 있게 해줌Context Switches
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
에 이들을 정의해두고 가져와 사용한다.
Unix는 C 프로그램이 프로세스를 제어할 수 있도록 시스템 콜 형태로 다양한 인터페이스를 제공한다.
프로세스 ID 구하기
pid_t getpid(void)
: 현 프로세스의 PID를 반환pid_t getppid(void)
: 현 프로세스의 부모(parent) 프로세스의 PID를 반환pid_t
는 sys/types.h
에, 두 함수 자체는 unistd.h
에 포함프로세스 생성과 종료
SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU
등 시그널에 의해 발생SIGCONT
시그널을 받으면 다시 시작된다main
함수가 반환했거나 exit()
이 호출된 경우프로세스의 생성과 종료에 관련된 system-level function은 다음이 있다.
exit()
: int status
를 입력받아 이를 status code로 하여 프로세스를 종료fork()
: 자식(child) 프로세스를 생성fork
는 한 번 호출되고 두 번 반환(부모/자식 프로세스에서 한번씩)하는 특이한 특징이 있다fork
를 사용하는 함수의 경우 프로세스 그래프를 사용하면 실행 순서의 partial ordering을 알아낼 수 있는데, 이를 위상정렬해 나올 수 있는 결과 중 무작위의 하나가 total ordering이 된다.자식 프로세스의 회수
프로세스가 종료(terminate)된 후에도 커널은 이를 시스템에서 바로 지우지 않고 부모 프로세스가 이를 회수(reap)할 때까지 terminated 상태를 유지한다. 이렇게 종료되었지만 회수되지 않은 프로세스를 zombie라고 한다.
waitpid
: 자식 프로세스가 stop이나 terminate되기를 기다릴 때 사용한다.#include <unistd.h>
pid_t waitpid(pid_t pid, int *statusp, int options)
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 참고)errno
에 ECHILD
를 기록wait
: waitpid
의 간략화된 버전으로 wait(&status)
는 waitpid(-1, &status, 0)
과 동일하다.프로세스를 Sleep시키기
#include <unistd.h>
unsigned int sleep(unsigned int secs);
int pause(void);
sleep
: 정상적으로 반환 시 0을 반환하고, signal에 의해 중간에 멈출 시에는 남은 시간을 반환함pause
: 프로세스에 의해 signal이 감지될 때까지 멈춤 상태로 대기프로그램의 로딩과 실행
execve
: 현재의 context에서 새로운 프로그램을 로드하고 실행시킴filename
을 찾지 못하는 등의 에러가 아닌 이상, 반환을 하지 않는 함수argv
와 envp
가 parameter로 같이 전달(각각 매개변수의 list와 환경변수의 list)"PWD=/usr/droh\0"
와 같은 형식)들과 매개변수의 문자열들을 저장하고, 그 위에 이들의 포인터의 null-terminated 배열 *envp[]
와 *argv[]
가 차례대로 나열됨. 그 위에 libc_start_main
의 stack frame이 위치getenv, setenv, unsetenv
등이 있음fork
와 execve
를 사용해 프로그램 실행하기 (pp. 790-792)
Unix 쉘과 웹 서버 등의 프로그램은 fork
와 execve
등의 함수를 매우 자주 사용한다.
sh
이지만 csh, tcsh, ksh, bash
등이 파생fork
로 자식 프로세스가 생성되고, 자식 프로세스에서 요청받은 프로그램을 실행함.&
) 바로 loop의 처음으로 돌아가고, 그렇지 않은 경우는 waitpid
를 사용해 작업이 끝나기까지 기다린 후 반복Linux signal: Exceptional control flow의 고수준, SW 버전
Signal Terminology
보내기(sending a signal): 커널이 목표 프로세스에 signal을 전송하는 과정
목표 프로세스의 context에 있는 특정한 상태를 바꿈으로써 수행
시그널 보내기가 수행되는 상황
kill
함수를 통해 커널에게 목표 프로세스로 signal을 보내줄 것을 요청했을 경우kill
은 이름과는 달리 SIGKILL
만 보낼 수 있는 것이 아니며, 원하는 시그널을 원하는 프로세스로 보낼 수 있다.받기(receiving a signal): 커널에 의해 특정 방식으로 signal에 반응할 것을 명령받을 때, 이를 signal을 받았다고 함
ignore, terminate, catch(signal handler를 사용) 중 하나의 방법으로 반응
pending signal: 보냈지만, 받아지지 않은 signal. 같은 종류의 signal은 하나만 pending 상태에 있을 수 있음
blocked signal: 프로세스가 받지 못하고, pending 상태에서 막혀있는 signal
pending/blocked signal의 집합은 bit vector 형태로 관리
Sending a Signal
getpgrp()
로 알 수 있음setpgid
를 사용해 변경 가능/bin/kill
: 다른 프로세스로 입의의 signal을 보낼 수 있음. /bin/kill -9 15213
: 9번 signal(SIGKILL
)을 PID 15213에 해당하는 프로세스로 보내줄 것을 커널에 요청kill
이라고 하지 않는 이유: 특정 유닉스 쉘에서는 kill이 따로 존재하기 때문ls | sort
와 같이 명령을 입력하면 두 명령이 하나의 job으로 묶임.Ctrl + C
: 포그라운드 job에 SIGINT
를 보냄 -> terminate시킴Ctrl + Z
: 포그라운드 job에 SIGSTP
를 보냄 -> suspendkill
function: 임의의 pid를 선택해 원하는 signal을 보낼 수 있음alarm
function: 스스로에게 SIGALARM
을 보냄(원하는 시간 secs
초 후)Receiving a Signal
커널이 프로세스 를 커널 모드에서 유저 모드로 바꿀 때, unblocked pending signal의 집합을 확인함
pending & ~blocked
를 확인하여, 각 signal마다 default action이 정해져 있음
SIGCONT
를 받을 때까지 프로세스가 정지(stop)됨sighandler_t signal(int signum, sighandler_t handler)
를 사용하여 변경이 가능SIGSTOP
과 SIGKILL
의 behavior는 변경이 불가Blocking & Unblocking Signals
Signal을 block하기 위한 방법
sigprocmask
를 이용해 block이 가능 (p. 801 참고)Writing Signal Handlers
Signal handler를 작성하는 것은 다음의 여러 이유로 어려운 작업:
1. main 프로그램과 concurrent하게 실행되면서 global variable을 공유함
2. signal의 받기가 이루어지는 과정과 timing은 직관에서 벗어남
3. 시스템마다 signal handling semantics가 각기 다름
Safe Signal Handling
write()
만이 사용가능errno
를 저장해두고 복원하기errno
를 설정함errno
를 저장해두고 반환할 때 복원하는 방법 있음volatile
으로 선언하기sig_atomic_t
타입으로 선언volatile sig_atomic_t flag;
로 선언시, 해당 변수로의 읽기나 쓰기가 단일 instruction에 처리되어 중간에 끼어드는 것이 불가능하게 됨을 보장Correct Signal Handling
동일 시그널이 처리중인 경우, 새로운 signal은 발생해도 queue되지 않음
SIGCHLD
의 handler를 install함Portable Signal Handling
시스템마다 signal-handling semantics가 달라서 발생하는 문제가 있음
sigaction
함수를 사용해 signal-handling semantics를 정확하게 정의 가능sigaction
함수를 대신 호출해주는 wrapper function Signal()
을 정의해서 사용할 수 있음Synchronizing Flows to avoid Nasty Concurrency Bugs
concurrent flow들이 동일 메모리 위치를 참조하게 싶은 경우, 올바른 결과를 얻기 위해서는 flow들을 동기화하는 것이 중요한 문제
e.g. p. 813의 프로그램: 부모 프로세스가 자식 프로세스를 만든 후 다시 재개되기도 전에 자식이 terminate된다면 문제가 생김
SIGCHLD
handler를 실행한다면 부모 프로세스는 addjob()
을 하기도 전에 deletejob()
부터 수행하게 됨addjob()
과 deletejob()
의 실행 순서 사이에 race가 있다고 할 수 있으며, addjob()
이 race에서 이겨야만 올바른 결과를 얻게 됨fork()
를 실행하기 전에 SIGCHLD
를 막고(block) child process에서 다시 해제함으로써 오류를 해결함Explicitly Waiting for Signal
sigsuspend(const sigset_t *mask)
: 일시적으로 blocked signal set을 mask
로 바꾼 후, handler를 호출하거나 terminate하는 signal을 받을 때까지 프로세스를 멈춤pause()
-> mask 복원의 과정을 atomic하게 옮겨놓은 것과 같음Nonlocal jump는 C에서 제공하는 유저레벨의 exceptional control flow로, setjmp
와 longjmp
의 두 함수를 사용한다.(`setjmp.h에 위치)
setjmp(jmp_buf env)
: buffer env
에 calling environment(스택 포인터, PC, general-purpose register 등)를 저장함longjmp(jmp_buf env, int retval)
: 매개변수 env
로부터 calling environment를 복원해와서, 가장 최근에 실행된 setjmp
의 위치로 jump함setjmp
는 retval
을 반환함즉, setjmp
는 호출은 한번 되는데 반환은 여러 번(호출 당시에 한번, jump될 때마다 한 번씩) 되는 함수이다.
Nonlocal jump의 일반적인 활용은 다음이 있다: