Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

125187
803
94454


[리눅스커널] 워크큐: 워커 쓰레드 핸들 worker_thread() 함수 분석 8장. 워크큐

워크는 워커 쓰레드가 실행합니다. 워커 쓰레드를 관리하는 자료구조는 struct worker 구조체이며 이를 워커라고 부릅니다. 이전 절까지는 자료구조 중심으로 워크를 분석했는데 이번에는 워커 쓰레드가 쓰레드 관점으로 어떻게 실행하는지 알아봅니다. 

다음은 워커 자료구조인 struct worker 구조체 선언부입니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue_internal.h#L24]
1 struct worker {
2 union {
3 struct list_head entry; 
4 struct hlist_node hentry; 
5 };
6 struct work_struct *current_work;
7 work_func_t current_func;
8 struct pool_workqueue *current_pwq; 
9 bool desc_valid;
10 struct list_head scheduled;
11
12 struct task_struct *task;
13 struct worker_pool *pool;
14
15 struct list_head node;
16 unsigned long last_active;
17 unsigned int flags;
18 int id;
19
20 char desc[WORKER_DESC_LEN];
21
22 struct workqueue_struct *rescue_wq;
23};

각 멤버의 의미를 살펴봅시다.

*current_work;
struct work_struct 구조체로 현재 실행하려는 워크를 가르키는 멤버입니다.

current_func;
실행하려는 워크 핸들러 주소를 저장하는 멤버입니다.

워크 구조체와 워크 핸들러는 다음과 같이 process_one_work() 함수에서 위 멤버에 저장합니다.
static void process_one_work(struct worker *worker, struct work_struct *work)
{
...
worker->current_work = work;
worker->current_func = work->func;
worker->current_pwq = pwq;


struct task_struct *task;
워커 쓰레드의 태스크 디스크립터 주소입니다.

struct worker_pool *pool;
워커가 포함된 워커 풀 주소를 저장하는 멤버입니다.

struct list_head node;
워커풀에 등록된 링크드 리스트입니다.

워커를 표현하는 구조체인 struct worker 를 살펴봤으니 워커 쓰레드 동작을 점검하겠습니다. 

워커 쓰레드가 어떤 일을 하는지 알려면 어느 코드를 분석해야 할까요? 커널 쓰레드 동작을 알아보려면 쓰레드 핸들 함수를 분석해야 합니다. 커널 쓰레드가 단계별로 어떤 동작을 하는지 쓰레드 핸들 함수에서 구현했기 때문입니다.

워커 쓰레드도 커널 쓰레드 종류 중 일부분이니 워커 쓰레드 동작을 점검하려면 워커 쓰레드 핸들 함수인 worker_thread()를 분석해야 합니다. worker_thread() 함수를 살펴보기 전 이 쓰레드 핸들 함수를 등록하는 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue.c#L2190]
1 static struct worker *create_worker(struct worker_pool *pool)
2 {
3 struct worker *worker = NULL;
...
4 worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
5       "kworker/%s", id_buf);

kthread_create_on_node() 함수는 커널 쓰레드를 생성할 때 호출합니다.
중요한 인자만 살펴보면, 첫 번째 인자로 쓰레드 핸들, 두 번째 인자로 쓰레드 핸들 매개 변수,
네 번째 인자로 쓰레드 이름을 지정합니다.

4번째 줄 코드를 보면 worker_thread() 함수를 쓰레드 핸들, 두 번째 인자로 쓰레드 핸들 매개 변수, 그리고 네 번째 인자로 워커 쓰레드 이름을 지정합니다.

이번에는 워커 쓰레드 핸들 함수 선언부를 살펴봅니다.
static int worker_thread(void *__worker);

인자
인자는 *__worker인데 워커 쓰레드를 생성할 때 전달했던 struct worker 구조체 주소입니다.
워커를 처리하는 핸들 주소를 worker_thread()란 쓰레드 핸들 함수로 넘겨 받는 겁니다.

이 자료구조는 각각 워커를 표현하며 워커 쓰레드 디스크립터라고 봐도 됩니다.

반환값
함수 선언부를 보면 int 타입을 반환합니다. 워커 해제 요청을 받아 worker_thread() 함수를 호출할 때만 0을 반환하고 이외에는 워커 쓰레드가 동작하는 동안 반환값을 전달하지 않고 쓰레드 핸들 함수 내에서 계속 실행합니다.

다음 destroy_worker() 함수 코드에서 워커 해제 요청을 합니다.
static void destroy_worker(struct worker *worker)
{
...
list_del_init(&worker->entry);
worker->flags |= WORKER_DIE;
wake_up_process(worker->task);
}

worker_thread() 함수 인자와 반환값을 알아봤습니다.

쓰레드 실행 흐름은 알기 위해서 쓰레드 핸들 함수 구조를 파악해야 합니다. 워커 쓰레드를 생성할 때 쓰레드 핸들로 worker_thread() 함수를 등록했으니 이 함수 중심으로 코드 분석을 해 봅시다.

먼저 워커 쓰레드 핸들인 worker_thread() 함수를 분석합니다.
1 static int worker_thread(void *__worker)
2 {
3 struct worker *worker = __worker;
4 struct worker_pool *pool = worker->pool;
5
6 worker->task->flags |= PF_WQ_WORKER;
7 woke_up:
8 spin_lock_irq(&pool->lock);
9
10 /* am I supposed to die? */
11 if (unlikely(worker->flags & WORKER_DIE)) {
12 spin_unlock_irq(&pool->lock);
13 WARN_ON_ONCE(!list_empty(&worker->entry));
14 worker->task->flags &= ~PF_WQ_WORKER;
15
16 set_task_comm(worker->task, "kworker/dying");
17 ida_simple_remove(&pool->worker_ida, worker->id);
18 worker_detach_from_pool(worker, pool);
19 kfree(worker);
20 return 0;
21 }
22
23 worker_leave_idle(worker);
24 recheck:
25 /* no more worker necessary? */
26 if (!need_more_worker(pool))
27 goto sleep;
28
29 /* do we need to manage? */
30 if (unlikely(!may_start_working(pool)) && manage_workers(worker))
31 goto recheck;
32
33 WARN_ON_ONCE(!list_empty(&worker->scheduled));
34
35 worker_clr_flags(worker, WORKER_PREP | WORKER_REBOUND);
36
37 do {
38 struct work_struct *work =
39 list_first_entry(&pool->worklist,
40  struct work_struct, entry);
41
42 pool->watchdog_ts = jiffies;
43
44 if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
45 /* optimization path, not strictly necessary */
46 process_one_work(worker, work);
47 if (unlikely(!list_empty(&worker->scheduled)))
48 process_scheduled_works(worker);
49 } else {
50 move_linked_works(work, &worker->scheduled, NULL);
51 process_scheduled_works(worker);
52 }
53 } while (keep_working(pool));
54
55 worker_set_flags(worker, WORKER_PREP);
56 sleep:
57 worker_enter_idle(worker);
58 __set_current_state(TASK_IDLE);
59 spin_unlock_irq(&pool->lock);
60 schedule();
61 goto woke_up;
62 }

