Simple Shell Example
New problems with Simple shell Example
2024.09.28 - [학교 수업/시스템 프로그래밍] - [시스템 프로그래밍] Race Condition
2024.09.24 - [학교 수업/시스템 프로그래밍] - [시스템 프로그래밍] Signal Programming
에서 봤었던, Simple shell의 문제점들을 간단히 정리하면 다음과 같습니다.
- Race condition
- main(): addjob()
- handler: deletejob()
- Blocking in waitpid
- while((pid = waitpid(-1, NULL, 0)) > 0)
main과 handler가 공통의 자료구조를 공유하기 때문에 발생하는 Race condition과 waitpid를 하는데 아직 실행중인 자식 프로세스로 인해 wait이 너무 오래 기다리게 한다는 점이 있었습니다.
Signal Handler as concurrent flows
A signal handler is a separate logical flow (not process) that runs concurrently with the main program
시그널 핸들러는 하나의 분리된, 독립된 logical flow입니다. 하지만 프로세스는 아닙니다. 이는 main 프로그램과 동시에 동작합니다.
위 그림을 보시면 handler와 main은 프로세스 A이고, child랑은 아에 다른 프로세스입니다. 하지만 handler는 main과 독립적인 logical flow를 가질 수 있습니다.
이 경우 handler와 main이 공유되는 자료를 갖는다면 Race condition이 발생할 가능성이 커집니다.
Nested Signal Handlers
Handlers can be interrupted by other handlers
핸들러는 다른 핸들러들에 의해 인터럽트 당할 수 있습니다.
Blocking and Unblocking Signals
Implicit blocking mechanism:
- Kernel blocks any pending signals of type currently being handled(e.g., A SIGINT handler can't be interrupted by another SIGINT)
내재적인 blocking 매커니즘으로는 커널이 어떤 시그널에 대해서 처리를 하고 있는 동안에는 그 처리하는 시그널에 대해서는 blocking을 합니다. 이는 signal이 queuing되지 않는다는 점 때문에 발생하는 현상입니다. 예를 들어 SIGINT가 들어와서 핸들러가 이에 맞는 동작을 수행하고 있는 동안은 다른 SIGINT에 의해 인터럽트되지 않습니다.
Explicit blocking and unblocking mechanism:
- sigprocmask function을 통해 임의로 blocking과 unblocking을 수행할 수 있습니다.
- Supporting functions:
- sigemptyset: create empty set
- sigfillset: add every signal number to set, 모두 blocking하는 bit vector를 설정
- sigaddset: add signal number to set, block설정
- sigdelset: delete signal number from set, block해제
Temporarily Blocking Signals
/* codes */
sigset_t mask, prev_mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
/* Block SIGINT and save previous blocked set */
sigprocmask(SIG_BLOCK, &mask, &prev_mask); // SIG_BLOCK은 signal을 block하라는 명령어
/* code region that will not be interrupted by SIGINT */
/* Restore previous blocked set, unblocking SIGINT */
sigprocmask(SIG_SETMASK, &prev_mask, NULL); // 원복
* 이때 sigset_t는 signal을 관리하는 bit-vector를 나타내는 자료형입니다.
Synchronizing flows to avoid races
void handler(int sig)
{
int olderrno = errno;
sigset_t mask_all, prev_all;
pid_t pid;
sigfillset(&mask_all);
while((pid = waitpid(-1, NULL, 0)) > 0) { /* Reap child */
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if(errno != ECHILD)
sio_error("waitpid error");
errno = olderrno;
}
int main(int argc, char **argv)
{
int pid;
sigset_t mask_all, prev_all;
sigfillset(&mask_all);
signal(SIGCHLD, handler);
initjobs(); /* Initialize the job list */
while(1){
if ((pid = fork()) == 0){ /* Child */
execve("/bin/date", argv, NULL(;
}
sigprocmask(SIG_BLOCK, &mask_all, &prev_all); /* Parent */
addjob(pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
exit(0);
}
Signal Handlers as Concurrent Flows
Simple shell with a subtle synchronization error because it asumes parent runs before child
simple shell은 또 미묘한, 작은 싱크를 맞춰야하는 에러가 있습니다. 이는 main에서 addjob을 하기 전에 context switch가 발생해서 child로 flow가 넘어가고, child가 종료되었을 때, SIGCHLD에 의해 handler가 불려, deletejob을 수행하고, main으로 넘어오면 addjob을 하는,
정리하면, addjob과 deletejob의 순서가 바뀌는 상황이 발생할 수 있다는것이 문제입니다.
이를 해결하기 위해서는 child process가 만들어지고, 이를 addjob할때까지는 main에서 SIGCHLD를 block해야 합니다. 그래야만 addjob → deletejob의 순서가 지켜집니다. 따라서 다음과 같이 수정해야합니다.
int main(int argc, char **argv)
{
int pid;
sigset_t mask_all, mask_one, prev_one;
sigfillset(&mask_all);
sigemptyset(&mask_one);
sigaddset(&mask_one, SIGCHLD);
signal(SIGCHLD, handler);
initjobs(); /* Initialize the job list */
while(1) {
sigprocmask(SIG_BLOCK, &mask_one, &prev_one); /* Block SIGCHLD */
if ((pid = fork()) == 0) /* Child */
sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD in child process */
execve("/bin/date", argv, NULL);
}
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(pid);
sigprocmask(SIG_SETMASK< &prev_one, NULL);
}
exit(0);
}
Safe Signal Handling
Handlers are tricky because they are concurrent with main program and share the same global data strctures which means that shared data structures can become corrupted.
핸들러는 굉장히 다루기 까다로운데, main 프로그램과 같은 전역 변수를 공유하면, 공유된 자료 구조는 충돌을 일으킬 수 있기 때문입니다.
Guidelines for Writing Safe Handlers
- G0: Keep your handlers as simple as possible(e.g., set a global flag and return) → handler가 명령을 수행하는 동안 모든 process가 멈추기 때문에 간단하게 작성해야합니다.
- G1: Call only async-signal-safe functions in your handlers which means that printf, sprintf, malloc and exit are not safe in handlers. → 핸들러에서는 async-signal-safe한 함수만을 사용해야합니다. 즉 printf나 sprintf, malloc, exit은 안전하지 않습니다.
- G2: Save and restore errno on entry and exit so that other handlers don't overwrite your value of errno → 핸들러에서 errno를 저장하고 원복한 후 exit해야합니다. 다른 핸들러들이 당신의 errno를 덮어쓰기 할 수 있기 때문입니다.
- G3: Protect accesses to shared data structures by temporarily blocking all signals to prevent possible corruption → 공유되는 자료구조에 접근할 때는 충돌을 막기 위해서 임시적으로 모든 시그널을 block해야합니다.
- G4: Declare global variables as volatile to prevent compiler from storing them in a register → 전역 변수는 컴파일러가 전역 변수를 레지스터에 저장하는 것을 막기 위해 volatile선언을 해줘야 합니다. 그 이유에 대해서는 아래에서 설명하겠습니다.
- G5: Declare global int flags as volatile sig_atomic_t. sig_atomic_t is guaranteed to be read/write in one instruction(e.g., flag=1, not flag++) → int타입 flag는 volatile sig_atomic_t로 선언해야합니다. 그래야 한 번의 명령으로 읽기/쓰기가 보장됩니다. 그러나 flag=1과 같은 명령은 한 번의 명령을 보장하지만, flag++과 같은 명령은 한 번의 명령을 보장하지 않는데, read → add → write의 세 번의 명령이 필요하기 때문입니다. 그렇다면 한 번의 명령으로 수행되지 않는 Assemble명령어 mov가 있을까요? 있습니다. 만약 data type이 long long이라면 bus의 크기때문에 한 번의 명령으로는 read/write이 수행될 수 없습니다. 절반씩 나눠서 옮기는 도중, signal이 들어오면 반드시 error가 발생합니다. 이를 방지하기 위해, atomicity를 보장하기 위해 다음과 같이 선언해주는 것입니다.
* volatile: memory에 data를 저장하고, memory상에서 접근하게 하는 것입니다. cpu는 최적화를 위해 memory에 있는 데이터를 register로 가져와 연산을 수행합니다. 하지만 global 변수의 경우 만약 handler에 의해 memory에 있는 값이 변경된다면 다시 main으로 넘어왔을 때, cpu는 register에 있는 값을 이용해서만 연산을 수행하므로, memory와 register가 sync가 맞지 않을 수 있습니다. 따라서 이를 방지하기 위해, memory에 있는 변수를 register로 옮기는 최적화를 막기 위해 volatile을 선언해줍니다.
G1: Async-signal-safety
Function is async-signal-safe if either reentrant (e.g., all variable stored on stack frame) or non-interruptible by signals. POSIX guarantees 117 functions to be async-signal-safe.
만약 모든 변수들이 스택에 저장되어있거나, 시그널에 의해 방해받지 않는 그런 함수들을 말합니다. POSIX는 117개의 함수를 async-signal-safe라고 보장하고 있습니다.
- Popular functions on the list
- _exit(), write(), wait(), waitpid(), sleep(), kill()
- Popular functions that are not on the list
- printf(), sprintf(), malloc(), exit()
- Unfortunate fact: write is the only async-signal-safe output function
G2: errno
앞서 봤던 handler 함수에서 olderrno라고 저장했던 방식입니다. 이를 통해 main에서의 errno를 유지할 수 있습니다.
void handler(int sig)
{
int olderrno = errno; /* store errno */
sigset_t mask_all, prev_all;
pid_t pid;
sigfillset(&mask_all);
while((pid = waitpid(-1, NULL, 0)) > 0) { /* Reap child */
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if(errno != ECHILD)
sio_error("waitpid error");
errno = olderrno;
}
G4 & G5: volatile sig_atomic_t
Handler for program explicitly waiting for SIGCHLD to arrive
volatile sig_atomic_t pid;
void sigchld_handler(int s)
{
int olderrno = errno;
/* Main is waiting for nonzero pid */
pid = waitpid(-1, NULL, 0);
errno = olderrno;
}
int main(int argc, char **argv)
{
sigset_t mask, prev;
signal(SIGCHLD, sigchld_handler);
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
while(1){
sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
if(fork() == 0) /* child */
exit(0);
/* parent */
pid = 0;
sigprocmask(SIG_SETMASK, &prev, NULL);
while (!pid); /* Wait for SIGCHLD to be received (wasteful!) */
printf(".");
}
}
fork()를 하고 pid=0으로 설정하는 사이 SIGCHLD signal이 들어오는 것을 방지합니다.
Explicitly Waiting for Signals
Program is correct, but very wasteful:
while(!pid);
Other options:
while(!pid) /* Too slow! */
sleep(1);
while(!pid) /* Race */
pause();
pid를 확인하고 pause를 걸기 전에 SIGCHLD가 들어오면 pid != 0이 되고, pause를 걸면, pause는 풀리지 않는다.
Solution: sigsuspend(&mask)
sigprocmask(SIG_BLOCK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);
과 똑같은 기능을 하는 함수입니다. 이를 통해 해결할 수 있습니다.
volatile sig_atomic_t pid;
void sigchld_handler(int s)
{
int olderrno = errno;
/* Main is waiting for nonzero pid */
pid = waitpid(-1, NULL, 0);
errno = olderrno;
}
int main(int argc, char **argv)
{
sigset_t mask, prev;
signal(SIGCHLD, sigchld_handler);
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
while(1){
sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
if(fork() == 0) /* child */
exit(0);
/* parent */
pid = 0;
while (!pid); /* SIGCHLD unblock! */
sigsuspend(&prev);
sigprocmask(SIG_SETMASK, &prev, NULL);
printf(".");
}
}
Blocking in waitpid problem..
SIGCHLD는 queuing될 수 없기 때문에, 종료되어있는 모든 children들을 reaping하기 위해
while((pid = waitpid(-1, NULL, 0)) > 0)
과 같이 해주었는데, 만약 한 자식 프로세스가 오랫동안 실행중이라면 계속 기다릴 수 있다는 문제가 있습니다.
따라서 waitpid의 option값에 WNOHANG을 넣어주어서 해결할 수 있습니다.
int main(void){
/* codes */
pid = fork();
if(pid > 0){ /* parent */
printf("This is parent process: wait for %d\n", pid);
sleep(3);
waitpid(pid, &status, WNOHANG);
printf("parent process return\n");
return 0;
}
else if(pid == 0){ /* child */
printf("This is child process(infinite loop)\n");
while(1);
}
return 0;
}
이를 통해 이미 종료되어있는 자식 프로세스들에만 reaping을 수행하고 아직 끝나지 않은 자식 프로세스들에 대해서는 reaping을 수행하지 않고 넘어갑니다. Reaping의 책임은 init process로 넘어갑니다.
- Return value of waitpid()
- On success, returns the pid of the child whose state has changed
- If WNOHANG was specified, but children have not yet change state, then 0 is returned
- On error, -1 is returned
'[학교 수업] > [학교 수업] 시스템 프로그래밍' 카테고리의 다른 글
[시스템 프로그래밍] Real-Time Signal (5) | 2024.10.08 |
---|---|
[시스템 프로그래밍] Assembly Language (1) | 2024.10.06 |
[시스템 프로그래밍] Race Condition (0) | 2024.09.28 |
[시스템 프로그래밍] Signal Programming (0) | 2024.09.24 |
[시스템 프로그래밍] Signals (0) | 2024.09.20 |