Linux Kernel(4.14) Hacks

rousalome.egloos.com

포토로그 Kernel Crash




[라즈베리파이] 시그널 - 커널 공간 시그널 함수 분석 - do_sigaction() [라즈베리파이] 시그널

다음으로 살펴볼 코드는 do_sigaction() 함수입니다. 먼저 do_sigaction() 함수는 선언부를 봅시다.
int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact);

do_sigaction() 함수에 전달하는 인자 속성은 다음과 같은데 sys_rt_sigaction() 함수에 전달된 인자 유형과 같습니다.
int sig: 설정한 시그널 번호
struct k_sigaction *act: 새롭게 설정하는 시그널 속성
struct k_sigaction *oact: 이전에 설정했던 시그널 속성

do_sigaction() 함수 인자를 알아봤으니 코드를 분석할 차례입니다.
1 int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
2 {
3 struct task_struct *p = current, *t;
4 struct k_sigaction *k;
5 sigset_t mask;
6
7 if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
8 return -EINVAL;
9
10 k = &p->sighand->action[sig-1];
11
12 spin_lock_irq(&p->sighand->siglock);
13 if (oact)
14 *oact = *k;
15
16 sigaction_compat_abi(act, oact);
17
18 if (act) {
19 sigdelsetmask(&act->sa.sa_mask,
20       sigmask(SIGKILL) | sigmask(SIGSTOP));
21 *k = *act;
22
23 if (sig_handler_ignored(sig_handler(p, sig), sig)) {
24 sigemptyset(&mask);
25 sigaddset(&mask, sig);
26 flush_sigqueue_mask(&mask, &p->signal->shared_pending);
27 for_each_thread(p, t)
28 flush_sigqueue_mask(&mask, &t->pending);
29 }
30 }
31
32 spin_unlock_irq(&p->sighand->siglock);
33 return 0;
34 }

7번째 줄 코드를 보겠습니다. 인자로 전달된 정수형 시그널 번호에 오류가 있는지 점검하는 예외처리 코드입니다. 유저 공간에서 정의되어 있지 않은 시그널 번호를 함수 인자로 전달할 수 있기 때문입니다.
7 if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
8 return -EINVAL;

이 조건을 만족하면 -EINVAL를 반환하고 함수 실행을 종료하면서 시그널 설정을 더 이상 진행하지 않습니다.

10번째 줄 코드를 보겠습니다.
3 struct task_struct *p = current, *t;
4 struct k_sigaction *k;
..
10 k = &p->sighand->action[sig-1];

현재 실행 중인 태스크 디스크립터 주소를 저장하고 있는 p이란 지역 변수로 시그널 번호인 sig에 해당하는 action 멤버 배열을 k이란 struct k_sigaction 구조체 지역 변수에 저장합니다.

struct task_struct 구조체 멤버 sighand->action[] 에는 시그널 종류 별로 어떤 동작을 처리할지에 대한 설정 정보가 저장돼 있습니다.

Trace32로 &p->sighand->action 멤버를 확인하면 다음과 같습니다.
1  (static struct task_struct) \Global\init_task = (
2    (long int) state = 0 = 0x0,
3    (void *) stack = 0x80C00000,
...
4    (struct sighand_struct *) sighand = 0x80C07B80 -> (
5      (atomic_t) count = ((int) counter = 3 = 0x3),
6      (struct k_sigaction [64]) action = (
7         [0] = ((struct sigaction) sa = ((__sighandler_t)sa_handler = 0x0, (long unsigned int) sa_flags = 0
8         [1] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_flags = 0
9         [2] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE00B4, (long unsigned int) sa_flags = 0
10       [3] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE00B4, (long unsigned int) sa_fla
11       [4] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_fla

...
12        [13] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_flags = 0
12        [63] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_flags = 0

action이란 배열의 크기는 64개이고4번째 줄 정보에 보이는 struct sighand_struct 구조체 멤버 중 하나입니다. 

다음과 같이 2~3번째 배열을 보면 sa_handler 멤버에서 주소를 볼 수 있습니다. 2번째 배열은 SIGINT, 3번째 배열은 SIGQUIT 시그널을 의미하며 유저 공간에서 설정된 시그널 핸들러 주소가 0x7AAE00B4임을 알 수 있습니다.
9 [2] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE00B4, (long unsigned int) sa_flags = 0
10 [3] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE00B4, (long unsigned int) sa_fla

다음 코드는 SIGINT와 SIGQUIT 시그널 번호 선언부입니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/uapi/asm/signal.h]
#define SIGINT  2
#define SIGQUIT  3

다음 13~14번째 줄 코드를 보겠습니다.
13 if (oact)
14 *oact = *k;

현재 실행 중인 프로세스에서 읽어온 k이란 포인터를 oact에 저장합니다.
k는 시그널 번호에 해당하는 struct k_sigaction 구조체인데 oact 포인터에 저장하는 동작은 기존에 설정된 시그널 속성을 oact 포인터로 저장한다는 의미입니다.

다음 18번째 줄 코드를 보겠습니다. 이 함수에서 가장 중요한 동작입니다.
18 if (act) {
19 sigdelsetmask(&act->sa.sa_mask,
20       sigmask(SIGKILL) | sigmask(SIGSTOP));
21 *k = *act;

do_sigaction() 함수로 전달된 act이란 인자는 새롭게 설정하는 시그널 속성을 의미합니다.
이를 k이란 포인터에 저장합니다. 이는 새롭게 설정한 struct k_action 구조체를 프로세스 태스크 디스크립터인 struct task_struct 구조체 내 sighand->action 배열에 저장한다는 의미입니다.

k이란 지역 변수 출처를 다시 확인하겠습니다.
3 struct task_struct *p = current, *t;
..
10 k = &p->sighand->action[sig-1];

가장 중요한 sys_rt_sigaction() 함수가 실행한 다음 변경되는 시그널 관련 자료 구조를 Trace32로 살펴봅시다.
1  (static struct task_struct) \Global\init_task = (
2    (long int) state = 0 = 0x0,
3    (void *) stack = 0x80C00000,
...
4    (struct sighand_struct *) sighand = 0x80C07B80 -> (
5      (atomic_t) count = ((int) counter = 3 = 0x3),
6      (struct k_sigaction [64]) action = (
7         [0] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_flags = 0
8         [1] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x0, (long unsigned int) sa_flags = 0
9         [2] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE11AC, (long unsigned int) sa_flags = 0
10       [3] = ((struct sigaction) sa = ((__sighandler_t) sa_handler = 0x7AAE00B4, (long unsigned int) sa_fla

시그널을 설정하면 시그널을 처리할 프로세스의 태스크 디스크립터 struct task_struct sighand->action[] 배열이 업데이트됩니다. 프로세스 태스크 디스크립터 sighand->action[64] 배열 중 시그널 번호에 해당하는 멤버가 변경된다는 것입니다.

만약 2번 SIGINT 시그널을 설정했고 시그널 핸들러 주소가 0x7AAE11AC이면 위와 같이 9번째 줄 정보와 같이 업데이트됩니다.

이렇게 시그널을 설정하고 난 후 SIGINT 시그널이 발생하면 프로세스가 시그널을 받아서 struct task_struct sighand->action[] 배열에 저장된 속성에 따라 시그널을 처리합니다.


#Reference 시스템 콜


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



[라즈베리파이] 시그널 - 커널 공간 시그널 함수 분석 - sys_rt_sigaction() [라즈베리파이] 시그널

먼저 sys_rt_sigaction() 함수 코드를 보기 전에 선언부 코드를 확인합시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/syscalls.h]
asmlinkage long sys_rt_sigaction(int,
 const struct sigaction __user *,
 struct sigaction __user *,
 size_t);

함수 선언부 가장 왼쪽에 있는 asmlinkage 지시자는 어셈블리 코드에서 이 함수를 호출한다는 의미입니다. 조금 더 구체적으로 vector_swi 레이블에서 sys_rt_sigaction() 함수로 분기합니다.

sys_rt_sigaction() 함수에 전달하는 인자와 속성은 다음 테이블과 같습니다.
int sig: 시그널 번호
struct k_sigaction *act: 새롭게 설정하는 시그널 속성
struct k_sigaction *oact: 이전에 설정했던 시그널 속성

sys_rt_sigaction() 함수 인자를 알아봤으니 코드를 살펴볼 차례입니다. 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/signal.c]
1 SYSCALL_DEFINE4(rt_sigaction, int, sig,
2 const struct sigaction __user *, act,
3 struct sigaction __user *, oact,
4 size_t, sigsetsize)
5 {
6 struct k_sigaction new_sa, old_sa;
7 int ret = -EINVAL;
8
9 /* XXX: Don't preclude handling different sized sigset_t's.  */
10 if (sigsetsize != sizeof(sigset_t))
11 goto out;
12
13 if (act) {
14 if (copy_from_user(&new_sa.sa, act, sizeof(new_sa.sa)))
15 return -EFAULT;
16 }
17
18 ret = do_sigaction(sig, act ? &new_sa : NULL, oact ? &old_sa : NULL);
19
20 if (!ret && oact) {
21 if (copy_to_user(oact, &old_sa.sa, sizeof(old_sa.sa)))
22 return -EFAULT;
23 }
24 out:
25 return ret;
26 }

먼저 13번째 줄 코드를 보겠습니다. 새롭게 설정하는 시그널 속성을 &new_sa.sa 이란 지역 변수에 저장합니다.
13 if (act) {
14 if (copy_from_user(&new_sa.sa, act, sizeof(new_sa.sa)))
15 return -EFAULT;
16 }

유저 공간에서 전달된 메모리 버퍼는 copy_from_user() 함수로 복사할 수 있습니다.

18번째 줄 코드는 do_sigaction() 함수를 실행합니다.
22 ret = do_sigaction(sig, act ? &new_ka : NULL, oact ? &old_ka : NULL);

24번째 줄 코드를 보겠습니다.
24 if (!ret && oact) {
25 if (!access_ok(VERIFY_WRITE, oact, sizeof(*oact)) ||
26     __put_user(old_ka.sa.sa_handler, &oact->sa_handler) ||
27     __put_user(old_ka.sa.sa_restorer, &oact->sa_restorer) ||
28     __put_user(old_ka.sa.sa_flags, &oact->sa_flags) ||
29     __put_user(old_ka.sa.sa_mask.sig[0], &oact->sa_mask))
30 return -EFAULT;
31 }

이전에 설정했던 시그널 속성을 old_ka 이란 포인터형 지역 변수에 저장합니다.


#Reference 시스템 콜


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


[라즈베리파이] 시그널 - 커널 공간 시그널 함수 분석 [라즈베리파이] 시그널

시그널과 연관된 동작을 변경하면 유저 공간에서 sigaction() 함수를 호출하면 됩니다. 이 함수를 호출하면 시스템 콜 핸들러인 sys_rt_sigaction() 함수가 실행하며 커널 공간에서 시그널을 설정합니다. 이 실행 흐름을 표현하면 다음 그림과 같습니다.
 

유저 공간에서 sigaction() 함수를 호출하면 커널 공간에서 sys_rt_sigaction() 함수를 호출합니다. 

유저 공간에서 sigaction() 함수를 호출하면 커널 공간에서 sys_rt_sigaction() 함수가 어떻게 실행하는지 바로 이해하기 어렵습니다. 이 과정을 조금 더 자세히 정리하면 다음과 같습니다.
[유저 공간]
1. r7에 sys_rt_sigaction() 함수에 해당하는 시스템 콜 번호인 174를 지정
2. “svc 0” 명령어로 시스템 콜 처리를 위해 소프트웨어 인터럽트를 발생해서 커널 공간 스위칭

[커널 공간]
3. vector_swi 레이블 실행
4. 유저 공간에서 저장한 시스템 콜 번호인 174를 r7 레지스터에서 읽음
5. sys_call_table에 접근해서 시스템 콜 핸들러인 sys_rq_sigaction 시스템 콜 핸들러로 분기

 시스템 콜을 실행하고 커널 공간으로 스위칭한 다음 시스템 콜 벡터인 vector_swi 레이블을 실행합니다. vector_swi 레이블에서 시스템 콜 테이블인 sys_call_table에 접근해서 시스템 콜 핸들러인 sys_rq_sigaction 시스템 콜 핸들러로 분기합니다.

Trace32로 sys_rq_sigaction 함수가 sys_call_table 이란 시스템 콜 테이블 어느 공간에 있는지 확인하면 다음과 같습니다.
data.view%symbol.long sys_call_table
________address|_data________|value_____________|symbol
0      NSD:80107FC4| 0x8012D4E0      \\vmlinux\kernel/signal\sys_restart_syscall
1      NSD:80107FC8| 0x80121E08      \\vmlinux\exit\sys_exit
2      NSD:80107FCC| 0x8011C6D0      \\vmlinux\fork\sys_fork
3      NSD:80107FD0| 0x802844FC      \\vmlinux\read_write\sys_read
...
173   NSD:80108278| 0x80108638      \\vmlinux\Global\sys_rt_sigreturn_wrapper
174   NSD:8010827C| 0x8012EB24      \\vmlinux\kernel/signal\sys_rt_sigaction
175   NSD:80108280| 0x8012D89C      \\vmlinux\kernel/signal\sys_rt_sigprocmask
176   NSD:80108284| 0x8012D9D4      \\vmlinux\kernel/signal\sys_rt_sigpending
177   NSD:80108288| 0x8012DDD0      \\vmlinux\kernel/signal\sys_rt_sigtimedwait
178   NSD:8010828C| 0x8012E3A8      \\vmlinux\kernel/signal\sys_rt_sigqueueinfo
179   NSD:80108290| 0x8012EDF8      \\vmlinux\kernel/signal\sys_rt_sigsuspend

가장 왼쪽 열에 보이는 174번호가 시스템 콜 번호이고 바로 오른쪽에 보이는 8010827C 주소가 
sys_rt_sigaction() 함수 주소입니다.


#Reference 시스템 콜


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


[라즈베리파이] 시그널 - 시그널 관련 시스템 콜 함수 소개 [라즈베리파이] 시그널


시그널 관련 시스템 호출 함수는 다음과 같습니다.
함수 이름 동작 시스템 콜 핸들러
kill 쓰레드 그룹에 시그널을 보냄 sys_kill
tkill 프로세스에 시그널을 보냄 sys_tkill
tgkill 특정 쓰레드 그룹에 있는 프로세스에게 시그널을 보냄 sys_tgkill
sigaction 시그널 속성 설정 sys_rt_sigaction
sigpending 펜딩된 시그널이 있는지 점검 sys_rt_sigpending
sigprocmask 블록되어 있는 시그널 집합을 수정 sys_rt_sigprocmask
sigsuspend 시그널을 기다림 sys_rt_suspend
pause 시그널을 수신할 때 까지 기다림 sys_pause

가장 왼쪽 행에 보이는 kill, tgkill과 같은 함수 이름은 유저 공간에서 호출하는 리눅스 저수준 표준 함수입니다. 이 함수를 호출하면 시스템 콜을 실행해서 커널 공간에서 sys_kill(), sys_tgkill()과 같은 시스템 콜 핸들러 함수를 실행합니다.

리눅스 표준 함수를 호출하면 어떤 흐름으로 시스템 콜 핸들러를 실행하는지 살펴봅니다. 또한 시스템 콜 핸들러 후속 함수 처리로 리눅스 커널이 시그널을 어떻게 처리하는지 상세히 분석합니다.

#Reference 시스템 콜


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


[라즈베리파이] 시그널 - 커널 공간에서 시그널 설정은 어떻게 할까? [라즈베리파이] 시그널

이전 시간에서 유저 프로세스에서 시그널을 설정하는 코드를 살펴봤습니다. 이번 시간에는 커널에서는 시그널을 설정하면 어떤 동작을 하는지 살펴보겠습니다.
 

유저 프로세스가 시그널 설정을 위해 리눅스 표준 함수를 쓰면 시스템 콜을 발생해서 해당 시스템 콜 핸들러를 실행합니다. sigaddset()와 같이 시그널 자료구조만 변경하는 함수는 시스템 콜을 발생하지 않고 커널에 시그널 설정을 요청하는 sigaction() 함수나 시그널을 기다리는 pause() 와 같은 함수를 호출할 때 시스템 콜을 발생합니다.


[라즈베리파이] 시그널 - 유저 공간 시그널 설정은 어떻게 할까? [라즈베리파이] 시그널

이번 시간에 절에서는 시그널을 설정하면 유저 공간에서 커널 공간까지 어떤 흐름으로 처리하는지 소스 코드를 보면서 살펴 보겠습니다. 유저 레벨 프로세스에서 시그널 설정을 어떻게 하는지 소스 코드와 함께 살펴봅시다. 유저 공간에서 시그널을 설정하는 간단한 코드입니다.
1 #include <unistd.h>
2 #include <signal.h>
3 #include <stdio.h>
4 #include <stdlib.h>
5
6 void sig_handler(int signum) {
7 switch(signum) {
8    case SIGINT:
9        printf("sig num [%d] \n", signum);
10 break;
11
12    case SIGALRM:
13 printf("sig num [%d] \n", signum);
14 break;
15
16    case SIGUSR1:
17 printf("sig num [%d] \n", signum);
18 break;
19
20   default:
21 printf(" default sig num [%d] \n", signum);
22 }
23 }
24 
25 int main() 
26 {
27 struct sigaction act;
28 sigset_t set;
29
30 sigemptyset(&(act.sa_mask));
31
32 sigaddset(&(act.sa_mask), SIGALRM);
33 sigaddset(&(act.sa_mask), SIGINT);
34 sigaddset(&(act.sa_mask), SIGUSR1);
35
36 act.sa_handler = sig_handler;
37
38 sigaction(SIGALRM, &act, NULL);
39 sigaction(SIGINT, &act, NULL);
40 sigaction(SIGUSR1, &act, NULL);
41
42    for (;;)
43  pause();
44 }

위 코드는 SIGALRM, SIGINT, SIGUSR1 시그널을 sig_handler() 함수를 시그널 핸들러로 등록합니다. 3가지 시그널이 전달되면 sig_handler() 함수를 호출하는 동작입니다. 

30번째 줄 코드부터 봅시다.
30 sigemptyset(&(act.sa_mask));
31
32 sigaddset(&(act.sa_mask), SIGALRM);
33 sigaddset(&(act.sa_mask), SIGINT);
34 sigaddset(&(act.sa_mask), SIGUSR1);

30~34번째 줄 코드는 sigemptyset() 함수를 써서 시그널 집합을 초기화한 후 sigaddset() 함수로 SIGALRM, SIGINT, SIGUSR1 시그널을 추가합니다.

다음 36~40번째 줄 코드를 분석하겠습니다.
36 act.sa_handler = sig_handler;
37
38 sigaction(SIGALRM, &act, NULL);
39 sigaction(SIGINT, &act, NULL);
40 sigaction(SIGUSR1, &act, NULL);

act.sa_handler에 sig_handler() 이란 시그널 핸들러 함수를 등록합니다. sigaction() 함수를 호출해서 프로세스가 SIGALRM, SIGINT, SIGUSR1 시그널 중 하나을 받으면 시그널 핸들러로 sig_handler() 함수를 실행하도록 지정합니다.

다음 42~43번째 줄 코드를 보겠습니다.
42    for (;;)
43  pause();

pause() 함수는 시그널을 받을 때까지 기다리는 역할을 수행합니다.

다음은 SIGALRM, SIGINT, SIGUSR1 시그널이 발생하면 리눅스 커널에서 호출하는 시그널 핸들러 함수인 sig_handler()를 분석하겠습니다.
6 void sig_handler(int signum) {
7 switch(signum) {
8    case SIGINT:
9        printf("sig num [%d] \n", signum);
10 break;
11
12    case SIGALRM:
13 printf("sig num [%d] \n", signum);
14 break;
15
16    case SIGUSR1:
17 printf("sig num [%d] \n", signum);
18 break;
19
20   default:
21 printf(" default sig num [%d] \n", signum);
22 }
23 }


sig_handler() 함수와 같은 시그널 핸들러는 직접적으로 누가 언제 실행할까요? 주인공은 리눅스 커널입니다. 시그널을 받은 프로세스가 시그널 핸들러를 실행하도록 리눅스 커널은 지원합니다. 

sig_handler() 함수는 SIGINT, SIGALRM, SIGUSR1 시그널이 전달되면 실행하는 시그널 핸들러 함수입니다. 시그널 번호를 signum 인자로 받아서 출력하는 역할을 수행합니다.

시그널은 어떻게 생성할 수 있을까요?
Ctl+C 키를 누르거나 alarm(), kill()과 같은 리눅스 표준 함수를 호출하면 커널은 SIGINT와 SIGALRM 시그널을 생성한 다음 시그널 핸들러로 등록한 sig_handler() 함수를 실행합니다.


#Reference 시스템 콜


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

.

[라즈베리파이] 시그널 - 커널에서 시그널은 어떻게 처리할까? [라즈베리파이] 시그널

리눅스 커널 입장에서 시그널은 프로세스 간 통신을 위한 간단한 인터페이스입니다. 커널은 상황에 따라 시그널을 생성하고 전달해주는 역할을 수행합니다.

리눅스 커널에서 시그널에 대한 처리는 2단계로 나눌 수 있습니다.

1> 시그널 생성
유저 공간에서 시그널을 설정하면 커널은 해당 프로세스의 태스크 디스크립터에 시그널 정보를 써줍니다. 시그널을 받을 프로세스 스택 최상단 주소에 있는 struct thread_info flags 멤버에 _TIF_SIGPENDING 매크로를 써 줍니다. 시그널을 받을 프로세스에게 누군가 시그널을 생성했고 해당 시그널이 전달될 것이라고 알려주는 것입니다. 이후 시그널을 받을 프로세스를 깨웁니다.

2> 시그널 처리
시그널을 받을 프로세스가 시스템 콜이나 인터럽트 처리를 마무리한 이 후 시그널을 처리합니다.

커널은 시그널 종류에 따라 유저 프로세스가 정해진 동작을 수행하도록 다음 동작을 합니다.
 - 시그널 핸들러를 설정 안했을 경우
   : SIGINT, SIGKILL 시그널인 경우 프로세스를 종료시킵니다.
 - 시그널 핸들러를 설정했을 경우
   :  시그널 핸들러 주소를 ARM 프로그램 카운터 정보에 써줘서 시그널 핸들러를 실행시킵니다.

커널은 시그널 핸들러를 실행시켜 줄 뿐 시그널 종류에 따라 세부적인 처리를 할 수가 없습니다. 프로세스에게 전달하는 정보는 시그널 번호가 전부이며 표준 시그널에는 인자나 메시지 또는 그외 정보를 전달할 수 없습니다.

커널은 시그널을 대부분 프로세스를 종료할 때 프로세스나 스레드 그룹에 전달하는 메시지 형태로 사용합니다.

시그널을 발생했으나 아직 전달되지 않은 시그널을 펜딩 중인 시그널(pending signal)이라고 합니다. 특정 타입의 펜딩 시그널은 프로세스당 항상 하나만 존재합니다. 같은 시그널을 동일한 프로세스에게 전달하면 시그널 큐에서 대기하는 것이 아니라 그냥 폐기됩니다. 

#Reference 시스템 콜


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

.

[라즈베리파이] 시그널 - 시그널을 받으면 프로세스 어떻게 반응할까? [라즈베리파이] 시그널

시그널을 전달받은 프로세스는 어떤 동작을 할까요? 크게 2가지 동작을 수행합니다.
1> 시그널을 무시한다.
말 그대로 아무런 동작을 하지 않습니다.  하지만 SIGKILL, SIGSTOP 과 같은 시그널은 프로세스가 무시할 수 없습니다. 특정 상황에서 지정한 프로세스를 반드시 종료해야 하는 상황이 있기 때문입니다.

2> 시그널에 명시된 동작을 수행합니다.
시그널 핸들러로 등록된 시그널 핸들러 함수를 실행하고 시그널 별로 명시된 동작을 수행합니다.
대부분 시그널을 받으면 프로세스가 취하는 액션은 프로세스 종료입니다.

프로세스는 어떤 이벤트에 대해서 특정 루틴을 수행해야 하기 때문에, 시그널을 한 번에 하나의 용도로만 사용합니다.

이번 시간까지는 유저 프로세스 입장에서 시그널 처리 동작에 대해 살펴봤습니다. 다음에 커널이 시그널 처리를 위해 어떤 동작을 하는지 알아봅시다.

#Reference 시스템 콜


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

.

[라즈베리파이] 시그널 - 시그널 번호와 동작 알아보기 [라즈베리파이] 시그널

책을 읽다가 발생하는 여러 비동기적인 이벤트(인터폰, 전화)가 있듯이, 유저 레벨 프로세스 동작 중에 발생할 수 있는 시그널도 여러 가지가 있습니다. 유닉스나 리눅스 커널 버전이 달라도 시그널 종류는 비슷합니다. 시그널은 POSIX 규약으로 정의된 표준이며 이제 맞게 리눅스 시스템 개발자가 구현하기 때문입니다. 다음은 라즈베리파이 리눅스 커널 4.14.70 버전에서 지원하는 시그널 번호를 확인한 결과입니다.
pi@raspberrypi:~ $ kill -l
 1) SIGHUP  2) SIGINT  3) SIGQUIT  4) SIGILL  5) SIGTRAP
 6) SIGABRT  7) SIGBUS  8) SIGFPE  9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

