Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

75235
1036
103645


[리눅스커널] 워크큐: process_one_work() 함수 분석 8장. 워크큐

우선 process_one_work() 함수 전체 코드를 보겠습니다.
1 static void process_one_work(struct worker *worker, struct work_struct *work)
2 __releases(&pool->lock)
3 __acquires(&pool->lock)
4 {
5 struct pool_workqueue *pwq = get_work_pwq(work);
6 struct worker_pool *pool = worker->pool;
7 bool cpu_intensive = pwq->wq->flags & WQ_CPU_INTENSIVE;
8 int work_color;
9 struct worker *collision;
10
11 /* ensure we're on the correct CPU */
12 WARN_ON_ONCE(!(pool->flags & POOL_DISASSOCIATED) &&
13      raw_smp_processor_id() != pool->cpu);
14
15 collision = find_worker_executing_work(pool, work);
16 if (unlikely(collision)) {
17 move_linked_works(work, &collision->scheduled, NULL);
18 return;
19 }
20
21 /* claim and dequeue */
22 debug_work_deactivate(work);
23 hash_add(pool->busy_hash, &worker->hentry, (unsigned long)work);
24 worker->current_work = work;
25 worker->current_func = work->func;
26 worker->current_pwq = pwq;
27 work_color = get_work_color(work);
28
29 list_del_init(&work->entry);
30
31 if (unlikely(cpu_intensive))
32 worker_set_flags(worker, WORKER_CPU_INTENSIVE);
33
34 if (need_more_worker(pool))
35 wake_up_worker(pool);
36
37 set_work_pool_and_clear_pending(work, pool->id);
38
39 spin_unlock_irq(&pool->lock);
40
41 lock_map_acquire(&pwq->wq->lockdep_map);
42 lock_map_acquire(&lockdep_map);
43 lockdep_invariant_state(true);
44
45 trace_workqueue_execute_start(work);
46 worker->current_func(work);
47
48 trace_workqueue_execute_end(work);
49 lock_map_release(&lockdep_map);
50 lock_map_release(&pwq->wq->lockdep_map);
51
52 if (unlikely(in_atomic() || lockdep_depth(current) > 0)) {
53 pr_err("BUG: workqueue leaked lock or atomic: %s/0x%08x/%d\n"
54        "     last function: %pf\n",
55        current->comm, preempt_count(), task_pid_nr(current),
56        worker->current_func);
57 debug_show_held_locks(current);
58 dump_stack();
59 }
60
61 cond_resched_rcu_qs();
62
63 spin_lock_irq(&pool->lock);
64
65 /* clear cpu intensive status */
66 if (unlikely(cpu_intensive))
67 worker_clr_flags(worker, WORKER_CPU_INTENSIVE);
68
69 /* we're done with it, release */
70 hash_del(&worker->hentry);
71 worker->current_work = NULL;
72 worker->current_func = NULL;
73 worker->current_pwq = NULL;
74 worker->desc_valid = false;
75 pwq_dec_nr_in_flight(pwq, work_color);
76 }

15번 줄 코드를 분석하겠습니다.
15 collision = find_worker_executing_work(pool, work);
16 if (unlikely(collision)) {
17 move_linked_works(work, &collision->scheduled, NULL);
18 return;
19 }

지금 실행하려는 워크를 다른 워커가 실행 중인지 점검하는 코드입니다. 한 개 워크는 한 개 워커에서 실행해야 워커 쓰레드를 효율적으로 쓸 수 있습니다. find_worker_executing_work() 함수를 열어보면 워커 풀 멤버인 busy_hash를 순회하며 현재 처리하는 워크가 이미 다른 워커에서 등록됐는지 점검합니다.

이 조건을 점검한 후 이미 같은 워크를 처리했던 워커(&collision->scheduled) 멤버에 워크를 이동합니다. 18번째 줄 코드와 같이 return 문을 실행해서 process_one_work() 함수를 바로 빠져 나옵니다.

