유저 프로세스 입장에서 시그널이란 


시그널이란 무엇일까요?

커널 입장에서 시그널은 프로세스에게 보내는 단순한 형태의 메시지라고 할 수 있습니다.

시그널 정보와 PID를 프로세스에게 전달하는 것입니다.


유저 프로세스 입장에서 프로세스는 무엇일까요? 유저 프로세스 관점으로 시그널은 실행 흐름을 제어하는 비동기적인 중단입니다. 이렇게 유저 프로세스와 커널 입장에서 시그널은 약간 다른 관점에서 볼 수 있습니다.


이번에 먼저 유저 프로세스 관점으로 시그널을 살펴보겠습니다.

여러분이 너무 보고 싶은 책이 있다고 가정하겠습니다. 

3시간 동안 읽을 수 있는 책 분량이라 주말에 책을 읽기 시작했습니다. 3시간 동안 아무런 방해를 받지 않고 책을 읽을 수 있으면 좋겠지만 이런 상황은 이상적인 환경입니다. 집에서 인터폰 벨리 울리던가 전화가 온다던가 여러 종류 중단이 발생 수 있기 때문입니다.


이런 중단은 예상하지 않았던 상황에서 발생하며 소프트웨어에서는 비동기적인 이벤트라고도 말합니다.


인터폰이 울리거나 회사나 친구한테 전화가 오면 우리는 보통 적절한 대응을 합니다. 보통 전화를 받거나 인터폰을 받고 대화를 합니다. 만약 여러분이 임베디드 리눅스 개발자인데 주말에 회사에서 전화가 왔다면 어떻게 할까요? 대부분 전화를 받을 것입니다. 시급한 문제가 생겼을 때 회사에서 개발자에게 전화를 하기 때문입니다. 하지만 가끔 중요하지 않은 다른 전화(광고, 부동산 투자)가 오면 전화를 안 받을 수도 있습니다.


프로세스도 마찬가지입니다. 유저 레벨 프로세스 기준으로 우리가 책을 읽는 것과 마찬가지로 정해진 시나리오에 따라 어떤 작업을 수행한다고 가정합시다. 책을 끝가지 방해 받지 않고 읽었으면 좋겠으나 유저 레벨 프로세스도 마찬가지로 예상치 못한(비동기적인) 중단으로 작업 흐름이 끊어 질 수 있습니다.


이렇게 인터폰이 울리거나 전화가 오는 것과 같이 유저 프로세스도 일을 하다가 비동기적인 중단을 겪을 수 있습니다. 이를 리눅스에서는 시그널이라고 하며 유저 프로세스는 시그널에 대해 이미 정해진 처리를 해줘야 합니다.


이런 유형의 다른 대표적인 중단은 인터럽트를 들 수 있습니다. 유저 프로세스 입장에서 시그널도 예상치 않았던 비동기적인 이벤트라고 볼 수 있습니다. 


대표적인 비동기적인 이벤트로 시그널이 발생하는 상황을 생각해봅시다.

1.리눅스 터미널에서 Ctl+C 키를 눌러서 프로세스를 종료

2.리눅스 터미널에서 다음 커맨드로 프로세스를 강제 종료

kill -9 [PID]

3.리눅스 커널에서 특정 조건에서 해당 프로세스를 종료


이렇게 언제 발생할지 모르는 비동기적인 중단(이벤트)에 대해 적절한 처리를 해줘야 합니다.


리눅스 커널에서도 자체적으로 시그널을 발생할 수 있습니다. 한 가지 예로 OOM(Out-of-memory) Killer를 들 수 있습니다. 잔여 메모리가 매우 부족할 때 OOM Killer 모듈은 프로세스를 강제 종료시켜서 메모리를 확보합니다. 종료할 프로세스에게 시그널을 전달합니다. 


안드로이드 시스템에서 OOM Killer가 실행하기 전 메모리 부족을 방지하기 위해 Lowmemory Killer란 모듈을 실행합니다. OOM Killer와 마찬가지로 프로세스를 종료시켜서 메모리를 확보합니다. 이 때도 종료할 프로세스에게 시그널을 전달합니다. 이 내용은 다음 소절에서 자세히 다룹니다.


시그널 번호와 동작 알아보기


책을 읽다가 발생하는 여러 비동기적인 이벤트(인터폰, 전화)가 있듯이, 유저 레벨 프로세스 동작 중에 발생할 수 있는 시그널도 여러 가지가 있습니다. 유닉스나 리눅스 커널 버전이 달라도 시그널 종류는 비슷합니다. 시그널은 POSIX 규약으로 정의된 표준이며 이제 맞게 리눅스 시스템 개발자가 구현하기 때문입니다. 다음은 라즈베리파이 리눅스 커널 4.14.70 버전에서 지원하는 시그널 번호를 확인한 결과입니다.

