커널에서 시그널 서브시스템의 핵심 동작은 다음과 같다.
* 시그널 전송: 시그널을 받은 프로세스에게 펜딩 시그널 정보를 써주고 시그널을 받을 프로세스를 깨운다.
* 시그널 받기: 깨어난 프로세스는 펜딩 시그널을 받아 시그널에 대한 후속 처리를 한다.
이번 시간에는 펜딩 시그널 정보를 써주고(시그널 생성), 펜딩된 시그널을 프로세스(시그널 받음)는 읽는 과정에
초점을 맞춰 소스를 분석해보자.
__send_signal() 함수 분석
먼저 __send_signal() 함수를 보자.
https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
enum pid_type type, int from_ancestor_ns)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;
int ret = 0, result;
...
/* 태스크 디스크립터의 &t->signal->shared_pending 혹은 &t->pending를
sigpending 구조체인 pending에 저장한다.
여기서 중요한 시사점이 있다. 전달되는 시그널의 유형에 따라 다른 필드에 펜딩 시그널을 저장한다는 점이다.
그룹에 전달되는 시그널이면: &t->signal->shared_pending
한 개 프로세스에 전달되는 시그널이면: &t->pending */
pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
...
/ * sigqueue 구조체인 q란 포인터형 지역 변수를 통해 동적 메모리를 할당 받는다. */
q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
if (q) {
/ * &pending->list 연결 리스트에 &q->list를 추가한다.
list_add_tail(&q->list, &pending->list);
이 코드에서 중요한 포인트는 시그널의 유형에 따라 서로 다른 구조에 펜딩 시그널을 써준다는 점이다.
* 그룹에 전달되는 시그널이면: &t->signal->shared_pending
* 한 개 프로세스에 전달되는 시그널이면: &t->pending */
이어서 시그널을 프로세스가 받는 과정에서 펜딩 시그널을 읽는 코드를 보자.
dequeue_signal() 함수 분석
dequeue_signal() 함수의 구현부의 코드를 보자.
https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c
01 int dequeue_signal(struct task_struct *tsk, sigset_t *mask, siginfo_t *info)
02 {
03 bool resched_timer = false;
04 int signr;
05
06 /* We only dequeue private signals from ourselves, we don't let
07 * signalfd steal them
08 */
09 signr = __dequeue_signal(&tsk->pending, mask, info, &resched_timer);
10 if (!signr) {
11 signr = __dequeue_signal(&tsk->signal->shared_pending,
12 mask, info, &resched_timer);
위 코드를 펜딩 시그널을 읽는 관점으로 분석하면;
* 09번째 줄: '&tsk->pending'에 있는 연결 리스트(단일 프로세스에게 시그널이 전달됐을 경우)를 통해 펜딩 시그널 정보를 읽는다.
* 11~12번째 줄: '&tsk->pending'에 있는 연결 리스트가 비어 있다면(그룹으로 시그널이 전달됐을 경우),
&tsk->signal->shared_pending에 있는 연결 리스트를 통해 펜딩 시그널 정보를 읽는다.
TRACE32 펜딩 시그널 디버깅 하기
이번에는 펜딩 시그널(그룹에게 전달된 시그널)을 받은 프로세스의 자료 구조를 디버깅을 통해 알아보자.
&t->pending->list
(struct task_struct *) (struct task_struct*)0xebd99200 = 0xEBD99200 -> (
(long int) state = 0x0,
(void *) stack = 0xEBDA4000,
(atomic_t) usage = ((int) counter = 0x6),
(unsigned int) flags = 0x00404100,
...
(sigset_t) blocked = ((long unsigned int [2]) sig = (0x0, 0x0)),
(sigset_t) real_blocked = ((long unsigned int [2]) sig = (0x0, 0x0)),
(sigset_t) saved_sigmask = ((long unsigned int [2]) sig = (0x0, 0x0)),
(struct sigpending) pending = (
(struct list_head) list = (
(struct list_head *) next = 0xEBD9984C,
(struct list_head *) prev = 0xEBD9984C),
(sigset_t) signal = ((long unsigned int [2]) sig = (0x0, 0x0))),
위에서 보이듯 pending->list 의 next와 prev 필드가 값은 주소를 저장하고 있다.
즉, 링크드 리스트가 비어 있다는 이야기이다.
이번에는 signal->shared_pending->list 를 확인해보자.
&t->signal->shared_pending->list
(struct task_struct *) (struct task_struct*)0xebd99200 = 0xEBD99200 -> (
(long int) state = 0x0,
(void *) stack = 0xEBDA4000,
(atomic_t) usage = ((int) counter = 0x6),
(unsigned int) flags = 0x00404100,
...
(struct signal_struct *) signal = 0xEBC5F440 -> (
(atomic_t) sigcnt = ((int) counter = 0x10),
(atomic_t) live = ((int) counter = 0x0E),
(int) nr_threads = 0x0E,
(struct list_head) thread_head = ((struct list_head *) next = 0xEBD99764, (struct list_head *)
(wait_queue_head_t) wait_chldexit = ((spinlock_t) lock = ((struct raw_spinlock) rlock = ((arch
(struct task_struct *) curr_target = 0xEBD99200,
(struct sigpending) shared_pending = (
(struct list_head) list = (
(struct list_head *) next = 0xF1E4A510 -> (
(struct list_head *) next = 0xEBC5F470 -> (
(struct list_head *) next = 0xF1E4A510,
(struct list_head *) prev = 0xF1E4A510),
(struct list_head *) prev = 0xEBC5F470),
(struct list_head *) prev = 0xF1E4A510),
(sigset_t) signal = ((long unsigned int [2]) sig = (0x0100, 0x0))),
(int) group_exit_code = 0x9,
(int) notify_count = 0x0,
signal->shared_pending->list 필드를 보니 링크드 리스트에 뭔가 주소(0xF1E4A510, 0xEBC5F470)가 보인다.
&t->signal->shared_pending->list 주소를 통해 펜딩 시그널의 정체인 struct sigqueue를 확인해보자.
동작 원리는 아래 코드와 같다.
https://elixir.bootlin.com/linux/v4.19.30/source/kernel/signal.c
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
enum pid_type type, int from_ancestor_ns)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;
int ret = 0, result;
...
q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
if (q) {
/ * &pending->list 연결 리스트에 &q->list를 추가한다.
list_add_tail(&q->list, &pending->list);
v.v % container_of(0xF1E4A510,struct sigqueue,list)
(struct sigqueue *) container_of(0xF1E4A510,struct sigqueue,list) = 0xF1E4A510 = -> (
(struct list_head) list = ((struct list_head *) next = 0xEBC5F470
(int) flags = 0 = 0x0 = '....',
(siginfo_t) info = (
(int) si_signo = 9 = 0x9
(int) si_errno = 0 = 0x0
(int) si_code = 0 = 0x0
(union) _sifields = ((int [29]) _pad = ([0] ...
(struct user_struct *) user = 0xF1F35900
출력 결과로 9번 SIG_KILL 시그널이 전달됐다는 사실을 알 수 있다.
이제껏 확인한 디버깅 정보의 주인공은 PID가 6269인 "mediaserver" 프로세스이다.
이번에는 프로세스 그룹에 속한 다른 프로세스의 목록을 보자.
crash> ps -g 6269
PID: 1218 TASK: ebd99200 CPU: 0 COMMAND: "mediaserver"
PID: 1461 TASK: eac70900 CPU: 0 COMMAND: "Binder:1218_1"
PID: 1483 TASK: eac95100 CPU: 1 COMMAND: "Binder:1218_2"
PID: 1487 TASK: eb82bf00 CPU: 3 COMMAND: "HwBinder:1218_1"
PID: 1489 TASK: ebc81200 CPU: 3 COMMAND: "Binder:1218_3"
PID: 6269 TASK: eb4ee300 CPU: 1 COMMAND: "MediaClock"
PID: 6270 TASK: d1a4a400 CPU: 3 COMMAND: "NuPlayerDriver "
PID: 6271 TASK: d1a49b00 CPU: 2 COMMAND: "generic"
PID: 6273 TASK: d1918000 CPU: 1 COMMAND: "NuPlayerRendere"
PID: 7197 TASK: d23be300 CPU: 0 COMMAND: "MediaClock"
PID: 7198 TASK: d23bda00 CPU: 1 COMMAND: "NuPlayerDriver "
PID: 7199 TASK: d219a400 CPU: 3 COMMAND: "generic"
PID: 7200 TASK: d219bf00 CPU: 0 COMMAND: "Binder:1218_4"
PID: 7210 TASK: d21a8900 CPU: 3 COMMAND: "NuPlayerRendere"
위 프로세스 목록 중에서 태스크 디스크립터가 d1a4a400인 "NuPlayerDriver " 프로세스의 펜딩 시그널 정보를 확인해보자.
(struct task_struct *) (struct task_struct*)0xd1a4a400 = 0xD1A4A400 -> (
(long int) state = 0x0,
(void *) stack = 0xE272C000,
(atomic_t) usage = ((int) counter = 0x3),
(unsigned int) flags = 0x40404040,
...
(struct nsproxy *) nsproxy = 0xC1E1A468,
(struct signal_struct *) signal = 0xEBC5F440 -> (
(atomic_t) sigcnt = ((int) counter = 0x10),
(atomic_t) live = ((int) counter = 0x0E),
(int) nr_threads = 0x0E,
(struct list_head) thread_head = ((struct list_head *) next = 0xEBD99764, (struct list_head *)
(wait_queue_head_t) wait_chldexit = (
(spinlock_t) lock = ((struct raw_spinlock) rlock = ((arch_spinlock_t) raw_lock = ((u32) sloc
(struct list_head) task_list = ((struct list_head *) next = 0xEBC5F464, (struct list_head *)
(struct task_struct *) curr_target = 0xEBD99200,
(struct sigpending) shared_pending = (
(struct list_head) list = (
(struct list_head *) next = 0xF1E4A510 -> (
(struct list_head *) next = 0xEBC5F470 -> (
(struct list_head *) next = 0xF1E4A510,
(struct list_head *) prev = 0xF1E4A510),
(struct list_head *) prev = 0xEBC5F470),
(struct list_head *) prev = 0xF1E4A510),
(sigset_t) signal = ((long unsigned int [2]) sig = (0x0100, 0x0))),
(int) group_exit_code = 0x9,
(int) notify_count = 0x0,
역시 0xF1E4A510와 0xEBC5F470 주소로 보아 &t->signal->shared_pending->list 주소를 통해 펜딩 시그널이 전달된 것을 알 수 있다.
# Reference: For more information on 'Linux Kernel';
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2

최근 덧글