이 방식으로 하나의 워크를 여러 워크에서 실행하지 않도록 관리합니다.

다음 23번째 줄 코드를 보겠습니다.
22 debug_work_deactivate(work);
23 hash_add(pool->busy_hash, &worker->hentry, (unsigned long)work);

pool->busy_hash이란 해시 테이블에 &worker->hentry를 등록합니다. 최근 실행한 워커 쓰레드는 워커 풀 bush_hash란 멤버에 등록됩니다. 이 워커 쓰레드를 표현하는 워커 자료구조를 가져와 실행했던 워크 정보를 읽습니다. 만약 워크 실행 정보가 일치하면 해당 워커 자료구조를 반환합니다. 효율적으로 워커를 쓰려는 목적입니다.

&pool->busy_hash 해시 테이블은 find_worker_executing_work() 함수에서 접근합니다. 

다음 24~26번 줄 코드를 분석하겠습니다.
24 worker->current_work = work;
25 worker->current_func = work->func;
26 worker->current_pwq = pwq;

24~25번 줄 코드에서 worker->current_work이란 멤버에 work를 등록하고 worker->current_func 함수 포인터에 워크 핸들러 함수를 지정합니다. 26번 줄 코드를 보면 worker->current_pwq에 워커 풀 주소를 저장합니다.

process_one_work() 함수에서 워크 핸들러는 worker->current_func 함수 포인터로 실행합니다. 이 코드 실행 전 워크 핸들러를 가르키는 work->func 포인터를 worker->current_func 에 저장하는 겁니다.

27번 째 줄 코드를 보겠습니다.
27 work_color = get_work_color(work);

get_work_color() 함수를 호출해서 struct work_struct->data 비트 중 color를 읽어 옵니다. 이 함수는 struct work_struct.data 멤버로 워크 실행 흐름을 점검할 때 알아볼 예정입니다.

29번 째 줄 코드를 보겠습니다.
29 list_del_init(&work->entry);

&work->entry 링크드 리스트를 초기화합니다. &work->entry는 워커풀의 work_list이란 링크드 리스트에 등록돼 있습니다. 이 연결을 끊는 동작입니다. 만약 코드가 실행하지 않으면 워커풀에 해당 워크를 계속 남아 있어 다른 워커 쓰레드가 이 워크를 실행합니다.

31번 줄 코드를 보겠습니다.
31 if (unlikely(cpu_intensive))
32 worker_set_flags(worker, WORKER_CPU_INTENSIVE);

cpu_intensive이란 전역 변수 필드가 1로 설정됐으면 워커의 플래그를 WORKER_CPU_INTENSIVE로 설정합니다. 

34번 줄 코드를 보겠습니다.
34 if (need_more_worker(pool))
35 wake_up_worker(pool);

만약 워커 풀에 워커 쓰레드가 없는 경우 wake_up_worker() 함수를 호출해서 워커 쓰레드 생성을 유도합니다.  

다음 37번 줄 코드를 보겠습니다. 
37 set_work_pool_and_clear_pending(work, pool->id);

struct work_struct.data 멤버에 워커 풀 아이디를 설정하고 pending 비트를 Clear합니다. 


set_work_pool_and_clear_pending() 함수는 조금 후에 더 알아 볼 예정입니다.


이제 process_one_work() 함수에서 가장 중요한 코드를 볼 차례입니다.
45 trace_workqueue_execute_start(work);
46 worker->current_func(work);
47
48 trace_workqueue_execute_end(work);

46번 줄 코드를 먼저 보겠습니다. worker->current_func에 등록된 워크 핸들러 함수를 호출합니다.

46번 줄 코드 앞 뒤로 trace_workqueue_execute_start() 와 trace_workqueue_execute_end() 함수가 있습니다. 각각 함수들은 ftrace에서 workqueue_execute_start와 workqueue_execute_end 이벤트가 켜져 있으면 ftrace 로그를 출력합니다.

