ARM Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

193239
1625
172590


[리눅스커널] 프로세스: 커널 스레드는 어떻게 생성할까? 4. 프로세스(Process) 관리

이어서 커널 스레드를 생성하는 과정에서 호출되는 함수를 소개하고 세부 코드를 분석하겠습니다. 커널 스레드가 생성되는 과정은 크게 2단계로 나눌 수 있습니다.

1) 1단계: kthreadd 프로세스에게 커널 스레드 생성을 요청

kthread_create()
kthread_create_on_node()

2) 2단계: kthreadd 프로세스가 커널 스레드를 생성

kthreadd() 
create_kthread()

각 단계별로 실행되는 함수를 살펴보겠습니다.

1단계: kthreadd 프로세스에게 커널 스레드 생성 요청

유저 프로세스를 생성하려면 fork() 함수를 호출해야 하듯이, 커널 스레드를 생성하려면 kthread_create() 커널 함수를 호출해야 합니다. 먼저 kthreadd 프로세스에게 커널 스레드 생성 요청을 하는 함수 실행 흐름을 살펴보겠습니다(그림 4.6).

 
 그림 4.6 커널 스레드 생성 흐름도 1단계

이 그림에서 왼쪽 부분은 커널 스레드 생성을 kthreadd 프로세스에게 요청하는 동작입니다.

kthread_create() 함수 분석

커널 스레드를 생성하려면 먼저 kthread_create() 함수를 호출해야 합니다. 다음 코드는 kthread_create() 함수로서, 매크로 형태로 작성된 것을 알 수 있습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/kthread.h
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    ...)

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

int (*threadfn)(void *data)
스레드 핸들 함수 주소를 저장하는 필드입니다. 커널 스레드의 세부 동작은 스레드 핸들 함수에 구현돼 있습니다.

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

int node
노드 정보입니다.

const char namefmt[]
커널 스레드 이름을 저장합니다.

kthread_create() 함수를 호출할 때 전달하는 인자를 알아봤습니다. 하지만 kthread_create() 함수 구현부만 봐서는 커널 스레드를 생성할 때 어떤 인자를 전달하는지 알기 어렵습니다. 이어서 커널 스레드를 생성하는 예제 코드를 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/drivers/vhost/vhost.c
1 long vhost_dev_set_owner(struct vhost_dev *dev)
2 {
3 struct task_struct *worker;
4 int err;
...
5 /* No owner, become one */
6 dev->mm = get_task_mm(current);
7 worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid);

7번째 줄을 보면 kthread_create() 함수의 첫 번째 인자로 vhost_worker 스레드 핸들 함수의 이름을 지정합니다.

7 worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid);

2번째 인자로 dev 변수를 지정하는데, 1번째 줄의 함수 인자를 보면 dev가 어떤 구조체인지 알 수 있습니다. vhost_dev 구조체의 주소를 2번째 인자로 전달하는 것입니다. 이 방식으로 전달하는 인자를 매개변수 혹은 디스크립터라고도 부릅니다. 

3번째 인자로 "vhost-%d"를 전달합니다. 이는 커널 스레드 이름을 나타냅니다.

---
이번에는 스레드 핸들 함수로 전달되는 매개변수를 점검해 봅시다. vhost_worker() 함수의 코드를 보겠습니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/drivers/vhost/vhost.c
01 static int vhost_worker(void *data)
02 {
03 struct vhost_dev *dev = data;
04 struct vhost_work *work, *work_next;
05 struct llist_node *node;

vhost_dev_set_owner() 함수에서 kthread_create() 함수를 호출할 때 두 번째 인자로 vhost_dev 구조체인 dev를 지정했습니다. 이 주소가 스레드 핸들인 vhost_worker() 함수의 인자로 전달되는 것입니다. 

이처럼 vhost_worker() 스레드 핸들 함수의 매개변수로 인자를 전달합니다. 1번째 줄에서 보이는 void 타입의 data 포인터를 vhost_dev 구조체로 형변환(캐스팅)합니다.

커널에서는 이 같은 방식으로 스레드를 관리하는 구조체를 매개변수로 전달합니다.
---

kthread_create() 함수를 호출해 커널 스레드를 생성하는 예제를 봤으니 이어서 kthread_create() 함수의 구현부를 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/kthread.h
1 #define kthread_create(threadfn, data, namefmt, arg...) \
2 kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)

