Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

82235
1036
103652


[리눅스커널][시그널] 시그널 생성: 커널은 언제 시그널 생성할까? 12장. 시그널

리눅스 커널도 특정 조건에서 시그널을 생성해서 보낼 수 있습니다. 이번 소절에서 3가지 상황에서 커널이 시그널을 생성하는 과정을 살펴보겠습니다.
1.키보드로 “Ctl + C” 키를 입력했을 때
2.커널 메모리 모듈 OOM(Out of Memory) Killer가 실행할 때
3.안드로이드 시스템에서Lowmemory killer란 모듈이 실행할 때

키보드로 “Ctl + C” 키를 입력

우리는 리눅스 터미널에서 빌드나 셸 스크립트를 실행한 후 도중에 멈추고 싶을 때가 있습니다. 이럴 때 어떻게 하나요? 

     “Ctl+C” 키를 키보드로 입력하면 된다.

필자도 리눅스를 셸 스크립트를 돌리다가 시간이 오래 걸리면 “Ctl+C” 키를 종종 쓰곤 합니다.
그러면 “Ctl+C” 키를 키보드로 입력하면 실행 중인 터미널 명령어가 멈추는 이유는 무엇일까요?

     “Ctl+C” 키를 누르면 종료 시그널을 생성한다.

그렇다면 리눅스 커널 어딘가에서 “Ctl+C” 입력을 감지하고 시그널을 생성할 것이라 예상할 수 있습니다. 이번에는 키보드로 “Ctl + C” 키를 입력했을 때 커널이 시그널을 생성하는 흐름을 점검합니다. 이 때 동작은 다음 그림과 같이 4단계로 분류할 수 있습니다.

먼저 키보드로 “Ctl + C” 키를 입력했을 때 커널이 시그널을 생성하는 흐름을 점검합니다. 이 때 동작은 다음 그림과 같이 4단계로 분류할 수 있습니다.
 


위 그림에서 함수 호출은 화살표와 같이 ret_from_fork() 함수에서 resched_curr() 함수 방향으로 수행됩니다.

위 그림에서 보이듯 터미널을 열고 "Ctl+C" 키를 키보드로 입력했을 경우 실행 흐름은 4단계로 분류할 수 있습니다.

1. TTY 버퍼 드라이버: TTY Receiver 버퍼에서 스페셜 키인 “Ctl+C”키 입력을 감지합니다.
2. 시그널 함수 접근: 시그널 인터페이스 함수에 접근 후 시그널 전처리 과정을 수행합니다.
3. 시그널 생성: 시그널을 생성해서 시그널을 받을 프로세스의 태스크 디스크립터에 시그널 정보를 써 줍니다.
4. 프로세스 깨움: 시그널을 받을 프로세스를 깨웁니다.

이번 소절에서는 groud_send_sig_info() 함수까지 시그널 함수에 접근하는 과정에 초점을 맞추고 코드를 분석합니다.

위에서 그린 그림은 사실 ftrace 로그로 받은 함수 흐름을 표현한 것입니다.
1 kworker/u8:2-1208  [003] d...  3558.051181: complete_signal+0x14/0x248 <-__send_signal+0x160/0x420
2    kworker/u8:2-1208  [003] d...  3558.051242: <stack trace>
3  => do_send_sig_info+0x50/0x7c
4  => group_send_sig_info+0x50/0x54
5  => __kill_pgrp_info+0x4c/0x7c
6  => kill_pgrp+0x44/0x74
7  => __isig+0x34/0x40
8  => isig+0x54/0x104
9  => n_tty_receive_signal_char+0x28/0x70
10 => n_tty_receive_char_special+0xa10/0xb78
11 => n_tty_receive_buf_common+0x610/0xc04
12 => n_tty_receive_buf2+0x24/0x2c
13 => tty_ldisc_receive_buf+0x30/0x6c
14 => tty_port_default_receive_buf+0x48/0x68
15 => flush_to_ldisc+0xb4/0xcc
16 => process_one_work+0x224/0x518
17 => worker_thread+0x60/0x5f0
18 => kthread+0x144/0x174
19 => ret_from_fork+0x14/0x28
20 kworker/u8:2-1208  [003] d...  3558.051261: signal_generate: sig=2 errno=0 code=128 comm=RPi_signal pid=1218 grp=1 res=0