위 ftrace 이벤트를 키고 라즈베리파이에서 ftrace 로그를 받으면 다음과 같은 메시지를 확인할 수 있습니다.
kworker/0:3-66 [000] .... 169.351355: workqueue_execute_start: work struct b9c1aa30: function mmc_rescan
kworker/0:3-66 [000] .... 169.351554: workqueue_execute_end: work struct b9c1aa30

위 로그는 mmc_rescan() 이란 함수가 워크 핸들러로 실행하는데 이 함수가 실행된 시점과 실행 시간을 알 수 있습니다. mmc_rescan() 이란 워크 핸들러 함수 실행 직전 타임스탬프는 169.351355이고 실행이 끝난 타임스탬프는 169.351554이니 총 0.000199(169.351554-169.351355)초 동안 실행됐음을 알 수 있습니다.

워크큐를 디버깅할 때 이 ftrace 메시지를 많이 참고하니 이 코드가 어디서 실행하는지 알아 둘 필요가 있습니다. 

예를 들어 실제 리눅스 커널 디버깅을 진행할 때 다음과 같은 메시지를 볼 수 있습니다.
첫째, workqueue_execute_start ftrace 로그가 찍히고 2초 후 workqueue_execute_end가 출력됐다면 워크 핸들러 실행 시간이나 워크 핸들러 실행 빈도를 점검할 필요가 있습니다. 

둘째, 어떤 워크에 대해서 workqueue_execute_start 로그 이후 workqueue_execute_end 로그가 안 보이면 해당 워크 핸들러에서 휴면에 빠졌다고 판단할 수 있습니다.

다음 52번 줄 코드를 보겠습니다.
52 if (unlikely(in_atomic() || lockdep_depth(current) > 0)) {
53 pr_err("BUG: workqueue leaked lock or atomic: %s/0x%08x/%d\n"
54        "     last function: %pf\n",
55        current->comm, preempt_count(), task_pid_nr(current),
56        worker->current_func);
57 debug_show_held_locks(current);
58 dump_stack();
59 }

예외 처리를 점검하는 코드입니다. 만약 현재 실행 중인 코드가 원자적 처리(Atomic Operation)이나 Lock 값이 0보다 크면 경고 메시지를 출력합니다.

원자적 처리의 대표적인 예가 인터럽트 컨택스트입니다. 인터럽트 컨택스트에서는 워커 쓰레드가 실행하면 안됩니다.

이런 예외 처리 코드를 보면 무심코 지나치지 말고 꼼꼼히 볼 필요가 있습니다. 여러분이 드라이버 개발 도중 이런 예외 처리 코드를 작성할 때 참고하면 아주 좋습니다.

다음 70 번째 줄 코드를 보겠습니다.
70 hash_del(&worker->hentry);

워크 핸들러를 실행했으니 워크 실행을 위해 등록했던 멤버들을 초기화하는 단계입니다. worker->hentry 멤버를 초기화합니다.

다음, 71~73번 줄 코드를 보겠습니다.
71 worker->current_work = NULL;
72 worker->current_func = NULL;
73 worker->current_pwq = NULL;

역시 struct worker 멤버(current_work, current_func, current_pwq)들을 NULL로 초기화하는 코드입니다.

이 코드로 워크 핸들러가 실행하면 워크를 해제한다는 점을 알 수 있습니다. 다시 워크를 실행해서 워크 핸들러를 호출하려면 schedule_work() 함수를 다시 호출해야 한다는 점을 알 수 있습니다. 

동적 타이머도 마찬가지로 동적 타이머 핸들러 함수를 실행하면 동적 타이머는 만료합니다.

여기까지 워크를 초기화하고 워크를 워크큐에 큐잉하고 나면 워크를 실행하는 코드 흐름까지 알아봤습니다. 이 동작을 설명하면서 워크를 실행하는 것은 워커 쓰레드라고 했습니다. 그럼 워커 쓰레드가 무엇인지 알아봐야 합니다. 다음 절에서 알아보겠습니다.

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


핑백

덧글

댓글 입력 영역