Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

186162
807
85254


[리눅스커널][시그널] 시그널 - 시그널 번호와 동작 알아보기 12장. 시그널

커널 입장에서 시그널은 프로세스에게 보내는 단순한 형태의 메시지라고 할 수 있습니다.
시그널 정보와 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.19.30/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 프로세스가 종료할 때 커널은 해당 프로세스의 부모 프로세스에게 전달

이와 같은 시그널은 어떻게 발생할 수 있을까요? 크게 3가지 방법이 있습니다.

유저 어플리케이션 코드로 kill(), tgkill()과 같은 리눅스 저수준 표준 함수를 호출해서 시그널을 발생할 수 있습니다. 

다른 방법으로 리눅스 터미널에서 kill 명령어로 시그널을 발생할 수 있습니다.
만약 특정한 프로세스에게 시그널을 전달하고 싶을 때 어떻게 하면 될까요? 리눅스 터미널에서 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

X-terminal을 실행하는 bash란 프로세스를 볼 수 있는데, 가장 마지막에 보이는 PID가 1386인 프로세스가 가장 마지막에 실행된 bash 프로세스입니다.

다음 명령어를 입력하면 1386 프로세스에게 SIGKILL 이란 시그널을 전달해 해당 프로세스를 종료시킵니다.
root@raspberrypi:# kill -SIGKILL 1386
가장 마지막에 실행된 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은 프로세스에게 시그널을 전달하는 명령어인데 대부분 프로세스를 종료하거나
시그널 종류에 따라 정해진 동작을 처리한다는 사실을 알 수 있습니다.


가장 마지막 방법으로 커널이 스스로 시그널을 발생해야 한다고 판단할 때 시그널을 생성합니다. 예를 들어 어떤 프로세스가 종료할 때 부모 프로세스에게 자식 프로세스가 종료한다는 정보를 SIGCHLD 시그널로 알립니다.

다음 소절에서는 유저 프로세스가 시그널을 받으면 어떤 동작을 수행하는지 점검합시다.

시그널을 받으면 프로세스 어떻게 동작할까?  

시그널을 전달받은 프로세스는 어떤 동작을 할까요? 크게 2가지 동작을 수행합니다.
1> 시그널을 무시한다.
말 그대로 아무런 동작을 하지 않습니다.  하지만 SIGKILL, SIGSTOP 과 같은 시그널은 프로세스가 무시할 수 없습니다. 특정 상황에서 지정한 프로세스를 반드시 종료해야 하기 때문입니다.

2> 시그널에 명시된 동작을 수행합니다.
시그널 핸들러로 등록된 시그널 핸들러 함수를 실행하고 시그널 별로 명시된 동작을 수행합니다.
대부분 시그널을 받으면 프로세스가 취하는 액션은 프로세스 종료입니다.

프로세스는 어떤 이벤트에 대해서 특정 루틴을 수행해야 하기 때문에, 시그널을 한 번에 하나의 용도로만 사용합니다.

이번 절까지는 유저 프로세스 입장에서 시그널 처리 동작에 대해 살펴봤습니다. 다음 소절에서는 커널이 시그널 처리를 위해 어떤 동작을 하는지 알아봅시다.

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

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

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

정리하면 시그널 생성 동작은 2단계로 나눌 수 있습니다.
1.1> 시그널을 받을 프로세스에게 시그널 정보 써주기
1.2> 시그널을 받을 프로세스를 깨우기

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

커널은 시그널 종류에 따라 유저 프로세스가 정해진 동작을 수행하도록 지원합니다.
 - 시그널 핸들러를 설정 안했을 경우
   : SIGINT, SIGKILL 시그널인 경우 프로세스를 종료시킵니다.
 - 시그널 핸들러를 설정했을 경우
   :  시그널 핸들러 주소를 ARM 프로그램 카운터 정보에 써줘서 시그널 핸들러를 실행시킵니다.

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

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

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

"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
그리고 혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려드리겠습니다!"

핑백

덧글

댓글 입력 영역