kthread_create() 함수는 선언부와 같이 매크로 타입입니다. 따라서 커널 컴파일 과정에서 전처리기는 kthread_create() 함수를 kthread_create_on_node() 함수로 바꿉니다. 즉, 커널이나 드라이버 코드에서 kthread_create() 함수를 호출하면 실제로 동작하는 코드는 kthread_create_on_node() 함수인 것입니다. 

kthread_create_on_node() 함수 분석

kthread_create() 매크로 함수의 실체인 kthread_create_on_node() 함수의 구현부를 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/kthread.c
1 struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
2    void *data, int node,
3    const char namefmt[],
4    ...)
5 {
6 struct task_struct *task;
7 va_list args;
8
9 va_start(args, namefmt);
10 task = __kthread_create_on_node(threadfn, data, node, namefmt, args);
11 va_end(args);
12
13 return task;
14}

함수 구현부를 봐도 특별한 일은 하지 않습니다. 다만 가변 인자를 통해 __kthread_create_on_node() 함수를 호출할 뿐입니다. 결국 __kthread_create_on_node() 함수에서 커널 스레드 생성 요청을 합니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/kthread.c  
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);

kmalloc() 함수를 호출해 kthread_create_info 구조체 크기만큼 동적 메모리를 할당받습니다.

다음 13~15번째 줄 코드를 봅시다.

13 create->threadfn = threadfn;
14 create->data = data;
15 create->node = node;

생성할 커널 스레드 핸들 함수와 매개변수 및 노드를 kthread_create_info 구조체의 필드에 저장합니다.

다음으로 19번째 줄 코드를 봅시다.

19 list_add_tail(&create->list, &kthread_create_list);
 
커널 스레드 생성 요청을 관리하는 kthread_create_list 연결 리스트에 &create->list를 추가합니다. kthreadadd 프로세스는 kthread_create_list 연결 리스트를 확인해 커널 스레드 생성 요청이 있었는지 확인합니다. 만약 kthread_create_list 연결 리스트가 비어 있지 않으면 누군가 커널 스레드 생성을 요청했으니 커널 스레드를 생성하는 일을 시작합니다.  

다음은 __kthread_create_on_node() 함수의 핵심 코드인 22번째 줄입니다.

22 wake_up_process(kthreadd_task);
 
kthreadd 프로세스의 태스크 디스크립터인 kthreadd_task를 인자로 삼아 wake_up_process() 함수를 호출해서 kthreadd 프로세스를 깨웁니다.

2단계: kthreadd 프로세스가 커널 스레드를 생성

kthreadd 프로세스를 깨우면 kthreadd 프로세스 스레드 핸들인 kthreadd() 함수가 실행을 시작합니다.

 
 그림 4.7 커널 스레드 생성 흐름: 2단계

먼저 이 그림에서 왼쪽 박스 부분을 눈으로 따라가 봅시다. 1단계에서 커널 스레드를 생성해 달라고 kthreadd 프로세스에게 요청했습니다. 그리고 kthreadd 프로세스를 깨웠습니다. 

다음으로 오른쪽 박스 부분을 보겠습니다. kthreadd 프로세스를 깨우면 kthreadd 프로세스 스레드 핸들인 kthreadd() 함수가 호출되어 프로세스를 생성합니다. 

kthreadd() 함수 분석

kthreadd 프로세스의 스레드 핸들인 kthreadd() 함수를 분석하겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/kthread.c
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 }

복잡해 보이는 kthreadd() 함수의 핵심 기능은 다음과 같습니다.

kthread_create_info 연결 리스트를 확인해 프로세스 생성 요청을 확인
create_kthread() 함수를 호출해 프로세스를 생성