kworker/u8:2(pid: 1208) 워커 스레드에서 TTY 리시버 버퍼 키 입력을 감지해서 시그널을 생성하는 흐름입니다.

20번째 줄 로그는 시그널을 생성하는 동작을 표현합니다. RRi_signal이란 프로세스에게 SIGINT 시그널을 생성하는 동작입니다.

키보드로 “Ctl+C” 키를 입력했을 때 호출되는 함수 흐름에서 먼저 __isig() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/drivers/tty/n_tty.c]
1 static void __isig(int sig, struct tty_struct *tty)
2 {
3 struct pid *tty_pgrp = tty_get_pgrp(tty);
4 if (tty_pgrp) {
5 kill_pgrp(tty_pgrp, sig, 1);
6 put_pid(tty_pgrp);
7 }
8 }

TTY 터미널 버퍼에서 “Ctl+C” 키 입력을 인지하면 __isig() 함수가 실행됩니다.
4번째 줄 코드와 같이 tty 터미널 프로세스 그룹 정보를 점검합니다. 다음 5번째 줄 코드와 같이 프로세스 그룹에 시그널을 전달하기 위해 kill_pgrp() 함수를 호출합니다.

TTY 터미널 버퍼에서 “Ctl+C”를 처리하는 동작을 깊게 들어가면 책의 범위를 벗어나는 내용을 설명을 드려야 합니다. 일단 ‘ “Ctl+C” 키를 키보드로 입력하면 __isig() 함수가 실행된다.’ 라는 정도로 기억합시다.

다음 kill_pgrp() 함수 코드를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 int kill_pgrp(struct pid *pid, int sig, int priv)
2 {
3 int ret;
4
5 read_lock(&tasklist_lock);
6 ret = __kill_pgrp_info(sig, __si_special(priv), pid);
7 read_unlock(&tasklist_lock);
8
9 return ret;
10 }

kill_pgrp() 함수는 __kill_pgrp_info() 함수를 감쏴주는 인터페이스 역할을 수행합니다.
1 int __kill_pgrp_info(int sig, struct siginfo *info, struct pid *pgrp)
2 {
3 struct task_struct *p = NULL;
4 int retval, success;
5
6 success = 0;
7 retval = -ESRCH;
8 do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
9 int err = group_send_sig_info(sig, info, p);
10 success |= !err;
11 retval = err;
12 } while_each_pid_task(pgrp, PIDTYPE_PGID, p);
13 return success ? 0 : retval;
14 }

9번째 줄 코드에서는 입력으로 프로세스 그룹 pid 구조체로 group_send_sig_info() 함수 호출로 프로세스 그룹에 시그널을 전달합니다.

이전에 kill 명령어를 입력하거나 리눅스 저수준 표준 함수인 kill() 함수를 호출하면 시그널을 생성한다고 알고 있습니다. 시그널은 유저 프로세스나 사용자 입력으로 생성하지만 이렇게 커널에서도 시그널을 생성합니다.

