Linux Kernel(4.14) Hacks

rousalome.egloos.com

포토로그 Kernel Crash




[라즈베리파이] 워크큐(Workqueue) - worker_thread() 함수 분석(2) [라즈베리파이] 워크큐


먼저 워커 쓰레드 핸들 함수인 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는 워커 쓰레드 처리 흐름에서 전처리 단계를 의미합니다.

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

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


핑백

덧글

댓글 입력 영역