프로세스가 종료되는 세 가지 이유는 아래와 같다.
- main 함수에서 return 할 때
- exit 함수를 호출할 때
- 다른 프로세스(커널모드, 부모)의 시그널에 의해 종료될 때
exit sys call의 매개변수는 status라는 정수 타입이다. 운영체제에서 종료 상태값이라 한다. [0, 255]의 범위를 가지고 있고 0이면 성공 나머지는 실패이다. main에서 return 하는 것과 같은 개념이라고 생각하면 된다.
shell 프로그래밍과 연동해서 더 큰 프로그래밍을 할 수 있도록 exit status가 존재한다. void 타입으로 main 함수를 만드는 것은 컴파일은 되지만 표준은 아니다. 그래서 아래와 같은 경고 문구가 나올 수 있다.
출력된 13은 printf의 반환값인 string의 size이다.
일반 프로그램의 exit status는 부모 프로세스에게 전달해주는 것이다. 그런데 두 프로세스는 분리되어 있어
일반적인 함수의 call return 관계가 아니다. stack frame에서 움직이는 값들이 아니다. 그렇기 때문에 커널이 개입하고 많은 이슈를 만든다.
위의 인자로 function pointer를 인자로 받는다. 종료할 때 호출하는 함수이다. clean up 함수들이 호출된다. 32개까지 등록할 수 있고 exit 함수가 호출되었을 때 자동적으로 호출된다.
exit()가 호출되었을 때, atexit() 함수가 호출되기 때문에 역순으로 호출되는 것을 확인할 수 있다.
만약 _exit(0)일 경우엔 아무것도 호출되지 않는다.
Zombie 용어는 CS에서 디도스 등과 같은 공격에 의해 감염된 PC나 스마트폰을 이야기한다.
시스템에선 Zombie state 또는 이 state를 가진 프로세스를 Zombie process라고 칭한다.
exit() syscall로 인해 프로세스가 종료되면 어떠한 일이 일어나는지 알아보자.
어떤 프로세스가 종료하면 프로세스의 resource가 삭제된다. 그런데 우리가 이전에 배운 프로세스의 메타데이터인 PCB는 삭제가 되지 않는다. PCB를 process descriptor라고 보겠다. 이 프로세스 디스크립터에 종료 상태값을 저장해놓는데, 부모 프로세스가 자식 프로세스의 종료 상태값을 알아야하기 때문에 종료 상태값을 가져갈 때 까지 남겨놓는 것이다. 이 행위를 Reaping이라 칭하고 이 PCB가 남아있는 상태를 Zombie state라고 칭한다. 이 가져가는 함수를 wait(), waitpid() sys call을 통해 처리한다.
wait(), waitpid()가 명시적으로 호출되지 않으면 부모 프로세스가 종료될 때 까지 process descriptor는 남아있다.
내부적으로 커널이 do_exit()라는 커널 함수를 통해 프로세스를 zombie state로 바꾼다.
참고로 리눅스는 이 process descriptor는 Unix는 array, Linux는 linked list 자료구조로 저장해 놓는다.
자식 프로세스가 exit(0) 이후 process descriptor가 남아있는 zombie state가 발생하고 부모 프로세스에서 exit(0)가 호출되기 전까지 유지된다.
background job으로 실행시킨다음 ps -a 명령어를 입력하면 <defunct>를 확인할 수 있다. 이는 zombie process를 확인할 수 있다. -u 옵션을 준다면 그럼 zombie state도 확인할 수 있다.
좀비 프로세스가 계속 쌓여간다면 memory leak이 발생해 대형 소프트웨어에선 큰 이슈이다. Reaping 처리를 잘 해야한다.
이번엔 자식 프로세스보다 부모 프로세스가 먼저 종료될 때를 알아볼 것이다.
코드를 보면 시간상 부모프로세스가 더 먼저 종료된다는 것을 알 수 있다. 이 때 자식 프로세스는 부모를 잃었기 때문에 고아 프로세스라고 한다. 여전히 고아 프로세스의 exit state를 받아야하기 때문에 init process를 고아 프로세스의 부모 프로세스로 재지정한다. 부모 프로세스의 pid가 73191에서 2635로 변경된 것을 확인할 수 있다.
linux system에서 init이 systemd로 이름이 바뀌었다.
위의 그림에서 A가 프로세스의 시작으로 보자.(부모프로세스)
B가 exit하는 경우를 알아보자
- cleanup handler가 호출된다
- zomebie state를 set하고 exit status를 PCB에 저장한다.
- system resources 할당을 해제한다.
- parent에게 signal을 보낸다.
- 고아 프로세스에게 새로운 부모를 준다.
부모가 자식 프로세스의 exit status를 가져가는 reaping을 수행하는 wait() sys call를 알아보자.
- 매개변수: 상태값을 인자로 받는다. 이 인자는 사실 출력값이다. 자식 프로세스가 종료되었을 때의 exit status이다.
- 리턴값: 자식 프로세스의 pid이다. 자식 프로세스가 없을 시 -1을 리턴한다.
wait() syscall이 호출되면 부모 프로세스는 대기 상태가 된다. 그럼 프로세스 스케줄러의 대상에서 빠진다.
인자로 들어간 status 포인터 변수는 exit status를 전달받는다. macro 함수가 인자값을 통해 정상적인 종료인지 아닌지를 확인할 수 있다.(WIFEXITED: 프로세스가 정삭적으로 종료되었다면 1 반환)
wait 밑의 if else 구문은 정상적 종료, 비정상적 종료를 따진다.
이는 자식의 exit에 맞춰 동기화 된다. wait() 함수에서 자식 프로세스가 종료되어야 부모 프로세스가 다시 실행되거나 그대로 대기한다.
wait
- wait는 caller를 block하여 exit status를 받을 때까지 기다리게 한다.
- wait는 많은 자식이 있으면 처음으로 종료되는 자식의 exit status를 받는다.
waitpid
waitpid는 block을 하거나 하지 않을 수 있다.(option)
- 인자로 0을 전달하면 block을 수행한다.
- 인자로 WNOHANG을 전달하면 block을 수행하지 않는다.
- pid 인자로 -1을 전달하면 처음으로 종료된 자식 프로세스의 exit status를 받는다.
- pid가 양의 정수일 경우 이 pid와 값이 일치하는 자식 프로세스의 exit status를 받는다.
-1이기 때문에 아무 프로세스나 받는다. 0이기 때문에 blocking된다.
pid는 순차적으로 생겨나지만, race는 어떻게 될지 모른다. 스케쥴러에 의해 결정되기 때문이다.
이렇게 명시적으로 자식 pid를 준다면 순서대로 프로세스의 exit status가 출력되는 것을 볼 수 있다.
waitpid를 보면 아무 자식 프로세스의 exit status를 받고 non blocking이다.
리턴값이 0보다 크다는 것은 종료된 자식 프로세스가 있다는 것이다.
0일 때는 바로 종료된 프로세스도 없고 에러도 없을 때이다. 이때 다른 일을 하면 된다.
위의 코드에서 background job는 정의되지 않기 때문에 no wait이다. 그럼 어떻게 될까? 자식 프로세스가 끝났을 때 zombie process의 exit status가 PCB에 계속 남아있게 된다. memory leak이 일어난다.
쉘 프로그램이 끝날 때 비로소 삭제된다.
지금까지 low-level에서 exception의 종류(exceptions, interrupts), system call들을 확인했다.
user process에겐 보이지 않는 것들이다. 소프트웨어적으로 이런 것들을 유저가 확인할 수 있게 해주는 것이 Signal이다. 이 시그널을 이용하여 외부로부터의 접근, 내부적 오류 등 다양한 이벤트를 시그널로 처리하여 유저에게 보여주는 것을 Signal handling이라고 한다.
process는 종료될 때 do_exit() 함수를 통해 시그널을 보낸다면 시그널이 왔을 때 부모가 Reaping을 할 수 있다. 이러한 방법으로 wait syscall 처럼 부모 프로세스가 계속 기다리거나 Reaping을 못하는 상황을 처리할 수 있다.
Low-level hardware exceptions은 지금까지 커널의 exception handler로 처리되었다.
High-level software의 형태가 Signal인 것이다. 이는 유저에게 보이는 exception들을 제공한다.
또한 프로세스들과 커널이 또 다른 프로세스를 방해하는 것을 허용한다.
시그널은 시스템에서 발생하는 event를 프로세스에게 알리는 메세지이다.
- 시그널은 exception과 비슷하다.
- 보내는 주체는 프로세스나 커널이다. 받는 주체는 프로세스이다.
- 시그널마다 ID로 분류하여 사용된다.
- 시그널은 ID와 action이 전달된다.
signal은 대개 커널이 보낸다.
- 커널은 0으로 나누는 연산이 일어나거나 프로세스가 종료되었을 때 signal을 보낸다.
- illegal instruction이 일어났을 때
- illegal memory reference가 일어났을 때
- kill을 호출한다면 명시적으로 커널에게 프로세스를 종료하라는 의미로, signal을 보낸다.
signal을 보냈을 때, PCB에 저장이된다. one bit pending 공간과 blocked 공간이 존재한다.
- pending은 도착한 signal의 집합이다. 이를 one bit으로 set 해준다.
도착하면 set, 확인하고 처리하면 clear이다.
그리고 signal을 받지 않게 block할 수 있다. 이를 모두 프로그래밍 레벨에서 처리할 수 있다.
프로세스가 signal을 받았을 때 세 가지의 경우로 처리한다.
- signal을 무시한다.
- 프로세스를 종료한다.
- user level의 handler로 signal을 처리한다.
pending은 one bit이기 때문에 여러번 와도 queued 또는 set 되지 않는다.
다시 한 번 signal이 도착했지만 처리되지 않은 것이 pending이다. block은 도착해도 받지 않은 것이다.
프로세스 A 수행중에 B로 switch가 되는데, 커널에서 유저모드로 바뀔 때 signal이 보내진다.
커널은 pending & ~blocked를 계산하여 pending 된 것만 찾는다.
만약 ~blocked가 이 상황에 0이라면 그냥 user code로 바뀐다. 만약 아니라면 signal이 온 것이다.
~blocked가 1인 상황엔 커널이 pnb에서 nonzero bit을 찾아 process에게 전달하고, clear한다.
이를 반복하여 pnb의 nonzero bit을 모두 전달한다.
시그널을 처리하는 Signal handler를 구현하는 법을 알아보자.
- sighandler_t signal(int signum, sighandler_t handler)
특정 signal number와 signal을 처리하는 함수를 인자로 전달한다.
리턴 밸류는 void*이다. pointer returning void이다. 인자는 함수의 주소를 전달하고
리턴 타입도 함수 주소이다. 아래와 같다.- typedef void (*sighandler_t)(int)
Different values for handler
- SIG_IGN: signum type의 signal을 무시한다.
- SIG_DFL: default action이 작동된다.
- 또는 유저가 만든 signal handler로 처리된다. 이를 catching 또는 handling이라 불리운다.
예시를 확인할 함수 포맷
아래의 포맷의 함수를 사용하여 installing 할 것이다.
두 번째 인자는 새로운 handler이고, 세 번째 인자는 기존의 handler이다.
signal handler는 분리된 flow이다. 다른 하나의 프로세스가 아니라는 것이다. 이 말은 concurrency의 문제를 일으킨다.
위의 코드를 확인해보면 while(1) 구문이 돌아갈 때, x++의 연산을 수행한다 해보자. 두 세개의 inst가 일어나는데 이 때 signal이 끼어든다면, x를 처리하는 레지스터에 handler가 overwrite 할 수 있다.
유저코드에서 signal이 전달되고 다시 한 번 context switch 이후 유저코드에서 signal을 받는 경우를 보자. 시그널이 오고 받는 중간 과정에 많은 연산과 호출이 일어날 것이고. 이 때 사용된 레지스터나 메모리가 overwrite 될 수 있기에, 막 사용하면 위험하다. 또한 handler가 처리되는 동안에 또 다른 signal을 받을 수 있기 때문에 위험하다.
위의 코드를 보면, 시그널이 처리되고 있을 때,