Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

88258
1323
114590


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

누군가 ‘시그널을 생성하는 핵심 함수가 무엇인가?’ 라고 질문을 한다면 __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() 함수를 호출해 시그널을 받을 프로세스를 깨움 
여기까지 시그널을 생성하는 세부 동작을 알아봤습니다.

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

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

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



핑백

덧글

댓글 입력 영역