Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[리눅스커널] 워크큐: 워커 스레드를 생성하는 create_worker() 함수 분석하기 8. Workqueue

워커 스레드를 생성하려면 create_worker() 함수를 호출해야 합니다. 이번 시간에는 create_worker() 함수 코드를 분석하면서 워커 스레드를 생성하는 과정을 배워보겠습니다.

먼저 create_worker() 함수가 하는 주요 동작은 다음과 같습니다.
워커풀 아이디 읽어오기
워커 스레드 이름을 지정해 워커 스레드 생성 요청하기
워커풀에 워커 스레드 등록하기
워커 정보를 갱신하고 만든 워커 스레드를 깨우기

각 단계 별 코드를 자세히 살펴보겠습니다.

다음은 create_worker() 함수 구현부입니다.
1 static struct worker *create_worker(struct worker_pool *pool)
2 {
3 struct worker *worker = NULL;
4 int id = -1;
5 char id_buf[16];
6
7 id = ida_simple_get(&pool->worker_ida, 0, 0, GFP_KERNEL);
8 if (id < 0)
9 goto fail;
10
11 worker = alloc_worker(pool->node);
12 if (!worker)
13 goto fail;
14
15 worker->pool = pool;
16 worker->id = id;
17
18 if (pool->cpu >= 0)
19 snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id,
20  pool->attrs->nice < 0  ? "H" : "");
21 else
22 snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);
23
24 worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
25       "kworker/%s", id_buf);
26 if (IS_ERR(worker->task))
27 goto fail;
28
29 set_user_nice(worker->task, pool->attrs->nice);
30 kthread_bind_mask(worker->task, pool->attrs->cpumask);
31
32 /* successful, attach the worker to the pool */
33 worker_attach_to_pool(worker, pool);
34
35 /* start the newly created worker */
36 spin_lock_irq(&pool->lock);
37 worker->pool->nr_workers++;
38 worker_enter_idle(worker);
39 wake_up_process(worker->task);
40 spin_unlock_irq(&pool->lock);
41
42 return worker;
43
44 fail:
45 if (id >= 0)
46 ida_simple_remove(&pool->worker_ida, id);
47 kfree(worker);
48 return NULL;
49 }

코드가 복잡해 보이지만 create_worker() 함수 핵심 코드는 18~25번째 줄입니다.
18 if (pool->cpu >= 0)
19 snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id,
20  pool->attrs->nice < 0  ? "H" : "");
21 else
22 snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);
23
24 worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
25       "kworker/%s", id_buf);

18~25 번째 코드는 워커 스레드 이름을 지정한 후 프로세스 생성 요청을 합니다.

이제부터 각 단계별 소스 코드를 분석하겠습니다. 

워커풀 아이디 읽어오기

7번째 줄 코드를 분석하겠습니다. 
7 id = ida_simple_get(&pool->worker_ida, 0, 0, GFP_KERNEL);
8 if (id < 0)
9 goto fail;

ida_simple_get() 함수를 호출해서 워커 풀 아이디를 가져옵니다. 이 아이디로 워커 풀를 관리합니다. 

워커 스레드 이름을 지정해 워커 스레드 생성 요청하기

다음은 18번째 줄 코드를 분석하겠습니다. 
18 if (pool->cpu >= 0)
19 snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id,
20  pool->attrs->nice < 0  ? "H" : "");
21 else
22 snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);
23
24 worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
25       "kworker/%s", id_buf);

18~22번째 줄은 다음 동작을 수행합니다.  

    워커 스레드 이름을 결정합니다.

18 번째 줄 코드는 pool->cpu >= 0 조건을 만족하는지 검사합니다.

워커풀이 percpu 타입으로 관리하는 유형이면 pool->cpu가 0보다 크니 19~20 번째 줄 코드를 실행합니다. 이외 single cpu로 관리하는 워커풀이면 22번째 줄 코드를 실행합니다.


percpu 타입으로 관리하는 워커 풀은 어떤 워크큐에서 쓸까요? 시스템 워크가 주로 씁니다. 시스템 워크큐로 워크를 생성할 때 19번째 줄 코드를 실행합니다.


pool->cpu는 워커 풀이 실행 중인 CPU 번호이고 워커 풀에서 관리하는 IDR 아이디 입니다.
19번째 줄 코드에서 이 기준으로 워커 스레드 이름을 짓습니다.

20번째 줄 코드를 보면 다음 조건에 따라 "H"이라는 스트링을 추가합니다.
pool->attrs->nice < 0

