Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

106258
1323
114608


[리눅스커널] 워크큐: 워커 쓰레드는 누가 언제 생성하나? 8. Workqueue

워커와 워커 쓰레드의 개념부터 정리하고 워커 쓰레드 생성에 대해 알아봅시다. 

워커 쓰레드는 커널 쓰레드의 한 종류로 워크를 실행하는 쓰레드입니다. 쓰레드 핸들 함수는 worker_thread() 입니다.

처음 커널 쓰레드를 분석할 때 먼저 쓰레드 핸들 함수를 분석해야 합니다. 쓰레드를 어떤 과정으로 실행 및 제어하는지 알 수 있기 때문입니다.

워커는 워커 쓰레드를 표현하는 자료구조이며 struct worker입니다. 워커 쓰레드와 워커는 비슷한 개념으로 볼 필요가 있습니다.

워커 쓰레드를 생성하기 위해서 우선 워커를 생성해야 합니다. 워커는 워커 쓰레드를 담근 그릇(container)와 비슷한 역할을 수행합니다. 만약 “워커를 해제했다” 라고 어떤 개발자가 말하면 “해당 워커 쓰레드를 해제했구나”란 뜻입니다.

이제 워커를 언제 생성하는지 알아볼 차례입니다. 워커는 create_worker() 함수를 호출할 때 생성합니다. 이 create_worker() 함수는 다양한 시나리오에서 다음 함수에서 호출합니다.
maybe_create_worker() 
get_unbound_pool() 
workqueue_prepare_cpu()

가령 리눅스 시스템에서 과부하가 걸려 여러 드라이버에서 워커를 아주 많이 생성했다고 가정합니다. 생성된 워크를 처리하기 위해 워커를 생성해야 하며 이 때 create_worker() 함수를 호출합니다.

하지만 기본적으로 워크큐 자료구조를 초기화할 때 부팅 시 워커를 생성합니다. 우선 이 코드부터 분석해 봅시다.

