Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

178162
807
85246


[리눅스커널][시그널] 시그널 생성: __send_signal() 커널 함수 분석 12장. 시그널

누군가 ‘시그널을 생성하는 핵심 함수가 무엇인가?’ 라고 질문을 한다면 __send_signal() 함수라고 대답할 수 있습니다. 그렇습니다. 시그널을 생성하는 핵심 함수는 __send_signal() 입니다. 

이제부터 __send_signal() 함수 코드를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
int group, int from_ancestor_ns)
3 {
4 struct sigpending *pending;
5 struct sigqueue *q;
6 int override_rlimit;
7 int ret = 0, result;
8
9 assert_spin_locked(&t->sighand->siglock);
10
11 result = TRACE_SIGNAL_IGNORED;
12 if (!prepare_signal(sig, t,
13 from_ancestor_ns || (info == SEND_SIG_FORCED)))
14 goto ret;
15
16 pending = group ? &t->signal->shared_pending : &t->pending;
17
18 result = TRACE_SIGNAL_ALREADY_PENDING;
19 if (legacy_queue(pending, sig))
20 goto ret;
...
21
22 q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
23 if (q) {
24 list_add_tail(&q->list, &pending->list);
25 switch ((unsigned long) info) {
26 case (unsigned long) SEND_SIG_NOINFO:
27    q->info.si_signo = sig;
28    q->info.si_errno = 0;
29    q->info.si_code = SI_USER;
30    q->info.si_pid = task_tgid_nr_ns(current,
31 task_active_pid_ns(t));
32    q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
33    break;
34 case (unsigned long) SEND_SIG_PRIV:
35    q->info.si_signo = sig;
36    q->info.si_errno = 0;
37    q->info.si_code = SI_KERNEL;
38    q->info.si_pid = 0;
39    q->info.si_uid = 0;
40 break;
41 default:
42    copy_siginfo(&q->info, info);
43    if (from_ancestor_ns)
44 q->info.si_pid = 0;
45 break;
46 }
47
48 userns_fixup_signal_uid(&q->info, t);
49
50 } else if (!is_si_special(info)) {
51 if (sig >= SIGRTMIN && info->si_code != SI_USER) {
52 result = TRACE_SIGNAL_OVERFLOW_FAIL;
53 ret = -EAGAIN;
54 goto ret;
55 } else {
56 result = TRACE_SIGNAL_LOSE_INFO;
57 }
58 }
59
60 out_set:
61 signalfd_notify(t, sig);
62 sigaddset(&pending->signal, sig);
63 complete_signal(sig, t, group);
64 ret:
65 trace_signal_generate(sig, info, t, group, result);
66 return ret;
67 }

먼저 __send_signal() 함수 분석에 앞서 이 함수가 어떤 일을 하는지 알아볼까요?
__send_signal() 함수 세부 처리 과정은 5단계로 분류할 수 있습니다.
1 단계:  시그널 예외 처리
2 단계: 시그널 펜딩 리스트에 시그널 정보를 저장한 후 시그널을 받을 프로세스 태스크 디스크립터에 써줌
3 단계: 시그널을 받을 프로세스 스택 최상단 주소에 있는 struct thread_info 구조체 flags에 _TIF_SIGPENDING 플래그를 써줌
4. 시그널을 받을 프로세스를 깨움
5. 시그널 ftrace 로그 출력

이어서 각 단계별로 세부 소스 코드를 분석해보겠습니다.

1 단계:  시그널 예외 처리 코드
12번째 줄 코드를 보겠습니다.
12 if (!prepare_signal(sig, t,
13 from_ancestor_ns || (info == SEND_SIG_FORCED)))
14 goto ret;

prepare_signal() 함수는 시그널을 생성하기 전 전처리 동작을 수행합니다. 시그널을 생성하기 위한 조건을 점검하는 것입니다. 시그널 생성 조건을 만족하지 않으면 ret 레이블을 실행해서 send_signal() 함수를 종료합니다.

prepare_signal() 함수는 시그널 종류에 따라 시그널 큐를 갱신하고 시그널을 처리 중인지 점검합니다. 또한, sig_ignored() 함수를 호출해서 시그널을 생성할 조건인지 확인합니다.

