SystemProgramming-Process
System programming에서 다룬 process 관련 내용들의 정리입니다.
Overview
아래 순서로 Process에 대한 내용을 정리하였고, 공부를 위해 요점만 정리한 것이기에 세부적인 내용은 CSAPP교재나 기타 자료들을 참고하시기 바랍니다.
- Process
- Exceptional Control Flow 1
- Exceptional Control Flow 2
- Signals
Process
Overview
여기선 Process의 개념적인 내용들을 다룬다.
Introduction
정의: 실행 중인 프로그램의 인스턴스
프로세스를 통해서 2가지 추상화가 가능하다.
- Logical control flow: 프로그램마다 각자 cpu를 할당 받은 것처럼 보인다
- Private virtual memory space: 각 프로세스마다 독립적인 메모리 공간을 가지는 것처럼 보인다.
각 프로세스마다 독립적인 자원을 가지고, 강의에서 크게 다루지는 않았으나 고유한 커널 데이터 구조체도 가지고 있다.
State
프로세스는 다음과 같은 상태를 가진다.
- New: 프로세스가 생성되는 중
- Running: CPU에서 명령어를 실행 중
- Waiting: 입출력 등 이벤트가 발생하기를 기다리는 중
- Ready: CPU 할당을 기다리는 중
- Terminated: 프로세스가 실행이 끝남
예를 들어 간단한 hello world 프로그램을 생각해보면,
- New: 프로세스가 생성되는 중
- admitted를 거쳐 Ready 상태로 전이
- 스케줄러에 의해 Running 상태로 전이
- interrupt가 발생하여 Waiting 상태로 전이
- 입출력 이벤트가 발생하여 Ready 상태로 전이
- 다시 스케줄러에 의해 Running 상태로 전이
- exit을 통해 Terminated 상태로 전이
위와 같은 과정을 거친다. 이때 7에서는 terminated 상태로, zombie 상태가 된다. 자세한 것은 뒤에서 다룬다.
Process Control Block (PCB)
운영체제에서 각 프로세스에 대한 정보를 기록하기 위한 구조체이다. 프로그램의 state, PC, CPU 레지스터, 메모리 관리 정보, 입출력 상태 정보 등 다양한 정보를 포함한다.
리눅스에선 task_struct, thread_struct 등이 PCB 역할을 한다.
Process Creation
프로세스는 항상 부모 프로세스에 의해서 생성된다. 따라서 tree 구조를 가지게 된다. 그리고 각 프로세스는 고유한 PID를 통해서 관리된다.
fork()는 부모 프로세스를 복제하여서 자식 프로세스를 만든다. 이때 자식 프로세스는 부모 프로세스의 메모리 공간을 복사하여서 가지게 되고 open files도 복사된다.
자세한 것은 뒤에 다룬다.
Context Switch
## Process부분은 이미 공부한 적이 있어 바로 Context Switch 부분으로 넘어간다. 이후 퀴즈 공부를 위해 다시 추가할수도 있다…
Exceptional Control Flow 1
Overview
여기선 hardware에서 exception이 구동되는 방식과 syscall이 어떻게 구현되는지에 대해서 다룬다.
Introduction
프로세서는 순차적으로 명령어를 읽고 실행할 뿐이고, 이것을 cpu의 control flow 라고 한다.
지금까지는 이 흐름을 바꾸기 위해 branch, jump 등을 사용했다. 근데, 시스템의 상태가 바뀐 경우 대처할 방법이 필요할 수 있다.
이를 위해 exceptional control flow가 존재한다.
exception은 Low-level에서 구현할 수도 있고 High-level에서 구현할 수도 있다. High-level은 kernel에서 구현되므로 여기선 Low-level에서 구현하는 방법에 대해서 다룬다.
Exception vs Interrupt
Exception: 프로그램의 실행 도중에 발생하는 예외 상황을 처리하기 위한 메커니즘이다. 예를 들어, 0으로 나누기, 잘못된 메모리 접근 등이 있다. Synchronous하게 발생하고 CPU에 의해 발생한다.
Interrupt: 외부 장치나 하드웨어에서 발생하는 신호로, CPU의 현재 작업을 중단하고 특정 작업을 처리하도록 요청하는 메커니즘이다. 예를 들어, 키보드 입력, 타이머 인터럽트 등이 있다. A class of exception이며 Asynchronous하게 발생하고 외부 장치에 의해 발생한다.
일단 Hardware에서 Exception의 구동 방식을 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
While(powered on)
{
if (interrupt is pending)
switch core to supervisor mode;
PC = exception_table[exception_number];
else
instruction = memory[PC];
execute(instruction);
PC++;
}
위치럼 기존 모델에 Interrupt_pending_flag를 추가하여서 interrupt가 발생했는지 확인한다. 만약 발생했다면 supervisor mode로 전환하고 exception handler로 점프한다. Exception table에는 interrupt 번호에 해당하는 handler를 가리키는 주소가 들어있다.
해당하는 handler가 실행한 후에는
- re-execute the interrupted instruction
- continue after the interrupted instruction
- abort
의 선택지가 있다.
Exception Types
- Traps
- 의도적인 exception
- e.g. TRAP 80
- next instruction부터 실행
- Faults
- 복구 가능한 오류
- e.g. page fault
- re-execute the instruction or abort
- Aborts
- 심각한 오류
- e.g. hardware failure
- Aborts execution
예시
fileIO에서는 syscall을 사용하는데, syscall은 trap의 예시이다.
User가 찾는 page가 메모리에 없는 경우 page fault가 발생하는데, 이는 fault의 예시이다.
Invalid memory access는 abort의 예시이다.
Asynchronous Exceptions
위 예시 및 exception type은 synchronous exception에 해당한다.
IO interrupt, timer interrupt 등은 asynchronous exception에 해당한다.
Asynchronous exception은 하드웨어에 IRQ라는 신호를 줄 수 있는 선이 있고 키보드 같은 장치가 이 선을 통해서 interrupt를 보낸다.
CPU는 이 신호를 감지하면 현재 실행 중인 instruction을 중단하고 exception handler로 점프한다.
이때 CPU가 intrupt를 허용할지 여부는 Interrupt mask register에 의해 결정된다. 이 레지스터는 bit mask형태로 interrupt를 관리한다.
interrupt를 처리하기로 했으면 interrupt vector routine에서 interrupt의 원인과 그에 맞는 handler에게 제어를 넘긴다.
Syscalls
System call은 software interrupt을 이용하여 구현된다.
/proc/interrupts파일을 통해서 interrupt 관련 stat을 확인할 수 있다.
watch --interval=1 cat /proc/interrupts명령어를 통해서 실시간으로 변화를 확인할 수 있다.
Linux에서 syscall은 아래와 같이 전달된다.
| - | User function | Syscall |
|---|---|---|
| Calling convention (register) | rdi, rsi, rdx, rcx, r8, r9 | rdi, rsi, rdx, r10, r8, r9 |
| Calling convention (stack) | stack 통해 추가 인자 전달 | stack 통해 추가 인자 전달, copy_from, to_user |
| Return value | rax | rax<0 에러, rax≥0 정상 |
Syscall IA32
int 0x80 명령어를 통해서 syscall을 호출한다.
그리고 레지스터는 eax(syscall id), ebx, ecx, edx, esi, edi, ebp를 사용한다.
esp는 커널에서 스택 포인터로 사용되므로 syscall에서 사용하지 않는다.
Exceptional Control Flow 2
Overview
여기선 Signals과 관련된 내용을 다룬다.
Signals
Process에게 어떠한 event가 발생했음을 알리는 방법으로 high-level의 exceptional control flow이다.
Signal의 종류는 1-31이 존재한다.
Signal table은 man page를 찾아보면 되고, SIGKILL(9), SIGSTOP(19)은 catch, block, ignore가 불가능하다는 점을 주의하자.
각 Signal에 대해 kernel은 기본 동작을 정의해놓았다.
- terminate/core dump
- ignore
- stop/continue
그리고 Signal에 대해 프로그램이 어떻게 동작할지도 정의할 수 있다.
- catch: custom handler 정의
- ignore: 무시
- block: unblock 하기 전까지 deliver하지 않음
Signal은 kernel에서 잘못된 메모리로 접근하거나 page fault가 나거나 SIGSEV가 발생하는 등의 상황에서 전달된다. 또는 kill 함수나 ctrl-c, ctrl-z 등의 키보드 입력을 통해서도 전달될 수 있다.
/bin/kill,/bin/killall명령어를 통해서 signal을 보낼 수 있다.
Signal Delivery
Signal이 동작하는 방식은 Signal Delivery → Pending Signals → Signal Reception 순서로 진행된다.
Signal delivery는 kernel이 signal을 PCB를 통해 process에게 전달하는 과정이다.
만약 이 Signal이 deliver되었지만 receive되지 않은 상태라면 pending signal이 된다. 이때 각 Signal은 존재 유무만 표기하기 때문에 여러번 전달되어도 하나로 취급된다. 즉, SIGINT가 10번 전달되어도 handler는 한번만 실행될 것이다.
이후에 kernel이 Signal을 받은 프로세스를 Running하게 되면 Signal state를 확인한 후 정의된 동작을 수행한다.
위사진을 보면 이해가 빠를 것이다.
참고로 Signal handler를 거친 이후에 바로 user code로 복귀하는 것이 아니라 kernel 모드로 복귀한 후에 user code로 복귀한다는 점을 주의하자.
이런 것도 가능하다.
약간의 실제 코드를 보면
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 각 프로세스마다 pending signal mask 와 blocked signal mask가 존재한다.
int peding = 0;
int blocked = 0;
pnb = pending & ~blocked; // deliver 가능한 signal mask
while (pnb != 0) {
int sig = least_significant_bit(pnb); // 가장 낮은 비트의 signal 선택
pnb &= ~(1 << sig); // 선택된 signal 제거
if (handler[sig] == SIG_IGN) {
continue; // 무시
} else if (handler[sig] == SIG_DFL) {
default_action(sig); // 기본 동작 수행
} else {
call_handler(handler[sig]); // custom handler 호출
}
pending &= ~(1 << sig); // 처리된 signal 제거
}
return to p
이런식으로 동작한다.
Signal API
| Action | API | varients |
|---|---|---|
| Send Signal | int kill(pid_t pid, int sig); | killpg, tkill, tkillpg |
| Request Alarm | unsigned int alarm(unsigned int seconds); | - |
| Register Handler | sighandler_t signal(int signum, sighandler_t handler); | Deprecated |
| Register Handler | int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); | - |
| Block Signals | int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); | - |
| Check Status of Blocked Signals | int sigpending(sigset_t *set); | - |
kill
/bin/kill은 프로세스에 signal을 보내는 명령어이다.
e.g. kill -9 1234는 PID가 1234인 프로세스에 SIGKILL(9) signal을 보낸다.
e.g. kill -9 -1234는 process group id가 1234인 프로세스 그룹에 SIGKILL(9) signal을 보낸다.
process group이란?
비슷한 작업을 수행하는 프로세스들의 집합이다.
ps 하면
1 2 3 4 $ ps -o pid,pgrp,comm PID PGRP COMMAND 1234 1234 bash ...위와 같이 pgrp 컬럼이 존재하는데, 이 값이 process group id이다.
모든 프로세스는 자신이 속한 process group id를 가지고 있다.
그리고 자식 프로세스는 부모 프로세스의 process group id를 상속받는다.
setpgid(),getpgid()함수를 통해서 process group id를 설정하거나 가져올 수 있다.
Sending Signals
Keyboard
ctrl-c: SIGINT terminate to foreground group
ctrl-z: SIGTSTP stop(suspend) to foreground group
C-level
1
2
3
#include <signal.h>
int kill(pid_t pid, int sig);
pid>0pid를 가진 프로세스에게 signal 전송=0호출 프로세스와 같은 process group에 속한 모든 프로세스에게 signal 전송=-1권한이 있는 시스템의 모든 프로세스에게 signal 전송< -1-pid를 가진 process group에 속한 모든 프로세스에게 signal 전송
sig>0전송할 signal 번호=0signal을 전송하지 않고 유효한 pid인지 확인 (deliver되는지 test)
- return
<0실패
e.g. kill(getpid(), SIGINT); 현재 프로세스에 SIGINT signal 전송
Registering Handlers
signal()
1
2
3
4
5
#include <signal.h>
typedef void (*sighandler_t)(int);
int signal(int signum, sighandler_t handler);
signum: signal 번호 (SIGKILL, SIGSTOP 제외)handler: signal이 도착했을 때 호출될 함수 포인터SIG_IGN: signal 무시SIG_DFL: 기본 동작 수행
- return
<0실패
Deprecated 되었으므로 sigaction을 사용하는 것을 권장한다.
sigaction()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// example - custom handler with sigaction
// Press ctrl-c multiple times to see nested signals being handled :)
void handler(int sig) {
static int level = 0;
level++;
printf("[nesting level %d] Hohoo, got signal %d\n", level, sig);
sleep(3);
level--;
}
struct sigaction sa;
sa.sa_handler = my_handler; // custom handler
sigemptyset(&sa.sa_mask); // allow nested signals
sa.sa_flags = SA_RESTART | SA_NODEFER; // restart syscalls
if (sigaction(SIGINT, &sa, NULL) < 0) {
perror("Cannot Install SIGINT handler");
exit(1);
}
Blocking Signals, Pending Signals
sigprocmask()
1
2
3
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
howSIG_BLOCK: set에 있는 signal들을 blockSIG_UNBLOCK: set에 있는 signal들의 block을 해제SIG_SETMASK: 현재 blocked signal mask를 set으로 설정
set: signal 집합oldset: null이 아니라면 이전 blocked signal mask를 저장할 변수- return
<0실패
sigpending()
1
2
3
#include <signal.h>
int sigpending(sigset_t *set);
set: pending signal들을 저장할 변수- return
<0실패
sigaddrset()
1
2
3
4
5
6
7
8
9
10
#include <signal.h>
sigset_t mask;
sigemptyset(&mask); // 집합을 "비어있는 상태"로 초기화
sigaddset(&mask, SIGINT); // 집합에 SIGINT 추가
sigaddset(&mask, SIGTERM); // 집합에 SIGTERM 추가
// 이제 mask에는 SIGINT, SIGTERM 두 개가 들어 있음
sigprocmask(SIG_BLOCK, &mask, NULL); // 이 둘을 차단(block)해라
Signal handling issues
- Pending signals are not queued
- 동일한 signal이 여러번 도착해도 하나로 취급됨
- System calls may be interrupted by signals
- signal handler가 실행된 후에 syscall이 다시 시작되지 않을 수 있음
SA_RESTART플래그를 통해서 재시작 가능
- Race conditions between signal handlers and main program
- signal handler와 main program이 동시에 같은 자원에 접근할 수 있음
각각의 예시는 생략하되, 중요하다는 것만 알아두자.
Example1
When reaping child processes, a signal handler for SIGCHLD might be used to call wait() when a child process terminates. But, wait will only reap one child process at a time. If multiple child processes terminate before the signal handler is invoked, only one will be reaped, and the others will remain as zombie processes until another SIGCHLD is delivered.
Furthermore, if the main program is also calling wait() to reap child processes, there could be a deadlock situation.
So…
- Use a loop in the signal handler to call waitpid() with the WNOHANG option until no more terminated child processes are found.
- Use non-blocking calls in the main program to avoid deadlocks.
Example2
Suppose we are writing a shell program that has to read user input from the terminal. If the user presses Ctrl-C while the shell is waiting for input, a SIGINT signal will be delivered to the shell. If the signal handler for SIGINT simply returns, the read() system call may be interrupted and return -1 with errno set to EINTR. This will cause our shell to exit unexpectedly.
So…
- Use the SA_RESTART flag when setting up the signal handler for SIGINT. This will cause interrupted system calls to be automatically restarted.
- Alternatively, check for EINTR in the return value of read() and restart the call manually.
1
2
3
do {
res = read(STDIN_FILENO, cmdline, sizeof(cmdline));
} while (res < 0 && errno == EINTR);
Example3
Race codition
A flaw in a program that the correctness of the program depends on the sequence or timing of uncontrollable events, such as the order in which threads are scheduled or signals are delivered.
Critical section
A section of code that must be executed atomically.
이 부분은 전체 코드를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int superImportantValue = 0;
void handler(int sig) {
// Signal handler that modifies superImportantValue
superImportantValue -= 42;
}
int main()
{
signal(SIGUSR1, handler);
...
superImportantValue += 42; // Critical section
...
}
위 코드를 assembly로 보면
1
2
3
4
5
6
7
8
9
handler:
mov eax, [superImportantValue] ; Load superImportantValue
sub eax, 42 ; Modify it
mov [superImportantValue], eax ; Store it back
main:
mov eax, [superImportantValue] ; Load superImportantValue
add eax, 42 ; Modify it (*)
mov [superImportantValue], eax ; Store it back
위와 같다. 만약 (*) 부분에서 signal이 도착한다면, handler는 add 42가 반영되기 전에 superImportantValue를 읽어서 42를 빼게 된다.
즉, superImportantValue는 결국 0이 되어야 하는데 -42가 되는 문제가 발생한다.
이러한 Critical Section에서는 동작이 atomic하게 이루어져야 한다.
Guideline
- Handler는 간단하게 유지
- Handler 안에는 async-signal-safe 함수만 호출
- man signal-safety
- printf, malloc 등은 unsafe
- errorno는 저장 및 복원
- 공유된 자원을 접근할 때는 모든 signal을 block
- Global 변수는 volatile로 선언
sig_atomic_t타입 사용- Atomic하게 읽고 쓸 수 있는 것이 보장된 정수 타입
flag = 1;이나if (flag)같은 단순한 연산에서만 atomicity 보장- flag++, flag += 1 같은 연산은 atomicity 보장 안됨
Comparing Exceptions and Signals
| Aspect | Exceptions | Signals |
|---|---|---|
| Level | Low-level (hardware) | High-level (software) |
| Source | Executing instruction, hardware events | Kernel, other processes, user actions |
| delivery | Immediate | Two-steps, 1) immediately to the kernel 2) later received when scheduled |
| Handling | Processor, vector table -> handler | OS kernel, per-process signal handlers |