다음은 workqueue_init() 함수 구현부입니다.
1 int __init workqueue_init(void)
2 {
3 struct workqueue_struct *wq;
4 struct worker_pool *pool;
5 int cpu, bkt;
6
7 wq_numa_init();
8
9 mutex_lock(&wq_pool_mutex);
10
...
11 /* create the initial workers */
12 for_each_online_cpu(cpu) {
13 for_each_cpu_worker_pool(pool, cpu) {
14 pool->flags &= ~POOL_DISASSOCIATED;
15 BUG_ON(!create_worker(pool));
16 }
17 }

11번 줄 코드를 보겠습니다.
12 for_each_online_cpu(cpu) {
13 for_each_cpu_worker_pool(pool, cpu) {
14 pool->flags &= ~POOL_DISASSOCIATED;
15 BUG_ON(!create_worker(pool));
16 }

각 CPU 별로 워커 풀에 접근해서 풀 노드 정보를 저장하는 코드입니다.

각 워크 풀 별로 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 }

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번 줄 코드는 pool->cpu >= 0 조건을 만족하는지 검사합니다.

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

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

19번 줄 코드를 보면 다음 상수 값으로 워커 쓰레드 이름을 짓습니다.
pool->cpu, id

pool->cpu는 워커 풀이 실행 중인 CPU 번호이고 워커 풀에서 관리하는 IDR 아이디 입니다.

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() 함수를 호출해서 워커를 워커풀에 등록합니다.

&pool->workers이란 링크드 리스트에 노드 주소를 저장합니다.
static void worker_attach_to_pool(struct worker *worker,
   struct worker_pool *pool)
{
...
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번 째 줄 코드는 워커 쓰레드를 깨우는 동작입니다. 입력 인자로 태스크 디스크립터인 worker->task 멤버를 전달합니다.

다음 worker_attach_to_pool() 함수 코드를 보겠습니다.
1 static void worker_attach_to_pool(struct worker *worker,
2    struct worker_pool *pool)
3 {
4 mutex_lock(&pool->attach_mutex);
5
6 set_cpus_allowed_ptr(worker->task, pool->attrs->cpumask);
7 if (pool->flags & POOL_DISASSOCIATED)
8 worker->flags |= WORKER_UNBOUND;
9
10 list_add_tail(&worker->node, &pool->workers);
11
12 mutex_unlock(&pool->attach_mutex);
13 }

10번째 줄 코드 분석을 진행합니다.
10 list_add_tail(&worker->node, &pool->workers);

&worker->node와 &pool->workers 변수 모두 struct list_head 타입 링크드 리스트입니다. &pool->workers이란 링크드 리스트에 &worker->node를 등록합니다. 이 코드가 실행하면 다음 그림과 같이 자료 구조가 업데이트됩니다.
 
[그림 7.9 워커를 워커풀에 등록 후 자료구조]

다른 관점으로 이 코드를 설명을 드리면 &pool->workers 링크드 리스트는 &worker->node 주소를 가리키고 있습니다. 이후 &pool->workers 주소에 접근해서 struct worker.node 멤버 오프셋을 빼서 struct work 구조체 주소에 접근합니다.

리눅스 커널에서는 이 방식을 아주 많이 씁니다. 링크드 리스트가 다른 자료구조 링크드 리스트 멤버 주소를 가르키는 동작입니다. container_of 매크로 함수도 이 방식을 적용해서 구현한 것입니다.

다음은 worker_enter_idle() 함수를 분석하겠습니다.
1 static void worker_enter_idle(struct worker *worker)
2 {
3 struct worker_pool *pool = worker->pool;
4
5 if (WARN_ON_ONCE(worker->flags & WORKER_IDLE) ||
6     WARN_ON_ONCE(!list_empty(&worker->entry) &&
7  (worker->hentry.next || worker->hentry.pprev)))
8 return;
9
10 worker->flags |= WORKER_IDLE;
11 pool->nr_idle++;
12 worker->last_active = jiffies;
13
14 list_add(&worker->entry, &pool->idle_list);
15
16 if (too_many_workers(pool) && !timer_pending(&pool->idle_timer))
17 mod_timer(&pool->idle_timer, jiffies + IDLE_WORKER_TIMEOUT);
18
19 WARN_ON_ONCE(!(pool->flags & POOL_DISASSOCIATED) &&
20      pool->nr_workers == pool->nr_idle &&
21      atomic_read(&pool->nr_running));
22 }

10번 코드를 봅시다.
10 worker->flags |= WORKER_IDLE;

worker->flags 를 WORKER_IDLE 플래그 설정을 합니다. 워커를 WORKER_IDLE 타입으로 설정하는 코드입니다.

다음 11번 줄 코드를 보겠습니다.
11 pool->nr_idle++;
12 worker->last_active = jiffies;

11번 줄 코드는 워커 풀 nr_idle 멤버를 +1만큼 증감합니다. nr_idle은 워커풀에 등록된 idle 워커 개수를 표현합니다.

다음 12번 줄 코드는 worker->last_active에 jiffies이란 현재 시각 정보를 저장합니다. 워커가 마지막에 처리된 시각을 저장하는 겁니다.

여기까지 워커를 생성하는 create_worker() 중심으로 워커 쓰레드를 생성하는 코드 흐름을 분석했습니다. 다음 절에서는 워커 쓰레드 핸들 함수인 worker_thread() 분석으로 워커 쓰레드 동작을 살펴보겠습니다.


#Reference 워크큐
워크큐 소개
워크큐 종류 알아보기
워크란  
워크를 워크큐에 어떻게 큐잉할까?
   워크를 큐잉할 때 호출하는 워크큐 커널 함수 분석   
워커 쓰레드란
워크큐 실습 및 디버깅
   ftrace로 워크큐 동작 확인   
   인터럽트 후반부로 워크큐 추가 실습 및 로그 분석 
   Trace32로 워크큐 자료 구조 디버깅하기 
딜레이 워크 소개  
   딜레이 워크는 누가 언제 호출할까?
라즈베리파이 딜레이 워크 실습 및 로그 확인  


핑백

덧글

댓글 입력 영역