2단계: 시그널 펜딩 리스트를 태스크 디스크립터 시그널 필드에 써줌
16번째와 32번째 줄 코드를 보겠습니다.
16 pending = group ? &t->signal->shared_pending : &t->pending;
...
32 q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
33 if (q) {
34 list_add_tail(&q->list, &pending->list);
35 switch ((unsigned long) info) {

16번째 줄 코드를 먼저 보겠습니다. 
시그널을 받을 프로세스가 스레드 그룹에 속해 있으면 &t->signal->shared_pending 필드를 pending 지역 변수에 저장합니다. 이외 조건에서 &t->pending 필드를 pending 변수에 저장합니다.

시그널을 전달 받은 프로세스 유형에 따라 펜딩 시그널을 서로 다른 필드에 저장하는 것입니다. 
  - 유저 프로세스
pending = &t->signal->shared_pending;

  - 커널 프로세스
pending = &t-> pending;

유저 레벨에서 생성된 프로세스는 &t->signal->shared_pending 필드 혹은 커널 스레드의 경우 &t->pending 필드에 펜딩 시그널에 저장돼 있습니다.

다음 34번째 줄 코드를 보겠습니다. struct sigqueue 구조체를 동적 할당 받은 다음 &pending->list 구조체에 추가합니다.

유저 프로세스가 시그널을 생성했을 때 시그널 정보를 저장하는 루틴입니다. struct sigqueue 구조체 info 필드에 시그널 정보를 저장합니다.
36 case (unsigned long) SEND_SIG_NOINFO:
37    q->info.si_signo = sig;
38    q->info.si_errno = 0;
39    q->info.si_code = SI_USER;
40    q->info.si_pid = task_tgid_nr_ns(current,
41 task_active_pid_ns(t));
42    q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
43    break;

만약 유저 어플리케이션에서 kill() 함수를 호출했거나 리눅스 터미널에서 “kill -9 [pid]” 포멧으로 명령어를 입력했을 경우 36~43번째 줄 코드가 실행합니다.

sig는 시그널 번호를 의미하는 정수값, SI_USER는 시그널 발생 소스가 유저 레벨 프로세스란 의미입니다. 

다음은 커널에서 시그널을 설정할 때 실행하는 루틴입니다.
44 case (unsigned long) SEND_SIG_PRIV:
45    q->info.si_signo = sig;
46    q->info.si_errno = 0;
47    q->info.si_code = SI_KERNEL;
48    q->info.si_pid = 0;
49    q->info.si_uid = 0;
50 break;

역시 sig는 시그널 정수 값을 의미하고 si_code 필드에 SI_KERNEL 플래그를 저장합니다. 커널 프로세스가 시그널을 생성했다는 정보입니다.

다음 70~73번째 줄 코드를 보겠습니다.
70 out_set:
71 signalfd_notify(t, sig);
72 sigaddset(&pending->signal, sig);
73 complete_signal(sig, t, group);

complete_signal() 함수를 호출해서 시그널을 받을 프로세스를 깨웁니다.

3단계: ftrace로 시그널 생성 완료를 출력
이어서 75번째 줄 코드를 보겠습니다.
74 ret:
75 trace_signal_generate(sig, info, t, group, result);
76 return ret;

75번째 줄 코드를 실행하면 ftrace 로그 출력합니다.
signal_generate 이벤트를 켰을 때 signal_generate ftrace 메시지를 출력합니다.


signal_generate ftrace 이벤트는 다음 명령어로 킬 수 있습니다.
"echo 1 > /sys/kernel/debug/tracing/events/signal/signal_generate/enable"

다음 로그는 signal_generate 이벤트를 출력할 때 메시지입니다.
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

시그널 번호가 2이니 SIGIN 시그널을 생성하며 시그널을 받을 프로세스가 RPi_signal 이라는 정보입니다.

이번 소절에서 소스 코드 분석으로 다음과 같은 내용을 알게 됐습니다.
1. 시그널을 받을 프로세스에게 시그널 정보를 써줌
     시그널을 받을 프로세스 struct task_struct 구조체 pending 필드에 펜딩 시그널 
     정보를 써줌
2. 시그널을 받을 프로세스를 깨움

이 동작이 시그널을 만드는 핵심입니다. 이렇게 시그널을 받을 프로세스의 태스크 디스크립터에 펜딩 시그널 정보를 써 준 다음 시그널을 받을 프로세스를 깨워줘야 합니다. 

complete_signal() 함수 분석
다음 시그널 생성 3 단계인 시그널을 받을 프로세스를 깨우는 함수 흐름을 살펴보겠습니다. 
먼저 complete_signal() 함수를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 static void complete_signal(int sig, struct task_struct *p, int group)
2 {
3 struct signal_struct *signal = p->signal;
...
4 if (sig_fatal(p, sig) &&
5     !(signal->flags & SIGNAL_GROUP_EXIT) &&
6     !sigismember(&t->real_blocked, sig) &&
7     (sig == SIGKILL || !p->ptrace)) {
8
9 if (!sig_kernel_coredump(sig)) {
10 signal->flags = SIGNAL_GROUP_EXIT;
11 signal->group_exit_code = sig;
12 signal->group_stop_count = 0;
13 t = p;
14 do {
15 task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
16 sigaddset(&t->pending.signal, SIGKILL);
17 signal_wake_up(t, 1);
18 } while_each_thread(p, t);
19 return;
20 }
21 }
22
23 signal_wake_up(t, sig == SIGKILL);
24 return;
25 }

complete_signal() 함수 주요 동작은 2단계로 분류할 수 있습니다.
  - 시그널을 받을 프로세스의 struct thread_info 구조체 flags 필드에 _TIF_SIGPENDING 플래그를 써주기
  - 시그널을 받을 프로세스를 깨우기

위 함수는 프로세스가 스레드 그룹에 속했을 경우와 단일 프로세스일 때 다른 조건으로 실행합니다.

4~21번째 줄 코드는 프로세스가 스레드 일 경우 스레드 그룹에 속한 모든 프로세스를 깨웁니다.
이외 조건에서 커널 스레드일 경우 23번째 줄 코드와 같이 signal_wake_up() 함수를 호출합니다.

시그널을 받을 프로세스의 struct thread_info 구조체 flags 필드에 _TIF_SIGPENDING 플래그를 써주기
이어서 signal_wake_up() 함수를 볼 차례입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched/signal.h]
1 static inline void signal_wake_up(struct task_struct *t, bool resume)
2 {
3 signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0);
4 }