시그널은 POSIX(Portable Operating System Interface) 규약에 정한 표준입니다. 어플리케이션 이식성을 높이기 위한 시도로 어플리케이션이 다양한 유닉스 계열 운영체제에서 구동할 수 있게 정한 것입니다. 리눅스 시스템 개발자들은 POSIX 규약에 따라 시스템 코드를 설계하고 코드를 구현합니다. 

1~34번까지는 유닉스 계열 운영체제(리눅스 포함)에서 같은 시그널 종류와 번호를 확인할 수 있습니다. 이를 정규 시그널이라고도 말합니다. 대신 35~63 시그널은 리얼 타임 시그널입니다.

정규 시그널과 리얼 타임 시그널의 차이점은 무엇일까요? 가장 큰 차이는 시그널 큐 처리 방식입니다. 정규 시그널은 같은 종류의 시그널을 연달아 보내면 프로세스는 한 가지 시그널만 받아 처리하지만 리얼 타임 시그널은 모듀 큐에 쌓아서 처리를 합니다.

각각 시그널은 int 형 정수로 선언되어 있는데 라즈베리파이에서 다음 해더 파일에서 시그널 종류별 int형 정수 번호를 확인할 수 있습니다.
root@raspberrypi:/usr/include # cat arm-linux-gnueabihf/asm/signal.h
#define SIGHUP          1
#define SIGINT           2
#define SIGQUIT         3
#define SIGILL            4
#define SIGTRAP         5
#define SIGABRT         6
#define SIGIOT           6
#define SIGBUS          7
#define SIGFPE           8
#define SIGKILL          9
#define SIGUSR1        10
#define SIGSEGV        11

유저 어플리케이션에서 시그널을 처리하는 함수를 작성하면 위와 같이 각 시그널 종류 별로 정의된 정수 값으로 시그널을 처리합니다.

유저 공간에서 정의된 시그널 번호는 리눅스 커널에서도 같은 번호로 관리합니다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/uapi/asm/signal.h]
#define SIGHUP  1
#define SIGINT  2
#define SIGQUIT  3
#define SIGILL  4
#define SIGTRAP  5
#define SIGABRT  6
#define SIGIOT  6
#define SIGBUS  7
#define SIGFPE  8
#define SIGKILL  9
#define SIGUSR1 10
#define SIGSEGV 11

32개 시그널 중 자주 활용하는 시그널을 정리하면 다음과 같습니다.
시그널 동작
SIGHUP 프로세스 제어 터미널이 종료될 때 세션 리더에게 전달, 터미널을 읽어버렸을때 발생
SIGINT 터미널 인터럽트 신호로(Ctl+C)키나 DELETE 키를 입력했을때 발생
SIGQUIT 사용자가 종료 문자(Ctl-\) 실행
SIGILL 유저가 유효하지 않은 명령어 실행 시도
SIGTRAP 트레이스 혹은 브레이크 포인트 실행
SIGABRT 프로세스가 비정상적인 종료 시중단 신호로 abort()에서 보냄
SIGIOT 비동기적인 I/O 이벤트 처리 시 보냄
SIGBUS 유효하지 않은 메모리 공간에 접근하거나 하드웨어 장애를 일으킬 때 커널이 생성
SIGFPE 부동 소수점을 연산 도중 오버플로우나 언더플로우가 발생하면 익셉션으로 발생하는 시그널
SIGKILL kill() 함수를 호출하면 프로세스를 종료시킴
SIGUSR1
SIGUSR2 유저 공간에서 처리하기 위해 정의하며 커널은 이 시그널을 쓰지 않음
SIGSEGV 유효하지 않은 메모리 접근을 시도할 때 커널이 해당 프로세스에 전달함
읽기나 쓰기 권한이 없는 메모리 공간에 접근하거나 실행할 수 없는 코드를 실행할 때 발생함
SIGUSR2 유저 공간에서 처리하기 위해 정의하며 커널은 이 시그널을 쓰지 않음
SIGPIPE 닫힌 파이프에 열고 쓸 때 실행
SIGALRM alarm() 함수가 자신을 실행한 프로세스에게 전달
SIGCHLD 프로세스가 종료할 때 커널은 해당 프로세스의 부모 프로세스에게 전달

유저 레벨 프로세스가 리눅스 저수준 표준 함수를 호출해서 시그널을 발생할 수 있습니다. 또한 어떤 프로세스가 종료할 때 부모 프로세스에게 자식 프로세스가 종료한다는 정보를 SIGCHLD 시그널로 알립니다.

만약 특정한 프로세스에게 시그널을 전달하고 싶을 때 어떻게 하면 될까요? 리눅스 터미널에서 kill 명령어를 쓰면 됩니다.

라즈베리파이에서 X-Terminal을 2개 열고 다음 명령어를 입력합시다.
1 root@raspberrypi:/usr/include # ps -ely | grep bash
2 S 1000 500 432 0 80 0 4096 1645 poll_s tty1 00:00:00 bash
3 S 1000 1150 1146 0 80 0 4192 1628 wait pts / 0 00:00:00 bash
4 S 0 1355 1350 0 80 0 3376 1433 wait pts / 0 00:00:00 bash
5 S 1000 1386 1146 0 80 0 3964 1628 poll_s pts / 1 00:00:00 bash
6 root@raspberrypi:# kill -SIGKILL 1386

위에서 5번 출력 결과를 보면 가장 마지막에 실행된 bash 프로세스의 PID가 1386이니 위와 같이 kill 명령어에 -SIGKILL 옵션을 줘서 실행하니 프로세스는 강제 종료합니다.


라즈베리파이 리눅스 시스템에서 kill 명령어 메뉴얼을 확인하면 다음과 같습니다.
root@raspberrypi:/home/pi# info kill
Up: Process control

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.

kill은 프로세스에게 시그널을 전달하는 명령어인데 대부분 프로세스를 종료하거나
시그널 종류에 따라 정해진 동작을 처리한다는 사실을 알 수 있습니다.


다음 시간에 유저 프로세스가 시그널을 받으면 어떤 동작을 수행하는지 점검합시다.

#Reference 시스템 콜


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



[라즈베리파이] 시스템 콜 - 시스템 콜 핸들러 실행을 마무리하면 어떤 동작을 할까? [라즈베리파이] 시스템 콜

시스템 콜을 수행하면 시스템 콜 핸들러를 통해 가상 파일시스템이나 커널 프로세스 함수를 실행합니다. 이후 시스템 콜 종류에 따라 시스템 콜 핸들러 하부 루틴을 수행하고 유저 공간 복귀합니다.

시스템 콜 실행 후 복귀할 때 ret_fast_syscall 레이블에서 리눅스 커널 시그널 및 스케줄링 관점에서 중요한 동작을 실행합니다.

시스템 콜 테이블을 통해 시스템 콜 핸들러를 분기하기 직전 다음 1번째 줄 코드와 같이 복귀 레지스터(r14, lr)에 ret_fast_syscall 레이블을 지정했습니다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/entry-common.S]
1 badr lr, ret_fast_syscall @ return address
2 ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine

시스템 콜 핸들러에서 시스템 콜 종류에 따라 시스템 콜 핸들러에서 실행을 마친 후 ret_fast_syscall 레이블을 실행합니다.

다음 ftrace 로그를 예로 들어 보겠습니다.
1 chromium-browse-1200 [001] 952.125229: _raw_spin_lock+0x10/0x54 <-__schedule+0xc0/0xa50
2 chromium-browse-1200 [001] 952.125238: <stack trace>
3 => futex_wait_queue_me+0x10c/0x1a8
4 => futex_wait+0xf8/0x234
5 => do_futex+0x10c/0xc58
6 => SyS_futex+0xec/0x194
7 => ret_fast_syscall+0x0/0x28

이 프로세스는 __schedule() 함수 호출로 준비 상태로 대기하고 있다가 스케줄러에 의해 다시 실행할 수 있습니다. 이 때 futex_wait_queue_me() 함수 혹은 서브 루틴 실행 후 1번에서 7번 ret_fast_syscall 레이블 방향으로 다시 복귀합니다.

시스템 콜을 ftrace 로 분석하면 대부분 시스템 콜은 아주 짧은 시간 내(0.005ms) 실행을 마무리합니다. 
Xorg-485 [001] 2720.496312: sys_enter: NR 297 (20, 7ee069dc, 0, 0, 0, 7ee069dc)
Xorg-485 [001] 2720.496317: sys_exit: NR 297 = 4 // __NR_recvmsg
Xorg-485 [001] 2720.496321: sys_enter: NR 146 (20, 7ee06b7c, 1, 8e500700, 0, 270d7b0)
Xorg-485 [001] 2720.496327: sys_exit: NR 146 = 32  // __NR_writev
Xorg-485 [001] 2720.496329: sys_enter: NR 297 (20, 7ee069dc, 0, 0, 0, 7ee069dc)
Xorg-485 [001] 2720.496331: sys_exit: NR 297 = -11  // __NR_recvmsg
Xorg-485 [001] 2720.496332: sys_enter: NR 104 (0, 7ee06c0c, 0, 0, 76fc6ce8, 1fb000)
Xorg-485 [001] 2720.496335: sys_exit: NR 104 = 0  // __NR_setitimer
Xorg-485 [001] 2720.496337: sys_enter: NR 252 (3, 7ee05c00, 100, e434, 0, 1fb000)
Xorg-485 [001] 2720.496375: sys_exit: NR 252 = 1 // __NR_recvmsg
주석문으로 표시한 부분은 시스템 콜 번호에 대응하는 시스템 콜 핸들러를 의미합니다.


다음 그림은 ret_fast_syscall 레이블에서 시작해서 no_work_pending 레이블까지 동작 흐름도입니다. 시스템 콜 실행을 종료하고 유저 공간으로 복귀하는 과정입니다.

각 레이블 별로 어떤 동작을 하는지 살펴봅시다.

ret_fast_syscall
프로세스 최상단 주소에 있는 struct thread_info flag 멤버가 _TIF_WORK_MASK 인지 점검합니다.

struct thread_info flag 멤버가 _TIF_WORK_MASK가 아닐 경우 다음 조건으로 처리합니다.
1> TIF_SYSCALL_WORK 이면 ?
   __sys_trace_return_nosave 레이블 실행으로 ftrace sys_exit 이벤트 로그 실행 후 유저 공간 복귀

2> TIF_SYSCALL_WORK 아니면 ?
 no_work_pending 레이블을 실행해서 유저 공간으로 복귀

slow_work_pending
 do_work_pending() 함수 실행해서 시그널 및 스케줄링 처리를 한 후  no_work_pending 레이블을 호출합니다. 리눅스 커널 관점에서 눈여겨봐야 할 레이블 코드입니다.
  
no_work_pending
 restore_user_regs 매크로 실행으로 유저 공간 복귀하는 레이블입니다.

다음 시간에는 각 레이블이 어떤 동작을 하는지 어셈블리 코드 분석으로 알아보겠습니다.

#Reference 시스템 콜


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





[라즈베리파이] 시스템 콜 - 매개 변수 점검(시스템 콜 핸들러) [라즈베리파이] 시스템 콜

리눅스 커널 시스템 콜 핸들러 인터페이스 함수를 열어 보면 수 많은 예외 처리 코드를 볼 수 있습니다. 유저 모드에서 시스템 콜을 호출할 때 잘못된 인자(스트링 갯수, 메모리 주소)를 전달할 수 있기 때문입니다.
 
먼저 write() 이라는 리눅스 시스템 저수준 함수를 살펴보겠습니다.

write() 함수를 호출할 때는 다음 6번 째 줄 코드와 같이 세 가지 인자를 전달해야 합니다.
[https://android.googlesource.com/platform/system/core/+/master/init/uevent_listener.cpp]
1 ListenerAction UeventListener::RegenerateUeventsForDir(DIR* d,
2                                                       const ListenerCallback& callback) const {
3    int dfd = dirfd(d);
4    int fd = openat(dfd, "uevent", O_WRONLY);
5    if (fd >= 0) {
6        write(fd, "add\n", 4);
7        close(fd);
...

위 코드는 안드로이드 디바이스에서 uevent를 처리하는 동작입니다.

첫 번째 인자인 fd는 파일 디스크립터, 두 번째 인자는 쓰려고 하는 스트링, 세 번째는 스트링 사이즈입니다.

다음 경로에 있는 unistd.h 해더 파일을 열어서 write() 함수 선언부를 보면 3가지 인자가 어떤 유형인지 확인할 수 있습니다.
[/usr/include/unistd.h]
extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur;
__fd: 파일 디스크립터
__buf: 파일 쓰기 버퍼
__n: 쓰려는 파일 사이즈

유저 공간에서 write() 함수를 호출하면 시스템 콜이 발생한 다음에 시스템 콜 핸들러인 sys_write() 함수를 호출합니다.

이번에 sys_write() 함수 선언부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/syscalls.h]
 long sys_write(unsigned int fd, const char *buf,
      size_t count);

그럼 두 함수 선언부를 같이 보겠습니다.
유저 공간: extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur;
커널 공간: long sys_write(unsigned int fd, const char *buf, size_t count);

유저 공간에서 write() 함수를 통해 전달한 3개 인자가 커널 공간에서 실행하는 sys_write() 함수로 그대로 호출합니다.
다른 파일 입출력 함수인 read() 함수도 마찬가지입니다.
유저 공간: extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur;
커널 공간: long sys_read(unsigned int fd, char *buf, size_t count);

대부분 리눅스 저수준 표준 함수를 통해 전달된 인자들은 시스템 콜 핸들러 함수 인자로 그대로 전달됩니다. 

그런데 유저 어플리케이션은 누가 어떻게 작성할지 예상할 수 없습니다. 누구나 리눅스 표준 함수 매뉴얼을 보고 코드를 작성한 후 리눅스 시스템에서 컴파일 후 실행할 수 있습닏. 

만약 유저 어플리케이션에서 다음과 같이 write() 함수로 인자를 잘못 전달했다고 가정합시다.
정상 코드: write(fd, “add\n”, 4);
오류 코드: write(fd, “add\n”, 4000);

쓰려고 하는 문자열 크기가 4이어야 하는데 실수로 4000을 입력한 것입니다. 위와 같이 오류 코드를 작성해서 실행하면 write() 함수에 대응하는 시스템 콜 핸들러인 sys_write() 함수에서 이 인자를 그래도 받아서 처리를 하면 리눅스 커널은 오동작을 할 가능성이 높습니다.

그래서 시스템 콜 핸들러는 함수로 전달된 매개 변수 인자를 먼저 확인합니다.
유저 공간에서 리눅스 저수준 표준 함수를 호출할 때 어떤 인자를 전달할지 모르기 때문입니다.

먼저 sys_write() 시스템 콜 핸들러 함수를 보면서 매개 인자를 어떻게 처리하는지 살펴 봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/read_write.c]
1 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
2 size_t, count)
3 {
4 struct fd f = fdget_pos(fd);
5 ssize_t ret = -EBADF;
6
7 if (f.file) {
8 loff_t pos = file_pos_read(f.file);
9 ret = vfs_write(f.file, buf, count, &pos);

4번째 줄 코드에서 fdget_pos() 함수를 호출해서 sys_write() 함수 첫 번째 인자인 fd 번호를 통해 커널 내부 파일 디스크립터를 읽은 다음에 가상 파일 시스템 인퍼페이스 계층에 접근해 vfs_write() 함수를 호출합니다. 

다음 vfs_write() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/read_write.c]
1 ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
2 {
3 ssize_t ret;
4
5 if (!(file->f_mode & FMODE_WRITE))
6 return -EBADF;
7 if (!(file->f_mode & FMODE_CAN_WRITE))
8 return -EINVAL;
9 if (unlikely(!access_ok(VERIFY_READ, buf, count)))
10 return -EFAULT;

함수 첫 부분부터 예외 처리 코드를 볼 수 있습니다. 유저 어플리케이션에서 open() 함수를 호출해서 반환된 fd(파일 디스크립터) 정수값이 아닌 유효하지 않은 쓰레기 fd 정수값으로 파일을 쓰려고 시도할 수 있습니다. 이 때 실행하는 예외처리 코드입니다.

5~8번째 코드를 보겠습니다.
파일 디스크립터 f_mode 멤버가 FMODE_WRITE나 FMODE_CAN_WRITE가 아니면 오류 타입 매크로 숫자를 반환하고 함수 실행을 멈춥니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/uapi/asm-generic/errno-base.h]
#define EBADF  9 /* Bad file number */
#define EFAULT 14 /* Bad address */
#define EINVAL 22 /* Invalid argument */

9번째 줄 코드는 sys_write() 함수로 전달된 버퍼에 오류가 있는지 점검합니다. 
9 if (unlikely(!access_ok(VERIFY_READ, buf, count)))
10 return -EFAULT;

유저 공간에서 잘못된 메모리 포인터 주소로 버퍼를 할당했을 수도 있기 때문입니다.


다음과 같이 ftrace 이벤트를 설정하고 ftrace 로그를 받으면 sys_write() 호출 시 어떤 인자가 전달되는지 알 수 있습니다.
"echo 1 > /sys/kernel/debug/tracing/events/raw_syscalls/enable"

1 RPi_signal-1218  [003] ....  3557.394288: sys_enter: NR 4 (1, 17ee008, 12, 0, 12, 17ee008)
2 RPi_signal-1218  [003] .n..  3557.394348: sys_exit: NR 4 = 18

1번째 줄 로그는 fd: 1, buf: 17ee008, size: 0x12(18 십진수) 로 sys_write() 함수 실행 시작을 알 수 있습니다. “NR 4” 로그에서 4는 시스템 콜 번호를 의미합니다.

다음 2번째 줄 로그를 보면 sys_write() 함수가 어떤 조건으로 실행을 마무리했는지 알 수 있습니다. 반환값은 문자열 스트링을 쓴 크기인데 18을 반환하고 있습니다.

만약 시스템 콜 핸들러에서 예외 처리 루틴이 실행되면 ftrace 로그에서 어떤 패턴을 확인할 수 있을까요?
1 RPi_signal-1105  [000] ....  3557.402524: sys_enter: NR 4 (1, 1c6e008, 12, 0, 12, 1c6e008)
2 RPi_signal-1105  [000] ....  3557.402528: sys_exit: NR 4 = -5  // #define SIGTRAP

2번째 로그 -5와 같이 마이너스 정수를 반환합니다.

이번에는 다른 시스템 콜 핸들러 코드를 봅시다.
유저 공간에서 reboot() 이란 함수를 호출하면 sys_reboot() 이란 시스템 콜 핸들러 함수가 실행합니다.

reboot() 함수 선언부는 다음과 같으며 cmd 인자로 시스템 리부팅 타입을 전달합니다.
int reboot(int cmd);

sys_reboot() 함수 앞 부분 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/reboot.c]
1 SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
2 void __user *, arg)
3 {
4 struct pid_namespace *pid_ns = task_active_pid_ns(current);
5 char buffer[256];
6 int ret = 0;
7
8 /* We only trust the superuser with rebooting the system. */
9 if (!ns_capable(pid_ns->user_ns, CAP_SYS_BOOT))
10 return -EPERM;
11
12 /* For safety, we require "magic" arguments. */
13 if (magic1 != LINUX_REBOOT_MAGIC1 ||
14 (magic2 != LINUX_REBOOT_MAGIC2 &&
15 magic2 != LINUX_REBOOT_MAGIC2A &&
16 magic2 != LINUX_REBOOT_MAGIC2B &&
17 magic2 != LINUX_REBOOT_MAGIC2C))
18 return -EINVAL;

9번째 줄 코드는 reboot() 함수를 호출한 프로세스가 슈퍼 유저 권한이 있는지 점검합니다. 리눅스 시스템에서는 슈퍼 유저만이 시스템을 종료할 수 있는 권한을 갖고 있기 때문입니다. 만약 이 조건을 만족 못할 경우 EPERM(1) 음수 값을 반환하고 함수 실행을 중단합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/uapi/asm-generic/errno-base.h]
#define EPERM  1 /* Operation not permitted */