worker_thread() 함수 실행은 다음 그림과 같이 4 단계로 나눌 수 있습니다.
 
각 단계별로 워커 쓰레드가 어떤 동작을 하는지 살펴보겠습니다.

깨어남
워크를 워크큐에 큐잉하면 wake_up_worker() 이란 함수를 호출합니다. 워커 쓰데르를 깨우는 동작입니다. 이 함수는 스케줄러에게 워커 쓰레드를 깨워 달라는 요청을 합니다. 스케줄러 정책에 따라 워커 쓰레드가 실행할 상황이 되면 스케줄러는 워커 쓰레드를 실행합니다. 이 때 실제 워커 쓰레드가 깨어나 실행을 시작합니다.

이전에 워커 쓰레드 해제 요청이 있었으면 워커를 해제하고 아이들 워커 상태에서 벗어납니다.

전처리
워커 쓰레드 실행 전에 전처리를 하는 단계입니다. need_more_worker() 함수를 실행으로 워커 쓰레드를 실행한 조건인지 점검합니다. 실제 워크를 워크큐에 큐잉하지 않았는데 워커 쓰레드를 깨울 수 있기 때문입니다. 이 조건을 만족하면 바로 슬립에 진입합니다. 이후 워커 플래그에서 WORKER_PREP와 WORKER_REBOUND를 해제(Clear) 합니다.

실행
워커풀에 worklist 이란 링크드 리스트에 접근해서 워크를 실행합니다. 워크를 모두 실행한 다음 워커 플레그에서 WORKER_PREP 를 설정합니다.

