Linux Kernel(4.14) Hacks

rousalome.egloos.com

포토로그 Kernel Crash




[리눅스커널] 프로세스 - 프로세스는 어떻게 생성할까? [라즈베리파이] 커널 프로세스

프로세스에 대한 이해를 하려면 프로세스가 어떻게 생성되는지 알면 좋습니다. 프로세스 생성 과정에서 프로세스를 관리하는 자료구조 관계를 알 수 있기 때문입니다. 

리눅스에서 구동되는 프로세스는 크게 유저 레벨에서 생성된 프로세스와 커널 레벨에서 생성된 프로세스가 있습니다. 

유저 레벨에서 생성된 프로세스는 유저 공간에서 프로세스를 생성하는 라이브러리(glibc) 도움을 받아 커널에게 프로세스 생성 요청을 합니다. 

커널 프로세스는 kthread_create() 함수를 호출해서 커널 내부에서 프로세스를 생성합니다. 커널 프로세스는 커널 스레드라고 부르며 커널 내부에서 스레드를 직접 관리합니다.

공통으로 리눅스에서 생성된 프로세스는 _do_fork() 함수를 호출합니다. 프로세스 생성하는 핵심함수는 _do_fork() 이니 이 함수를 중심으로 프로세스가 어떻게 생성되는지 알아봅시다.

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




[리눅스커널] 프로세스 소개(도입부) [라즈베리파이] 커널 프로세스

프로세스는 추상화된 개념이라 다양한 관점으로 바라 볼 수 있습니다. 이론으로 이해는 가지만 머리에 남기 어려운 내용이 프로세스입니다. 프로세스를 이론이 아닌 라즈베리파이를 직접 실행하면서 ftrace와 리눅스 커널 코드를 보면서 설명합니다. 이번에 소개되는 명령어나 ftrace로그는 라즈베리파이가 있는 분은 직접 실습하면서 익히길 바랍니다.

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




[리눅스커널] 프로세스 - 기본 유저레벨 프로세스 생성 실습 및 ftrace 로그 분석(1/2) [라즈베리파이] 커널 프로세스

다시 라즈베리파이에서 X-terminal 프로그램을 실행해서 셸을 엽시다. 
root@raspberrypi:/boot# ps -ely | grep bash
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   541   443  0  80   0  4024  1645 poll_s tty1     00:00:00 bash
S  1000   880   878  0  80   0  4008  1628 wait   pts/0    00:00:00 bash
S     0   977   972  0  80   0  3284  1416 wait   pts/0    00:00:00 bash
S  1000   993   989  0  80   0  3960  1628 poll_s pts/1    00:00:00 bash

grep bash 명령어로 현재 실행 중인 프로세스 중에 bash 프로세스를 출력합니다. 출력 결과 4개 bash 프로세스 목록을 볼 수 있습니다.

이 상태에서 X-terminal 셸을 하나 더 실행하고 다음 명령어를 입력해서 bash 프로세스 목록을 확인합시다.
root@raspberrypi:/boot# ps -ely | grep bash
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   541   443  0  80   0  4024  1645 poll_s tty1     00:00:00 bash
S  1000   880   878  0  80   0  4008  1628 wait   pts/0    00:00:00 bash
S     0   977   972  0  80   0  3284  1416 wait   pts/0    00:00:00 bash
S  1000   993   989  0  80   0  3960  1628 poll_s pts/1    00:00:00 bash
S  1000  1027   878  3  80   0  4036  1628 poll_s pts/2    00:00:00 bash

이전에 출력한 결과와 비교해봅시다. 맨 마지막 줄 로그를 보면 pid가 1027인 bash 프로세스가 보입니다. 셸을 하나 더 열고 “ps –ely” 명령어를 입력하니 bash(pid:1027)과 같이 새로 생성된 프로세스를 볼 수 있습니다. 이렇게 새로운 프로그램을 실행하면 이에 해당하는 프로세스가 생성됩니다.

라즈베리파이 X-Terminal 셸 화면을 마우스로 더블 클릭하는 순간 라즈베리파이 배경 화면을 처리하는 프로세스가 이벤트를 받아서 bash라는 프로세스를 생성합니다. 이 때 리눅스 저수준 함수인 fork()를 호출합니다. 이렇게 유저 레벨 프로세스는 셸이나 다른 프로세스를 통해서 실행을 시작합니다. 유저 레벨 프로세스는 혼자서 실행할 수 없습니다.

이번에는 라즈베리파이에서 소스 에디터로 많이 쓰는 Geany란 프로그램을 열겠습니다.
root@raspberrypi:/boot# ps -ely | grep geany
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   989   671  1  80   0 28276 25827 poll_s ?        00:00:06 geany

Geany 프로그램을 하나 더 열고 다음 명령어를 입력합시다.
root@raspberrypi:/boot# ps -ely | grep geany
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S  1000   989   671  1  80   0 28276 25827 poll_s ?        00:00:06 geany
S  1000  1297   671 38  80   0 25204 13533 poll_s ?        00:00:01 geany

PID가 1297인 geany 프로세스가 생성됐습니다.

프로세스를 어렵게 생각할 필요가 없습니다. 셸이나 geany이란 프로그램을 실행하면 메모리에서 실행하는 것이 프로세스입니다. 유저 레벨에서 실행하는 프로세스는 이렇게 유저 동작으로 생성됩니다.

이번에 리눅스 시스템 프로그래밍으로 프로세스를 생성해 봅시다. 소스 코드는 다음과 같으니 같이 입력해 봅시다.
1 #include <stdio.h>
2 #include <unistd.h>
3
4 #define PROC_TIMES 500
5 #define SLEEP_DURATION 3  // second unit
6
7 int main() 
8 {
9 int proc_times = 0;
10
11 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
12 printf("raspbian tracing \n");
13 sleep(SLEEP_DURATION);
14 }
15
16 return 0;
17 }

위와 같은 프로그램을 리눅스 시스템 프로그램이라고 합니다. 리눅스 시스템을 관리하는 sleep()이나 fork() 함수를 직접 호출하기 때문에 응용 프로그램 입장에서 저수준 프로그래밍이라고도 합니다. 위 함수를 리눅스 시스템 저수준 함수(API)라고 부르겠습니다.

위 코드는 다음 코드 이외에 다른 동작을 하지 않습니다.
11 for(proc_times = 0; proc_times < PROC_TIMES; proc_times++) {
12 printf("raspbian tracing \n");
13 sleep(SLEEP_DURATION);
14 }

소스 코드를 잠깐 봅시다.

12번째 줄 코드와 같이 “raspbian tracing”이란 메시지를 셸로 출력하고 13번째 줄 코드와 같이 3초 동안 휴면에 들어갈 뿐입니다.

위 코드를 입력한 다음 raspbian_test.c 란 이름으로 저장합시다. 컴파일을 쉽게 하기 위해 다음과 같이 코드를 작성하고 파일 이름을 Makefile으로 저장합시다.
raspbian_proc: raspbian_test.c
gcc -o raspbian_proc raspbian_test.c

“make” 명령어로 위와 같은 메이크 파일을 실행하면 raspbian_proc이란 실행 파일이 생성됩니다.

메이크 파일은 여러 모듈을 일일이 컴파일 명령어를 입력하기 어려우니 컴파일 설정 속도를 빠르게 하기 위해 고안된 겁니다. 실전 프로젝트에서 메이크 파일은 자주 쓰니 잘 알아둡시다.

make란 명령어를 입력해서 raspbian_test.c 파일을 컴파일하면 raspbian_proc이란 프로그램을 생성할 수 있습니다.

raspbian_proc 이란 프로세스가 어떻게 생성되고 실행되는지 파악하려면 다음과 같이 ftrace 로그를 설정할 필요가 있습니다. 코드를 봅시다.
1  #!/bin/sh
3  echo 0 > /sys/kernel/debug/tracing/tracing_on
4  sleep 1
5  echo "tracing_off"
6
7 echo 0 > /sys/kernel/debug/tracing/events/enable
8 sleep 1
9 echo "events disabled"
10
11 echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
12 sleep 1
13 echo "set_ftrace_filter init"
14 
15 echo function > /sys/kernel/debug/tracing/current_tracer
16 sleep 1
17 echo "function tracer enabled"
18
19 echo SyS_clone do_exit > /sys/kernel/debug/tracing/set_ftrace_filter
20 echo _do_fork copy_process* >> /sys/kernel/debug/tracing/set_ftrace_filter
21
22 sleep 1
23 echo "set_ftrace_filter enabled"
24
25 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
26 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
27 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_fork/enable
28 echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_exit/enable
29
30 echo 1 > /sys/kernel/debug/tracing/events/signal/enable
31
32 sleep 1
33 echo "event enabled"
34
35 echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
36 echo 1 > /sys/kernel/debug/tracing/options/sym-offset
37 echo "function stack trace enabled"
38
39 echo 1 > /sys/kernel/debug/tracing/tracing_on
40 echo "tracing_on"
조금 더 알아보기
ftrace 에서 시스템 콜 핸들러 함수 심볼(이름)을 Alias 심볼로 씁니다.

예를 들어 sys_write() 함수에 대한 alias 심볼은 SyS_write와 같습니다.
다음 전처리 코드는 fs/read_write.c 파일에 위치한 write 시스템 콜 핸들러 함수 선언부입니다.
[out/fs/.tmp_read_write.i]
1 long sys_write(unsigned int fd, const char * buf, size_t count) __attribute__((alias("SyS_write")));
2
3 [https://elixir.bootlin.com/linux/v4.14.70/source/fs/read_write.c]
4 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
5 size_t, count)
6 {
7 struct fd f = fdget_pos(fd);
8 ssize_t ret = -EBADF;

1번째 줄 코드를 보면 함수 인자 오른쪽에 다음과 같은 코드를 볼 수 있습니다.
__attribute__((alias("SyS_write")));

GCC 컴파일러가 함수 컴파일시 alias 심볼을 적용한 것인데 sys_write() 함수에 대한 alias 심볼이 SyS_write입니다.

ftrace 로그에서 SyS_xxx 로 어떤 함수를 표현하면 실제 함수 이름은 sys_xxx() 이라고 생각해도 좋습니다.

따라서 ftrace 로그 설정 시 set_ftrace_filter로 SyS_clone 함수로 지정한 겁니다.
19 echo SyS_clone do_exit > /sys/kernel/debug/tracing/set_ftrace_filter

이렇게 지정하면 ftrace는 실제 리눅스 커널 코드에서 sys_clone 함수를 추적(트레이싱)합니다.

위와 같이 코드를 작성한 후 clone_process_debug.sh 와 같은 이름을 저장한 후 다음과 같이 이 셸 스크립트를 실행합시다.
./clone_process_debug.sh

위 셸 스크립트를 실행하면 5~6초 내 ftrace 로그 설정이 끝납니다. 이후 raspbian_test.c 파일을 컴파일하면 생성되는 raspbian_proc이란 프로그램을 다음 명령어로 실행합시다. 
root@raspberrypi:/home/pi# ./raspbian_proc 
raspbian tracing 
raspbian tracing 
raspbian tracing 
raspbian tracing 
raspbian tracing 
raspbian tracing

raspbian_proc 이란 프로그램을 실행하니 3초 간격으로 “raspbian tracing”이란 메시지를 출력합니다. 소스 코드에서 구현한 대로 실행합니다.

raspbian_proc 프로그램을 실행했으니 이에 해당하는 프로세스가 생성됐을 것이라 예상할 수 있습니다. 이번에는 “ps -ely” 명령어를 입력해서 프로세스 목록을 확인합시다.
root@raspberrypi:/home/pi# ps -ely
1 S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
2 S     0     1     0  0  80   0  5956  6991 SyS_ep ?        00:00:02 systemd
3 S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
...
4
5 S     0   895   890  0  80   0  3420  1448 wait   pts/0    00:00:00 bash
6 S  1000   991   685  0  80   0  7500  7842 poll_s ?        00:00:00 ibus-engine-han
...
7  S     0  1078  1073  0  80   0  3244  1416 wait   pts/2    00:00:00 bash
8  I     0  1079     2  0  80   0     0     0 worker ?        00:00:00 kworker/3:2
9  I     0  2302     2  0  80   0     0     0 worker ?        00:00:00 kworker/0:1
10 S     0 17082   895  0  80   0   344   453 hrtime pts/0    00:00:00 raspbian_proc
11 I     0 17084     2  0  80   0     0     0 worker ?        00:00:00 kworker/u8:1
12 I     0 17085     2  0  80   0     0     0 worker ?        00:00:00 kworker/1:0
13 R     0 17086  1078  0  80   0  1156  1918 -      pts/2    00:00:00 ps

프로세스 목록 10번째 항목을 보면 pid가 17082인 raspbian_proc 프로세스가 보입니다. 리눅스 시스템에서 raspbian_proc 프로세스가 READY 상태이란 의미입니다. 
1 S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
5 S     0   895   890  0  80   0  3420  1448 wait   pts/0    00:00:00 bash
...
10 S     0 17082   895  0  80   0   344   453 hrtime pts/0    00:00:00 raspbian_proc

1번째 줄 로그에서 PPID가 보입니다. 이 정보는 부모 프로세스의 pid를 의미합니다. raspbian_proc 프로세스의 부모 프로세스는 pid가 895입니다. pid가 895인 프로세스를 확인하니 프로세스 목록 5번째 항목과 같이 bash 프로세스입니다. raspbian_proc 프로세스의 부모 프로세스는 bash임을 알 수 있습니다.

raspbian_proc 프로세스의 부모 프로세스는 왜 bash(pid:895) 일까요? raspbian_proc 프로세스를 실행할 때 X-Terminal bash 셸에서 다음 명령어로 실행했기 때문입니다.
root@raspberrypi:/home/pi# ./raspbian_proc 

이렇게 유저 레벨 프로세스는 셸이나 다른 프로세스를 통해서 실행을 시작합니다. 만약 라즈베리파이 바탕 화면에 있는 아이콘을 클릭해서 프로그램을 시작해서 유저 레벨 프로세스를 실행했다고 가정합시다. 이 경우 바탕화면을 제어하는 프로세스가 부모 프로세스가 됩니다.

raspbian_proc 프로세스를 이렇게 15초 동안 실행시킨 다음에 다른 x-terminal 셸을 실행을 실행해서 다음과 같이 raspbian_proc 프로세스를 강제 종료해봅시다.
root@raspberrypi:/home/pi# kill -9  17082

kill 명령어로 pid를 지정하면 강제로 지정한 프로세스를 종료합니다. -9는 강제로 프로세스를 종료시키는 옵션입니다.

다음 명령어를 입력해서 kill이란 명령어가 어떤 의미인지 확인합시다.
root@raspberrypi:/home/pi# info kill

24.1 ‘kill’: Send a signal to processes
=======================================

The ‘kill’ command sends a signal to processes, causing them to
terminate or otherwise act upon receiving the signal in some way.
Alternatively, it lists information about signals.  Synopses:

     kill [-s SIGNAL | --signal SIGNAL | -SIGNAL] PID...
     kill [-l | --list | -t | --table] [SIGNAL]...

   Due to shell aliases and built-in ‘kill’ functions, using an
unadorned ‘kill’ interactively or in a script may get you different
functionality than that described here.  Invoke it via ‘env’ (i.e., ‘env
kill ...’) to avoid interference from the shell.

매뉴얼 내용과 같이 kill 명령어는 프로세스를 종료하는 역할을 수행합니다.

이번에는 다음과 같은 셸 스크립트를 실행해서 ftrace 로그를 추출합시다.
#!/bin/sh

echo 0 > /sys/kernel/debug/tracing/tracing_on
echo "ftrace off"

sleep 3

cp /sys/kernel/debug/tracing/trace . 
mv trace ftrace_log.c

위 코드를 get_ftrace.sh 이름으로 저장해놓고 ftrace 로그를 받을 때 다음 명령어를 실행합니다.
root@raspberrypi:/home/pi#./get_ftrace.sh

그러면 같은 폴더에 ftrace.c이란 파일이 생성됐음을 확인할 수 있습니다.

이제까지 프로세스 생성과 종료 과정을 저장한 ftrace 로그를 추출하기 위해 진행한 과정을 정리하면 다음과 같습니다.
1. 다음 명령어로 프로세스 실행
root@raspberrypi:/home/pi# ./raspbian_proc 

2. ftrace 로그 설정 및 시작
3. ps 명령어로 프로세스 동작 확인

4. raspbian_proc 프로세스 종료
root@raspberrypi:/home/pi# kill -9  17082

5. ftrace 로그 추출

다음 시간에서는 프로세스 생성과 종료 흐름이 저장된 ftrace 로그를 심층 분석하는 시간을 갖겠습니다.




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




[리눅스커널] 프로세스 - 커널 프로세스 생성 시 do_fork() 함수 흐름 [라즈베리파이] 커널 프로세스

커널 공간에서 시스템 리소스(메모리, 전원) 관리를 수행하는 프로세스를 커널 스레드라고 합니다. 커널 스레드는 어떻게 생성할까요? 다음과 같이 kthread_create() 함수에 적절한 인자를 전달하면 됩니다.

커널 스레드를 생성하는 코드를 같이 봅시다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/drivers/vhost/vhost.c#L334]
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);

위 7번째 줄 코드에서 kthread_create() 함수를 실행하면 커널 스레드를 생성하는 것입니다. kthread_create() 이란 함수를 호출하면 커널 스레드 생성을 담당하는 kthreadd란 프로세스에게 커널 스레드 생성 요청을 합니다. 이후 kthreadd 스레드는 _do_fork() 함수를 실행해서 프로세스를 생성합니다. 

커널 스레드도 프로세스의 한 종류라 볼 수 있습니다.

대부분 커널 스레드는 시스템이 부팅할 때 생성하며 리눅스 커널이 커널 스레드가 필요할 때 동적으로 생성합니다. 예를 들어 리눅스 드라이버에서 많은 워크를 워크큐에 큐잉하면 커널 스레드의 종류인 워커 스레드를 더 생성해야 합니다. 시스템 메모리가 부족할 경우 메모리를 회수해서 가용 메모리를 확보하는 커널 스레드를 생성할 때도 있습니다. 보통 시스템이 더 많은 일을 해야 할 때 커널 스레드를 생성합니다.

프로세스 생성에 대한 소개를 했으니 다음에 라즈베리파이로 유저 공간에서 실행 중인 프로세스를 점검해봅시다.

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




[리눅스커널] 프로세스 - 유저 레벨 프로세스 생성 시 _do_fork() 함수 흐름 [라즈베리파이] 커널 프로세스

먼저 유저 레벨 프로세스는 어떻게 생성할까요? 저수준 리눅스 어플리케이션 프로그램으로 fork() 함수를 호출하면 리눅스에서 제공하는 라이브러리 도움을 받아 커널에게 프로세스 생성 요청을 합니다. 여기까지가 유저 모드에서 프로세스를 요청하는 단계입니다.

리눅스에서 제공하는 라이브러리는 시스템 콜을 발생하고 리눅스 커널에서는 fork() 함수에 대응하는 시스템 콜 핸들러인 sys_clone() 함수를 호출합니다.

먼저 sys_clone() 함수 코드를 봅시다.
 [https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L2020]
1 #ifdef __ARCH_WANT_SYS_CLONE
2 #ifdef CONFIG_CLONE_BACKWARDS
3 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
4  int __user *, parent_tidptr,
5  unsigned long, tls,
6  int __user *, child_tidptr)
...
7 #endif
8 {
9 return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
10 }
11 #endif

아래와 같이 시스템 콜 함수를 정의하면, sys_clone() 이란 시스템 콜 래퍼 함수를 생성합니다.
3 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,

sys_clone() 함수는 _do_fork() 함수를 그대로 호출합니다.

마찬가지로 sys_fork()와 sys_vfork() 이란 시스템 콜 함수를 확인해봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L2020]
1 SYSCALL_DEFINE0(fork)
2 {
3 #ifdef CONFIG_MMU
4 return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
5 #else
6 /* can not support in nommu mode */
7 return -EINVAL;
8 #endif
9 }
10
11 SYSCALL_DEFINE0(vfork)
12 {
13 return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
14 0, NULL, NULL, 0);
15 }


sys_fork()와 sys_vfork() 함수도 역시 _do_fork() 함수를 그대로 호출합니다. 

정리하면 유저 공간에서 생성한 프로세스는 sys_clone(), sys_fork() 그리고 sys_vfork() 시스템 콜 함수를 통해서 _do_fork() 함수를 호출합니다.

그런데 유저 공간(리눅스 시스템 프로그래밍)에서 fork() 이란 함수로 프로세스를 생성하면 시스템 콜 함수로 sys_clone() 를 호출합니다. 예전 리눅스 커널 버전에서는 fork()을 쓰면 sys_fork() 함수를 호출했으나 최근 리눅스 커널에서는 sys_clone() 함수를 실행합니다. vfork() 시스템 콜 함수도 fork() 시스템 콜을 개선하기 위해 이전 리눅스 커널 버전에서 썼던 레거시(과거) 코드입니다.

유저 레벨에서 생성한 프로세스와 스레드를 커널에서 동등하게 처리합니다. 그러니 sys_clone() 함수를 호출하는 것입니다.

정리하면 유저 공간에서 fork() 란 함수를 호출하면 시스템 콜이 실행해어 커널 공간에서 sys_clone() 함수를 호출합니다. 


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




[리눅스커널] 프로세스는 어떻게 생성하나? - _do_fork() 함수 호출 [라즈베리파이] 커널 프로세스

프로세스에 대한 이해를 하려면 프로세스가 어떻게 생성되는 지 알면 좋습니다. 프로세스 생성 과정에서 프로세스를 관리하는 자료구조 관계를 알 수 있기 때문입니다. 

리눅스에서 구동되는 프로세스는 크게 유저 레벨에서 생성된 프로세스와 커널 레벨에서 생성된 프로세스가 있습니다. 

유저 레벨에서 생성된 프로세스는 유저 공간에서 프로세스를 생성하는 라이브러리(glibc) 도움을 받아 커널에게 프로세스 생성 요청을 합니다. 커널 프로세스는 kthread_create() 함수를 호출해서 커널 내부에서 프로세스를 생성합니다. 커널 프로세스는 커널 스레드라고 부르며 커널 내부에서 스레드를 직접 관리합니다.

공통으로 리눅스에서 생성된 프로세스는 _do_fork() 함수를 호출합니다. 프로세스 생성하는 핵심함수는 _do_fork() 이니 이 함수를 중심으로 프로세스가 어떻게 생성되는지 알아봅시다.

_do_fork() 함수 소개

리눅스에서 구동 중인 모든 프로세스는 _do_fork() 함수가 실행할 때 생성됩니다. 프로세스는 누가 생성할까요? 리눅스 시스템에서 프로세스 생성을 전담하는 프로세스가 있습니다. 주인공은 init과 kthreadd 프로세스입니다.

유저 레벨 프로세스는 init 프로세스, 커널 레벨 프로세스(커널 스레드)는 kthreadd 프로세스가 생성하는 것입니다. 프로세스 생성 과정에 대해서 조금 더 정확히 말하면 프로세스는 생성이 아니라 복제된다고 설명할 수 있습니다.

프로세스를 생성할 때 여러 리소스(메모리 공간, 페이지 테이블, 가상 메모리 식별자)를 커널로부터 할당 받아야 합니다. 프로세스 동작에 필요한 리소스를 각각 할당 받으면 시간이 오래 걸리니 이미 생성된 프로세스에서 복제하는 것입니다.


리눅스 커널에서는 속도 개선을 위해 반복해서 실행하는 코드를 줄이려는 노력을 한 흔적을 볼 수 있습니다. 커널 메모리 할당자인 슬럽 메모리 할당자(Slub Memory Allocator)도 유사한 역할을 수행합니다. 드라이버에서 자주 메모리 할당 요청을 하는 쓰는 구조체를 정의해서 해당 구조체에 대한 메모리를 미리 확보해 놓습니다. 메모리 할당 요청 시 바로 이미 확보한 메모리를 할당하는 속도가 빠르기 때문입니다.


프로세스 생성 과정도 마찬가지입니다. 프로세스를 생성할 때 이미 생성된 프로세스에서 복제하는 것이 더 효율적입니다. 따라서 모든 프로세스는 부모와 자식 프로세스를 확인할 수 있습니다.

먼저 _do_fork() 함수 선언부를 보면서 이 함수에 전달되는 인자와 반환값을 확인합시다.