pi@raspberrypi:~ $ kill -l

 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP

 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1

11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM

16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP

21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ

26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR

31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3

38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8

43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13

48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12

53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7

58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2

63) SIGRTMAX-1 64) SIGRTMAX


시그널은 POSIX(Portable Operating System Interface) 규약에 정한 표준입니다. 어플리케이션 이식성을 높이기 위한 시도로 어플리케이션이 다양한 유닉스 계열 운영체제에서 구동할 수 있게 정한 것입니다. 리눅스 시스템 개발자들은 POSIX 규약에 따라 시스템 코드를 설계하고 코드를 구현합니다. 


1~34번까지는 유닉스 계열 운영체제(리눅스 포함)에서 같은 시그널 종류와 번호를 확인할 수 있습니다. 이를 정규 시그널이라고도 말합니다. 대신 35~63 시그널은 리얼 타임 시그널입니다.


정규 시그널과 리얼 타임 시그널의 차이점은 무엇일까요? 가장 큰 차이는 시그널 큐 처리 방식입니다. 정규 시그널은 같은 종류의 시그널을 연달아 보내면 프로세스는 한 가지 시그널만 받아 처리하지만 리얼 타임 시그널은 모듀 큐에 쌓아서 처리를 합니다.


각각 시그널은 int 형 정수로 선언되어 있는데 라즈베리파이에서 다음 해더 파일에서 시그널 종류별 int형 정수 번호를 확인할 수 있습니다.

root@raspberrypi:/usr/include # cat arm-linux-gnueabihf/asm/signal.h

#define SIGHUP          1

#define SIGINT           2

#define SIGQUIT         3

#define SIGILL            4

#define SIGTRAP         5

#define SIGABRT         6

#define SIGIOT           6

#define SIGBUS          7

#define SIGFPE           8

#define SIGKILL          9

#define SIGUSR1        10

#define SIGSEGV        11


유저 어플리케이션에서 시그널을 처리하는 함수를 작성하면 위와 같이 각 시그널 종류 별로 정의된 정수 값으로 시그널을 처리합니다.


유저 공간에서 정의된 시그널 번호는 리눅스 커널에서도 같은 번호로 관리합니다. 

[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/uapi/asm/signal.h]

#define SIGHUP 1

#define SIGINT 2

#define SIGQUIT 3

#define SIGILL 4

#define SIGTRAP 5

#define SIGABRT 6

#define SIGIOT 6

#define SIGBUS 7

#define SIGFPE 8

#define SIGKILL 9

#define SIGUSR1 10

#define SIGSEGV 11


32개 시그널 중 자주 활용하는 시그널을 정리하면 다음과 같습니다.

시그널 동작

SIGHUP 프로세스 제어 터미널이 종료될 때 세션 리더에게 전달, 터미널을 읽어버렸을때 발생

SIGINT 터미널 인터럽트 신호로(Ctl+C)키나 DELETE 키를 입력했을때 발생

SIGQUIT 사용자가 종료 문자(Ctl-\) 실행

SIGILL 유저가 유효하지 않은 명령어 실행 시도

SIGTRAP 트레이스 혹은 브레이크 포인트 실행

SIGABRT 프로세스가 비정상적인 종료 시중단 신호로 abort()에서 보냄

SIGIOT 비동기적인 I/O 이벤트 처리 시 보냄

SIGBUS 유효하지 않은 메모리 공간에 접근하거나 하드웨어 장애를 일으킬 때 커널이 생성

SIGFPE 부동 소수점을 연산 도중 오버플로우나 언더플로우가 발생하면 익셉션으로 발생하는 시그널

SIGKILL kill() 함수를 호출하면 프로세스를 종료시킴

SIGUSR1

SIGUSR2 유저 공간에서 처리하기 위해 정의하며 커널은 이 시그널을 쓰지 않음

SIGSEGV 유효하지 않은 메모리 접근을 시도할 때 커널이 해당 프로세스에 전달함

읽기나 쓰기 권한이 없는 메모리 공간에 접근하거나 실행할 수 없는 코드를 실행할 때 발생함

SIGUSR2 유저 공간에서 처리하기 위해 정의하며 커널은 이 시그널을 쓰지 않음

SIGPIPE 닫힌 파이프에 열고 쓸 때 실행