슬립
워커 상태를 아이들로 설정하고 슬립에 진입합니다. wake_up_worker() 함수가 호출되서 워커 쓰레드가 깨어날 때까지 슬립 상태를 유지합니다.

워커 쓰레드 전체 실행 흐름을 점검했으니 이제 코드 분석을 시작합니다.

6번 줄 코드부터 보겠습니다.
6 worker->task->flags |= PF_WQ_WORKER;

struct worker->task멤버에 태스크 디스크립터 주소가 있습니다. struct task_struct.flags 필드에 PF_WQ_WORKER라는 매크로를 OR 연산으로 저장합니다. 현재 프로세스가 워커 쓰레드이라고 설정하는 겁니다.

7번 줄 코드를 보겠습니다. 워커 쓰레드 실행 흐름 중 “깨어남” 단계입니다.
7 woke_up:
8 spin_lock_irq(&pool->lock);
9
10 /* am I supposed to die? */
11 if (unlikely(worker->flags & WORKER_DIE)) {
12 spin_unlock_irq(&pool->lock);
13 WARN_ON_ONCE(!list_empty(&worker->entry));
14 worker->task->flags &= ~PF_WQ_WORKER;
15
16 set_task_comm(worker->task, "kworker/dying");
17 ida_simple_remove(&pool->worker_ida, worker->id);
18 worker_detach_from_pool(worker, pool);
19 kfree(worker);
20 return 0;
21 }

워커 쓰레드가 깨어나면 실행하는 레이블입니다. 

woke_up 이란 레이블은 언제 실행할까요? 다음 60번 줄 코드와 같이 워커 쓰레드가 휴면에 들어간 다음 프로세스 스케쥴링으로 깨어나면 61번 줄 코드를 실행합니다. goto 으로 woke_up; 레이블로 이동하는 겁니다.
56 sleep:
57 worker_enter_idle(worker);
58 __set_current_state(TASK_IDLE);
59 spin_unlock_irq(&pool->lock);
60 schedule();
61 goto woke_up;

work_up 레이블을 실행하면 다음과 같이 worker->flags 멤버와 WORKER_DIE 매크로를 AND 연산해서 결과가 1이면 12~20번째 줄 코드를 실행해서 워커 쓰레드를 종료합니다.
11 if (unlikely(worker->flags & WORKER_DIE)) {

worker->flags와 WORKER_DIE 매크로와 AND 연산하기 전 어느 코드에서 worker->flags에 WORKER_DIE 매크로를 설정했을까요?

다음 destory_worker()이란 함수 3번 줄 코드입니다. 
1 static void destroy_worker(struct worker *worker)
2 {
...
3 worker->flags |= WORKER_DIE;
4 wake_up_process(worker->task);
5}

worker->flags 멤버에 OR 연산으로 WORKER_DIE 매크로를 저장한 다음 wake_up_process() 함수 호출로 해당 워커 쓰레드를 깨웁니다. 위 destroy_worker() 함수 3~4번 줄 코드를 실행하면 worker_thread() 함수의 7번과 11번 줄 코드를 실행해서 워커 쓰레드를 종료하는 겁니다.

다음 23번 코드를 보겠습니다.
23 worker_leave_idle(worker);

워커 상태를 idle에서 변경합니다.
worker_leave_idle() 함수를 열어 보면 7번째 줄 코드와 같이 worker->flags 멤버에서 WORKER_IDLE 값을 Clear 시킵니다.
1 static void worker_leave_idle(struct worker *worker)
2 {
3 struct worker_pool *pool = worker->pool;
4
5 if (WARN_ON_ONCE(!(worker->flags & WORKER_IDLE)))
6 return;
7 worker_clr_flags(worker, WORKER_IDLE);
8 pool->nr_idle--;
9 list_del_init(&worker->entry);
10}

다음 24번 줄 코드를 보겠습니다. 워커 쓰레드의 “전처리” 실행 단계입니다.
24 recheck:
25 /* no more worker necessary? */
26 if (!need_more_worker(pool))
27 goto sleep;

struct worker_pool.worklist 멤버에 접근해서 큐잉한 워크가 있는지와 struct worker_pool.nr_running 멤버에 저장된 실행 중인 워커 쓰레드 갯수를 점검합니다.
워크를 워크큐에 큐잉한 적이 없다면 워커 쓰레드를 실행할 필요가 없으니 goto sleep; 구문을 실행합니다.

스케쥴링으로 워커 쓰레드가 실행했을때 예외 처리 코드입니다.  

다음 35번 줄 코드를 분석하겠습니다.
35 worker_clr_flags(worker, WORKER_PREP | WORKER_REBOUND);

worker->flags 멤버에서 (WORKER_PREP | WORKER_REBOUND) 연산한 결과를 Clear시킵니다. 워커 상태가 WORKER_PREP와 WORKER_REBOUND가 아니라는 의미입니다. WORKER_PREP는 워커 쓰레드 처리 흐름에서 전처리 단계를 의미합니다.

여기까지 워커 쓰레드 예외처리나 상태를 변경하는 루틴입니다. 

이번에는 worker_thread() 함수에서 가장 중요한 37번 줄 코드를 분석할 차례입니다. 워커 쓰레드의 “실행” 단계입니다.
37 do {
38 struct work_struct *work =
39 list_first_entry(&pool->worklist,
40  struct work_struct, entry);
41
42 pool->watchdog_ts = jiffies;
43
44 if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
45 /* optimization path, not strictly necessary */
46 process_one_work(worker, work);
47 if (unlikely(!list_empty(&worker->scheduled)))
48 process_scheduled_works(worker);
49 } else {
50 move_linked_works(work, &worker->scheduled, NULL);
51 process_scheduled_works(worker);
52 }
53 } while (keep_working(pool));