kthreadd() 함수 분석을 시작하기 전에 한 가지 생각해 볼 점이 있습니다. kthreadd 프로세스를 깨우면 kthreadd() 함수가 실행한다고 했는데, 실제 kthreadd() 함수의 어느 코드가 실행할까요?

kthreadd 프로세스는 kthreadd() 함수 내에서 프로세스 생성 요청이 있었는지 kthread_create_list 연결 리스트를 지속적으로 점검합니다. 만약 kthreadd 프로세스가 요청한 프로세스 생성을 처리한 다음 더는 생성할 프로세스가 없을 때는 어느 코드를 실행할까요?

16 if (list_empty(&kthread_create_list))
17 schedule();
18 __set_current_state(TASK_RUNNING);

위와 같이 17번째 줄 코드를 실행합니다. schedule() 함수를 호출해 스스로 휴면 상태로 진입하는 동작입니다. 즉, 커널 스레드 생성 요청이 없으면 kthread_create_list 연결 리스트가 비게 되어 휴면하는 것입니다.

이후 커널 스레드 생성 요청을 받아 kthreadd 프로세스를 누군가 깨우면 18번째 줄 코드를 실행합니다. 그 이유는 schedule() 함수로 휴면 상태로 진입한 후 다음에 실행되는 코드이기 때문입니다.

kthreadd 프로세스가 깨어나면 실행되는 코드를 알아봤으니 이어서 함수 코드를 분석하겠습니다.

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 필드를 통해 kthread_create_info 구조체의 주소를 읽습니다.

연결 리스트 타입인 kthread_create_list 전역변수와 스레드 생성 정보를 나타내는 kthread_create_info 구조체의 관계는 다음과 같습니다.

 
그림 4.8 kthread_create_list 연결 리스트와 kthread_create_info 구조체의 관계

kthread_create_info 구조체를 보면서 위 그림을 설명하겠습니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/kthread.c
01 struct kthread_create_info
02 {
03 int (*threadfn)(void *data);
04 void *data;
05 int node;
06
07 struct task_struct *result;
08 struct completion *done;
09
10 struct list_head list;
11 };

kthread_create_info 구조체의 마지막 필드는 list이며 struct list_head 타입입니다. 그런데 kthread_create_list 전역변수의 next 필드가 kthread_create_info 구조체의 list 필드 주소를 가리킵니다. 그림 4.8의 맨 왼쪽 부분에 있는 kthread_create_list의 next 전역변수는 화살표로 kthread_create_info list 구조체의 필드를 가리키는 것입니다.

kthread_create_info 구조체에서 list 필드의 오프셋을 계산해 kthread_create_info 구조체의 시작 주소를 알 수 있습니다.  

29번째 줄 코드를 봅시다.

29 create_kthread(create);

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

create_kthread() 함수 분석

이제 create_kthread() 함수를 봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/kthread.c
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 매크로를 OR 연산한 결과를 3번째 인자로 설정합니다.

kernel_thread() 함수 분석

이번에는 kernel_thread() 함수를 분석하겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c
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() 함수를 호출해 프로세스를 생성하는 일을 시작합니다. 코드 분석을 통해 커널 스레드도 프로세스의 한 종류인 것을 알 수 있습니다.

지금까지 커널 스레드를 생성하는 과정을 코드 분석을 통해 알아봤습니다. 배운 내용을 정리해 봅시다.

첫째, 커널 스레드를 생성하려면 어떤 함수를 호출해야 할까?
kthread_create() 함수를 호출해야 합니다. 함수의 인자로 '스레드 핸들 함수', '매개변수', '커널 스레드 이름'을 지정해야 합니다.

둘째, 커널 스레드를 생성하는 단계는 무엇일까?
kthreadd 프로세스에게 커널 스레드 생성을 요청한 후 kthreadd 프로세스를 깨웁니다. kthreadd 프로세스는 깨어나 자신에게 커널 스레드 생성 요청이 있었는지 확인한 후 만약 생성 요청이 있다면 커널 스레드를 생성합니다.

* 유튜브 강의 동영상도 있으니 같이 들으시면 좋습니다. 




#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)


# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2







핑백

덧글

댓글 입력 영역