[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L2020]
extern long _do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *, unsigned long);

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);

먼저 반환값을 확인합시다.
함수 선언부와 같이 반환값 타입은 long이며 PID를 반환합니다. 프로세스 생성 시 에러가 발생하면 PTR_ERR() 매크로로 지정된 에러 값을 반환합니다.

_do_fork() 함수에 전달하는 인자값들을 점검합시다.

unsigned long clone_flags;

프로세스를 생성할 때 전달하는 매크로 옵션 정보를 저장합니다. 이 멤버에 다음 매크로를 저장합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/uapi/linux/sched.h]
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM 0x00000100  
#define CLONE_FS 0x00000200  
#define CLONE_FILES 0x00000400  
#define CLONE_SIGHAND 0x00000800  
#define CLONE_PTRACE 0x00002000 
#define CLONE_VFORK 0x00004000 
#define CLONE_PARENT 0x00008000 
#define CLONE_THREAD 0x00010000

unsigned long stack_start;

보통 유저 영역에서 스레드를 생성할 때 복사하려는 스택 주소입니다. 이 스택 주소는 유저 공간에서 실행 중인 프로세스 스택 주소입니다.

unsigned long stack_size;
보통 유저 영역 실행 중인 스택 크기입니다. 보통 유저 영역에서 스레드를 생성할 때 복사합니다.

int __user *parent_tidptr;
int __user *child_tidptr;

부모와 자식 스레드 그룹을 관리하는 핸들 정보입니다.

커널에서 _do_fork() 함수를 언제 호출할까요? 생성하려는 프로세스 유형에 따라 함수 호출 흐름이 나뉩니다.
 1. 유저 모드에서 생성한 프로세스: sys_clone() 시스템 콜 함수 
 2. 커널 모드에서 생성한 커널 스레드: kernel_thread() 함수
프로세스는 유저 모드에서 생성된 프로세스와 커널 모드에서 생성된 프로세스로 분류할 수 있습니다. 각각 유저 레벨 프로세스와 커널 레벨 프로세스라고 부릅니다.


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



[리눅스커널] 프로세스 확인하기 - ps [라즈베리파이] 커널 프로세스

리눅스 시스템 개발자(디바이스 드라이버, 데브옵스)로 오래 동안 실력을 인정 받으려면 리눅스 커널을 잘 알면 좋습니다. 하지만 리눅스 커널은 그 내용이 방대하고 깊이가 있어 단기간에 익히기 어려운 기술 영역입니다. "프로세스란 무엇인가"란 질문으로 리눅스 커널을 시작합니다. 성경이나 불경같이 근엄한 단어를 많이 보입니다.  안타깝게도 20페이지 정도 읽다가 포기합니다. 너무 이론으로 프로세스를 설명하기 때문입니다.

프로세스에 익숙해지려면 리눅스 시스템에 익숙해져야 합니다. 이번 장에서는 라즈베리파이에서 명령어를 입력하고 ftrace 로그에서 출력되는 로그로 프로세스 동작을 확인합니다.

먼저 다음 리눅스 명령어로 시스템에서 프로세스 목록을 확인합시다. 이를 위해 라즈베리파이에서 x-terminal 프로그램을 실행해서 셸을 열어야 합니다. 
root@raspberrypi:/home/pi# ps -ely
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S     0     1     0  0  80   0  5956  6991 SyS_ep ?        00:00:02 systemd
S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
...
S  1000   867   517  0  80   0  7720 12887 poll_s ?        00:00:00 gvfsd-trash
S  1000   876   730  0  80   0 20084 12108 poll_s ?        00:00:07 lxterminal
S  1000   877   876  0  80   0  1324   590 unix_s ?        00:00:00 gnome-pty-helpe
S  1000   878   876  0  80   0  4028  1628 wait   pts/0    00:00:00 bash
S     0   886   878  0  80   0  3380  1908 poll_s pts/0    00:00:00 sudo
S     0   890   886  0  80   0  3076  1818 wait   pts/0    00:00:00 su
리눅스 시스템에서 프로세스 목록을 보기 위해서는 "ps"라는 명령어를 입력하면 됩니다.

x-terminal 셸을 실행한 상태에서 "info ps" 명령어를 입력하면 ps 명령어의 의미를 알 수 있습니다.

-------
PS(1)                                   User Commands                                   PS(1)

NAME
       ps - report a snapshot of the current processes.

SYNOPSIS
       ps [options]

리눅스 시스템에서 돌고 있는 프로세스를 출력하는 명령어입니다. 리눅스 시스템에서 디버깅을 할 때 많이 쓰는 명령어이니 자주 활용합시다.

리눅스 시스템에서 생성된 모든 프로세스(유저 레벨, 커널 스레드)는 init 프로세스를 표현하는 전역 변수 init_tasks.next 멤버에 연결 리스트로 등록돼 있습니다. ps 명령어를 입력하면 이 연결 리스트를 순회하면서 프로세스 정보(struct task_struct)를 얻어 프로세스 정보를 출력하는 겁니다.

이번에는 ps 명령어에 "-ejH" 이란 옵션을 주고 프로세스를 부모 자식 프로세스 관계로 출력합시다.
1 root@raspberrypi:/home/pi # ps -ejH
2   PID  PGID   SID TTY          TIME CMD
3    2     0     0 ?        00:00:00 kthreadd
4    4     0     0 ?        00:00:00   kworker/0:0H
5    6     0     0 ?        00:00:00   mm_percpu_wq
6    7     0     0 ?        00:00:00   ksoftirqd/0
...
7  17103     0     0 ?     00:00:00   kworker/1:1
8  17108     0     0 ?     00:00:00   kworker/u8:0
9     1     1     1 ?        00:00:02 systemd
10   94    94    94 ?        00:00:00   systemd-journal
11  127   127   127 ?        00:00:00   systemd-udevd
12  274   274   274 ?        00:00:00   systemd-timesyn

4~6번 줄에 보이는 "kworker/0:0H", "mm_percpu_wq" 그리고 "ksoftirqd/0" 이란 프로세스의 부모 프로세스는 3번 줄에 있는 "kthreadd" 입니다. 

pid가 2이 "kthreadd" 프로세스는 커널 공간에서 실행 중인 프로세스를 생성하는 역할을 수행합니다. 위 출력 결과에서 4~8번 줄에 있는 프로세스들은 같은 행으로 정렬돼 있습니다. 이 목록에서 보이는 프로세스를 커널 스레드, 커널 프로세스라고 합니다. 커널 공간에서만 실행합니다. 

리눅스 커널에서는 프로세스 마다 PID(Process id)라는 int 형 ID를 부여합니다.
swapper 프로세스는 PID가 0이고 init 프로세스는 PID가 1 그리고 커널 스레드를 생성하는 kthreadd 프로세스는 PID가 2입니다.
새로운 프로세스를 생성할 때 커널이 부여하는 PID 정수값은 증가합니다. PID로 프로세스가 언제 생성됐는지 추정할 수 있습니다.

PID는 최댓값은 32768로 정해져 있습니다.

이번에는 9번째 줄 로그를 봅시다. pid가 1인 systemd 프로세스가 보입니다.
9     1     1     1 ?        00:00:02 systemd

pid가 1인 프로세스를 임베디드 리눅스에서는 init 프로세스라고 하며 모든 유저 공간에서 생성된 프로세스의 부모 프로세스 역할을 수행합니다.

프로세스는 인간을 객체화해서 고안한 내용이 많습니다. 프로세스는 각자 부모 자식 프로세스들이 있고 자식 프로세스가 종료할 때 부모 프로세스에게 신호를 알립니다.

만약 조부모, 부모, 자식 프로세스가 있다고 가정합니다. 예외 상황으로 부모 프로세스가 종료되면 자식 프로세스 입장에서 부모 프로세스가 사라집니다. 이 때 조부모가 부모 프로세스가 됩니다. 이런 상황에서 init 프로세스가 조부모 역할(새로운 부모 프로세스)을 수행합니다.

다음에 리눅스 커널 소스 코드를 열어서 프로세스를 생성할 때 어떤 함수가 실행하는지 살펴봅시다.


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




[리눅스커널] 프로세스 - 프로세스, 태스크란 [라즈베리파이] 커널 프로세스

프로세스는 추상적이고 다양한 의미를 담고 있어 다양한 관점으로 설명할 수 있습니다.

프로세스란 무엇일까요? 프로세스(Process)는 리눅스 시스템 메모리에서 실행 중인 프로그램을 말합니다. 스케줄링 대상인 태스크와 유사한 의미로 쓰입니다. 다수 프로세스를 실시간으로 사용하는 기법을 멀티프로세싱이라고 말하며 같은 시간에 멀티 프로그램을 실행하는 방식을 멀티태스킹이라고 합니다.

우리가 쓰고 있는 스마트폰 동작을 잠깐 생각해봅시다. 
전화를 하면서 메모를 남기고, 음악을 들으면서 브라우저를 볼 수 있습니다. 여러 어플리케이션이 동시에 실행하고 있습니다. 이것은 멀티태스킹을 수행해서 프로그램을 시분할 방식으로 처리하기 때문에 가능합니다.

이번에는 리눅스 개발자 입장에서 프로세스에 대해 생각해 봅시다. 프로세스는 리눅스 시스템 메모리에 적재되어 실행을 대기하거나 실행하는 실행 흐름을 의미합니다. 프로세스가 실행을 대기한다면 실행할 때 어떤 과정을 거칠까요? 프로세스는 어떤 구조체로 식별할까요? 다양한 의문이 생깁니다.

프로세스를 관리하는 자료구조에자 객체를 태스크 디스크립터라고 말하고 구조체는 struct task_struct 입니다. 이 구조체에 프로세스가 쓰는 메모리 리소스, 프로세스 이름, 실행 시각, 프로세스 아이디(PID), 프로세스 스택 최상단 주소가 저장돼 있습니다.

프로세스를 struct task_struct 이란 구조체로만 표현할 수 있을까요? 위에서 프로세스란 실행 흐름 그 자체라고 정의했습니다. 프로세스 실행 흐름은 어느 구조체에 저장할 수 있을까요?

프로세스는 실행할 때 리눅스 커널 함수를 호출합니다. 임베디드 리눅스 디버거의 전설인 Trace32 프로그램으로 콜 스택을 하나 봅시다.
1 -000|__schedule()
2 -001|schedule_timeout()
3 -002|do_sigtimedwait()
4 -003|sys_rt_sigtimedwait()
5 -004|ret_fast_syscall(asm)

위 함수 호출 방향은 5번째 줄에서 1번째 줄입니다. 콜 스택을 간단히 해석하면 유저 공간 프로그램에서 sigtimedwait() 이란 함수를 호출하면 이에 대응하는 시스템 콜 핸들러 함수인 sys_rt_sigtimedwait() 함수 실행 후 스케줄링되는 함수 흐름입니다.

프로세스는 함수를 호출하면서 실행을 합니다. 그런데 함수를 호출하고 실행할 때 어떤 리소스를 쓸까요? 프로세스 스택 메모리 공간입니다.

모든 프로세스들은 커널 공간에서 실행할 때 각자 스택 공간을 할당 받으며 스택 공간에서 함수를 실행합니다. 

위에서 본 프로세스가 스케줄러에 의해 다시 실행한다고 가정합시다. 그럼 어떻게 실행할까요?
1 -000|__schedule()
2 -001|schedule_timeout()
3 -002|do_sigtimedwait()
4 -003|sys_rt_sigtimedwait()
5 -004|ret_fast_syscall(asm)

1번 함수에서 5번 함수 방향으로 되돌아 올 겁니다. 이는 이 프로세스가 마지막에 실행했던 레지스터 세트와 실행 흐름이 프로세스 스택 공간에 저장돼 있었기 때문입니다.

프로세스를 실행 흐름을 표현하는 또 하나 중요한 공간은 프로세스 스택 공간이며 이 프로세스 스택 최상단 주소에 struct thread_info 란 구조체가 있습니다.

정리하면 프로세스는 추상적인 개념이지만 프로세스 정보와 프로세스 실행 흐름을 저장하는 구조체와 메모리 공간이 있습니다. 리눅스 커널에서 실시간으로 구동하는 프로세스에 대해 잘 알려면 이 자료구조를 잘 알 필요가 있습니다.

우리가 열심히 분석하는 리눅스 커널 소스 코드를 실행하는 주체가 프로세스이며 프로세스 스택 공간에서 실행하는 것입니다.

태스크란
태스크는 무엇일까요? 태스크는 리눅스 이외 다른 운영체제에서 예전부터 쓰던 용어입니다.
운영체제 이론을 다루는 예전 이론서는 대부분 태스크란 단어를 많이 볼 수 있습니다.

태스크는 운영체제에서 어떤 의미일까요? 말 그대로 실행(Execution)이라 할 수 있습니다.
운영체제 책들을 보면 첫 장에서 태스크에 대한 설명을 볼 수 있습니다. 최근 운영 체제에서는 대부분 기본으로 멀티 태스킹 환경에서 프로그램을 실행하나 예전에는 특정 코드나 프로그램 실행을 일괄 처리했습니다. 이 실행 및 작업 단위를 태스크라고 불렀습니다.


화면이 없는 간단한 시나리오의 임베디드 시스템에서는 태스크 2개로 서로 시그널을 주고 받으며 시스템 전체를 제어할 수 있습니다. 

하지만 태스크에 대한 개념은 현재 프로세스와 겹치는 부분이 많습니다. 태스크에 대한 의미가 프로세스와 스레드에 대한 개념이 도입하면서 발전했습니다. 태스크를 실행하는 단위인 실행(Execution)을 결정하는 기준이 스케줄링으로 바뀐 겁니다. 

예전에 쓰던 용어를 현재 소프트웨어에 그대로 쓰는 경우가 많습니다. 이를 레거시(Legacy)라고 말하고 과거 유물이란 뜻도 있습니다. 예전에 썼던 태스크란 용어를 리눅스 커널 용어나 소스 코드에서 그대로 쓰고 있습니다. 프로세스 속성을 표시하는 구조체 이름을 struct task_struct으로 쓰고 있습니다. 

프로세스 마다 속성을 표현하는 struct task_struct 구조체는 태스크 디스크립터라고 하며 프로세스 디스크립터라고도 말합니다.

리눅스 커널 함수 이름이나 변수 중에 task란 단어가 보이면 프로세스 관련 코드라 생각해도 좋습니다.

예를 들어 다음 함수는 모두 프로세스를 관리 및 제어하는 역할을 수행하며 함수 이름에 보이는 태스크는 프로세스로 바꿔도 무방합니다.
dump_task_regs
get_task_mm
get_task_pid
idle_task
task_tick_stop

리눅스 커널에서 태스크는 프로세스와 같은 개념으로 쓰는 용어입니다. 소스 코드나 프로세스에 대한 설명을 읽을 때 태스크란 단어를 보면 프로세스와 같은 개념으로 이해합시다.

스레드란
스레드는 무엇일까요? 간단히 말하면 유저 레벨에서 생성된 가벼운 프로세스라 말할 수 있습니다. 멀티 프로세스 실행 시 컨택스트 스위칭을 수행해야 하는데 이 때 비용(시간)이 많이 듭니다. 실행 중인 프로세스의 가상 메모리 정보를 저장하고 새롭게 실행을 시작하는 프로세스도 가상 메모리 정보를 로딩해야 합니다. 또한 스레드를 생성할 때는 프로세스를 생성할 때 보다 시간이 덜 걸립니다.

스레드는 자신이 속한 프로세스 내의 다른 스레드와 파일 디스크립터, 파일 및 시그널 정보에 대한 주소 공간을 공유합니다. 프로세스가 자신만의 주소 공간을 갖는 것과 달리 스레드는 스레드 그룹 안의 다른 스레드와 주소 공간을 공유합니다.

하지만 커널 입장에서는 스레드를 다른 프로세스와 동등하게 관리합니다. 대신 각 프로세스 식별자인 태스크 디스크립터(struct task_struct)에서 스레드 그룹 여부를 점검할 뿐입니다.


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




[리눅스 커널] 유저 모드란 [라즈베리파이] 커널 프로세스

유저 레벨 프로세스에 대해 알아보기 전에 유저 모드가 무엇인지 먼저 살펴봅시다.

우리가 라즈베리파이에서 바탕 화면에 있는 아이콘을 클릭해서 어떤 프로그램을 실행한다고 가정합시다. 이 때 프로그램은 유저 모드나 커널 모드 중 하나로 동작합니다. ftrace 로그로 커널 동작을 확인하면 정확히 유저 모드와 커널 모드를 자주 스위칭합니다.

유저 모드와 커널 모드로 나누는 기준은 무엇일까요? 이는 메모리 접근과 실행 권한으로 두 모드로 분류합니다.

실행 모드를 유저 모드와 커널 모드로 나누는 이유를 알기 위해 한 가지 예를 들겠습니다. 어떤 시스템 메모리 공간을 0~4G까지 가상 메모리에서 연속으로 쓰고 있다고 가정합시다. 이 때 커널 코드와 전역 변수가 0~4G 메모리 구간에 메모리 주소로 매핑되어 있는 상황입니다.

만약 어떤 운영체제에서 라즈베리파이 Geany와 같은 어플리케이션까지 리눅스 시스템 개발자가 구현하면 큰 문제가 발생하지 않을 것입니다. 리눅스 시스템 개발자는 커널 코드 실행 흐름이나 메모리 제어하는 동작을 잘 알고 있을 가능성이 높기 때문입니다. 이럴 때 유저와 커널 모드로 실행 흐름을 두 개로 나눠서 설계할 필요가 없을 것입니다.

그런데 시스템이나 커널 개발자가 아닌 리눅스 커널 세부 동작을 잘 모르는 응용 프로그램 개발자가 어플리케이션을 유저 모드와 커널 모드가 없는 운영체제에서 개발한다고 생각해봅시다. 응용 프로그램에서 메모리를 제대로 관리하면 문제가 되지 않지만 실수로 코드를 잘못 작성해서 커널 자료구조나 함수가 위치한 메모리 공간을 오염시키면 어떤 문제가 발생할까요? 커널 패닉 같이 시스템 오동작을 유발할 것입니다.

비슷한 이유로 메모리에 직접 접근 못하는 프로그램 언어가 설계됐고 이를 Managed 언어라고 합니다. 대표적인 예로 자바를 들 수 있습니다. 메모리에 직접 접근 가능한 언어는 대표적으로 C와 C++입니다. 
 
응용 어플리케이션을 개발하고 설치할 수 있는 라즈베리파이 같은 범용 운영체제에서는 0~2G 가상 메모리 공간까지는 유저 모드만 접근할 수 있고 커널 모드에서는 0~4G 메모리까지 접근할 수 있도록 제한을 걸어 둡니다. 

유저와 커널 모드로 메모리 접근 권한을 달리 주는 것은 운영체제 설계 기법 중 하나입니다. 

우리가 실행하는 모든 어플리케이션은 유저 모드와 커널 모드 중 하나 모드에서 동작한다고 했습니다. 그런데 유저 어플리케이션 입장에서 커널에 어떤 서비스를 요청해야 할 때가 있습니다. 유저 모드 어플리케이션에서 커널에 어떤 서비스를 요청할 때는 어떻게 해야 할까요?  파일을 읽고 쓰거나 현재 실행 중인 프로세스 정보를 얻고 싶을 때입니다.

이 때 시스템 콜을 실행하면 유저 모드에서 커널 모드로 전환하며 이 동작을 시스템 콜이라고 합니다. 유저 모드에서 커널 코드를 직접 실행하지 못하고 시스템 콜을 통해서만 커널 모드로 진입한 다음 특정 서비스를 요청하는 것입니다.

커널 입장에서는 유저 모드에서 요청한 서비스에 대해 어떻게 동작을 할까요? 먼저 유저 공간에서 시스템 콜을 실행하면서 전달한 인자에 오류가 있는 지 점검합니다. 유저 모드에서 어플리케이션 개발자가 시스템 콜로 인자를 잘못 전달했는데 커널에서 이 인자를 그대로 읽어서 처리하면 커널은 오동할 가능성이 높기 때문일 일 것입니다.

그래서 시스템 콜이 실행하고 호출되는 시스템 콜 핸들러 함수에서는 예외 처리 루틴이 많습니다.

유저 모드에 대해 조금 더 생각해 봅시다. 유저 모드에 해당하는 파일은 무엇일까요?

다음 파일 경로에 리눅스 시스템 구동에 필요한 라이브러리가 있습니다. 
root@raspberrypi:/home/pi# ls /usr/lib/arm-linux-gnueabihf/
alsa-lib libltdl.so.7
audit libltdl.so.7.3.1
avahi liblwres.so.141
bluetooth liblwres.so.141.0.3
caca liblz4.so.1
cifs-utils liblz4.so.1.7.1
coreutils libm.a
...

이 파일이 유저 모드 코드라 할 수 있습니다. 리눅스에서 실행 중인 유저 어플리케이션은 이 라이브러리와 링킹되어 동작합니다.

이번에는 유저 레벨 프로세스에 대해 생각해봅시다. 유저 레벨 프로세스는 어떻게 생성될까요? 여러분이 라즈베리파이 바탕 화면에 있는 아이콘을 더블 클릭하거나 다음과 같은 코드를 컴파일해서 생성되는 파일을 실행할 때 같은 흐름으로 실행을 시작합니다.

어떤 프로그램이던 main 이란 함수가 있는데 둘 다 main 이 실행한다고 보면 됩니다. 유저 레벨에서 프로세스를 생성할 때 커널 서비스에게 요청을 해야 프로세스 생성이 가능합니다.

문제는 유저 모드에서 혼자 프로세스를 생성하지 못합니다. 리눅스에서 제공하는 라이브러리 도움으로 프로세스 생성 요청이 가능합니다. 이런 역할을 GNU libc가 제공하며 이를 glibc이라고도 합니다.

유저 어플리케이션과 유저 레벨 프로세스는 어떤 의미일까요? 유저 어플리케이션은 라즈베리파이에서 보는 프로그램을 의미하고 유저 레벨 프로세스는 이 어플리케이션을 실행하는 주체를 의미합니다. 커널 입장에서는 유저 어플리케이션이나 유저 레벨 프로세스는 같은 의미입니다.

유저 레벨 프로세스와 커널 레벨 프로세스의 가장 큰 차이점은 무엇일까요? 실행 출발점이 다릅니다.

유저 레벨 프로세스는 유저 모드에서 fork() 나 pthread_create() 함수 호출로 glibc 리눅스 라이브러리 파일 도움으로 커널에 서비스를 요청하면서 실행됩니다.
root@raspberrypi:/home/pi# ls /usr/lib/arm-linux-gnueabihf/libc.a
/usr/lib/arm-linux-gnueabihf/libc.a

커널에 어떤 서비스를 요청할 일이 생기면 시스템 콜을 실행해야 합니다.

하지만 커널 레벨 프로세스는 커널 모드에서 실행합니다. 커널의 kthread_create() 함수 호출로 실행되는 겁니다.


[리눅스커널] 동기화 - 커널 프로세스 레이스 컨디션 [라즈베리파이]커널 동기화

이번에 커널 공간에서만 실행하는 커널 쓰레드에서 발생하는 Race를 확인합니다. 커널 쓰레드 중 많이 알려진 워커 쓰레드를 예를 듭시다.

이를 위해 다음 패치 코드를 적용할 필요가 있습니다.
1 diff --git a/kernel/workqueue.c b/kernel/workqueue.c
2 --- a/kernel/workqueue.c
3 +++ b/kernel/workqueue.c
4 @@ -2187,6 +2187,12 @@ static void process_scheduled_works(struct worker *worker)
5  *
6  * Return: 0
7  */
8  +
9  +static unsigned int global_func_exe_times = 0;
10 +
11 +extern void trace_function_dummy_call(void);
12 +
13 static int worker_thread(void *__worker)
14 {
15  struct worker *worker = __worker;
16 @@ -2195,6 +2201,15 @@ static int worker_thread(void *__worker)
17  /* tell the scheduler that this is a workqueue worker */
18  worker->task->flags |= PF_WQ_WORKER;
19 woke_up:
20 + trace_printk("[+] comm: %s, pid: %d, global_w_func_exe_times: %d from(%pS) \n", 
21 + current->comm, current->pid, global_func_exe_times, (void *)__builtin_return_address(0));
22 +
23 + global_func_exe_times++;
24 +
25 + trace_function_dummy_call();
26 +
27  spin_lock_irq(&pool->lock);
28 
29  /* am I supposed to die? */
30 @@ -2255,6 +2270,11 @@ static int worker_thread(void *__worker)
31  } while (keep_working(pool));
32 
33  worker_set_flags(worker, WORKER_PREP);
34 +
35 + trace_printk("[-] comm: %s, pid: %d, global_w_func_exe_times: %d from(%pS) \n", 
36 + current->comm, current->pid, global_func_exe_times, (void *)__builtin_return_address(0));
37 +
38 sleep:
39  /*
40   * pool->lock is held and there's no work to process and no need to

원래 worker_thread() 함수 구현부는 다음과 같습니다.
1 static int worker_thread(void *__worker)
2 {
3 struct worker *worker = __worker;
4 struct worker_pool *pool = worker->pool;
5
6 /* tell the scheduler that this is a workqueue worker */
7 worker->task->flags |= PF_WQ_WORKER;
8 woke_up:
9 spin_lock_irq(&pool->lock);

