Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

106258
1323
114608


[라즈베리파이] 워크큐(Workqueue) - 워크는 누가 언제 호출하나? (2) @process_one_work 8. Workqueue

From...


다음 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(워크큐)
워크큐(Workqueue) Overview



덧글

댓글 입력 영역