코드 분석에 들어가기 앞서 do~while 문이 실행할 조건을 결정하는 keep_working() 함수를 알아볼 필요가 있습니다.
1 static bool keep_working(struct worker_pool *pool)
2 {
3 return !list_empty(&pool->worklist) &&
4 atomic_read(&pool->nr_running) <= 1;
5}

3번째 줄 코드는 워커풀에 큐잉된 워크가 있는지 점검합니다. 

3~4번 줄 코드와 같이 워커 풀에 큐잉된 워크가 있는 지와 실행 중인 워커 쓰레드 갯수를 AND 연산해서 리턴합니다. keep_working() 함수가 포함된 do~while 문은 워커 풀에 큐잉된 워크를 모두 처리할 때까지 do~while 루프 안 코드를 실행한다는 의미입니다.

다시 코드 분석을 시작합니다.
먼저 38번 코드를 봅시다. &pool->worklist 주소에 접근해서 struct work_struct 구조체 주소를 읽어 옵니다.
38 struct work_struct *work =
39 list_first_entry(&pool->worklist,
40  struct work_struct, entry);

&pool->worklist 멤버로 struct work_list->entry를 저장하는 링크드 리스트에 접근해서 struct work_struct 구조체 주소를 읽습니다. 

38~40번 줄 코드 실행 원리를 단계별로 조금 더 짚어 보겠습니다
[1]. struct work_struct.entry 멤버 오프셋을 계산합니다.
[2]. &pool->worklist 주소에서 struct work_struct.entry 멤버 오프셋을 빼서 struct work_struct *work에 저장합니다.

[1] 단계 코드 동작을 확인하겠습니다. list_first_entry () 매크로 함수 두 번째와 세 번째 인자는 각각 struct work_struct, entry입니다.

struct work_struct 구조체에서 entry 멤버가 위치한 오프셋 주소를 의미합니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h#L101]
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
}