위 커널 오리지널 8~9번째 줄 코드 사이에 +로 표시된 코드(20~26번째 줄 패치 코드)를 입력합니다.

이번에는 패치 코드에서 34~37번째 구간 작성 방법을 알아 봅시다. 
1 static int worker_thread(void *__worker)
2 {
...
3 } else {
4 move_linked_works(work, &worker->scheduled, NULL);
5 process_scheduled_works(worker);
6 }
7 } while (keep_working(pool));
8
9 worker_set_flags(worker, WORKER_PREP);
10 sleep:


위 커널 오리지널 코드 9~10번째 줄 코드 사이에 다음 코드를 입력하면 됩니다.
34 +
35 + trace_printk("[-] comm: %s, pid: %d, global_w_func_exe_times: %d from(%pS) \n", 
36 + current->comm, current->pid, global_func_exe_times, (void *)__builtin_return_address(0));

이 패치를 적용한 다음 커널 빌드 후 라즈베리파이에 설치하고 리부팅을 합시다. 패치 코드 내용은 이전 절에 다룬 내용과 같으니 생략합니다.

ftrace 로그를 설정 방법은 이전에 소개된 내용과 같습니다. trace_function_dummy_call() 함수에 set_filter를 겁시다. Race가 발생하는 ftrace 로그를 분석하기 전에 Race가 발생하지 않을 때 ftrace 로그를 봅시다.
1 kworker/3:1-3162  [003] ....  2048.385285: worker_thread: [+] comm: kworker/3:1, pid: 3162, global_func_exe_times: 34594 from(kthread+0x144/0x174) 
2 kworker/3:1-3162  [003] ....  2048.385287: trace_function_dummy_call: [++] comm:kworker/3:1, pid:3162, from(worker_thread+0x36c/0x6cc) 
3 kworker/3:1-3162  [003] ....  2048.385289: workqueue_execute_start: work struct ba383c8c: function lru_add_drain_per_cpu
4 kworker/3:1-3162  [003] ....  2048.385293: workqueue_execute_end: work struct ba383c8c
5 kworker/3:1-3162  [003] d...  2048.385296: worker_thread: [-] comm: kworker/3:1, pid: 3162, global_func_exe_times: 34595 from(kthread+0x144/0x174) 
첫 번째 줄 로그를 보면 global_func_exe_times이란 전역 변수가 34594임을 알 수 있습니다. pid가 3162인 kworker/3:1이란 프로세스가 실행 중입니다.

3~4번째 줄 로그는 worker_thread() 함수에서 호출한 process_one_work() 함수에서 출력하는 워크 실행 정보입니다. 워크 핸들러가 lru_add_drain_per_cpu() 함수입니다.
이전에 워크큐에 대한 글을 읽었으면 이해할 수 있는 ftrace 로그입니다.

이번에는 5번째 줄 로그를 봅시다. global_func_exe_times이란 전역 변수를 +1만큼 증감했으니 34595가 됩니다.
패치 코드 구간에 한 개의 CPU3에서 kworker/3:1 프로세스만 실행했습니다.

이번에 Race가 발생하는 로그를 봅시다.
1  kworker/3:1-3162  [003] ....  2048.406656: worker_thread: [+] comm: kworker/3:1, pid: 3162, global_func_exe_times: 34602 from(kthread+0x144/0x174) 
2 kworker/1:2-1376  [001] ....  2048.406660: worker_thread: [+] comm: kworker/1:2, pid: 1376, global_w_func_exe_times: 34602 from(kthread+0x144/0x174) 
3 kworker/3:1-3162  [003] ....  2048.406661: trace_function_dummy_call+0x0/0x58 <8067196c>: [++] comm:kworker/3:1, pid:3162, from(worker_thread+0x36c/0x6cc) 
4 kworker/1:2-1376  [001] ....  2048.406663: trace_function_dummy_call+0x0/0x58 <8067196c>: [++] comm:kworker/1:2, pid:1376, from(worker_thread+0x36c/0x6cc) 
5 kworker/1:2-1376  [001] ....  2048.406668: workqueue_execute_start: work struct ba361b18: function drain_local_pages_wq
6 kworker/3:1-3162  [003] ....  2048.406668: workqueue_execute_start: work struct ba383b18: function drain_local_pages_wq
7 kworker/3:1-3162  [003] ....  2048.406688: workqueue_execute_end: work struct ba383b18
8 kworker/3:1-3162  [003] d...  2048.406692: worker_thread+0x370/0x6cc <80138730>: [-] comm: kworker/3:1, pid: 3162, global_w_func_exe_times: 34604 from(kthread+0x144/0x174) 
9  kworker/1:2-1376  [001] ....  2048.406720: workqueue_execute_end: work struct ba361b18
10 kworker/1:2-1376  [001] ....  2048.406726: workqueue_execute_start: work struct 958a5ab4: function wq_barrier_func
11 kworker/1:2-1376  [001] ....  2048.406730: workqueue_execute_end: work struct 958a5ab4
12 kworker/1:2-1376  [001] d...  2048.406732: worker_thread+0x370/0x6cc <80138730>: [-] comm: kworker/1:2, pid: 1376, global_w_func_exe_times: 34604 from(kthread+0x144/0x174)

첫 번째 줄 로그를 보겠습니다.
2048.406656초에 CPU3에서 구동 중인 kworker/3:1 프로세스가 실행해서 global_func_exe_times이란 전역 변숫값인 34602를 출력합니다.

2번째 줄 로그를 분석할 차례입니다.
2048.406660초에 CPU0에서 구동 중인 kworker/1:2 프로세스가 global_func_exe_times이란 전역 변수를 출력하는데 값은 34602입니다.

패치 코드를 보면 20번 줄 ftrace 메시지를 출력하고 바로 global_func_exe_times 전역 변수를 +1만큼 증감합니다.
20 + trace_printk("[+] comm: %s, pid: %d, global_func_exe_times: %d from(%pS) \n", 
21 + current->comm, current->pid, global_func_exe_times, (void *)__builtin_return_address(0));
22 +
23 + global_func_exe_times++;

CPU3에서 20번째 줄 코드를 실행한 후 23번째 줄 코드와 같이 global_func_exe_times 전역 변수를 +1만큼 증감하기 직전 CPU0에서 돌던 kworker/1:2 프로세스가 20번 코드를 실행한 겁니다.

다음 3번째 줄 로그입니다.
3 kworker/3:1-3162  [003] ....  2048.406661: trace_function_dummy_call: [++] comm:kworker/3:1, pid:3162, from(worker_thread+0x36c/0x6cc) 

CPU3 kworker/3:1 프로세스가 2048.406661초에 trace_function_dummy_call() 함수를 실행합니다.

4번째 줄 로그를 보겠습니다.
4 kworker/1:2-1376  [001] ....  2048.406663: trace_function_dummy_call: [++] comm:kworker/1:2, pid:1376, from(worker_thread+0x36c/0x6cc) 

CPU1에서 2048.406663초에 trace_function_dummy_call() 함수를 실행합니다.
3 kworker/3:1-3162  [003] ....  2048.406661: trace_function_dummy_call: [++] comm:kworker/3:1, pid:3162, from(worker_thread+0x36c/0x6cc) 

CPU3에서 2048.406661초에 trace_function_dummy_call() 함수를 실행합니다. 두 개의 CPU가 번갈아 가면서 함수를 지그재그로 실행하고 있습니다.

5~6번째 줄 로그를 보겠습니다.
5 kworker/1:2-1376  [001] ....  2048.406668: workqueue_execute_start: work struct ba361b18: function drain_local_pages_wq
6 kworker/3:1-3162  [003] ....  2048.406668: workqueue_execute_start: work struct ba383b18: function drain_local_pages_wq
7 kworker/3:1-3162  [003] ....  2048.406688: workqueue_execute_end: work struct ba383b18
8 kworker/3:1-3162  [003] d...  2048.406692: worker_thread: [-] comm: kworker/3:1, pid: 3162, global_func_exe_times: 34604 from(kthread+0x144/0x174) 
9  kworker/1:2-1376  [001] ....  2048.406720: workqueue_execute_end: work struct ba361b18
CPU1에서 구동 중인 kworker/1:2 프로세스와 CPU3에서 실행 중인 kworker/3:1 프로세스가 2048.406668이란 시각에 동시에 ba361b18주소에 있는 워크를 실행합니다.

7~8번째 줄 로그를 보면 CPU3에서 ba361b18이란 워크 실행을 마치고 global_func_exe_times 전역 변수를 34604으로 출력합니다.

CPU3 입장에서 global_func_exe_times 변수를 +1만큼 증감하기 전 값은 34602였습니다.
1  kworker/3:1-3162  [003] ....  2048.406656: worker_thread: [+] comm: kworker/3:1, pid: 3162, global_func_exe_times: 34602 from(kthread+0x144/0x174) 

