-
Exceptional Control Flow 2 - Signal대학/시스템소프트웨어 2022. 10. 22. 22:17
- Signal
시그널은 OS가 어떤 이벤트가 일어났음을 프로세스에게 알리는 것이다.
다음의 예시 상황을 가정해보자.
int *ptr; ptr = 0x00000100;
이렇게 합법적으로 할당받은 메모리 공간이 아닌 곳에 접근할 경우, Segmentation violation exception이 발생한다.
이런 exception 들은 CPU에서 kernel에 전달되고, kernel(OS)는 이 코드를 실행시킨 프로세스에게 signal을 보내게 된다.
시그널을 받은 프로세스가 취할 수 있는 행동은 다음 세가지 행동 중 하나이다.
- 시그널을 무시한다.
- 프로세스를 종료한다.
- 개발자가 정의한 시그널 핸들러로 시그널을 처리한다.
시그널은 들어왔다고 바로 처리되는게 아니다.
프로세스가 ready 상태에서 시그널이 도착하면, 시그널은 도착하고 프로세스가 running 상태가 되기 전까지,
즉, 처리되기 전까지 pending 상태를 갖는다. 이후 프로세스가 running 상태가 되면 signal pending list를 검사하고
리스트에 들어있는 시그널을 처리하게 된다.
- kill
kill 함수로 특정 프로세스에게 특정 시그널을 보낼 수 있다.
pid_t pid[N]; int child_status; for (int i=0; i<N; i++) if ((pid[i] = fork()) == 0) while (1) // child infinite loop for (int i=0l i<N; i++) kill(pid[i], SIGINT); // 자식 프로세스 비정상 종료 처리 for (int i=0; i<N; i++) { pid_t wpid = wait(&child_status); if (WIFEXITED(child_status)) {...} // 1 else {...} // 2 }
이 코드의 경우 자식 프로세스가 exit()에 의해 종료되지 않고, 비정상 종료 되었으므로,
1번 라인은 무시하고 2번 라인이 실행되게 된다.
- Signal handler
각 시그널들은 기본적으로 수행하는 동작이 정해져있다.
하지만, 시그널 핸들러를 등록하면 특정 시그널이 도착했을 때, 개발자가 원하는 기능을 수행하도록 할 수 있다.
void mySignalHandler(int signo) { printf("Signal catched!"); } int main() { siganl(SIGINT, mySignalHandler); while (1) {} }
signal() 함수로 특정 시그널을 받았을 때, 시그널 핸들러 함수를 실행하도록 할 수 있다.
이 프로세스가 무한 루프 중에, ctrl+c 를 입력하면 SIGINT 시그널이 발생하는데,
그럼 mySignalHandler가 해당 시그널을 캐치하게 된다.
기존 함수를 호출할 때의 함수와 시그널 핸들러 사이의 공통점은 함수 형태라는 것과, 동일한 프로세스 내에서 실행된다는 점이다.
차이점은 기존 함수는 함수가 호출하고, 실행 순서를 예측할 수 있다는 것이다.
하지만, 시그널 핸들러는 kernel이 호출하며 실행 순서, 시점을 예측할 수 없다는 것이다.
즉, 시그널 핸들러는 같은 프로세스 내에서 처리되는 동작이므로, 시그널 핸들러 내에서 exit()을 호출, 즉 프로세스를 종료시킬 경우
메인 함수를 포함한 프로세스 전체가 종료되게 된다.
추가로, alarm()이란 함수도 있는데, alarm(3); 이라 설정하면, 해당 프로세스가 잠자든, 정지되어 있든 상관없이 3초 뒤에
SIGALRM 시그널이 본인 프로세스로 주어진다. (시그널 핸들러가 등록되어 있는 경우 그 시그널 핸들러가 동작하게 된다)
- Race condition
char *pGlobMem; ubt globCnt = 0; void sigHandler(int signo) { globCnt++; for (int i=0; i<1000; i++) pGlobMem[i] = globCnt; for (int i=0; i<1000; i++) printf("%d\n", pGlobMem[i]); } int main() { pGlobMem = malloc(1000); memset(pGlobMem, 0, 1000); signal(SIGINT, sigHandler) }
위 함수의 경우 ctrl+c를 누를 때 마다 숫자가 하나 증가하고 그 숫자를 1000 번 출력하는 기능을 수행한다.
만약, 숫자가 증가되는 도중에 또 한 번 ctrl+c를 누른다면?
위 그림처럼 레이스 컨디션이 발생해 2가 2000번 출력되는 결과가 나올 것이다.
이런 레이스 컨디션을 방지하기 위해서 masking(blocking)을 사용한다.
void sigHandler(int signo) { globCnt++; sigmask(SIGINT); for (int i=0; i<1000; i++) pGlobMem[i] = globCnt; for (int i=0; i<1000; i++) printf("%d\n", pGlobMem[i]); sigunmask(SIGINT); }
for문 밖에 SIGINT 시그널을 마스킹, 언마스킹을 하면 해당 for문이 실행되는 동안은 SIGINT 시그널을 블록하게 된다.
여기서 마스크(블록)한다는 것은 시그널을 무시하는 게 아닌, 언마스크 상태까지 pending하겠다는 의미이다.
즉, 위 코드에서 for문 실행도중 ctrl+c를 통해 SIGINT 시그널을 보낸다면, 1111 출력이 완료될 때 까지 pending 했다가
출력이 완료되는 시점 SIGINT가 언마스크가 된 후 시그널을 받아 2222 가 출력되는 것이다.
(참고로 Ignore와는 다른게, Ignore은 시그널을 pending 시키는게 아닌 무시해버린다)
- Signal masking
이 부분은 코드와 그에대한 설명으로 넘어가겠다.
(시그널관련 코드에 대한 자세한 설명은 대학/자료구조실습 카테고리에 설명해 두었음)
sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, SIGINT); sigaddset(&sigset, SIGQUIT); sigprocmask(SIG_BLOCK, &sigset, NULL); ...(중요한 코드)... sigprocmask(SIG_UNBLOCK, &sigset, NULL);
위 코드를 통해 SIGINT, SIGQUIT 시그널을 한 번에 블록 처리했다.
블록 하고 나서 중요한 코드를 실행시켜 레이스 컨디션 등 방해받지 않도록 한다.
코드 실핼 수 다시 두 시그널을 언블록 하여 원래 상태로 돌려놓는다.
struct sigaction action; sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, SIGINT); sigaddset(&sigset, SIGQUIT); action.sa_handler = sigHandler; action.sa_mask = sigset; action.sa_flags = 0; sigaction(SIGUSR1, &action, NULL);
위 코드를 통해 SIGUSR1 시그널이 호출되면 sigHandler가 동작하는데,
sigHandler 시그널 핸들러가 동작하는 동안에는 SIGINT, SIGQUIT 시그널이 블록 처리된다.
- 프로세스 계층구조 (process hierarchy)
Unix 시스템이 부팅될 때의 과정을 살펴보자.
0번 프로세스는 커널 프로세스이고 , 이 커널 프로세스가 init 프로세스를 fork하여 만든다.
1번 init 프로세스는 /sbin/init의 실행파일을 실행시키는데, 이 프로세스가 user process의 조상이 된다.
init 프로세스는 자식 프로세스를 fork하고 getty 프로그램을 실행시킨다.
그리고 이 getty 프로세스는 login 프로그램을 실행시킨다.
로그인이 성공적으로 수행된다면 login 프로세스는 shell 프로그램을 실행시킨다.
'대학 > 시스템소프트웨어' 카테고리의 다른 글
Threads (0) 2022.12.11 Inter-Process Communication (2) 2022.10.23 Timer (0) 2022.10.22 Exceptional Control Flow 1 - Process (0) 2022.10.22 System-Level I/O (0) 2022.10.22