오프셋 계산에 대한 이해를 돕기 위해 Trace32 프로그램으로 0xb62d3604 주소에 있는 struct work_struct 멤버를 보겠습니다.
var.view %l %t %y (struct work_struct*)0xb62d3604
1  (struct work_struct *) [-] (struct work_struct*)0xb62d3604 = 0xB62D3604 -> (
2    (atomic_long_t) [D:0xB62D3604] data = ((int) [D:0xB62D3604] counter = 0),
3    (struct list_head) [D:0xB62D3608] entry = ((struct list_head *)  
4    (work_func_t) [D:0xB62D3610] func = 0x804FDCA8 = flush_to_ldisc)

3번 줄 디버깅 정보를 보면 entry는 0xB62D3608 주소에 있습니다. struct work_struct 구조체 주소가 0xB62D3604 이니 struct work_struct 구조체에서 entry 멤버가 위치한 오프셋 주소는 0x4(0xB62D3608 - 0xB62D3604)입니다.

[2]번 은  &pool->worklist 주소에서 0x4를 빼서 struct work_struct *work 이란 지역 변수에 저장합니다.

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

위 그림에서 worklist에서 (struct work_struct) 박스로 향하는 화살표를 눈여겨봅시다. 워크를 워크큐에 큐잉하면 워커풀인 struct worker_pool.worklist에 워크의 struct work_struct.entry 주소를 등록합니다.

다음 코드는 위 그림 오른쪽 하단에 entry에서 (struct work_struct) 으로 향하는 화살표와 같습니다.
38 struct work_struct *work =
39 list_first_entry(&pool->worklist,
40  struct work_struct, entry);

struct work_struct.entry 주소를 알았으니 struct work_struct 구조체에서 entry 멤버가 위치한 오프셋을 빼서 struct work_struct 주소를 계산하는 겁니다.

&pool->worklist 주소에서 struct work_struct 주소에 접근하는 방법은 마지막 디버깅 소절에서 알아볼 예정입니다.

다음은 42번 줄 코드를 보겠습니다.
42 pool->watchdog_ts = jiffies;

pool->watchdog_ts 멤버에 현재 시각 정보를 표현하는 jiffies를 저장합니다. 이 값으로 워커 쓰레드 와치독 정보를 갱신합니다. 

참고로, 임베디드 시스템에서 와치독(Watchdog)은 어떤 소프트웨어가 정해진 시간 내에 실행하는지를 확인하는 정보입니다. 와치독(Watchdog)은 임베디드 리눅스 개발 도중 아주 많이 쓰이는 용어이자 개념이므로 잘 알아 둡시다.

이번에는 44번 줄 코드입니다.
44 if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
45 /* optimization path, not strictly necessary */
46 process_one_work(worker, work);

38~40번 줄 코드에서 얻어온 struct work_struct 구조체 주소는 work이란 포인터형 지역 변수에 저장돼 있습니다. 이 변수로 struct work_struct.data 멤버에 접근해서 WORK_STRUCT_LINKED 매크로와 AND 연산을 수행합니다. struct work_struct.data 멤버가 WORK_STRUCT_LINKED이면 배리어 워크이니 50번줄 else 문을 실행합니다. 대부분 워크는 배리어 워크가 아니므로 46번 코드를 실행합니다. 나머지 else문은 배리어 워크를 처리하는 동작입니다.

do~while 문에서 워커 풀에 큐잉된 워크를 모두 처리한 후 실행하는 55번 줄 코드를 보겠습니다.
55 worker_set_flags(worker, WORKER_PREP);

worker.flags 멤버에 WORKER_PREP 매크로를 저장합니다. 워커 쓰레드 상태가 WORKER_PREP 즉 전처리 상태임을 나타냅니다.

이제 56번 줄 코드를 보겠습니다. 워커 쓰레드의 “슬립” 단계입니다.
56 sleep:
57 worker_enter_idle(worker);
58 __set_current_state(TASK_IDLE);
59 spin_unlock_irq(&pool->lock);
60 schedule();
61 goto woke_up;
62 }

56번 레이블은 sleep인데 57~59줄 코드에서 워커 쓰레드가 휴면에 들어간 준비를 하고 60번 줄 코드에서 휴면에 들어갑니다. 

57번 코드에서 worker_enter_idle() 함수를 호출해서 워커 쓰레드 멤버인 worker->flags를 WORKER_IDLE로 설정합니다. 워커 쓰레드가 워크를 처리하고 난 후 진입하는 상태입니다.

58번 줄 코드는 __set_current_state() 함수를 호출해서 워커 쓰레드 태스크 디스크립터 state 멤버를 TASK_IDLE 매크로로 변경합니다. 태스크 디스크립터 state 멤버를 TASK_IDLE로 변경해서 커널 스케쥴링 처리를 하지 않게 합니다.

59번 줄 코드에서 스핀락을 풀고 60번 줄 코드와 같이 schedule() 함수 호출로 휴면에 들어갑니다.

워크를 큐잉 할때나 워커 쓰레드를 깨울 때 wake_up_worker() 혹은 wake_up_process() 함수를 호출하는데 이 때 61번 줄 코드가 다시 실행합니다. 61번 줄 코드는 woke_up이라는 레이블로 점프를 합니다.

여기까지 워커 쓰레드 실행 흐름을 상세히 알아봤습니다. 다음 절에는 분석한 코드가 라즈베리파이에서는 어떻게 동작하는지 ftrace 로그로 살펴보겠습니다.

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


핑백

덧글

댓글 입력 영역