Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

110187
803
94439


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



이번에는 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~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 멤버를 보겠습니다.
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(워크큐)
워크큐(Workqueue) Overview


핑백

덧글

댓글 입력 영역