EPERM 매크로는 권한 없이 어떤 코드를 실행했다는 정보입니다.

13~18번째 줄 코드는 magic2 인자로 전달된 매직 값을 점검합니다.
reboot() 함수는 리눅스 시스템 전원을 내리거나 리부팅을 하는 중요한 역할을 수행하므로 위와 같은 예외 처리 조건을 추가한 것입니다.

시스템 콜 핸들러 함수나 시스템 콜 핸들러에서 호출하는 함수에서 위와 같이 매개 변수에 오류가 있는지 점검하는 코드를 많이 볼 수 있습니다. 이는 유저 공간에서 저수준 함수로 잘못된 인자가 시스템 콜 핸들러 함수로 전달됐을 경우 오동작을 막기 위한 예외 처리 코드입니다.
리눅스 해커들은 여러 가지 인자 조합으로 시스템 콜 핸들러 예외 처리 루틴의 헛점을 노립니다.
이런 코드를 Security Hole이라고 하며 이를 방지하기 위한 Security 패치들이 리눅스 커널 코드에 반영되고 있습니다.

리눅스 해커들은 보통 상용 리눅스 시스템의 루트 권한 획득을 노립니다. 루트 권한으로 시스템을 마음대로 제어할 수 있기 때문입니다. 리눅스 해커들은 ARM, x86 아키텍처에서 시스템 콜을 어떤 흐름으로 처리하는지 어셈블리 코드를 외울 정도로 숙지하고 있습니다.

#Reference 시스템 콜


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

.

[라즈베리파이] 시그널 - 유저 프로세스 입장에서 시그널이란 [라즈베리파이] 시그널

시그널이란 무엇일까요?
커널 입장에서 시그널은 프로세스에게 보내는 단순한 형태의 메시지라고 할 수 있습니다.
시그널 정보와 PID를 프로세스에게 전달하는 것입니다.

유저 프로세스 입장에서 프로세스는 무엇일까요? 유저 프로세스 관점으로 시그널은 실행 흐름을 제어하는 비동기적인 중단입니다. 이렇게 유저 프로세스와 커널 입장에서 시그널은 약간 다른 관점에서 볼 수 있습니다.

이번에 먼저 유저 프로세스 관점으로 시그널을 살펴보겠습니다.
여러분이 너무 보고 싶은 책이 있다고 가정하겠습니다. 
3시간 동안 읽을 수 있는 책 분량이라 주말에 책을 읽기 시작했습니다. 3시간 동안 아무런 방해를 받지 않고 책을 읽을 수 있으면 좋겠지만 이런 상황은 이상적인 환경입니다. 집에서 인터폰 벨리 울리던가 전화가 온다던가 여러 종류 중단이 발생 수 있기 때문입니다.

이런 중단은 예상하지 않았던 상황에서 발생하며 소프트웨어에서는 비동기적인 이벤트라고도 말합니다.

인터폰이 울리거나 회사나 친구한테 전화가 오면 우리는 보통 적절한 대응을 합니다. 보통 전화를 받거나 인터폰을 받고 대화를 합니다. 만약 여러분이 임베디드 리눅스 개발자인데 주말에 회사에서 전화가 왔다면 어떻게 할까요? 대부분 전화를 받을 것입니다. 시급한 문제가 생겼을 때 회사에서 개발자에게 전화를 하기 때문입니다. 하지만 가끔 중요하지 않은 다른 전화(광고, 부동산 투자)가 오면 전화를 안 받을 수도 있습니다.

프로세스도 마찬가지입니다. 유저 레벨 프로세스 기준으로 우리가 책을 읽는 것과 마찬가지로 정해진 시나리오에 따라 어떤 작업을 수행한다고 가정합시다. 책을 끝가지 방해 받지 않고 읽었으면 좋겠으나 유저 레벨 프로세스도 마찬가지로 예상치 못한(비동기적인) 중단으로 작업 흐름이 끊어 질 수 있습니다.

이렇게 인터폰이 울리거나 전화가 오는 것과 같이 유저 프로세스도 일을 하다가 비동기적인 중단을 겪을 수 있습니다. 이를 리눅스에서는 시그널이라고 하며 유저 프로세스는 시그널에 대해 이미 정해진 처리를 해줘야 합니다.

이런 유형의 다른 대표적인 중단은 인터럽트를 들 수 있습니다. 유저 프로세스 입장에서 시그널도 예상치 않았던 비동기적인 이벤트라고 볼 수 있습니다. 

대표적인 비동기적인 이벤트로 시그널이 발생하는 상황을 생각해봅시다.
1.리눅스 터미널에서 Ctl+C 키를 눌러서 프로세스를 종료
2.리눅스 터미널에서 다음 커맨드로 프로세스를 강제 종료
kill -9 [PID]
3.리눅스 커널에서 특정 조건에서 해당 프로세스를 종료

이렇게 언제 발생할지 모르는 비동기적인 중단(이벤트)에 대해 적절한 처리를 해줘야 합니다.

리눅스 커널에서도 자체적으로 시그널을 발생할 수 있습니다. 한 가지 예로 OOM(Out-of-memory) Killer를 들 수 있습니다. 잔여 메모리가 매우 부족할 때 OOM Killer 모듈은 프로세스를 강제 종료시켜서 메모리를 확보합니다. 종료할 프로세스에게 시그널을 전달합니다. 

안드로이드 시스템에서 OOM Killer가 실행하기 전 메모리 부족을 방지하기 위해 Lowmemory Killer란 모듈을 실행합니다. OOM Killer와 마찬가지로 프로세스를 종료시켜서 메모리를 확보합니다. 이 때도 종료할 프로세스에게 시그널을 전달합니다. 이 내용은 소절에서 자세히 다룹니다.

다음 시간에서는 시그널의 종류에 대해 알아봅시다.


#Reference 시스템 콜


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

.

[라즈베리파이] 시스템 콜 - 시스템 콜 핸들러는 어떤 동작을 할까? [라즈베리파이] 시스템 콜

유저 공간에서 시스템 콜을 발생하면 커널 공간으로 모드가 스위칭된 다음 vector_swi 이란 소프트웨어 인터럽트 벡터를 실행하는 부분까지 살펴봤습니다. vector_swi 레이블에서 시스템 콜 테이블에 접근해서 시스템 콜 핸들러를 호출합니다.

시스템 콜 핸들러에서는 어떤 동작을 수행할까요?
시스템 콜 핸들러는 시스템 콜 종류에 따른 처리를 합니다. 가상 파일 시스템 계층에 접근하거나 프로세스 관리 함수를 실행하는 것입니다. 하지만 시스템 콜 핸들러에서 공통으로 처리하는 패턴이 있습니다. 그것은 시스템 콜 핸들러 함수 인자로 전달된 인자를 점검하는 예외 처리 루틴입니다.

다음 시간에서는 시스템 콜 핸들러에서 매개 인자를 점검하는 코드를 살펴보겠습니다.

#Reference 시스템 콜


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

.

[라즈베리파이] 시스템 콜 - 커널 공간에서 시스템 콜 테이블 확인하기 [라즈베리파이] 시스템 콜

시스템 콜 테이블은 sys_call_table 심볼에 저장돼 있습니다. 쉽게 설명을 드리면 전역 변수라고 봐도 무방합니다.

T32 프로그램으로 시스템 콜 테이블을 확인하면 다음과 같습니다.
d.v %y.l sys_call_table
________address||value______|symbol
NSD:80107FC4| 0x8012C6F4  \\vmlinux\kernel/signal\sys_restart_syscall
NSD:80107FC8| 0x801212C0  \\vmlinux\exit\sys_exit
NSD:80107FCC| 0x8011C100  \\vmlinux\fork\sys_fork
NSD:80107FD0| 0x8026AB24  \\vmlinux\read_write\sys_read
NSD:80107FD4| 0x8026ABC4  \\vmlinux\read_write\sys_write
NSD:80107FD8| 0x80268508  \\vmlinux\open\sys_open
NSD:80107FDC| 0x80267108  \\vmlinux\open\sys_close
NSD:80107FE0| 0x8013CC38  \\vmlinux\sys_ni\compat_sys_epoll_pwait
NSD:80107FE4| 0x80268558  \\vmlinux\open\sys_creat

Trace32 프로그램으로 sys_call_table 심볼을 unsigned int 단위로 캐스팅을 하면 다음과 같은 출력 결과를 확인할 수 있습니다.
  [0] = 2148714228 = 0x8012C6F4,  // sys_restart_syscall
  [1] = 2148668096 = 0x801212C0, // sys_exit
  [2] = 2148647168 = 0x8011C100, // sys_fork
  [3] = 2150017828 = 0x8026AB24, // sys_read
  [4] = 2150017988 = 0x8026ABC4, // sys_write
  [5] = 2150008072 = 0x80268508, // sys_open
  [6] = 2150002952 = 0x80267108, // sys_close
  [7] = 2148781112 = 0x8013CC38, // compat_sys_epoll_pwait
  [8] = 2150008152 = 0x80268558, // sys_creat

위 디버깅 정보에서 1,2...8과 같은 배열 인덱스가 시스템 콜 번호이고 주석문으로 표시된 함수가 시스템 콜 핸들러 함수입니다.

다음과 같은 해더 파일에도 시스템 콜 번호를 지정해놨음을 확인할 수 있습니다. 
[/usr/include/arm-linux-gnueabihf/asm/unistd.h]
#define __NR_restart_syscall (__NR_SYSCALL_BASE+  0)
#define __NR_exit (__NR_SYSCALL_BASE+  1)
#define __NR_fork (__NR_SYSCALL_BASE+  2)
#define __NR_read (__NR_SYSCALL_BASE+  3)
#define __NR_write (__NR_SYSCALL_BASE+  4)
#define __NR_open (__NR_SYSCALL_BASE+  5)
#define __NR_close (__NR_SYSCALL_BASE+  6)
...
#define __NR_mlock2 (__NR_SYSCALL_BASE+390)
#define __NR_copy_file_range (__NR_SYSCALL_BASE+391)
#define __NR_preadv2 (__NR_SYSCALL_BASE+392)
#define __NR_pwritev2 (__NR_SYSCALL_BASE+393)
#define __NR_pkey_mprotect (__NR_SYSCALL_BASE+394)
#define __NR_pkey_alloc (__NR_SYSCALL_BASE+395)
#define __NR_pkey_free (__NR_SYSCALL_BASE+396)

시스템 콜 테이블 1~2 인덱스인 __NR_exit와 __NR_fork 매크로를 분석하겠습니다.
#define __NR_exit (__NR_SYSCALL_BASE+  1)
#define __NR_fork (__NR_SYSCALL_BASE+  2)
__NR_exit 시스템 콜 정보를 확인합시다.
__NR_SYSCALL_BASE 매크로는 시스템 콜 베이스 주소이며 1이 시스템 콜 번호입니다. 

마찬가지로 __NR_fork 매크로는 2번 시스템 콜을 표현합니다.

각 매크로는 다음 규칙으로 선언되어 있습니다.
매크로 리눅스 저수준 표준함수 시스템 콜 핸들러
__NR_exit exit sys_exit
__NR_fork fork sys_fork
__NR_read read sys_read
__NR_write write sys_write
__NR_function function sys_function

매크로 앞에 붙은 __NR 스트링을 제외하면 리눅스 저수준 표준 함수이름이고 __NR을 sys로 바꾸면 시스템 콜 핸들러가 되는 것입니다. 이 규칙으로 소스 코드를 확인하면 됩니다.

각 시스템 콜 핸들러의 선언부는 다음 해더 파일에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/syscalls.h]
asmlinkage long sys_fork(void);
asmlinkage long sys_exit(int error_code);
asmlinkage long sys_read(unsigned int fd, char __user *buf, size_t count);
asmlinkage long sys_write(unsigned int fd, const char __user *buf,
  size_t count);

참고로 함수 선언부에 asmlinkage가 보이면 어셈블리 코드에서 바로 호출(브랜치)할 수 있다는 의미입니다.

다음 시간에는 유저 공간에서 리눅스 저수준 함수인 open(), read() 그리고 write() 함수를 호출하면 커널 공간에선 어떤 실행 흐름으로 시스템 콜을 처리하는지 점검합니다.

#Reference 시스템 콜


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