두 번째 인자에 따라 다른 인자로 signal_wake_up_state() 함수를 호출합니다.

다음 signal_wake_up_state() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c]
1 void signal_wake_up_state(struct task_struct *t, unsigned int state)
2 {
3 set_tsk_thread_flag(t, TIF_SIGPENDING);
4
5 if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
6 kick_process(t);
7 }

3번째 줄 코드에서는 set_tsk_thread_flag() 함수를 호출합니다. 

set_tsk_thread_flag() 함수를 볼까요?
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/sched.h]
static inline void set_tsk_thread_flag(struct task_struct *tsk, int flag)
{
set_ti_thread_flag(task_thread_info(tsk), flag);
}

프로세스 스택 최상단 주소에 있는 struct thread_info flags 필드를 _ TIF_SIGPENDING 플래그로 설정합니다. 시그널을 받을 프로세스에게 시그널이 전달됐다는 정보를 써주는 것입니다.

다음 5번째 줄 코드에서는 wake_up_state() 함수를 호출합니다.

시그널을 받을 프로세스를 깨우기
마지막으로 wake_up_state() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/core.c]
1 int wake_up_state(struct task_struct *p, unsigned int state)
2 {
3 return try_to_wake_up(p, state, 0);
4 }

try_to_wake_up() 함수를 호출해서 시그널을 받을 프로세스를 깨웁니다.

이번 소절에서는 시그널을 생성하는 역할을 수행하는 __send_signal() 함수 코드를 분석했습니다. 코드 분석으로 알게 된 내용을 정리해볼까요?

1단계: 시그널을 받을 프로세스에게 시그널 정보를 써줌
  - struct task_struct: signal 혹은 signal->shared_pending 필드에 시그널 
정보를 저장
 - struct thread_info: _TIF_SIGPENDING 플래그 저장

2단계: 시그널을 받을 프로세스를 깨워줌 
  - signal_wake_up() 함수를 호출해 시그널을 받을 프로세스를 깨움 
여기까지 시그널을 생성하는 세부 동작을 알아봤습니다.

이어서 다음 절에서는 시그널을 프로세스가 받아 처리하는 과정을 살펴보겠습니다.

"궁금하신 점이 있으면 댓글 달아 주세요. 아는 한 성실히 답변 올려드리겠습니다."

덧글

댓글 입력 영역