워크큐에서 2종류의 워커풀이 있습니다. 우선 순위를 높혀서 워크를 실행하는 워커 풀(cpu_worker_pools[1])은 pool->attrs->nice 값이 보통 -20입니다. 이 밖의 워커 풀(cpu_worker_pools[0]) 은 pool->attrs->nice 값이 0입니다.

라즈베리파이에서 터미널을 열고 다음 명령어를 입력하면 워커 스레드를 확인할 수 있습니다.
root@raspberrypi:/# ps -ely | grep kworker
1 S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
2 I     0     4     2  0  60 -20     0     0 worker ?        00:00:00 kworker/0:0H
3 I     0    16     2  0  60 -20     0     0 worker ?        00:00:00 kworker/1:0H
...
4 I     0    29     2  0  80   0     0     0 worker ?        00:00:00 kworker/0:1
5 I     0    30     2  0  80   0     0     0 worker ?        00:00:00 kworker/1:1

위에서 볼 수 있는 모든 워커 스레드는 percpu 타입 워커풀에서 생성됐음을 알 수 있습니다.

2~3번째 줄에서 보이는 워커 스레드 프로세스 이름 마지막에 H가 붙어 있습니다. 

    우선 순위를 높혀 처리하는 워커풀(cpu_worker_pools[1])에서 생성됐다는 정보입니다.

이렇게 워커 스레드 프로세스 이름만 봐도 워커 스레드 속성을 짐작할 수 있습니다. 

22번째 줄 코드는 singlecpu 워커 풀에서 생성된 워커 스레드 이름을 짓습니다.

다음 명령어로 워커 스레드 이름을 확인할 수 있는데 워커 스레드 중 “u” 스트링이 붙은 프로세스입니다.
root@raspberrypi:/# ps -ely | grep kworker
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
...
I     0   466     2  0  60 -20     0     0 worker ?        00:00:00 kworker/u9:0
I     0   471     2  0  60 -20     0     0 worker ?        00:00:00 kworker/u9:2
I     0   501     2  0  80   0     0     0 worker ?        00:00:00 kworker/u8:3
I     0  1353     2  0  80   0     0     0 worker ?        00:00:00 kworker/u8:1
I     0  1470     2  0  80   0     0     0 worker ?        00:00:00 kworker/u8:0

다음 29번째 줄 코드를 보겠습니다.
29 set_user_nice(worker->task, pool->attrs->nice);
30 kthread_bind_mask(worker->task, pool->attrs->cpumask);

프로세스 우선 순위인 nice를 워커 스레드의 태스크 디스크립터 구조체 struct task_struct 필드인 static_prio에 저장합니다.

워커풀에 워커 스레드 등록하기

이번에는 워커풀에 생성한 워커 스레드를 등록하는 코드를 분석합니다.

이어서 33번째 줄 코드를 보겠습니다. 
33 worker_attach_to_pool(worker, pool);

worker_attach_to_pool() 함수를 호출해서 워커를 워커풀에 등록합니다.

다음 4번째 줄 코드와 같이 &pool->workers 연결 리스트에 노드 주소를 저장합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/workqueue.c]
01 static void worker_attach_to_pool(struct worker *worker,
02    struct worker_pool *pool)
03 {
...
04 list_add_tail(&worker->node, &pool->workers);

워커 정보를 갱신하고 만든 워커 스레드를 깨우기

마지막 단계로 워커 정보를 갱신하고 워커 스레드를 깨우는 동작을 살펴보겠습니다.

다음 37번째 코드 분석으로 들어갑니다.
37 worker->pool->nr_workers++;
38 worker_enter_idle(worker);
39 wake_up_process(worker->task);

워커를 생성했으니 워커 개수를 관리하는 nr_workers 변수를 +1만큼 증감합니다.
38번째 줄 코드는 worker_enter_idle() 함수를 호출해서 워커를 idle 상태로 변경합니다.

마지막으로 39번 째 줄 코드는 wake_up_process() 함수를 호출해 워커 스레드를 깨웁니다. 입력 인자로 태스크 디스크립터인 worker->task 필드를 전달합니다. 이렇게 워커 스레드는 생성한 후 바로 실행 요청을 합니다.

이번 소절에는 워커를 생성하는 세부 동작을 분석했습니다. 이어서 create_worker() 함수에서 호출한 워크큐 내부 함수를 분석하겠습니다.


핑백

덧글

댓글 입력 영역