Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

5363
1898
209234


[리눅스커널] 워크큐: 워커 스레드 핸들 worker_thread() 함수 분석하기 (2/2) 7. 워크큐(Workqueue)

2단계: “전처리” 단계

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

need_more_worker() 함수는 다음 동작을 수행합니다.
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 플래그는 워커 스레드 처리 흐름에서 전처리 단계를 의미합니다.

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

3단계: “실행” 단계
이번에는 worker_thread() 함수에서 가장 중요한 37~53번째 줄 코드를 분석할 차례입니다. 
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() 함수를 알아볼 필요가 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/workqueue.c]
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번째 줄 코드를 봅시다. 
38 struct work_struct *work =
39 list_first_entry(&pool->worklist,
40  struct work_struct, entry);

sturct worker_pool 구조체 worklist 필드 주소에 접근해 struct work_struct 구조체 주소를 work에 저장합니다. 

&pool->worklist 필드로 struct work_list 구조체 entry 필드 주소를 읽는 동작입니다.

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.19.30/source/include/linux/workqueue.h]
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
}

오프셋 계산에 대한 이해를 돕기 위해 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 이란 지역 변수에 저장합니다.

이 동작은 다음 그림으로 설명할 수 있습니다.
 
[그림 7.14] 큐잉한 워크에 접근하는 동작 

위 그림 가장 오른쪽 아랫 부분을 눈으로 따라가 볼까요? 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 주소를 계산하는 것입니다.

다음 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문은 배리어 워크를 처리하는 동작입니다. 

    46번째 줄에서는 process_one_work() 함수를 호출해 워크를 실행합니다. 

워커 스레드의 핵심 동작입니다. 이렇게 do~while 문에서 워커 풀에 큐잉된 워크를 모두 처리한 후 실행하는 55번째 줄 코드를 보겠습니다.
55 worker_set_flags(worker, WORKER_PREP);

struct worker 구조체 flags 필드에 WORKER_PREP 플래그를 저장합니다. 워커 스레드를 전처리 상태로 변경하는 것입니다.
  
4단계: “슬립” 단계
다음 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번째 코드를 보겠습니다.
57 worker_enter_idle(worker);

worker_enter_idle() 함수를 호출해서 워커 스레드 필드인 struct worker 구조체 flags를 WORKER_IDLE로 설정합니다. 워커 스레드가 워크를 처리하고 난 후 변경하는 상태입니다.

다음 58번째 줄 코드입니다.
58 __set_current_state(TASK_IDLE);

__set_current_state() 함수를 호출해서 워커 스레드 태스크 디스크립터 state 필드를 TASK_IDLE 플래그로 바꿉니다.  

    태스크 디스크립터 state 필드를 TASK_IDLE로 바꿔서 커널 스케줄러가 다른 조건으로 
   워커 스레드를 처리하게 합니다.

59번째 줄 코드에서 스핀락을 풀고 60번째 줄 코드와 같이 schedule() 함수 호출로 휴면에 들어갑니다.
59 spin_unlock_irq(&pool->lock);
60 schedule();
61 goto woke_up;

이렇게 60번째 줄 코드를 실행하면 워커 스레드는 휴면에 진입합니다. 그러면 워커 스레드가 다시 깨어나 실행을 시작할 때 어느 코드부터 실행할까요? 61번째 줄 코드를 실행해 woke_up 레이블로 이동합니다.


워커 스레드는 어떻게 깨울까요? 워크를 워크큐에 큐잉하고 난 후 wake_up_woker() 함수를 호출해 워커 스레드를 깨웁니다.


여기까지 워커 스레드 실행 흐름을 상세히 알아봤습니다. 
코드 분석으로 워커 스레드 핸들인 worker_thread() 함수에서 다음과 같은 동작을 한다는 사실을 알게 됐습니다. 
워커 스레드 종료
워크 실행 조건 점검
워크 실행

다음 절에는 그 동안 분석한 코드가 라즈베리파이에서는 어떻게 동작하는지 ftrace 로그로 확인하는 실습 시간을 갖겠습니다. 

핑백

덧글

댓글 입력 영역