Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

89258
1323
114591


[라즈베리파이] 워크큐(Workqueue) - 워크를 워크큐에 어떻게 큐잉할까?(2) 8. Workqueue


이번에는 28줄 코드를 보겠습니다.
28 last_pool = get_work_pool(work);
struct work_struct 구조체인 work 변수로 get_work_pool() 함수를 호출해서 struct worker_pool 구조체 주소를 last_pool 지역변수로 읽습니다. get_work_pool() 함수는 조금 후 분석할 예정입니다. 이 코드만 보면 이해하기 쉽지 않으니 다음 그림을 같이 보겠습니다.


워크를 실행한 적이 있으면 struct work_struct.data 란 멤버 변수에 풀워크 주소를 저장합니다. get_work_pool() 함수는 위 그림에서 [1],[2] 번호와 같이 동작하면서 워커풀 주소를 가져옵니다.

다음 29번 줄 코드를 분석하겠습니다.
28 last_pool = get_work_pool(work);
29 if (last_pool && last_pool != pwq->pool) {
30 struct worker *worker;
31
32 spin_lock(&last_pool->lock);
33
34 worker = find_worker_executing_work(last_pool, work);
35
36 if (worker && worker->current_pwq->wq == wq) {
37 pwq = worker->current_pwq;
38 } else {
39 /* meh... not running there, queue here */
40 spin_unlock(&last_pool->lock);
41 spin_lock(&pwq->pool->lock);
42 }
43 } else {
44 spin_lock(&pwq->pool->lock);
45 }

29번 줄부터 시작하는 조건문은 큐잉하는 워크를 이미 워커풀에서 실행한 적이 있는지 점검합니다. 워크를 처음 실행하면 struct work_struct.data에 풀워크 주소를 저장하여 이를 통해 워커풀 주소에 접근할 수 있습니다. 이미 워크를 처리한 워커 쓰레드가 있으면 해당 워커 쓰레드를 재사용하려는 의도입니다. 

