Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

0112
737
82110


4.5 커널 스레드 4장. 프로세스 관리

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


#Reference 시스템 콜

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



핑백

덧글

댓글 입력 영역