이번에 입력한 패치 코드는 커널 공간에서 수행 중인 프로세스가 겪는 Race를 확인하기 위해 입력한 겁니다.
다음 함수를 커널 동기화 관점으로 분석해 봅시다.
1 static int worker_thread(void *__worker)
2 {
3 struct worker *worker = __worker;
4 struct worker_pool *pool = worker->pool;
5
6 worker->task->flags |= PF_WQ_WORKER;
7 woke_up:
8 spin_lock_irq(&pool->lock);
9
10 /* am I supposed to die? */
11 if (unlikely(worker->flags & WORKER_DIE)) {
12 spin_unlock_irq(&pool->lock);
13 WARN_ON_ONCE(!list_empty(&worker->entry));
14 worker->task->flags &= ~PF_WQ_WORKER;
15
16 set_task_comm(worker->task, "kworker/dying");
17 ida_simple_remove(&pool->worker_ida, worker->id);
18 worker_detach_from_pool(worker, pool);
19 kfree(worker);
20 return 0;
21 }
22
23 worker_leave_idle(worker);

8번째 줄 코드에서 spin_lock_irq(&pool->lock); 함수를 호출해서 스핀락을 걸고 있습니다.
워커 쓰레드에서 워크를 실행할 때 워커 풀에 등록된 워크를 실행하기 때문에 위와 같이 워커풀에 있는 락을 걸어준 겁니다.

 코드 분석 전에 worker_thread() 함수는 워커 쓰레드가 실행하면 구동하는 핸들 함수이라는 점을 상기합시다. 즉 여러 워커 쓰레드가 이 함수에서 동시에 실행할 수 있다는 이야기입니다. 

코드를 분석하니 여러 가지 의문이 생깁니다. 다시 Q&A 시간입니다.

이전에 프로세스만의 유일한 메모리나 자료구조를 활용하면 락을 걸 필요가 없다고 했다. 위 코드에서 관련 자료구조는 무엇일까?

반복하지만 worker_thread() 함수는 워커 쓰레드만 실행하는 쓰레드 핸들 함수입니다. 
1 static int worker_thread(void *__worker)
2 {
3 struct worker *worker = __worker;

커널 쓰레드는 쓰레드를 처리할 수 있는 매개변수(디스크립터)를 첫 번째 인자로 전달합니다.
void 포인터 형태인 __worker 인자는 3번째 줄 코드와 같이 struct worker 구조체로 캐스팅됩니다.

이 __worker 인자가 워커 쓰레드를 식별하는 주소를 담고 있습니다.
워커 쓰레드를 표현하는 자료구조는 struct worker인데 worker_thread() 함수에서 전달하는 __worker 인자로 유일한 주소를 전달한다는 겁니다.

kworker/1:2-1376와 kworker/3:1-3162 프로세스는 유일한 struct worker 구조체 주소로 처리한다는 겁니다.

그래서 다음 11~23번째 줄 코드에서 worker 포인터 변수로 워커 쓰레드를 종료하는 동작을 실행합니다.
11 if (unlikely(worker->flags & WORKER_DIE)) {
12
13 WARN_ON_ONCE(!list_empty(&worker->entry));
14 worker->task->flags &= ~PF_WQ_WORKER;
15
16 set_task_comm(worker->task, "kworker/dying");
17 ida_simple_remove(&pool->worker_ida, worker->id);
18 worker_detach_from_pool(worker, pool);
19 kfree(worker);
20 return 0;
21 }
22
23 worker_leave_idle(worker);

worker 이란 포인터는 워커 쓰레드마다 있는 주소입니다. 여기서 의문이 생깁니다.

이전 절에서 프로세스를 관리할 수 있는 유일한 메모리 혹은 자료구조는 태스크 디스크립터라고 설명했다. 위 경우는 다른가?

아닙니다. 어떤 코드를 실행할 때도 태스크를 처리할 수 있는 유니크한 자료구조는 태스크 디스크립터입니다. 그런데 커널 쓰레드나 인터럽트 디스크립터의 경우 해당 프로세스 실행 용도에 따른 자료구조(구조체)가 있기 마련입니다. 모든 실행을 태스크 디스크립터로 관리하면 태스크 디스크립터 멤버를 추가하거나 불필요한 코드 실행을 해야 합니다.

이번에 프로세스에서 Race 발생 상황을 확인했습니다. 다음에는 어떤 함수가 실행 중 인터럽트가 발생해서 다시 같은 함수가 실행하는 상황을 확인해 보겠습니다.


[리눅스커널] 프로세스 태스트 디스크립터 디버깅: 스레드와 스레드 그룹 [라즈베리파이] 커널 프로세스

T32로 프로세스 목록을 보면 다음과 같이 계층 구조를 볼 수 있다.
magic___|___command_________|#thr|state_____|spaceid|pids_|
C1A171B8|   swapper/0       | 420|current   | 0000  | 0. 2. 3. 5. 6. 7. 8. 9. 10
F1618000|   init            | -  |sleeping  | 0001  | 1.
E9AF8740|   ueventd         | -  |sleeping  | 01D0  | 464.
E9AFAB80|   logd            | 7. |sleeping  | 0208  | 520.
E9AFC140|      logd.daemon  |    |sleeping  |       | 524.
E9AFC880|      logd.reader  |    |sleeping  |       | 525.
E9AFCFC0|      logd.writer  |    |sleeping  |       | 526.
E9AFD700|      logd.control |    |sleeping  |       | 527.
DEED8740|      logd.reader.p|    |sleeping  |       | 25232.
DA61CFC0|      logd.reader.p|    |sleeping  |       | 10190.

logd는 스레드 그룹 리더이고 logd.daemon부터 logd.reader.p는 스레드 그룹에 속한 스레드들이다.

먼저 logd 스레드 그룹 리더의 태스크 디스크립터를 보자.
  (struct task_struct *) (struct task_struct*)0xE9AFAB80 = 0xE9AFAB80 =  -> (
    (long int) state = 1 = 0x1 = '....',
    (void *) stack = 0xEA5F8000 =  -> ,
    (atomic_t) usage = ((int) counter = 2 = 0x2 = '....'),
    (unsigned int) flags = 4210944 = 0x00404100 = '.@A.',
...
    (pid_t) pid = 520 = 0x0208 = '....',
    (pid_t) tgid = 520 = 0x0208 = '....',
...
    (struct list_head) thread_group = (
      (struct list_head *) next = 0xE9AFC56C 
        (struct list_head *) next = 0xE9AFCCAC 
          (struct list_head *) next = 0xE9AFD3EC 
            (struct list_head *) next = 0xE9AFDB2C  
              (struct list_head *) next = 0xDEED8B6C

역시나 pid와 스레드 그룹 아이디가 520으로 같고 thread_group이란 연결 리스트 해드에 여러 연결 리스트가 연결돼있다.

thread_group.next에는 0xE9AFC56C 주소를 볼 수 있는데 이는 스레드 그룹에 등록된 스레드의 struct task_struct.thread_group 멤버가 위치한 주소를 의미한다.

struct task_struct 구조체에서 thread_group이란 멤버가 위치한 주소 오프셋은 0x42C이니 다음 계산식으로 0xE9AFC140가 스레드 태스크 디스크립터 주소이다.
0xE9AFC140 = 0xE9AFC56C-0x42C

0xE9AFC140 주소를 struct task_struct 구조체로 캐스팅해서 보자.
v.v %all %l (struct task_struct*)0xE9AFC140
  (struct task_struct *) [-] (struct task_struct*)0xE9AFC140 = 0xE9AFC140 -> (
    (long int) [D:0xE9AFC140] state = 1 = 0x1,
    (void *) [D:0xE9AFC144] stack = 0xE8F3A000,
    (atomic_t) [D:0xE9AFC148] usage = ((int) [D:0xE9AFC148] counter = 2 = 0x2),
    (unsigned int) [D:0xE9AFC14C] flags = 1077952576 = 0x40404040,
    (unsigned int) [D:0xE9AFC150] ptrace = 0 = 0x0,
...
    (pid_t) [D:0xE9AFC510] pid = 524 = 0x020C,
    (pid_t) [D:0xE9AFC514] tgid = 520 = 0x0208,
...
    (struct list_head) [D:0xE9AFC56C] thread_group = (
      (struct list_head *) [D:0xE9AFC56C] next = 0xE9AFCCAC,
      (struct list_head *) [D:0xE9AFC570] prev = 0xE9AFAFAC),

0xE9AFC56C 주소에 thread_group 멤버가 위치해있다.

또한 이 스레드의 pid는 524이고 스레드 그룹 리더의 pid는 520이다.

리마인드를 위해  logd 스레드 그룹 리더의 태스크 디스크립터 thread_group.next가 가르키는 주소를 다시 보자. 역시 0xE9AFC56C 주소를 저장하고 있다.
  (struct task_struct *) (struct task_struct*)0xE9AFAB80 = 0xE9AFAB80 =  -> (
    (long int) state = 1 = 0x1 = '....',
    (void *) stack = 0xEA5F8000 =  -> ,
    (atomic_t) usage = ((int) counter = 2 = 0x2 = '....'),
    (unsigned int) flags = 4210944 = 0x00404100 = '.@A.',
...
    (pid_t) pid = 520 = 0x0208 = '....',
    (pid_t) tgid = 520 = 0x0208 = '....',
...
    (struct list_head) thread_group = (
      (struct list_head *) next = 0xE9AFC56C 
        (struct list_head *) next = 0xE9AFCCAC 
          (struct list_head *) next = 0xE9AFD3EC 
            (struct list_head *) next = 0xE9AFDB2C  
              (struct list_head *) next = 0xDEED8B6C

[안드로이드] 시스템 데몬 부분 빌드 dev utility

안드로이드에서 리눅스 시스템 프로그래밍을 실습하고 싶을 때가 있습니다.

android\system\core 폴더에서 simple_proc 폴더 하나를 생성합시다. 
android\system\core\simple_proc

다음과 같이 메이크 파일을 하나 작성한 후 android\system\core\simple_proc 폴더에서 Android.mk 이름으로 저장합시다.
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := test

LOCAL_SRC_FILES := \
  test.cpp \


LOCAL_MODULE_TAGS := optional

LOCAL_FORCE_STATIC_EXECUTABLE := true

LOCAL_STATIC_LIBRARIES := \
    libadbd \
    libbase \
    libfs_mgr \
    liblog \
    libcutils \
    libc

include $(BUILD_EXECUTABLE)

컴파일하려는 소스 코드이름은 test.cpp 이고 생성하려는 프로그램 이름도 test입니다.
test.cpp 소스 코드 구현부는 다음과 같습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
        int i = 0;

        for( i = 0; i < 100; i++) {
                printf("Tracing Log \n");
                sleep(5);
        }

        return 0;
}

위와 같이 코드 작성 후 test.cpp로 코드를 저장합시다.
다음 명령어를 입력해서 안드로이드 빌드 설정을 합니다.
source build/envsetup.sh
lunch

아래 폴더로 이동합시다.
android/system/core/simple_proc

mm . -B 명령어로 모듈을 빌드합시다.
android/system/core/simple_proc$ mm 

test 프로그램이 생성되면 다음 명령어로 디바이스에 프로그램을 복사합니다.
adb push test /data/

adb shell 명령어 입력 후 다음과 같이 test 프로그램 권한을 부여합니다.
chmod 0777 /data/test

이번에는 test 프로그램을 실행합시다. 다음 명령어를 눈여겨봅시다.
adb shell /data/test 
while true; do ps | grep logd ;sleep 1;done

1초 간격으로 프로세스 리스트를 출력하는 스크립트입니다.


[리눅스커널] 동기화 - 레이스 발생 동작 확인 [라즈베리파이]커널 동기화

커널 동기화 기법은 리눅스 커널에서 가장 배우기 어려운 내용 중 하나입니다. 커널 동기화 기법을 사실상 이론으로 이해하기 때문입니다. 아무리 커널 동기화나 레이스를 이론으로 이해해도 실전 개발에서 배운 내용을 적용하기 어렵습니다. 

예를 들어 레이스로 커널 크래시가 발생했다고 가정합시다. 이 문제를 해결하기 위해서는 어떻게 해야 할까요? 우선 어느 코드 구간이 임계영역인지 분석해야 합니다. 이후 어떤 커널 동기화 방법(스핀락, 뮤텍스)를 써야 할지 판단해야 합니다.

이런 레이스로 인한 커널 동기화 문제를 해결하려면 리눅스 시스템에서 레이스가 어떤 방식으로 발생하는지 직접 체험해야 합니다. 그래야 리눅스 시스템에서 커널 동기화 기법이 왜 필요하며 어떤 커널 동기화 기법(스핀락, 뮤텍스, percpu)을 적용할지 결정할 수 있습니다.

어떻게 실제 리눅스 시스템에서 레이스를 겪을 수 있을까요? 라즈베리파이에서 레이스가 발생하는 상황을 ftrace로 확인할 수 있습니다.

참고로, 커널 고수 개발자나 SoC(System On-Chip) 시스템 개발을 할 때 ftrace를 활용해서 레이스 컨디션으로 발생한 문제를 분석합니다.
     
먼저 어떤 함수 실행 도중 다른 프로세스가 같은 코드를 실행해서 레이스 컨디션이 발생하는 다음 3가지 상황을 살펴보겠습니다. 
1. 유저 공간에서 생성된 프로세스
2. 커널 공간에서 구동하는 프로세스(커널 쓰레드)
3. 인터럽트가 발생해서 같은 함수 두 번 호출

[리눅스커널] 동기화 - 유저 프로세스 시스템 콜 호출 시 레이스 컨디션 [라즈베리파이]커널 동기화

유저 공간에서 생성된 프로세스는 여러 개 쓰레드를 생성합니다. 이 쓰레드들이 같은 디바이스 노드에 접근해서 시스템 콜을 발생하며 커널과 통신합니다.

fork() 라는 함수를 유저 공간에서 시스템 콜로 호출하면 유저 공간에서 sys_clone()이란 함수 호출로 프로세스를 생성합니다. 유저 공간에서 open(), write() 그리고 ioctl() 이란 함수를 호출하면 이에 대응하는 시스템 콜을 호출해서 커널 공간과 통신합니다.

이번에는 시스템 콜 관련 함수에서 발생한 레이스 컨디션을 확인합니다. 
먼저 패치 코드를 소개하겠습니다.
1 diff --git a/kernel/exit.c b/kernel/exit.c
2 --- a/kernel/exit.c
3 +++ b/kernel/exit.c
4 @@ -760,10 +760,24 @@ static void check_stack_usage(void)
5 static inline void check_stack_usage(void) {}
6 #endif
8  +void trace_function_dummy_call(void)
9  +{
10 + trace_printk("[++] comm:%s, pid:%d, from(%pS) \n", 
11 + current->comm, current->pid, (void *)__builtin_return_address(0));
12 +}
13 +
14 void __noreturn do_exit(long code)
15 {
16  struct task_struct *tsk = current;
17  int group_dead;
18 +
19 + trace_printk("[+] comm: %s, pid: %d, global_func_exe_times7: %d\n", 
20 + current->comm, current->pid, global_func_exe_times);
21 +
22 + global_func_exe_times++;
23 +       trace_function_dummy_call();
24 +
25  profile_task_exit(tsk);
26  kcov_task_exit(tsk);
27 @@ -809,6 +823,13 @@ void __noreturn do_exit(long code)
28  }
29 
30  exit_signals(tsk);  /* sets PF_EXITING */
31 +
32 + trace_printk("[-] comm: %s, pid: %d, global_func_exe_times8: %d\n", 
33 + current->comm, current->pid, global_func_exe_times);
34 +
35  /*
36    * Ensure that all new tsk->pi_lock acquisitions must observe
37   * PF_EXITING. Serializes against futex.c:attach_to_pi_owner().

패치 코드는 do_exit() 이라는 함수에 작성하면 됩니다.

패치 작성하는 방법을 먼저 소개합니다. 
먼저 19~22번째 줄 코드는 do_exit() 함수 앞부분에 작성하고 32~34번째 줄 코드는 30번째 exit_signals(tsk); 코드 다음에 입력하면 됩니다.

우선 19번째 줄 코드를 함께 살펴보겠습니다. 코드 내용은 어렵지 않습니다.
19 + trace_printk("[+] comm: %s, pid: %d, global_func_exe_times7: %d\n", 
20 + current->comm, current->pid, global_func_exe_times);
21 +
22 + global_func_exe_times++;

현재 구동 중인 프로세스 이름과 pid 그리고 global_func_exe_times 전역 변수를 ftrace 로그로 출력합니다. 

22번째 줄 코드는 global_func_exe_times 이란 전역 변수를 +1만큼 증감하는 동작입니다.

다음 23번째 줄 코드에서 trace_function_dummy_call() 함수를 호출합니다.
23 +       trace_function_dummy_call();

8~9번째 줄 코드에 구현된 trace_function_dummy_call()는 디버깅 용도로 생성한 함수입니다.
8  +void trace_function_dummy_call(void)
9  +{
10 + trace_printk("[++] comm:%s, pid:%d, from(%pS) \n", 
11 + current->comm, current->pid, (void *)__builtin_return_address(0));
12 +}

이 함수는 왜 생성해서 호출할까요? 이 함수를 ftrace set filter에 지정하면 콜스택을 확인할 수 있기 때문입니다. 레이스 컨디션 발생 시 조금 더 많은 디버깅 정보를 얻을 수 있습니다.

이 패치 코드를 작성한 후 ftrace 로그를 설정해야 합니다.
그동안 ftrace 설정 방법을 많이 소개했으니 예전과 비교해 달라지는 점만 소개하겠습니다.
1 echo  trace_function_dummy_call > /sys/kernel/debug/tracing/set_ftrace_filter
2
3 echo 1 > /sys/kernel/debug/tracing/options/sym-offset

먼저 1번째 줄 코드를 보겠습니다.
set_ftrace_filter로 trace_function_dummy_call() 함수를 지정합니다.

마지막 3번째 줄 코드를 보겠습니다.
ftrace가 콜스택을 출력할 때 함수 주소 정보를 오프셋 주소를 출력(9번째 줄)합니다.
이 옵션을 키고 ftrace 로그를 받으면 다음과 같이 함수 옆에 주소 정보를 볼 수 있습니다.
 => __mmc_start_request+0x78/0x1ac  
 => mmc_start_request+0x158/0x184  
 => mmc_wait_for_req+0x94/0xfc  
 => mmc_io_rw_extended+0x258/0x2f8 
 => sdio_io_rw_ext_helper+0x14c/0x1c0  
 => sdio_readsb+0x2c/0x34 
 => brcmf_sdiod_buffrw+0x50/0xa0 [brcmfmac]  
 => brcmf_sdiod_recv_pkt+0x74/0x80 [brcmfmac] 
 => brcmf_sdiod_recv_buf+0x3c/0x88 [brcmfmac]  
 => brcmf_sdio_dataworker+0x1b68/0x2384 [brcmfmac] 
 => process_one_work+0x224/0x518 
 => worker_thread+0x60/0x5f0 
 => kthread+0x144/0x174  
 => ret_from_fork+0x14/0x28  
 
위 ftrace 로그 콜스택 중 worker_thread 함수 옆에 있는 주소 정보를 같이 확인합시다.
worker_thread+0x60/0x5f0

worker_thread() 함수 전체 크기는 0x5f0이며 worker_thread() 함수 시작 주소 기준으로 0x60에 떨어진 코드에서 process_one_work() 함수를 호출했다는 의미입니다.

먼저 레이스가 발생하지 않을 때 ftrace 로그를 먼저 보겠습니다. 레이스가 발생하지 않았을 때 패턴의 로그를 분석하는 이유는 레이스가 발생했을 때 로그 패턴과 비교하기 위해서입니다.

분석할 로그는 다음과 같습니다.
1 <...>-3106  [001] .... 1342.119405: do_exit: [+] comm: TaskSchedulerRe, pid: 3106, global_func_exe_times: 43
2 <...>-3106  [001] .... 1342.119407: trace_function_dummy_call <-do_exit
3 <...>-3106  [001] .... 1342.119423: <stack trace>
4 => get_signal+0x35c/0x69c  
5 => do_signal+0x300/0x3d4  
6 => do_work_pending+0xb4/0xcc 
7 => slow_work_pending+0xc/0x20 
8 <...>-3106  [001] ....  1342.119427: trace_function_dummy_call: [++] comm:TaskSchedulerRe, pid:3106, from(do_exit+0x890/0xc18) 
9 <...>-3106 [001] .... 1342.119431: do_exit: [-] comm: TaskSchedulerRe, pid: 3106, global_func_exe_times: 44

1번째 줄 로그는 do_exit() 함수 앞부분 코드가 실행할 때 출력합니다.
pid가 3106인 "TaskSchedulerRe" 프로세스가 종료하는 동작입니다.

이때 global_func_exe_times 전역 변수는 43입니다. 이 ftrace 로그를 출력하고 다음 코드를 실행해서 global_func_exe_times 전역 변수를 +1만큼 증감하니 44가 됩니다.
global_func_exe_times++;

2~7번째 줄 로그는 trace_function_dummy_call() 함수 콜스택입니다. 
유저 공간에 실행 중인 프로세스 간 통신을 위해 시그널을 처리하는 동작입니다. 유저 공간 생성된 프로세스는 시그널 통신으로 종료합니다.

8번째 줄 로그는 trace_function_dummy_call() 함수에서 출력하는 프로세스 정보입니다. 1번째 줄 로그와 같은 프로세스 이름과 pid를 볼 수 있습니다.

9번째 줄 로그로 do_exit() 함수 하단에 추가한 패치 코드가 실행함을 알 수 있습니다. 
global_func_exe_times 전역 변수를 +1만큼 증감했으니 44를 출력합니다.

다음 do_exit() 함수에서 6~24줄 코드까지 CPU1에서 pid가 3106인 TaskSchedulerRe 프로세스가 실행했음을 알 수 있습니다. 레이스가 발생하지 않았다고 할 수 있습니다.
1 void __noreturn do_exit(long code)
2 {
3 struct task_struct *tsk = current;
4 int group_dead;
5
6 profile_task_exit(tsk);
7 kcov_task_exit(tsk);
8
9 WARN_ON(blk_needs_flush_plug(tsk));
10
11 if (unlikely(in_interrupt()))
12 panic("Aiee, killing interrupt handler!");
13 if (unlikely(!tsk->pid))
14 panic("Attempted to kill the idle task!");
15 set_fs(USER_DS);
16 ptrace_event(PTRACE_EVENT_EXIT, code);
17 validate_creds_for_do_exit(tsk);
18 if (unlikely(tsk->flags & PF_EXITING)) {
19 pr_alert("Fixing recursive fault but reboot is needed!\n");
20 tsk->flags |= PF_EXITPIDONE;
21 set_current_state(TASK_UNINTERRUPTIBLE);
22 schedule();
23 }
24 exit_signals(tsk);  /* sets PF_EXITING */

레이스가 발생하지 않은 코드를 봤으니 레이스가 발생했을 때 ftrace 로그를 보겠습니다.
1 CompositorTileW-3064  [003] ....  1396.127136: do_exit: [+] comm: CompositorTileW, pid: 3064, global_func_exe_times: 111
2 CompositorTileW-3064  [003] ....  1396.127139: trace_function_dummy_call <-do_exit
3 GpuMemoryThread-3061  [000] ....  1396.127141: do_exit: [+] comm: GpuMemoryThread, pid: 3061, global_func_exe_times: 111
4  GpuMemoryThread-3061  [000] ....  1396.127144: trace_function_dummy_call+0x14/0x58   <-do_exit+0x890/0xc18 
5 CompositorTileW-3064  [003] ....  1396.127169: <stack trace>
6 => get_signal+0x35c/0x69c 
7 => do_signal+0x74/0x3d4 
8 => do_work_pending+0xb4/0xcc  
9 => slow_work_pending+0xc/0x20  
10 GpuMemoryThread-3061  [000] ....  1396.127170: <stack trace>
11 => get_signal+0x35c/0x69c 
12 => do_signal+0x74/0x3d4  
13 => do_work_pending+0xb4/0xcc  
14 => slow_work_pending+0xc/0x20  
15 CompositorTileW-3064  [003] ....  1396.127172: trace_function_dummy_call: [++] comm:CompositorTileW, pid:3064, from(do_exit+0x890/0xc18) 
16 GpuMemoryThread-3061  [000] ....  1396.127174: trace_function_dummy_call: [++] comm:GpuMemoryThread, pid:3061, from(do_exit+0x890/0xc18) 
17 CompositorTileW-3064  [003] ....  1396.127178: do_exit: [-] comm: CompositorTileW, pid: 3064, global_func_exe_times: 114

1번째 줄 로그를 보면 global_func_exe_times 값이 111입니다. 이 ftrace 로그는 다음 코드 6번째 줄 코드가 실행됐을 때 출력합니다.
1 void __noreturn do_exit(long code)
2 {
struct task_struct *tsk = current;
int group_dead;
5 +
6 + trace_printk("[+] comm: %s, pid: %d, global_func_exe_times: %d\n", 
7 + current->comm, current->pid, global_func_exe_times);
8 +
9 + global_func_exe_times++;
10 +       trace_function_dummy_call();

위 do_exit() 함수 6번째 줄 코드를 실행하면 바로 9번째 줄 코드를 실행해서 global_func_exe_times 전역 변수를 +1만큼 증감합니다. 그 결과는 당연히 112라고 예상할 수 있습니다. 여기서 의문이 생깁니다.

CPU3에서 실행 중인 CompositorTileW-3064 프로세스가 global_func_exe_times 전역 변수를 111로 출력하고 바로 +1만큼 증감했는데, CPU0에서 구동 중인 GpuMemoryThread-3061 프로세스는 global_func_exe_times 값을 111로 출력한다. 뭔가 이상하지 않나?

맞습니다. 3번째 줄 ftrace 로그를 보면 뭔가 이상합니다.
3 GpuMemoryThread-3061 [000] ....  1396.127141: do_exit: [+] comm: GpuMemoryThread, pid: 3061, global_func_exe_times: 111

CPU0에서 구동 중인 Pid가 3061인 GpuMemoryThread 프로세스가 실행할 때 global_func_exe_times 전역 변수는 112이어야 합니다. CPU0에서 구동 중인 CompositorTileW 프로세스가 global_func_exe_times 전역변수를 +1만큼 증감했기 때문입니다. 여기서 다시 의문이 생깁니다.

Pid가 3061인 GpuMemoryThread 프로세스가 global_func_exe_times 값을 111로 출력하는 이유는 뭘까?

CPU3에서 실행 중인 CompositorTileW 프로세스(pid: 3064)가 global_func_exe_times 값을 111을 출력하고 +1을 증감하려는 사이 CPU0에서 구동 중인 GpuMemoryThread 프로세스가 do_exit() 함수에 접근해서 global_func_exe_times 전역 변수를 출력했기 때문입니다.

CPU3에서 구동 중인 CompositorTileW 프로세스(pid: 3064)와 CPU0에서 돌던 GpuMemoryThread(pid: 3061) 프로세스가 거의 동시에 do_exit() 함수 6~24번째 줄 코드에 접근한 겁니다.

do_exit() 함수에 여러 프로세스가 접근한 두 개 CPU 실행 흐름을 그림으로 표현하면 다음과 같습니다.  

시간 순서대로 CPU3와 CPU0가 실행한 순서를 알아봅시다.
 
[1] 실행 - CPU3
1396.127136초에 CPU3에서 구동 중인 CompositorTileW-3064 프로세스가 6번째 줄 코드를 실행합니다. global_func_exe_times 전역변수를 111로 출력합니다. 이후 8번째 줄 코드를 실행해서 global_func_exe_times 전역 변수를 +1만큼 증감할 겁니다.

[2] 실행 - CPU0
1396.127141초에 CPU0에서 구동 중인 GpuMemoryThread-3061 프로세스가 6번째 줄 코드 실행으로 global_func_exe_times 전역변수를 111로 출력합니다. CPU3에서 6번째 줄 코드를 실행하고 8번째 줄 코드를 실행하기 직전에 6번째 줄 코드를 실행한 겁니다.
 
[3] 실행 - CPU3
정확한 타임스탬프를 예측하기 어려우나 CPU3에서 global_func_exe_times 전역 변수를 +1만큼 증감했습니다.

[4] - CPU0
마찬가지입니다. CPU0에서 global_func_exe_times 전역 변수를 +1만큼 증감했습니다.

이번에는 CPU3에서 돌던 CompositorTileW-3064 프로세스의 함수 실행 흐름과 CPU0에서 구동 중인 GpuMemoryThread-3061 프로세스 콜스택을 봅시다.
5 CompositorTileW-3064  [003] ....  1396.127169: <stack trace>
6 => get_signal+0x35c/0x69c 
7 => do_signal+0x74/0x3d4 
8 => do_work_pending+0xb4/0xcc  
9 => slow_work_pending+0xc/0x20  
10 GpuMemoryThread-3061  [000] ....  1396.127170: <stack trace>
11 => get_signal+0x35c/0x69c 
12 => do_signal+0x74/0x3d4  
13 => do_work_pending+0xb4/0xcc  
14 => slow_work_pending+0xc/0x20 
조금 더 알아보기
유저 공간에서 프로세스가 동작 중에 인터럽트가 발생하면 __irq_usr이란 인터럽트 벡터를 실행합니다.
1  NSR:80789280|__irq_usr:  sub     r13,r13,#0x48    ; r13,r13,#72
2  NSR:80789284|            stmib   r13,{r1-r12}
3  NSR:80789288|     mrc     p15,0x0,r7,c1,c0,0x0   ; p15,0,r7,c1,c0,0 (system control)
4  NSR:8078928C|            ldr     r8,0x80789160
5  NSR:80789290|            ldm     r0,{r3-r5}
6  NSR:80789294|            add     r0,r13,#0x3C     ; r0,r13,#60
7  NSR:80789298|            mvn     r6,#0x0          ; r6,#0
8  NSR:8078929C|            str     r3,[r13]
9  NSR:807892A0|            ldr     r8,[r8]
10 NSR:807892A4|            stm     r0,{r4-r6}
11 NSR:807892A8|            stmdb   r0,{r13-r14}^
12 NSR:807892AC|            teq     r8,r7
13 NSR:807892B0|     mcrne   p15,0x0,r8,c1,c0,0x0   ; p15,0,r8,c1,c0,0 (system control)
14 NSR:807892B4|            mov     r11,#0x0         ; r11,#0
15 NSR:807892B8|            bl      0x801E9E1C       ; trace_hardirqs_off
16 NSR:807892BC|            ldr     r1,0x807892DC
17 NSR:807892C0|            cpy     r0,r13
18 NSR:807892C4|            adr     r14,0x807892CC
19 NSR:807892C8|            ldr     pc,[r1]
20 NSR:807892CC|            lsr     r9,r13,#0x0D
21 NSR:807892D0|            lsl     r9,r9,#0x0D
22 NSR:807892D4|            mov     r8,#0x0          ; r8,#0
23 NSR:807892D8|            b       0x80107E6C       ; ret_to_user_from_irq

19번째 코드에서 인터럽트를 처리하는 함수들을 호출한 다음 ret_to_user_from_irq() 함수를 호출합니다. ret_to_user_from_irq() 함수에서 slow_work_pending() 함수를 호출하는데 slow_work_pending() 함수에서 유저 공간에서 설정한 시그널 처리를 해야 할 상태인지 점검합니다.
NSR:80107E6C|ret_to_user_from_irq:   ldr     r2,[r9,#0x8]
NSR:80107E70|                        cmp     r2,#0x7F000000   ; r2,#2130706432
NSR:80107E74|                        blne    0x8010B5FC       ; addr_limit_check_failed
NSR:80107E78|                        ldr     r1,[r9]
NSR:80107E7C|                        tst     r1,#0x0F         ; r1,#15
NSR:80107E80|                        bne     0x80107E48       ; slow_work_pending

동작 순서를 정리하면 다음과 같습니다.
유저 공간 프로세스 실행 중 -> 인터럽트 발생 -> _irq_usr()이란 인터럽트 벡터 실행 -> 인터럽트 핸들러 실행 종료 -> 유저 공간에서 요청한 시그널 처리

콜스택이 동일합니다. 두 개의 프로세스를 종료하는 시그널을 전달받아 동시에 프로세스를 종료하는 상황입니다.

여기까지 코드를 분석했는데 몇 가지 의문점이 생기니 다시 Q&A 시간을 갖겠습니다.

실제 리눅스 커널 함수인 do_exit()에서는 이 코드 구간에 스핀락이나 뮤텍스를 써서 Critical Section을 보호하지 않는다. 그 이유는 무엇일까?

프로세스가 동시에 같은 함수에 접근할 때마다 반드시 락을 걸 필요는 없습니다. 대신 프로세스를 식별할 수 있는 유일한 디스크립터만 있으면 됩니다.

CPU3에서 돌던 CompositorTileW-3064 프로세스와 CPU0에서 구동 중인 GpuMemoryThread-3061은 각자 태스크 디스크립터(struct task_struct)가 있습니다. 
1 void __noreturn do_exit(long code)
2 {
3 struct task_struct *tsk = current;
4 int group_dead;
5
6 profile_task_exit(tsk);
7 kcov_task_exit(tsk);

do_exit() 함수를 잠깐 보면 3번째 줄에서 스택 주소에 접근해서 현재 실행 중인 프로세스 정보를 담고 있는 태스크 디스크립터에 접근합니다. 이 태스크 디스크립터로 프로세스를 종료하는 동작을 실행하는 겁니다.

CPU3에서 실행 중인 CompositorTileW 프로세스(pid: 3064)와 CPU0에서 구동 중인 GpuMemoryThread 프로세스는 각각 태스크 디스크립터를 갖고 있습니다.

따라서 동시에 같은 함수를 수행 중이라고 해도 각각 프로세스를 관리하는 태스크 디스크립터로 코드를 실행하면 구지 락을 걸 필요가 없습니다.

프로세스를 관리할 수 있는 유일한 메모리 혹은 자료구조는 무엇일까?

프로세스는 각자 서로 다른 스택 공간에서 실행합니다. CompositorTileW-3064와 GpuMemoryThread-3061 프로세스는 각각 서로 다른 스택 공간에서 코드를 실행합니다.

프로세스끼리 각자 실행 중인 스택 공간을 공유할 수 없습니다. 각자 독립적인 실행 공간입니다. 전역 변수를 지역 변수에 저장하고 지역 변수에서 쓰기나 읽기를 하는 코드로 레이스를 방지할 수도 있습니다. 지역 변수는 프로세스 각자 실행 중인 스택 메모리 공간을 할당해 씁니다.

그래서 레이스를 방지하기 위해 전역 변수값이나 공용 메모리값을 프로세스별로 유일하게 접근하는 스택 공간에 저장해서 처리하는 기법도 적용합니다.

커널 동기화 기법을 익힐 때 가장 중요한 점은 프로세스가 어떻게 실행하는지와 프로세스를 처리하는 자료구조를 알고 있어야 합니다.

다음에 커널 프로세스가 동작할 때 발생하는 레이스 컨디션에 대해서 확인하겠습니다.

[리눅스커널] Process - 프로세스, 경량 프로세스, 스레드 소개 [라즈베리파이] 커널 프로세스

프로세스라는 개념은 모든 멀티프로그래밍 운영체제의 기본입니다.
프로세스는 실행중인 프로그램의 인스턴스로 정의할 수 있는데, 16명의 유저가 vi를 동시에 실행하면 각각 16개의 프로세스가 존재합니다. (물론 vi 코드는 동일한 것을 공유할 수 있습니다). 
리눅스 코드에서는 프로세스를 태스크(task)나 쓰레드(thread)라고 부릅니다.

프로세스, 경량 프로세스 그리고 쓰레드
프로세스라는 용어는 여러가지 다른 추상화 개념으로 씁니다. 프로세스는 프로그램이 어디까지 실행되었는지를 완벽하게 알고 있는 자료 구조체라 볼 수 있습니다. 

프로세스는 마치 인간과 같습니다. 프로세스는 생성되고 중요하게 관리될 수 있으며 사소하기도 한 삶을 살고, 자식 프로세스를 생성하기도 하고 마지막에는 죽습니다. 
아주 작은 차이가 있다면 프로세스 사이에서는 성별이랑 관계없는 부모 하나만 있다는 겁니다. 

커널 입장에서 프로세스는 시스템 자원을 할당받는 개체입니다. 

프로세스가 생성될 때 부모 프로세스와 거의 동일합니다. 부모의 주소 공간을 물려받고, 부모와 동일한 코드를 실행합니다(프로세스를 생성하는 코드 이후의 명령어를 시작으로 실행을 한다). 부모와 자식은 프로그램 코드가 들어있는 text 페이지는 공유하고 데이터 영역(스택과 힙)은 별도로 갖게 되어, 자식 프로세스가 메모리의 특정 위치에 수정한 내용을 부모가 모를 수도 있습니다.

초기의 유닉스 커널에서는 이런 간단한 모델을 채용했지만, 근래의 유닉스 시스템은 그렇지 않스빈다. 근래의 유닉스 시스템은 멀티쓰레드 애플리케이션이라는 것을 지원하는데, 이는 하나의 유저 프로그램이 애플리케이션 자료 구조를 상당부분 공유하면서 독립적으로 코드를 실행하는 쓰레드를 갖습니다. 이런 시스템에서는 프로세스가 여러개의 유저 쓰레드(또는 그냥 쓰레드)로 구성되어 있고, 각 쓰레드는 프로세스의 실행 흐름을 나타냅니다. 요즘에는 대부분의 멀티쓰레드 애플리케이션이 pthread(POSIX thread) 라이브러리라는 표준 라이브러리 함수를 이용하여 작성합니다. 

예전 버전 리눅스 커널은 멀티쓰레드 애플리케이션을 지원하지 않았습니다. 커널 관점에서 멀티쓰레드 애플리케이션이라는 것은 그냥 프로세스일 뿐이었습니다. 멀티 쓰레드 애플리케이션의 여러 실행 흐름들은 POSIX 계열의 pthread 라이브러리를 통해 전히 유저 모드에서 생성되고 처리되고 스케줄됐습니다. 

그러나 멀티쓰레드를 그런 식으로 구현하는 것은 신통치 않았습니다. 예를 들어 체스 프로그램이 두 개의 쓰레드를 사용한다고 하자. 한 쓰레드는 사용자가 체스를 옮길 때까지 기다리면서 그래픽 체스 보드를 건트롤하고 컴퓨터 플레이어가 체스 옮기는 것을 보여주고, 다른 쓰레드는 그 다음 번에 체스를 어디로 움직어야 할지 고민합니다. 첫 번째 쓰레드가 사용자가 체스를 움직이길 기다리는 동안에 두 번째 쓰레드는 계속해서 실행되어야 하므로 사용자가 생각하는 시간을 활용해야 합니다. 하지만 체스 프로그램이 단일 프로세스라면, 첫번째 쓰레드가 사용자의 액션을 기다리라는 시스템 콜을 호출하면 블럭킹되어 두번째 쓰레드까지 블럭킹됩니다. 대신에 프로세스가 계속해서 실행될 수 있게 복잡한 논블럭킹 테크닉을 채용해야 합니다. 

리눅스는 경량 프로세스를 사용하여 멀티쓰레드 애플리케이션을 지원합니다. 기본적으로 두개의 경량 프로세스들은 주소 공간, 파일 등의 리소스를 공유합니다. 두 프로스세스 중 하나가 공유 리소스를 수정하면, 나머지 프로세스도 이를 즉시 알 수 있습니다. 물론 두 프로세스가 공유 리소스를 접근할 때에는 동기화를 이뤄야 합니다. 

멀티쓰레드 애플리케이션을 구현하는 직관적인 방법은 각 쓰레드를 경량 프로세스와 연관지으면 됩니다. 이로 쓰레드들은 동일한 메모리 공간과 파일을 공유하여 애플리케이션 자료 구조에 접근할 수 있습니다. 동시에 각 쓰레드는 커널에 의해 독립적으로 스케줄되기 때문에 한 쓰레드가 일을 하는 동안에 한 쓰레드는 슬립에 들어갈 수 있습니다. POSIX 계열의 리눅스 경량 프로세스를 사용하는 예로는 LinuxThreads, Native POSIX Thread Library (NPTL), IBM’s Next Generation Posix Threading Package (NGPT)가 있습니다. 

POSIX 계열의 멀티쓰레드 애플리케이션은  쓰레드 그룹을 지원하는 커널에 의해 가장 잘 처리합니다. 리눅스에서 쓰레드 그룹은 기본적으로 멀티쓰레드 애플리케이션을 구현하는 경량 프로세스 집합이며 getpid(), kill(), _exit() 같은 시스템 콜에 대해서 하나의 묶음으로 동작합니다.


[리눅스커널] 워크큐(Workqueue) - 딜레이워크(delayed_work)는 어떻게 실행하나? [라즈베리파이] 워크큐

딜레이 워크를 실행하려면 어떤 함수를 호출해야 할까요? schedule_delayed_work() 함수를 실행하면 됩니다.

먼저 딜레이 워크를 실행하는 드라이버 코드를 예제로 열어 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/drivers/mmc/host/bcm2835.c#L651]
1 static
2 bool bcm2835_send_command(struct bcm2835_host *host, struct mmc_command *cmd)
3 {
...
3 if (!cmd->data && cmd->busy_timeout > 9000)
4 timeout = DIV_ROUND_UP(cmd->busy_timeout, 1000) * HZ + HZ;
5 else
6 timeout = 10 * HZ;
7 schedule_delayed_work(&host->timeout_work, timeout);
 
7번째 코드를 살펴보겠습니다.
struct delayed_work 구조체 주소를 &host->timeout_work 변수로 전달합니다. 두 번째 인자는 timeout인데 HZ 단위 시각 정보입니다. 

딜레이 워크를 실행하려면 이렇게 워크를 지연할 시각 정보를 두 번째 인자로 전달해야 합니다. 이때 지연하는 시각 정보는 HZ 단위이라는 점 기억해야 합니다. 

HZ는 1초 안에 동적 타이머를 실행하는 횟수로 진동수라고 말합니다. 리눅스 커널에서 상대 시각을 처리할 때 쓰는 개념입니다.

다음과 같이 6번째 줄 코드가 실행하면 10초만큼 지연해서 딜레이 워크를 실행합니다.
6 timeout = 10 * HZ;

간단히 딜레이 워크를 실행하는 코드를 확인했습니다. 워크 코드를 상세히 분석하고 ftrace 로그를 점검했으면 그리 낯설지는 않을 겁니다.

딜레이 워크를 실행하는 schedule_delayed_work() 함수를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h#L604]
1 static inline bool schedule_delayed_work(struct delayed_work *dwork,
2  unsigned long delay)
3 {
4 return queue_delayed_work(system_wq, dwork, delay);
5 }

4번째 줄 코드를 보면 queue_delayed_work() 함수를 호출합니다. 
여기서 queue_delayed_work() 함수 첫 번째 인자로 struct workqueue_struct 타입 system_wq를 전달합니다. system_wq는 시스템 워크큐이니 schedule_delayed_work() 함수를 호출하면 시스템 워크큐를 쓰는 겁니다.

schedule_delayed_work() 함수 첫 번째 인자인dwork와 두 번째 인자인 delay는 그대로 queue_delayed_work() 함수에 전달합니다.

이어서 queue_delayed_work() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue.c#L1550]
1 static inline bool queue_delayed_work(struct workqueue_struct *wq,
2       struct delayed_work *dwork,
3       unsigned long delay)
4{
5 return queue_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
6}

5번째 줄 코드를 보면 첫 번째 인자로 WORK_CPU_UNBOUND를 추가한 다음 queue_delayed_work_on() 함수를 호출합니다. 

바로 queue_delayed_work_on() 함수를 살펴보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue.c#L1550]
1 bool queue_delayed_work_on(int cpu, struct workqueue_struct *wq,
2    struct delayed_work *dwork, unsigned long delay)
3 {
4 struct work_struct *work = &dwork->work;
5 bool ret = false;
6 unsigned long flags;
7
8 /* read the comment in __queue_work() */
9 local_irq_save(flags);
10
11 if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
12 __queue_delayed_work(cpu, wq, dwork, delay);
13 ret = true;
14 }
15
16 local_irq_restore(flags);
17 return ret;
18 }

워크를 워크큐에 큐잉하기 전에 호출한 queue_work_on() 함수에서 봤던 코드와 유사합니다. 딜레이 워크도 이 함수에와 유사한 동작을 수행하는 것으로 보입니다.

코드 분석 전 우선 4번째 줄 지역 변수 선언부를 봅시다.
4 struct work_struct *work = &dwork->work;

struct delayed_work 구조체 첫 번째 멤버인 워크 주소를 work에 저장합니다.

다음 11~14번째 줄 코드를 분석하겠습니다.
11 if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
12 __queue_delayed_work(cpu, wq, dwork, delay);
13 ret = true;
14 }

struct work_struct.data 멤버와 WORK_STRUCT_PENDING_BIT 매크로와 AND 비트 연산을 한 다음 결괏값이 0이면 if 문 내 코드를 실행합니다.

11~14번째 줄 코드를 이해하기 쉽게 표현하면 다음과 같습니다.
if ( !(work->data == WORK_STRUCT_PENDING_BIT)) {
work->data =| WORK_STRUCT_PENDING_BIT;
__queue_delayed_work(cpu, wq, dwork, delay);
ret = true;
}

work_data_bits(work) 함수를 호출해서 struct work_struct.data 멤버에 접근한 후 WORK_STRUCT_PENDING_BIT가 아니면 12~13번 줄 코드를 실행합니다.

이렇게 work->data와 WORK_STRUCT_PENDING_BIT를 비교하는 이유는 뭘까요? 딜레이 워크를 중복해서 실행하지 않기 위해서입니다. 딜레이 워크도 워크와 같이 실행 요청을 하면 struct delayed_work.work.data 멤버에 WORK_STRUCT_PENDING_BIT(1) 값을 저장합니다. 

딜레이 워크 실행 요청을 했으면 work->data가 WORK_STRUCT_PENDING_BIT(1) 로 변경됩니다. 딜레이 워크를 워커 쓰레드에서 실행하기 전 queue_delayed_work_on() 함수를 두 번 호출하면 딜레이 워크를 중복 실행 요청한다고 보는 겁니다.  정리하면 딜레이 워크를 중복해서 워크큐에 실행 요청하지 않게 처리하는 예외 처리 코드입니다.

이제 딜레이 워크를 실행하는 핵심 함수인 __queue_delayed_work()를 살펴보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue.c#L1550]
1 static void __queue_delayed_work(int cpu, struct workqueue_struct *wq,
2 struct delayed_work *dwork, unsigned long delay)
3 {
4 struct timer_list *timer = &dwork->timer;
5 struct work_struct *work = &dwork->work;
6
7 WARN_ON_ONCE(!wq);
8 WARN_ON_ONCE(timer->function != delayed_work_timer_fn ||
9      timer->data != (unsigned long)dwork);
10 WARN_ON_ONCE(timer_pending(timer));
11 WARN_ON_ONCE(!list_empty(&work->entry));
12
13 if (!delay) {
14 __queue_work(cpu, wq, &dwork->work);
15 return;
16 }
17
18 dwork->wq = wq;
19 dwork->cpu = cpu;
20 timer->expires = jiffies + delay;
21
22 if (unlikely(cpu != WORK_CPU_UNBOUND))
23 add_timer_on(timer, cpu);
24 else
25 add_timer(timer);
26}

7~11번 예외 처리 코드를 보겠습니다.
7 WARN_ON_ONCE(!wq);
8 WARN_ON_ONCE(timer->function != delayed_work_timer_fn ||
9      timer->data != (unsigned long)dwork);
10 WARN_ON_ONCE(timer_pending(timer));
11 WARN_ON_ONCE(!list_empty(&work->entry));

__queue_delayed_work() 함수에 전달된 인자를 점검하는 코드입니다. 
인자가 예상된 값이 아니면 WARN() 매크로를 실행해서 커널 로그로 콜스택을 출력합니다.
WARN() 매크로는 코드 흐름에 소프트웨어적인 오류가 있을 때 실행합니다. 그래서 소스 코드를 보다가 WARN() 매크로를 보면 뭔가 논리적인 오류가 있는 조건이라고 봐야합니다.

WARN 매크로를 실행하면 다음과 같은 디버깅 정보를 커널 로그로 출력합니다.
[출처: https://www.unix.com/programming/148285-what-unbalanced-irq.html
https://www.linuxquestions.org/questions/programming-9/problem-with-interrupt-handle-770992/]
WARNING: at kernel/irq/manage.c:225 __enable_irq+0x3b/0x57()
Unbalanced enable for IRQ 4
Modules linked in: svsknfdrvr [last unloaded: osal_linux]
Pid: 634, comm: ash Tainted: G W 2.6.28 #1
Call Trace:
[<c011a7f9>] warn_slowpath+0x76/0x8d
[<c012fac8>] profile_tick+0x2d/0x57
[<c011ed72>] irq_exit+0x32/0x34
[<c010f22c>] smp_apic_timer_interrupt+0x41/0x71
[<c01039ec>] apic_timer_interrupt+0x28/0x30
[<c011b2b4>] vprintk+0x1d3/0x300
[<c013a2af>] __setup_irq+0x11c/0x1f2
[<c013a177>] __enable_irq+0x3b/0x57
[<c013a506>] enable_irq+0x37/0x54
[<c68c9156>] svsknfdrvr_open+0x5e/0x65 [svsknfdrvr]
[<c016440a>] chrdev_open+0xce/0x1a4
[<c016433c>] chrdev_open+0x0/0x1a4
[<c01602f7>] __dentry_open+0xcc/0x23a
[<c016049a>] nameidata_to_filp+0x35/0x3f
[<c016b3c5>] do_filp_open+0x16f/0x6ef
[<c0278fd5>] tty_write+0x1a2/0x1c9
[<c0160128>] do_sys_open+0x42/0xcb

다시 코드 분석으로 돌아갑시다.

7번째 줄 코드는 워크큐 주소가 NULL이면 WARN() 매크로를 실행해서 콜스택을 커널 로그로 출력합니다.

8번째 줄 코드는 타이머 핸들러 함수가 delayed_work_timer_fn() 인지 점검하고 타이머 핸들러 매개 인자가 dwork인지 점검합니다.

10번째 줄 코드는 딜레이 워크 타이머가 현재 실행 중인지 확인하고 11번 줄 코드는 워크 내 링크드 리스트가 NULL이 아닌지 점검합니다.

모두 디바이스 드라이버 개발자가 딜레이 워크를 중복 실행하거나 인자를 잘못 전달할 경우 에러 메시지를 출력하는 코드입니다. 이런 루틴을 Sanity Check 코드라고도 합니다. 리눅스 커널 많은 핵심 함수 앞부분에 이런 코드가 많습니다.

13번째 줄 코드를 보겠습니다.
13 if (!delay) {
14 __queue_work(cpu, wq, &dwork->work);
15 return;
16 }

delay이라는 인자는 HZ 단위 지연 시각 정보를 저장하고 있습니다. 만약 딜레이 워크에 지연 시각을 0으로 설정하면 delay는 0이니 if문 조건을 만족해서 14번째 줄 코드를 실행합니다. __queue_work() 함수를 호출해서 바로 워크를 워크큐에 큐잉합니다.

이후 15번째 줄 코드와 같이 바로 __queue_delayed_work() 함수를 빠져나옵니다. 이 코드로 재미있는 사실을 알 수 있습니다. 워크 대신에 딜레이 워크를 선언하고 지연시각을 0으로 설정하면 딜레이 워크를 워크로 쓸 수 있다는 점입니다.

디바이스 드라이버에서 딜레이 워크 선언부를 보면 HZ 시각만큼 지연해서 워크를 실행한다고 믿지 말고 schedule_delayed_work(); 함수 두 번째로 전달하는 지연시각을 확인해야겠습니다.

다음 18번째 줄 코드를 보겠습니다.
18 dwork->wq = wq;
19 dwork->cpu = cpu;
20 timer->expires = jiffies + delay;

18~19번 줄 코드를 보면 워크큐 주소를 dwork->wq에 저장하고 실행 중인 cpu 번호를 dwork->cpu에 저장합니다.

20번 줄 코드는 현재 시각 정보를 담고 있는 jiffies에 HZ단위 지연 시각인 delay를 더해서 timer->expires 멤버에 저장합니다. 딜레이 워크용 동적 타이머 만료 시각을 저장하는 코드입니다. 

딜레이 워크에서 가장 중요한 코드입니다.

다음 코드는 딜레이 워크용 동적 타이머를 실행하는 동작입니다.
22 if (unlikely(cpu != WORK_CPU_UNBOUND))
23 add_timer_on(timer, cpu);
24 else
25 add_timer(timer); 

add_timer_on() 혹은 add_timer() 함수를 호출해서 동적 타이머를 실행합니다.

여기까지 __queue_delayed_work() 함수부터 schedule_delayed_work() 함수까지 딜레이 워크를 실행하는 코드를 분석했습니다. 

이 코드 흐름으로 딜레이 워크는 워크와 어떤 차이가 있을까요? 워크는 schedule_work() 함수를 호출하면 워크를 워커풀 구조체 멤버인 struct worker_pool.worklist 링크드 리스트에 큐잉했습니다.

딜레이 워크의 경우 schedule_delayed_work() 함수를 호출하면 딜레이 워크를 워크큐에 바로 큐잉하지는 않습니다. 단지 동적 타이머를 실행해서 지연 시각 후에 동적 타이머 핸들러인 delayed_work_timer_fn() 함수가 실행되도록 합니다. 그래서 이번 소절에서 schedule_delayed_work() 함수를 호출하면 딜레이 워크를 워크큐에 큐잉하지 않고 단지 실행 요청한다고 설명을 드린 겁니다.

그럼 딜레이 워크는 워크큐에 언제 큐잉할까요? 딜레이 워크 동적 타이머 핸들러인 delayed_work_timer_fn() 함수가 실행될 때 수행합니다. 상세분석은 다음에 이어집니다.

[리눅스커널] 워크큐(Workqueue) - 딜레이워크(delayed_work)는 어떻게 초기화하나? [라즈베리파이] 워크큐

딜레이 워크를 실행하기 위해서 먼저 딜레이 워크를 초기화해야 합니다. 이를 위해 INIT_DELAYED_WORK() 매크로 함수를 호출해야 합니다.

먼저 딜레이 워크를 초기화하는 디바이스 드라이버 코드를 열어 봅시다.
[https://elixir.bootlin.com/linux/v4.14.43/source/drivers/thermal/da9062-thermal.c#L248]
1 static int da9062_thermal_probe(struct platform_device *pdev)
2 {
3 struct da9062 *chip = dev_get_drvdata(pdev->dev.parent);
4 struct da9062_thermal *thermal;
...
5 INIT_DELAYED_WORK(&thermal->work, da9062_thermal_poll_on);


5번째 줄 코드를 보면 첫 번째 인자인 &thermal->work 로 struct delayed_work 구조체 주소를 전달하고, 두 번째 인자로 워크 핸들러인 da9062_thermal_poll_on() 함수를 지정합니다. 

딜레이 워크 초기화 과정은 워크 초기화 코드와 비교해서 어떤 차이가 있을까요? 딜레이 워크는 초기화 과정에서 핸들러 함수를 바로 지정합니다.

이제 INIT_DELAYED_WORK() 함수 구현부 코드를 보면서 어떤 동작을 수행하는지 조금 더 확인합시다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h#L259]
1 #define INIT_DELAYED_WORK(_work, _func) \
2 __INIT_DELAYED_WORK(_work, _func, 0)
3
4 #define __INIT_DELAYED_WORK(_work, _func, _tflags) \
5 do { \
6 INIT_WORK(&(_work)->work, (_func)); \
7 __setup_timer(&(_work)->timer, delayed_work_timer_fn, \
8       (unsigned long)(_work), \
9       (_tflags) | TIMER_IRQSAFE); \
10 } while (0)

2번 줄 코드를 보면 INIT_DELAYED_WORK() 매크로 함수는  __INIT_DELAYED_WORK() 함수로 치환됩니다. 이때 __INIT_DELAYED_WORK() 매크로 함수에 전달된 인자는 그대로   __INIT_DELAYED_WORK() 함수에 전달합니다.

6번째 줄 코드를 보겠습니다.
워크를 초기화할 때 썼던 INIT_WORK() 매크로 함수를 써서 워크를 초기화합니다. 여기서 __INIT_DELAYED_WORK() 함수에 전달된 첫 번째 인자는 _work인데 구조체는 struct delayed_work입니다.

INIT_WORK() 매크로 코드는 다음과 같습니다. 링크드 리스트인 entry 멤버를 초기화하고 data에 WORK_DATA_INIT() 값인 0xFFFFFFE0을 지정합니다. func 멤버에는 워크 핸들러 함수 주소를 저장합니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h#L236]
1 #define INIT_WORK(_work, _func) \
2 __INIT_WORK((_work), (_func), 0)
3
4 #define __INIT_WORK(_work, _func, _onstack) \
5 do { \
6 __init_work((_work), _onstack); \
7 (_work)->data = (atomic_long_t) WORK_DATA_INIT(); \
8 INIT_LIST_HEAD(&(_work)->entry); \
9 (_work)->func = (_func);

위에서 살펴봤듯이 struct delayed_work의 첫 번째 멤버는 work이고 타입은 struct work_struct 이니 struct delayed_work->work이란 인자로 워크를 초기화하는 겁니다.

다음 7~9번째 줄 코드를 보겠습니다.
__setup_timer() 함수를 호출해서 동적 타이머를 초기화합니다. 동적 타이머 구조체는 &(_work)->timer이고 동적 타이머 핸들러는 delayed_work_timer_fn() 함수입니다. __setup_timer() 함수에 전달하는 세 번째 인자는 _work인데 구조체는 struct delayed_work입니다.

__setup_timer() 함수는 동적 타이머를 초기화하는 동작입니다.

딜레이 워크는 지정한 시각 후에 실행하는 워크입니다. 이를 위해 동적 타이머를 쓰는 겁니다. 딜레이 워크 함수로 지연 시각을 지정하면 동적 타이머 핸들러인 delayed_work_timer_fn() 함수가 지연 시각 후 실행합니다.

이번에 딜레이 워크를 초기화하는 코드 리뷰 분석을 했습니다. 코드를 열어보니 워크와 비슷한 코드가 많습니다. 딜레이 워크를 워크를 HZ 단위 시각으로 지연해서 실행하는 워크이기 때문입니다. 

다음에 딜레이 워크를 실행하는 동작을 알아보겠습니다.

[라즈베리파이] 워크큐(Workqueue) - 딜레이워크(delayed_work) 소개 [라즈베리파이] 워크큐

워크큐는 대표적인 커널 후반부 처리 기법으로 후반부 처리 코드를 워크 핸들러에서 실행합니다. 동기적으로 처리 할 필요가 없는 코드를 워크 핸들러에 위치시켜 비동기적으로 커널 쓰레드 레벨에서 처리하는 겁니다. 이런 구조로 드라이버를 설계하면 다양한 디바이스 드라이버 시나리오에 맞게 코드를 구성할 수 있습니다. 워크에서 유연성을 추가한 기법이 딜레이 워크입니다.

구체적으로 딜레이 워크란 무엇일까요? 딜레이 워크는 워크를 일정 시각(HZ 단위) 후에 지연시켜 실행합니다. 여기서 말하는 지연 시각은 디바이스 드라이버 시나리오에 맞게 변경할 수 있습니다.

예를 들어 다음 온도를 콘트롤하는 드라이버 시나리오를 생각해 봅시다.
1. 온도가 높아지면 인터럽트가 발생
2. 인터럽트 핸들러에서 워크를 워크큐에 큐잉
3. 워크 핸들러에서 온도 콘트롤 디바이스에 CPU 클락을 낮췄다는 응답을 온도 콘트롤 디바이스에 전달
  - 딜레이 워크를 워크큐에 큐잉함(지연 시각은 60밀리 초 설정)
  - 시나리오: 온도 콘트롤 디바이스는 50밀리 초 후에 응답을 전달함
4. 딜레이 워크가 실행하면서 온도 콘트롤 디바이스에서 전달한 응답을 확인

위 온도 콘트롤 디바이스가 새롭게 바뀌어 온도 디바이스 응답 시간이 50초에서 80밀리 초로 변경됐다고 가정하겠습니다. 이럴 때 디바이스 드라이버 코드를 많이 수정해야 할 수도 있습니다. 그런데 딜레이 워크로 후반부 처리 코드를 설계하면 딜레이 워크의 지연 시각을 90 밀리 초로 변경하면 됩니다. 

여기서 딜레이 워크 지연 시각을 80밀리 초가 아닌 90밀리 초로 주는 이유는 충분히 응답을 기다리는 시각을 고려해야 하기 때문입니다.

딜레이 워크는 어떤 코드로 표현할 수 있을까요? 정답은 struct delayed_work 이란 구조체입니다. 해당 코드를 같이 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h]
1 struct delayed_work {
2 struct work_struct work;
3 struct timer_list timer;
4
5 struct workqueue_struct *wq;
6 int cpu;
};

각 멤버의 의미를 하나씩 살펴봅시다.

work
가장 첫 번째 멤버는 struct work_struct 타입의 work입니다. 이번 장에서 살펴봤던 워크를 표현하는 자료구조입니다.

timer
3번째 줄 코드를 보면 struct timer_list 이란 구조체인 timer를 볼 수 있습니다. 워크를 동적 타이머로 처리하기 위한 타이머 자료 구조입니다. 이 동적 타이머로 지정한 시간 후 워크를 실행합니다.

wq
struct workqueue_struct 구조체로 해당 딜레이 워크가 속한 워크큐 주소를 담고 있습니다.

cpu
딜레이 워크를 실행한 cpu 번호입니다.

딜레이 워크 자료구조를 보니 기존 struct work_struct 타입인 work 멤버와 struct timer_list이란 멤버를 볼 수 있습니다. 워크를 일정 시간 지연하기 위해 동적 타이머를 쓴다는 사실을 알 수 있습니다. 구조체 멤버를 보면 워크와 비슷한 동작을 한다고 유추할 수 있습니다.

이번에 딜레이 워크 구조체를 소개했으니 다음 소절에서 자료 구조로 딜레이 워크를 어떻게 초기화하는지 살펴보겠습니다. 이미 워크에 대해 상세히 코드 분석을 했으니 워크와 차이점 위주로 분석하겠습니다.


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


[리눅스] 특정 process에서 생성된 thread의 갯수 확인하는 방법 [라즈베리파이] 커널 프로세스

mysqld 프로그램의 쓰레드 갯수를 확인하려면 다음 명령어를 입력하면 됩니다.
cat /proc/$(pidof mysqld)/status | grep ^Threads
Threads:        17

다음 명령어를 입력하니 쓰레드 pid를 확인할 수 있습니다.
$ ps -eL -o pid,cmd,lwp,nlwp | grep mysqld
 1063 /usr/sbin/mysqld             1063   17
 1063 /usr/sbin/mysqld             1155   17
 1063 /usr/sbin/mysqld             1156   17
 1063 /usr/sbin/mysqld             1157   17
 1063 /usr/sbin/mysqld             1158   17
 1063 /usr/sbin/mysqld             1159   17
 1063 /usr/sbin/mysqld             1160   17
 1063 /usr/sbin/mysqld             1161   17
 1063 /usr/sbin/mysqld             1162   17
 1063 /usr/sbin/mysqld             1163   17
 1063 /usr/sbin/mysqld             1164   17
 1063 /usr/sbin/mysqld             1306   17
 1063 /usr/sbin/mysqld             1307   17
 1063 /usr/sbin/mysqld             1308   17
 1063 /usr/sbin/mysqld             1309   17
 1063 /usr/sbin/mysqld             1412   17
 1063 /usr/sbin/mysqld             1473   17


[리눅스] 스레드 사용 시 장점 [라즈베리파이] 커널 프로세스

평균 처리 시간이 짧아진다.
예를 들어 A라는 task가 10이라는 시간이 걸리고 B라는 task가 1이라는 시간이 걸린다고 가정합시다.
1) single thread로 처리할 경우
  : A를 먼저 처리하고 B를 처리한다고 하면, A는 처리에 10이라는 시간이 들고 B는 처리에 11이라는 시간이 든다. 평균 처리 시간은 10.5
2) multi thread로 처리할 경우
  : A를 먼저 처리하고 B를 처리한다고 가정하고, time slice를 1씩 고르게 배분한다고 가정하면, A를 처리하는데 11이라는 시간이 들고 B를 처리하는데 2라는 시간이 든다. 평균 처리 시간은 6.5

context switch cost를 고려하지 않았을 때, 총 처리시간은 11로 같지만 A,B를 처리하는데 평균적으로 걸린 시간은 multi thread 기반일 때가 빠르다는 것을 알 수 있습니다. 

여기서 스케쥴링이 잘 될 경우 single thread 기반이 더 빠르다는 것입니다. B를 먼저 처리하고 A를 처리한다면 평균 처리 시간이 6이 되고 더욱이 context switch cost도 없습니다.

하지만 이는 스케쥴을 잘 해줘야 하는 cost가 들기 때문에 **일반적**으로 multi thread 기반의 프로그래밍이 평균 처리 속도를 빠릅니다.


[라즈베리파이] 워크큐(Workqueue) - ftrace 동작 확인 [라즈베리파이] 워크큐

이번에는 라즈베리파이에서 워크가 실제 어떻게 동작하는지 ftrace로 확인하겠습니다. 

이를 위해 다음과 같이 ftrace 로그를 설정할 필요가 있습니다.
#!/bin/sh

echo 0 > /sys/kernel/debug/tracing/tracing_on
sleep 1
echo "tracing_off"

echo 0 > /sys/kernel/debug/tracing/events/enable
sleep 1
echo "events disabled"

echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
sleep 1
echo "set_ftrace_filter init"

echo function > /sys/kernel/debug/tracing/current_tracer
sleep 1
echo "function tracer enabled"

echo insert_work brcmf_sdio_dataworker > /sys/kernel/debug/tracing/set_ftrace_filter
sleep 1
echo "set_ftrace_filter enabled"

echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable

echo 1 > /sys/kernel/debug/tracing/events/workqueue/enable 

sleep 1
echo "event enabled"

echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
echo "function stack trace enabled"

echo 1 > /sys/kernel/debug/tracing/tracing_on
echo "tracing_on"

이전 ftrace 설정과 다른 점은 다음과 같이 set_ftrace_filter로 insert_work() 함수와 brcmf_sdio_dataworker() 함수를 지정한다는 겁니다.
echo insert_work brcmf_sdio_dataworker > /sys/kernel/debug/tracing/set_ftrace_filter

insert_work() 함수는 워크를 워크큐에 큐잉할 때 호출되고 brcmf_sdio_dataworker() 함수는 라즈비안에서 구동하는 워크 핸들러 함수입니다.

두 함수를 set_ftrace_filter로 지정하면 워크를 워크큐에 큐잉하고 워커 쓰레드에서 워크를 실행하는 콜스택을 볼 수 있습니다.
 
다음은 워크큐 관련 ftrace 이벤트를 키는 설정입니다.
echo 1 > /sys/kernel/debug/tracing/events/workqueue/enable 

/sys/kernel/debug/tracing/events/workqueue 디렉토리에 가면 아래 워크큐 관련 이벤트를 볼 수 있는데, 위 명령어로 다음 이벤트들을 킬 수 있습니다. 
workqueue_queue_work, workqueue_activate_work, workqueue_execute_start, workqueue_execute_end

이렇게 ftrace를 설정하고 10초 후 ftrace 로그를 받습니다.

분석하려는 ftrace 로그는 다음과 같습니다.
1 irq/92-mmc1-65    [002] d...   335.716213: workqueue_queue_work: work struct=b972ca68 function=brcmf_sdio_dataworker [brcmfmac] workqueue=b7afd200 req_cpu=4 cpu=4294967295
2 irq/92-mmc1-65    [002] d...   335.716214: workqueue_activate_work: work struct b972ca68
3 irq/92-mmc1-65    [002] d...   335.716215: insert_work <-__queue_work
4 irq/92-mmc1-65    [002] d...   335.716236: <stack trace>
5 => brcmf_sdio_isr
6 => brcmf_sdiod_ib_irqhandler
7 => process_sdio_pending_irqs
8 => sdio_run_irqs
9 => bcm2835_mmc_thread_irq
10 => irq_thread_fn
11 => irq_thread
12 => kthread
13 => ret_from_fork
14 <idle>-0 [001] dnh. 335.716247: sched_wakeup: comm=kworker/u8:1 pid=1353 prio=120 target_cpu=001
15 irq/92-mmc1-65 [002] d... 335.716249: sched_switch: prev_comm=irq/92-mmc1 prev_pid=65 prev_prio=49 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
16 <idle>-0 [001] d... 335.716251: sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=kworker/u8:1 next_pid=1353 next_prio=120
17 kworker/u8:1-1353  [001] ....   335.716257: workqueue_execute_start: work struct b972ca68: function brcmf_sdio_dataworker [brcmfmac]
18 kworker/u8:1-1353  [001] ....   335.716258: brcmf_sdio_dataworker <-process_one_work
19 kworker/u8:1-1353  [001] ....   335.716267: <stack trace>
20 => worker_thread
21 => kthread
22 => ret_from_fork

위 ftrace 로그는 다음 그림과 같이 3 단계로 나눌 수 있습니다.

각각 단계별로 동작 흐름을 정리해 보겠습니다.

[1] 단계
워크를 워크큐에 큐잉하는 동작입니다. __queue_work() -> insert_work() 함수 흐름으로 워크를 워크큐에 큐잉하는 겁니다.

[2] 단계
워크를 워크큐에 큐잉하고 난 후 wake_up_worker() 함수 호출로 워커 쓰레드를 깨웁니다.

[3] 단계
스케쥴링으로 워커 쓰레드가 깨어나 동작을 시작합니다. 워커 쓰레드 핸들 함수인 worker_thread() 함수에서 process_one_work() 함수를 호출해서 워크 핸들러 함수를 호출합니다.

이제 [1]단계 ftrace 로그부터 분석을 시작합니다.

1번 줄 로그부터 분석하겠습니다.
1 irq/92-mmc1-65 [002] d... 335.716213: workqueue_queue_work: work struct=b972ca68 function=brcmf_sdio_dataworker [brcmfmac] workqueue=b7afd200 req_cpu=4 cpu=4294967295
2 irq/92-mmc1-65    [002] d...   335.716214: workqueue_activate_work: work struct b972ca68

먼저 가장 왼쪽 "irq/92-mmc1-65" 메시지를 보면 실행 중인 프로세스는 pid가 65인 "irq/92-mmc1"입니다. 

이런 유형의 프로세스는 IRQ Thread라고 이전 장에서 배웠습니다. IRQ Thread 이름으로 92번 "mmc1" 인터럽트 후반부를 처리하는 IRQ Thread라는 점을 유추할 수 있습니다.

이제 workqueue_queue_work와 workqueue_activate_work 이벤트 로그를 보겠습니다.
워크 핸들러가 brcmf_sdio_dataworker() 함수인 워크를 워크큐에 큐잉하는 동작입니다. struct work_struct 구조체 주소는 0xb972ca68입니다.

2번 줄 로그를 보면 workqueue_activate_work 이벤트 메시지가 보이니 풀 워크에 링크드 리스트에 struct work_struct.entry 주소를 추가했다는 정보를 알 수 있습니다.

다음 3번 줄 로그를 보겠습니다.
3 irq/92-mmc1-65    [002] d...   335.716215: insert_work <-__queue_work
4 irq/92-mmc1-65    [002] d...   335.716236: <stack trace>
5 => brcmf_sdio_isr
6 => brcmf_sdiod_ib_irqhandler
7 => process_sdio_pending_irqs
8 => sdio_run_irqs
9 => bcm2835_mmc_thread_irq
10 => irq_thread_fn
11 => irq_thread
12 => kthread
13 => ret_from_fork

3~13줄 로그로 전체 콜스택 흐름을 보면 irq_thread_fn() -> bcm2835_mmc_thread_irq() 함수 흐름으로 IRQ Thread 핸들러 bcm2835_mmc_thread_irq() 함수가 처리되는 과정에서 워크를 워크큐에 큐잉했음을 알 수 있습니다. 이후 __queue_work() 함수와 insert_work() 함수 순서로 함수 호출이 수행되어 워크를 워크큐에 큐잉합니다.

이 때 struct work_struct 구조체 주소는 0xb972ca68이며 워크 핸들러는 brcmf_sdio_dataworker() 함수 입니다.

다음 14번 줄 로그부터 분석하겠습니다.
14 <idle>-0 [001] dnh. 335.716247: sched_wakeup: comm=kworker/u8:1 pid=1353 prio=120 target_cpu=001
15 irq/92-mmc1-65 [002] d... 335.716249: sched_switch: prev_comm=irq/92-mmc1 prev_pid=65 prev_prio=49 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
16 <idle>-0 [001] d... 335.716251: sched_switch: prev_comm=swapper/1 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=kworker/u8:1 next_pid=1353 next_prio=120

워크를 워크큐에 큐잉했으니 insert_work() 함수에서 wake_up_worker() 함수를 호출해서 워커 쓰레드를 깨우는 동작을 합니다.

14번 줄 로그는 <idle>-0 프로세스에서 "kworker/u8:1" 이란 워커 쓰레드를 깨우는 동작입니다. pid가 1353인 "kworker/u8:1" 프로세스를 런큐에 큐잉하면 스케쥴러는 이 시점의 프로세스 우선 순위를 점검하고 스케쥴러는 "kworker/u8:1" 프로세스를 실행합니다.

15번 줄 로그는 "irq/92-mmc1-65" 이란 IRQ Thread는  "swapper/2"로 스케쥴링됩니다.
16번 줄 로그에서 "<idle>-0" 프로세스에서 "kworker/u8:1" 프로세스로 스케쥴링됩니다. 이제 "kworker/u8:1"이란 워커 쓰레드가 실행하기 직전입니다.

이제부터 다음 그림 [3] 단계 ftrace 로그를 분석을 시작하겠습니다. [1]과 [2] 단계에서 워크를 워크큐에 큐잉한 다음 워커 쓰레드를 깨웠으니 [3] 단계에서는 워커 쓰레드가 깨어나 큐잉한 워크를 실행합니다.

17번 로그를 볼 차례입니다.
17 kworker/u8:1-1353  [001] ....   335.716257: workqueue_execute_start: work struct b972ca68: function brcmf_sdio_dataworker [brcmfmac]

우선 가장 오른편 로그를 보면 실행 중인 프로세스는 pid가 1353인 “kworker/u8:1”입니다. “kworker/u8:1”이란 프로세스 이름으로 워커 쓰레드임을 알 수 있습니다.

마지막 18번 줄 로그부터 콜스택을 확인해보겠습니다.
18 kworker/u8:1-1353  [001] ....   335.716258: brcmf_sdio_dataworker <-process_one_work
19 kworker/u8:1-1353  [001] ....   335.716267: <stack trace>
20 => worker_thread
21 => kthread
22 => ret_from_fork

18번과 20번 줄 로그로 worker_thread() -> process_one_work() 흐름으로 함수 호출 흐름을 볼 수 있습니다. 

워커 쓰레드는 쓰레드 핸들 함수가 worker_thread() 입니다. worker_thread 함수에서 process_one_work() 함수를 호출해서 brcmf_sdio_dataworker() 이란 워크 핸들러 함수를 호출합니다.

리눅스 커널 코드 분석을 하면 시야가 좁아져 전체 코드 흐름을 파악하기 어렵습니다. 그래서 커널 코드를 분석하고 나서 ftrace 로 함수 실행 흐름을 점검할 필요가 있습니다.

다음에 워크를 관련 코드를 실제 입력해서 라즈베리파이에서 동작을 확인합니다.


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


[리눅스] tgid와 pid에 대해서... [라즈베리파이] 커널 프로세스

예전 커널부터 리눅스는 태스크 마다, tgid 와 pid 멤버가 있었습니다.

tgid 는 thread group id 을 나타내고, (posix.1 에서 정의하는 프로세스)
pid 는 thread id 를 나타냅니다,

예전에 모든 스레드는, tgid 와 pid 가 같았습니다.

새로운 커널에서는 leader thread 인 경우만, tgid 와 pid 가 같고, child thread 인 경우는 tgid 와 pid 는 다릅니다.

getpid 를 호출하면 tgid 를 돌려주므로, 같은 thread group 인 경우 다 같습니다.
pid 를 알고 싶으면, sys_gettid() 커널 함수를 호출하면 되고, child thread 에 signal 을 보내고 싶으면, sys_tkill() 커널 함수를 호출하면 됩니다.

아직까지는 유저모드에서 사용할 수 있는 gettid, tkill 은 없는 것 같습니다.

그리고, /proc 은 /proc/tgid 에 대해서만 생성해주기 때문에, /proc 을 통해서는 child thread 에 대해서
알기 어려울 것 같습니다.

[라즈베리파이] 워크큐(Workqueue) - worker_thread() 함수 분석(3) [라즈베리파이] 워크큐



이번에는 worker_thread() 함수에서 가장 중요한 37번 줄 코드를 분석할 차례입니다. 


위 그림에서 워커 쓰레드의 “실행” 단계입니다. 코드를 봅시다.
37 do {
38 struct work_struct *work =
39 list_first_entry(&pool->worklist,
40  struct work_struct, entry);
41
42 pool->watchdog_ts = jiffies;
43
44 if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
45 /* optimization path, not strictly necessary */
46 process_one_work(worker, work);
47 if (unlikely(!list_empty(&worker->scheduled)))
48 process_scheduled_works(worker);
49 } else {
50 move_linked_works(work, &worker->scheduled, NULL);
51 process_scheduled_works(worker);
52 }
53 } while (keep_working(pool));

코드 분석에 들어가기 앞서 do~while 문이 실행할 조건을 결정하는 keep_working() 함수를 알아볼 필요가 있습니다.
1 static bool keep_working(struct worker_pool *pool)
2 {
3 return !list_empty(&pool->worklist) &&
4 atomic_read(&pool->nr_running) <= 1;
5}

3~4번 줄 코드와 같이 워커 풀에 큐잉된 워크가 있는 지와 실행 중인 워커 쓰레드 갯수를 AND 연산해서 리턴합니다. keep_working() 함수가 포함된 do~while 문은 워커 풀에 큐잉된 워크를 모두 처리할 때까지 do~while 루프 안 코드를 실행한다는 의미입니다.

다시 코드 분석을 시작합니다.
먼저 38번 코드를 봅시다. &pool->worklist 주소에 접근해서 struct work_struct 구조체 주소를 읽어 옵니다.
38 struct work_struct *work =
39 list_first_entry(&pool->worklist,
40  struct work_struct, entry);

&pool->worklist 멤버로 struct work_list->entry를 저장하는 링크드 리스트에 접근해서 struct work_struct 구조체 주소를 읽습니다. 

38~40번 줄 코드 실행 원리를 단계별로 조금 더 짚어 보겠습니다.
[1]. struct work_struct.entry 멤버 오프셋을 계산합니다.
[2]. &pool->worklist 주소에서 struct work_struct.entry 멤버 오프셋을 빼서 struct work_struct *work에 저장합니다.

[1] 단계 코드 동작을 확인하겠습니다. list_first_entry () 매크로 함수 두 번째와 세 번째 인자는 각각 struct work_struct, entry입니다.

struct work_struct 구조체에서 entry 멤버가 위치한 오프셋 주소를 의미합니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h#L101]
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
}

오프셋 계산에 대한 이해를 돕기 위해 Trace32 프로그램으로 0xb62d3604 주소에 있는 struct work_struct 멤버를 보겠습니다.
1  (struct work_struct *) [-] (struct work_struct*)0xb62d3604 0xB62D3604 -> (
2    (atomic_long_t) [D:0xB62D3604] data = ((int) [D:0xB62D3604] counter = 0),
3    (struct list_head) [D:0xB62D3608] entry = ((struct list_head *)  
4    (work_func_t) [D:0xB62D3610] func = 0x804FDCA8 = flush_to_ldisc)

3번 줄 디버깅 정보를 보면 entry는 0xB62D3608 주소에 있습니다. struct work_struct 구조체 주소가 0xB62D3604 이니 struct work_struct 구조체에서 entry 멤버가 위치한 오프셋 주소는 0x4(0xB62D3608 - 0xB62D3604)입니다.

[2]번 은  &pool->worklist 주소에서 0x4를 빼서 struct work_struct *work 이란 지역 변수에 저장합니다.

이 동작은 다음 그림으로 설명할 수 있습니다.


위 그림에서 worklist에서 (struct work_struct) 박스로 향하는 화살표를 눈여겨봅시다. 워크를 워크큐에 큐잉하면 워커풀인 struct worker_pool.worklist에 워크의 struct work_struct.entry 주소를 등록합니다.

다음 코드는 위 그림 오른쪽 하단에 entry에서 (struct work_struct) 으로 향하는 화살표와 같습니다.
38 struct work_struct *work =
39 list_first_entry(&pool->worklist,
40  struct work_struct, entry);

struct work_struct.entry 주소를 알았으니 struct work_struct 구조체에서 entry 멤버가 위치한 오프셋을 빼서 struct work_struct 주소를 계산하는 겁니다.

&pool->worklist 주소에서 struct work_struct 주소에 접근하는 방법은 마지막 디버깅 소절에서 알아볼 예정입니다.

다음은 42번 줄 코드를 보겠습니다.
42 pool->watchdog_ts = jiffies;

pool->watchdog_ts 멤버에 현재 시각 정보를 표현하는 jiffies를 저장합니다. 이 값으로 워커 쓰레드 와치독 정보를 갱신합니다. 

참고로, 임베디드 시스템에서 와치독(Watchdog)은 어떤 소프트웨어가 정해진 시간 내에 실행하는지를 확인하는 정보입니다. 와치독(Watchdog)은 임베디드 리눅스 개발 도중 아주 많이 쓰이는 용어이자 개념이므로 잘 알아 둡시다.

이번에는 44번 줄 코드입니다.
44 if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
45 /* optimization path, not strictly necessary */
46 process_one_work(worker, work);

38~40번 줄 코드에서 얻어온 struct work_struct 구조체 주소는 work이란 포인터형 지역 변수에 저장돼 있습니다. 이 변수로 struct work_struct.data 멤버에 접근해서 WORK_STRUCT_LINKED 매크로와 AND 연산을 수행합니다. struct work_struct.data 멤버가 WORK_STRUCT_LINKED이면 배리어 워크이니 50번줄 else 문을 실행합니다. 대부분 워크는 배리어 워크가 아니므로 46번 코드를 실행합니다. 나머지 else문은 배리어 워크를 처리하는 동작입니다.

do~while 문에서 워커 풀에 큐잉된 워크를 모두 처리한 후 실행하는 55번 줄 코드를 보겠습니다.
55 worker_set_flags(worker, WORKER_PREP);

worker.flags 멤버에 WORKER_PREP 매크로를 저장합니다. 워커 쓰레드 상태가 WORKER_PREP 즉 전처리 상태임을 나타냅니다.



이제 56번 줄 코드를 보겠습니다. 위 그림에서 워커 쓰레드의 “슬립” 단계입니다.
56 sleep:
57 worker_enter_idle(worker);
58 __set_current_state(TASK_IDLE);
59 spin_unlock_irq(&pool->lock);
60 schedule();
61 goto woke_up;
62 }

56번 레이블은 sleep인데 57~59줄 코드에서 워커 쓰레드가 휴면에 들어간 준비를 하고 60번 줄 코드에서 휴면에 들어갑니다. 

57번 코드에서 worker_enter_idle() 함수를 호출해서 워커 쓰레드 멤버인 worker->flags를 WORKER_IDLE로 설정합니다. 워커 쓰레드가 워크를 처리하고 난 후 진입하는 상태입니다.

58번 줄 코드는 __set_current_state() 함수를 호출해서 워커 쓰레드 태스크 디스크립터 state 멤버를 TASK_IDLE 매크로로 변경합니다. 태스크 디스크립터 state 멤버를 TASK_IDLE로 변경해서 커널 스케쥴링 처리를 하지 않게 합니다.

59번 줄 코드에서 스핀락을 풀고 60번 줄 코드와 같이 schedule() 함수 호출로 휴면에 들어갑니다.

워크를 큐잉 할때나 워커 쓰레드를 깨울 때 wake_up_worker() 혹은 wake_up_process() 함수를 호출하는데 이 때 61번 줄 코드가 다시 실행합니다. 61번 줄 코드는 woke_up이라는 레이블로 점프를 합니다.

여기까지 워커 쓰레드 실행 흐름을 상세히 알아봤습니다. 다음 절에는 분석한 코드가 라즈베리파이에서는 어떻게 동작하는지 ftrace 로그로 살펴보겠습니다.

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


[라즈베리파이] 프로세스 - 스케줄링(Preemption): 커널 모드 인터럽트 발생 [라즈베리파이] 커널 프로세스

이번에는 커널 모드에서 인터럽트가 발생했을때 스케줄링(Preemption)하는 동작을 살펴봅니다.

커널 모드에서 커널 프로세스가 실행 중 인터럽트가 발생하면 __irq_svc 이란 인터럽트 벡터로 PC를 바꿉니다.
__irq_svc 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/entry-armv.S]
1 __irq_svc:
2 svc_entry
3 irq_handler
4
5 #ifdef CONFIG_PREEMPT
6 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
7 ldr r0, [tsk, #TI_FLAGS] @ get flags
8 teq r8, #0 @ if preempt count != 0
9 movne r0, #0 @ force flags to 0
10 tst r0, #_TIF_NEED_RESCHED
11 blne svc_preempt
#endif

6~7번째 줄 코드부터 봅시다.
6 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
7 ldr r0, [tsk, #TI_FLAGS] @ get flags

프로세스 스택 주소로 struct thread_info 구조체에 접근해서
struct thread_info.preempt_count를 r8, struct thread_info.flags를 r0에 저장합니다.

이번에 8~9번째 줄 코드를 봅시다.
8 teq r8, #0 @ if preempt count != 0
9 movne r0, #0 @ force flags to 0

r8에는 struct thread_info.preempt_count 값이 저장돼 있습니다.
이 값이 0이 아니면 r0를 0으로 강제로 바꿔 버립니다.

만약 r8이 0이면 r0 레지스터는 값을 그대로 갖고 있습니다.

r8이 struct thread_info.preempt_count이고 r0이 struct thread_info.flags이라면 다음과 같이 쉽게 어셈블리 코드를 C 언어 형태로 바꿀 수 있습니다.
if (struct thread_info.preempt_count == 0 ) {
if (struct thread_info.flags == _TIF_NEED_RESCHED) {
 svc_preempt();
}
else {
thread_info_flags = 0;
}

이해를 돕게 위해 작성한 코드이니 정말 위와 같이 코드를 입력한 후 컴파일은 하지 맙시다.

struct thread_info.preempt_count 값이 0이어야 struct thread_info.flags 멤버가 _TIF_NEED_RESCHED(2) 인지 점검하는 겁니다.

svc_preempt 레이블 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/entry-armv.S]
1 svc_preempt:
2 mov r8, lr
3 1: bl preempt_schedule_irq @ irq en/disable is done inside

3번째 줄 코드와 같이 preempt_schedule_irq() 함수를 호출합니다.

preempt_schedule_irq() 함수 구현부 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/core.c#L3596]
1 asmlinkage __visible void __sched preempt_schedule_irq(void)
2 {
3 enum ctx_state prev_state;
4
5 /* Catch callers which need to be fixed */
6 BUG_ON(preempt_count() || !irqs_disabled());
7
8 prev_state = exception_enter();
9
10 do {
11 preempt_disable();
12 local_irq_enable();
13 __schedule(true);
14 local_irq_disable();
15 sched_preempt_enable_no_resched();
16 } while (need_resched());

preempt_schedule_irq() 함수를 보면 13번째 줄 코드에서 __schedule() 함수를 호출합니다.

분석한 내용을 정리합시다.
1> 인터럽트 발생(커널 공간에서 실행 중인 프로세스)
2> 인터럽트 핸들러 실행
3> struct thread_info.preempt_count 값이 0인지 점검
  3.1> struct thread_info.preempt_count 값이 0이면,  struct thread_info.preempt_count 값 _TIF_NEED_RESCHED인지 점검
  3.2> struct thread_info.preempt_count 값이 _TIF_NEED_RESCHED 이면  svc_preempt 레이블 실행
4> svc_preempt 레이블에서 preempt_schedule_irq() 함수를 호출해서 __schedule() 함수 호출

이번에는 Trace32 프로그램으로 위에서 분석한 함수 흐름을 확인해봤습니다. __schedule() 함수에 브레이크 포인트를 걸고 정말 인터럽트 발생 후 스케줄링을 하는지 점검했습니다.
-000|__schedule()
-001|preempt_schedule_irq()
-002|svc_preempt(asm)
-003|__irq_svc(asm)
 -->|exception
-004|blk_flush_plug_list()
-005|current_thread_info(inline)
-005|blk_finish_plug()
-006|ext4_writepages()
-007|__filemap_fdatawrite_range()
-008|filemap_write_and_wait_range()
-009|ext4_sync_file()
-010|vfs_fsync()
-011|fdput(inline)
-011|do_fsync()
-012|ret_fast_syscall(asm)

004번째 콜스택을 보면 blk_flush_plug_list() 함수 실행 중이었습니다.
003번째 콜스택에 __irq_svc() 이란 레이블이 보이니 인터럽트가 발생했음을 알 수 있습니다.

이후 svc_preempt() -> preempt_schedule_irq() -> __schedule() 흐름으로 스케줄링을 하는 겁니다.


인터럽트가 발생했을때 스케줄링(Preemption) 하는 조건을 살펴봤습니다.

[라즈베리파이] 프로세스 - 스케줄링(Preemption): 유저 공간 실행 중 인터럽트 발생 [라즈베리파이] 커널 프로세스

이번에는 인터럽트가 발생했을때 스케줄링(Preemption)하는 동작을 살펴봅니다.

우리는 그 동안 schedule() 함수를 호출했을때만 스케줄링이 실행하는지 알고 있습니다. 그것은 맞는 말입니다.
그런데, 인터럽트가 발생했을때 스케줄링이 실행된다는 사실은 잘 모릅니다.

이 사실을 알면 많은 레이스 컨디션 발생 원인을 알 수 있습니다.

이제부터 __irq_usr이란 인터럽트 벡터부터 __schedule() 함수가 실행하는 코드 흐름을 알아보겠습니다.

유저 공간에서 프로세스 실행 도중 인터럽트가 발생하면 인터럽트 벡터로 __irq_usr이란 레이블을 실행합니다.
먼저 __irq_usr 코드를 보겠습니다. 인터럽트 처리가 아키텍처에 의존적이니 어셈블리 코드로 구현됐습니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/entry-armv.S]
1 __irq_usr:
2 usr_entry
3 kuser_cmpxchg_check
4 irq_handler
5 get_thread_info tsk
6 mov why, #0
7 b ret_to_user_from_irq

4번째 줄 코드는 irq_handler이란 매크로를 실행합니다.
이는 인터럽트 핸들러를 실행하는 코드입니다.

5번째 줄 코드는 현재 프로세스 스택 주소를 통해 struct thread_info 구조체를 읽어오는 동작입니다.
struct thread_info.flags 멤버에 프로세스 스케줄링을 제어하는 필드가 있기 때문입니다.

7번째 줄 코드를 보면 ret_to_user_from_irq 이란 레이블로 branch하는 어셈블리 명령어를 볼 수 있습니다.
어셈블리어로 다음 형식으로 쓰며 C 코드로 함수 호출하는 동작과 같습니다.
b [주소]
b [함수 이름]

ret_to_user_from_irq 레이블을 분석합시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/entry-common.S]
1 ENTRY(ret_to_user_from_irq)
2 ldr r2, [tsk, #TI_ADDR_LIMIT]
3 cmp r2, #TASK_SIZE
4 blne addr_limit_check_failed
5 ldr r1, [tsk, #TI_FLAGS]
6 tst r1, #_TIF_WORK_MASK
7 bne slow_work_pending

먼저 5번째 줄 코드를 보겠습니다.
5 ldr r1, [tsk, #TI_FLAGS]

struct thread_info 구조체에서 flags 멤버를 r1이란 레지스터에 로딩하는 명령어입니다.
참고:다음 코드를 보면 TI_FLAGS 매크로가 struct thread_info 구조체 어떤 오프셋인지 알 수 있습니다.

[https://elixir.bootlin.com/linux/v4.14.43/source/arch/arm/kernel/asm-offsets.c]
DEFINE(TI_FLAGS, offsetof(struct thread_info, flags));

다음은 r1와 _TIF_WORK_MASK 매크로와 AND 연산을 수행합니다.
6 tst r1, #_TIF_WORK_MASK

_TIF_WORK_MASK 매크로와 r1 AND 연산한 결과가 1이면 slow_work_pending 레이블을 실행하는 동작입니다.

이 때 ARM CPSR 레지스터 Z 비트(0x1D3)가 0입니다.
_TIF_WORK_MASK 매크로와 r1 AND 연산한 결과가 0이면 ARM CPSR 레지스터((0x40001D3)) Z 비트가 1이니 slow_work_pending 레이블을 실행하지 않습니다.
tst 명령어는 C 코드에서 if문을 처리할 때 쓰는 어셈블리 명령어이니 잘 알아둡시다.

_TIF_WORK_MASK 매크로 정의를 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/include/asm/thread_info.h]
#define _TIF_WORK_MASK (_TIF_NEED_RESCHED | _TIF_SIGPENDING | \
 _TIF_NOTIFY_RESUME | _TIF_UPROBE)

_TIF_WORK_MASK 매크로는 _TIF_NEED_RESCHED, _TIF_SIGPENDING, _TIF_NOTIFY_RESUME 그리고 _TIF_UPROBE 매크로를 OR 연산한 값입니다.

프로세스의 struct thread_info->flags 가 _TIF_NEED_RESCHED, _TIF_SIGPENDING, _TIF_NOTIFY_RESUME 그리고 _TIF_UPROBE 값 중 하나이면
slow_work_pending 레이블을 호출한다는 겁니다.
참고:
각각 매크로는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/include/asm/thread_info.h] 
#define TIF_SIGPENDING 0 /* signal pending */
#define TIF_NEED_RESCHED 1 /* rescheduling necessary */
#define TIF_NOTIFY_RESUME 2 /* callback before returning to user */
#define TIF_UPROBE 3 /* breakpointed or singlestepping */
...
#define _TIF_SIGPENDING (1 << TIF_SIGPENDING)
#define _TIF_NEED_RESCHED (1 << TIF_NEED_RESCHED)
#define _TIF_NOTIFY_RESUME (1 << TIF_NOTIFY_RESUME)
#define _TIF_UPROBE (1 << TIF_UPROBE)

각각 매크로의 실제 값은 다음과 같습니다.
#define _TIF_SIGPENDING 1 ( 1<< 0 )
#define _TIF_NEED_RESCHED 2 ( 1<< 1 )
#define _TIF_NOTIFY_RESUME 4 ( 1<< 2 )
#define _TIF_UPROBE 8 ( 1<< 4 )

유저 공간에서 실행 중인 프로세스 입장에서는 어떤 함수 실행 중 인터럽트가 발생해서 동작을 멈춘 상태였습니다.

스케줄링 동작에 초점을 맞추면 struct thread_info.flags에 _TIF_NEED_RESCHED 이면 slow_work_pending 레이블을 실행합니다.

slow_work_pending 레이블 코드를 볼 차례입니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/entry-common.S]
slow_work_pending:
1 mov r0, sp @ 'regs'
2 mov r2, why @ 'syscall'
3 bl do_work_pending

3번째 줄 코드와 같이 do_work_pending() 함수로 PC(프로그램 카운터)를 변경합니다.

1번째 줄 코드를 보면 r0에 스택 포인터 주소를 저장합니다. ARM 함수 호출 규약에서 r0는 함수 첫 번째 인자를 전달하는 역할을 수행합니다.

이번에는 do_work_pending() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/signal.c#L609]
1 asmlinkage int
2 do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
3 {
4 trace_hardirqs_off();
5 do {
6 if (likely(thread_flags & _TIF_NEED_RESCHED)) {
7 schedule();
8 } else {

6~7번째 줄 코드를 봅시다.
6 if (likely(thread_flags & _TIF_NEED_RESCHED)) {
7 schedule();

thread_flags 이란 인자는 struct thread_info.flags에 있는 값입니다.
이 값이 _TIF_NEED_RESCHED이면 schedule() 함수를 호출해서 스케줄링(Preemption)을 실행합니다.

분석한 내용을 정리합시다.
1> 인터럽트 발생(유저 공간에서 실행 중인 프로세스)으로 __irq_usr 이란 인터럽트 벡터 실행
2> 인터럽트 핸들러 실행
3> struct thread_info.flags 값이 _TIF_NEED_RESCHED 이면 slow_work_pending 레이블을 호출
4> slow_work_pending 레이블에서 do_work_pending() 함수를 호출해서 스케줄링 실행

다음에는 커널 모드에서 커널 함수 실행 도중 인터럽트가 발생했을때 스케줄링(Preemption)이 어떻게 동작하는지 알아봅시다.

[라즈베리파이] 프로세스 - 주기적으로 스케줄링 설정 요청(타이머 인터럽트) [라즈베리파이] 커널 프로세스

리눅스 커널 개발자들은 언제 레이스 컨디션이 발생할까 걱정을 많이 합니다.
그래서 임계 영역을 어느 코드 구간으로 설정하지 고민합니다.

레이스 컨디션은 언제 어디서나 발생할 수 있지만, 이 걱정을 하기 전에 리눅스 커널에서 스케줄링은 언제 어떻게 수행하는지 점검할 필요가 있습니다.

이번에는 스케줄링을 언제 요청하는지 알아봅시다.
1 > 타이머 인터럽트 발생하여 실행 중인 프로세스가 동작을 멈춤
1 > 인터럽트 벡터(el1_irq, el0_irq)가 실행한 후 타이머 인터럽트 핸들러가 실행
1 > 타이머 인터럽트 핸들러가 실행한 후 schedule_tick() 함수를 실행

local_irq_disable() 함수를 호출하지 않은 이상 인터럽트는 언제든 발생해서 실행 중인 코드를 멈출 수 있습니다.
실행 중인 코드를 멈추고 다시 스케줄링 요청 설정을 한다는 이야기입니다.

먼저 scheduler_tick() 이란 함수를 분석합시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/core.c#L3003]
1 void scheduler_tick(void)
2 {
3 int cpu = smp_processor_id();
4 struct rq *rq = cpu_rq(cpu);
5 struct task_struct *curr = rq->curr;
6 struct rq_flags rf;
7
8 sched_clock_tick();
9
10 rq_lock(rq, &rf);
11
12 update_rq_clock(rq);
13 curr->sched_class->task_tick(rq, curr, 0);
14 cpu_load_update_active(rq);

위 함수에서 13번째 줄 코드를 봅시다.
13 curr->sched_class->task_tick(rq, curr, 0);

실행 중인 프로세스 태스크 디스크립터를 통해 스케줄 클래스 함수를 호출하는 동작입니다.

Trace32로 "curr->sched_class->task_tick" 구조체에서 task_tick이란 멤버가 어떤 함수를 기르키는지 확인해 보겠습니다. 
  (struct task_struct *) (struct task_struct*)0xFFFFFFC5E9DB5580 = 0xFFFFFFC5E9DB5580 = 
    (struct thread_info) thread_info = ((long unsigned int) flags = 2 = 0x2, (mm_segment_t) addr_lim
    (long int) state = 0 = 0x0,
    (void *) stack = 0xFFFFFFC5E9D60000 = __efistub__end+0x2E0F944000,
    (atomic_t) usage = ((int) counter = 2 = 0x2),
    (unsigned int) flags = 4194368 = 0x00400040,
    (unsigned int) ptrace = 0 = 0x0,
    (struct llist_node) wake_entry = ((struct llist_node *) next = 0x0 = ),
    (int) on_cpu = 1 = 0x1,
    (unsigned int) cpu = 2 = 0x2,
    (unsigned int) wakee_flips = 0 = 0x0,
    (long unsigned int) wakee_flip_decay_ts = 0 = 0x0,
    (struct task_struct *) last_wakee = 0x0 = ,
    (int) wake_cpu = 2 = 0x2,
    (int) on_rq = 1 = 0x1,
    (int) prio = 120 = 0x78,
    (int) static_prio = 120 = 0x78,
    (int) normal_prio = 120 = 0x78,
    (unsigned int) rt_priority = 0 = 0x0,
    (struct sched_class *) sched_class = 0xFFFFFF97D7F09A28 = fair_sched_class -> (
      (struct sched_class *) next = 0xFFFFFF97D7F09960 = idle_sched_class,
      (void (*)()) enqueue_task = 0xFFFFFF97D6AFC4A8 = enqueue_task_fair,
      (void (*)()) dequeue_task = 0xFFFFFF97D6AF4530 = dequeue_task_fair,
      (void (*)()) yield_task = 0xFFFFFF97D6AF4368 = yield_task_fair,
      (bool (*)()) yield_to_task = 0xFFFFFF97D6AF6D7C = yield_to_task_fair,
      (void (*)()) check_preempt_curr = 0xFFFFFF97D6AF4198 = check_preempt_wakeup,
      (struct task_struct * (*)()) pick_next_task = 0xFFFFFF97D6B00C4C = pick_next_task_fair,
      (void (*)()) put_prev_task = 0xFFFFFF97D6AF6D38 = put_prev_task_fair,
      (int (*)()) select_task_rq = 0xFFFFFF97D6AF9B0C = select_task_rq_fair,
      (void (*)()) migrate_task_rq = 0xFFFFFF97D6AF74DC = migrate_task_rq_fair,
      (void (*)()) task_woken = 0x0 = ,
      (void (*)()) set_cpus_allowed = 0xFFFFFF97D6AE3C9C = set_cpus_allowed_common,
      (void (*)()) rq_online = 0xFFFFFF97D6AF3DE0 = rq_online_fair,
      (void (*)()) rq_offline = 0xFFFFFF97D6AF3E28 = rq_offline_fair,
      (void (*)()) set_curr_task = 0xFFFFFF97D6AF2FCC = set_curr_task_fair,
      (void (*)()) task_tick = 0xFFFFFF97D6AFB49C = task_tick_fair,
      (void (*)()) task_fork = 0xFFFFFF97D6AF43F4 = task_fork_fair,
      (void (*)()) task_dead = 0xFFFFFF97D6AF7470 = task_dead_fair,
      (void (*)()) switched_from = 0xFFFFFF97D6AF2094 = switched_from_fair,
      (void (*)()) switched_to = 0xFFFFFF97D6AF3BC4 = switched_to_fair,
      (void (*)()) prio_changed = 0xFFFFFF97D6AF0390 = prio_changed_fair,
      (unsigned int (*)()) get_rr_interval = 0xFFFFFF97D6AEDD44 = get_rr_interval_fair,
      (void (*)()) update_curr = 0xFFFFFF97D6AF4090 = update_curr_fair,
      (void (*)()) task_change_group = 0xFFFFFF97D6AF3C6C = task_change_group_fair,
      (void (*)()) fixup_walt_sched_stats = 0xFFFFFF97D6AED3D0 = walt_fixup_sched_stats_fair),

위 디버깅 정보에서 task_tick() 이란 함수 포인터는 task_tick_fair() 함수를 지정하고 있습니다.
이 프로세스는 sched_class로 fair_sched_class Fair 스케줄 클래스를 지정하고 있습니다.

task_tick_fair() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/fair.c#L9044]
1 static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
2 {
3 struct cfs_rq *cfs_rq;
4 struct sched_entity *se = &curr->se;
5
6 for_each_sched_entity(se) {
7 cfs_rq = cfs_rq_of(se);
8 entity_tick(cfs_rq, se, queued);
9 }
10
...
}

10번째 줄 코드를 보면 entity_tick() 함수를 호출합니다.
각각 스케줄링 엔티디를 처리하는 겁니다.

entity_tick() 함수 구현부를 봅시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/fair.c#L3990]
1 static void
2 entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
3 {
4 update_curr(cfs_rq);
5
6 update_load_avg(curr, UPDATE_TG);
7 update_cfs_shares(curr);
8
9 #ifdef CONFIG_SCHED_HRTICK
10 if (queued) {
11 resched_curr(rq_of(cfs_rq));
12 return;
13 }

11번째 줄 코드를 보면 스케줄링 요청을 설정하는 동작을 수행하는 resched_curr() 함수를 호출합니다.

resched_curr() 함수 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/core.c#L479]
1 void resched_curr(struct rq *rq)
2 {
3 struct task_struct *curr = rq->curr;
4 int cpu;
5
6 lockdep_assert_held(&rq->lock);
7
8 if (test_tsk_need_resched(curr))
9 return;
10
11 cpu = cpu_of(rq);
12
13 if (cpu == smp_processor_id()) {
14 set_tsk_need_resched(curr);
15 set_preempt_need_resched();
16 return;
17 }

8번째 줄 코드는 현재 실행 중인 프로세스 태스크 디스크립터 주소로 struct thread_info.preempt_count 값이 TIF_NEED_RESCHED 인지 점검합니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/include/linux/sched.h]
static inline int test_tsk_need_resched(struct task_struct *tsk)
{
return unlikely(test_tsk_thread_flag(tsk,TIF_NEED_RESCHED));
}

struct thread_info.preempt_count 값이 TIF_NEED_RESCHED 이면 중복해서 스케줄링 요청으로 하는 것이니 9번째 줄 return; 코드를 실행해서 함수를 바로 빠져 나옵니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/include/linux/sched.h]
static inline void set_tsk_need_resched(struct task_struct *tsk)
{
set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}

태스크 struct thread_info.preempt_count를 TIF_NEED_RESCHED로 설정합니다.

TIF_NEED_RESCHED 매크로는 ARM64 및 ARM32 아키텍처 모두 1입니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm64/include/asm/thread_info.h#L80]
#define TIF_NEED_RESCHED 1

[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/include/asm/thread_info.h]
#define TIF_NEED_RESCHED 1

다시 정리합시다.
 
만약 bash란 프로세스가 kmalloc() 이란 함수를 호출해서 메모리 할당 중에 타이머 인터럽트가 발생했다고 가정합시다.
bash란 프로세스의 struct thread_info->preempt_count가 0x0입니다.

bash 프로세스 실행 중...
1> kmalloc() 함수 실행 중
2> 타이머 인터럽트 발생
3> 다음 함수 흐름으로 struct thread_info.preempt_count를 TIF_NEED_RESCHED로 설정
     : scheduler_tick() -> task_tick_fair() -> entity_tick() -> resched_curr()
4> 타이머 인터럽트 처리를 마치고 다시 kmalloc() 함수 실행

반복하지만 위 코드 흐름에서 스케줄링이 되는 것이 아니라 스케줄링 요청을 한다는 겁니다. (헷갈리면 안됩니다.)

이렇게 struct thread_info.preempt_count를 TIF_NEED_RESCHED로 설정하면 언제 스케줄링 될까요?
A> wake_up_process(), wake_up_state() 혹은 default_wake_function() 함수가 실행할 때... check_preempt_curr() 함수에서 struct thread_info.preempt_count이 TIF_NEED_RESCHED이면 스케줄링 수행을 결정합니다.

B> 인터럽트 발생 시... 
struct thread_info.preempt_count이 TIF_NEED_RESCHED이면 스케줄링 수행을 결정합니다.

이 코드는 언제 실행하는지 다음 시간에 분석하겠습니다.

[라즈베리파이] Process - 대기큐(Wait queue)- (1) [라즈베리파이] 커널 프로세스

대기 큐(wait queue) 소개
대기 큐는 커널에서 여러 용도로 사용합니다. 특히 인터럽트 핸들링과 프로세스 동기화, 타이밍으로 씁니다. 프로세스는 디스크 연산이 끝나기를 기다리거나, 시스템 리소스가 해제되기를 기다리며 시간이 얼마간 흐르길 기다려야 할 때가 있습니다

대기 큐는 여러 이벤트에 대한 조건부로 대기를 구현하고 표현합니다. 특정 이벤트를 기다리는 프로세스는 적절한 대기 큐에 자기 자신을 넣고 CPU 제어를 포기합니다. 그러므로 대기 큐는 잠자고 있는 프로세스들이 모여있는 장소라고 볼 수 있습니다. 이 프로세스들은 특정 조건이 true가 되면 커널이 깨워줍니다. 

대기 큐는 이중 링크드 리스트로 구현되어 있으며, 이 리스트의 각 개체에는 프로세스 디스크립터를 가리키는 포인터가 들어있습니다. 대기 큐 각각은 wait_queue_head_t 구조체 타입의 대기 큐 헤드에 의해 식별할 수 있습니다. 
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;

대기 큐는 커널의 주요 기능 뿐만 아니라 인터럽트 핸들러에 의해서도 씁니다. 따라서 대기 큐 리스트는 프로세스가 동시에 접근해서 레이스 컨디션이 발생해 의도치 않은 결과가 발생하지 않도록 보호를 잘 해야 합니다. 동기화는 대기 큐의 헤드에 있는 lock 이라는 스핀락으로 수행합니다. task_list 필드는 대기 중인 프로세스 리스트의 헤드입니다.. 

wait_queue_entry  타입의 대기 큐 개체는 다음과 같습니다. 
[https://elixir.bootlin.com/linux/v4.14.49/source/include/linux/wait.h]
1 struct wait_queue_entry {
2 unsigned int flags;
3 void *private;
4 wait_queue_func_t func;
5 struct list_head entry;
6 };

5번째 줄 코드인 entry 멤버는 잠들어 있는 프로세스를 표현하고, 이 프로세스들은 어떤 이벤트가 발생하길 기다리고 있습니다.
 
동일한 이벤트가 발생되기를 기다리는 프로세스 리스트들을 가리키는 포인터가 들어있다.  프로세스의 디스크립터 주소는 task 필드에 저장돼 있습니다.

그러나 대기 큐에 들어있는 모든 잠든 프로세스를 깨우는 게 항상 편리한 것은 아닙니다. 예를 들어 2개 이상의 프로세스가 배타적인 접근 권한의 리소스가 해제되길 기다리고 있다면, 대기 큐에서 프로세스 하나만 깨우는 게 합리적입니다. 이 프로세스가 리소스를 가져가고, 나머지 프로세스들은 계속 자고 있어야 합니다.

그러므로 잠자는 프로세스에는 두가지 종류가 있습니다. 

배타적 프로세스(대기 큐 개체의 flags 필드가 1로 표기됨)는 커널이 선택해서 깨웁니다. 
그리고 비배타적 프로세스(flags 필드가 0)는 이벤트가 발생했을 때 커널이 항상 깨워줍니다. 한번에 한 프로세스에게만 접근 권한을 주는 리소스를 기다리고 있는 프로세스는 전형적인 배타적 프로세스라 할 수 있습니다. 

디스크 블럭 전송이 완료되기를 기다리는 프로세스 그룹들을 생각해 봅시다.
이들 프로세스는 전송이 완료되자마자 모두 깨어나야 합니다. 나중에 살펴보겠지만, func 필드는 대기 큐에 잠들어 있는 프로세스를 어떻게 깨워야 하는지를 지정하는 데 사용됩니다. 

대기 큐 처리하기
새로운 대기 큐는 DECLARE_WAIT_QUEUE_HEAD(name) 매크로를 사용하여 정의합니다. 
[https://elixir.bootlin.com/linux/v4.14.49/source/include/linux/wait.h#L58]
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.head = { &(name).head, &(name).head } }

#define DECLARE_WAIT_QUEUE_HEAD(name) \
struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

이 매크로는 name이라는 새로운 대기 큐 헤드 변수를 선언하고, lock과 head 필드를 초기화합니다.
init_waitqueue_head() 함수는 동적으로 할당되었던 대기 큐 헤드 변수를 초기화합니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/include/linux/wait.h]
static inline void init_waitqueue_entry(struct wait_queue_entry *wq_entry, struct task_struct *p)
{
wq_entry->flags = 0;
wq_entry->private = p;
wq_entry->func = default_wake_function;
}

비배타적 프로세스인 p는 default_wake_function() 이라는 함수에 의해 깨어날 것이며,  try_to_wake_up() 함수의 랩퍼 함수입니다. 
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/core.c#L3616]
int default_wake_function(wait_queue_entry_t *curr, unsigned mode, int wake_flags,
  void *key)
{
return try_to_wake_up(curr->private, mode, wake_flags);
}

또 다른 방법으로는 DEFINE_WAIT 매크로를 사용하는 겁니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/include/linux/wait.h#L999]
#define DEFINE_WAIT_FUNC(name, function) \
struct wait_queue_entry name = { \
.private = current, \
.func = function, \
.entry = LIST_HEAD_INIT((name).entry), \
}

#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)

다음 코드를 보면 wait_queue_t 변수를 선언하고, 이 변수를 현재 CPU에서 돌고 있는 프로세스의 디스크립터와 autoremove_wake_function() 함수의 주소로 초기화합니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/drivers/char/nwbutton.c#L31]
static DECLARE_WAIT_QUEUE_HEAD(button_wait_queue); /* Used for blocking read */

autoremove_wake_function() 함수는 잠들어 있는 프로세를 깨우기 위해 default_wake_function()를 호출하고 그런 다음 대기 큐리스트에서 대기 큐 개체를 제거합니다.  
[https://elixir.bootlin.com/linux/v3.19.8/source/kernel/sched/wait.c#L291]
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int ret = default_wake_function(wait, mode, sync, key);

if (ret)
list_del_init(&wait->task_list);
return ret;
}

잠들어 있는 프로세를 깨우기 위해 default_wake_function()를 호출하고 그런 다음 대기 큐리스트에서 대기 큐 개체를 제거합니다.

마지막으로 커널 개발자는 init_waitqueue_func_entry() 함수를 호출하여 사용자 정의 wake up 함수를 설정할 수 있습니다. 

주요 대기큐 함수들

add_wait_queue() 함수
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/wait.c#L24]
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;

wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
__add_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
개체가 정의되고나면 대기 큐를 삽입해야 합니다. add_wait_queue() 함수는 비배타적 프로세스를 대기 큐 리스트의 맨 처음에 삽입합니다. 

add_wait_queue_exclusive() 함수 
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/wait.c]
void add_wait_queue_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;

wq_entry->flags |= WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
__add_wait_queue_entry_tail(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}

배타적 프로세스를 대기 큐의 맨 마지막에 삽입합니다.

remove_wait_queue() 함수 
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/sched/wait.c]
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;

spin_lock_irqsave(&wq_head->lock, flags);
__remove_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}

대기 큐 리스트에서 프로세스를 제거합니다.

waitqueue_active() 함수
static inline int waitqueue_active(struct wait_queue_head *wq_head)
{
return !list_empty(&wq_head->head);
}

해당 대기 큐 리스트가 비어있는지 확인합니다.

[라즈베리파이] Process - 프로세스 상태 [라즈베리파이] 커널 프로세스

이름으로 알 수 있듯이, state 필드는 프로세스가 어떤 동작 중인지 알려줍니다. 이 필드는 플래그의 배열이고 각 플래그는 프로세스 상태를 표현합니다.. 현재 리눅스 버전에서는 이 상태들이 상호 배타적이고, state의 플래그 하나만 설정하므로 나머지 플래그들은 Clear합니다. 프로세스 상태는 아래와 같습니다. 

TASK_RUNNING
#define TASK_RUNNING 0x0000

프로세스가 CPU에서 실행중이거나 실행되려고 기다리는 중입니다. 실제 CPU에서 실행 중인 프로세스는 struct runqueues.curr란 멤버에 등록되어 있습니다.

TASK_INTERRUPTIBLE 
#define TASK_INTERRUPTIBLE 0x0001

프로세스는 특정 조건이 true가 될 때까지 잠들어 있는 중입니다. 인터럽트가 발생하거나, 프로세스가 기다리고 있는 시스템 리소스가 확보되거나, 시그널이 전달되면 프로세스를 깨울 수 있습니다. (즉, 다시 TASK_RUNNING 상태로 돌려놓습니다.)

TASK_UNINTERRUPTIBLE
#define TASK_UNINTERRUPTIBLE 0x0002

TASK_INTERRUPTIBLE 과 같으나, 시그널이 전달될 때 상태가 바뀌지 않습니다. 프로세스가 인터럽트 발생 시 반응하지 않고 특정 이벤트가 발생할 때까지 기다려야 하는 특정 조건에서 설정합니다.
프로세스가 뮤텍스를 확보하지 못해 휴면에 들어갈 때 TASK_UNINTERRUPTIBLE로 변경합니다.

가장 큰 예는 프로세스가 뮤텍스를 얻지 못했을 때 자신을 TASK_UNINTERRUPTIBLE 상태로 변경하고 휴면에 진입합니다. 해당 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/kernel/locking/mutex.c#L1129]
static noinline void __sched
__mutex_lock_slowpath(struct mutex *lock)
{
__mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_);
}

위와 코드가 실행될 때 해당 콜 스택은 다음과 같습니다.
kworker/u16:3-250   [003] ...2 13594.312403: schedule_preempt_disabled+0x8/0x28 <-__mutex_lock_common+0x520/0xb24
kworker/u16:3-250   [003] ...2 13594.312409: <stack trace>
 => __mutex_lock+0x40/0x50
 => __mutex_lock_slowpath+0x28/0x34
 => mutex_lock+0x54/0x60
 => clk_prepare_lock+0x3c/0x90
 => clk_core_get_rate+0x18/0x50
 => clk_get_rate+0x20/0x34
 => dev_get_cur_freq+0x24/0x54
 => update_devfreq+0xa4/0x1a8
 => devfreq_monitor+0x30/0x8c
 => process_one_work+0x1b0/0x3c4
 => worker_thread+0x20c/0x33c
 => kthread+0x124/0x134
 => ret_from_fork+0x10/0x18

또한 프로세스가 장치 파일을 열고 해당 장치 드라이버가 해당 하드웨어 장치를 프로빙할 때 쓰입니다. 
장치 드라이버는 프로빙이 완료될 때까지 인터럽트를 받으면 안되고, 만약 인터럽트를 받게 되면 하드웨어 장치는 예측불가한 상태에 놓이게 될 수 있기 때문입니다. 

TASK_STOPPED
#define __TASK_STOPPED 0x0004

프로세스 동작이 완료됐습니다. SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 시그널을 받으면 이 상태로 들어갑니다.

TASK_TRACED 
#define __TASK_TRACED 0x0008

디버거에 의해 프로세스 실행이 멈춘 상태입니다. 프로세스가 또 다른 프로세스에 의해 모니터될 때, 프로세스가 TASK_TRACED 상태에 놓일 수 있습니다. 

다음 두 상태는 state 필드와 exit_state(tsk->exit_state) 필드 모두에 저장될 수 있습니다. 필드 이름으로도 알 수 있듯이 프로세스의 실행이 종료되면 이 상태로 들어갑니다. 


EXIT_ZOMBIE
#define EXIT_ZOMBIE 0x0020
프로세스 실행이 종료되었지만, 부모 프로세스가 아직 wait4() 또는 waitpid() 시스템 콜을 호출하여 종료한 자식 프로세스에 대한 정보를 받아 처리를 안 한 상태를 생각해 봅시다.
부모 프로세스가 죽은 자식 프로세스의 프로세스 디스크립터를 필요로할 수 있어서 커널은 wait() 류의 시스템 콜이 호출되기 전에는 프로세스 디스크립터를 없애지 않습니다.
이런 프로세스 상태가  EXIT_ZOMBIE입니다.

EXIT_DEAD
#define EXIT_DEAD 0x0010

부모 프로세스가 wait4() 또는 waitpid() 시스템 콜을 호출하였기 때문에 시스템이 프로세스를 삭제하는 중입니다. EXIT_ZOMBIE에서 EXIT_DEAD로 변경되면 프로세스에 wait() 류의 시스템 콜을 실행중인 프로세스로 인한 레이스 컨디션을 피할 수 있습니다.

state 필드의 값은 할당 문으로 설정합니다. 예를 들면 다음과 같습니다.
p->state = TASK_RUNNING;

커널에서는 _set_current_state와 set_current_state 매크로를 이용합니다.
#define __set_current_state(state_value) do { current->state = (state_value); } while (0)
#define set_current_state(state_value)  smp_store_mb(current->state, (state_value))

current라는 현재 실행 중인 프로세스 태스크 디스크립터에 접근하는 매크로를 써서 struct task_struct->state 에 프로세스 상태를 저장합니다.

pt3_fetch_thread() 함수의 24번째 줄 코드를 봅시다. 
1 static int pt3_fetch_thread(void *data)
2 {
3 struct pt3_adapter *adap = data;
4 ktime_t delay;
5 bool was_frozen;
6
7 #define PT3_INITIAL_BUF_DROPS 4
8 #define PT3_FETCH_DELAY 10
9 #define PT3_FETCH_DELAY_DELTA 2
10
11 pt3_init_dmabuf(adap);
12 adap->num_discard = PT3_INITIAL_BUF_DROPS;
13
14 dev_dbg(adap->dvb_adap.device, "PT3: [%s] started\n",
15 adap->thread->comm);
16 set_freezable();
17 while (!kthread_freezable_should_stop(&was_frozen)) {
18 if (was_frozen)
19 adap->num_discard = PT3_INITIAL_BUF_DROPS;
20
21 pt3_proc_dma(adap);
22
23 delay = PT3_FETCH_DELAY * NSEC_PER_MSEC;
24 set_current_state(TASK_UNINTERRUPTIBLE);  //<<--

이들 매크로는 특정 프로세스와 현재 실행중인 프로세스의 상태를 설정합니다. 특히나 이들 매크로는 컴파일러나 CPU로 인해 다른 명령어와 할당 연산이 섞이지 않도록 보장해줍니다. 명령어 순서가 섞이면 레이스 컨디션이 발생으로 시스템은 오동작 할 수 있습니다.


[라즈베리파이] 워크큐(Workqueue) - worker_thread() 함수 분석(2) [라즈베리파이] 워크큐


먼저 워커 쓰레드 핸들 함수인 worker_thread() 를 분석합니다.
1 static int worker_thread(void *__worker)
2 {
3 struct worker *worker = __worker;
4 struct worker_pool *pool = worker->pool;
5
6 worker->task->flags |= PF_WQ_WORKER;
7 woke_up:
8 spin_lock_irq(&pool->lock);
9
10 /* am I supposed to die? */
11 if (unlikely(worker->flags & WORKER_DIE)) {
12 spin_unlock_irq(&pool->lock);
13 WARN_ON_ONCE(!list_empty(&worker->entry));
14 worker->task->flags &= ~PF_WQ_WORKER;
15
16 set_task_comm(worker->task, "kworker/dying");
17 ida_simple_remove(&pool->worker_ida, worker->id);
18 worker_detach_from_pool(worker, pool);
19 kfree(worker);
20 return 0;
21 }
22
23 worker_leave_idle(worker);
24 recheck:
25 /* no more worker necessary? */
26 if (!need_more_worker(pool))
27 goto sleep;
28
29 /* do we need to manage? */
30 if (unlikely(!may_start_working(pool)) && manage_workers(worker))
31 goto recheck;
32
33 WARN_ON_ONCE(!list_empty(&worker->scheduled));
34
35 worker_clr_flags(worker, WORKER_PREP | WORKER_REBOUND);
36
37 do {
38 struct work_struct *work =
39 list_first_entry(&pool->worklist,
40  struct work_struct, entry);
41
42 pool->watchdog_ts = jiffies;
43
44 if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
45 /* optimization path, not strictly necessary */
46 process_one_work(worker, work);
47 if (unlikely(!list_empty(&worker->scheduled)))
48 process_scheduled_works(worker);
49 } else {
50 move_linked_works(work, &worker->scheduled, NULL);
51 process_scheduled_works(worker);
52 }
53 } while (keep_working(pool));
54
55 worker_set_flags(worker, WORKER_PREP);
56 sleep:
57 worker_enter_idle(worker);
58 __set_current_state(TASK_IDLE);
59 spin_unlock_irq(&pool->lock);
60 schedule();
61 goto woke_up;
62 }

worker_thread() 함수 실행은 다음 그림과 같이 4 단계로 나눌 수 있습니다.


각 단계별로 워커 쓰레드가 어떤 동작을 하는지 살펴보겠습니다.

깨어남
워크를 워크큐에 큐잉하면 wake_up_worker() 이란 함수를 호출합니다. 워커 쓰데르를 깨우는 동작입니다. 이 함수는 스케줄러에게 워커 쓰레드를 깨워 달라는 요청을 합니다. 스케줄러 정책에 따라 워커 쓰레드가 실행할 상황이 되면 스케줄러는 워커 쓰레드를 실행합니다. 이 때 실제 워커 쓰레드가 깨어나 실행을 시작합니다.

이전에 워커 쓰레드 해제 요청이 있었으면 워커를 해제하고 아이들 워커 상태에서 벗어납니다.

전처리
워커 쓰레드 실행 전에 전처리를 하는 단계입니다. need_more_worker() 함수를 실행으로 워커 쓰레드를 실행한 조건인지 점검합니다. 실제 워크를 워크큐에 큐잉하지 않았는데 워커 쓰레드를 깨울 수 있기 때문입니다. 이 조건을 만족하면 바로 슬립에 진입합니다. 이후 워커 플래그에서 WORKER_PREP와 WORKER_REBOUND를 해제(Clear) 합니다.

실행
워커풀에 worklist 이란 링크드 리스트에 접근해서 워크를 실행합니다. 워크를 모두 실행한 다음 워커 플레그에서 WORKER_PREP 를 설정합니다.

슬립
워커 상태를 아이들로 설정하고 슬립에 진입합니다. wake_up_worker() 함수가 호출되서 워커 쓰레드가 깨어날 때까지 슬립 상태를 유지합니다.

워커 쓰레드 전체 실행 흐름을 점검했으니 이제 코드 분석을 시작합니다.

6번 줄 코드부터 보겠습니다.
6 worker->task->flags |= PF_WQ_WORKER;

struct worker->task멤버에 태스크 디스크립터 주소가 있습니다. struct task_struct.flags 필드에 PF_WQ_WORKER라는 매크로를 OR 연산으로 저장합니다. 현재 프로세스가 워커 쓰레드이라고 설정하는 겁니다.

7번 줄 코드를 보겠습니다. 워커 쓰레드 실행 흐름 중 “깨어남” 단계입니다.
7 woke_up:
8 spin_lock_irq(&pool->lock);
9
10 /* am I supposed to die? */
11 if (unlikely(worker->flags & WORKER_DIE)) {
12 spin_unlock_irq(&pool->lock);
13 WARN_ON_ONCE(!list_empty(&worker->entry));
14 worker->task->flags &= ~PF_WQ_WORKER;
15
16 set_task_comm(worker->task, "kworker/dying");
17 ida_simple_remove(&pool->worker_ida, worker->id);
18 worker_detach_from_pool(worker, pool);
19 kfree(worker);
20 return 0;
21 }

워커 쓰레드가 깨어나면 실행하는 레이블입니다. 

woke_up 이란 레이블은 언제 실행할까요? 다음 60번 줄 코드와 같이 워커 쓰레드가 휴면에 들어간 다음 프로세스 스케쥴링으로 깨어나면 61번 줄 코드를 실행합니다. goto 으로 woke_up; 레이블로 이동하는 겁니다.
56 sleep:
57 worker_enter_idle(worker);
58 __set_current_state(TASK_IDLE);
59 spin_unlock_irq(&pool->lock);
60 schedule();
61 goto woke_up;

work_up 레이블을 실행하면 다음과 같이 worker->flags 멤버와 WORKER_DIE 매크로를 AND 연산해서 결과가 1이면 12~20번째 줄 코드를 실행해서 워커 쓰레드를 종료합니다.
11 if (unlikely(worker->flags & WORKER_DIE)) {

worker->flags와 WORKER_DIE 매크로와 AND 연산하기 전 어느 코드에서 worker->flags에 WORKER_DIE 매크로를 설정했을까요?

다음 destory_worker()이란 함수 3번 줄 코드입니다. 
1 static void destroy_worker(struct worker *worker)
2 {
...
3 worker->flags |= WORKER_DIE;
4 wake_up_process(worker->task);
5}

worker->flags 멤버에 OR 연산으로 WORKER_DIE 매크로를 저장한 다음 wake_up_process() 함수 호출로 해당 워커 쓰레드를 깨웁니다. 위 destroy_worker() 함수 3~4번 줄 코드를 실행하면 worker_thread() 함수의 7번과 11번 줄 코드를 실행해서 워커 쓰레드를 종료하는 겁니다.

다음 23번 코드를 보겠습니다.
23 worker_leave_idle(worker);

워커 상태를 idle에서 변경합니다.
worker_leave_idle() 함수를 열어 보면 7번째 줄 코드와 같이 worker->flags 멤버에서 WORKER_IDLE 값을 Clear 시킵니다.
1 static void worker_leave_idle(struct worker *worker)
2 {
3 struct worker_pool *pool = worker->pool;
4
5 if (WARN_ON_ONCE(!(worker->flags & WORKER_IDLE)))
6 return;
7 worker_clr_flags(worker, WORKER_IDLE);
8 pool->nr_idle--;
9 list_del_init(&worker->entry);
10}

다음 24번째 줄 코드를 보겠습니다. 워커 쓰레드의 “전처리” 실행 단계입니다.
24 recheck:
25 /* no more worker necessary? */
26 if (!need_more_worker(pool))
27 goto sleep;

struct worker_pool.worklist 멤버에 접근해서 큐잉한 워크가 있는지와 struct worker_pool.nr_running 멤버에 저장된 실행 중인 워커 쓰레드 갯수를 점검합니다.
워크를 워크큐에 큐잉한 적이 없다면 워커 쓰레드를 실행할 필요가 없으니 goto sleep; 구문을 실행합니다.

스케쥴링으로 워커 쓰레드가 실행했을때 예외 처리 코드입니다.  

다음 35번 줄 코드를 분석하겠습니다.
35 worker_clr_flags(worker, WORKER_PREP | WORKER_REBOUND);

worker->flags 멤버에서 (WORKER_PREP | WORKER_REBOUND) 연산한 결과를 Clear시킵니다. 워커 상태가 WORKER_PREP와 WORKER_REBOUND가 아니라는 의미입니다. WORKER_PREP는 워커 쓰레드 처리 흐름에서 전처리 단계를 의미합니다.

여기까지 워커 쓰레드 예외처리나 상태를 변경하는 루틴입니다. 

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


[리눅스커널] 워크큐(Workqueue) - worker_thread() 함수 분석(1) [라즈베리파이] 워크큐


워크는 워커 쓰레드가 실행합니다. 워커 쓰레드를 관리하는 자료구조는 struct worker 구조체이며 이를 워커라고 부릅니다. 이전까지 자료구조 중심으로 워크를 분석했는데 이번에는 워커 쓰레드가 쓰레드 관점으로 어떻게 실행하는지 알아봅니다. 

다음은 워커 자료구조인 struct worker 구조체 선언부입니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue_internal.h#L24]
1 struct worker {
2 union {
3 struct list_head entry; 
4 struct hlist_node hentry; 
5 };
6 struct work_struct *current_work;
7 work_func_t current_func;
8 struct pool_workqueue *current_pwq; 
9 bool desc_valid;
10 struct list_head scheduled;
11
12 struct task_struct *task;
13 struct worker_pool *pool;
14
15 struct list_head node;
16 unsigned long last_active;
17 unsigned int flags;
18 int id;
19
20 char desc[WORKER_DESC_LEN];
21
22 struct workqueue_struct *rescue_wq;
23};

각 멤버의 의미를 살펴봅시다.

*current_work;
struct work_struct 구조체로 현재 실행하려는 워크를 가르키는 멤버입니다.

current_func;
실행하려는 워크 핸들러 주소를 저장하는 멤버입니다.


워크 구조체와 워크 핸들러는 다음과 같이 process_one_work() 함수에서 위 멤버에 저장합니다.
static void process_one_work(struct worker *worker, struct work_struct *work)
{
...
worker->current_work = work;
worker->current_func = work->func;
worker->current_pwq = pwq;

struct task_struct *task;
워커 쓰레드의 태스크 디스크립터 주소입니다.

struct worker_pool *pool;
워커가 포함된 워커 풀 주소를 저장하는 멤버입니다.

struct list_head node;
워커풀에 등록된 링크드 리스트입니다.

워커를 표현하는 구조체인 struct worker 를 살펴봤으니 워커 쓰레드 동작을 점검하겠습니다. 

워커 쓰레드가 어떤 일을 하는지 알려면 어느 코드를 분석해야 할까요? 커널 쓰레드 동작을 알아보려면 쓰레드 핸들 함수를 분석해야 합니다. 커널 쓰레드가 단계별로 어떤 동작을 하는지 쓰레드 핸들 함수에서 구현했기 때문입니다.

워커 쓰레드도 커널 쓰레드 종류 중 일부분이니 워커 쓰레드 동작을 점검하려면 워커 쓰레드 핸들 함수인 worker_thread()를 분석해야 합니다. worker_thread() 함수를 살펴보기 전 이 쓰레드 핸들 함수를 등록하는 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue.c#L2190]
1 static struct worker *create_worker(struct worker_pool *pool)
2 {
3 struct worker *worker = NULL;
...
4 worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
5       "kworker/%s", id_buf);

kthread_create_on_node() 함수는 커널 쓰레드를 생성할 때 호출합니다.
중요한 인자만 살펴보면, 첫 번째 인자로 쓰레드 핸들, 두 번째 인자로 쓰레드 핸들 매개 변수,
네 번째 인자로 쓰레드 이름을 지정합니다.

4번째 줄 코드를 보면 worker_thread() 함수를 쓰레드 핸들, 두 번째 인자로 쓰레드 핸들 매개 변수, 그리고 네 번째 인자로 워커 쓰레드 이름을 지정합니다.

이번에는 워커 쓰레드 핸들 함수 선언부를 살펴봅니다.
static int worker_thread(void *__worker);

인자
인자는 *__worker인데 워커 쓰레드를 생성할 때 전달했던 struct worker 구조체 주소입니다.
워커를 처리하는 핸들 주소를 worker_thread()란 쓰레드 핸들 함수로 넘겨 받는 겁니다.

이 자료구조는 각각 워커를 표현하며 워커 쓰레드 디스크립터라고 봐도 됩니다.

반환값
함수 선언부를 보면 int 타입을 반환합니다. 워커 해제 요청을 받아 worker_thread() 함수를 호출할 때만 0을 반환하고 이외에는 워커 쓰레드가 동작하는 동안 반환값을 전달하지 않고 쓰레드 핸들 함수 내에서 계속 실행합니다.

다음 destroy_worker() 함수 코드에서 워커 해제 요청을 합니다.
static void destroy_worker(struct worker *worker)
{
...
list_del_init(&worker->entry);
worker->flags |= WORKER_DIE;
wake_up_process(worker->task);
}

worker_thread() 함수 인자와 반환값을 알아봤습니다.

쓰레드 실행 흐름은 알기 위해서 쓰레드 핸들 함수 구조를 파악해야 합니다. 워커 쓰레드를 생성할 때 쓰레드 핸들로 worker_thread() 함수를 등록했으니 이 함수 중심으로 코드 분석을 해 봅시다.

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



1 2 3 4 5 6 7 8 9 10 다음