Signals and Traps
하드웨어가 인터럽트에 의해 운영된다면 소프트웨어는 시그널에 의해 운영된다고 할 수 있습니다. 시그널이라는 기능이 없다면 오류로 무한 루프를 도는 프로세스를 정지시킬 수도, 종료시킬 수도 없고 child process 가 종료되었을때 parent process 에게 알릴 수도 없습니다. 시그널은 프로세스에 전달되는 software interrupt 라고 할 수 있습니다. OS 내에 정의되어 있고 여러가지 system events 가 발생했을때 프로세스에 알리기 위해 사용됩니다. 스크립트 실행중에 ctrl-c 키를 누를때, 터미널 프로그램을 종료시키는 경우에도 signal 이 사용되며 특정 프로세스가 다른 프로세스를 종료시킬때, 또는 두 프로세스 간에 서로 통신을 하기위해 signal 을 사용할 수도 있습니다.
Signal sending
신호는 보내는 쪽에서 임의대로 아무때나 상대 프로세스에게 보낼 수 있습니다. 신호를 보낼 때 특정 메시지를 함께 전달하는 것은 아니며 신호를 받는 쪽에서는 누가 신호를 보냈는지도 알 수 없습니다. 신호는 OS 에의해 보내질 수 있고 사용자가 kill 명령으로 보낼 수도 있고 또한 자기 자신에게 신호를 보낼 수도 있습니다. 실제로 OS 에서는 목적지 프로세스의 PCB (Process Control Block) signal table 에 해당 신호 값을 설정하는 것으로 이루어집니다.
Operating System scheduling
Signal receiving 을 설명하기 전에 OS scheduling 에 대해 알아보겠습니다. 모든 실행되는 프로세스는 cpu 입장에서 보았을때 2가지 상태를 오가면서 실행됩니다. 실제 cpu 인스트럭션을 실행하는 상태( cpu burst )와 I/O 를 위해 wait 하는 상태( I/O burst ) 입니다. 실행중인 process A 가 I/O wait 상태가 되면 cpu 는 idle 상태가 되므로 OS 스케줄러에 의해 process B 로 실행이 변경됩니다.
cpu 의 실행이 process A 에서 process B 로 변경되는것을 context switching 이라고 하는데 이때는 process A 에서 실행중인 cpu 상태( 여러 register 값들 ) 이 백업되어 저장되고 이전에 저장되었던 process B 의 cpu 상태값 이 로드되어 실행을 계속하게 됩니다. core 가 하나인 cpu 에서 동영상을 여러개 실행시켰을때 동시에 실행되는 것처럼 보이는 것을 보면 얼마나 자주 프로세스 간에 context swtching 이 일어나는지 알 수 있습니다.
프로세스 상태를 OS 입장에서 5 가지로 구분해 볼 수 있습니다. 먼저 프로세스가 생성되면( new ) 스케줄러가 관리하는 ready queue 에 들어가게 되고( ready ) 이후에 스케줄러에 의해 실행됩니다( running ). 실행 중인 프로세스는 I/O wait 이 발생하거나 wait() system call 을 수행하게 되면 waiting 상태로 변경되고 scheduled out 됩니다. 이후 I/O, wait 이 완료되면 다시 ready 상태로 돼서 scheduled 됩니다. 또한 running 상태에서는 프로세스에 할당된 시간이 만료되거나 스케줄러에 의해 interrupt 되면 실행이 중단되고 ready 상태가 되며 다음 프로세스가 running 하게됩니다. 프로세스가 실행을 완료하거나 종료신호를 받으면 exit( terminate ) 됩니다.
실행 중인 프로세스가 context switch 되는 것은 직접 스케줄러 함수를 호출하거나 ( voluntary_switches ),
timer interrupt 에 의해 스케줄러가 실행되면서 발생할 수 있습니다 ( involuntary_switches ).
/proc/$PID/sched 또는 /proc/$PID/status 를 통해 값을 조회해 볼 수 있습니다.
Signal receiving
신호를 받는다는 것는 스케줄러에 의해 프로세스가 실행을 재개하기 전에 PCB 의 신호 항목을 조회하여 설정되어 있을 경우 해당 signal handler 를 실행하는 것을 의미합니다. 그러므로 만약 waiting 상태에 있는 프로세스에 signal sending 이 되면 ready 상태로 이동하게 됩니다.
sending 과 receiving 사이 그러니까 신호가 전달되어 PCB 신호 항목이 설정되어 있으나 아직 handler 가 실행되지 않은 동안을 pending 이라고 하는데 이때 다시 신호가 전달된다면 무시됩니다. ( on, off 할수있는 1 bit flag 으로 생각하면됨 ) 그러므로 사용자 handler 를 이용해 전달받은 신호 개수를 카운트한다면 올바른 결과를 얻을 수 없습니다. 예를 들어서 스크립트에서 background 로 여러개의 프로세스를 실행시킨 후에 SIGCHLD 신호를 trap 해서 종료 상태 값을 구한다고 했을 경우 동시에 프로세스가 종료한다면 중복되는 신호는 잃어버리게 됩니다.
실제 신호값이 조회되고 처리되는 시점은 프로세스가 kernel mode 에서 user mode 로 복귀할 때입니다. system call, interrupt (timer interrupt...) 처리를 위해서는 kernel mode 진입과 user mode 복귀가 수반되므로 현재 실행 중에 있는 프로세스라도 신호가 처리될 수 있습니다.
Signal handler
Signal 이 프로세스에 전달되었을때 실행되는 코드를 signal handler 라고 합니다. 모든 signal 은 default action 을 위한 signal handler 를 가지고 있습니다. 그러므로 SIGTSTP 신호가 전달되면 프로세스가 중단되고 SIGTERM 신호를 받으면 종료됩니다. shell 에서는 trap 명령을 이용하여 사용자가 직접 signal handler 를 등록해 사용할 수 있습니다.
하드웨어 인터럽트와 시그널이 다른 점 중에 하나는 인터럽트는 커널 모드에서 OS 에 의해 처리되지만 시그널은 유저 모드에서 사용자 프로그램에 의해서도 처리된다는 점입니다.
INT handler 가 실행되는 순서
스크립트를 a.sh -> b.sh -> c.sh 순서로 실행하여( a.sh 에서 b.sh 호출, b.sh 에서 c.sh 호출 ) 현재 sleep 상태에 있다면 3 개의 프로세스는 모두 같은 foreground process group 에 속하게 되고 ctrl-c 로 종료를 시도할 경우 프로세스 그룹에 INT 신호가 전달되어 함께 종료하게 됩니다.
이때 a.sh, b.sh, c.sh 프로세스에는 INT 신호가 PCB 에 설정되고 c.sh 프로세스가 default INT handler 에 의해 종료하면 OS 스케줄러에 의해 b.sh 이 실행되면서 default INT handler 에 의해 종료하게 되고 마찬가지로 a.sh 도 default INT handler 에 의해 프로세스가 종료하게 됩니다.
한가지 참고해야될 사항은 앞선 프로세스가 default handler 가 아닌 사용자 handler 를 실행하고 종료할 경우 뒤이어지는 default handler 실행에서 bash
와 sh
이 처리되는 방식이 다릅니다.
다음은 c.sh 에서 sleep 상태에 있을때 ctrl-c 로 종료를 시도한 경우입니다.
1. a.sh, b.sh, c.sh 모두 default handler ( 아무런 trap 설정도 안한상태 )
a.sh : default INT handler 에 의해 종료
b.sh : default INT handler 에 의해 종료
c.sh : default INT handler 에 의해 종료
2. a.sh 은 default handler, b.sh 는 사용자 handler, c.sh 은 default handler
a.sh : 종료되지 않고 나머지 코드 실행 ( sh 의경우: default handler 에 의해 종료 )
b.sh : 사용자 INT handler 실행
c.sh : default INT handler 에 의해 종료
3. a.sh, b.sh 은 default handler, c.sh 은 사용자 handler
a.sh : 종료되지 않고 나머지 코드 실행 ( sh 의경우: default handler 에 의해 종료 )
b.sh : 종료되지 않고 나머지 코드 실행 ( sh 의경우: default handler 에 의해 종료 )
c.sh : 사용자 INT handler 실행
4. a.sh 는 사용자 handler, b.sh 는 default handler, c.sh 은 사용자 handler
a.sh : 사용자 INT handler 실행
b.sh : 종료되지 않고 나머지 코드 실행 ( sh 의경우: default handler 에 의해 종료 )
c.sh : 사용자 INT handler 실행
5. a.sh, b.sh, c.sh 모두 사용자 handler
a.sh : 사용자 INT handler 실행
b.sh : 사용자 INT handler 실행
c.sh : 사용자 INT handler 실행
이와 같이 프로세스에서 신호가 처리되는 방식은 딱히 정해져 있는 것이 아니고 프로그래머가 여러 가지 방법을 (ignore, block, signal handler) 이용해 직접 변경을 할 수가 있습니다.
Ignore 된 신호
사용자의 trap 설정에 의해 ignore 된 신호는 기본적으로 child 프로세스에게 상속됩니다. 그러므로 a.sh 에서 INT 신호를 ignore 했다면 b.sh, c.sh 에서 설정된 사용자 INT handler 와 default handler 는 호출되지 않게 됩니다.