.

    [Linux][Kernel] gcc - asmlinkage란 [라즈베리파이][커널]매크로분석

    커널 소스를 보다보면 asmlinkage로 선언된 함수들을 볼 수 있습니다.

    대표적으로 시스템 콜 핸들러를 들 수 있으며 다음 해더 파일에 정의되어 있습니다.
    [https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/syscalls.h]
    asmlinkage long sys_fork(void);
    asmlinkage long sys_exit(int error_code);
    asmlinkage long sys_read(unsigned int fd, char __user *buf, size_t count);
    asmlinkage long sys_write(unsigned int fd, const char __user *buf,
      size_t count);

    asmlinkage는 어셈블리 코드에서 직접 호출(링크)할 수 있다는 의미며 다음과 같이 커널 소스의 <include/linux/linkage.h>에 다음과 같이 정의되어 있습니다.
    [https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/linkage.h]
    #include <linux/compiler_types.h>
    #include <linux/stringify.h>
    #include <linux/export.h>
    #include <asm/linkage.h>

    #ifdef __cplusplus
    #define CPP_ASMLINKAGE extern "C"
    #else
    #define CPP_ASMLINKAGE
    #endif

    #ifndef asmlinkage
    #define asmlinkage CPP_ASMLINKAGE
    #endif

    어셈블리 코드에서 어떤 함수를 직접 호출할 수 있다는 것은 무엇을 의미할까요?
    일반적으로 C 함수는 어셈블리 코드에서 별 어려움없이 호출할 수 있지만 함수의 인자를 넘기거나 리턴값을 받는 부분 등의 호출 규약(ARM Call Convention)이 필요합니다.

    다음 코드에서 ARM 프로세스에서 sys_write() 함수에서 전달되는 인자를 확인합시다.
    1 NSR:8028459C|E1A0C00D  sys_write:cpy     r12,r13
    2 NSR:802845A0|E92DDBF0            push    {r4-r9,r11-r12,r14,pc}
    3 NSR:802845A4|E24CB004            sub     r11,r12,#0x4     ; r11,r12,#4
    4 NSR:802845A8|E24DD008            sub     r13,r13,#0x8     ; r13,r13,#8
    5 NSR:802845AC|E52DE004            str     r14,[r13,#-0x4]!
    6 NSR:802845B0|EBFA28DA            bl      0x8010E920       ; __gnu_mcount_nc
    7 NSR:802845B4|E1A09001            cpy     r9,r1            ; r9,buf
    8 NSR:802845B8|E1A08002            cpy     r8,r2            ; r8,count

    7~8번째 줄 코드를 보면 r1와 r2 레지스터로 전달된 인자를 r9와 r8에 전달함을 알 수 있습니다.

    하지만 x86 아키텍처에서는 때에 따라 레지스터, 스택 메모리 영역에 함수의 인자를 저장하여 함수도 전달하도록 지원합니다.

    당연히 인자를 레지스터에 저장하여 넘기는 방식이 빠르기 때문에 (fastcall) 최적화 옵션을 켜고 컴파일하는 경우 인자를 레지스터를 통해 전달하도록 함수의 호출부와 구현부를 변경해 버릴 수 있습니다. (일반적인 최적화 방법) 이 경우 GCC를 통해 자동 생성되는 코드는 적절히 변환되므로 문제가 없지만 직접 작성한 어셈블리 코드에서 함수를 호출하는 경우 문제가 발생합니다.

    이를 방지하기 위해 어셈블리 코드와 링크되는 함수는 인자를 (레지스터를 이용하지 않고) 스택을 이용해서 전달하도록
    선언합니다. 이를 위해 asmlinkage 매크로를 함수 선언부에 지정해야 합니다.

    asmlinkage 매크로를 선언한 <include/linux/linkage.h> 해더를 보면
    <asm/linkage.h> 파일을 #include 하고 있습니다.

    대부분의 아키텍처에서 이 파일은 asmlinkage를 정의하지 않지만 i386에서는 다음과 같이 정의했습니다.
    #define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
    #define FASTCALL(x) x __attribute__((regparm(3)))
    #define fastcall __attribute__((regparm(3)))

    regparm 속성은 레지스터를 이용하여 전달한 인자의 수를 지정합니다.
    asmlinkage로 선언된 경우 모두 스택을 이용하고 (레지스터로 0개의 인자 전달) fastcall로 선언된 경우 최대 3개의 인자를 레지스터로 전달합니다.


    [라즈베리파이] 시스템 콜 - 소프트웨어 인터럽트 벡터(vector_swi) 코드 분석 [라즈베리파이] 시스템 콜

    커널 공간에서 시스템 콜 실행 출발점은 vector_swi 레이블입니다. svc 명령어를 실행하면 소프트웨어 인터럽트 벡터인 vector_swi 레이블으로 브랜치하기 때문입니다. vector_swi 레이블 어셈블리 코드 분석으로 ARM 리눅스 커널에서 시스템 콜이 어떻게 동작하는지 살펴봅시다.

    vector_swi 레이블 어셈블리 코드는 다음과 같습니다.
    1  80107ee0 <vector_swi>:
    2  80107ee0:  e24dd048  sub  sp, sp, #72 ; 0x48
    3  80107ee4:  e88d1fff    stm   sp, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip}
    4  80107ee8:  e28d803c  add   r8, sp, #60 ; 0x3c
    5  80107eec:  e9486000  stmdb  r8, {sp, lr}^
    6  80107ef0:  e14f8000   mrs r8, SPSR
    7  80107ef4:  e58de03c  str lr, [sp, #60] ; 0x3c
    8  80107ef8:  e58d8040  str r8, [sp, #64] ; 0x40
    9  80107efc:  e58d0044  str  r0, [sp, #68] ; 0x44
    10 80107f00: e3a0b000  mov  fp, #0
    11 80107f04: ee11cf10   mrc 15, 0, ip, cr1, cr0, {0}
    12 80107f08: e59fa0b0  ldr sl, [pc, #176] ; 80107fc0 <__cr_alignment>
    13 80107f0c: e59aa000  ldr sl, [sl]
    14 80107f10: e13a000c  teq sl, ip
    15 80107f14: 1e01af10  mcrne  15, 0, sl, cr1, cr0, {0}
    16 80107f18: e92d500f  push   {r0, r1, r2, r3, ip, lr}
    17 80107f1c: eb0355a9  bl 801dd5c8 <trace_hardirqs_on>
    18 80107f20: e8bd500f  pop {r0, r1, r2, r3, ip, lr}
    19 80107f24: f1080080  cpsie i
    20 80107f28: e1a096ad  lsr r9, sp, #13
    21 80107f2c: e1a09689  lsl r9, r9, #13
    22 80107f30: e28f808c  add r8, pc, #140 ; 0x8c
    23 80107f34 <local_restart>:
    24 80107f34: e599a000 ldr sl, [r9]
    25 80107f38: e92d0030 push {r4, r5}
    26 80107f3c: e31a00f0  tst sl, #240 ; 0xf0
    27 80107f40: 1a000008 bne 80107f68 <__sys_trace>
    28 80107f44: e3570e19 cmp r7, #400 ; 0x190
    29 80107f48: e24fee11 sub lr, pc, #272 ; 0x110
    30 80107f4c: 3798f107 ldrcc pc, [r8, r7, lsl #2]
    31 80107f50: e28d1008 add r1, sp, #8
    32 80107f54: e357080f  cmp r7, #983040 ; 0xf0000
    33 80107f58: e2270000 eor r0, r7, #0
    34 80107f5c: 2a0010db bcs 8010c2d0 <arm_syscall>
    35 80107f60: e3a08000 mov r8, #0
    36 80107f64: ea00d333  b 8013cc38 <sys_ni_syscall>
    위와 같은 어셈블리 코드는 어떻게 추출할까요?

    리눅스 커널을 빌드하면 리눅스 커널 폴더에 심볼 정보가 포함된 vmlinux 파일이 생성됩니다. 라즈베리파이에서 제공하는 objdump 란 바이너리 유틸리티를 쓰면 vmlinux 에서 어셈블리 코드를 추출할 수 있습니다. 명령어는 다음과 같습니다.
    root@raspberrypi:/home/pi# vmlinux –d > linux_kernel_assemble.txt
    ret_fast_syscall 레이블 동작은 3단계로 구분할 수 있습니다.
    1단계: 프로세스 스택 공간에 유저 공간에서 실행했던 레지스터 세트를 푸시
    2단계: 시스템 콜 핸들러를 실행한 후 복귀할 주소를 ret_fast_syscall 레이블로 저장
    3단계: 시스템 콜 테이블에 접근해서 시스템 콜 핸들러 함수 분기

    먼저 1단계 코드를 분석하겠습니다. 1번째 줄 코드를 보겠습니다.
    1  80107ee0 <vector_swi>:
    2  80107ee0:  e24dd048  sub  sp, sp, #72 ; 0x48

    sp는 r13 레지스터로 현재 실행 중인 코드의 스택 주소를 저장합니다. sub 명령어를 실행해서 스택 주소를 0x48만큼 뺍니다. 스택 주소를 0x48만큼 빼는 동작은 0x48 바이트만큼 스택 공간을 확보한다는 의미입니다.

    vector_swi 레이블을 실행할 때는 ARM 프로세서 레지스터 세트는 유저 공간에서 소프트웨어 인터럽트를 발생하기 직전 정보를 저장하고 있습니다. 

    3번째 줄 코드를 보겠습니다.
    3  80107ee4:  e88d1fff    stm   sp, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip}

    프로세스 스택 공간에 r0에서 r12 레지스터까지 푸시(저장)합니다. r0~r12 레지스터는 유저 공간에서 실행 중인 레지스터를 의미합니다.
    4  80107ee8:  e28d803c  add   r8, sp, #60 ; 0x3c

    스택 주소를 0x3c만큼 더해서 r8 레지스터에 저장합니다.
    스택 주소를 이동해서 스택 공간에 다른 데이터를 저장하기 전 주로 실행하는 명령어입니다.
    5  80107eec:  e9486000  stmdb  r8, {sp, lr}^

    r13, r14 레지스터를 푸시할 스택 공간을 0으로 비웁니다.
    20~21번째 줄 코드를 분석하겠습니다.
    20 80107f28: e1a096ad  lsr r9, sp, #13
    21 80107f2c: e1a09689  lsl r9, r9, #13

    현재 실행 중인 스택 주소 프로세스 최상단 스택 주소에 접근해 r9 레지스터에 저장합니다.
    프로세스 최상단 주소에 있는 struct thread_info 구조체 flags 멤버에 접근하기 위해서입니다.
      
    다음 vector_swi 레이블에서 가장 중요한 22번째 줄 코드를 보겠습니다.
    22 80107f30: e28f808c  add r8, pc, #140 ; 0x8c

    현재 실행 중인 코드 프로그램 카운터에서 0x8c만큼 더해서 r8 레지스터에 저장합니다.
    22번째 줄 코드가 0x80107f30 주소에 있으니 프로그램 카운터는 0x80107f30 주소를 저장하고 있을 것입니다. 그런데 ARM 프로세스는 파이프라인 방식으로 어셈블리 명령어를 병렬로 처리하므로 0x80107f2c 주소 기준으로 -0x4 바이트만큼 오프셋을 계산할 필요가 있습니다.

    0x80107f2c 주소에서 0x8c만큼 더해서 다음과 같이 r8 레지스터에 저장하라는 코드입니다.
    r8 = 0x80107fc4 = 0x80107f2c + 0x8c

    r8 레지스터에 저장된 0x80107fc4 주소에 어떤 심볼이 있는 지 확인해 봅시다.
    sys_call_table 심볼이 보입니다.
    80107fc4 <sys_call_table>:
    80107fc4:8012c6f4 801212c0 8011c100 8026ab24
    80107fd4:8026abc4 80268508 80267108 8013cc38
    80107fe4:80268558 8027a428 8027a0e4 80271d68
    80107ff4:80267abc 8013cc38 80279f74 80267dc8
    ...
    801085c4:804ddd18 80225d30 8013cc38 80271d98 
    801085d4:8013cc38 80202d50 8023cf84 8026b224  
    801085e4:8026af60 8026affc 80241e9c 80241eb8 
    801085f4:80241f24 8013cc38 8013cc38 8013cc38

    80107fc4 주소부터 80xx_xxxx 주소가 배열같이 정렬되어 있습니다. 이 주소는 시스템 콜 핸들러 함수 입니다.

    이해를 돕기 위해 Trace32로 sys_call_table 테이블을 확인해 보겠습니다.
    다음과 같이 명령어를 입력합시다.
    d.v %y.l sys_call_table
    ________address|value_______|symbol
        NSD:80107FC4|0x8012D4E0   \\vmlinux\kernel/signal\sys_restart_syscall
    1   NSD:80107FC8|0x80121E08   \\vmlinux\exit\sys_exit
    2   NSD:80107FCC|0x8011C6D0   \\vmlinux\fork\sys_fork
    3   NSD:80107FD0|0x802844FC   \\vmlinux\read_write\sys_read
    4   NSD:80107FD4|0x8028459C   \\vmlinux\read_write\sys_write
    5   NSD:80107FD8|0x80281788   \\vmlinux\open\sys_open
    6   NSD:80107FDC|0x80280380   \\vmlinux\open\sys_close
    7   NSD:80107FE0|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait
    8   NSD:80107FE4|0x802817D8   \\vmlinux\open\sys_creat
    9 NSD:80107FE8|0x80294770   \\vmlinux\fs/namei\sys_link
    10 NSD:80107FEC|0x80294428   \\vmlinux\fs/namei\sys_unlink
    11 NSD:80107FF0|0x8028BE98   \\vmlinux\exec\sys_execve
    12 NSD:80107FF4|0x80280D3C   \\vmlinux\open\sys_chdir
    13 ...
    14 NSD:801085E4|0x80284BAC   \\vmlinux\read_write\sys_preadv2
    15 NSD:801085E8|0x80284C48   \\vmlinux\read_write\sys_pwritev2
    16 NSD:801085EC|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait
    17 NSD:801085F0|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait
    18 NSD:801085F4|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait
    19 NSD:801085F8|0x80289668   \\vmlinux\fs/stat\sys_statx
    20 NSD:801085FC|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait
    21 NSD:80108600|0x8013DE74   \\vmlinux\sys_ni\compat_sys_epoll_pwait

    확인 결과 sys_call_table 심볼에는 sys_xxx() 패턴으로 구현된 시스템 콜 핸들러 함수 주소가 0x80107FC4~0x80108600 주소 구간에 저장되어 있습니다.

    0x80107FC8~0x80107FD0 주소에 단지 0x80121E08, 0x8011C6D0, 0x802844FC 값이 있을 뿐인데, 주소를 심볼 단위로 변환해서 보여주는 Trace32 "d.v %y.l [심볼]" 명령어로 바로 주소에 대응하는 함수를 알게 된 것입니다. 
    2   NSD:80107FC8|0x80121E08   \\vmlinux\exit\sys_exit
    3   NSD:80107FCC|0x8011C6D0   \\vmlinux\fork\sys_fork
    4   NSD:80107FD0|0x802844FC   \\vmlinux\read_write\sys_read

    24~25번째 줄은 시스템 콜 디버깅을 위한 코드입니다.
    24 80107f34: e599a000 ldr sl, [r9]
    25 80107f38: e92d0030 push {r4, r5}
    26 80107f3c: e31a00f0  tst sl, #240 ; 0xf0
    27 80107f40: 1a000008 bne 80107f68 <__sys_trace>

    24번째 줄 코드는 r9 레지스터 저장된 프로세스 스택 최상단 주소로 struct thread_info 구조체에 접근해서 struct thread_info 첫 번째 멤버인 flags를 sl(r10) 레지스터에 저장하는 동작입니다.

    flags 값으로 시스템 콜을 ftrace로 출력할지 결정하며 ftrace 에서 시스템 콜 디버깅 이벤트를 켰을 경우 __sys_trace 레이블을 실행하는 코드입니다. ftrace 로그 분석 방법은 디버깅 절에서 더 다룹니다.

    28번째 줄 코드를 보겠습니다.
    28 80107f44: e3570e19 cmp r7, #400 ; 0x190

    r7 레지스터는 시스템 콜 번호를 저장하고 있다는 점을 기억합시다.

    cmp는 r7 레지스터에 저장된 값과 400을 비교하는 명령어입니다.
    r7 레지스터에 저장된 시스템 콜 번호와 시스템 콜 최대값인 400과 비교하는 것입니다.

    제대로 시스템 콜 번호를 지정했으면 시스템 콜 번호가 400보다 적을 겁니다. 하지만 유저 모드에서 직접 시스템 콜 번호를 r7에 잘못 지정해서 소프트웨어 인터럽트를 발생했을 경우를 위한 예외 처리 코드입니다.

    cmp 명령어 결과는 ARM 프로세서 CPSR(Current Program Status Register) 레지스터의 Zero와 Carry비트에 업데이트 됩니다. 정상적으로 r7 레지스터에 시스템 콜 번호가 저장돼있으면 r7 레지스터는 400보다 적을 것이므로 Z와 C 비트는 1로 변경되지 않습니다.

    만약 r7에 저장된 시스템 콜 번호가 400이거나 400보다 크면 CPSR 레지스터 C 비트는 1로 바뀝니다.

    이 명령어 결과로 시스템 콜 핸들러 함수로 분기하는 다음 30번째 줄 코드 실행 흐름에 영향을 끼칩니다.
    30 80107f4c: 3798f107 ldrcc pc, [r8, r7, lsl #2]

    ldrcc 명령어는 ldr+cc 조합 명령어인데 CPSR 레지스터 C 비트가 1이 아닐 때만 ldr 명령어를 수행합니다. 리눅스 관점으로 이 명령어를 분석하면 시스템 콜 번호가 400 이하 일 때만 시스템 콜 테이블에 지정된 핸들러 함수로 분기합니다.

    29번째 줄 코드를 봅시다. 
    29 80107f48: e24fee11 sub lr, pc, #272 ; 0x110

    이번에는 2단계 코드를 볼 차례입니다.
    이 명령어를 실행하는 코드 주소에서 0x110 만큼 뺄샘 연산을 한 결과를 lr(r14) 복귀 레지스터에 저장합니다.
    lr = 0x80107e44 = 0x80107f44 - 0x110

    0x80107e44 주소에는 다음과 같이 ret_fast_syscall 레이블이 저장돼 있습니다.
    80107e44 <ret_fast_syscall>: 
    80107e44: e5ad0008  str r0, [sp, #8]!
    80107e48: f10c0080  cpsid i

    시스템 콜 핸들러 실행을 마무리한 다음에 복귀할 주소를 ret_fast_syscall 레이블로 지정한 것입니다.

    ftrace로 시스템 콜 실행 후에 호출되는 함수 콜스택을 보면 다음 8번째 줄 로그와 같이 ret_fast_syscall 레이블을 볼 수 있습니다.
    1 lxpanel-731   [002] 118.058060: mutex_lock+0x14/0x130 <-kstat_irqs_usr+0x24/0x44
    2 lxpanel-731   [002] 118.058069: <stack trace>
    3 => seq_read+0x1dc/0x504
    4 => proc_reg_read+0x6c/0x90
    5 => __vfs_read+0x3c/0x134
    6 => vfs_read+0x9c/0x164
    7 => SyS_read+0x4c/0xa0
    8 => ret_fast_syscall+0x0/0x28

    7번에서 3번째 줄 함수 방향으로 함수를 실행한 후 다시 3번째 줄 함수에서 7번째 줄 함수로 복귀합니다. 이후 8번째 줄에서 보이는 ret_fast_syscall 레이블을 실행합니다.

    3단계 코드를 볼 차례입니다.
    마지막 30번째 줄 코드를 보겠습니다. 역시 vector_swi 레이블에서 가장 중요한 코드입니다.
    30 80107f4c: 3798f107 ldrcc pc, [r8, r7, lsl #2]

    시스템 콜 테이블에 접근해 시스템 콜 번호에 해당 하는 핸들러 함수로 실행 흐름을 바꾸는 코드입니다.

    ldrcc 어셈블리 명령어는 ldr+cc 조합으로 구성돼 있습니다. ARM CPSR 레지스터의 C(Carry) 비트가 1로 설정되어 있으면 ldr 명령어를 실행하지 않습니다.

    ARM CPSR 레지스터의 C(Carry) 비트는 어떤 조건에서 1로 설정될까요? 

    28번째 줄 어셈블리 명령어에서 시스템 콜 번호가 400보다 같거나 클 경우 CPSR 레지스터 Carry 비트를 1로 설정합니다.
    28 80107f44: e3570e19 cmp r7, #400 ; 0x190
    ...
    30 80107f4c: 3798f107 ldrcc pc, [r8, r7, lsl #2]

    유저 공간에서 적절한 시스템 콜 번호를 지정했을때만 시스템 콜 핸들러를 실행하는 예외 코드입니다.

    ldrcc 명령어 동작 조건에 대해 알아 봤으니 30번째 줄 명령어 전체 동작 분석을 시작하겠습니다. 30번 줄 명령어는 이해하기 쉽게 다음 과정으로 변환시킬 수 있습니다.
    ldrcc pc, [r8, r7, lsl #2]
    pc = *(r8 + (r7 << 2))

    r7에 저장된 값을 왼쪽으로 2만큼 비트 연산(Logical Shift Left: lsl)을 한 결과를 r8 레지스터에 더합니다. 계산 결과로 이 메모리 주소에 있는 값을 pc에 로딩하는 것입니다.

    이 명령어 실행을 할 때 r7, r8 레지스터가 어떤 값을 갖고 있는지 먼저 점검합시다. 

    r8 레지스터는 시스템 콜 테이블 주소 위치를 담고 있고, r7은 유저 공간에서 지정한 시스템 번호를 저장하고 있습니다.

    r7이 시스템 콜 번호인4 그리고 시스템 콜 데이블 심볼 주소인 0x80107fc4를 r8이 저장하고 있다고 가정하고 이 명령어 실행 과정을 확인합시다.
    ldrcc pc, [r8, r7, lsl #2]

    [계산 과정]
    pc = *(r8 + (r7 << 2))
    pc = *(r8 + (4 << 2))
    pc = *(0x80107fc4 + 0x10)
    pc = *(0x80107fd4)
    pc =  0x8028459c   \\vmlinux\read_write\sys_write

    각 명령어 실행 결과 프로그램 카운터 레지스터는 sys_write() 함수 주소인 0x8028459c를 저장하게 됩니다.

    시스템 콜 테이블인 sys_call_table 주소에 있는 메모리 정보는 다음과 같습니다.
    ________address|value_______|symbol
    0   NSD:80107FC4|0x8012D4E0   \\vmlinux\kernel/signal\sys_restart_syscall
    1   NSD:80107FC8|0x80121E08   \\vmlinux\exit\sys_exit
    2   NSD:80107FCC|0x8011C6D0   \\vmlinux\fork\sys_fork
    3   NSD:80107FD0|0x802844FC   \\vmlinux\read_write\sys_read
    4   NSD:80107FD4|0x8028459C   \\vmlinux\read_write\sys_write 
    5   NSD:80107FD8|0x80281788   \\vmlinux\open\sys_open

    sys_call_table 심볼에는 4바이트 단위로 시스템 콜 핸들러 주소가 저장돼 있습니다. 라즈베리파이가 32 비트 ARM 아키텍처를 적용했으니 심볼은 4바이트(32비트) 단위 주소인 것입니다. 

    ARM 프로그램 카운터 레지스터가 어떤 주소로 변경되면 해당 주소를 실행한다는 의미입니다. ARM 아키텍처에서 프로그램 카운터 레지스터에 저장된 주소에 있는 기계어를 Fetch하기 때문입니다.

    이번 시간에 시스템 콜 핵심 어셈블리 코드를 분석했습니다. 유저 공간에서 시스템 콜을 실행하면 코드 공간으로 스위칭되는 진입점 코드를 확인한 것입니다. 리눅스 커널을 이론으로 이해하는 것보다 어셈블리 코드를 분석하면 더 오랫동안 머리 속에 남습니다.

    #Reference 시스템 콜


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

    .

      [라즈베리파이] 시스템 콜 - 커널 모드에서 시스템 콜을 어떻게 실행할까? [라즈베리파이] 시스템 콜

      이번 절에서는 커널 공간에서 시스템 콜을 어떻게 실행하는지 살펴보겠습니다.
      다음 그림 전체 흐름도에서 검은색으로 표시된 부분을 눈여겨봅시다.

      위 그림에서 검은색으로 된 부분입니다. 유저 공간에서 소프트웨어 인터럽트를 유발했으니 소프트웨어 인터럽트 벡터인 vector_swi 레이블이 실행합니다. 유저 공간에서 “svc 0x0” 이란 명령어를 실행하면 vector_swi 이란 시스템 콜 처리용 인터럽트 벡터로 프로그램 카운터를 이동하는 것입니다. 

      유저 공간에서 지정한 시스템 콜 번호는 r7 레지스터에 실려서 옵니다. 이 시스템 콜 번호를 통해 시스템 콜 테이블에 접근한 후 시스템 콜 핸들러로 분기합니다.

      ARM 프로세서 관점으로 시스템 콜을 실행하면 커널 공간에서 다음과 같은 동작을 수행합니다.
      1> 유저 공간에서 실행 중인 프로세스 레지스터 세트를 프로세스 최하단 스택 공간에 푸시
      2> 시스템 콜에서 유저 공간에서 전달한 시스템 콜 번호를 r7 레지스터에서 읽어 시스템 콜 테이블에서 시스템 콜 핸들러 함수로 분기
         : 각 시스템 콜 핸들러 함수 서브 루틴 실행
      3> 프로세스 최하단 스택 공간에 푸시한 레지스터를 팝해서 ARM 레지스터에 다시 저장
        : 소프트웨어 인터럽트 발생 직전 코드로 이동함

      소프트웨어 인터럽트 벡터 소개
      시스템 콜은 ARM 아키텍처와 연관이 깊은 동작입니다. 시스템 콜은 익셉션의 한 종류로 실행하는데 익셉션은 CPU 마다 달리 구현되어 있습니다.

      익셉션이 발생하면 ARM 프로세서는 실행하던 동작을 멈추고 익셉션 종류 별로 이미 정해진 주소를 프로그램 카운터로 바꿉니다. 익셉션 별로 실행하는 이미 정해진 주소는 익셉션 벡터라고 합니다. 

      시스템 콜을 호출하면 소프트웨어 인터럽트가 발생하며 유저 공간에서 커널 공간으로 이동한 다음 익셉션 벡터인 vector_swi 레이블로 함수 주소를 바꿉니다. 이후 다음과 같은 동작을 수행합니다.
      1> 유저 공간에서 실행 중인 프로세스 레지스터를 프로세스 최하단 스택 공간에 푸시
      2> 시스템 콜에서 유저 공간에서 전달한 시스템 콜 번호를 R7 레지스터에서 읽어 시스템 콜 테이블에서 시스템 콜 핸들러 함수로 분기
         : 각 시스템 콜 핸들러 함수 서브 루틴 실행
      3> 프로세스 최하단 스택 공간에 푸시한 레지스터를 팝해서 ARM 레지스터에 다시 저장
        : 소프트웨어 인터럽트 발생 직전 코드로 이동함

      유저 공간에서 “svc 0x0” 이란 명령어를 실행하면 vector_swi 이란 시스템 콜 처리용 인터럽트 벡터로 프로그램 카운터를 이동합니다. vector_swi 레이블은 ARM 프로세스를 직접 제어하는 어셈블리 코드로 구성돼 있습니다.

      1초에 수 십번 이상 시스템 콜이 수행하기 때문에 ARM 프로세스를 최적화해서 구동할 수 있는 어셈블리 코드로 구성돼 있습니다.


      시스템 콜 벡터 테이블이 실행하기 직전 R7에 시스템 콜 번호가 저장돼 있다는 사실을 기억합시다.

      #Reference 시스템 콜


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

      .

      [라즈베리파이] 시스템 콜 - 유저 공간 시스템 콜 발생 어셈블리 코드 분석 [라즈베리파이] 시스템 콜

      아키텍처별로 시스템 콜을 실행하는 동작은 다릅니다. 시스템 콜을 실행하는 동작은 어셈블리 코드로 구현되어 있습니다.

      라즈베리파이가 탑재한 ARM(ARM32) 아키텍처에서는 시스템 콜은 다음과 같이 구현돼 있습니다.
      1. r0 ~ r5 레지스터에 시스템 콜로 전달할 인자 지정
      2. r7 레지스터에 시스템 콜 번호 저장
      3. "svc 0x00000000" 명령어 실행

      GNU C 라이브러리 파일에서 실제 시스템 콜을 실행하는 어셈블리 코드를 봅시다.

      write() 함수에 대한 시스템 콜을 실행하는 코드를 소개합니다.
      00000020 <__libc_write>:
      1  20: e59fc060  ldr ip, [pc, #96] ; 88 <__libc_write+0x68>
      2  24: e79fc00c  ldr ip, [pc, ip]
      3  28: e33c0000  teq ip, #0
      4  2c: e52d7004  push {r7} ; (str r7, [sp, #-4]!)
      5  30: 1a000005  bne 4c <__libc_write+0x2c>
      6  34: e3a07004  mov r7, #4
      7  38: ef000000  svc 0x00000000
      6번째 줄 코드를 보면 r7 레지스터에 write() 함수에 대응하는 시스템 콜 번호인 4를 저장합니다.


      라즈베리파이에서 다음 해더 파일에서 __NR_write 매크로를 보면 인덱스가 4임을 알 수 있습니다.
      [/usr/include/arm-linux-gnueabihf/asm/unistd.h]
      #define __NR_write (__NR_SYSCALL_BASE+  4)

      이 인덱스가 시스템 콜 번호입니다.

      이번에는 open() 함수를 호출하면 GNU C 라이브러리에서 시스템 콜을 실행하는 코드를 보겠습니다.
      00000020 <__libc_open>:
      1  20: e59fc060  ldr ip, [pc, #96] ; 88 <__libc_open+0x68>
      2  24: e79fc00c  ldr ip, [pc, ip]
      3  28: e33c0000  teq ip, #0
      4  2c: e52d7004  push {r7} ; (str r7, [sp, #-4]!)
      5  30: 1a000005  bne 4c <__libc_open+0x2c>
      6  34: e3a07005  mov r7, #5
      7  38: ef000000  svc 0x00000000

      6번째 줄 코드를 보면 r7 레지스터에 5를 저장합니다. 이후 7번째 줄 코드에서 “svc 0x0” 명령어를 실행해서 Supervisor Call 즉 소프트웨어 인터럽트를 유발합니다.

      라즈베리파이에서 다음 해더 파일에서 __NR_open 매크로를 확인하면 인덱스가 5임을 알 수 있습니다.
      [/usr/include/arm-linux-gnueabihf/asm/unistd.h]
      #define __NR_open (__NR_SYSCALL_BASE+  5)

      r7 레지스터에 저장하는 5란 정수는 open() 함수에 대한 시스템 콜 번호입니다.

      write(), open() 함수를 호출하면 시스템 콜이 실행되고 커널 공간에서 sys_write() 와 sys_open() 함수를 호출할 것이라 예상합니다.

      대부분 리눅스 저수준 표준 함수 이름과 시스템 콜 핸들러 함수는 일치하는 경우가 많지만,
      모든 함수가 이런 규칙으로 동작하는 것은 아닙니다.

      exit() 함수를 생각해 봅시다. exit() 함수를 호출하면 우리는 당연히 sys_exit() 함수로 시스템 콜 번호를 지정할 것이라 예상할 수 있습니다.

      exit() 함수를 호출하면 libc.a 라이브러리 파일 내 다음 경로에 있는 _exit 레이블을 실행합니다.
      [glibc/sysdeps/unix/sysv/linux/_exit.c]
      00000000 <_exit>:
      1    0: e92d4080  push {r7, lr}
      2    4: e1a02000  mov r2, r0
      3    8: e3a070f8  mov r7, #248 ; 0xf8
      4    c: ef000000  svc 0x00000000

      3~4번째 줄 코드를 보겠습니다.
      3    8: e3a070f8  mov r7, #248 ; 0xf8
      4    c: ef000000  svc 0x00000000

      r7 레지스터에 시스템 콜 번호인 248를 저장합니다.
      라즈베리파이에서 다음 해더 파일을 보면 248에 대한 시스템 콜 함수는 매크로는 __NR_exit_group입니다.
      [/usr/include/arm-linux-gnueabihf/asm/unistd.h]
      #define __NR_exit_group (__NR_SYSCALL_BASE+248)

      리눅스 커널 기준으로 sys_call_table 심볼에 있는 시스템 콜 데이블 248번에 해당하는 시스템 콜 핸들러 함수는 sys_exit_group() 입니다. 프로세스를 종료할 때 프로세스가 속한 스레드 그룹까지 종료하는 경우가 많아서 sys_exit_group() 함수를 호출하는 것입니다.

      참고로, GNU C 라이브러리 코드는 다음 경로에서 확인할 수 있습니다.
      [https://code.woboq.org/userspace/glibc/stdlib/exit.c.html]
      1 void
      2 _exit (int status)
      3 {
      4  while (1)
      5    {
      6 #ifdef __NR_exit_group
      7      INLINE_SYSCALL (exit_group, 1, status);
      8 #endif
      9      INLINE_SYSCALL (exit, 1, status);
      10 #ifdef ABORT_INSTRUCTION
      11      ABORT_INSTRUCTION;
      12 #endif
      13    }
      14 }

      7번째 줄 코드를 실행하면 r7 레지스터에 __NR_exit_group 시스템 콜 번호인 248을 저장하고 소프트웨어 인터럽트를 유발합니다. 

      코드를 분석하고 실행 흐름을 예측하는 것은 중요합니다. 그런데 개발 도중 만나는 여러 문제를 해결하기 위해서는 리눅스 시스템에서 실제 어떤 동작을 하는지 점검할 필요가 있습니다. 저도 exit() 함수를 호출하면 248번 시스템 콜을 실행한다는 사실은 라즈베리파이에서 GDB 디버깅을 통해 확인했습니다.


      이번 시간에 유저 공간에서 시스템 콜을 발생하는 어셈블리 코드를 분석했습니다. 
      다음 시간에 유저 공간에서 발생한 시스템 콜을 커널 공간에서 어떻게 처리하는지 살펴보겠습니다.

      #Reference 시스템 콜


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

      .


      [라즈베리파이] 시그널 - 시그널 핸들러 실행 및 커널 복귀 과정 [라즈베리파이] 시그널

      signal은 특정 프로세스에게 어떤 메시지를 전달할 수 있는 가장 기본적인 수단입니다.
      signal은 다른 (user-level) 프로세스로부터 직접적으로 받거나 혹은 (주로 문제가 될 만한 동작으로인해) 커널로부터 받을 수 있습니다.

      이러한  signal은 kernel-mode에서 처리가 되는데 주로 시스템 콜이나 인터럽트 처리 등을 마치고 user-mode로 돌아오기 직전에 해당 프로세스에게 전달된 signal이 있는지 검사하여 실행됩니다.
      (SMP 커널에서는 user-mode에서 실행 중인 프로세스가 signal을 처리해야 하면 강제로 scheduling하도록 IPI를 보내서 kernel-mode로 들어오게 만들기도 합니다.)

      signal을 받은 프로세스의 기본적인 반응은 거의 대부분 해당 프로세스의 실행을 종료하는 것이며,
      이 밖에 signal의 종류에 따라 실행을 중지하거나 그냥 무시하는 경우도 있습니다.

      응용 프로그램은 커널에서 제공하는 몇 가지 시스템 콜을 이용하여 특정한 signal을 받았을 때 기본 동작을 수행하는 대신 사용자가 원하는 동작을 수행하는 signal handler를 등록해 둘 수 있습니다.
      (물론 이런 식으로 처리할 수 없는 강제적인 signal도 있습니다.)

      우선 다음과 같은 예제를 살펴봅시다.
      sighandler.c:
      #include <stdio.h>
      #include <signal.h>

      static void unused_func(void)
      {
        printf("%s\n", __FUNCTION__);
      }

      static void sighandler(int sig)
      {
        printf("%s\n", __FUNCTION__);
      }

      int main(void)
      {
        struct sigaction sa;

        /* set up signal handler */
        sa.sa_handler = sighandler;
        sigaction(SIGUSR1, &sa, NULL);

        /* send signal to myself */
        printf("before raise()\n");
        raise(SIGUSR1);
        printf("after  raise()\n");
        
        return 0;
      }

      하지만 이러한 signal handler은 user-mode에서 실행되어야 한다는 문제가 있습니다.
      앞서 말했다시피 signal에 대한 처리를 수행하는 것은 커널인데 signal handler는 잠시 user-mode에서 실행하고 실행이 끝나면 다시 커널로 돌아와야 합니다.

      리눅스는 kernel-mode로 진입 시 kernel stack에 user-mode에서 실행 중이던 context를 저장하는데 일단 kernel-mode를 벗어나면 kernel stack은 초기화되어버리기 때문에 signal handler를 마치고 다시 kernel-mode로 돌아가게되면
      원래 돌아가야 할 user-mode에 대한 정보를 잃어버리게 됩니다.

      이를 해결하기 위해서는 signal handler를 실행하기 전에 원래의 kernel-stack에 있는 user context 정보를 (frame이라고 부른다.) signal handler를 실행할 user stack에 임시로 저장해 두었다가 signal handler가 마치고 kernel mode로 돌아오면 임시로 저장해 둔 정보를 이용하여 kernel stack을 다시 복구하는 방법을 사용합니다. 
      [https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/signal.c]
      static int
      setup_frame(struct ksignal *ksig, sigset_t *set, struct pt_regs *regs)
      {
      struct sigframe __user *frame = get_sigframe(ksig, regs, sizeof(*frame));
      int err = 0;

      if (!frame)
      return 1;

      /*
       * Set uc.uc_flags to a value which sc.trap_no would never have.
       */
      __put_user_error(0x5ac3c35a, &frame->uc.uc_flags, err);

      err |= setup_sigframe(frame, regs, set);
      if (err == 0)
      err = setup_return(regs, ksig, frame->retcode, frame);

      return err;
      }

      이제 모든 signal을 처리하고 user mode로 돌아가게 되면 원래 signal이 발생했던 시점부터 다시 실행을 시작할 수 있게 됩니다.

      실제로 이러한 frame  정보는 커널 내에 다음과 같이 정의되어 있습니다.
      [https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/signal.c]
      struct sigframe {
      struct ucontext uc;
      unsigned long retcode[2];
      };

      여기서 ucontext 구조체에 각종 레지스터들의 현재 값을 저장해둡니다.

      ucontext 구조체 정의는 다음 코드에서 확인할 수 있습니다.
      [https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/ucontext.h]
      struct ucontext {
      unsigned long   uc_flags;
      struct ucontext  *uc_link;
      stack_t   uc_stack;
      struct sigcontext uc_mcontext;
      sigset_t   uc_sigmask;
      /* Allow for uc_sigmask growth.  Glibc uses a 1024-bit sigset_t.  */
      int   __unused[32 - (sizeof (sigset_t) / sizeof (int))];
      /* Last for extensibility.  Eight byte aligned because some
         coprocessors require eight byte alignment.  */
        unsigned long   uc_regspace[128] __attribute__((__aligned__(8)));
      };

      그렇다면 signal handler가 실행을 마치고 kernel로 돌아간다는 것을 kernel이 알아야 합니다.
      이것이 어떻게 가능할까요??

      user-mode에서 kernel-mode로 전환하기 위해서는 system call을 이용해야 합니다.
      따라서 signal handler의 복귀를 위한 특별한 system call이 존재하며 (sigreturn과 rt_sigreturn)
      커널은 signal handler를 실행하기 전에 return address가 해당 system call을 호출하는 코드(__kernel_sigreturn)를 가리키도록 미리 설정합니다.

      따라서 signal handler에서 명시적으로 커널로 복귀하는 코드가 없어도 수행을 마치면 커널로 돌아갈 수 있습니다.

      __kernel_sigreturn의 코드는 아주 단순합니다.
      stack에서 4byte를 pop하고 sigreturn 시스템 콜을 호출하는 것이 전부입니다.
      (참고로 __NR_sigreturn은 ARM에서 119로 정의되어 있다.)
      [https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/sigreturn_codes.S]
      sigreturn_codes:

      /* ARM sigreturn syscall code snippet */
      arm_slot 0
      ARM_OK( mov r7, #(__NR_sigreturn - __NR_SYSCALL_BASE) )
      ARM_OK( swi #(__NR_sigreturn)|(__NR_OABI_SYSCALL_BASE) )

      /* Thumb sigreturn syscall code snippet */
      thumb_slot 0
      movs r7, #(__NR_sigreturn - __NR_SYSCALL_BASE)
      swi #0

      /* ARM sigreturn_rt syscall code snippet */
      arm_slot 1
      ARM_OK( mov r7, #(__NR_rt_sigreturn - __NR_SYSCALL_BASE) )
      ARM_OK( swi #(__NR_rt_sigreturn)|(__NR_OABI_SYSCALL_BASE) )

      /* Thumb sigreturn_rt syscall code snippet */
      thumb_slot 1
      movs r7, #(__NR_rt_sigreturn - __NR_SYSCALL_BASE)
      swi #0

      이 sigreturn이라는 시스템 콜은 커널이 signal handler를 수행한 후에 간접적으로 호출하도록 만들어진 것이므로
      user-level에서는 직접적인 사용을 금지하고 있습니다.

      예를 들어 signal handler에서 직접 sigreturn()을 호출하도록 프로그램을 작성해도 libc가 이를 무시하고 실제 시스템 콜을 호출하지 않습니다. 실제로 glibc-2.9의 sigreturn() 구현은 아래와 같습니다.
      glibc/signal/sigreturn.c:
      #include <signal.h>
      #include <errno.h>

      int
      __sigreturn (context)
           struct sigcontext *context;
      {
        __set_errno (ENOSYS);
        return -1;
      }
      stub_warning (sigreturn)

      weak_alias (__sigreturn, sigreturn)
      #include <stub-tag.h>

      위의 예제에서 sighandler() 함수 내에 sigreturn((void *) 0); 을 추가한 후 컴파일하면 다음과 같이 출력됩니다.
      $ gcc sighandler.c 
      /tmp/ccEOGoWm.o: In function `sighandler':
      sighandler.c:(.text+0x50): warning: warning: sigreturn is not implemented and will always fail

      한 마디로 sigreturn은 쓰지 말라는 이야기입니다.
      하지만 (포기하지 말자!) __kernel_sigreturn에서와 같이 asm 코드로 직접 시스템 콜을 호출하면 동일한 효과를 얻을 수 있습니다.

      한 가지 주의할 것은 (위의 sigreturn 함수의 prototype으로부터 얻을 수 있는 정보이기도 하다!)
      sigreturn 시스템 콜이 호출되는 시점에는 esp 레지스터가 sigframe의 sigcontext 구조체를 가리키고 있어야 한다는 점이빈다. (frame + 8)
      커널의 sigreturn 서비스 루틴은 esp에서 8을 빼서 sigframe의 위치를 찾습니다.
      (offsetof(struct sigframe, sc) = 8이다!)

      이제 대강 얘기를 풀어놓았으니 실제 예제를 가지고 몇가지 장난을 좀 쳐 보겠습니다.
      먼저 위의 예제를 그냥 컴파일 후 실행하면 다음과 같은 결과를 얻습니다.
      $ ./a.out
      before raise()
      sighandler
      after  raise()

      이제 sighandler에서 sigframe 정보를 추출하고,
      (sigframe은 함수의 return address부분부터 시작하므로 parameter 바로 아래의 주소에서 시작한다.)
      위에서 호출하지 않았던 unused_func으로 eip를 설정하면
      signal handler가 수행된 후에 커널로 제어가 넘어가고 다시 user-mode로 복귀할 때
      unused_func()이 호출되는 것을 볼 수 있습니다.
      static void sighandler(int sig)
      {
        struct sigframe *frame = (struct sigframe *) (&sig - 1);
        printf("%s\n", __FUNCTION__);
        frame->sc.eip = (unsigned long) unused_func;
      }

      다음은 위의 실행 결과입니다.
      $ gcc sighandler.c
      $ ./a.out
      before raise()
      sighandler
      <--------------------------- 여기서 user-mode로 return됨
      unused_func
      after  raise()

      이번에는 sigreturn() 시스템 콜을 직접 호출하여 커널로 복귀해 봅시다
      먼저 sighandler() 함수에서는 기존의 return address를 unused_func()의 주소로 바꿉니다.
      static void sighandler(int sig)
      {
        struct sigframe *frame = (struct sigframe *) (&sig - 1);
        printf("%s\n", __FUNCTION__);
        /* frame->sc.eip = (unsigned long) unused_func; */
        frame->sc.pretcode = (void *) unused_func;
      }

      unused_func에서는 sp (stack pointer)을 앞서 말한대로 &frame->sc와 맞춰야합니다..
      이제 sp 값에 대해서 한 번 살펴봅시다.

      우선 signal handler가 호출되는 순간 커널은 sp가 frame을 가리키도록 설정합니다.
      frame의 처음 두 필드는 return address와 parameter로 사용되는 signal 번호이므로
      이는 일반적인 함수 호출 시의 스택 구성과 완전히 동일합니다.

      signal handler가 수행을 마치고 ret instruction을 수행하면 스택에서 return address를 pop하므로
      이제 esp는 &frame->sig 값을 가집니다. (= frame + 4)

      다음으로는 바로 unused_func() 함수가 수행되는데,
      (다른 함수들과 마찬가지로) 이 함수가 제일 먼저 수행하는 일은
      ebp를 스택에 push, esp를 ebp에 저장, 로컬 변수 및 함수 호출에 필요한 스택 영역 확보 순입니다.
      $ objdump -d a.out | grep -A 5 unused
      08048484 <unused_func>:
       8048484:    55                       push   %ebp
       8048485:    89 e5                    mov    %esp,%ebp
       8048487:    83 ec 18                 sub    $0x18,%esp
       804848a:    c7 04 24 39 86 04 08     movl   $0x8048639,(%esp)
       8048491:    e8 26 ff ff ff           call   80483bc <puts@plt>

      즉 ebp에 (이전의 esp 값 - 4) 값이 들어있다는 것을 알 수 있습니다.
      따라서 ebp 값 + 8하면 &frame->sc 값을 얻을 수 있습니다.

      이제 unused_func()을 다음과 같이 수정합니다.
      static void unused_func(void)
      {
        printf("%s\n", __FUNCTION__);
        asm volatile("leal 8(%ebp), %esp; movl $119, %eax; int $0x80");
      }

      "leal 8(%ebp)" 부분은 "movl %ebp, %esp; addl $8, %esp" 명령과 동일합니다.
      이제 sigreturn의 시스템 콜 번호인 119를 eax에 저장하고 시스템 콜을 호출합니다. (int $0x80)
      아쉽게도? 출력 결과는 앞의 프로그램과 동일합니다. (추가한 설명 부분의 위치만 약간 바뀌었다.)
      $ gcc sighandler.c
      $ ./a.out
      before raise()
      sighandler
      unused_func
      <--------------------------- 여기서 user-mode로 return됨
      after  raise()
      signal handler 등록 시 SA_INFO flag를 설정하여 sa_sigaction 핸들러를 이용하는 경우에도
      sigframe의 구성과 sigreturn 대신 rt_sigreturn이 사용되는 몇 가지 차이 만 있을 뿐
      동작하는 방식은 동일하므로 약간만 변형하여 같은 결과를 얻을 수 있습니다.



      [라즈베리파이] 시스템 콜 - 유저 공간에서 시스템 콜 발생 [라즈베리파이] 시스템 콜

      유저 공간에서 커널 공간까지 어떤 코드로 시스템 콜을 구현했는지 분석을 시작합니다. 유저 모드에서 시스템 콜을 발생하면 커널 모드로 어떻게 스위칭하고 이후 시스템 콜 핸들러가 실행하는지 파악하는 것입니다.

      가장 먼저 이런 의문이 생깁니다. 시스템 콜은 실행 시작점은 어디일까요? 
      유저 공간에서 리눅스 저수준 함수를 호출했을 때가 시스템 콜 실행의 출발점입니다. 다음 전체 시스템 콜 흐름도에서 검은색으로 된 부분입니다.
       
      리눅스 저수준 함수를 호출하면 실행하는 GNU C 라이브러리(glibc)가 어떤 흐름올 시스템 콜을 호출하는지 알아봅시다.

      GNU C 라이브러리 실행 흐름
      유저 공간에서 시스템 콜을 실행하기 위해서는 저수준 표준 함수를 호출해야 합니다.
      저수준 표준 함수는 glibc 라이브러리에 구현돼 있어 실제 시스템 콜을 발생하는 코드를 확인하기 어렵습니다. 

      glibc란 라이브러리 파일의 정체는 무엇일까요? glibc는 라즈베리파이에서 다음 경로에 있는 libc.a란 파일입니다.
      root@raspberrypi:/home/pi# ls /usr/lib/arm-linux-gnueabihf/libc.a
      /usr/lib/arm-linux-gnueabihf/libc.a

      libc.a 를 리눅스에서 제공하는 라이브러리라고 하며 표준 입출력 함수과 프로세스 생성 요청과 같은 핵심 동작을 수행합니다.

      이번 시간에는 libc.a 파일에 있는 어셈블리 코드를 열어서 시스템 콜 호출을 어떻게 실행하는지 점검합시다. liba.c에 있는 파일을 어셈블리 코드로 변환하려면 라즈베리파이에서 기본으로 제공하는 objdump라는 바이너리 유틸리티를 실행하면 됩니다.

      다음과 같은 명령어를 입력하면 libc.a에 구현된 어셈블리 코드를 glibc_code.txt 파일로 저장할 수 있습니다.
      root@raspberrypi:/home/pi# objdump –d > glibc_code.txt

      이번에는 GNU C 라이브러리가 어떤 계층으로 구성됐는지 알아봅시다.

      저수준 표준 함수인 fork(), exit() open(), write(), exit() 함수를 호출하면 GNU C 라이브러리 함수를 통해 시스템 콜을 실행합니다. 리눅스 저수준 함수를 호출하면 GNU C 라이브러리에서 시스템 콜을 발생한다는 것입니다.

      이런 의문이 생깁니다. 이전 절에서 시스템 콜은 ARM 프로세서 “svc” 어셈블리 명령어인 Supervisor Call로 소프트웨어 인터럽트 방식으로 동작한다고 설명했습니다. GNU C 라이브러리 어딘가에 이 명령어를 실행하는 코드가 있다는 것입니다.
      GNU C 라이브러리는 크게 3개 계층으로 분류할 수 있습니다.

      리눅스 저수준 함수 인터페이스
      유저 어플리케이션에서 직접 호출하는 표준 함수 인터페이스입니다. 이를 래퍼(Wrapper) 함수라고도 합니다. 위 그림에서 화살표로 표시된 부분입니다. 
      리눅스 저수준 프로그램에서 fork() 이란 함수를 호출했다고 가정합시다. fork() 함수는 공용 인터페이스입니다. 어떤 아키텍처(x86, ARM, ARM64, PowerPC)에서도 fork() 함수를 호출하는 진입 경로는 같다는 것입니다.

      표준 함수 계층
      CPU 아키텍처에 무관한 표준 인터페이스 계층입니다. ARM32, ARM64, x86, x86_64 아키텍처에서 공용으로 처리하는 루틴입니다. 리눅스 저수준 프로그램은 실행하는 CPU에 무관하게 작성되어 있습니다. 리눅스 시스템이 구동하는 아키텍처에 독립적인 코드입니다.
      리눅스 시스템 프로그램은 어떤 CPU 아키텍처에서도 구동할 수 있게 인터페이스를 구성해야 합니다.

      아키텍처 의존적 코드
      마지막 계층은 GNU C 라이브러리 가장 하단에 있는 아키텍처에 의존적인 코드입니다. 리눅스 저수준 프로그램을 실행하는 환경에 따라 아키텍처에 의존적인 코드는 바뀝니다. 라즈베리파이는 ARM32(Aarch32) 아키텍처이니 이 ARM32에 맞는 어셈블리 코드로 구성되어 있습니다. 만약 최신 안드로이드 디바이스의 경우 ARM64(Aarch64) 어셈블리 코드로 아키텍처 의존적인 코드는 구성되어 있습니다.

      이 계층에서 Supervisor 명령어를 실행해서 소프트웨어 인터럽트를 발생하는 코드를 분석하겠습니다.


      #Reference 시스템 콜


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

      .


      [라즈베리파이] 시스템 콜 테이블이란 [라즈베리파이] 시스템 콜

      시스템 콜 테이블을 알아보기 전 POSIX 규약에 대해서 점검합시다.
      POSIX(Portable Operating System Interface)란 응용 어플리케이션이 다양한 유닉스 계열 운영체제에서 호환성을 유지하면서 실행할 수 있게 만든 표준 규약입니다. 대표적인 POSIX 규약은 리눅스 저수준 함수, 시스템 콜 테이블, 프로세스 통신용으로 쓰는 시그널 번호입니다. 응용 어플리케이션은 각자 알고리즘과 구현 시나리오에 집중하고 리눅스 저수준 함수 세부 구현 내역과 리눅스 커널 동작에 신경을 쓸 필요가 없습니다.

      리눅스 POSIX 표준으로 시스템콜 번호와 핸들러는 지정되어 있습니다. 시스템콜 테이블은 시스템콜 번호를 인덱스로 시스템 콜 핸들러를 분기시키는 역할을 수행합니다.

      시스템 콜 테이블은 시스템 콜 번호와 시스템 콜 핸들러 함수 주소로 구성되어 있습니다.
      번호 시스템 콜 핸들러 함수 위치
      1 sys_exit kernel/exit.c
      2 sys_fork kernel/fork.c
      3 sys_read fs/read_write.c
      4 sys_write fs/read_write.c
      5 sys_open fs/open.c
      6 sys_close fs/open.c
      7 compat_sys_epoll_pwait kernel/sys_ni.c
      8 sys_creat fs/open.c
      9 sys_link fs/namei.c
      10 sys_unlink fs/namei.c
      11 sys_execve fs/exec.c
      12 sys_chdir fs/open.c

      위와 같은 시스템 콜 번호와 시스템 콜 핸들러 함수 목록은 POSIX 규약에 명시되어 있습니다. 따라서 리눅스 커널은 POSIX 규약에 명시된 시스템 콜 테이블에 따라 시스템 콜 세부 코드를 구현합니다.
      32비트 계열 아키텍처와 64비트 아키텍처별로 시스템 콜 테이블이 다릅니다. 위 테이블은 Aarch32, 32 비트 계열 ARM 아키텍처에서 처리하는 시스템 콜 테이블입니다. 참고로 라즈베리파이는 Aarch32, 32 비트 계열 ARM 아키텍처에서 구동합니다.

      유저 모드에서 R7 레지스터에 시스템 콜 번호를 지정한 다음 소프트웨어 인터럽트를 발생하고, 커널 모드로 vector_swi 벡터로 실행이 바뀌면 R7에 저장된 시스템콜 번호에 따라 시스템콜 번호를 분기시켜줍니다. 

      리눅스 커널에서는 어떤 전역 변수가 시스템콜 테이블일까요? sys_call_table 이란 전역 변수가 시스템콜 테이블입니다. Trace32로 본 시스템콜 테이블은 다음과 같습니다.
      d.v %y.l sys_call_table
      ________address|value_______|symbol
          NSD:80107FC4|0x8012D4E0   \\vmlinux\kernel/signal\sys_restart_syscall
      1   NSD:80107FC8|0x80121E08   \\vmlinux\exit\sys_exit
      2   NSD:80107FCC|0x8011C6D0   \\vmlinux\fork\sys_fork
      3   NSD:80107FD0|0x802844FC   \\vmlinux\read_write\sys_read
      4   NSD:80107FD4|0x8028459C   \\vmlinux\read_write\sys_write
      5   NSD:80107FD8|0x80281788   \\vmlinux\open\sys_open
      6   NSD:80107FDC|0x80280380   \\vmlinux\open\sys_close

      32비트 아키텍처에서는 4바이트 단위로 심볼에 주소를 처리합니다. 따라서 함수 주소 길이는 4바이트입니다. 4바이트 단위로 시스템 콜 핸들러 함수 주소를 sys_call_table 주소에 저장하는 것입니다.

      가장 왼쪽에 보이는 숫자가 시스템콜 번호이고 가장 왼쪽에 보이는 함수 이름이 시스템 콜 핸들러입니다. 시스템콜 번호 3번에 해당하는 시스템콜 핸들러는 sys_read() 함수 4번에 대응하는 핸들러는 sys_write() 함수입니다.

      리눅스 저수준 함수 이름이 abc이면 이에 대응하는 시스템 콜 핸들러 함수 이름은 대부분 sys_abc() 입니다. 이 기준으로 유저 공간에서 read() 함수를 호출하면 소프트웨어 인터럽트가 발생한 후 시스템 콜 테이블을 통해 sys_read() 함수로 분기합니다.

      다음에 유저 영역에서 시스템 콜 실행 직전 어떤 흐름으로 시스템 콜 번호를 지정하는지 어셈블리 코드 분석으로 상세히 살펴봅니다.

      #Reference 시스템 콜


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

      .

      한국 개발업체에서 절대 리눅스 전문가가 나올 수 없는 이유(2: 쓰레기 관리자들) 에세이


      2. 쓰레기 관리자들
      운이 좋게 개발 능력을 키워가는 고참 개발자가 보이면 한국 개발 업체 조직 책임자들은 투명 완장을 채워준다.
      개발과 관리를 동시에 하라는 것이다.

      개발자가 조직 책임자에게 개발만 집중하고 싶다라고 말하면 조직 책임자는 업무의 폭이 좁다라고 말한다.
      업무의 폭이라? 고참 개발자가 되면 개발과 관리를 동시에 진행해서 프로젝트를 리딩해야 한다는 소리다.

      그럼 고참 개발자에게 관리를 시키는 이유가 뭘까? 그 이유는 간단한다.
      고참 개발자에게 관리 업무를 시키면 그 위 조직 책임자는 편하게 일할 수 있기 때문이다.
      고참 개발자가 관리를 하면 우선 자신은 최신 기술에 대해 파악할 필요가 없다.

      고참 개발자에게 투명 완장을 채워 준 다음 그 고참 개발자가 차려 주는 보고만 받아 먹으면 되기 때문이다.
      얼마나 편한가? 고참 개발자가 차려 주는 밥상을 조직 책임자는 받아 쳐 먹기만 하면 되니까 말이다.

      개발자들과 그 어려운 용어를 섞으면서 대화를 할 필요도 없고, 혹시 내가 기술을 잘 이해 못해서 이런 저런 질문을 하면 "개발자들은 정신 나간 관리자라고 얼마나 욕을 할까?" 란 생각도 할 필요가 없다.

      그런데 개발과 관리를 동시에 해내야 한다? 과연 이런 개발자가 존재할까? 
      정말 이 세상에 개발과 관리를 동시에 잘 해내는 사람이 정말 있을까?

      개발과 관리를 동시에 해내지 못하면 능력이 떨어진다고 봐야 할까?
      이건 미친 개소리다. 이런 생각을 하는 관리자는 개발도 관리도 전혀 모를 가능성이 높을 인간들이다.

      글로벌 소프트웨어 업체에서는 개발과 관리 트랙을 따로 구분해서 개발자와 관리자들을 육성한다.

      개발이나 관리도 전문 분야이기 때문에 서로 집중해야 하는 업무 스킬과 능력이 다른 것이다.
      소프트웨어 프로젝트 관리가 얼마나 많은 전문적인 스킬을 요구하는 힘든 전문 업무인가?

      위에 있는 관리자들에게 쳐 맞고 개발자들을 잘 달래가면서 프로젝트를 잘 이끌어 나가야하는 엄청난 인내심과 끈기를 요구하는 일이다. 그리고 고객사와 관계, 커뮤니케이션, 업무 갈등이 생겼을 때 최적의 솔류션을 찾아 내는 능력..

      그리고 기술 변화는 얼마나 더 심한가?  개발자가 관리자와 대화를 할 때 관리자가 기술에 대한 이해력이 떨어져 보고 내용을 못 알아들으면 대부분 개무시를 한다. 그래서 관리자도 끊임없이 공부를 해서 뒤쳐지지 않으면 안된다.
      서과장: 김부장님, 이 문제는 시스템 콜을 처리하는 vfs_write() 함수에서 메모리를 GFP_KERNEL 옵션으로 줬기 때문에 발생하는 것입니다.

      김부장: 시스템 콜이라고? 와이 파이 콜은 잘 되는데 왜 시스템 콜이 문제라는 거지?

      서과장: (황당...) ... (표정 관리)

      그런데 고참 개발자에게 관리 업무를 시키면 위 조직 책임자는 이런 상황을 면할 수 있다.
      고참 개발자가 관리를 하면 우선 자신(조직 책임자)은 최신 기술에 대해 파악할 필요가 없기 때문이다.

      고참 개발자에게 투명 완장을 채워 준 다음 그 고참 개발자가 차려 주는 보고를 받아 먹으면 되기 때문이다.
      얼마나 편한가?

      그럼 고참 개발자는 개발과 관리를 동시에 진행할 수 있을까?

      우선, 대부분 관리와 개발을 동시에 진행하는 고참 개발자는 대부분 많은 스트레스를 받기 마련이다.
      로그나 코드를 볼 시간이 현저히 줄어 들기 때문이다.

      프로젝트 관리를 하다 보면 얼마나 전화가 많이 오나? 고참 개발자가 코드를 보다가 전화가 와도 잘 받아야 한다.
      관리자 업무 중 전화 통화가 중요한 비중을 차지하기 때문이다.

      따라서 집중하면서 코드나 로그를 볼 수 있는 시간이 현저히 줄어든다. 

      그리고 첨예한 의견 대립으로 진행되는 회의에 들어갔다 오면 바로 집중해서 코드나 로그를 볼 수 있나? 관리와 개발 업무를 할 때 쓰는 두뇌 자체가 다르기 때문에 이런 상황에서 바로 집중하기 매우 어렵다.

      또한, 관리를 한다고 해도 기존에 개발했던 개발 경험치를 활용해서 문제를 짐작하면서 방향을 찾는 경우가 많다.
      고참 개발자가 최근 까지 개발에 많은 시간을 투자했다고 이런 경험치는 잘 들어맞을 가능성이 높다.

      하지만 시간이 흘러 코드나 로그를 볼 수 있는 시간이 줄어들면 결국 개발 능력을 떨어지고, 도태될 가능성이 매우 높아진다.

      만약 고참 개발자가 개발과 관리를 동시에 진행하다가 개발 능력이 떨어지면 아예 관리로 들어서는 경우가 많다.

      이런 상태 고참 개발자들을 위 조직 책임자는 기가 막히게 알아 차린다.
      이런 고참 개발자가 개발이 아닌 관리를 본격적으로 하려고 하면 조직 책임자는 크게 2가지 태도로 분류할 수 있다.
      1. 양심이 있는 조직 책임자들은 이런 고참 개발자들을 적절히 자신의 라인에 끼워서 관리 트랙에 올라 타도록 이끌어 준다. 이 정도 되는 조직 책임자들을 누가 욕하나?

      2. 그냥 쓰다가 버린다. 음식물 쓰레기를 버리거나 변기통에 똥이 막혀 있으면 뚫어야 하는 누군가는 해야 하나 정말 짜증하는 관리만 맡긴다. 개발 능력이 떨어져 퇴물이 된 고참 개발자는 방법이 있나? 이런 일이라도 해야지.

      이런 과정을 반복하는 고참 개발자들은 체력도 떨어져 몸도 망가지고 회사에서는 이용만 당하는 처량한 신세가 될 가능성이 높다.

      이런 상황에서 전문 개발자가 될 수 있을까?


      .

      한국 개발업체에서 절대 리눅스 전문가가 될 수 없는 이유(1) - SW문화 에세이

      임베디드 리눅스 개발 업체에서 실제 있었던 일이다.

      업무 시간에 있었던 일이다.
      현재 조직 책임자가 업무 시간에 리눅스 커널 소스 코드를 보지 말라고 했다.
      리눅스 커널 소스 코드는 다 알아서 스스로 보는 건데 뭘 그런 걸 보냐는 소리인 것이다.

      이게 임베디드 리눅스 프로젝트를 개발하는 부서장이 하는 소리다.
      평소에 이 임베디드 리눅스 업체 관리자가 뇌깔리는 소리는 다음과 같다.
      1. 리눅스 커널은 안정화된 코드이기 때문이 다 가져다 쓴다.
      그러니 리눅스 커널을 보드에 잘 돌리는 기술만 익히면 된다. 이게 최근 임베디드 리눅스 개발의 추세다.

      2. 리눅스 커널은 디바이스 드라이버가 지나다니는 통로일 뿐이다.
      그 제반 기술을 익혀서 뭘하나? 문제가 나오지도 않는데 말이야.

      3. 빌드 스크립트나 컴파일 환경을 잘 익혀서 다른 보드에 리눅스를 빨리 올리는 기술을 습득해라.

      만약 리눅스 프로젝트 개발 도중 문제(커널 크래시, 스톨)가 발생하면 어떻게 하나?

      커널 로그를 보고 리눅스 메일링 리스트에서 유사한 패치를 적용하라고 한다.
      물론 깊이 있는 문제 분석을 하지는 않는다.

      커널 크래시가 발생했을 때 커널 로그만 보고 여러 가지 소설을 쓴다.
      이건 메모리 불량일꺼야?
      갑자기 전원에 문제가 생겼을 것이야?

      물론 아무런 근거도 없다. 크래시 유틸리티나 T32와 같은 크래시 디버깅 툴을 쓸 생각 조차 하지 않는다.

      문제가 발생하면 깊이 있는 분석을 할 능력이 없다.
      그래도 문제가 발생했으니 안심 패치를 적용해야 고객사에게 그래도 정신적인 평온감을 주지 않을까?
      그래서 리눅스 커널 패치 아무거나 반영하고 문제를 해결했다고 거짓말을 한다.

      이런 방식으로 개발을 하는 이유가 뭘까?
      임베디드 리눅스 개발 프로젝트 자체가 대부분 도전적이지 않고, 유지 보수만 하는 수준이기 때문에 커널 크래시나 스톨과 같은 문제가 거의 나오지 않는다. 나온다고 해도 이런 방식으로 문제를 접근한다.

      개발 문화
      대부분 한국의 소프트웨어 개발 업체 문화가 이렇다.
      "잘 되어 있는 소프트웨어를 가져다 쓴다."

      소프트웨어에 대한 원천 기술 연구 개발 의지가 거세돼 있다.

      그냥 잘 돌아가는 소프트웨어를 잘 가져다가 쓰면 된다란 수준에 머물러 있다.
      디바이스 트리나 디바이스 드라이버 소스 트리만 바꾸어가면서 보드에 리눅스 커널을 올리는 수준의 역할만 반복하는 것이다.

      한국 임베디드 리눅스 개발 업체에서 리눅스 커널 코드를 깊히 있게 들여다 보는 개발자가 얼마나 있을까? 내가 리눅스 커뮤니티에서 만나본 많은 개발자들과 대화를 통해서 알아본 바로는 거의 없다고 봐야 한다. 
      이런 나의 예상이 틀리길 바란다.
      그래도 난 내 예상보다 전문성을 키워나가면서 제대로 임베디드 리눅스를 개발하는 개발자가 많기를 바란다.

      이런 방식으로 오랫동안 개발 경력을 쌓으면 필드에서 임베디드 리눅스 전문가라고 인정 받을 수 있을까?
      난 절대 아니라고 본다. 이들은 임베디드 리눅스 개발자가 아니라 리눅스 코드 몽키라고 부르고 싶다.

      돈 몇 만원 주면 메뉴얼대로 코드를 짜는 코드 몽키와 같이 이런 저런 보드에 리눅스만 올려대는 리눅스 코드 몽키지 않는가?

      To be continued...



      [라즈베리파이] 시스템 콜 - 소프트웨어 인터럽트란 [라즈베리파이] 시스템 콜

      소프트웨어 인터럽트를 한 문장으로 정의해 봅시다. 
      소프트웨어 인터럽트는 인터럽트가 아닙니다. 인터럽트는 하드웨어서 올려주는 전기 신호로 언제 발생할지 모르는 비동기적인 이벤트이나 통지입니다.

      그런데 소프트웨어 인터럽트는 ARM 프로세서에서 제공하는 “svc” 어셈블리 명령어를 실행하면 동작합니다. 소프트웨어 인터럽트를 발생하는 주체는 소프트웨어 관점으로 프로세스입니다. 소프트웨어 인터럽트란 용어의 인터럽트는 하드웨어 디바이스에서 비동기적으로 전달하는 신호는 아닙니다.

      실제 인터럽트가 발생하면 ARM 프로세서는 인터럽트를 익셉션의 한 종류로 처리합니다. 리눅스 커널에서 다음은 같이 동작합니다.
      1. 익셉션 발생(인터럽트는 비동기적인 신호)
      2. 익셉션 벡터로 ARM 프로그램 카운터를 이동
      3. 익셉션 벡터에서 기존에 실행 중인 레지스터 세트를 스택 공간에 저장
      4. 익셉션 종류에 따른 서브 루틴으로 분기
      5. 익셉션 처리를 마무리한 후 익셉션 서브 루틴을 실행한 주소로 복귀
      6. 스택에 푸시한 레지스터를 ARM 레지스터 세트에 로딩해서 익셉션 발생 전 실행했던 주소로 이동

      소프트웨어 인터럽트는 인터럽트가 아니라고 했습니다.
      대신 소프트웨어 인터럽트는 ARM에서 지원하는 어셈블리 코드 "svc" 명령어를 명시적으로 실행해서 익셉션을 유발하는 동작입니다. 따라서 소프트웨어 인터럽트는 비동기적인 이벤트는 아닙니다. 

      위에서 언급한 인터럽트 익셉션이 발생했을시 6가지 동작 과정에서 익셉션이란 단어를 소프트웨어 인터럽트란 단어로 바꿔 봅시다.
      1. 유저 모드에서 svc "0x00000000" 명령어 실행으로 커널 코드 진입
      2. 소프트웨어 인터럽트 벡터로 ARM 프로그램 카운터를 이동
      3. 소프트웨어 인터럽트 벡터에서 기존에 실행 중인 레지스터 세트를 스택 공간에 저장
      4. 소프트웨어 인터럽트 종류에 따른 서브 루틴으로 분기
      5. 소프트웨어 인터럽트 처리를 마무리한 후 소프트웨어 인터럽트 서브 루틴을 실행한 주소로 복귀
      6. 소프트웨어 인터럽트에 푸시한 레지스터를 ARM 레지스터 세트에 로딩해서 소프트웨어 인터럽트 전 실행했던 주소로 이동(유저 모드 복귀)

      소프트웨어 인터럽트를 유발하는 소스가 다른 것이지 ARM 프로세서에서 인터럽트 벡터를 실행해서 인터럽트를 처리하는 방식은 같습니다.

      ARM 프로세서 인터럽트 벡터가 __irq_svc 인 것과 마찬가지로 소프트웨어 인터럽트 벡터는 vector_swi 입니다.
      즉 다음과 같은 과정으로 실행 흐름이 변경되는 것입니다.

      위 동작은 순수히 ARM 프로세서 익셉션 관점으로 설명을 한 것입니다.

      ARM 프로세서 입장에서 지금 실행 중인 운영체제가 리눅스인지 모릅니다. 따라서 소프트웨어 인터럽트를 발생한 다음 시스템 콜 테이블로 분기하는 동작을 합니다. 이는 리눅스 시스템에서 시스템 콜을 POSIX에 따라 구현한 것입니다.

      #Reference 시스템 콜


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

      .

      [안드로이드][리눅스커널] 시그널 - 유저 공간 abort(SIGABRT) 시 동작 시 흐름 [Linux][ARM] Core Analysis

      유저 공간에서 zygote가 강제 종료되면서 부팅을 못하는 상황입니다. 커널 로그로 아래 메시지를 볼 수 있습니다.
      아래 로그는 init 프로세스가 zygote에 SIGABRT(6) 시그널을 전달해서 zygote를 종료시키고 있습니다.
      [   46.116831 / 01-02 01:20:24.859][0] init: Service 'zygote' (pid 1777) killed by signal 6
      [   46.124107 / 01-02 01:20:24.869][1] init: Service 'zygote' (pid 1777) killing any children in process group

      그럼 이 동작을 할 때 커널 관점으로 어떤 코드가 수행되는지 살펴보겠습니다.

      zygote는 커널 공간에서 "main" 이란 쓰레드로 수행됩니다. 그래서 아래 코드를 눈여겨 보면 __send_signal()/send_sigqueue 함수에서 전달하는 struct task_struct *t 가 시그널 전달 받는 프로세스입니다. 이 프로세스의 comm이란 멤버가 프로세스 이름을 담고 있습니다.

      참고로 아래와 같이 ftrace log를 설정하고, 강제로 zygote를 종료하는 테스트를 수행했습니다.
      adb shell "echo 1 > /d/tracing/events/sched/sched_process_exit/enable"
      adb shell "echo 1 > /d/tracing/events/sched/sched_process_fork/enable"

      adb shell "echo 1 > /d/tracing/events/signal/enable"

      kill -6 918, 즉 kill -6 [pid]로 자이고트에 SIGABRT 시그널을 전달하는 것입니다.
      root      918   1     1111816 81936 poll_sched b2ae1ec4 S zygote
      austin:/ # kill -6 918

      그럼 다음과 같은 ftrace 로그를 볼 수 있습니다. main이란 프로세스가 sig 6 SIGABT를 맞고 결국 종료됩니다.
      <...>-456   [005] d..2  1186.470605: signal_generate: sig=6 errno=0 code=-6 comm=main pid=918 grp=0 res=0
      <...>-918   [005] d..2  1186.470786: signal_deliver: sig=6 errno=0 code=-6 sa_handler=0 sa_flags=14000000
      <...>-6202  [007] d..2  1186.470881: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
      <...>-6205  [005] d..2  1186.470895: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
      <...>-6203  [007] d..2  1186.470919: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
      <...>-6204  [005] d..2  1186.470969: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
      <...>-6203  [007] ...1  1186.471086: sched_process_exit: comm=FinalizerDaemon pid=6203 prio=120
      <...>-918   [005] ...1  1186.471092: sched_process_exit: comm=main pid=918 prio=120
      <...>-6204  [005] ...1  1186.471780: sched_process_exit: comm=FinalizerWatchd pid=6204 prio=120
      <...>-6202  [007] ...1  1186.471798: sched_process_exit: comm=ReferenceQueueD pid=6202 prio=120

      그래서 프로세스 이름이 main일 때 강제 커널 패닉을 유발하는 코드를 작성했습니다.
      diff --git a/kernel/signal.c b/kernel/signal.c
      index 7408330..3157014 100644
      --- a/kernel/signal.c
      +++ b/kernel/signal.c
      @@ -1031,6 +1031,12 @@ static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
              assert_spin_locked(&t->sighand->siglock);

              result = TRACE_SIGNAL_IGNORED;
      +       if (!strcmp(t->comm, "main")) {
      +               show_stack(NULL, NULL);
      +               printk("[+][Pompeii] proc %s is ignored \n", t->comm);
      +               BUG();
      +       }
      +
              if (!prepare_signal(sig, t,
                              from_ancestor_ns || (info == SEND_SIG_FORCED)))
                      goto ret;
      @@ -1120,6 +1126,7 @@ out_set:
              sigaddset(&pending->signal, sig);
              complete_signal(sig, t, group);
       ret:
      +       printk("[+][Pompeii] proc %s sig: %d \n", t->comm, sig);
              trace_signal_generate(sig, info, t, group, result);
              return ret;
       }
      @@ -1568,6 +1575,15 @@ int send_sigqueue(struct sigqueue *q, struct task_struct *t, int group)
              unsigned long flags;
              int ret, result;

      +
      +       if (!strcmp(t->comm, "main")) {
      +               show_stack(NULL, NULL);
      +               printk("[+][Pompeii] proc %s is ignored \n", t->comm);
      +               show_stack(NULL, NULL);
      +
      +               BUG();
      +       }
      +

      위 코드를 반영하고 타겟 디바이스에 다운로드를 했습니다. 역시 예상대로 부팅 후 20초 정도 시간이 지나자 커널 패닉이 발생했습니다.

      유저 영역 콜스택까지 확인하니 아래 함수 흐름으로 __send_signal 함수가 호출됩니다.
      참고로 심볼 정보가 포함된 libc.so 파일은 symbols\system\lib64에 위치한 libc.so로 로딩해야 한다는 점을 잊지 마세요.
      -000|do_undefinstr(regs = 0xFFFFFFC001714630)
      -001|__send_signal.constprop.27(sig = 1865235760, info = 0xFFFFFFC06F2D3E40, t = 0xFFFFFFC0000AE488, grou
      -002|send_signal(inline)
      -002|do_send_sig_info(sig = 6, info = 0xFFFFFFC06F2D3E40, p = 0xFFFFFFC06F2C8000, ?)
      -003|do_send_specific(tgid = 748, ?, sig = 6, info = 0xFFFFFFC06F2D3E40)
      -004|do_tkill(tgid = 748, pid = 748, sig = 6)
      -005|SYSC_tgkill(inline)
      -005|sys_tgkill(?, ?, ?)
      -006|el0_svc_naked(asm)
       -->|exception
      -007|tgkill(asm)
      -008|pthread_kill(?, sig = 6)
      -009|raise(sig = 748)
      -010|abort()
      -011|android_log_write_string8_len(?, ?, ?)

      유저 공간에서 어떤 일이 있었는지 확인해보니 아래 흐름으로 abort가 발생했습니다.
      abort -> raise -> pthread_kill -> tgkill

      정리하면, 유저 영역에서 abort가 발생하면 커널 영역에 signal 6를 전달함을 알 수 있죠.

      그런데 dump state 로그를 확인하면 아래 콜스택을 볼 수 있습니다.
      /system/lib64/libc.so (tgkill+8)
      /system/lib64/libc.so (pthread_kill+64)
      /system/lib64/libc.so (raise+24)
      /system/lib64/libc.so (abort+52)
      /system/lib64/libart.so art::Runtime::Abort()                                                                          /system/lib64/libart.so art::LogMessage::~LogMessage()                                                                  /system/lib64/libart.so art::Thread::AssertNoPendingException()    
      /system/lib64/libart.so art::ClassLinker::FindClass(art::Thread*, char const*, art::Handle<art::mirror::ClassLoader>)    /system/lib64/libart.so art::ClassLinker::FindArrayClass(art::Thread*, art::mirror::Class**)                                  /system/lib64/libart.so art::JNI::NewObjectArray(_JNIEnv*, int, _jclass*, _jobject*)
      /system/lib64/libandroid_runtime.so                                                                                              /system/framework/arm64/boot-framework.oat (offset 0x1663000) (android.hardware.location.ContextHubService.nativeInitialize+124)                 
      /system/framework/arm64/boot-framework.oat (offset 0x1663000) (android.hardware.location.ContextHubService.<init>+300)                            
      /system/framework/oat/arm64/services.odex (offset 0xd1b000)

      C++ 코드에서 클래스 메쏘드로 구현된 함수들은 심볼들이 이상하게 보입니다.
      아래 폴더에 있는 바이너리 유틸리티 프로그램을 쓰면 원래 심볼로 보여줍니다. c++filt 옵션을 기억합시다. 
      android/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel-nm              ./aarch64-linux-androidkernel-nm libart.so | c++filt > sym_libart.c

      참고로 dump state 원래 로그는 아래와 같습니다. 알 수 없는 _ZN3art7Runtime5AbortEv 심볼이 보이죠.
      01-03 00:37:24.599  1721  1721 F DEBUG   : backtrace:
      01-03 00:37:24.599  1721  1721 F DEBUG   :     #00 pc 000000000006b8c8  /system/lib64/libc.so (tgkill+8)
      01-03 00:37:24.599  1721  1721 F DEBUG   :     #01 pc 0000000000068d4c  /system/lib64/libc.so (pthread_kill+64)
      01-03 00:37:24.599  1721  1721 F DEBUG   :     #02 pc 00000000000242b8  /system/lib64/libc.so (raise+24)
      01-03 00:37:24.599  1721  1721 F DEBUG   :     #03 pc 000000000001ccd4  /system/lib64/libc.so (abort+52)
      01-03 00:37:24.599  1721  1721 F DEBUG   :     #04 pc 000000000042c5d0  /system/lib64/libart.so (_ZN3art7Runtime5AbortEv+352)
      01-03 00:37:24.599  1721  1721 F DEBUG   :     #05 pc 00000000000e4b24  /system/lib64/libart.so (_ZN3art10LogMessageD2Ev+1204)
      01-03 00:37:24.599  1721  1721 F DEBUG   :     #06 pc 000000000044eaf4  /system/lib64/libart.so (_ZNK3art6Thread24AssertNoPendingExceptionEv+836)
      01-03 00:37:24.599  1721  1721 F DEBUG   :     #07 pc 0000000000123ec8  /system/lib64/libart.so (_ZN3art11ClassLinker9FindClassEPNS_6ThreadEPKcNS_6HandleINS_6mirror11ClassLoaderEEE+68)
      01-03 00:37:24.599  1721  1721 F DEBUG   :     #08 pc 000000000011f170  /system/lib64/libart.so (_ZN3art11ClassLinker14FindArrayClassEPNS_6ThreadEPPNS_6mirror5ClassE+744)
      01-03 00:37:24.600  1721  1721 F DEBUG   :     #09 pc 0000000000348c3c  /system/lib64/libart.so (_ZN3art3JNI14NewObjectArrayEP7_JNIEnviP7_jclassP8_jobject+652)
      01-03 00:37:24.600  1721  1721 F DEBUG   :     #10 pc 0000000000142254  /system/lib64/libandroid_runtime.so
      01-03 00:37:24.600  1721  1721 F DEBUG   :     #11 pc 0000000001baa5d0  /system/framework/arm64/boot-framework.oat (offset 0x1663000) (android.hardware.location.ContextHubService.nativeInitialize+124)
      01-03 00:37:24.600  1721  1721 F DEBUG   :     #12 pc 0000000001ba9e80  /system/framework/arm64/boot-framework.oat (offset 0x1663000) (android.hardware.location.ContextHubService.<init>+300)
      01-03 00:37:24.600  1721  1721 F DEBUG   :     #13 pc 0000000000dc8474  /system/framework/oat/arm64/services.odex (offset 0xd1b000)

      정리하면 유저 공간에서 Abort가 떨어지면 abort() -> raise() -> pthread_kill() -> tgkill() 흐름으로 sys_tgkill란 시스템 콜이 호출되어 커널 공간에서는 __send_signal 함수가 호출됩니다.

      위 동작은 모두 Aarch64 아키텍처 환경에서 설명을 드린 건데요. Aarch32 아키텍처는 약간 동작이 다릅니다.
      -000|__do_user_fault(tsk = 0x46, addr = 0, fsr = 5, sig = 11, code = 0, regs = 0x0)
      -001|__do_kernel_fault(inline)
      -001|do_page_fault(addr = 3883189760, fsr = 5, regs = 0xE5BEFFB0)
      -002|do_translation_fault(?, fsr = 3854499232, ?)
      -003|do_DataAbort(addr = 0, fsr = 5, regs = 0xE5BEFFB0)
      -004|__dabt_usr(asm)

      arm32 비트 아키텍처에선 유저 영역에서 어보트가 떨어지면 __dabt_usr란 익셉션 벡터가 실행되고, (물론 유저 공간에서 수행 중인 레지스터를 스택에 푸시합니다.)
      NSR:C0F733E0|E24DD048  __dabt_usr:       sub     r13,r13,#0x48    ; r13,r13,#72
      NSR:C0F733E4|E98D1FFE                    stmib   r13,{r1-r12}
      NSR:C0F733E8|EE117F10                    mrc     p15,0x0,r7,c1,c0,0x0   ; p15,0,r7,c1,c0,0 (system control)
      NSR:C0F733EC|E51F80B4                    ldr     r8,0xC0F73340
      NSR:C0F733F0|E8900038                    ldm     r0,{r3-r5}
      NSR:C0F733F4|E28D003C                    add     r0,r13,#0x3C     ; r0,r13,#60
      NSR:C0F733F8|E3E06000                    mvn     r6,#0x0          ; r6,#0
      NSR:C0F733FC|E58D3000                    str     r3,[r13]
      NSR:C0F73400|E5988000                    ldr     r8,[r8]
      NSR:C0F73404|E8800070                    stm     r0,{r4-r6}
      NSR:C0F73408|E9406000                    stmdb   r0,{r13-r14}^
      NSR:C0F7340C|E1380007                    teq     r8,r7
      NSR:C0F73410|1E018F10                    mcrne   p15,0x0,r8,c1,c0,0x0   ; p15,0,r8,c1,c0,0 (system control)
      NSR:C0F73414|E3A0B000                    mov     r11,#0x0         ; r11,#0
      NSR:C0F73418|EBC5BDA8                    bl      0xC00E2AC0       ; trace_hardirqs_off
      NSR:C0F7341C|E1A0200D                    cpy     r2,r13
      NSR:C0F73420|EBC2BC6E                    bl      0xC00225E0       ; v7_early_abort

      v7_early_abort란 레이블에서 MMU의 fault register를 r1 레지스터로 읽습니다. 
      NSR:C00225E0|EE151F10  v7_early_abort:     mrc     p15,0x0,r1,c5,c0,0x0   ; p15,0,r1,c5,c0,0 (data fault status)
      NSR:C00225E4|EE160F10                      mrc     p15,0x0,r0,c6,c0,0x0   ; p15,0,r0,c6,c0,0 (data fault address)
      NSR:C00225E8|EAFF97CF                      b       0xC000852C       ; do_DataAbort

      위 코드에서 "mrc     p15,0x0,r1,c5,c0,0x0" 명령어를 잘 기억하세요.

      결국 MMU fault register에서 읽은 값에 따라  fsr_info[5] 전역변수에 이미 정의된 do_translation_fault 함수를 호출하는군요.
        (static struct fsr_info [32]) fsr_info = (
          [0] = (
            (int (*)()) fn = 0xC001F58C = do_bad,
            (int) sig = 11,
            (int) code = 0,
            (char *) name = 0xC13F1AD6 = kallsyms_token_index+0x4FC6),
          [1] = (
            (int (*)()) fn = 0xC0021628 = do_alignment,
            (int) sig = 7,
            (int) code = 196609,
            (char *) name = 0xC13F1AE7 = kallsyms_token_index+0x4FD7),
          [2] = (
            (int (*)()) fn = 0xC00184B4 = hw_breakpoint_pending,
            (int) sig = 5,
            (int) code = 196612,
            (char *) name = 0xC13F0451 = kallsyms_token_index+0x3941),
          [3] = (
            (int (*)()) fn = 0xC001F58C = do_bad,
            (int) sig = 11,
            (int) code = 196609,
            (char *) name = 0xC13F17F3 = kallsyms_token_index+0x4CE3),
          [4] = (
            (int (*)()) fn = 0xC0F7528C = do_translation_fault,
            (int) sig = 11,
            (int) code = 196609,
            (char *) name = 0xC13F17D9 = kallsyms_token_index+0x4CC9),
          [5] = (
            (int (*)()) fn = 0xC0F7528C = do_translation_fault,
            (int) sig = 11,
            (int) code = 196609,
            (char *) name = 0xC13F193F = kallsyms_token_index+0x4E2F), 

          [6] = ((int (*)()) fn = 0xC001F58C = do_bad, (int) sig = 11, (int) code = 196609, (char *) name = 0xC13F17F3 = kallsyms_token_index+0x4CE3),
          [7] = ((int (*)()) fn = 0xC0F74E4C = do_page_fault, (int) sig = 11, (int) code = 196609, (char *) name = 0xC13F1970 = kallsyms_token_index+0x4E60),

      .

      [라즈베리파이] 시스템 콜 - ARM 프로세서 관점 시스템 콜 처리 [라즈베리파이] 시스템 콜

      리눅스 시스템에서 시스템 콜을 어떻게 구현했는지 코드를 따라가면 어셈블리 코드를 만나게 됩니다.
      보통 어셈블리 코드는 ARM 프로세서 입장에서 실행하는 동작을 구현하는 경우가 많습니다. 어셈블리 코드로 구현되어 있다는 것은 시스템 콜이 아키텍처(ARM, x86) 동작과 연관이 있다는 의미입니다.
       
      라즈베리파이는 ARM 아키텍처에서 구동하므로 ARM(Aarch32, ARM 32비트) 프로세서 기준으로 시스템콜이 어떻게 동작하는지 알아봅시다.

      ARM 프로세서 입장에서 시스템콜 동작을 더 자세히 알아보려면 ARM 프로세스 모드에 대해 살펴볼 필요가 있습니다. ARM 프로세서는 Supervisor, FIQ, IRQ, ABORT, UNDEF, USER, 6가지 모드를 지원하며 각 모드 별 레지스터 세트를 저장하고 있습니다. 

      ARM 기반 리눅스 커널에서 리눅스 커널은 Supervisor 모드, 유저 어플리케이션은 USER 모드에서 실행합니다. 리눅스 커널에서 보통 커널 모드는 ARM의 Supervisor 모드, 유저 모드는 ARM의 USER 모드에 대응합니다.

      ARM 프로세서에서 시스템콜에 대한 익셉션으로 소프트웨어 인터럽트로 정의했으며, 소프트웨어 인터럽트가 발생하면 다른 익셉션들과 마찬가지로 이미 정해진 주소인 vector_swi 레이블로 브랜치합니다.

      ARM 프로세서에서 각 모드를 전환하려면 익셉션을 발생시켜야 합니다.
      리눅스 커널의 유저 모드에서 커널 모드로 진입하려면 ARM 프로세서 기준으로 USER 모드에서 Supervisor 모드로 실행 모드 변환을 해야 합니다. 이를 위해 익셉션을 유발해야 하며 USER 모드에서 Supervisor(커널) 모드 진입을 위해 다음 Supervisor Call이란 어셈블리 명령어를 실행해야 합니다.

      ARM 프로세서 관점으로 소프트웨어 인터럽트를 발생하는 Supervisor Call 흐름도는 다음과 같습니다.
       
      ARM 아키텍처 관점으로 시스템 콜은 소프트웨어 인터럽트로 처리하며 익셉션의 한 종류로 간주합니다. ARM 프로세서에서 익셉션이 발생하면 이미 정해놓은 주소로 ARM 프로그램 카운터를 브랜치하고 정해진 동작을 합니다. 대표적인 익셉션으로 인터럽트를 예로 들 수 있습니다. 소프트웨어 인터럽트가 발생하면 다른 익셉션들과 마찬가지로 이미 정해진 주소인 vector_swi 레이블로 브랜치합니다.

      ARM 프로세서에서는 소프트웨어 인터럽트를 다음 2번째 줄과 같은 명령어로 실행합니다.
      1 0x76f01170 <__libc_fork+276>    mov    r7, #120        ; 0x78
      2 0x76f01174 <__libc_fork+280>    svc    0x00000000

      각 아키텍처별로 커널 모드에서 유저 모드로 변환시키는 방식이 다릅니다.

      ARM 프로세스 입장에서는 Supervisor Call 을 실행하면 모드 전환만 신경을 쓰지 이 동작이 시스템 콜로 어떤 동작을 하는지 모릅니다.
      수많은 운영체제들이 ARM 프로세서를 CPU로 쓰고 있습니다. 리눅스는 ARM 프로세서에서 탑재되는 수많은 운영체제 중 하나일 뿐입니다.

      ARM 프로세서 기반으로 구동하는 리눅스 커널은 Supervisor Call로 소프트웨어 인터럽트 발생 전 시스템 테이블 번호를 다음 그림과 같이 r7 레지스터에 저장합니다.
              
      USER 모드에서 ARM r7 레지스터에 POSIX 규약에서 정의한 시스템 콜 번호를 지정하고 Supervisor Call로 Supervisor 모드로 전환합니다. Supervisor 모드는 커널 코드가 실행하는 커널 공간임을 기억합시다. Supervisor(커널) 모드에서 vector_swi 레이블을 실행할 때 USER 모드에서 저장한 r7 레지스터를 읽습니다. 시스템 콜 테이블인 sys_call_table 변수 주소에 접근하여 r7에 저장된 시스템 콜 번호에 따라 시스템 콜 핸들러로 분기합니다.

      #Reference 시스템 콜


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

      .

      [라즈베리파이] 시스템 콜 - 전체 흐름도 소개 [라즈베리파이] 시스템 콜

      이전에 소개한 시스템 콜 흐름도와 시스템 콜 동작은 그리 간단하지 않습니다. 시스템 콜 세부 동작을 알려면 다음 시스템 전체 흐름도를 이해해야 합니다.

      다음 그림은 이번에 다룰 전체 시스템 콜 흐름도입니다.
       
      open(), write() 그리고 read() 함수는 파일을 열고 읽어서 쓰는 파일 입출력 동작이고, fork()와 exit() 함수는 프로세스 생성과 종료와 연관된 동작을 실행합니다. 이를 리눅스 저수준 함수라고 부릅니다. 다른 관점으로 GNU C 라이브러리로 진입하는 함수이며 이를 API(Application Programming Interface) 라고 부릅니다.

      리눅스 시스템에서는 390여 개의 표준 함수들이 있는데 위 그림에서 대표적인 함수 5개를 표현한 것입니다.

      라즈베리파이에서 다음 파일을 열어보면 시스템 콜 번호를 확인할 수 있습니다.
      [/usr/include/arm-linux-gnueabihf/asm/unistd.h]
      #define __NR_restart_syscall  (__NR_SYSCALL_BASE+  0)
      #define __NR_exit (__NR_SYSCALL_BASE+  1)
      #define __NR_fork (__NR_SYSCALL_BASE+  2)
      ...
      #define __NR_pkey_mprotect (__NR_SYSCALL_BASE+394)
      #define __NR_pkey_alloc (__NR_SYSCALL_BASE+395)
      #define __NR_pkey_free (__NR_SYSCALL_BASE+396)

      시스템 콜을 제대로 이해하려면 시스템 콜을 발생하는 유저 공간부터 시스템 콜을 실행하는 커널 공간 계층까지 전체 흐름도를 살펴볼 필요가 있습니다.

      시스템 콜 실행 흐름은 4단계로 나눌 수 있습니다.

      1 단계: 리눅스 저수준 표준 함수 호출
      유저 어플리케이션에서 파일시스템에 접근해서 파일을 열고 읽고 쓰려고 할 때 open(), write(), read() 함수를 호출해야 합니다. 혹은 프로세스를 생성하거나 종료할 때 fork() 나 exit() 함수를 호출합니다. 이 함수들은 API(Application Programming Interface)라고 말합니다. 유저 어플리케이션에서 리눅스 커널에서 제공하는 기능을 쓰기 위해 만든 인터페이스를 의미합니다. 이 인터페이스는 모두 리눅스 시스템에서 제공하는 GNU C 라이브러리 내부에 구현돼 있습니다.

      2 단계: 유저 공간에서 시스템 콜 실행
      리눅스 시스템 저수준 함수를 호출하면 리눅스 시스템에서 제공하는 GNU C 라이브러리 내 코드가 실행합니다. 라이브러리 내부 ARM 어셈블리 코드 실행으로 시스템 콜을 발생합니다. 이 과정을 제대로 이해하려면 ARM에서 시스템 콜을 어떻게 처리하는지 살펴볼 필요가 있습니다.

      3 단계: 커널 공간에서 시스템 콜 실행
      시스템 콜이 실행하면 커널 공간으로 이동해서 시스템 테이블에 접근한 후 각 리눅스 저수준 함수(API) 종류별로 대응하는 시스템 콜 핸들러 함수로 분기합니다. sys_open(), sys_write() 그리고 sys_read() 함수들은 가상 파일 시스템을 통해 파일 시스템에 접근합니다. sys_clone() 그리고 sys_exit() 함수들은 프로세스 생성과 종료와 연관된 커널 드라이버에 있는 계층에 접근합니다.
      시스템 콜 핸들러 함수는 리눅스 저수준 함수 앞에 sys_ 접두사가 붙는 경우가 대부분입니다. write() 함수는 sys_write() 함수, read() 함수는 sys_read() 함수에 대응합니다. 하지만 모든 시스템 콜 핸들러 함수가 이 규칙을 따르지는 않습니다. 리눅스 저수준 함수 fork()는 sys_clone() 시스템 콜 핸들러가 실행합니다.

      4단계: 커널 공간에서 시스템 콜 핸들러 실행
      시스템 콜 핸들러에서는 유저 공간에서 전달한 매개 인자에 오류를 점검 후 시스템 콜 종류에 따라 가상 파일 시스템 계층이나 프로세스 관리 함수에 접근합니다.

      시스템 콜 핸들러에서는 유저 공간에서 전달한 매개 인자에 오류를 점검 후 시스템 콜 종류에 따라 가상 파일 시스템 계층이나 프로세스 관리 함수에 접근합니다.

      여기까지 유저 공간에서 커널 공간까지 시스템 콜 처리 과정입니다.

      시스템 콜 인터페이스 동작을 더 정확하게 이해하려면 ARM 프로세스에서 시스템 콜을 어떻게 처리하는지 알아야 합니다. 이 내용은 다음에 다룹니다.

      #Reference 시스템 콜


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




      [라즈베리파이] 시스템 콜 소개 [라즈베리파이] 시스템 콜

      리눅스에서는 실행 공간을 메모리 접근과 실행 권한에 따라 유저 공간과 커널 공간으로 분류합니다. 

      먼저 커널 공간이 무엇인지 알아봅시다. 
      커널 코드가 실행할 때는 모든 커널 함수 호출이 가능하며 제약 없이 메모리 공간에 접근해서 하드웨어를 제어할 수 있습니다. 이런 시스템 상태와 메모리 접근을 커널 공간이라고 부릅니다. 

      다음은 유저 공간을 소개하겠습니다. 유저 어플리케이션 코드가 구동하는 동작과 상태를 유저 공간이라고 합니다. 유저 어플리케이션은 유저 공간에서 실행하며 메모리 공간 접근에 제한이 있고 하드웨어에 직접 접근할 수 없습니다. 

           
      유저 어플리케이션에서 권한이 없는 메모리 공간에 접근하면 커널은 오류를 감지해서 해당 프로세스를 종료시킵니다.

      시스템 콜은 누가 언제 실행할까요?
      시스템 콜은 유저 모드에서 실행 중인 어플리케이션에서 커널에게 어떤 서비스를 요청할 때 실행합니다. 유저 어플리케이션에서 파일 시스템에 접근해서 파일을 읽고 쓰거나 PID와 같은 프로세스 정보를 얻으려 할 때 주어진 규약에 맞게 커널에 서비스를 요청을 하는 것입니다. 이를 위해 시스템 콜을 발생해서 유저 공간에서 커널 공간으로 실행 흐름을 이동합니다.

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

      이번에 시스템 콜 세부 동작을 왜 잘 알아야 하는지 생각해봅시다.
      시스템 콜은 리눅스 시스템에서 당연히 잘 동작하는데 왜 알아야 할까요? 그 이유는 문제 해결 능력을 키우기 위해서입니다. 리눅스 시스템 저수준 함수를 써서 응용 어플리케이션 코드는 누구나 작성할 수 있습니다. 하지만 시스템 콜이 유저 공간에서 커널 공간까지 어떤 흐름으로 동작하는지 모르면 어디부터 문제 원인을 분석해야 할지 알 수 없습니다.

       시스템 콜이 어떤 흐름으로 동작하는지 잘 모르고 매뉴얼에 있는 내용만 참고해서 코드 작성하는 분보다 시스템 콜 전체 흐름을 제대로 이해한 분이 더 안정적인 코드를 작성할 가능성이 높습니다. 특정 리눅스 시스템 함수를 호출했는데 갑자기 에러 코드를 음수로 반환한다고 가정합시다. 시스템 인터페이스 구조를 알면 어느 코드부터 분석을 시작할지 판단할 수 있습니다.

      다음에 시스템 콜 전체 흐름도와 동작에 대해서 살펴보겠습니다.

      #Reference 시스템 콜


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


      [라즈베리파이] 시스템 콜(시스템 호출)은 왜 필요하나? [라즈베리파이] 시스템 콜

      시스템 콜은 유저 모드에서 커널 모드로 진입하는 동작입니다. 다른 관점으로 시스템 콜은 유저 공간과 커널 공간 사이 가상 계층이자 인터페이스라고 볼 수도 있습니다. 이 계층은 다음과 같은 특징이 있습니다.
      1. 시스템 안정성과 보안을 지킬 수 있습니다. 유저모드에서 어플리케이션이 커널 공간에 아무런 제약없이 접근한다고 가정합시다. 실수로 어플리케이션이 커널 코드 영역 메모리를 오염을 시키면 시스템은 오동작할 가능성이 높습니다.

      2. 유저 어플리케이션에서 추상화된 하드웨어 인터페이스를 제공합니다. 유저 모드에서 구동 중안 어플리케이션 입장에서 하나의 파일 시스템 위에서 구동 중인 것으로 착각하게 합니다.

      3. 시스템 콜 구현으로 유저 어플리케이션의 호환성과 이식성을 보장할 수 있습니다. 리눅스 시스템은 시스템 콜 인터페이스는 POSIX(Portable Operating System Interface) 이란 유닉스 표준 규약에 맞게 구현되어 있기 때문입니다. 이로 유저 어플리케이션 코드를 라즈베리파이, 안드로이드 등 리눅스 계열의 시스템과 유닉스 운영체제에서도 구동할 수 있습니다.

      4. 유저 공간에서 실행하는 어플리케이션에서 커널 공간으로 진입하는 인터페이스를 두고 커널과 독립적으로 구동합니다. 유저 어플리케이션 입장에서 파일 시스템과 프로세스 생성과 같은 내부 동작에 신경 쓸 필요가 없습니다.

      리눅스 디바이스 드라이버와 가상 파일 시스템 함수도 시스템 콜을 통해 시스템 콜 핸들러를 통해 관련 코드를 실행합니다.  

      또한 시스템 콜은 ARM 아키텍처와 연관이 깊은 동작입니다. ARM 프로세서는 시스템 콜을 익셉션의 한 종류인 소프트웨어 인터럽트로 실행하기 때문입니다. ARM 프로세스 관점으로 시스템 콜을 어떻게 처리하는지 알아볼 필요가 있습니다.

      #Reference 시스템 콜


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


      [라즈베리파이] 프로세스 - 커널 스레드 종류 [라즈베리파이] 커널 프로세스

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

      출력 결과에서 볼 수 있는 프로세스 목록은 어느 리눅스 시스템에서도 볼 수 있습니다.

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

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

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

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

      워크큐에서 요청된 워크를 실행하는 프로세스입니다. 모든 워커 스레드는 프로세스 이름이 “kworer/” 시작하며 워커 속성에 따라 오른쪽에 번호와 알파벳으로 속성이 붙습니다.

      워커 스레드를 이름은 다음 3~10번째 줄 코드에서 확인할 수 있습니다.
      [https://elixir.bootlin.com/linux/v4.14.70/source/kernel/workqueue.c]
      1 static struct worker *create_worker(struct worker_pool *pool)
      2 {
      ...
      3 if (pool->cpu >= 0)
      4 snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id,
      5  pool->attrs->nice < 0  ? "H" : "");
      6 else
      7 snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);
      8
      9 worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
      10       "kworker/%s", id_buf);

      스레드 핸들 함수는 worker_thread() 이며 process_one_work() 함수를 호출해서 워크를 실행하는 역할을 수행합니다.

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

      ksoftirqd 스레드는 smp_boot 스레드이며 프로세스 이름 가장 오른쪽에 실행 중인 CPU 번호를 볼 수 있습니다. ksoftirqd/0 스레드는 CPU0(첫 번째 CPU)에서만 실행합니다.

      ksoftirqd[CPU번호]  스레드 생성 코드와 스레드 동작 선언부는 다음과 같습니다.
      [https://elixir.bootlin.com/linux/v4.14.70/source/kernel/softirq.c]
      1 static struct smp_hotplug_thread softirq_threads = {
      2 .store = &ksoftirqd,
      3 .thread_should_run = ksoftirqd_should_run,
      4 .thread_fn = run_ksoftirqd,
      5 .thread_comm = "ksoftirqd/%u",
      6 };
      7
      8 static __init int spawn_ksoftirqd(void)
      9 {
      10 cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
      11   takeover_tasklets);
      12 BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
      13
      14 return 0;
      15 }

      ksoftirqd[0--4] 스레드는 12번째 줄 코드와 같이 smpboot 스레드로 생성하며, 스레드 핸들 함수는 4번째 줄 코드와 같이 run_ksoftirqd() 함수입니다. ksoftirqd 스레드 핸들인 run_ksoftirqd() 함수는 주로 Soft IRQ 서비스 요청을 _do_softirq() 함수에서 정해진 시각 내 처리하지 못했을 때 실행합니다.

      5번째 줄 코드에서 ksoftirqd[0--4] 스레드 이름을 알 수 있습니다. "ksoftirqd/" 스트링으로 시작하며 오른쪽은 CPU 번호를 지정합니다.

      Trace32 프로그램으로 softirq_threads 변수를 확인하면 다음과 같은 정보 확인이 가능합니다.
      var.view %hex %symbol %type %string softirq_threads 
        (static struct smp_hotplug_thread) softirq_threads = (
          (struct task_struct * *) store = 0xA9C42A90 = ksoftirqd,
          (struct list_head)list = ((struct list_head *) next = 0x80C0D844 = hotplug_threads 
          (int (*)()) thread_should_run = 0x801222C8 = ksoftirqd_should_run,
          (void (*)()) thread_fn = 0x80122BF8= run_ksoftirqd,
          (void (*)()) create = 0x0 = ,
          (void (*)()) setup = 0x0 = ,
          (void (*)()) cleanup = 0x0 = ,
          (void (*)()) park = 0x0 = ,
          (void (*)()) unpark = 0x0 = ,
          (cpumask_var_t) cpumask = (((long unsigned int [1]) bits = (255))),
          (bool) selfparking = FALSE,
          (char *) thread_comm = 0xA974C2E3  -> "ksoftirqd/%u")

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

      IRQ 스레드라고 하며 인터럽트 후반부 처리용 프로세스입니다. 인터럽트 핸들러에서 IRQ 스레드를 깨우며 인터럽트를 프로세스 레벨에서 실행합니다.

      IRQ 스레드 이름은 다음 규칙으로 생성합니다.
      "irq/" + "인터럽트 번호" + "인터럽트 이름"

      irq/92-mmc1 IRQ 스레드는 이름만 봐도 92번 mmc1 인터럽트 후반부 처리용 스레드란 사실을 알 수 있습니다. 

      IRQ 스레드를 생성하는 코드는 다음과 같습니다.
      [https://elixir.bootlin.com/linux/v4.14.70/source/kernel/irq/manage.c]
      1 static int
      2 setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
      3 {
      ...
      4 if (!secondary) {
      5 t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
      6    new->name);
      7 } else {
      8 t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq,
      9    new->name);
      10 param.sched_priority -= 1;
      11 }

      한 개 인터럽트에 2개 이상 IRQ 스레드를 생성할 때 이외에 대부분 5~6번 코드를 실행해서 IRQ 스레드를 생성합니다.

      IRQ Thread 핸들 함수는 irq_thread() 로 보이지만 인터럽트 별로 지정한 IRQ 스레드 핸들 함수를 분기시키는 역할만을 수행합니다. 자세한 내용은 인터럽트 장을 참고하세요.

      대표적인 커널 스레드를 소개했으니 다음에 커널 스레드를 어떻게 생성하는지 알아봅시다.


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


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