X-Terminal로 Ctl+C 키를 입력했을 때 어떤 코드가 실행할까요?
TTY 터미널 디바이스로 키를 입력하면 n_tty_receive_char_special() 함수가 호출됩니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/drivers/tty/n_tty.c]
1 static int
2 n_tty_receive_char_special(struct tty_struct *tty, unsigned char c)
3 {
4 struct n_tty_data *ldata = tty->disc_data;
..
5 if (L_ISIG(tty)) {
6 if (c == INTR_CHAR(tty)) {
7 n_tty_receive_signal_char(tty, SIGINT, c);
8 return 0;
9 } else if (c == QUIT_CHAR(tty)) {
10 n_tty_receive_signal_char(tty, SIGQUIT, c);
11 return 0;
12 } else if (c == SUSP_CHAR(tty)) {
13 n_tty_receive_signal_char(tty, SIGTSTP, c);
14 return 0;
15 }
16 }

5~15번째 줄 코드를 보면 TTY Receive 버퍼로 어떤 키를 입력했는지 점검합니다. Ctl+C를 입력한 경우 6~7번째 줄 코드와 같이 SIGINT 시그널 입력으로 n_tty_receive_signal_char() 함수를 호출합니다.

이 함수 후속 루틴에서 __isig() 함수가 호출되어 터미널 디바이스에 시그널을 전달하는 것입니다.

이렇게 키보드에서 “Ctl + C” 키를 누르면 종료 시그널을 생성할 수 있습니다.  이어서 커널 드라이버에서도 시그널을 생성하는 과정을 소개합니다.

OOM(Out-of-Memory) Killer 시그널 전달

리눅스 커널 메모리 관리 기법 중 OOM(Out-of-Memory) Killer 모듈을 간단히 소개합니다.
리눅스 커널은 주기적으로 잔여 메모리 양을 점검하고 메모리를 확보를 위한 여러 모듈을 실행합니다.

Page Reclaim, Page Compaction이 가장 대표적인 예입니다. 이와 같은 리눅스 커널 메모리 시스템이 잔여 메모리(페이지) 확보를 시도해도 잔여 메모리가 부족하면 OOM Killer(Out of Memory Killer) 모듈을 실행합니다.

OOM Killer 동작 원리는 간단합니다. 프로세스를 종료해서 프로세스가 확보한 메모리를 회수합니다. 이 때 OOM Killer에서는 종료할 프로세스에게 시그널을 전달해서 프로세스를 종료시킵니다.

사실 OOM Killer가 실행할 상황이면 리눅스 커널 시스템 메모리가 심각하게 부족한 상태인 경우가 많습니다. 대부분 리눅스 시스템 개발자는 OOM Killer가 발생하면 커널 패닉을 유발하는 코드를 추가해서 OOM Killer가 발생한 원인을 분석하는 경우가 많습니다.

다음으로 OOM Killer가 실행해서 프로세스에게 종료 시그널을 전달하는 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/mm/oom_kill.c]
01 static void __oom_kill_process(struct task_struct *victim)
02 {
03 struct task_struct *p;
04 struct mm_struct *mm;
05 bool can_oom_reap = true;
06
07 p = find_lock_task_mm(victim);
...
08 /* Get a reference to safely compare mm after task_unlock(victim) */
09 mm = victim->mm;
10 mmgrab(mm);
...
11 do_send_sig_info(SIGKILL, SEND_SIG_FORCED, victim, PIDTYPE_TGID);
12 mark_oom_victim(victim);
13 pr_err("Killed process %d (%s) total-vm:%lukB, anon-rss:%lukB, file-rss:%lukB, shmem-rss:%lukB\n",
14 task_pid_nr(victim), victim->comm, K(victim->mm->total_vm),
15 K(get_mm_counter(victim->mm, MM_ANONPAGES)),
16 K(get_mm_counter(victim->mm, MM_FILEPAGES)),
17 K(get_mm_counter(victim->mm, MM_SHMEMPAGES)));

11 번째 줄 코드를 보면 강제 종료할 victim 이란 struct task_struct 구조체 멤버를 인자로 전달해서 do_send_sig_info() 함수를 호출합니다.
11 do_send_sig_info(SIGKILL, SEND_SIG_FORCED, victim, PIDTYPE_TGID);

다음 로그는 OOM Killer가 동작했을 때 패턴입니다.
[https://serverfault.com/questions/652362/understanding-oom-killer-logs]
1 beam.smp invoked oom-killer: gfp_mask=0xd0, order=0, oom_score_adj=0
2 beam.smp cpuset=/ mems_allowed=0
3 CPU: 0 PID: 20908 Comm: beam.smp Not tainted 3.13.0-36-generic #63~precise1-Ubuntu
4 Hardware name: Xen HVM domU, BIOS 4.2.amazon 05/23/2014
5 ffff880192ca6c00 ffff880117ebfbe8 ffffffff817557fe 0000000000000007
...
6  [ pid ]   uid  tgid total_vm      rss nr_ptes swapents oom_score_adj name
7  [ 9537]     0  9537     8740      712      21     1041             0 my_init
8  [13097]     0 13097       48        3       3       16             0 runsvdir
9  [13098]     0 13098       42        4       3       19             0 runsv
10 [13100]     0 13100       42        4       3       38             0 runsv
11 [13101]     0 13101       42        4       3       17             0 runsv
12 [13102]     0 13102       42        4       3        4             0 runsv
13 [13103]     0 13103       42        4       3       39             0 runsv
14 [13104]     0 13104     4779      243      15       60             0 cron
15 [13105]     0 13105     8591      601      22     1129             0 ruby
16 [13107]     0 13107    20478      756      43      560             0 syslog-ng
17 [13108]     0 13108    11991      642      28     1422             0 ruby
18 [20826]     0 20826     4467      249      14       63             0 run
19 [20827]     0 20827     1101      144       8       29             0 huobi
20 [20878]     0 20878     3708      172      13       48             0 run_erl
21 [20879]     0 20879   249481    57945     321    72955             0 beam.smp
22 [20969]     0 20969     1846       83       9       27             0 inet_gethost
23 [20970]     0 20970     3431      173      12       33             0 inet_gethost
...
24 [10901]     0 10901     1101      127       8       25             0 sh
25 [10902]     0 10902     1078       68       7       22             0 sleep
26 [10903]     0 10903     1078       68       8       22             0 sleep
27 Memory cgroup out of memory: Kill process 20911 (beam.smp) score 1001 or sacrifice child
28 Killed process 20977 (sh) total-vm:4404kB, anon-rss:0kB, file-rss:508kB

pid가 20911인 beam.smp 프로세스와 자식 프로세스인 sh(pid: 20977)를 종료해서 메모리를 확보하는 동작입니다.


저자도 리눅스 개발 도중에 위와 같은 OOM Killer 로그를 가끔 봅니다. 이런 문제를 할당 받으면 머리가 약간 멍해집니다. “이 문제를 어떻게 해결할까?”란 생각을 하면서 이런 저런 궁리를 시작합니다.

OOM Killer는 메모리가 부족했을 때 실행합니다. 메모리 부족의 원인은 매우 다양하며 이런 문제를 해결하는데 많은 시간이 걸리기도 합니다. 그런데 대부분 메모리 부족의 가장 큰 원인은 다음과 같습니다.

     "메모리 누수(Memory Leak)이며 동적 메모리를 지속적으로 할당하고 해제하지 않을 때 
       발생한다."

 실제 개발 현장에서는 메모리 부족 현상을 해결하기 위해 어떤 정보를 모으고 분석할까요? 1분 간격으로 프로세스 별 메모리 사용량을 커널 로그나 시스템 로그로 출력해서 프로세스 메모리 사용량을 모니터링하는 경우가 많습니다.

만약 특정 프로세스가 꾸준히 메모리을 할당하면 그 프로세스가 어떤 흐름으로 메모리를 쓰는지 추적하는 방식입니다.

안타까운 현실은 이 방식을 써도 메모리 부족 현상을 해결하기 어려울 때가 있다는 것입니다. 

리눅스 시스템 전반 동작을 책임지는 시스템 개발자나 데브웁스 개발자 입장에서 메모리 부족 현상은 골치거리 중 하나입니다.


Lowmemory Killer에서 시그널 전달

이번에는 라즈베리파이 이외 다른 리눅스 시스템에서 시그널을 전달하는 방식을 소개합니다. 안드로이드(P 이전 버전)에서 Lowmemory killer 동작 시 시그널을 전달하는 동작입니다.

안드로이드 시스템에서 메모리 부족 시 프로세스를 종료해서 잔여 메모리 공간을 확보하는 Lowmemorykiller 커널 드라이버도 시그널 함수를 씁니다. 리눅스 커널에서 페이지가 일정 갯수 이하로 떨어지면 (안드로이드에서 정의한) 프로세스별로 설정한 adj 값에 따라, 프로세스를 종료시킵니다.
/proc/<pid>/lowmem_adj

OOM Killer와 마찬가지로 프로세스를 종료시켜서 메모리를 확보하는 것입니다.

다음 코드는 안드로이드 드라이버에서 프로세스를 종료해서 메모리를 확보하는 lowmem_scan() 함수입니다.
[https://elixir.bootlin.com/linux/v4.4.160/source/drivers/staging/android/lowmemorykiller.c]
1 static unsigned long lowmem_scan(struct shrinker *s, struct shrink_control *sc)
2 {
3 struct task_struct *tsk;
4 struct task_struct *selected = NULL;
...
5 if (selected) {
6 task_lock(selected);
7 send_sig(SIGKILL, selected, 0);
8
9 if (selected->mm)
10 mark_oom_victim(selected);
11 task_unlock(selected);
12 lowmem_print(1, "send sigkill to %d (%s), adj %hd, size %d\n",
13      selected->pid, selected->comm,
14      selected_oom_score_adj, selected_tasksize);
15 lowmem_deathpending_timeout = jiffies + HZ;
16 rem += selected_tasksize;
17 }

5번째 줄 코드 분석 이전에 selected 지역 변수 정보를 알아봅시다.
5번째 줄 코드 이전에 안드로이드 시스템에서 정한 규칙으로 종료할 프로세스의 태스크 디스크립터 주소를 selected 이란 지역 변수에 저장하고 있습니다.

다음 7번째 줄 코드를 보면 selected(struct task_struct) 인자로 해당 프로세스에게 SIGKILL 시그널을 전달합니다.
7 send_sig(SIGKILL, selected, 0);

위와 같은 코드가 실행하면 어떤 로그 패턴을 확인할 수 있을까요? 다음은 lowmemorykiller 란 모듈이 동작할 때 커널 로그입니다.
[https://android.stackexchange.com/questions/89544/lowmemorykiller-is-killing-loads-of-system-apps-and-forcing-user-space-death-bu]
1 <6>[002033.125104,1] lowmemorykiller: Killing 'system:ui' (8151), adj 1000
2 <6>[002035.895963,0] : Report pwrkey press event
3 <6>[002035.307927,1] lowmemorykiller: Killing 'droid.deskclock' (28714), adj 1000
4 <6>[002036.387002,0] : Report pwrkey release event
5 <6>[002036.478425,0] lowmemorykiller: Killing 'com.ebay.mobile' (26933), adj 1000

lowmemorykiller모듈에서 'system:ui' (pid: 8151), 'droid.deskclock' (pid: 28714), 'com.ebay.mobile' (pid: 26933) 프로세스를 종료하는 로그입니다.

많은 분들은 리눅스 터미널에서 kill 명령어로 유저 어플리케이션에서 kill, tgkill 함수를 호출할 때 유저 프로세스에서 시그널을 생성한다고 알고 있습니다. 하지만 리눅스 커널 스스로 판단해서 시그널을 생성할 수 있고 3가지 예를 들어서 해당 코드를 점검했습니다.

다음 소절에서는 시그널을 생성하는 핵심 함수인 __send_signal() 함수를 분석하겠습니다.

#Referene 시그널
시그널이란
시그널 설정은 어떻게 할까
시그널 생성 과정 함수 분석
프로세스는 언제 시그널을 받을까
시그널 전달과 처리는 어떻게 할까?
시그널 제어 suspend() 함수 분석 
시그널 ftrace 디버깅

"혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답글 올려드리겠습니다!" 





핑백

덧글

댓글 입력 영역