do_exit() 함수 분석
do_exit() 함수로 커널이 프로세스를 종료 시키는 세부 동작 못지 않게 프로세스가 종료되는 흐름을 파악하는 것이 중요합니다. 그 이유는 무엇일까요? 

유저 어플리케이션 프로세스나 커널 프로세스가 예외 상황에서 의도하지 않게 종료해서 문제가 발생하는 경우가 있습니다. 이런 문제를 만났을 때 커널 어느 함수부터 분석을 해야할 지 결정할 수 있습니다.

이번에는 do_exit() 함수가 어떻게 실행되는지 알아봅시다.

- exit() 시스템 콜 실행
유저 어플리케이션 프로세스가 자신을 종료하려면 리눅스 저수준 함수로 exit() 함수를 호출합니다. 이 때 시스템 콜을 발생시킨 다음 sys_group_exit() 시스템콜 핸들러를 실행합니다.

- 다른 프로세스가 프로세스 종료 시그널을 전달했을 경우
프로세스 종료 시그널을 다른 프로세스가 전달했을 경우에도 do_exit() 함수가 호출될 수 있습니다. 또한 커널 함수로 send_signal() 함수를 호출하면 특정 프로세스 종료 시그널을 전달할 수 있습니다. 프로세스 종료 시그널을 받은 프로세스는 do_exit() 함수를 실행해서 자신을 종료합니다.

이렇게 프로세스를 종료할 때 do_exit() 함수를 호출합니다. 이 함수는 프로세스가 쓰는 리소스를 해제하는 역할을 수행합니다.

먼저 do_exit() 함수에 전달되는 인자와 반환값을 확인합시다.
void __noreturn do_exit(long code);

do_exit() 함수는 프로세스에 대한 리소스를 정리한 후 do_task_dead() 함수를 호출한 후 schedule() 함수를 실행합니다. 따라서 do_exit() 함수를 끝까지 실행하지 않습니다. 그래서 함수 선언부에 __noreturn 이란 매크로로 지정한 것입니다.

do_exit() 함수에서 do_task_dead() 함수를 호출해서 schedule() 함수 실행으로 함수 흐름을 마무리하는 이유는 무엇일까요?

그 동안 프로세스 실행 흐름에 대해 살펴봤듯이 프로세스는 자신의 스택 공간에서 함수를 실행합니다. do_exit() 함수는 프로세스 스택 공간에서 실행을 시작하는 것입니다.

그런데 프로세스 리소스를 해제하는 함수를 호출하면서 태스크 디스크립터 대부분 멤버와 스택 공간까지 해제합니다. do_exit() 함수 실행을 끝낸 다음에 복귀를 못하는 것입니다. 따라서 do_task_dead()/schedule() 함수를 호출해서 do_exit() 함수 실행을 마무리합니다.

이번에는 이 함수에 전달하는 code란 인자를 점검합시다. code라는 인자는 프로세스 종료 코드를 의미합니다.

만약 kill -9 라는 명령어로 프로세스를 종료하면 code 인자로 9가 전달됩니다.

다음은 Trace32로 do_exit() 함수를 호출했을 때 브레이크 포인트를 걸고 본 디버깅 정보입니다.
-000|do_exit(?)
-001|do_group_exit(exit_code = 9)
-002|get_signal(?)
-003|do_notify_resume(regs = 0A4233EC0, thread_flags = 9)
-004|work_pending(asm)

"kill -9 [PID]" 명령어로 프로세스를 종료하니 exit_code 인자에 9가 전달된 것입니다.  

do_exit() 함수는 다음 순서로 프로세스를 종료합니다.
- 이미 프로세스가 종료 중 인데 다시 do_exit() 함수를 호출하는지 점검합니다.
- 프로세스 리소스(파일 디스크립터, 가상 메모리, 시그널) 등등을 해제합니다.
- 부모 프로세스에게 자신이 종료되고 있다고 알립니다.
- 프로세스 실행 상태를 struct task_struct state 멤버에 TASK_DEAD 로 설정합니다.
- do_task_dead() 함수를 호출해서 schedule() 함수를 실행하여 스케줄링 됩니다..

분석할 do_exit() 함수 코드는 다음과 같습니다.
1 void __noreturn do_exit(long code)
2 {
3 struct task_struct *tsk = current;
4 int group_dead;
...
5 if (unlikely(in_interrupt()))
6 panic("Aiee, killing interrupt handler!");
7 if (unlikely(!tsk->pid))
8 panic("Attempted to kill the idle task!");
...
9 if (unlikely(tsk->flags & PF_EXITING)) {
10 pr_alert("Fixing recursive fault but reboot is needed!\n");
11
12 tsk->flags |= PF_EXITPIDONE;
13 set_current_state(TASK_UNINTERRUPTIBLE);
14 schedule();
15 }
16
17 exit_signals(tsk);  /* sets PF_EXITING */
...
18 exit_mm();
...
19 exit_files(tsk);
20 exit_fs(tsk);
...
21 do_task_dead();
...
22}

먼저 다음 코드를 보겠습니다.
5 if (unlikely(in_interrupt()))
6 panic("Aiee, killing interrupt handler!");
7 if (unlikely(!tsk->pid))
8 panic("Attempted to kill the idle task!");

특이한 상황에 부팅 도중 init 프로세스가 중료하는 경우가 있습니다. 이 때 다음와 같은 커널 로그와 함께 커널 패닉이 발생합니다.
[  837.981513 / 10-11 11:11:00.958][4] Kernel panic - not syncing: Attempted to kill init! exitcode=0x0000000b
[  837.981513 / 10-11 11:11:00.958][4] 
[  837.981547 / 10-11 11:11:00.958][6] CPU6: stopping
[  837.981571 / 10-11 11:11:00.958][6] CPU: 6 PID: 339 Comm: mmc-cmdqd/0 Tainted: P        W  O   3.18.31-perf #1

init 프로세스는 유저 레벨 프로세스를 생성하고 관장하는 역할을 수행합니다. 그래서 init 프로세스가 불의의 상황으로 종료하면 강제로 커널 패닉을 유발합니다.
보통 리눅스 커널 버전을 업그레이드 후 root 파일 시스템이나 다바이스 노드를 생성 못했을 때 init 프로세스가 종료됩니다.
 
다음 코드를 보겠습니다.
9 if (unlikely(tsk->flags & PF_EXITING)) {
10 pr_alert("Fixing recursive fault but reboot is needed!\n");
11
12 tsk->flags |= PF_EXITPIDONE;
13 set_current_state(TASK_UNINTERRUPTIBLE);
14 schedule();
15 }