34번 줄 코드는 이 조건을 점검합니다.
34 worker = find_worker_executing_work(last_pool, work);
35
36 if (worker && worker->current_pwq->wq == wq) {
37 pwq = worker->current_pwq;

find_worker_executing_work() 함수를 호출해서 워크를 실행한 워커 쓰레드 구조체 주소를 worker란 지역변수에 저장합니다. 워커 쓰레드 구조체 worker->current_pwq->wq에 저장된 워크큐 주소와 지금 큐잉하는 워크에 대응하는 워크큐와 같으면 struct pool_workqueue 구조체 주소를 가져옵니다. 위 그림에서 [3] 번호에 대응하는 동작입니다. 

워크를 여러 개의 워커 쓰레드에서 처리하지 못하는 제약 조건을 둔 겁니다. get_work_pool()와 find_worker_executing_work() 함수는 조금 후 더 알아볼 예정입니다.

59번 줄 코드를 보겠습니다.
59 trace_workqueue_queue_work(req_cpu, pwq, work);
60
61 if (WARN_ON(!list_empty(&work->entry))) {
62 spin_unlock(&pwq->pool->lock);
63 return;
64 }

59줄 코드는 ftrace로 workqueue_queue_work이란 워크큐 이벤트를 키면 실행하는 코드입니다. 다음과 같은 ftrace 로그를 출력합니다. 
make-13395 [000] d.s.   589.830896: workqueue_queue_work: work struct=b9c1aa30 function=mmc_rescan workqueue=b9c06700 req_cpu=4 cpu=0
make-13395 [000] d.s.   589.830897: workqueue_activate_work: work struct b9c1aa30

위 로그를 보면 struct work_struct 구조체 주소인 b9c1aa30와 워크 핸들러 함수인 mmc_rescan()를 볼 수 있습니다. 워크를 워크큐에 큐잉했다는 정보입니다.

schedule_work() 함수를 호출하면 워크를 보통 워크큐에 큐잉했다고 짐작합니다. 하지만 위 ftrace 로그에서 workqueue_queue_work와 workqueue_activate_work 메시지를 확인하기 전까지 제대로 동작할 것이라 너무 확신하면 안됩니다. 특정 상황에서 워크를 워크큐에 큐잉하는 schedule_work() 함수를 호출해도 워크가 워크큐에 제대로 큐잉을 못할 수도 있기 때문입니다. 

그래서 새로운 워크를 선언한 다음 워크큐에 큐잉하는 코드를 작성했을 때 위와 같은 ftrace 메시지를 확인할 필요가 있습니다. 

실력있는 개발자가 되려면 자신이 구현한 드라이버 코드가 제대로 동작하는지 점검하는 방법도 알고 있어야 합니다. 

다음 68번째 줄 코드를 분석하겠습니다.
68 if (likely(pwq->nr_active < pwq->max_active)) {
69 trace_workqueue_activate_work(work);
70 pwq->nr_active++;
71 worklist = &pwq->pool->worklist;
72 if (list_empty(worklist))
73 pwq->pool->watchdog_ts = jiffies;
74 } else {
75 work_flags |= WORK_STRUCT_DELAYED;
76 worklist = &pwq->delayed_works;
77 }

74번째 줄 코드는 워크를 처리할 워커 개수가 255개를 넘어섰을 때 동작합니다. 이때 struct pool_workqueue 구조체 멤버 중 delayed_works인 링크드 리스트를 worklist 지역 변수에 저장합니다. 여기서 보이는 delayed_works는 딜레이 워크(struct delayed_work)와 다른 개념입니다. 실행 중인 워커 개수가 많다는 것은 그만큼 처리해야 할 워크가 많다는 것을 의미합니다. 그래서 delayed_works 이란 링크드 리스트(struct list_head) 멤버에 워크를 등록하고 적절한 시점 이후 이 워크를 처리합니다.

딜레이 워크(struct delayed_work)는 특정 시각 후 워크를 실행합니다. 위 코드의 delayed_works와 딜레이 워크는 헷갈릴 수 있으니 주의하시길 바랍니다.

정리하면, 특정 상황에서 커널 서브 시스템이나 드라이버에서 워크를 아주 많이 생성해서 워크를 처리할 워커 개수가 255개를 넘어섰을 때를 제외하고 보통69~73번 줄 코드를 실행합니다.

if~else 문 아래 78번줄 이후 코드는 69~73번 코드가 실행했다고 가정하고 분석하겠습니다.

if~else 문이 실행하는 조건을 점검했으니 69번 줄 코드부터 분석을 시작합니다.

69번 줄 코드는 ftrace에서 workqueue_activate_work 이벤트를 키면 다음과 같은 로그를 출력합니다. 코드와 ftrace 로그를 함께 보겠습니다.
69 trace_workqueue_activate_work(work);
..
make-13395 [000] d.s.   589.830896: workqueue_queue_work: work struct=b9c1aa30 function=mmc_rescan workqueue=b9c06700 req_cpu=4 cpu=0
make-13395 [000] d.s.   589.830897: workqueue_activate_work: work struct b9c1aa30

워크를 표현하는 struct work_struct 자료구조 주소를 출력하는데 보통 workqueue_queue_work ftrace 로그 이후에 출력합니다.

70번째 줄 코드는 현재 실행 중인 워커 개수를 1만큼 증감합니다.
70 pwq->nr_active++;

71번과 76번째 줄 코드를 함께 보겠습니다.
71 worklist = &pwq->pool->worklist; 
...
76 worklist = &pwq->delayed_works;

71번째 줄 코드를 분석하겠습니다.
pwq 변수는 struct pool_workqueue 구조체이고 pool은 struct worker_pool 구조체입니다. &pwq->pool 코드로 struct worker_pool 구조체에 접근해서 worklist 멤버를 worklist 지역 변수에 저장하는 코드입니다.

76번은 struct pool_workqueue 구조체인 pwq 변수를 통해 delayed_works란 멤버 주소를 worklist 지역 변수로 가져옵니다.

코드만 보면 자료구조를 어떻게 처리하는지 이해하기 어려우니 다음 그림으로 각각 코드가 어떻게 동작하는지 확인할 수 있습니다.

[1] &pwq->pool->worklist는 71번 줄 [2] &pwq->delayed_works는 76번 줄 코드를 의미합니다. 워크큐 자료 구조를 변경하는 코드를 볼 때 이렇게 전체 자료 구조 흐름을 머릿속으로 그리면서 분석하면 이해가 더 빠릅니다.

이번에는 79번 줄 코드를 보겠습니다. insert_work() 함수에 전달하는 주요 인자들을 살펴보고 insert_work() 함수 분석을 하겠습니다.
79 insert_work(pwq, work, worklist, work_flags);

pwq 는 per-cpu 타입인 struct pool_workqueue 구조체 주소를 담고 있고, work는 _queue_work() 함수로 워크큐에 큐잉하려는 워크입니다. worklist는 &pwq->pool->worklist 코드로 저장된 struct list_head 구조체인 링크드 리스트입니다. 

insert_work() 함수를 호출해서 &pwq->pool->worklist 이란 링크드 리스트에 워크 링크드 리스트(struct work_struct.entry)를 등록합니다. insert_work() 함수에서 wake_up_worker() 이란 함수를 호출해서 워크를 실행할 워커 쓰레드를 깨웁니다.

여기까지 워크를 큐잉하는 흐름을 알아봤습니다. 다음에 insert_work() 함수를 상세히  분석합니다.

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


핑백

덧글

댓글 입력 영역