1. Race Condition - Simple 예제
시그널핸들러와 이 시그널을 호출하는 main() 함수에 둘 다 $\verb|*i = *i + value;|$ 코드가 있다고 생각하자. 이 코드를 어셈블리 코드로 나타내면 아래와 같다. (ecx, edx는 각 레지스터 이름에 해당하는 변수 이름을 임의로 지정하였다.)
my_sighanler:
ecx = *i; // movq (%rax), %ecx #1
ecx = ecx + value; // addl %eax, %ecx #2
*i = ecx; // movq %ecx, (%rax) #3
main:
edx = *i; // movq (%rax), %edx #4
edx = edx + value; // addl %eax, %edx #5
*i = edx; // movq %edx, (%rax) #6
main() 함수가 4, 5까지 실행되다가 시그널이 도착했다고 가정해보자. 4랑 5를 지나면 $\verb|value + *i|$ 값이 edx에 저장된 상태가 된다. 이 때 시그널이 도착하고 1부터 3까지의 과정을 거치면 %rax에는 $\verb|*i + value|$ 값이 저장된다. 시그널 핸들러가 끝나면 다시 main() 으로 돌아오는데, 이때 6번 과정을 통해 다시 %rax 공간에 $\verb|*i + value|$ 값이 저장된다. %edx 레지스터의 값이 시그널 핸들러에서 인식을 못해서 value가 2번 더해지지 않는 현상이 발생하는데, 이것이 Race Condition 이다.
2. Race Condition - job list 예제
int main(int argc, char** argv) {
int pid;
signal(SIGCHLD, handler);
initjobs(); /* Initialize the job list */
while (1) {
if ((pid = fork()) == 0) { /* Child */
execve("/bin/date", argv, NULL);
}
addjob(pid); /* Add child to job list (Linked List) */
}
exit(0);
}
void handler(int sig) {
int olderrno = errno;
pid_t pid;
while ((pid = waitpid(-1, NULL, 0)) > 0) { /* Reap child */
deletejob(pid); /* Delete the child from the job list */
}
if (errno != ECHILD)
sio_error("waitpid error");
errno = olderrno;
}
시그널은 queueing 되지 않아 여러번 발생할 수 있으므로 while 반복문으로 첫 번째로 Reaping 해준 자식 프로세스 외에도 종료된 좀비 프로세스가 있는지 확인하고 Reaping 해준다. Reaping 할 때는 $\verb|deletejob(pid)|$를 통해 job list에 있던 프로세스를 삭제한다. 근데 이 함수가 실행되는 중에 main() 에서 $\verb|addjob(pid)|$를 통해 새로운 pid를 넣을려고 했을수도 있다. (Race Condition 발생) 그러면 포인터 충돌이 일어나서 연결 리스트의 구조가 깨지게 되고, 이후에도 계속 포인터가 꼬이는 오류가 일어날 수 있다.
- Critical Section: $\verb|*i, joblist|$ 처럼 자원이 공유되는 영역을 의미한다. 이 영역은 반드시 동시에 실행되서는 안된다.
- Mutual Exclusion: Critical Section 에 하나의 스레드만 실행되도록 보장하는 것을 의미한다. 예를 들어 job list 예제에서 $\verb|addjob, deletejob|$이 한쪽이 실행되면 리턴되기 전까지 다른 한쪽은 실행하지 않도록 하는 것이다.
- Atomicity: Mutual Exclusion을 보장하면 해당 Critical Section은 Atomic 하게 실행된다고 이야기한다.
3. Blocking Signals
1. 자동으로 blocking하는 메커니즘: 어떤 시그널이 실행되는 중에 동일한 시그널이 계속 올 경우, Kernel이 내재적으로 시그널 핸들러를 실행하지 않는다.
2. 명시적으로 blocking과 unblocking하는 메커니즘: $\verb|sigprocmask / sigemptyset, sigfillset, sigaddset, sigdelset|$
$\verb|sigprocmask|$를 통해 Blocked 비트 벡터의 특정 비트들을 1로 마스킹하여 어떤 시그널에 대해서 내가 풀어주기 전까지 Block을 할 수 있다. 나머지도 비슷한 기능을 하는 함수들이다.
아래 코드는 $\verb|SIGINT|$가 발생하지 않도록 일시적으로 Blocking하는 예제이다.
sigset_t mask, prev_mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
/* Block SIGINT and save previous blocked set */
sigprocmask(SIG_BLOCK, &mask, &prev_mask);
/* ... Code region that will not be interrupted by SIGINT */
/* Restore previous blocked set, unblocking SIGINT */
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
- $\verb|sigset_t|$ 는 Blocked 비트 벡터 표현을 위한 자료형이고, $\verb|prev_mask|$는 원래 값을 저장해두는 용도이다.
- $\verb|mask|$를 모두 0으로 채운 뒤, $\verb|mask|$에 $\verb|SIGINT|$에 해당하는 비트들이 1로 되도록 마스킹한다. 즉, $\verb|mask|$는 현재 $\verb|SIGINT|$가 발생하지 않도록 하는 도구이다.
- 이제 $\verb|SIGINT|$가 발생하지 않도록 $\verb|sigprocmask|$를 이용하여 실제로 Blocking을 한다. 첫번째 인자로 BLOCK한다는 것을 명시하고, 두번째 인자의 $\verb|mask|$에 해당하는 시그널을 Blocking 한다. 세번째 인자는 Block 시키기 직전의 상태를 기록해두는 용도로 보낸다. 그럼 9번째 줄처럼 이후 코드에서는 $\verb|SIGINT|$가 Blocking 된다.
- 다시 $\verb|SIGINT|$를 활성화시켜주기 위해 $\verb|sigprocmask|$를 이용하여 unblocking을 한다. 이때 $\verb|mask|$를 이용하여 환원하는 것이 아니라, $\verb|prev_mask|$를 이용해서 $\verb|sigprocmask|$를 호출하기 직전 기존 상태로 환원해야한다. (기존에 이미 Block된 상태에서 또 Block된 상태였을 경우, 바로 unblock해서는 안되기 때문. 호출되기 전 기존 상태로 되돌아간다는 개념이 적절하다.)
4. Race Condition 해결하기 - job list 예제
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 process */
sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
execve("/bin/date", argv, NULL);
}
sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Parent process */
addjob(pid); /* Add the child to the job list */
sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
}
exit(0);
}
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); /* Delete the child from the job list */
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if (errno != ECHILD)
sio_error("waitpid error");
errno = olderrno;
}
- 먼저 $\verb|main()|$ 함수에서는 모든 시그널을 Blocking하기 위해 $\verb|sigfillset|$을 사용한다. 따라서 $\verb|addjob(pid)|$ 함수가 실행되는 동안 아무런 시그널의 방해를 받지 않는 것이 보장된다.
- 시그널이 실행되는 동안에도 또 새로운 시그널이 호출되어 방해될 수 있으므로 시그널 내에도 $\verb|deletejob(pid)|$이 일어나는 동안 모든 signal들을 Blocking 해준다. 따라서 main()과 시그널 핸들러가 동시에 Critical Section에 접근하는 것을 막을 수 있다.
- $\verb|mask_one|$은 \verb|SIGCHLD| 만을 Blocking 하기 위해서 만든 것이다. $\verb|mask_one|$을 이용하여 $\verb|SIGCHLD|$ 시그널을 막은채로 부모가 $\verb|fork()|$를 실행한다. 따라서 $\verb|addjob()|$ 을 실행하기 전까지는 절대 $\verb|deletejob()|$을 실행할 수 없게 된다.
- $\verb|fork()|$로 생성된 자식 프로세스의 경우 바로 $\verb|SIGCHLD|$ 시그널을 unblock 해준다. 자식 프로세스에서는 $\verb|SIGCHLD|$ 시그널을 받아도 아무 상관이 없기 때문에 해제해도 상관 없다.
- $\verb|main()|$에서 $\verb|addjob()|$이 실행됐으므로 $\verb|SIGCHLD|$ 핸들러가 발생해도 상관없으므로 unblock을 해준다.
'학부 수업 정리 > 시스템프로그래밍 (21-2)' 카테고리의 다른 글
[시스템] 7. Threads (0) | 2021.12.28 |
---|---|
[시스템] 6. IPC (Inter Process Communication) (0) | 2021.12.28 |
[시스템] 4. 어셈블리 기초 (0) | 2021.12.28 |
[시스템] 3. Signal 프로그래밍 (0) | 2021.12.28 |
[시스템] 2. 멀티 프로세스 프로그래밍 (0) | 2021.12.28 |