프로세스가 종료 중이면 do_exit() 함수에서 호출되는 exit_signals() 함수에서  태스크 디스크립터 struct task_struct flags를 PF_EXITING로 설정합니다.
void exit_signals(struct task_struct *tsk)
{
...
tsk->flags |= PF_EXITING;

프로세스가 종료 중인데 다시 do_exit() 함수를 실행해서 중복 종류를 시도하는 경우 예외 처리 코드입니다.

프로세스 태스크 디스크립터 멤버인 state에 TASK_UNINTERRUPTIBLE로 상태를 지정하고 schedule() 함수를 호출해서 다른 프로세스가 동작하도록 합니다.

다음 코드를 보겠습니다.
17 exit_signals(tsk);  /* sets PF_EXITING */

프로세스 struct task_strtuct.flags를 PF_EXITING 으로 변경합니다.
종료할 프로세스가 처리할 시그널이 있으면 retarget_shared_pending() 함수를 실행해서 시그널을 대신 처리할 프로세스를 선정합니다.

다음 코드를 보겠습니다.
18 exit_mm();

프로세스의 메모리 디스크립터(struct mm_struct) 리소스를 해제하고 메모리 디스크립터 사용 카운트를 1만큼 감소합니다.

다음 코드를 봅시다.
19 exit_files(tsk);
20 exit_fs(tsk);

프로세스에 쓰고 있는 파일 디스크립터 정보를 해제합니다.

schedule() 함수를 호출하면, finish_task_switch() 함수에서 do_exit() 함수로 종료하는 프로세스의 태스크 디스크립터와 스택 메모리를 해제합니다.
static struct rq *finish_task_switch(struct task_struct *prev)
__releases(rq->lock)
{
...
if (unlikely(prev_state == TASK_DEAD)) {
if (prev->sched_class->task_dead)
prev->sched_class->task_dead(prev);

kprobe_flush_task(prev);

/* Task is done with its stack. */
put_task_stack(prev);

put_task_struct(prev);
}

put_task_stack() 함수를 호출해서 프로세스 스택 메모리 공간을 해제하여 커널 메모리 공간에 반환합니다. 바로 put_task_struct() 함수를 실행해서 프로세스를 표현하는 자료구조인 struct task_struct 가 위치한 메모리를 해제합니다.

참고로, finish_task_switch() 함수에 ftrace 필터를 걸고 이 함수가 호출되는 함수 흐름을 확인하면 다음과 같습니다.
->transport-8537  [002] d..3 714.329199: finish_task_switch+0x28/0x20c <-__schedule+0x2bc/0x84c
     ->transport-8537  [002] d..3   714.329206: <stack trace>
 => schedule+0x3c/0x9c
 => schedule_timeout+0x288/0x378
 => unix_stream_read_generic+0x588/0x754
 => unix_stream_recvmsg+0x70/0x94
 => sock_read_iter+0xd4/0x100
 => new_sync_read+0xd8/0x11c
 => vfs_read+0x118/0x178
 => SyS_read+0x60/0xc0
 => ret_fast_syscall_+0x4/0x28

프로세스 스케줄링 동작 때 호출되는 함수입니다.


이전 시간까지 유저 프로세스와 커널 프로세스가 어떤 흐름으로 생성되는지 살펴봤습니다.
둘 다 _do_fork() 함수를 호출한다는 사실을 알 수 있습니다.

프로세스 생성 시 공통으로 실행하는 _do_fork() 함수 코드를 분석하면서 커널이 어떻게 프로세스를 생성하는지 살펴봅니다.

4.6.1 _do_fork() 함수 분석
_do_fork() 함수을 분석하기 앞서 이 함수 동작을 분류해봅시다.

1단계: 프로세스 생성
copy_process() 함수를 호출해서 프로세스를 생성합니다. 프로세스를 생성하는 세부 동작을 파악하려면 copy_process() 함수를 분석할 필요가 있습니다.

2단계: 생성한 프로세스 실행 요청
copy_process() 함수를 호출해서 프로세스 생성을 마쳤으면 wake_up_new_task() 함수를 호출해서 프로세스를 바로 깨웁니다. 프로세스를 깨운다는 의미는 스케줄러에게 프로세스 실행 요청을 하는 것입니다.

_do_fork() 함수 전체 흐름에 대해 알아봤으니 이번에는 _do_fork() 함수에 전달되는 인자와 반환값을 살펴봅시다.
long _do_fork(unsigned long clone_flags,
      unsigned long stack_start,
      unsigned long stack_size,
      int __user *parent_tidptr,
      int __user *child_tidptr,
      unsigned long tls)

_do_fork() 함수는 프로세스를 생성한 다음 생성한 프로세스의 PID를 반환합니다.

_do_fork() 함수 인자와 반환값을 알아봤으니 이제 소스 리뷰를 할 차례입니다.
1  long _do_fork(unsigned long clone_flags,
2       unsigned long stack_start,
3       unsigned long stack_size,
4       int __user *parent_tidptr,
5       int __user *child_tidptr,
6       unsigned long tls)
7 {
8 struct task_struct *p;
9 int trace = 0;
10 long nr;
...
11
12 p = copy_process(clone_flags, stack_start, stack_size,
13  child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
14 add_latent_entropy();
...
15
16 if (!IS_ERR(p)) {
17 struct completion vfork;
18 struct pid *pid;
19
20 trace_sched_process_fork(current, p);
21
22 pid = get_task_pid(p, PIDTYPE_PID);
23 nr = pid_vnr(pid);
24
...
25 wake_up_new_task(p);
...
26 }
27 return nr;
28}

12번째 줄 코드를 봅시다.
12 p = copy_process(clone_flags, stack_start, stack_size,
13  child_tidptr, NULL, trace, tls, NUMA_NO_NODE);

copy_process() 란 함수를 호출해서 부모 프로세스의 메모리 및 시스템 정보를 생성하려는 자식 프로세스에게 복사합니다. _do_fork() 함수에 전달된 clone_flags와 stack_stack 그리고 stack_size 인자를 copy_process() 함수에 전달합니다.

다음 16번째 줄 조건문 코드를 보겠습니다.
16 if (!IS_ERR(p)) {

copy_process() 함수를 실행하면 이 함수는 프로세스를 식별하는 태스크 디스크립터를 p이란 포인터 변수를 반환합니다. 이 태스크 디스크립터에 오류가 있는지 점검하는 코드가 16번째 줄 코드입니다. 17~25번째 줄 코드 구간은 태스크 디스크립터에 오류가 없으면 실행합니다.

다음 20~22번째 줄 코드를 봅시다.
20 trace_sched_process_fork(current, p);
21
22 pid = get_task_pid(p, PIDTYPE_PID);
23 nr = pid_vnr(pid);

20번째 줄 코드는 sched_process_fork 이란 ftrace 이벤트를 켰을 때 실행하며 다음과 같은 ftrace 로그를 확인할 수 있습니다.
kthreadd-2 [003] ....  3495.071290: copy_process0x14/0x17d8 <-_do_fork+0xb0/0x3ec
kthreadd-2 [003] ....  3495.071304: <stack trace>
 => kthreadd+0x1dc/0x268
 => ret_from_fork+0x14/0x28
kthreadd-2     [003] ....  3495.071381: sched_process_fork: comm=kthreadd pid=2 child_comm=kthreadd child_pid=17193
kworker/u8:0-17193 [002] d...  3495.071431: sched_switch: prev_comm=kthreadd prev_pid=17193 prev_prio=120 prev_state=D ==> next_comm=swapper/2 next_pid=0

다음은 22~23번째 줄 코드입니다.
22 pid = get_task_pid(p, PIDTYPE_PID);
23 nr = pid_vnr(pid);

pid를 계산해서 nr이란 지역 변수에 저장합니다.

_do_fork() 함수는 copy_process() 함수를 호출해서 프로세스를 생성하고 프로세스 PID를 커널로부터 할당 받아 반환합니다. 프로세스 생성 핵심 함수는 copy_process() 인데 다음에 분석이 이어집니다.

4.6.2 copy_process() 함수 분석
프로세스를 생성하는 핵심 동작은 copy_process() 함수에서 수행합니다. 대부분 부모 프로세스에 있는 리소스를 복사하는 흐름입니다.

분석할 copy_process() 함수 코드는 다음과 같습니다.
1 static __latent_entropy struct task_struct *copy_process(
2 unsigned long clone_flags,
3 unsigned long stack_start,
4 unsigned long stack_size,
5 int __user *child_tidptr,
6 struct pid *pid,
7 int trace,
8 unsigned long tls,
9 int node)
10 {
11 int retval;
12 struct task_struct *p;
...
13 retval = -ENOMEM;
14 p = dup_task_struct(current, node);
15 if (!p)
16 goto fork_out;
...
17 /* Perform scheduler related setup. Assign this task to a CPU. */
18 retval = sched_fork(clone_flags, p);
19 if (retval)
20 goto bad_fork_cleanup_policy;
21
...
22 retval = copy_files(clone_flags, p);
23 if (retval)
24 goto bad_fork_cleanup_semundo;
25 retval = copy_fs(clone_flags, p);
26 if (retval)
27 goto bad_fork_cleanup_files;
28 retval = copy_sighand(clone_flags, p);
29 if (retval)
20 goto bad_fork_cleanup_fs;

먼저 14번째 줄 코드를 봅시다.
14 p = dup_task_struct(current, node);
15 if (!p)
16 goto fork_out;

생성한 프로세스의 태스크 디스크립터인 struct task_struct 구조체와 커널 프로세스 스택 공간을 할당합니다. 

18번째 줄 코드입니다.
18 retval = sched_fork(clone_flags, p);
19 if (retval)
20 goto bad_fork_cleanup_policy;

태스크 디스크립터 struct task_struct 구조체에서 스케줄링 관련 초기화를 진행합니다.

다음 22~24번째 줄 코드를 분석하겠습니다.
22 retval = copy_files(clone_flags, p);
23 if (retval)
24 goto bad_fork_cleanup_semundo;

프로세스의 파일 디스크립터 관련 내용(파일 디스크립터, 파일 디스크립터 테이블)을 초기화하는 동작입니다. 부모 struct file_struct 구조체 내용을 자식 프로세스에게 복사합니다.

만약 프로세스 생성 플래그 중 CLONE_FILES 로 프로세스를 생성했을 경우 참조 카운트만 증가합니다.

다음 25번째 줄 코드를 확인하겠습니다.
25 retval = copy_fs(clone_flags, p);
26 if (retval)
27 goto bad_fork_cleanup_files;

프로세스의 파일 디스크립터 관련 내용(파일 디스크립터, 파일 디스크립터 테이블)을 초기화하는 동작입니다. 부모 struct file_struct 구조체 내용을 자식 프로세스에게 복사합니다.

다음 분석할 코드는 29번째 줄입니다.
28 retval = copy_sighand(clone_flags, p);
29 if (retval)
20 goto bad_fork_cleanup_fs;

프로세스가 등록한 시그널 핸들러 정보인 struct sighand_struct 구조체를 생성해서 복사합니다.

이번 절 까지 프로세스를 생성하는 함수를 살펴 봤습니다. 다음 절에서는 프로세스를 종료할 때 실행하는 do_exit() 함수를 분석하겠습니다.


4.5 커널 스레드
이전절까지 유저 영역에서 실행한 프로세스가 어떻게 실행됐는지 점검했습니다.
이번에는 커널 공간에서만 실행하는 커널 프로세스가 어떻게 생성하는지 알아봅시다.

4.5.1 커널 스레드란
커널 프로세스는 커널 공간에서만 실행하는 프로세스를 의미하며 대부분 커널 스레드 형태로 구동합니다. 커널 스레드는 리눅스 시스템 프로그래밍에서 데몬과 비슷한 일을 합니다. 데몬과 커널 스레드는 백그라운드 작업으로 실행하면서 시스템 메모리나 전원을 제어하는 역할을 수행합니다. 

커널 스레드는 커널 내부에서 백그라운드로 구동하면서 커널 시스템에 도움을 주는 역할을 수행합니다. 데몬과 커널 스레드의 차이점은 보통 유저 영역와 시스템 콜을 받지 않고 동작한다는 점입니다. 리눅스 커널 개발자나 드라이버 개발자가 구현하는 경우가 많습니다.

정리하면 커널 스레드는 다음과 같은 3가지 특징이 있습니다.
1. 커널 스레드는 커널 공간에서만 실행하며 유저 공간과 상호작용을 하지 않습니다.
2. 커널 스레드는 실행, 휴면 등 모든 동작을 커널에서 직접 제어 관리합니다.
3. 대부분 커널 스레드는 시스템이 부팅할 때 생성되고 시스템이 종료할 때까지 백그라운드로 실행합니다.

4.5.2 커널 스레드 종류
리눅스 커널에서 구동중인 대표적인 커널 스레드를 알아보기 위해 라즈베리파이에서 다음 명령어를 입력합시다.
root@raspberrypi:/home/pi# ps -ejH
1 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
2     0     2     0     0 ?           -1 S        0   0:00 [kthreadd]
3     2     4     0     0 ?           -1 I<       0   0:00  \_ [kworker/0:0H]
4     2     7     0     0 ?           -1 S        0   0:00  \_ [ksoftirqd/0]
5     2    10     0     0 ?           -1 S        0   0:00  \_ [migration/0]
6     2    11     0     0 ?           -1 S        0   0:00  \_ [cpuhp/0]
7     2    12     0     0 ?           -1 S        0   0:00  \_ [cpuhp/1]
8     2    13     0     0 ?           -1 S        0   0:00  \_ [migration/1]
9     2    14     0     0 ?           -1 S        0   0:00  \_ [ksoftirqd/1]
..
 10   2    66     0     0 ?           -1 S        0   0:00  \_ [irq/92-mmc1]

“ps –ejH” 명령어를 입력하면 출력하는 정보가 커널 스레드 목록입니다. 각 정보에서 보이는 커널 스레드를 소개하겠습니다.

kthreadd 프로세스
    0     2     0     0 ?           -1 S        0   0:00 [kthreadd]

모든 커널 스레드의 부모 프로세스입니다.
스레드 핸들 함수는 kthreadd() 이며 커널 스레드를 생성하는 역할을 주로 수행합니다.

워커 스레드
    2     4     0     0 ?           -1 I<       0   0:00  \_ [kworker/0:0H]

워크큐에서 요청된 워크를 실행하는 프로세스입니다.
스레드 핸들 함수는 worker_thread() 이며 process_one_work() 함수를 호출해서 워크를 실행하는 역할을 수행합니다.

ksoftirqd 프로세스
    2     7     0     0 ?           -1 S        0   0:00  \_ [ksoftirqd/0]

ksoftirqd 스레드는 smp_boot 스레드이며 프로세스 이름 가장 오른쪽에 실행 중인 CPU 번호를 볼 수 있습니다.

ksoftirqd 스레드 핸들 함수는 run_ksoftirqd() 인데 주로 Soft IRQ 서비스 요청을 _do_softirq() 함수에서 처리하지 못했을 때 실행합니다.

irq/92-mmc1 스레드
    2    66     0     0 ?           -1 S        0   0:00  \_ [irq/92-mmc1]

IRQ 스레드라고 하며 인터럽트 후반부 처리용 프로세스입니다. 대표적인 커널 스레드를 소개했으니 다음 절에서 커널 스레드를 어떻게 생성하는지 알아봅시다.

4.5.3 커널 스레드는 어떻게 생성하나?
이번에는 커널 스레드 생성 요청과 커널 스레드를 생성하는 코드 흐름을 살펴봅시다. 커널 스레드를 생성하려면 다음과 같이 kthread_create() 함수를 호출해야 합니다.
1 #define kthread_create(threadfn, data, namefmt, arg...) \
2 kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
3
4 struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
5    void *data, int node,
6    const char namefmt[],
7    ...)

먼저 이 함수에 전달하는 인자부터 살펴봅시다.

threadfn
스레드 핸들 함수입니다. 커널 스레드 동작을 살펴보기 위해서 스레드 핸들 함수를 분석해야 합니다.

data
스레드 핸들 함수로 전달하는 매개 변수입니다. 주로 주소를 전달하며 스레드를 식별하는 구조체 주소를 전달합니다.

namefmt
커널 스레드 이름을 전달하는 역할을 수행합니다.

위와 같이 커널 스레드 생성 시 전달하는 인자에 대한 이해를 돕기 위해 다음 코드를 같이 봅시다.
1 long vhost_dev_set_owner(struct vhost_dev *dev)
2 {
3 struct task_struct *worker;
4 int err;
...
5 /* No owner, become one */
6 dev->mm = get_task_mm(current);
7 worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid);
...
8 static int vhost_worker(void *data)
9 {
10 struct vhost_dev *dev = data;
11 struct vhost_work *work, *work_next;
12 struct llist_node *node;
13
14 for (;;) {
15 set_current_state(TASK_INTERRUPTIBLE);
16
...
17 llist_for_each_entry_safe(work, work_next, node, node) {
18 clear_bit(VHOST_WORK_QUEUED, &work->flags);
19 __set_current_state(TASK_RUNNING);
20 work->fn(work);
21 if (need_resched())
22 schedule();
23 }
24 }

7번 째 줄 코드를 보면 kthread_create() 함수 첫 번째 인자로 vhost_worker 스레드 핸들 함수이름을 지정합니다.
7 worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid);

2번째 인자로 dev 변수를 지정하는데 1번째 줄 코드를 보면 dev가 어떤 구조체 인지 알 수 있습니다. struct vhost_dev 란 구조체 주소를 2번째 인자로 전달하는 겁니다.

이런 매개 변수와 같은 역할을 보통 디스크립터라고도 부릅니다. 3번째 인자로 "vhost-%d"를 전달하는데 이는 커널 스레드 이름을 지정하는 겁니다.

이번에는 스레드 핸들 함수로 전달되는 매개 변수를 점검합니다.

8번째 줄 코드를 보면 vhost_worker() 함수로 void 타입 포인터인 data를 전달합니다.
8 static int vhost_worker(void *data)
9 {
10 struct vhost_dev *dev = data;

커널 스레드를 생성할 때 두 번째 인자로 struct vhost_dev 구조체인 dev를 지정했습니다.

vhost_worker() 스레드 핸들 함수 인자로 매개 변수 인자를 전달하며 10번째 줄 코드와 같이 void 타입 data 포인터를 struct vhost_dev 구조체로 형 변환(캐스팅)합니다.

리눅스 커널에서는 이런 방식으로 스레드를 관리하는 유일한 구조체를 매개 변수로 전달합니다.

커널 스레드 생성은 2단계로 나눠서 분류할 수 있습니다.
1. 커널 스레드를 kthreadadd란 프로세스에 요청
2. kthreadadd 프로세스는 요청된 프로세스를 생성

먼저 커널 스레드 생성 요청 코드를 봅시다.
1 #define kthread_create(threadfn, data, namefmt, arg...) \
2 kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
3
4 struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
5    void *data, int node,
6    const char namefmt[],
7    ...)
8 {
9 struct task_struct *task;
10 va_list args;
11
12 va_start(args, namefmt);
13 task = __kthread_create_on_node(threadfn, data, node, namefmt, args);
14 va_end(args);
15
16 return task;
17}

kthread_create() 함수에 커널 스레드를 식별하는 인자와 함께 호출하면 kthread_create_on_node() 함수를 호출합니다. kthread_create_on_node() 함수는 가변 인자를 아규먼트로 받고 __kthread_create_on_node() 함수를 호출합니다.

커널 스레드 생성 요청은 __kthread_create_on_node() 함수에서 이루어집니다. 코드를 봅시다.
1 struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
2     void *data, int node,
3     const char namefmt[],
4     va_list args)
5 {
6 DECLARE_COMPLETION_ONSTACK(done);
7 struct task_struct *task;
8 struct kthread_create_info *create = kmalloc(sizeof(*create),
9      GFP_KERNEL);
10
11 if (!create)
12 return ERR_PTR(-ENOMEM);
13 create->threadfn = threadfn;
14 create->data = data;
15 create->node = node;
16 create->done = &done;
17
18 spin_lock(&kthread_create_lock);
19 list_add_tail(&create->list, &kthread_create_list);
20 spin_unlock(&kthread_create_lock);
21
22 wake_up_process(kthreadd_task);

먼저 8~9번째 줄 코드를 봅시다.
8 struct kthread_create_info *create = kmalloc(sizeof(*create),
9      GFP_KERNEL);

struct kthread_create_info 구조체 메모리를 struct kthread_create_info 크기만큼 할당 받습니다.

다음 13~15번째 줄 코드를 봅시다.
13 create->threadfn = threadfn;
14 create->data = data;
15 create->node = node;

커널 스레드 핸들 함수와 매개 변수 및 노드를 struct kthread_create_info 멤버에 저장합니다.

다음 19번째 줄 코드를 봅시다.
19 list_add_tail(&create->list, &kthread_create_list);

커널 스레드 생성 요청을 관리하는 kthread_create_list이란 링크드 리스트에 &create->list를 추가합니다. kthreadadd란 프로세스는 커널 스레드 생성으로 깨어나면 kthread_create_list 링크드 리스트가 비어 있는지 확인하고 커널 스레드를 생성합니다.

__kthread_create_on_node() 함수 핵심 코드인 22번째 줄입니다.
22 wake_up_process(kthreadd_task);
 
kthreadd 프로세스의 태스크 디스크립터인 kthreadd_task를 인자로 wake_up_process() 함수를 호출해서 kthreadd 프로세스를 깨웁니다.

kthreadd 프로세스의 스레드 핸들러인 kthreadd() 함수 코드를 분석하겠습니다.
1 int kthreadd(void *unused)
2 {
3 struct task_struct *tsk = current;
4
5 /* Setup a clean context for our children to inherit. */
6 set_task_comm(tsk, "kthreadd");
7 ignore_signals(tsk);
8 set_cpus_allowed_ptr(tsk, cpu_all_mask);
9 set_mems_allowed(node_states[N_MEMORY]);
10
11 current->flags |= PF_NOFREEZE;
12 cgroup_init_kthreadd();
13
14 for (;;) {
15 set_current_state(TASK_INTERRUPTIBLE);
16 if (list_empty(&kthread_create_list))
17 schedule();
18 __set_current_state(TASK_RUNNING);
19
20 spin_lock(&kthread_create_lock);
21 while (!list_empty(&kthread_create_list)) {
22 struct kthread_create_info *create;
23
24 create = list_entry(kthread_create_list.next,
25     struct kthread_create_info, list);
26 list_del_init(&create->list);
27 spin_unlock(&kthread_create_lock);
28
29 create_kthread(create);
30
31 spin_lock(&kthread_create_lock);
32 }
33 spin_unlock(&kthread_create_lock);
34 }

wake_up_process(kthreadd_task); 함수를 호출해서 kthreadd 프로세스를 깨우면 실행하는 코드입니다.
16 if (list_empty(&kthread_create_list))
17 schedule();
18 __set_current_state(TASK_RUNNING);

커널 스레드 생성 요청이 없으면 kthread_create_list 이란 링크드 리스트가 비게 되고 휴면에 진입하다가 커널 스레드 생성 요청이 오면 18번째 줄 코드를 실행합니다.

21~32번째 줄 코드를 보기 전에 while 문 조건인 21번째 줄 코드를 봅시다.
kthread_create_list 이란 링크드 리스트가 비어있지 않으면 21~32번째 줄 코드를 실행해서 커널 스레드를 생성합니다.
21 while (!list_empty(&kthread_create_list)) {
22 struct kthread_create_info *create;
23
24 create = list_entry(kthread_create_list.next,
25     struct kthread_create_info, list);
26 list_del_init(&create->list);
27 spin_unlock(&kthread_create_lock);
28
29 create_kthread(create);
30
31 spin_lock(&kthread_create_lock);
32 }

24~25번째 줄 코드를 봅시다.
kthread_create_list.next 멤버를 통해 struct kthread_create_info 구조체 주소를 읽습니다.

29번째 줄 코드를 분석합시다.
29 create_kthread(create);

create_kthread() 함수를 호출해서 커널 스레드를 생성합니다.

이제 create_kthread() 함수를 봅시다.
1 static void create_kthread(struct kthread_create_info *create)
2 {
3 int pid;
4
5 #ifdef CONFIG_NUMA
6 current->pref_node_fork = create->node;
7 #endif
8 /* We want our own signal handler (we take no signals by default). */
9 pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);

9번째 줄 코드와 같이 kernel_thread() 함수를 호출하는데 CLONE_FS, CLONE_FILES, SIGCHLD 매크로를 아규먼트로 설정합니다.

kernel_thread() 함수를 분석하겠습니다.
1 pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
2 {
3 return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
4 (unsigned long)arg, NULL, NULL, 0);
5 }


3번째 줄 코드와 같이 _do_fork() 함수를 호출합니다.
_do_fork() 함수는 프로세스를 생성할 때 실행하는 함수로 알고 있습니다.

이로 커널 스레드도 프로세스의 한 종류인 것을 알 수 있습니다.

Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 


#Reference 시스템 콜

Reference(워크큐)
워크큐(Workqueue) Overview


부모 자식 프로세스 생성 실습 및 ftrace 로그 분석 


이번에 리눅스 시스템 프로그래밍으로 프로세스를 생성해 봅시다. 소스 코드는 다음과 같으니 같이 입력해 봅시다.
1  #include <stdio.h>
2  #include <unistd.h>
3  #include <sys/types.h>
4
5  #define PROC_TIMES  7
6  #define SLEEP_DURATION  2
7  #define FORK_MAX_TIMES  3
8
9  void raspbian_proc_process(void);
10
11 void raspbian_proc_process(void) 
12 {
13 int proc_times = 0;
14
15 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
16 printf("raspbian tracing ppid:%d pid:%d \n", getppid(), getpid());
17 sleep(SLEEP_DURATION);
18 }
19
20  exit(EXIT_SUCCESS);
21 }

22 int main() 
23 {
24 pid_t pid;
25 int fork_times = 0;
26
27 printf("About to fork process \n");
28
29 pid = fork();
30
31 if ( pid == 0 )  {
32 printf("start execution of child process\n");
33 raspbian_proc_process();
34 }
35
36 else if ( pid > 0 ) {
37 printf("start execution of parent process\n");
38 raspbian_proc_process();
39 }
40
41 return 0;
42 }

이전 절에 작성한 코드에서 다음 동작이 추가됐습니다. 29번째 줄 코드부터 분석을 시작합니다.
29 pid = fork();
30
31 if ( pid == 0 )  {
32 printf("start execution of child process\n");
33 raspbian_proc_process();
34 }
35
36 else if ( pid > 0 ) {
37 printf("start execution of parent process\n");
38 raspbian_proc_process();
39 }

fork() 함수를 호출해서 자식 프로세스를 생성합니다. fork() 함수는 실행 후 pid란 지역변수로 반환값을 저장합니다. pid가 0이면 자식 프로세스 혹은 pid가 0보다 크면 부모 프로세스가 생성된 겁니다. 만약 fork() 함수 실행 도중 오류가 발생하면 자식 프로세스가 생성되지 않고 -1를 반환합니다.

위 코드를 입력한 다음 raspbian_test_fork.c 란 이름으로 저장합시다.
raspbian_fork: raspbian_test_fork.c
gcc -o raspbian_fork raspbian_test_fork.c

이전 시간에 작성한 ftrace 로그 설정 셸 스크립트인 ./clone_process_debug.sh를 실행합니다.

다음 명령어로 raspbian_fork 프로세스를 실행합시다.
root@raspberrypi:/home/pi # ./raspbian_fork 
About to fork process 
start execution of parent process
raspbian tracing ppid:895 pid:17120 
start execution of child process
raspbian tracing ppid:17120 pid:17121 
raspbian tracing ppid:895 pid:17120 
raspbian tracing ppid:17120 pid:17121

화면에 출력되는 메시지는 raspbian_test_fork.c 파일에서 작성한 코드가 실행하면서 출력합니다. pid가 17120/17121인 프로세스가 실행하는 것으로 보입니다.

다음 명령어로 실행 중인 프로세스를 확인합시다.
root@raspberrypi:/home/pi # ps -ely | grep raspbian_fork
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000 17120   895  0  80   0   320   453 hrtime pts/0    00:00:00 raspbian_fork
S  1000 17121 17120  0  80   0    96   453 hrtime pts/0    00:00:00 raspbian_fork

위 메시지로 보아 pid가 17121인 raspbian_fork 프로세스의 부모 프로세스의 pid는 17120임을 알 수 있습니다. 마찬가지로 pid가 17120인 raspbian_fork 프로세스 부모 프로세스 pid는 895입니다. 

이렇게 raspbian_fork 프로세스를 실행하면 14초 정도 구동하다가 프로세스가 종료합니다. 그 이유는 14초 정도 raspbian_proc_process() 함수 실행 후 다음 20번째 줄 코드와 같이 exit() 함수를 실행해서 프로세스를 종료하기 때문입니다.
11 void raspbian_proc_process(void) 
12 {
13 int proc_times = 0;
14
15 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
16 printf("raspbian tracing ppid:%d pid:%d \n", getppid(), getpid());
17 sleep(SLEEP_DURATION);
18 }
19
20  exit(EXIT_SUCCESS);
21 }

이 후 이전에 썼던 방식으로 다음 스크립트를 실행해서 ftrace 로그를 라즈베리파이에서 추출합시다.
root@raspberrypi:/home/pi#./get_ftrace.sh

프로세스 생성과 종료 과정 메시지가 포함된 ftrace 전체 로그는 다음과 같습니다. 각 단계 별로 ftrace 로그를 분석하겠습니다.
1 raspbian_fork-17120 [003] ....1318.513909: copy_process+0x14/0x17d8 <-_do_fork+0xb0/0x3ec
2  raspbian_fork-17120 [003] ....1318.513921: <stack trace>
3 => _do_fork+0xb0
4 => SyS_clone+0x30
5 => ret_fast_syscall+0x0
...
6 raspbian_fork-17120 [003] d...1318.514446: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_prio=120
7 raspbian_fork-17121 [002] d...1318.514869: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
8 raspbian_fork-17120 [003] d...1320.514615: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_prio=120
9 raspbian_fork-17121 [002] d...1320.515011: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120
...
10 raspbian_fork-17120 [003] d...1322.514829: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120
11 raspbian_fork-17121 [002] d...1322.515192: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120
...
12 raspbian_fork-17121 [002] ....  1343.333582: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
13 raspbian_fork-17120 [003] ....  1343.333583: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
14 raspbian_fork-17121 [002] ....  1343.333621: <stack trace>
15 => SyS_exit_group+0x24/SyS_exit_group+0x24
16 => ret_fast_syscall+0x0/0x28
17   raspbian_fork-17120 [003] ....  1343.333621: <stack trace>
18 => SyS_exit_group+0x24/SyS_exit_group+0x24
19 => ret_fast_syscall+0x0/0x28
20 lxterminal-876   [000] d...  1343.333844: sched_switch: prev_comm=lxterminal prev_pid=876 prev_prio=120 prev_state=S ==> next_comm=Xorg next_pid=454 next_prio=120
21 Xorg-454   [000] dn..  1343.333946: sched_wakeup: comm=lxterminal pid=876 prio=120 target_cpu=000
22 Xorg-454   [000] d...  1343.333957: sched_switch: prev_comm=Xorg prev_pid=454 prev_prio=120 prev_state=R ==> next_comm=lxterminal next_pid=876 next_prio=120
23 raspbian_fork-17120 [003] ....  1343.333959: sched_process_exit: comm=raspbian_fork pid=17120 prio=120
24 raspbian_fork-17121 [002] ....  1343.333980: sched_process_exit: comm=raspbian_fork pid=17121 prio=120
25 raspbian_fork-17120 [003] d...  1343.334028: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0
26 lxterminal-876   [000] dnh.  1343.334048: sched_wakeup: comm=bash pid=895 prio=120 target_cpu=000
27 raspbian_fork-17121 [002] d...  1343.334049: signal_generate: sig=17 errno=0 code=2 comm=systemd pid=1 grp=1 res=0

1번째 로그부터 분석을 시작합니다.
1 raspbian_fork-17120 [003] ....1318.513909: copy_process+0x14/0x17d8 <-_do_fork+0xb0/0x3ec
2 raspbian_fork-17120 [003] ....1318.513921: <stack trace>
3 => _do_fork+0xb0
4 => SyS_clone+0x30
5 => ret_fast_syscall+0x0

pid가 17120인 raspbian_fork 프로세스가 pid가 17121인 raspbian_fork 프로세스를 생성하는 동작입니다.
raspbian_test_fork.c 파일에서 유저 공간에서 프로세스를 생성하는 코드는 29번째 줄과 같았습니다. 
22 int main() 
23 {
24 pid_t pid;
25 int fork_times = 0;
26
27 printf("About to fork process \n");
28
29 pid = fork();

유저 공간에서 fork() 함수를 호출하면 시스템 콜이 실행해서 커널 공간에서 SyS_clone() 이란 함수를 호출하는 겁니다.

다음 6~11번째 줄 로그를 분석해봅시다.
6 raspbian_fork-17120 [003] d...1318.514446: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_prio=120
7 raspbian_fork-17121 [002] d...1318.514869: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
...
8 raspbian_fork-17120 [003] d...1320.514615: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_prio=120
9 raspbian_fork-17121 [002] d...1320.515011: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120
...
10 raspbian_fork-17120 [003] d...1322.514829: sched_switch: prev_comm=raspbian_fork prev_pid=17120 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120
11 raspbian_fork-17121 [002] d...1322.515192: sched_switch: prev_comm=raspbian_fork prev_pid=17121 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:0 next_pid=17108 next_prio=120

raspbian_fork-17120와 raspbian_fork-17121 프로세스가 2초 간격으로 스케줄링되어 실행합니다. 
raspbian_test_fork.c 파일에서 다음과 같이 두 개 프로세스가 2초 주기로 휴면되도록 구현한 코드가 실행하는 겁니다.
11 void raspbian_proc_process(void) 
12 {
13 int proc_times = 0;
14
15 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
16 printf("raspbian tracing ppid:%d pid:%d \n", getppid(), getpid());
17 sleep(SLEEP_DURATION);
18 }
19
20  exit(EXIT_SUCCESS);
21 }

다음은 유저 레벨 프로세스에서 위에서 보이는 20번째 줄 exit(EXIT_SUCCESS); 함수를 실행하면 커널에서 어떤 동작을 하는지 살펴볼 차례입니다. 

exit() 리눅스 저수준 표준 함수 중 하나이며 리눅스 시스템 프로그램에서 많이 쓰는 함수입니다. 
다음 리눅스 메뉴얼 페이지에서 exit() 함수에 대한 내용을 읽을 수 있습니다.
EXIT(3)                   Linux Programmer's Manual                  EXIT(3)
NAME         top
       exit - cause normal process termination
SYNOPSIS         top
       #include <stdlib.h>

       void exit(int status);
DESCRIPTION         top
       The exit() function causes normal process termination and the value
       of status & 0377 is returned to the parent (see wait(2)).

명시적으로 프로세스를 종료시키는 동작입니다.
로그 분석으로 돌아가겠습니다.
12 raspbian_fork-17121 [002] ....  1343.333582: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
13 raspbian_fork-17120 [003] ....  1343.333583: do_exit+0x14/0xc18 <-do_group_exit+0x50/0xe4
14 raspbian_fork-17121 [002] ....  1343.333621: <stack trace>
15 => SyS_exit_group+0x24/SyS_exit_group+0x24
16 => ret_fast_syscall+0x0/0x28
17   raspbian_fork-17120 [003] ....  1343.333621: <stack trace>
18 => SyS_exit_group+0x24/SyS_exit_group+0x24
19 => ret_fast_syscall+0x0/0x28

raspbian_fork-17120와 raspbian_fork-17121 프로세스가 종료하는 동작입니다.
이 로그에서 프로세스가 종료하는 흐름은 다음 그림과 같습니다.

리눅스 저수준 exit() 함수를 유저 프로세스에서 실행하니 해당 함수에 대한 시스템 콜 핸들러인 sys_exit_group() 함수에서 do_group_exit() -> do_exit() 순서로 함수를 호출해서 두 프로세스를 종료합니다.

마지막으로 27~31번째 줄 로그를 봅시다.
27 raspbian_fork-17120 [003] ....  1343.333959: sched_process_exit: comm=raspbian_fork pid=17120 prio=120
28 raspbian_fork-17121 [002] ....  1343.333980: sched_process_exit: comm=raspbian_fork pid=17121 prio=120
29 raspbian_fork-17120 [003] d...  1343.334028: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0
30 lxterminal-876   [000] dnh.  1343.334048: sched_wakeup: comm=bash pid=895 prio=120 target_cpu=000
31 raspbian_fork-17121 [002] d...  1343.334049: signal_generate: sig=17 errno=0 code=2 comm=systemd pid=1 grp=1 res=0

sched_process_exit 이란 ftrace 이벤트로 pid 17120, 17121 인 raspbian_fork 프로세스가 종료하고, 각각 부모 프로세스에 시그널을 전달합니다.

raspbian_fork-17120 프로세스는 bash란 부모 프로세스에 시그널을 전달하고, raspbian_fork-17121 프로세스는 부모 프로세스인 raspbian_fork-17120가 종료됐으니 pid가 1인 systemd 프로세스에 시그널을 전달합니다.
29 raspbian_fork-17120 [003] d...  1343.334028: signal_generate: sig=17 errno=0 code=2 comm=bash pid=895 grp=1 res=0
30 lxterminal-876   [000] dnh.  1343.334048: sched_wakeup: comm=bash pid=895 prio=120 target_cpu=000
31 raspbian_fork-17121 [002] d...  1343.334049: signal_generate: sig=17 errno=0 code=2 comm=systemd pid=1 grp=1 res=0

유저 공간에서 fork() 이란 함수를 호출하면 리눅스 커널에서 어떤 과정으로 프로세스를 생성하는지 확인했습니다. 이번에 다룬 내용을 정리합시다.
1. 유저 공간에서 fork() 함수를 호출하면 시스템 콜이 실행되어 커널 공간에 있는 SyS_clone()이란 함수를 호출하고 _do_fork() 이란 프로세스를 생성하는 함수를 호출합니다.

2. 유저 레벨 프로세스는 스스로 프로세스를 생성하지 못합니다. 시스템 라이브러리 도움을 받아서 커널 공간에 프로세스 생성 요청을 합니다.

3. 프로세스를 종료할 때 do_exit() 함수를 호출합니다.

4. 프로세스가 종료할 때 부모 프로세스에게 자신이 종료됐다는 사실을 시그널로 알립니다.

유저 공간에서 어떤 함수를 호출하면 리눅스 커널에서 어떤 함수 흐름으로 코드가 실행되는지 전체 그림을 그리면서 파악하는 것이 중요합니다.

Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 



라즈베리파이에서 IRQ 스레드 생성 과정 디버깅하기

이번 절에선 request_threaded_irq() 함수를 호출하면 결국 kthread_create() 함수를 실행해서 IRQ 스레드를 생성하는 과정까지 짚어 봤습니다. 이제 디버깅 과정을 통해 배운 내용을 다지는 시간을 갖겠습니다.

먼저 디버깅을 위한 패치 코드를 소개합니다.

 

+기호로 볼드체로 된 부분이 추가할 코드입니다.
패치 코드를 입력할 함수는 __kthread_create_on_node() 입니다. 이전 절에서 분석하지 않은 __kthread_create_on_node() 함수에 IRQ 스레드를 생성하는 코드를 작성한 이유는 무엇일까요?

그 이유을 알게 위해서 다음 setup_irq_thread() 함수 코드를 살펴볼 필요가 있습니다.
1 static int
2 setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
3 {
...
4 if (!secondary) {
4 t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
5    new->name);

4 번째 줄 코드와 같이 set_up_irqthread() 함수에서 kthread_create() 함수를 호출하면 IRQ 스레드를 생성한다고 알고 있습니다. kthread_create() 함수 구현부 코드를 보면 kthread_create_on_node() 함수를 바로 호출하고 이어서 __kthread_create_on_node() 함수를 호출합니다. 그래서 __kthread_create_on_node() 함수에 디버깅 코드를 작성한 것입니다. 

다음 코드에서 볼드체로 된 부분을 눈여겨봅시다.
1 #define kthread_create(threadfn, data, namefmt, arg...) \
2 kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
3
4 struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
5    void *data, int node,
6    const char namefmt[],
7    ...)
8 {
9 struct task_struct *task;
10  va_list args;
11
12 va_start(args, namefmt);
13 task = __kthread_create_on_node(threadfn, data, node, namefmt, args);

우리는 kthread_create() 함수를 호출하면 커널 쓰레드를 생성한다고 알고 있습니다. kthread_create() 함수를 보면 2 번째 줄 코드와 같이 kthread_create_on_node() 함수로 치환됩니다. 

이어서 kthread_create_on_node() 함수를 열어보면 13 번째 줄 코드와 같이  __kthread_create_on_node() 함수를 호출합니다.

__kthread_create_on_node() 함수에 디버깅 코드를 작성한 이유를 살펴봤습니다. 이제 패치 코드를 분석할 차례입니다. 

먼저 수정한 4번째 줄 코드부터 분석합니다.
struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
    void *data, int node,
    const char namefmt[],
    va_list args)
4 +
5 + int irq_thread_enable = !strncmp(namefmt, "irq", 3); 
6 +      char *process_name = &namefmt[0];

__kthread_create_on_node() 함수로 전달되는 namefmt란 파라미터를 “irq” 문자열과 비교합니다. Strncmp() 함수는 지정한 문자열 개수만큼만 스트링을 비교하는 라이브러리 함수입니다. 이때 namefmt 변수 첫 번째 주소 기준으로 세 개 스트링이 “irq” 이면 irq_thread_enable 변수를 1로 설정합니다. 

IRQ 스레드 이름은 setup_irq_thread() 함수에서 "irq/%d-%s" 인자로 kthread_create() 함수를 호출할 때 지정한다고 알고 있습니다. 

다음 5번째 줄 코드를 보겠습니다. 
setup_irq_thread() 함수에서 지정한 "irq/%d-%s" 가 __kthread_create_on_node() 함수의 namefmt 인자로 전달됩니다. 그래서 이 앞 세 개의 스트링이 “irq”인지 비교하는 코드를 작성한 것입니다.  
1 static int
2 setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
3 {
...
4 if (!secondary) {
5 t = kthread_create(irq_thread, new, "irq/%d-%s", irq,  // <<-[1]
6    new->name);

다음 디버깅 정보를 출력하는 13번째 줄 코드를 볼 차례입니다.
13+    if (irq_thread_enable) {
14+        void *irq_threadfn = (void*)threadfn;
15+        
16+        printk("[+] irq_thread handler: %pS caller:(%pS) \n", 
17+ irq_threadfn, (void *)__builtin_return_address(0)); 
18+        dump_stack();
19+ }

13~19번째 줄 코드는irq_thread_enable 지역변수가 1이면 실행합니다.
irq_thread_enable 변수가 1이면 무엇을 의미할까요? 쓰레드 이름이 “irq”로 시작하니 IRQ 스레드를 생성하는 조건입니다. 14번째 줄 코드에서 IRQ 스레드 핸들 함수 포인터를 저장하고 있는 threadfn을 irq_threadfn  포인터에 캐스팅합니다. threadfn인자는 irq_thread() 함수 주소를 저장하고 있습니다. 

16번째 줄 코드는 IRQ 스레드 핸들 함수와 자신을 호출한 함수 정보를 출력합니다. 
16+        printk("[+] irq_thread handler: %pS caller:(%pS) \n", 
17+ irq_threadfn, (void *)__builtin_return_address(0));

이어서 18번째 줄 코드는 콜스택을 출력하는 dump_stack 함수를 호출합니다.
18+        dump_stack();

이렇게 코드를 입력하고 커널 빌드를 완료한 후 라즈베리파이에 커널 이미지를 설치합니다.
이후 라즈베리파이를 리부팅 시키고 커널 로그(/var/log/kern.log)을 열어봅시다.

이번에는 패치 코드가 실행하면서 출력한 커널 로그를 분석하겠습니다. 분석할 전체 로그는 다음과 같습니다.
1  [0.722882] mmc-bcm2835 3f300000.mmc: mmc_debug:0 mmc_debug2:0
2  [0.722892] mmc-bcm2835 3f300000.mmc: DMA channel allocated
3  [0.722933] [+] process_name: irq/%d-%s caller:(kthread_create_on_node+0x30/0x38) 
4  [0.722947] [+] irq_thread handler: irq_thread+0x0/0x20c
 caller:(kthread_create_on_node+0x30/0x38) 
5  [0.722958] CPU: 0 PID: 31 Comm: kworker/0:1 Not tainted 4.14.39-v7+ #15
6  [0.722962] Hardware name: BCM2835
7  [0.722980] Workqueue: events deferred_probe_work_func
8  [0.723006] (unwind_backtrace) from [<8010c21c>] (show_stack+0x20/0x24)
9  [0.723022] (show_stack) from (dump_stack+0xc8/0x10c)
10 [0.723039] (dump_stack) from (__kthread_create_on_node+0x1c4/0x1e0)
11 [0.723055] (__kthread_create_on_node) from (kthread_create_on_node+0x30/0x38)
12 [0.723070] (kthread_create_on_node) from (setup_irq_thread+0x54/0xe4)
13 [0.723086] (setup_irq_thread) from (__setup_irq+0x260/0x730)
14 [0.723101] (__setup_irq) from (request_threaded_irq+0xec/0x160)
15 [0.723118] (request_threaded_irq) from (devm_request_threaded_irq+0x78/0xcc)
16 [0.723140] (devm_request_threaded_irq) from (bcm2835_mmc_probe+0x514/0x644)
17 [0.723159] (bcm2835_mmc_probe) from (platform_drv_probe+0x60/0xc0)
18 [0.723176] (platform_drv_probe) from (driver_probe_device+0x244/0x2f8)
19 [0.723193] (driver_probe_device) from (__device_attach_driver+0xa8/0xdc)
20 [0.723210] (__device_attach_driver) from (bus_for_each_drv+0x70/0xa4)
21 [0.723228] (bus_for_each_drv) from (__device_attach+0xc0/0x124)
22 [0.723245] (__device_attach) from (device_initial_probe+0x1c/0x20)
23 [0.723261] (device_initial_probe) from (bus_probe_device+0x94/0x9c)
24 [0.723278] (bus_probe_device) from (deferred_probe_work_func+0x68/0x150)
25 [0.723296] (deferred_probe_work_func) from (process_one_work+0x224/0x518)
26 [0.723317] (process_one_work) from (worker_thread+0x60/0x5f0)
27 [0.723333] (worker_thread) from (kthread+0x144/0x174)
28 [0.723348] (kthread) from (ret_from_fork+0x14/0x28)

먼저 커널 쓰레드 이름과 쓰레드 핸들 함수를 분석해 봅시다.
3  [0.722933] [+] process_name: irq/%d-%s caller:(kthread_create_on_node+0x30/0x38) 
4  [0.722947] [+] irq_thread handler: irq_thread+0x0/0x20c
 caller:(kthread_create_on_node+0x30/0x38) 

쓰레드 이름은 irq/%d-%s 이고 자신을 호출한 함수가 kthread_create_on_node+0x30입니다. 
또한 IRQ 스레드 핸들 함수는 irq_thread()란 정보를 알 수 있습니다.

다음 5~6 번째 줄 로그입니다.
5  [0.722958] CPU: 0 PID: 31 Comm: kworker/0:1 Not tainted 4.14.39-v7+ #15
6  [0.722962] Hardware name: BCM2835

이 함수가 어느 프로세스에서 실행됐는지 알려주는 로그입니다. PID가 31인 kworker/0:1란 워커쓰레드가 CPU0에서 수행됐음을 알 수 있습니다.

이번에는 콜스택을 살펴보겠습니다.
platform_drv_probe() 함수에서 bcm2835_mmc_probe() 함수를 호출하고 있습니다. 이 정보로 부팅 과정에 IRQ 스레드를 생성한다는 사실을 알 수 있습니다. 보통 드라이버에서 짠 코드의 함수 이름에 probe가 보이면 부팅 도중 한 번 실행합니다.
11 [0.723055] (__kthread_create_on_node) from (kthread_create_on_node+0x30/0x38)
12 [0.723070] (kthread_create_on_node) from (setup_irq_thread+0x54/0xe4)
13 [0.723086] (setup_irq_thread) from (__setup_irq+0x260/0x730)
14 [0.723101] (__setup_irq) from (request_threaded_irq+0xec/0x160)
15 [0.723118] (request_threaded_irq) from (devm_request_threaded_irq+0x78/0xcc)
16 [0.723140] (devm_request_threaded_irq) from (bcm2835_mmc_probe+0x514/0x644)
17 [0.723159] (bcm2835_mmc_probe) from (platform_drv_probe+0x60/0xc0)
18 [0.723176] (platform_drv_probe) from (driver_probe_device+0x244/0x2f8)

콜스택을 보면 이번 절에서 분석한 함수를 볼 수 있습니다.
request_threaded_irq() 함수로 출발해서 __kthread_create_on_node() 함수까지 호출되고 있습니다. kthread_create() 함수 대신 실제 kthread_create_on_node() 함수가 실행한다는 점도 확인할 수 있습니다.

이번 소절에선 IRQ 스레드를 생성하는 흐름을 점검했습니다. 
request_threaded_irq() 함수를 호출하면 __kthread_create_on_node() 함수를 호출해서 IRQ 스레드를 생성합니다. 

다음 절에서는 IRQ 스레드가 어떻게 실행하는지 점검해보겠습니다.


IRQ 스레드 생성 예제 코드 분석 

이번에는 IRQ 스레드를 생성하는 예제 코드를 소개합니다. 실제 request_threaded_irq() 함수를 호출해서 IRQ 스레드를 생성하는 과정을 살펴보겠습니다.

분석할 코드는 다음과 같습니다.
1 static int dwc3_gadget_start(struct usb_gadget *g,
struct usb_gadget_driver *driver)
3 {
4 struct dwc3 *dwc = gadget_to_dwc(g);
5 unsigned long flags;
6 int ret = 0;
7 int irq;
8
9 irq = dwc->irq_gadget;
10 ret = request_threaded_irq(irq, dwc3_interrupt, dwc3_thread_interrupt,
11 IRQF_SHARED, "dwc3", dwc->ev_buf);

먼저 request_threaded_irq() 함수에 전달하는 인자를 살펴봅시다.
   - irq: 인터럽트 번호
   - dwc3_interrupt: 인터럽트 핸들러
   - dwc3_thread_interrupt: 인터럽트 스레드 핸들 함수
   - IRQF_SHARED: 인터럽트 플래그
   - "dwc3": 인터럽트 이름
   - dwc->ev_buf: 인터럽트 핸들러와 인터럽트 스레드 핸들 함수에 전달하는 매개인자

5장에서 인터럽트 핸들러를 설정할 때 썼던 request_irq() 함수와 유사해 보입니다. request_irq() 함수를 호출할 때 비슷한 타입의 인자를 전달하는, request_threaded_irq() 함수는 IRQ 스레드 핸들러인 dwc3_thread_interrupt() 함수를 추가합니다.

request_threaded_irq() 함수를 호출하면 해당 인터럽트에 대한 전용 IRQ 스레드가 생성됩니다. 

리눅스 커널에서 IRQ 스레드 이름을 어떻게 결정할까요?? 위 인터럽트 번호가 47이면 IRQ 스레드 이름은 "irq/47-dwc3" 입니다.

인터럽트 발생 후 dwc3_interrupt() 이란 인터럽트 핸들러에서 인터럽트에 대한 처리를 한 다음 "irq/47-dwc3" IRQ 스레드를 깨울지 결정합니다. 이후 "irq/47-dwc3" IRQ 스레드가 깨어나면 스레드 핸들러인 dwc3_thread_interrupt() 함수가 호출되는 구조입니다.

이번에는 인터럽트 핸들러인 dwc3_thread_interrupt() 함수를 보면서 세부 동작을 확인합시다. 
static irqreturn_t dwc3_interrupt(int irq, void *_evt)
{
struct dwc3_event_buffer *evt = _evt;
return dwc3_check_event_buf(evt);
}

dwc 인터럽트가 발생하면 dwc3_interrupt() 이란 인터럽트 핸들러가 실행됩니다. dwc3_interrupt() 함수는 특별한 동작을 하지 않습니다. 바로 dwc3_check_event_buf() 함수를 호출합니다.

dwc3_check_event_buf() 함수 구현부는 다음과 같습니다.
1 static irqreturn_t dwc3_check_event_buf(struct dwc3_event_buffer *evt)
2 {
3 struct dwc3 *dwc = evt->dwc;
4 u32 amount;
5 u32 count;
6 u32 reg;
7
8 if (pm_runtime_suspended(dwc->dev)) {
9 pm_runtime_get(dwc->dev);
10 disable_irq_nosync(dwc->irq_gadget);
11 dwc->pending_events = true;
12 return IRQ_HANDLED;
13 }
...
14 if (amount < count)
15 memcpy(evt->cache, evt->buf, count - amount);
16
17 dwc3_writel(dwc->regs, DWC3_GEVNTCOUNT(0), count);
18
19 return IRQ_WAKE_THREAD;
20}

위 함수를 눈여겨보면 시스템 상태에 따라 IRQ_HANDLED와 IRQ_WAKE_THREAD를 리턴합니다. 인터럽트가 발생한 후 일을 더 할 필요가 없을 때는 다음 12 번째 줄 코드와 같이 IRQ_HANDLED를 반환합니다.
8 if (pm_runtime_suspended(dwc->dev)) {
9 pm_runtime_get(dwc->dev);
10 disable_irq_nosync(dwc->irq_gadget);
11 dwc->pending_events = true;
12 return IRQ_HANDLED;
13 }

그런데 IRQ 스레드가 해당 인터럽트 핸들러 실행 이후 후속 처리를 수행해야 할 때는 IRQ_WAKE_THREAD를 반환합니다.
17 dwc3_writel(dwc->regs, DWC3_GEVNTCOUNT(0), count);
18
19 return IRQ_WAKE_THREAD;
20}

이후 IRQ 스레드가 깨어난 후 IRQ 스레드 핸들러인 dwc3_thread_interrupt() 함수가 실행됩니다. 이 함수에서 인터럽트 핸들러에서 바로 처리하지 못한 일을 수행합니다. 


IRQ 스레드는 언제 생성할까?

IRQ 스레드를 생성하기 위해서는 request_threaded_irq() 함수를 호출하면 됩니다. 
IRQ 스레드를 생성하는 흐름도는 다음과 같습니다. 










request_threaded_irq() 함수를 호출하면 다음 동작을 수행합니다.
 - 전달한 IRQ 스레드 정보를 인터럽트 컨택스트에 설정
 - kthread_create() 함수를 호출해서 IRQ 스레드 생성

kthread_create() 함수는 kthread_create_on_node() 함수로 치환됩니다. 위 그림에서 이해를 돕기 위해 kthread_create() 함수를 호출하면 kthread_create_on_node() 함수를 호출하는 것처럼 표시했습니다.

우리는 커널 쓰레드를 생성할 때 kthread_create() 함수를 호출한다고 배웠습니다. IRQ 스레드도 이 kthread_create() 함수를 호출해서 생성합니다. IRQ 스레드도 커널 스레드의 한 종류입니다. 

request_threaded_irq() 함수부터 __kthread_create_on_node() 함수까지 IRQ 스레드 어떤 방식으로 생성하는지 코드를 분석하겠습니다.
 
먼저 우선 인터럽트 핸들러를 설정하는 코드를 확인할 필요가 있습니다. 어떤 인터럽트 핸들러를 설정하는 코드를 봐야 할까요? 당연히 IRQ 스레드를 생성하는 인터럽트 핸들러 코드를 봐야합니다.

이전 소절에서 확인했듯이 라즈베리파이에서는 "irq/92-mmc1" IRQ Thread를 확인할 수 있습니다. 
root@raspberrypi:/home/pi/dev_raspberrian# ps –ely | grep irq
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S     0    65     2  0   9   -     0     0 irq_th ?        00:00:00 irq/92-mmc1

IRQ 스레드는 어떤 자료구조에서 관리할까요?
정답은 인터럽트 디스크립터입니다. IRQ 스레드 핸들러와 관련 필드 정보를 관리합니다.

인터럽트 디스크립터는 인터럽트 세부 속성과 IRQ 스레드 정보까지 저장합니다.

IRQ 스레드를 생성하기 위해서 request_threaded_irq() 함수를 호출해야 합니다.
함수 선언부와 인자를 소개합니다.
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
     irq_handler_t thread_fn,
     unsigned long flags, const char *name, void *dev);

request_threaded_irq() 함수에 전달되는 인자들은 다음과 같습니다.

unsigned int  irq: 인터럽트 번호
irq_handler_t   handler: 인터럽트 핸들러 주소
irq_handler_t   thread_fn: IRQ 스레드 핸들 함수 주소
unsigned long  flags: 인터럽터 핸들링 플래그
const char   name: 인터럽트 이름

request_threaded_irq() 함수는 동작은 2단계로 나눌 수 있습니다.

1 단계: 인터럽트 디스크립터 설정
requested_threaded_irq() 함수에 전달된 인자를 인터럽트 디스크립터 필드에
저장합니다.

2 단계: IRQ 스레드 생성
irq_handler_t thread_fn 인자가 IRQ 스레드 핸들 주소를 저장하면 IRQ 스레드를 생성합니다.

request_threaded_irq() 함수 분석
IRQ 스레드 생성 단계에 대해 소개했으니 이제 코드 분석으로 넘어갑시다.
request_threaded_irq() 함수 코드를 소개합니다.
1 int request_threaded_irq(unsigned int irq, irq_handler_t handler,
 irq_handler_t thread_fn, unsigned long irqflags,
 const char *devname, void *dev_id)
4 {
struct irqaction *action;
struct irq_desc *desc;
int retval;
...
8 action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
9 if (!action)
10 return -ENOMEM;
...
11  action->handler = handler;
12  action->thread_fn = thread_fn;  
13  action->flags = irqflags;
14  action->name = devname;
15  action->dev_id = dev_id;
16 
17  chip_bus_lock(desc);
18  retval = __setup_irq(irq, desc, action);  

먼저 8~10 번째 줄 코드를 봅시다.
8 action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
9 if (!action)
10 return -ENOMEM;

struct irqaction 타입인 action 지역 변수에 struct irqaction 구조체 크기만큼 동적 메모리를 할당합니다. 

action란 포인터 타입 지역 변수의 타입은 struct irqaction 구조체입니다. 이 변수는 8번째 줄 코드와 같이 메모리 할당을 받은 다음에 9번째와 13번째 줄 코드와 같이 인터럽트 핸들러와 IRQ 스레드 핸들러 정보를 저장합니다. 이 변수는 나중에 인터럽트 디스크립터의 action 필드에 저장됩니다. 

만약 메모리를 할당 못하면 10 번째 줄 코드을 실행해 –ENOMEM 매크로를 반환하며 함수 실행을 종료합니다.

다음에 볼 11~15 번째 줄 코드는 인자를 action 필드에 저장하는 동작입니다.
11  action->handler = handler;
12  action->thread_fn = thread_fn;  
13  action->flags = irqflags;
14  action->name = devname;
15  action->dev_id = dev_id;

특히 12 번째 줄 코드를 보면 IRQ 스레드 핸들 함수 주소를 저장하고 있는 thread_fn 포인터를 action->thread_fn에 저장합니다.

struct irqaction 타입 action 변수는 해당 인터럽트 디스크립터에 저장됩니다.
Trace32로 확인한 인터럽트 디스크립터 자료 구조는 다음과 같습니다.
1 (struct irq_desc *) (struct irq_desc*)0xB008B300
...
2    (struct irqaction *) action = 0xBB4E6E40  
3      (irq_handler_t) handler = 0x8061EC00 = bcm2835_mmc_irq, /* 인터럽트 핸들러 */
4      (void *) dev_id = 0xBB47B010  /* 인터럽트 핸들러 핸들
5      (void *) percpu_dev_id = 0x0 = ,
6      (struct irqaction *) next = 0x0 = ,
7      (irq_handler_t) thread_fn = 0x8061DCC4 = bcm2835_mmc_thread_irq, /* IRQ Thread 핸들러 */
8      (struct task_struct *) thread = 0xBB516CC0 /* “irq/92-mmc1” IRQ 스레드의 태스크 디스크립터 */
9      (struct irqaction *) secondary = 0x0 = ,
10      (unsigned int) irq = 92, /* 인터럽트 번호 */

위 인터럽트 디스크립터는 라즈베리파이 92번 인터럽트를 관리하는 IRQ 스레드를 설정 정보를 포함합니다. 각각 필드에 대한 설명은 주석문을 참고하시기 바랍니다.

다음 18번째 줄 코드를 보면 __setup_irq() 함수를 호출합니다.
16  retval = __setup_irq(irq, desc, action);

여기까지는 인터럽트 핸들러를 등록하는 실행 흐름과 똑같습니다. 그런데 __setup_irq() 함수 코드를 조금 더 살펴보면 Thread 스레드로 설정할 때만 동작하는 코드를 볼 수 있습니다. 

__setup_irq() 함수 분석
이어서 __setup_irq() 코드를 같이 분석해 봅시다.
1 static int
2 __setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
3 {
4 struct irqaction *old, **old_ptr;
5 unsigned long flags, thread_mask = 0;
...
6 nested = irq_settings_is_nested_thread(desc);
...
7 if (new->thread_fn && !nested) {  
8 ret = setup_irq_thread(new, irq, false);
9 if (ret)
10 goto out_mput;

우선 __setup_irq 함수에 전달되는 파라미터는 아래와 같습니다.
irq: 인터럽트 번호
desc: 인터럽트 디스크립터
new: 인터럽트 디스크립터의 action 멤버(struct irq_desc->action)

__setup_irq() 함수는 IRQ 스레드 핸들 함수가 등록됐는지 점검한 후 등록이 됐으면 setup_irq_thread() 함수를 호출해서 IRQ 스래드를 생성합니다.

먼저 6 번째 줄 코드를 봅시다. 
6 if (new->thread_fn && !nested) {  
7 ret = setup_irq_thread(new, irq, false);

이 코드는 두 가지 조건을 점검합니다.  struct irqaction 타입인 new->thead_fn 필드에 함수 포인터가 등록됐거나 nested 변수가 0일 때 setup_irq_thread() 함수를 호출합니다. 

nested 변수는 현재 설정하는 IRQ 스레드가 nested 타입인지 점검합니다. 이 기능을 쓸 경우 nested 변수가 1이 됩니다. nested 변수를 읽어 오는 다음 코드를 눈여겨봅시다.
6 nested = irq_settings_is_nested_thread(desc);

여기서 new->thead_fn 로 IRQ 스레드 핸들러 함수가 등록됐고 nested 변수가 0이면 7번째 줄 코드가 실행됩니다.

setup_irq_thread() 함수 분석 

이어서 IRQ 스레드를 생성 역할을 수행하는 setup_irq_thread() 함수를 분석하겠습니다.
1 static int
2 setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
3 {
4 struct task_struct *t;
5 struct sched_param param = {
6 .sched_priority = MAX_USER_RT_PRIO/2,
7 };
8
9 if (!secondary) {
10 t = kthread_create(irq_thread, new, "irq/%d-%s", irq,   
11    new->name);
12 } else {
13 t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq,
14    new->name);
15 param.sched_priority -= 1;
16 }

위 코드를 보면 특별한 동작을 수행하지 않습니다. kthread_create() 함수를 호출해서 커널 스레드를 생성합니다. 이 코드로 IRQ 스레드도 커널 스레드의 한 종류라고 말할 수 있겠습니다.


커널 스레드는 커널 공간에서만 실행하는 프로세스입니다. 
유저 공간과 시스템 콜로 통신하지 않고 배경 작업으로 커널 리소스를 관리하는 역할을 수행합니다.

커널에는 다양한 커널 스레드를 볼 수 있습니다.
커널 서브 시스템이나 드라이버 목적에 맞는 커널 스레드를 생성할 수 있는 것입니다.
워크큐를 실행하는 스레드를 워커 스레드, 메모리가 부족할 때 kswapd 스레드 그리고 Soft IRQ 후반부 처리용 ksoftirqd 스레드를 예를 들 수 있습니다.

IRQ 스레드로 여러 커널 스레드 중 하나입니다.


먼저 10번째 줄 코드를 보겠습니다. irq_thread() 이란 IRQ 스레드를 제어하는 함수와 함께 "irq/%d-%s"란 이름으로 IRQ Thread를 생성합니다. 
9 t = kthread_create(irq_thread, new, "irq/%d-%s", irq,   
10    new->name);

kthread_create() 함수를 호출할 때 다음과 같은 인자를 지정합니다.
   - irq_thread: IRQ 스레드 핸들 함수
   - new: IRQ 스레드 핸들 매개 인자(struct irqaction)
   - "irq/%d-%s": IRQ 스레드 이름 타입
   - irq: 인터럽트 번호
   - new->name: 인터럽트 이름 

kthread_create() 함수에 전달하는 두 번째 파라미터인 new는 스레드 핸들 함수로 전달되는 매개 인자입니다. 이 매개 인자는 struct irqaction 구조체 타입입니다.

IRQ 스레드의 스레드 핸들인 irq_thread() 함수 코드를 보면서 매개 인자 처리 방식에 대해 조금 더 살펴보겠습니다.
1 static int irq_thread(void *data)
2 {
3 struct callback_head on_exit_work;
4 struct irqaction *action = data;

우리는 커널 쓰레드를 생성하면 커널 스레드드 핸들 함수에서 무한 루프를 돌면서 스레드 목적에 맞는 동작을 수행합니다.  그런데 IRQ 스레드는 irq_thread() 함수가 이 역할을 수행합니다. 

IRQ 스레드가 실행할 때 irq_thread() 함수가 실행하는데 함수 인자로 void 타입 data 포인터를 전달합니다. 위 irq_thread() 함수 4 번째 줄 코드를 눈여겨보면 이 포인터를 struct irqaction * 타입으로 캐스팅합니다.

이 과정을 다음 다이어그램으로 정리할 수 있습니다.

이제 IRQ 스레드를 생성하는 함수 흐름을 알아봤으니 IRQ 스레드를 생성하는 예제 코드를 살펴보겠습니다. 
[drivers/mmc/host/bcm2835-mmc.c]
1 static int bcm2835_mmc_add_host(struct bcm2835_host *host)
2 {
3 struct mmc_host *mmc = host->mmc;
4 struct device *dev = mmc->parent;
...
5 bcm2835_mmc_init(host, 0);
6 ret = devm_request_threaded_irq(dev, host->irq, bcm2835_mmc_irq,
7 bcm2835_mmc_thread_irq, IRQF_SHARED,
8 mmc_hostname(mmc), host); 

bcm2835_mmc_add_host() 함수에서 라즈베리파이에서 92번 인터럽트 핸들러와 해당 IRQ Thread를 설정하는 코드를 볼 수 있습니다.

위 코드를 보면 request_threaded_irq() 함수 대신 devm_request_threaded_irq() 함수를 써서 IRQ 스레드를 설정합니다. 함수 이름이 다르니 다른 동작을 하는 함수로 보입니다. 하지만 devm_request_threaded_irq() 함수를 열어 보면 request_threaded_irq() 함수를 호출합니다.
1 int devm_request_threaded_irq(struct device *dev, unsigned int irq,
2       irq_handler_t handler, irq_handler_t thread_fn,
3       unsigned long irqflags, const char *devname,
4       void *dev_id)
5 {
6 struct irq_devres *dr;
7 int rc;
8
...
9
10 rc = request_threaded_irq(irq, handler, thread_fn, irqflags, devname,
11   dev_id);

devm_request_threaded_irq() 함수는 인터럽트 설정 정보를 디바이스 드라이버에서 체계적으로 관리하는 동작 이외에 request_threaded_irq() 함수와 같은 역할을 수행합니다. IRQ 스레드 관점으로 devm_request_threaded_irq() 함수를 호출하면 request_threaded_irq() 함수가 실행된다고 봐도 무방합니다.

아래 코드에서 devm_request_threaded_irq() 함수에서 호출하는 request_threaded_irq() 함수는 어디서 많이 본 함수 같지 않나요? 맞습니다. 인터럽트 핸들러를 등록할 때 호출하는 request_irq() 함수에서 request_threaded_irq() 함수를 호출했습니다.  5장에서 본 다음 자료 구조를 떠 올립시다.
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
    const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

이번에 request_threaded_irq() 함수에 전달하는 인자가 약간 다른 것 같습니다.
6 ret = devm_request_threaded_irq(dev, host->irq, bcm2835_mmc_irq,
7 bcm2835_mmc_thread_irq, IRQF_SHARED,
8 mmc_hostname(mmc), host); 

6번째 줄을 보면 bcm2835_mmc_irq() 함수를 인터럽트 핸들러로 등록합니다. 이 함수는 92번 “mmc1” 인터럽트가 발생하면 호출되는 함수입니다. 

7번째 줄 코드를 보면 request_threaded_irq() 함수 세 번째 인자로 bcm2835_mmc_thread_irq() 함수를 전달합니다. 이 함수를 IRQ Thread 핸들러라고 합니다. IRQ Thread가 실행될 때 호출되는 핸들러 함수입니다.  

조금 더 이해를 돕기 위해 request_threaded_irq() 함수의 선언부를 보면 세 번째 인자로 irq_handler_t thread_fn가 선언돼 있습니다. 두 번째 인자는 인터럽트 핸들러 함수입니다.
[include/linux/interrupt.h]
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
     irq_handler_t thread_fn,
     unsigned long flags, const char *name, void *dev);

thread_fn 이란 함수 포인터에 bcm2835_mmc_thread_irq() 이란 IRQ Thread 핸들러 함수를 등록하는 것입니다.

이를 알기 쉬운 코드 형식으로 표현하면 아래와 같이 각각 인자를 다음과 같이 등록합니다.
인터럽트 번호irq = host->irq
인터럽트 핸들러:  handler = bcm2835_mmc_irq
IRQ Thread 핸들러: thread_fn = bcm2835_mmc_thread_irq

인터럽트가 발생했을 때 인터럽트 컨택스트에서 수행하는 인터럽트 핸들러는 bcm2835_mmc_irq() 함수이고 “irq/92-mmc1” IRQ Thread에서 실행하는 핸들러 함수는 bcm2835_mmc_thread_irq() 함수인 것입니다. 

다음은 IRQ 스레드를 생성하는 코드를 보면서 92번 인터럽트에 대한 IRQ 스레드 이름을 어떤 규칙으로 생성하는지 알아봅시다.
1 static int
2 setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
3 {
4 struct task_struct *t;
5 struct sched_param param = {
6 .sched_priority = MAX_USER_RT_PRIO/2,
7 };
8
9 if (!secondary) {
10 t = kthread_create(irq_thread, new, "irq/%d-%s", irq,   
11    new->name);
12 } else {
13 t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq,
14    new->name);
15 param.sched_priority -= 1;
16 }


이 정보를 참고하면 “irq=92 name=mmc1” 인터럽트의 IRQ 스레드 이름은 “irq/92-mmc1”라는 점을 유추할 수 있습니다. 

“irq/92-mmc1” IRQ 스레드는 언제 실행될까요? 이 내용은 다음 소절에서 자세히 다룰 예정인데, 92번 인터럽트가 발생하면 호출되는 인터럽트 핸들러가 IRQ 스레드 실행 여부를 결정합니다. 만약 92번 인터럽트가 발생하지 않으면 IRQ 스레드는 실행하지 않습니다.
 


IRQ 스레드란
리눅스 커널을 익히는 과정에서 만나는 걸림돌 중 하나가 어려운 용어입니다. 어려운 개념을 낯선 용어로 설명하니 이해하기 어렵죠. IRQ Thread의 의미를 알기 전에 IRQ란 용어부터 알아볼까요? IRQ는 Interrupt Request의 약자로 하드웨어에서 발생한 인터럽트를 처리 한다는 의미입니다. 조금 더 구체적으로 인터럽트 발생 후 인터럽트 핸들러까지 처리하는 흐름입니다. 

IRQ Thread란 뭘까요? 인터럽트 핸들러에서는 처리하면 오래 걸리는 일을 수행하는 프로세스입니다. 인터럽트 후반부 처리를 위한 인터럽트 처리 전용 프로세스입니다. 리눅스 커널에서는 IRQ Thread를 irq_thread라고도 합니다. 리눅스 커널 커뮤니티에서는 threaded IRQ 방식이라고도 합니다. 용어는 달라도 같은 의미입니다. 

IRQ Thread 기법은 인터럽트 후반부 처리를 IRQ Thread에서 수행하는 방식을 의미합니다. 이제 용어를 간단히 정리했으니 IRQ Thread에 대해 조금 더 알아볼게요. 

IRQ 스레드 확인하기
라즈베리안에서는 IRQ Thread를 어떻게 확인할 수 있을까요? ps -ely 명령어를 입력하면 프로세스 목록을 볼 수 있습니다. 
root@raspberrypi:/home/pi/dev_raspberrian# ps -ely
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S     0     1     0  0  80   0  6012  6750 SyS_ep ?        00:00:02 systemd
S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
//...
S     0    64     2  0  70 -10     0     0 down_i ?        00:00:00 SMIO
S     0    65     2  0   9   -     0     0 irq_th ?        00:00:00 irq/92-mmc1
I     0    66     2  0  80   0     0     0 worker ?        00:00:00 kworker/0:3

위 목록에서 irq/92-mmc1가 보이죠. 이 프로세스가 IRQ Thread입니다. 커널에서 지어준 이름인데 다음 규칙에 따라 이름을 짓습니다.
"irq/인터럽트 번호-인터럽트 이름"

irq/92-mmc1프로세스는 위 규칙에 따라 어떻게 해석하면 될까요? mmc1이란 이름의 92번째 인터럽트를 처리하는 IRQ Thread라 할 수 있습니다. 라즈베리안에서는 위와 같이 1개의 IRQ Thread만 볼 수 있는데, 다른 리눅스 시스템에서는 보통 10개 이상 IRQ Thread를 확인할 수 있습니다.

이번에는 다른 리눅스 시스템에서 IRQ Thread를 확인해볼까요? 
다음 로그는 안드로이드를 탑재한 Z5 compact 제품(Snapdragon 810)의 리눅스 개발자 커뮤니티에서 공유된 로그입니다.
1  root      14    2     0      0     worker_thr 0000000000 S kworker/1:0H
2  root      15    2     0      0     smpboot_th 0000000000 S migration/2
3  root      16    2     0      0     smpboot_th 0000000000 S ksoftirqd/2
//...
4  root      36    2     0      0     rescuer_th 0000000000 S rpm-smd
5  root      38    2     0      0     irq_thread 0000000000 S irq/48-cpr
6  root      39    2     0      0     irq_thread 0000000000 S irq/51-cpr
//...
7  root      199   2     0      0     irq_thread 0000000000 S irq/212-msm_dwc
//...
8  root      65    2     0      0     irq_thread 0000000000 S irq/261-msm_iom
9  root      66    2     0      0     irq_thread 0000000000 S irq/263-msm_iom
10 root      67    2     0      0     irq_thread 0000000000 S irq/261-msm_iom
11 root      68    2     0      0     irq_thread 0000000000 S irq/263-msm_iom
12 root      69    2     0      0     irq_thread 0000000000 S irq/261-msm_iom
13 root      70    2     0      0     irq_thread 0000000000 S irq/263-msm_iom
14 root      71    2     0      0     irq_thread 0000000000 S irq/261-msm_iom
15 root      72    2     0      0     irq_thread 0000000000 S irq/263-msm_iom
16 root      73    2     0      0     irq_thread 0000000000 S irq/261-msm_iom
17 root      74    2     0      0     irq_thread 0000000000 S irq/263-msm_iom
18 root      75    2     0      0     irq_thread 0000000000 S irq/261-msm_iom

ps 명령어로 출력한 전체 프로세스 중 IRQ Thread만 가려냈는데요. irq_thread 타입으로 볼드체로 표시한 프로세스가 IRQ Thread입니다. IRQ Thread가 14개나 있네요.

위 로그 중에 “irq/212-msm_dwc”란 IRQ Thread를 해석하면 msm_dwc란 이름의 212번째 인터럽트를 처리하는 IRQ Thread라고 볼 수 있겠죠. 
7 root      199   2     0      0     irq_thread 0000000000 S irq/212-msm_dwc

이렇게 상용 리눅스 시스템의 IRQ Thread 개수를 소개한 이유는 여러분이 IRQ Thread는 1개 밖에 없다고 착각할 수 있기 때문입니다. 왜냐면, 라즈베리안에서 IRQ Thread가 1개 밖에 없으니까요. 

이렇게 라즈베리안에 비해 상용 안드로이드 리눅스 디바이스에서는 IRQ Thread가 더 많은 것 같습니다. IRQ Thread가 더 많으면 더 좋은 시스템일까요? 그렇지는 않습니다. 이는 시스템에 인터럽트를 어떻게 설계하고 구성했는지에 따라 다릅니다. 

어떤 인터럽트가 발생하면 인터럽트 핸들러 이후에 실행되는 IRQ Thread가 있다는 것은 뭘 의미할까요? 해당 인터럽트가 발생하면 소프트웨어적으로 할 일이 많다고 봐야겠죠. 인터럽트가 발생했을 때 바로 해야 하는 일은 인터럽트 핸들러에서 처리하고, 조금 후 프로세스 레벨에서 해도 되는 일은 IRQ Thread에서 수행하는 것입니다.

IRQ Thread를 생성하기 위해서는 어떻게 해야 하죠? 방법은 간단합니다. 인터럽트 핸들러를 설정하는 request_irq 함수 대신 request_threaded_irq 을 호출하면 됩니다.

request_threaded_irq 함수의 원형은 다음과 같은데요. 세 번째 파라미터인 thread_fn에 IRQ Thread가 실행되면 호출할 핸들러만 지정해주면 됩니다. 그러면 커널에서 인터럽트 이름과 번호 정보를 바탕으로 IRQ Thread를 생성해 줍니다.
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
     irq_handler_t thread_fn,
     unsigned long flags, const char *name, void *dev);

여기서 Top Half는 인터럽트 핸들러인 handler 그리고 Bottom Half는 IRQ Thread가 수행되면 수행하는 핸들러 함수인 thread_fn 라고 할 수 있겠죠.

실제 request_threaded_irq를 호출하는 코드를 보면서 이 내용을 함께 알아볼까요? 다음 코드는 다른 리눅스 시스템에서 USB 드라이버 설정 코드입니다. 
1 static int dwc3_gadget_start(struct usb_gadget *g,
struct usb_gadget_driver *driver)
3 {
4 struct dwc3 *dwc = gadget_to_dwc(g);
5 unsigned long flags;
6 int ret = 0;
7 int irq;
8
9 irq = dwc->irq_gadget;
10 ret = request_threaded_irq(irq, dwc3_interruptdwc3_thread_interrupt,
11 IRQF_SHARED, "dwc3", dwc->ev_buf);

인터럽트 핸들러를 설정할 때 썼던 request_irq 함수와 유사해 보이네요. 그런데 request_threaded_irq 함수도 비슷하게 인터럽트 핸들러를 등록하는 것 같습니다. request_irq를 호출할 때 같은 인자를 채우는데, request_threaded_irq는 IRQ Thread 핸들러인 dwc3_thread_interrupt 함수를 추가하는군요.

request_threaded_irq 함수 호출로 해당 인터럽트에 대한 전용 IRQ Thread가 생성됩니다. 리눅스 커널에서 이 IRQ Thread 이름을 어떻게 지을까요? 위 인터럽트 번호가 47이면  "irq/47-dwc3"이 되겠죠.

인터럽트 발생 후 dwc3_interrupt란 인터럽트 핸들러에서 인터럽트에 대한 처리를 한 다음 "irq/47-dwc3" IRQ Thread를 깨울지 결정합니다. 이후 "irq/47-dwc3" IRQ Thread가 깨어나면 dwc3_thread_interrupt 함수가 호출되는 구조죠.

해당 코드를 볼까요? dwc 인터럽트가 발생하면 dwc3_interrupt란 인터럽트 핸들러가 실행되겠죠? 다음 dwc3_interrupt 함수를 보면 바로 dwc3_check_event_buf 함수를 호출하는군요. 
static irqreturn_t dwc3_interrupt(int irq, void *_evt)
{
struct dwc3_event_buffer *evt = _evt;
return dwc3_check_event_buf(evt);
}

dwc3_check_event_buf 함수 구현부는 다음과 같습니다.
1 static irqreturn_t dwc3_check_event_buf(struct dwc3_event_buffer *evt)
2 {
3 struct dwc3 *dwc = evt->dwc;
4 u32 amount;
5 u32 count;
6 u32 reg;
7
8 if (pm_runtime_suspended(dwc->dev)) {
9 pm_runtime_get(dwc->dev);
10 disable_irq_nosync(dwc->irq_gadget);
11 dwc->pending_events = true;
12 return IRQ_HANDLED;
13 }
//...
14 if (amount < count)
15 memcpy(evt->cache, evt->buf, count - amount);
16
17 dwc3_writel(dwc->regs, DWC3_GEVNTCOUNT(0), count);
18
19 return IRQ_WAKE_THREAD;
20}

위 함수를 눈여겨보면 시스템 상태에 따라 IRQ_HANDLED와 IRQ_WAKE_THREAD를 리턴합니다. 인터럽트가 발생한 후 일을 더 할 필요가 없을 때는 다음 코드와 같이 IRQ_HANDLED를 리턴합니다.
8 if (pm_runtime_suspended(dwc->dev)) {
9 pm_runtime_get(dwc->dev);
10 disable_irq_nosync(dwc->irq_gadget);
11 dwc->pending_events = true;
12 return IRQ_HANDLED;
13 }

특정 조건으로 IRQ Thread가 해당 인터럽트에 대한 처리를 더 수행해야 할 때는 IRQ_WAKE_THREAD를 리턴합니다.
17 dwc3_writel(dwc->regs, DWC3_GEVNTCOUNT(0), count);
18
19 return IRQ_WAKE_THREAD;
20}

이후 IRQ Thread가 깨어난 후 IRQ Thread 핸들러인 dwc3_thread_interrupt가 인터럽트 핸들러에서 바로 하지 못한 일을 수행합니다. 

여기까지 라즈베리안에서 실행하는 드라이버 코드는 아니지만, 다른 시스템 드라이버에서는 어떤 방식으로 request_threaded_irq을 호출했는지 간단히 리뷰했습니다. 다음 시간에는 라즈베리안에서 IRQ Thread가 어떻게 실행하는지 더 자세히 알아볼까요?


인터럽트 후반부 기법을 적용하는 이유

인터러트 후반부 기법을 쓰는 이유에 대해 알아보기 전에 커널이 인터럽트를 어떤 방식으로 처리하는지 살펴볼 필요가 있습니다. 5장에서 배운 내용을 정리해보겠습니다.
   1. 인터럽트가 발생하면 커널은 실행 중인 프로세스를 멈추고 인터럽트 벡터를 실행해서 인터럽트 핸들러를 실행합니다.
   2. 인터럽트 핸들러는 짧고 빨리 실행해야 합니다.
   3. 인터럽트를 처리하는 구간이 인터럽트 컨택스트인데 이를 in_interrupt() 함수가 알려줍니다.

인터럽트 후반부 기법을 적용해야 하는 이유는 인터럽트 컨택스트에서 빨리 실행을 끝내야 하기 때문입니다. 인터럽트는 실행 중인 코드를 멈추고 인터럽트를 핸들링하기 때문입니다.

자연스럽게 임베디드 리눅스 개발자뿐만 아니라 임베디드 개발에서 다음과 같은 고민을 하게됐습니다.
인터럽트 핸들러나 인터럽트 컨택스트에서 어떻게 하면 빨리 코드를 처리해야 할까?

여러 고민 끝에 인터럽트가 발생하면 이를 처리할 코드를 2 단계로 나누게 됐습니다. 빨리 실행해야 할 코드는 인터럽트 컨택스트, 실시간으로 빨리 처리하지 않아도 되는 코드는 인터럽트를 핸들링한 다음에 실행하는 것입니다. 이 과정에서 다음과 같은 인터럽트 후반부 처리 기법을 이끌어낸 것입니다. 

이를 위해 커널은 다음과 같은 인터럽트 후반부 처리 방식을 지원합니다.
   - IRQ 스레드
   - Soft IRQ
   - 태스크릿
   - 워크큐

다음 소절에서는 인터럽트 컨택스트에서 시간이 오래 걸리는 코드를 수행하면 어떤 일이 발생하는지 살펴보겠습니다.

인터럽트 컨택스트에서 많은 일을 하면 어떻게 될까?

인터럽트 후반부 처리 기법을 왜 적용해야 하는지 설명드리기 위해 인터럽트 컨택스트에서 많은 일을 하는 코드를 적용하면 시스템이 어떻게 오작동하는지 소개합니다.

디바이스 드라이버를 개발하다 보면 인터럽트 컨택스트에서 인터럽트 핸들링을 할 때 많은 일을 하는 코드를 입력할 때가 있습니다. 대표적으로 다음과 같은 예를 들 수 있습니다.
   - I/O을 시작하는 코드
   - 과도한 while loop
   - 유저 공간으로uevent를 전달해서 인터럽트 발생을 알림
   - 스케줄링을 지원하는 커널 함수 호출

위와 같은 코드를 입력하면 시스템 반응 속도가 아주 느려지거나 평소에 볼 수 없는 오류 메시지를 볼 수 있습니다. 

인터럽트 핸들러와 서브 루틴에서 코드를 작성할 때 빨리 실행하는 코드를 입력해야 합니다. 여기서 중요한 의문점이 생깁니다.
인터럽트 컨택스트에서 인터럽트를 핸들링 하는 코드 실행 시간을 어떻게 측정할 수 있을까? 

이를 위해 ftrace 기능에서 지원하는 graph_function 트레이서 기능을 써서 인터럽트 핸들링을 할 때 얼마나 시간이 걸리는 지 측정할 필요가 있습니다. 이번 장 디버깅 장에서 이 방식에 대해 소개합니다.

심지어 인터럽트 컨택스트에서 스케줄링을 지원하는 함수를 쓸 경우 커널은 강제 커널 크래시를 유발합니다. 

여기서 인터럽트 핸들러에서 많은 일을 하다가 커널 패닉이 발생하는 예를 들어 보겠습니다.  다음 로그는 인터럽트 핸들러 실행 도중 발생한 커널 패닉 로그입니다. 함수가 일렬로 정렬해 있습니다.
01 [21.719319] [callstack mxt_interrupt,2449] task[InputReader]========= 
02 [21.719382] BUG: scheduling while atomic: InputReader/1039/0x00010001
03 [21.719417] (unwind_backtrace+0x0/0x144) from (dump_stack+0x20/0x24)
04 [21.719432] (dump_stack+0x20/0x24) from (__schedule_bug+0x50/0x5c)
05 [21.719444] (__schedule_bug+0x50/0x5c) from (__schedule+0x7c4/0x890)
06 [21.719455] (__schedule+0x7c4/0x890) from [<c0845d70>] (schedule+0x40/0x80)
07 [21.719468] (schedule+0x40/0x80) from [<c0843bc0>] (schedule_timeout+0x190/0x33c)
08 [21.719480] (schedule_timeout+0x190/0x33c) from (wait_for_common+0xb8/0x15c)
09 [21.719491] (wait_for_common+0xb8/0x15c) from (wait_for_completion_timeout+0x1c/0x20)
10 [21.719504] (wait_for_completion_timeout+0x1c/0x20) from (tegra_i2c_xfer_msg+0x380/0x958)
11 [21.719517] (tegra_i2c_xfer_msg+0x380/0x958) from (tegra_i2c_xfer+0x314/0x438)
12 [21.719531] (tegra_i2c_xfer+0x314/0x438) from (i2c_transfer+0xc4/0x128)
13 [21.719546] (i2c_transfer+0xc4/0x128) from (__mxt_read_reg+0x70/0xc8)
14 [21.719560] (__mxt_read_reg+0x70/0xc8) from (mxt_read_and_process_messages+0x58/0x1648)
15 [21.719572] (mxt_read_and_process_messages+0x58/0x1648) from (mxt_interrupt+0x78/0x144)
16 [21.719588] (mxt_interrupt+0x78/0x144) from (handle_irq_event_percpu+0x88/0x2ec)
17 [21.719601] (handle_irq_event_percpu+0x88/0x2ec) from (handle_irq_event+0x4c/0x6c)
18 [21.719614] (handle_irq_event+0x4c/0x6c) from (handle_level_irq+0xbc/0x118)
19 [21.719626] (handle_level_irq+0xbc/0x118) from (generic_handle_irq+0x3c/0x50)
20 [21.719642] (generic_handle_irq+0x3c/0x50) from (tegra_gpio_irq_handler+0xa8/0x124)
21 [21.719655] (tegra_gpio_irq_handler+0xa8/0x124) from (generic_handle_irq+0x3c/0x50)
22 [21.719669] (generic_handle_irq+0x3c/0x50) from (handle_IRQ+0x5c/0xbc)
23 [21.719682] (handle_IRQ+0x5c/0xbc) from (gic_handle_irq+0x34/0x68)
24 [21.719694] (gic_handle_irq+0x34/0x68) from (__irq_svc+0x40/0x70)

참고로 위 로그가 동작한 시스템은 엔비디아 Tegra4i SoC 디바이스입니다. 그래서 tegra가 붙은 함수들이 보입니다. 라즈베리파이 이외에 다른 리눅스 시스템에서 인터럽트를 어떻게 처리하는지 알면 좋으니 리눅스 시스템에서 발생한 문제를 소개합니다.

로그를 꼼꼼히 분석하겠습니다. 함수들이 줄 서 있는데 어느 부분 로그부터 읽어봐야 할까요? 함수들이 가장 먼저 실행된 순서로 정렬돼 있으니 가장 아랫부분 로그부터 봐야 합니다. 이제부터 5장에서 배운 내용을 떠 올리면서 로그 분석을 시작합니다.
 
가장 처음 실행된 함수 로그부터 보겠습니다. 24 번째 줄 코드를 눈으로 따라가 보면 __irq_svc 레이블이 보일 것입니다. 
21 [21.719655] (tegra_gpio_irq_handler+0xa8/0x124) from (generic_handle_irq+0x3c/0x50)
22 [21.719669] (generic_handle_irq+0x3c/0x50) from (handle_IRQ+0x5c/0xbc)
23 [21.719682] (handle_IRQ+0x5c/0xbc) from (gic_handle_irq+0x34/0x68)
24 [21.719694] (gic_handle_irq+0x34/0x68) from (__irq_svc+0x40/0x70)

인터럽트가 발생했다는 사실을 알 수 있습니다. 우리는 인터럽트가 발생하면 인터럽트 벡터인 __irq_svc가 실행한다는 것을 5장에서 배웠습니다. 또한 인터럽트 벡터인 __irq_svc 함수부터 실행된 콜스택(함수 흐름)이니 인터럽트 컨택스트입니다. 

다음 14~16 번째 로그를 보겠습니다.
14 [21.719560] (__mxt_read_reg+0x70/0xc8) from (mxt_read_and_process_messages+0x58/0x1648)
15 [21.719572] (mxt_read_and_process_messages+0x58/0x1648) from (mxt_interrupt+0x78/0x144)
16 [21.719588] (mxt_interrupt+0x78/0x144) from (handle_irq_event_percpu+0x88/0x2ec)

아래 로그로 인터럽트 핸들러로 mxt_interrupt() 함수가 호출됐다는 사실을 알 수 있습니다. 

우리는 5장에서 인터럽트 핸들러는 __handle_irq_event_percpu() 함수에서 호출한다고 배웠습니다. 그런데 위 로그에서는 handle_irq_event_percpu() 함수에서 인터럽트 핸들러를 호출합니다.

그 이유는 이 로그를 출력한 시스템의 리눅스 커널 버전이 3.10.77 버전이기 때문입니다. 다음 코드를 보면 5번째 줄 코드에서 인터럽트 핸들러를 호출합니다.
1 irqreturn_t
2 handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
3 {
...
4 do {
...
5 res = action->handler(irq, action->dev_id);

리눅스 커널 버전에 따라 인터럽트를 처리하는 함수가 달라질 수 있습니다.

다음 로그를 보겠습니다. 커널 패닉이 발생한 이유를 출력하고 있습니다. 
05 [21.719444] (__schedule_bug+0x50/0x5c) from (__schedule+0x7c4/0x890)
06 [21.719455] (__schedule+0x7c4/0x890) from [<c0845d70>] (schedule+0x40/0x80)
07 [21.719468] (schedule+0x40/0x80) from [<c0843bc0>] (schedule_timeout+0x190/0x33c)
08 [21.719480] (schedule_timeout+0x190/0x33c) from (wait_for_common+0xb8/0x15c)
09 [21.719491] (wait_for_common+0xb8/0x15c) from (wait_for_completion_timeout+0x1c/0x20)
10 [21.719504] (wait_for_completion_timeout+0x1c/0x20) from (tegra_i2c_xfer_msg+0x380/0x958)
11 [21.719517] (tegra_i2c_xfer_msg+0x380/0x958) from (tegra_i2c_xfer+0x314/0x438)
12 [21.719531] (tegra_i2c_xfer+0x314/0x438) from (i2c_transfer+0xc4/0x128)
13 [21.719546] (i2c_transfer+0xc4/0x128) from (__mxt_read_reg+0x70/0xc8)

함수 흐름으로 보아 wait_for_common() 함수를 호출해서 complete() 함수가 수행되기를 기다리는 상황입니다. 그런데 complete() 함수 호출을 안 하니 schedule_timeout() 함수를 호출합니다.

schedule_timeout() 함수에서 다음 순서로 함수를 호출합니다. 
__schedule() -> __schedule_bug() 

이번에 커널 패닉이 발생하는 이유를 알려주는 로그를 소개합니다. 
01 [21.719319] [callstack mxt_interrupt,2449] task[InputReader]========= 
02 [21.719382] BUG: scheduling while atomic: InputReader/1039/0x00010001

인터럽트 컨택스트에서 스케줄링을 하니 커널은 이를 감지하고 커널 패닉을 유발하는 것입니다.

02 번째 줄 로그를 보면 InputReader는 프로세스 이름, pid는 1039 그리고 0x00010001는 프로세스 struct thread_info구조체 preempt_count 필드 값입니다. 

프로세스 struct thread_info구조체 preempt_count 필드가 0x00010001 이므로 현재 실행 중인 코드를 인터럽트 컨택스트입니다. 0x00010001와 HARDIRQ_OFFSET(0x10000) 를 AND 비트 연산한 결과가 1이기 때문입니다.

위 로그에서 “scheduling while atomic”란 메시지가 보입니다. 메시지를 그대로 풀면 “아토믹 도중에 스케줄링 중이다.”라고 볼 수 있습니다.

여기서 atomic이란 무슨 의미일까요? 커널에서는 다음과 같은 상황을 atomic이라고 말합니다.
   - 선점 스케줄링이 되지 않는 실행 단위(어셈블리 명령어) 
   - 어떤 코드나 루틴이 실행 도중 스케줄링을 수행하면 안되는 컨택스트

커널에서는 인터럽트 컨택스트도 실행 도중 스케줄링하면 안되는 컨택스트로 판단합니다. 즉 인터럽트 컨택스트는 atomic operation입니다. 따라서 경고 메시지를 출력하는 겁니다. (atomic operation은 커널 동기화 장에서 자세히 다룹니다.)

정리하면 인터럽트 컨택스트에서 스케줄링을 시도하니 커널은 이를 감지하고 커널 패닉을 유발하는 겁니다. 

임베디드 개발에서 다음과 같은 고민이 생겼습니다.
인터럽트 컨택스트에선 빨리 일을 해야 하는데, 어떻게 해야 이런 문제를 피할 수 있을까? 

이 과정에서 Bottom Half와 Top Half란 개념을 정립했으며 이 기준으로 인터럽트 후반부 처리 기법을 적용하기 시작했습니다. 


Top Half/Bottom Half 란 무엇일까?

이전 소절에선 인터럽트 컨택스트에서 인터럽트 핸들링을 빠른 시간에 마무리해야 한다고 설명을 드렸습니다. 또한 시간이 오래 걸리는 함수를 호출하면 시스템은 커널 패닉과 같이 시스템이 오동작 할 수 있다고 소개했습니다.

여기서 한 가지 의문이 생깁니다.
인터럽트가 발생하면 인터럽트 핸들러에서 처리할 일이 많을 때는 어떻게 해야 할까? 

이럴 때 해야 할 일을 2가지로 나누면 됩니다. 
빨리 처리해야 하는 일과 조금 있다가 처리해도 되는 일입니다. 임베디드 용어로 인터럽트가 발생 후 빨리 처리해야 하는 일은 Top Half, 조금 있다가 처리해도 되는 일은 Bottom Half라고 말합니다. 

인터럽트 핸들러가 하는 일은 Top Half라고 할 수 있습니다. Bottom Half는 인터럽트에 대한 처리를 프로세스 레벨에서 수행하는 방식입니다. 

리눅스 커널에서 Bottom Half을 어떤 방식으로 구현할까요? 
인터럽트 핸들러는 일하고 있던 프로세스를 멈춘 시점인 인터럽트 컨택스트에서 실행합니다. 급하게 처리해야 할 일은 인터럽트 컨택스트에서 처리하고 조금 후 실행해도 되는 일은 프로세스 레벨에서 처리합니다. 이를 위해 커널에서 대표적으로 다음과 같은 기법을 인터럽트 후반부 기법으로 지원합니다.
   - IRQ 스레드
   - Soft IRQ
   - 워크큐

인터럽트 컨택스트와 커널 쓰레드 레벨에서 어떤 코드를 동작할 때 어떤 차이점이 있을까요?
우선 인터럽트 컨택스트에서는 호출할 수 있는 함수가 제한돼 있습니다. 

리눅스 커널에서는 인터럽트 컨택스트에서 많은 일을 하는 함수를 호출할 때 경고 메시지를 출력하거나 커널 패닉을 유발해서 시스템 실행을 중단시킵니다. 예를 들어 스케줄링을 지원하는 뮤텍스나 schedule() 함수를 쓰면 커널은 강제로 커널 패닉을 유발합니다.

뮤텍스 함수는 스케줄링 동작과 연관돼 있습니다. 프로세스가 뮤텍스를 획득하려고 시도하는데 만약 다른 프로세스가 이미 뮤텍스를 획득했으면 휴면에 진입합니다.

그런데 인터럽트 컨택스트에 비해 커널 쓰레드에서는 커널이 제공하는 스케쥴링을 포함한 모든 함수를 쓸 수 있습니다. 그래서 시나리오에 따라 유연하게 코드를 설계할 수 있습니다.

예를 들어 인터럽트가 발생했을 때 이를 유저 공간에 알리고 싶을 경우가 있습니다. 안드로이드 디바이스 같은 경우에 터치를 입력하면 발생하는 인터럽트를 uevent로 유저 공간에 알릴 수 있습니다. 유저 공간에 uevent를 보내는 동작은 시간이 오래 걸리는 일입니다. 따라서 시간이 오래 걸리는 코드는 인터럽트 후반부에서 처리하도록 드라이버 구조를 잡아야 합니다.

인터럽트 후반부 처리 기법 종류 및 소개

이번 장에서는 리눅스 커널이 Bottom Half을 처리하는 대표적인 기법인 IRQ 스레드와 Soft IRQ 기법을 다룹니다. 워크큐는 워크큐를 다루는 장에서 살펴볼 예정입니다. 세 가지 기법 인터럽트 후반부 처리하는 방식이 조금씩 다릅니다. 하지만 인터럽트 핸들러에서 해야 할 일을 2 단계로 나눈다는 점은 같습니다. 

이 세 가지 기법의 특징이 뭔지 알아보겠습니다.
IRQ 스레드
인터럽트를 처리하는 전용 IRQ 스레드에서 인터럽트 후속 처리를 합니다. 만약 rasp란 24번 인터럽트가 있으면 “irq/24-rasp”란 IRQ 스레드가 24번 인터럽트 후반부를 전담해서 처리합니다.

Soft IRQ
인터럽트 핸들러 실행이 끝나면 바로 일을 시작합니다. 인터럽트 핸들러 바로 처리해야 할 일을 마무리한 후 인터럽트 후반부 처리를 Soft IRQ 컨택스트에서 실행합니다. Soft IRQ 서비스 핸들러 실행 도중 시간이 오래 걸리면 ksoftirqd란 프로세스를 깨우면 Soft IRQ 서비스를 종료합니다. 
ksoftirqd란 프로세스에서 나머지 인터럽트 후반부를 처리하는 구조입니다.  

워크큐
인터럽트 핸들러가 실행될 때 워크를 워크큐에 큐잉하고 프로세스 레벨의 워커 쓰레드에서 인터럽트 후반부 처리를 하는 방식입니다.

그러면 위에서 세 가지 기법 중 어떤 방식을 인터럽트 후반부 처리로 적용해야 할까요?
사실 인터럽트를 처리하는 드라이버를 작성할 때 어떤 기법을 쓸 지는 드라이버 담당자의 몫입니다. 인터럽트 발생 빈도와 이를 처리하는 시나리오에 따라 위 세 가지 기법을 적절히 조합해서 드라이버 코드를 작성해야 합니다. 이를 위해서 인터럽트를 시스템에서 처리하는 방식과 인터럽트가 얼마나 자주 발생하는지를 알아야 합니다. 

리눅스 디바이스 드라이버를 개발할 때 다양한 하드웨어 디바이스를 인터럽트로 제어합니다. 또한 인터럽트를 관리하는 방식도 리눅스 시스템 마다 다릅니다.

이런 다양한 상황에서 어떤 인터럽트 방식을 적용해야 할지를 Q/A로 정리하면 다음과 같습니다.
Q: 인터럽트가 1초에 수 백번 발생하는 디바이스의 경우 어떤 인터럽트 후반부 기법을 적용해야 할까?

A: IRQ 스레드 방식과 워크큐 방식은 그리 적합하지 않습니다. IRQ 스레드는 RT 프로세스로 구동됩니다. 인터럽트가 많이 발생하면 IRQ 스레드를 깨워야 하고 IRQ 스레드는 RT 프로세스로 구동하므로 다른 프로세스들이 선점 스케줄링을 할 수 없습니다. IRT 스레드 핸들러 실행 시간이 조금이라도 길어지면 다른 프로세스들이 실행을 못하고 대기해야 하므로 시스템 반응 속도가 느려질 수 있습니다.

만약 IRQ 스레드 방식을 적용해야 한다면 IRQ 스레드 핸들러 함수 실행 시간이 매우 짧아야 합니다. 예를 들면, IRQ 스레드 핸들러 함수에 printk() 함수와 같이 커널 로그를 출력하는 코드도 되도록 입력하지 말아야 합니다.

또한 워크큐를 실행하는 워커 스레드는 일반 프로세스로 프로세스 우선 순위가 높지 않습니다.
인터럽트 발생 횟수만큼 워크 핸들러가 실행을 못할 수 있습니다.

따라서 인터럽트가 자주 발생하는 디바이스는 Soft IRQ나 태스크릿 방식을 적용하는 것이 바람직합니다.

Q. 현재 개발 중인 시스템은 인터럽트 개수가 200개 정도된다. 어떤 방식을 적용하면 좋을까?

1초에 인터럽트가 수 백번 발생하는 경우를 제외하곤 IRQ 스레드 방식을 적용하면 별 문제가 없습니다. 그런데 인터럽트 개수만큼 IRQ 스레드를 생성하면 기본으로 프로세스를 관리할 때 필요한 태스크 디스크립터와 같은 메모리 공간을 써야 합니다. 만약 현재 개발 중인 시스템 RAM 용량이 8G 이상이면 별 문제가 되지 않을 것입니다.
인터럽트가 발생 빈도가 낮고 빠른 시간에 인터럽트 후반부를 처리하지 않아도 될 경우 워크큐 기법을 적용하는 것도 좋습니다.

어떤 인터럽트 후반부 방식을 적용할지 결정하는 것은 어려운 일입니다. 정답이 없기 때문입니다. 이런 상황에서 필요한 것이 최적화인 것 같습니다. 인터럽트 후반부 단계에서 인터럽트 처리를 최적화하도록 설계를 잘 하려면 먼저 커널이 인터럽트를 처리하는 세부 동작과 인터럽트 후반부 기법들의 세부 구현 방식을 잘 알고 있어야 합니다.

다음 절에서 인터럽트 후반부 기법 중 IRQ 스레드 방식을 살펴보겠습니다.
 


Quite interesting patch was released under the following url.

https://source.codeaurora.org/quic/la/kernel/msm-3.18/commit/?h=rel/msm-3.18&id=a0039b1e721b7b3ee1cbe7f7f9d44d451ac74543


The detailed patch is to initialize the stack array with a different way as below.

-------------------------------------------------------------------------------------

usb : dwc3: Initialize kernel stack variables properly

If kernel stack variables are not initialized properly,

there  is a chance of kernel information disclosure.

So, initialize kernel stack variables with null characters.


-rw-r--r-- drivers/usb/dwc3/debugfs.c 10

1 files changed, 4 insertions, 6 deletions

diff --git a/drivers/usb/dwc3/debugfs.c b/drivers/usb/dwc3/debugfs.c

index ce7cd96..fb252ec 100644

--- a/drivers/usb/dwc3/debugfs.c

+++ b/drivers/usb/dwc3/debugfs.c

@@ -402,7 +402,7 @@ static ssize_t dwc3_mode_write(struct file *file,

  struct dwc3 *dwc = s->private;

  unsigned long flags;

  u32 mode = 0;

- char buf[32];

+ char buf[32] = {0};


  if (copy_from_user(&buf, ubuf, min_t(size_t, sizeof(buf) - 1, count)))

  return -EFAULT;

@@ -482,7 +482,7 @@ static ssize_t dwc3_testmode_write(struct file *file,

  struct dwc3 *dwc = s->private;

  unsigned long flags;

  u32 testmode = 0;

- char buf[32];

+ char buf[32] = {0};


  if (copy_from_user(&buf, ubuf, min_t(size_t, sizeof(buf) - 1, count)))

  return -EFAULT;

@@ -589,7 +589,7 @@ static ssize_t dwc3_link_state_write(struct file *file,

  struct dwc3 *dwc = s->private;

  unsigned long flags;

  enum dwc3_link_state state = 0;

- char buf[32];

+ char buf[32] = {0};


  if (copy_from_user(&buf, ubuf, min_t(size_t, sizeof(buf) - 1, count)))

  return -EFAULT;

@@ -630,12 +630,10 @@ static ssize_t dwc3_store_ep_num(struct file *file, const char __user *ubuf,

 {

  struct seq_file *s = file->private_data;

  struct dwc3 *dwc = s->private;

- char kbuf[10];

+ char kbuf[10] = {0};

  unsigned int num, dir, temp;

  unsigned long flags;


- memset(kbuf, 0, 10);

-

  if (copy_from_user(kbuf, ubuf, count > 10 ? 10 : count))

  return -EFAULT;

-------------------------------------------------------------------------------------


In order to find out how the assemble code is updated after compling the patch-set, let me walk through more within dwc3_mode_write() .

The interesting debug information inside updated assemble code after "char buf[32] = {0};" is declared is that memset is appended by ARM-GCC compiler

[1]: R1 is updated as 0x0.

[2]: R2 is updated as 32 in decimal format.

[3]: R0 is belonging to stack memory area.

[4]: Call to memset is made.

memset(R0: buf, R1: 0x0, R2: 32)


[After]

  static ssize_t dwc3_mode_write(struct file *file,

                  const char __user *ubuf, size_t count, loff_t *ppos)

  {

 c0629d9c:       e1a0c00d        mov     ip, sp

 c0629da0:       e92ddbf0        push    {r4, r5, r6, r7, r8, r9, fp, ip, lr, pc}

 c0629da4:       e24cb004        sub     fp, ip, #4

 c0629da8:       e24dd028        sub     sp, sp, #40     ; 0x28

 c0629dac:       e52de004        push    {lr}            ; (str lr, [sp, #-4]!)

 c0629db0:       ebe7958c        bl      c000f3e8 <__gnu_mcount_nc>

 c0629db4:       e59f7130        ldr     r7, [pc, #304]  ; c0629eec <dwc3_mode_write+0x150>

 c0629db8:       e1a04001        mov     r4, r1

 c0629dbc:       e1a05002        mov     r5, r2

 c0629dc0:       e3a01000        mov     r1, #0  //<<--[1]

 c0629dc4:       e3a02020        mov     r2, #32  //<<--[2]

 c0629dc8:       e5973000        ldr     r3, [r7]

 c0629dcc:       e50b3028        str     r3, [fp, #-40]  ; 0xffffffd8

 c0629dd0:       e59030bc        ldr     r3, [r0, #188]  ; 0xbc

 c0629dd4:       e24b0048        sub     r0, fp, #72     ; 0x48  //<<--[3]

 c0629dd8:       e5936060        ldr     r6, [r3, #96]   ; 0x60    

 c0629ddc:       ebf2ec6f        bl      c02e4fa0 <memset>        //<<--[4]  

 c0629de0:       e1a0200d        mov     r2, sp                    

 c0629de4:       e3c21d7f        bic     r1, r2, #8128   ; 0x1fc0


[Before]

[1]: Assemble code before the patch is applied, the stack memory location mapped to buf[32] is saved into R0 without initilization.

 static ssize_t dwc3_mode_write(struct file *file,

                 const char __user *ubuf, size_t count, loff_t *ppos)

 {

 c0629d9c:       e1a0c00d        mov     ip, sp

 c0629da0:       e92ddbf0        push    {r4, r5, r6, r7, r8, r9, fp, ip, lr, pc}

 c0629da4:       e24cb004        sub     fp, ip, #4

 c0629da8:       e24dd028        sub     sp, sp, #40     ; 0x28

 c0629dac:       e52de004        push    {lr}            ; (str lr, [sp, #-4]!)

 c0629db0:       ebe7958c        bl      c000f3e8 <__gnu_mcount_nc>

 c0629db4:       e1a05002        mov     r5, r2

//.. skip ,,

 c0629dfc:       1a000005        bne     c0629e18 <dwc3_mode_write+0x7c>

 c0629e00:       e24b0048        sub     r0, fp, #72     ; 0x48  //<<--[1]

 c0629e04:       e1a02003        mov     r2, r3

 c0629e08:       ebf2e301        bl      c02e2a14 <__copy_from_user>

 c0629e0c:       e3500000        cmp     r0, #0


Let me make it a rule to initialize the stack array like char kbuf[10] = {0}; instead of char kbuf[10];

+ Recent posts