-
시스템 프로그래밍 실습 7주차 : SignalsSystem Programming/Ubuntu Linux 2021. 10. 10. 23:13728x90
시스템 프로그래밍 실습 7주차 : Signals
[목차]
Multitasking
Signal
- Sending a signal
- Receiving a signal
들어가기 앞서 ....
의미전달에 사용되는 대표적인 방법은 메세지와 신호다. 메세지는 여러가지 의미를 갖을 수 있지만 복잡한 대신 , 신호는 1:1로 의미가 대응되기 때문에 간단하다!
=> 실제로는 여러 Process들에 대해 Multitasking하기 때문에 각각의 process들에게서 신호 받는 방법 -> Signal!
예를 들어 Process를 하나 만들어보자
#include <signal.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> int main(){ printf("give signal...\n"); sleep(30); exit(0); }
이후 컴파일을 하여 실행파일로 만들어준 후, 실행하면 30초간 프로그램이 실행될 것!
1) 이 상태에서 Ctrl+C를 누르게 되면 SIGINT 신호를 보내게 된다. 기본동작은 종료이므로 프로세스가 종료하게 된다.
2) 또는 kill -시그널번호 프로세스 id 를 통해서 시그널을 보낼 수 있다. 그러니까 SIGINT를 보내려면 kill -2 pid 또는 kill -SIGINT pid를 통해서 SIGINT보낼 수 있다.
# ./a.out give signal... ^C #
우리는 이 시그널을 제어하고 싶다! SIGINT 신호 발생시 종료한다는 문자열을 출력하고 3초 이후에 종료하고 싶다.
그래서 리눅스는 signal핸들러를 제공한다!
#include <signal.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> void interruptHandler(int sig){ printf("this program will be exited in 3 seconds..\n"); sleep(3); exit(0); } int main(){ signal(SIGINT, interruptHandler); printf("input Ctrl+C\n"); while(1); }
signal을 사용하면, 위와 같이 SIGINT 신호 발생 시 종료한다는 문자열을 출력하고 3초 이후에 종료할 수 있다!
#include <signal.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> void stopHandler(int sig){ printf("this program will stop in 3 seconds..\n"); sleep(3); exit(0); } int main(){ signal(SIGSTOP, stopHandler); printf("input Ctrl+Z\n"); while(1); }
SIGINT와 달리 Ctrl+Z를 입력하여 SIGSTOP을 보내면 그냥 멈춘다. 핸들링되지 않는다는 것!
SIGKILL과 SIGSTOP은 사용자가 절대 제어할 수 없다!
(어떤 이유로 인해 프로세스를 무조건 죽여야하는 경우가 있다. 만약 좀비프로세스를 계속해서 생성하는 프로세스가 있는데, 이 프로세스를 죽이지 못한다면 안 되기 때문에! )
또한 핸들러에 전달인자 sig는 시그널의 종류를 나타냅니다. 그렇기 때문에 시그널의 종류에 따라 처리할 수 있죠.
#include <signal.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> void signalHandler(int sig){ if(sig==SIGINT){ printf("this program will stop in 3 seconds..\n"); sleep(3); exit(0); } if(sig==SIGQUIT){ printf("signal SIGQUIT\n"); } } int main(){ signal(SIGINT, signalHandler); signal(SIGQUIT, signalHandler); printf("input Ctrl+C or Ctrl+\\ \n"); while(1); }
프로그램을 실행하여 Ctrl+\를 입력해서 SIGQUIT신호를 보내도 종료하지 않고, Ctrl+C를 입력하여 SIGINT를 보냈을 때 3초안에 종료한다.
# ./a.out input Ctrl+C or Ctrl+\ ^\signal SIGQUIT ^\signal SIGQUIT ^\signal SIGQUIT ^\signal SIGQUIT ^Cthis program will stop in 3 seconds..
Multitasking
- fork()으로 새로운 process를 생성한다
-> Called once, returns twice
- exit()으로 자신의 process를 끝낸다
-> 그 process를 zombie 상태에 넣는다
-> Called once, never returns
- wait()과 waitpid()로 끝난 자식 프로세스를 기다리고 가져간다
- exec()로 현재 존재하는 process에서 새로운 program을 부른다
-> Called once, never returns
=> Multitasking하기 때문에 각각의 process들에게서 신호 받는 방법 -> Signal!
Signal
- Signal이란 프로세스에게 시스템 내에 어떤 종류의 이벤트가 일어났다는 것을 알리는 작은 메세지
- exception들과 interrupt들을 위한 kernel abstraction
- Kernel로부터 process로 보냄
- small Integer ID's로 다른 signal들 구분한다
- signal에 있는 정도는 그의 ID와 그게 도착했다는 사실이다
* 시그널 처리 방법 4가지
- 프로세스가 받은 시그널에 따라 기본 동작을 수행한다. 각 시그널에는 기본 동작이 지정되어 있다. 대부분 시그널의 기본 동작은 프로세스를 종료하는 것이다. 이외에 시그널을 무시하거나 프로세스의 수행 일시 중지/재시작 등을 기본 동작으로 수행한다.
- 프로세스가 받은 시그널을 무시한다. 프로세스가 시그널을 무시하기로 지정하면 유닉스는 프로세스에 시그널을 전달하지 않는다.
- 프로세스는 시그널의 처리를 위해 미리 함수를 지정해놓고 시그널을 받으면 해당 함수를 호출해 처리한다. 시그널 처리를 위해 지정하는 함수를 시그널 핸들러라고 한다. 시그널을 받으면 기존 처리 작업을 중지한 후 시그널 핸들러를 호출하며, 시그널 핸들러의 동작이 완료되면 기존 처리 작업을 계속 수행한다.
- 프로세스는 특정 부분이 실행되는 동안 시그널이 발생하지 않도록 블록할 수 있다. 블록된 시그널은 큐에 쌓여 있다가 시그널 블록이 해제되면 전달된다.
시그널을 목적지 프로세스로 전달하는 것은 두 단계로 이루어진다!
1) Sending a signal 2) receiving a signal
1) Sending a signal
: kernel은 시그널을 목적지 process의 context에서의 상태를 update하는 방식으로 목적지 process로 보낸다(send/deliver)
: kernel은 다음과 같은 이유 중 하나로 signal을 보낸다
: 프로그램에서 시그널을 보내려면 kill, raise, abort 함수를 사용하면 된다! 이 중 가장 많이 쓰는 함수는 kill이며 프로세스를 종료시킬 때 주로 사용. 예를 들어, PID가 3255인 프로세스를 강제로 종료하려면 다음과 같이 사용!
# kill -9 3255
kill 명령은 인자로 지정한 프로세스에 시그널을 보내는 명령입니다. 위의 예는 PID가 3255인 프로세스에 9번 시그널을 보내라는 의미이며, 9번 시그널은 SIGKIL이고, 프로세스를 강제로 종료!
2) Receiving a signal
: 목적지 process는 signal의 전달에 반응하도록 kernel에 의해 강요되면 signal을 받는다
: 세 가지 방식대로 react한다
1) signal을 무시한다(ignore)
2) default action을 수행한다
3) signal-handler 함수에 의해 발생되는 signal을 잡는다
- signal-handler란?
: 프로세스가 시그널을 받을 때 기본 처리 방법은 대부분 프로세스를 종료하는 것. 그러나 프로세스가 종료하기 전에 처리할 작업이 남아 있거나 특정 시그널에 대해서는 종료하고 싶지 않을 경우에는 시그널을 처리하는 함수인 signal-handler를 지정할 수 있다
- catching a signal이란?
: signal-handler를 사용하여 시그널을 확인해서 처리하는 일!
예시) 'man 7 signal'로 확인할 수 있다
Signal semantics
-Signal은 보내졌지만 받아지지 않았을 때 Pending(보류)된다
: 어떠한 특정 타입에 대해서 pending signal는 최대 하나 있을 수 있다 ( There can be at most one pending signal of any particular type )
: 어떤 프로세스가 이미 특정 타입에 대해서 pending signal을 이미 가지고 있다면, 같은 타입의 Signal들이 queued되지 않는다 (queue에 들어가지 않는다)
- Process는 특정 signal들의 receipt을 선택적으로 막는다(block)
: Blocked signals은 전송은 되지만, unblock되기 전까지 받아지진 않는다
: process에 의해 막아질 수 없는 signal은 SIGKILL과 SIGSTOP이다!
- pending signal들은 최대 한번 받아질 수 있다
: Kernel uses a bit vector for indicating pending signals
: 커널은 각 프로세스에 대해 pending 비트 벡터(signal mask) 내에 시그널의 집합을 관리하며, blocked 비트 벡터 내에서 블록된 시그널의 집합을 관리한다.
: 커널은 pending 내에 비트 k를 타입 k의 시그널이 배달될 때마다 설정하며, 시그널 타입 k가 수신될 때마다 pending의 비트 k를 0으로 만든다.
Sending Signals
Process Group
- getpgrp 함수를 호출해서 현재 프로세스의 process group ID를 얻을 수 있다.
- setpgid 함수를 호출해서 프로세스 pid의 프로세스 그룹을 pgid로 변경한다. pid나 pgid 인자에 0을 주면 현재 프로세스의 pid를 사용한다.
쉘에서 최대 한 개의 포그라운드 작업(job)과 0개 이상의 백그라운드 작업이 존재할 수 있다.
: ctrl-c(ctrl-z) 누르면 foreground process group에 있는 모든 것들에 SIGINT(SIGSTP)을 보낼 수 있다
- SIGINT: default action is to terminate each process
- SIGTSTP: default action is to stop (suspend) each process
#include <signal.h>
int kill(pid_t pid, int sig);- 아무 signal을 아무 process group이나 process로 보낼 때 쓰인다
- kill 함수는 pid에 대응하는 프로세스에 sig로 지정한 시그널을 보냄 / pid는 특정 프로세스 또는 프로세스 그룹을 의미
- sig에 0(널 시그널)을 지정하면 실제로 시그널을 보내지 않고 오류를 확인합니다.
- pid
: pid 인자가 0보다 크면 pid로 sig를 보낸다
: pid 인자가 0이면 kill 함수는 호출한 프로세스가 속한 프로세스 그룹의 모든 프로세스에 시그널 sig를 보낸다.
: pid 인자가 -1이면 process 1을 제외한 모든 프로세스에게 sig를 보낸다
: pid 인자가 -1보다 작으면 kill 함수는프로세스 그룹 |pid|의 모든 프로세스로 보낸다.
- 성공시 0 리턴, 실패시 -1 리턴
- sig
: sig가 0이면, 아무런 signal도 보내지지 않지만 error checking은 한다!
- ‘man 2 kill’로 이 System call를 확인할 수 있다
예제)
1) fork로 자식 프로세스를 생성하고 while(1)로 무한루프를 돌린다.(N의 갯수 만큼 만듦)
2) N의 횟수 만큼, 특정 pid에 대해 kill 시그널로 인터럽트 명령을 보낸다.
3) N의 횟수 만큼, 부모 프로세스가 자식의 종료를 wait를 기다리게 하고 종료 후에, 정상 종료를 확인하고, 정상 종료일 경우 어떤 자식 프로세스가 종료되었는지 표시한다. 정상 종료가 아닐경우 abort된 경우이므로, abort 메세지를 출력한다.
4) 정상 종료가 아니므로 abort 메세지를 출력한다
rf.
WIFEXITED(status) : 자식이 정상적으로 종료되었다면 non-zero 이다.
WEXITSTATUS(status) : exit()를 호출하기 위한 인자나 return 값이 설정되고 종료된 자식의 반환 코드의 최하위 8비트를 평가한다. 이 매크로는 정상정료 - WIFEXITED(status) - 일때만 평가된다.
그 외 다른 Sending Signal 함수들 ...
시그널 보내기: raise(3)
#include <signal.h>
int raise(int sig);
raise 함수는 이를 호출한 프로세스에 인자로 지정한 시그널을 보냅니다. 만약 시그널 핸들러가 호출되면 시그널 핸들러의 수행이 끝날 때까지 raise 함수는 리턴하지 않습니다. raise 함수는 수행을 성공하면 0을, 실패하면 -1을 리턴합니다.
시그널 보내기: abort(3)
#include <stdlib.h>
void abort(void);
abort 함수는 이를 호출한 프로세스에 SIGABRT 시그널을 보냅니다. SIGABRT 시그널은 프로세스를 비정상적으로 종료시키고 코어 덤프 파일을 생성합니다. SIGABRT 시그널은 최소한 해당 프로세스가 연 파일은 모두 닫습니다. abort 함수는 raise(SIGABRT)와 같은 동작을 수행하지만 프로세스를 종료시키므로 리턴하지 않습니다.Installing Signal Handlers
커널이 프로세스를 커널 모드(kernel mode)에서 사용자 모드(user mode)로 전환할 때, 커널은 해당 프로세스의 pending & ~blocked 집합을 체크한다.
이 집합이 비어있으면, 커널은 제어를 해당 프로세스의 논리 제어흐름(logical control flow) 내의 다음 인스트럭션으로 전달한다.
이 집합이 비어있지 않다면, 커널은 집합 내에서 시그널을 선택해서 프로세스가 이 시그널을 수신하게 한다.
시그널을 수신한 프로세스는 해당 시그널에 맞는 동작을 한다. 해당 동작이 끝나면, 제어는 p의 논리 제어흐름 내의 다음 인스트럭션으로 돌아간다.
각 시그널은 기본 동작(default action)을 가지며, SIGSTOP, SIGKILL을 제외한 시그널의 기본 동작을 signal 함수를 통해 수정할 수 있다. signal 함수는 시그널 signum에 대한 동작을 다음 세 가지 방법 중 하나로 바꿀 수 있다.
시그널 핸들러 지정: signal(3)
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);signal 함수를 사용하면 시그널을 받을 때 해당 시그널을 처리할 함수나 상수를 지정할 수 있다. 첫번째 인자인 signum에는 SIGKILL과 SIGSTOP 시그널을 제외한 모든 시그널을 지정할 수 있다. 두 번째 인자인 handler에는 signum로 지정한 시그널을 받았을 때 처리할 방법을 지정한다.
handler 인자에는 다음 세 가지 중 하나를 설정해야 한다. 그리고 이것을 handler를 설치한다(installing handler)고 한다. handler의 호출은 시그널을 잡는다고 하며(catching the signal), handler의 실행은 시그널을 처리한다고 한다(handling the signal)
- SIG_IGN: signum 타입의 시그널을 무시하도록 지정
- SIG_DFL: signum 타입의 시그널의 기본 처리 방법을 수행하도록 지정
- 그 외의 handler: 시그널 핸들러의 주소
- 이는 프로세스가 signum 타입의 시그널을 수신할 때마다 handler가 호출된다. 이 handler는 시그널 핸들러(signal handler)라고 부르는 사용자 함수 주소다.
signal 함수는 시그널 핸들러의 주소를 리턴합니다. signal 함수가 실패하면 SIG_ERR를 리턴합니다.
만약 두 번째 인자인 handler가 함수 주소고, signum가 SIGKILL, SIGTRAP, SIGPWR이 아니면 signal 함수는 시그널을 처리한 후 시그널 처리 방법을 기본 처리 방법(SIG_DFL)으로 재설정합니다.
따라서 시그널 처리를 계속하려면 signal 함수를 호출해 시그널을 처리한 후 다시 signal 함수를 설정해야 합니다.
시그널 핸들러 지정: sigset(3)
#include <signal.h> void (*sigset(int tsig, void (*disp)(int)))(int);
sigset 함수의 인자 구조는 signal 함수와 동일합니다. sigset 함수도 첫번째 인자인 sig에 SIGKILL과 SIGSTOP 시그널을 제외한 어떤 시그널도 지정할 수 있습니다. 두 번째 인자인 disp에도 signal 함수처럼 시그널 핸들러 함수의 주소나 SIG_IGN, SIG_DFL 중 하나를 지정해야 합니다. 리턴값은 시그널 핸들러 함수의 주소입니다. sigset 함수가 실패하면 SIG_ERR를 리턴합니다.
sigset 함수가 signal 함수와 다른 점은 시그널 핸들러가 한 번 호출된 후에 기본 동작으로 재설정하지 않고 시그널 핸들러를 자동으로 재지정한다는 것입니다.예시)
1) 10개의 자식 프로세스를 생성한 후, while문으로 무한히 돌린다
2) 자식 프로세스를 kill() 함수로 다 끝낸다. kill함수로 끝냈기 때문에 SIGINT를 만나 signal을 보냈다
3) 이후 부모 프로세스가 자식 프로세스가 끝나길 기다리게 하는 wait()을 사용한 후, 정상 종료를 확인하고, 정상 종료일 경우 어떤 자식 프로세스가 종료되었는지 표시한다.
4) 예제 1과 달리 정상 종료했음을 알 수 있다. Signal함수로 받았기 때문?
rf.
WIFEXITED(status) : 자식이 정상적으로 종료되었다면 non-zero 이다.
WEXITSTATUS(status) : exit()를 호출하기 위한 인자나 return 값이 설정되고 종료된 자식의 반환 코드의 최하위 8비트를 평가한다. 이 매크로는 정상정료 - WIFEXITED(status) - 일때만 평가된다.
Handling Signals
위에서 봤던 내용 복습..
-Signal은 보내졌지만 받아지지 않았을 때 Pending(보류)된다
: 어떠한 특정 타입에 대해서 pending signal는 최대 하나 있을 수 있다 ( There can be at most one pending signal of any particular type )
: 어떤 프로세스가 이미 특정 타입에 대해서 pending signal을 이미 가지고 있다면, 같은 타입의 Signal들이 queued되지 않는다 (queue에 들어가지 않는다)
- Process는 특정 signal들의 receipt을 선택적으로 막는다(block)
: Blocked signals은 전송은 되지만, unblock되기 전까지 받아지진 않는다
: process에 의해 막아질 수 없는 signal은 SIGKILL과 SIGSTOP이다!
- pending signal들은 최대 한번 받아질 수 있다
: Kernel uses a bit vector for indicating pending signals
: 커널은 각 프로세스에 대해 pending 비트 벡터(signal mask) 내에 시그널의 집합을 관리하며, blocked 비트 벡터 내에서 블록된 시그널의 집합을 관리한다.
: 커널은 pending 내에 비트 k를 타입 k의 시그널이 배달될 때마다 설정하며, 시그널 타입 k가 수신될 때마다 pending의 비트 k를 0으로 만든다.- Pending signal들은 queued되지 않는다
- 한 시그널의 핸들러가 동작하고 있을 때, 새로 도착한 시그널은 block된다
- 가끔 read()와 같은 system call은 시그널의 전달에 의해 interrupt되고 난 이후에 자동으로 재시작되지 않는다.
1) 부모 프로세스가 10번 돌아 자식 프로세스를 생성한 뒤, 바로 exit(0) 실행
2) exit()으로 인해 SIGCHLD가 발생되고 handler 함수 작동
-> 하지만 한 handler 작동될 때, 다른 signal 받을 수 없기 때문에 몇몇 프로세스에서는 무시되어 handler 실행 안 되며, 그 몇몇 프로세스는 좀비 프로세스가 됨 ( = 자식 프로세스가 종료되었지만, 부모가 wait을 통해 상태를 보고받지 못한 경우에 생긴다)
한 시그널의 핸들러가 동작하고 있을 때, 새로 도착한 시그널은 block된다
어떠한 특정 타입에 대해서 pending signal는 최대 하나 있을 수 있다 어떤 프로세스가 이미 특정 타입에 대해서 pending signal을 이미 가지고 있다면, 같은 타입의 Signal들이 queued되지 않는다!
=> 따라서 다음과 같은 결과가 나온다! 아무런 blocked signal을 받지 못함 Virtual machine에서는 cpu 1개만 사용하기 때문이고, multicore machine을 쓸 때는 어떤 signal들은 무시되어 좀비 프로세스를 생성할 것!
<-> 하지만 위의 예제와 달리 빨간색 코드를 추가하였다! !
*) waitpid()는 wait 함수처럼 자식 프로세스를 기다릴때 사용하는 함수. 즉, 자식 프로세스의 종료상태를 회수할 때 사용한다. 하지만 waitpid 함수는 자식 프로세스가 종료될 때 까지 차단되는 것을 원하지 않을 경우, 옵션을 사용하여 차단을 방지할 수 있다. 그리고 기다릴 자식 프로세스를 좀더 상세히 지정할 수 있다.
위 waitpid(-1, NULL, WNOHANG)
- pid가 -1 일 경우 (pid == -1) : 임의의 자식 프로세스를 기다림
- WNOHANG : 기다리는 PID가 종료되지 않아서 즉시 종료 상태를 회수 할 수 없는 상황에서 호출자는 차단되지 않고 반환값으로 0을 받음 -> "blocking"을 막기 위해 사용된다! (caller가 block되는 것을 막고 return 값으로 0을 받게 한다 ) waitpid가 끝나지 않고 shutdown status를 회수하지 못할 때 ~?
알람 시그널은 일정한 시간이 지난 후에 자동으로 시그널이 발생하도록 합니다. 일정한 시간 후에 한번 발생시킬 수도 있고, 일정 시간 간격을 두고 주기적으로 알람 시그널을 발생시킬 수도 있습니다. 다음은 알람 시그널 관련 함수입니다.
unsigned int alarm(unsigned int sec);
alarm은 seconds초후에 프로세스에 SIGALRM 시그널이 전달되도록 설정한다. 만약 seconds가 0이면 alarm이 수행되지 않는다. 어떤 경우에도 이전에 설정된 alarm은 취소된다.#include <signal.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int count = 0; void alarmHandler(int signal) { count++; printf("Count :: %d\n",count); fflush(stdout); // alarm if(count < 5) alarm(2); else { printf("Count :: %d .. TIME OUT !\n",count); exit(0); } } int main() { // 알람 시그널의 핸들러를 설치한다. signal(SIGALRM, alarmHandler); // 2초후 알람이 울리도록 한다. alarm(2); while(1) { printf("Now I'm in while statement\n"); sleep(1); } return 0; }
signal(SIGALRM, alarmHandler);을 통해 alarm(n)이 발동되면 n초후 시그널에 의해 alarmHandler로 넘어가게 되고,
alarmHandler함수가 실행된다. 이때 n초 전에는 while(1)에서 돌게되고 n초 후에는 alarmHandler에 들어가서 돌다가 또 alarm(2)가 있으니 또 2초후 alarmHandler에 들어가게 된다.
(이때 또 2초가 되기전까지는 while에 들어온다.)
https://koyo.kr/post/csapp-ecf-signal-1/
https://darkprogrammer.tistory.com/5
https://reakwon.tistory.com/46
https://jhnyang.tistory.com/143
waitpid 함수 사용하기(wait함수와 비교) :: 개발여행기 (tistory.com)
728x90'System Programming > Ubuntu Linux' 카테고리의 다른 글
시스템 프로그래밍 실습 9주차 : System V IPC (0) 2021.10.22 시스템 프로그래밍 실습 8주차 : IPC (0) 2021.10.17 시스템 프로그래밍 실습 6주차 : Daemon (0) 2021.10.03 시스템 프로그래밍 실습 5주차 : Processes (0) 2021.09.27 시스템 프로그래밍 실습 4주차 : File I/O (0) 2021.09.20