SIGALRM alarm() 함수가 자신을 실행한 프로세스에게 전달

SIGCHLD 프로세스가 종료할 때 커널은 해당 프로세스의 부모 프로세스에게 전달


유저 레벨 프로세스가 리눅스 저수준 표준 함수를 호출해서 시그널을 발생할 수 있습니다. 또한 어떤 프로세스가 종료할 때 부모 프로세스에게 자식 프로세스가 종료한다는 정보를 SIGCHLD 시그널로 알립니다.


만약 특정한 프로세스에게 시그널을 전달하고 싶을 때 어떻게 하면 될까요? 리눅스 터미널에서 kill 명령어를 쓰면 됩니다.


라즈베리파이에서 X-Terminal을 2개 열고 다음 명령어를 입력합시다.

1 root@raspberrypi:/usr/include # ps -ely | grep bash

2 S 1000 500 432 0 80 0 4096 1645 poll_s tty1 00:00:00 bash

3 S 1000 1150 1146 0 80 0 4192 1628 wait pts / 0 00:00:00 bash

4 S 0 1355 1350 0 80 0 3376 1433 wait pts / 0 00:00:00 bash

5 S 1000 1386 1146 0 80 0 3964 1628 poll_s pts / 1 00:00:00 bash

6 root@raspberrypi:# kill -SIGKILL 1386


위에서 5번 출력 결과를 보면 가장 마지막에 실행된 bash 프로세스의 PID가 1386이니 위와 같이 kill 명령어에 -SIGKILL 옵션을 줘서 실행하니 프로세스는 강제 종료합니다.



라즈베리파이 리눅스 시스템에서 kill 명령어 메뉴얼을 확인하면 다음과 같습니다.

root@raspberrypi:/home/pi# info kill

Up: Process control


24.1 ‘kill’: Send a signal to processes

=======================================

The ‘kill’ command sends a signal to processes, causing them to

terminate or otherwise act upon receiving the signal in some way.


kill은 프로세스에게 시그널을 전달하는 명령어인데 대부분 프로세스를 종료하거나

시그널 종류에 따라 정해진 동작을 처리한다는 사실을 알 수 있습니다.


커널에서 시그널은 어떻게 처리할까? 


리눅스 커널 입장에서 시그널은 프로세스 간 통신을 위한 간단한 인터페이스입니다. 커널은 상황에 따라 시그널을 생성하고 전달해주는 역할을 수행합니다.


리눅스 커널에서 시그널에 대한 처리는 2단계로 나눌 수 있습니다.


1> 시그널 생성

유저 공간에서 시그널을 설정하면 커널은 해당 프로세스의 태스크 디스크립터에 시그널 정보를 써줍니다. 시그널을 받을 프로세스 스택 최상단 주소에 있는 struct thread_info flags 멤버에 _TIF_SIGPENDING 매크로를 써 줍니다. 시그널을 받을 프로세스에게 누군가 시그널을 생성했고 해당 시그널이 전달될 것이라고 알려주는 것입니다. 이후 시그널을 받을 프로세스를 깨웁니다.


2> 시그널 처리

시그널을 받을 프로세스가 시스템 콜이나 인터럽트 처리를 마무리한 이 후 시그널을 처리합니다.


커널은 시그널 종류에 따라 유저 프로세스가 정해진 동작을 수행하도록 다음 동작을 합니다.

 - 시그널 핸들러를 설정 안했을 경우

   : SIGINT, SIGKILL 시그널인 경우 프로세스를 종료시킵니다.

 - 시그널 핸들러를 설정했을 경우

   :  시그널 핸들러 주소를 ARM 프로그램 카운터 정보에 써줘서 시그널 핸들러를 실행시킵니다.


커널은 시그널 핸들러를 실행시켜 줄 뿐 시그널 종류에 따라 세부적인 처리를 할 수가 없습니다. 프로세스에게 전달하는 정보는 시그널 번호가 전부이며 표준 시그널에는 인자나 메시지 또는 그외 정보를 전달할 수 없습니다.


커널은 시그널을 대부분 프로세스를 종료할 때 프로세스나 스레드 그룹에 전달하는 메시지 형태로 사용합니다.


시그널을 발생했으나 아직 전달되지 않은 시그널을 펜딩 중인 시그널(pending signal)이라고 합니다. 특정 타입의 펜딩 시그널은 프로세스당 항상 하나만 존재합니다. 같은 시그널을 동일한 프로세스에게 전달하면 시그널 큐에서 대기하는 것이 아니라 그냥 폐기됩니다. 

+ Recent posts