Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[라즈베리파이] 워크큐(Workqueue) - 워크를 워크큐 큐잉시 핵심함수 분석 8. Workqueue

From:


이제부터 __queue_work() 함수에서 호출하는 다른 워크큐 주요 함수들을 알아봅니다. 시스템 워크큐 뿐만 아니라 다른 워크큐에서도 이용하는 함수들이니 잘 기억할 필요가 있습니다.

get_work_pool() 함수
get_work_pool() 함수 분석을 시작하겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue.c#L709]
1 static struct worker_pool *get_work_pool(struct work_struct *work)
2 {
3 unsigned long data = atomic_long_read(&work->data);
4 int pool_id;
5
6 assert_rcu_or_pool_mutex();
7
8 if (data & WORK_STRUCT_PWQ)
9 return ((struct pool_workqueue *)
10 (data & WORK_STRUCT_WQ_DATA_MASK))->pool;
11
12 pool_id = data >> WORK_OFFQ_POOL_SHIFT;
13 if (pool_id == WORK_OFFQ_POOL_NONE)
14 return NULL;
15
16 return idr_find(&worker_pool_idr, pool_id);
17}
18
19 static void insert_work(struct pool_workqueue *pwq, struct work_struct *work,
20 struct list_head *head, unsigned int extra_flags)
21 {
22 struct worker_pool *pool = pwq->pool;
23
24 set_work_pwq(work, pwq, extra_flags);
...
}

3번 줄 코드를 보겠습니다.
3 unsigned long data = atomic_long_read(&work->data);

data이란 지역 변수에 struct work_struct.data 멤버를 저장합니다.

다음 8번 줄 코드입니다.
8 if (data & WORK_STRUCT_PWQ)
9 return ((struct pool_workqueue *)
10 (data & WORK_STRUCT_WQ_DATA_MASK))->pool;

data 지역 변수와 WORK_STRUCT_WQ(4)와 AND 연산 결과가 true이면 코드를 실행합니다.

WORK_STRUCT_WQ enum은 다음 비트 연산으로 4입니다.
WORK_STRUCT_WQ(4) = (1 << 2) = (1 << WORK_STRUCT_PWQ_BIT)
각 enum 매크로 코드는 다음 해더 파일에 정의돼 있습니다.
[/include/linux/workqueue.h]
enum {
WORK_STRUCT_PWQ_BIT = 2, /* data points to pwq */
...
WORK_STRUCT_PWQ = 1 << WORK_STRUCT_PWQ_BIT,

그럼, struct work_struct.data 멤버에 WORK_STRUCT_PWQ 값을 언제 저장할까요?

__queue_work() 함수에서 호출된 insert_work() 함수를 볼 필요가 있습니다.
19 static void insert_work(struct pool_workqueue *pwq, struct work_struct *work,
20 struct list_head *head, unsigned int extra_flags)
21 {
22 struct worker_pool *pool = pwq->pool;
23
24 set_work_pwq(work, pwq, extra_flags);

24번 줄 코드를 보면 set_work_pwq() 함수를 호출합니다. struct work_struct.entry를 워커 풀에 등록하기 직전 struct work_struct.data 멤버를 WORK_STRUCT_PWQ 로 저장합니다.
static void set_work_pwq(struct work_struct *work, struct pool_workqueue *pwq,
 unsigned long extra_flags)
{
set_work_data(work, (unsigned long)pwq,
      WORK_STRUCT_PENDING | WORK_STRUCT_PWQ | extra_flags);
}

data 변수와 WORK_STRUCT_WQ_DATA_MASK(0xFFFF_FF00) 값과 AND 비트 연산한 결과를 
struct pool_workqueue 구조체로 캐스팅한 다음 struct pool_workqueue 멤버인 pool을 리턴합니다.

다음은 라즈베리파이에서 주기적으로 실행하는 flush_to_ldisc() 가 핸들러 함수인 워크를 Trace32로 확인한 내용입니다.


struct work_struct.data는 0xba340205입니다. 마지막 바이트가 5이니 WORK_STRUCT_PWQ(4)와  AND 연산한 결과는 1입니다. 또한 0xba340205 값과 WORK_STRUCT_WQ_DATA를 AND 연산하면 struct pool_workqueue 주소에 접근할 수 있습니다.

다음은 WORK_STRUCT_WQ_DATA_MASK(0xFFFF_FF00) enum값을 계산하는 과정입니다.
WORK_STRUCT_WQ_DATA_MASK = 0xFFFF_FF00

우선 다음 연산 과정으로 WORK_STRUCT_FLAG_MASK는 0xFF임을 확인했습니다. 그런데 WORK_STRUCT_WQ_DATA_MASK는 다음 연산으로 정의돼 있습니다.
WORK_STRUCT_WQ_DATA_MASK = ~WORK_STRUCT_FLAG_MASK

 ~은 비트 반전 연산자입니다. WORK_STRUCT_FLAG_MASK 를 2진수 기준으로 0은 1, 1은 0으로 비트 반전하는 계산 과정은 다음과 같습니다.
WORK_STRUCT_WQ_DATA_MASK = ~WORK_STRUCT_FLAG_MASK
~(0000 0000 0000 0000 0000 0000 1111 1111) - 이진수
 (1111 1111 1111 1111 1111 1111 0000 0000) - 이진수
0xFFFF_FF00  - 16진수

WORK_STRUCT_FLAG_MASK은 255이며 16진수 이진수별 값은 다음과 같습니다.
WORK_STRUCT_FLAG_MASK = 255 = 0xFF(16진수) = 1111 1111(이진수)

WORK_STRUCT_FLAG_MASK은 소스 코드를 보면 다음 규칙으로 계산합니다.
WORK_STRUCT_FLAG_MASK = (1UL << WORK_STRUCT_FLAG_BITS) - 1

1을 왼쪽으로 WORK_STRUCT_FLAG_BITS 값만큼 비트 시프트한 결과에 1를 빼는 연산입니다.
연산 과정은 다음과 같습니다.
(1UL << WORK_STRUCT_FLAG_BITS) - 1
(1UL << 8) - 1
(256) - 1
 255

WORK_STRUCT_FLAG_BITS이 8이니 1을 왼쪽으로 비트 쉬프트 한 결과에 1을 뺀 결과가 WORK_STRUCT_FLAG_MASK인 겁니다.

다음은 WORK_STRUCT_FLAG_BITS enum 값을 계산할 차례입니다. 연산 결과 WORK_STRUCT_FLAG_BITS는 8입니다.
WORK_STRUCT_FLAG_BITS = 8

WORK_STRUCT_FLAG_BITS은 두 매크로는 더한 값입니다.
WORK_STRUCT_FLAG_BITS = WORK_STRUCT_COLOR_SHIFT + WORK_STRUCT_COLOR_BITS

연산 과정은 다음과 같습니다.
WORK_STRUCT_COLOR_SHIFT + WORK_STRUCT_COLOR_BITS
WORK_STRUCT_FLAG_BITS = 8 = 4 + 4 = WORK_STRUCT_FLAG_BITS + WORK_STRUCT_COLOR_BITS

각 enum 정의문은 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h]
1 enum {
2 WORK_STRUCT_PENDING_BIT = 0, /* work item is pending execution */
...
3 #ifdef CONFIG_DEBUG_OBJECTS_WORK
4 WORK_STRUCT_STATIC_BIT = 4, /* static initializer (debugobjects) */
5 WORK_STRUCT_COLOR_SHIFT = 5, /* color for workqueue flushing */
6 #else
7 WORK_STRUCT_COLOR_SHIFT = 4, /* color for workqueue flushing */
8 #endif
9 WORK_STRUCT_COLOR_BITS = 4,
...
10 WORK_STRUCT_FLAG_BITS = WORK_STRUCT_COLOR_SHIFT + WORK_STRUCT_COLOR_BITS,
...
11 WORK_STRUCT_FLAG_MASK = (1UL << WORK_STRUCT_FLAG_BITS) - 1,
12 WORK_STRUCT_WQ_DATA_MASK = ~WORK_STRUCT_FLAG_MASK,
...
};

struct work_data.data 멤버를 WORK_OFFQ_POOL_SHIFT(5) 만큼 오른쪽으로 쉬프트한 결과를 pool_id에 저장합니다.
WORK_OFFQ_POOL_SHIFT enum 매크로는 다음 코드로 치환되어 5라는 값을 저장합니다.
WORK_STRUCT_COLOR_SHIFT = 4,
WORK_OFFQ_FLAG_BITS = 1,
WORK_OFFQ_FLAG_BASE = WORK_STRUCT_COLOR_SHIFT,
WORK_OFFQ_POOL_SHIFT = WORK_OFFQ_FLAG_BASE + WORK_OFFQ_FLAG_BITS,

이렇게 워크큐는 enum으로 정의한 매크로 연산으로 비트 연산을 수행합니다. 워크큐는 32비트 및 64비트 아키텍처에서 호환성을 유지하면서 실행해야 하므로 이렇게 복잡하게 enum 매크로 연산을 하는 겁니다.

마지막 16줄 코드입니다.
16 return idr_find(&worker_pool_idr, pool_id);

pool_id 아이디로 워커 풀 주소를 읽어서 리턴하는 구문입니다. worker_pool_idr는 각 노드 별 워커풀이 등록됐음을 참고하세요.

find_worker_executing_work() 함수
이번에는 find_worker_executing_work() 함수를 분석하겠습니다.
1 static struct worker *find_worker_executing_work(struct worker_pool *pool,
2  struct work_struct *work)
3 {
4 struct worker *worker;
5
6 hash_for_each_possible(pool->busy_hash, worker, hentry,
7        (unsigned long)work)
8 if (worker->current_work == work &&
9     worker->current_func == work->func)
10 return worker;
11
12 return NULL;
13}

find_worker_executing_work() 함수 이름을 해석하면 두 번째 인자로 전달되는 실행 중인 워크 struct work_struct *work에 해당하는 워커를 찾아서 리턴한다는 의미입니다. 이 함수 리턴 타입은 워커를 표현하는 자료구조인 struct worker입니다.

 먼저 6번 줄 코드를 분석하겠습니다.
struct worker_pool.busy_hash이란 멤버에는 실행 중인 워커가 등록되어 있습니다. busy_hash는 64개 배열로 구성돼 있는데 이 자료구조는 다음 그림으로 설명할 수 있습니다.

위 그림에서 워크큐 전체 자료구조에서 워커풀이 있는지 머릿속으로 그리면서 분석할 필요가 있습니다.

struct worker_pool 구조체 멤버 중 busy_hash는64개 배열입니다. 6~7번 코드는 이 bush_hash 해시 테이블을 순회하면서 NULL이 아닌 주소를 읽습니다. 다시 반복하지만 busy_hash 해시 테이블에는 최근에 실행한 워커 주소가 저장돼 있습니다. 위 그림에서는 3번째 인덱스 해시 리스트 테이블에 워커 포인터가 등록돼 있습니다. 

현재 처리 중인 struct work_struct 워크 주소가 0xBCFE67F8 이고 워크 핸들러가 pm_runtime_work() 함수인 경우 busy_hash[3] 배열에서 있는 워커 주소를 읽습니다.

8~10번 줄 코드는 find_worker_executing_work() 함수에 전달하는 워크와 워크 핸들러를 워커 멤버인 worker->current_work 와 worker->current_func를 비교합니다.
8 if (worker->current_work == work &&
9     worker->current_func == work->func)
10 return worker;

비교해서 같으면 worker이란 struct worker 구조체 주소를 리턴합니다. 이미 워크를 실행할 워커가 있으면 해당 워커를 반환하는 동작입니다.

wake_up_worker
다음은 워크큐에서 가장 중요한 워커 쓰레드를 깨우는 wake_up_worker() 함수를 살펴보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue.c]
1 static void wake_up_worker(struct worker_pool *pool)
2 {
3 struct worker *worker = first_idle_worker(pool);
4
5 if (likely(worker))
6 wake_up_process(worker->task);
7}

first_idle_worker() 함수를 호출해서 워커 풀에 등록된 아이들 워커를 가져옵니다.

first_idle_worker() 함수 구현부를 보면 다음 6번 줄 코드와 같이 &pool->idle_list 주소에 접근해서 struct worker 구조체 구조를 읽어 옵니다.
1 static struct worker *first_idle_worker(struct worker_pool *pool)
2 {
3 if (unlikely(list_empty(&pool->idle_list)))
4 return NULL;
5
6 return list_first_entry(&pool->idle_list, struct worker, entry);
7 }

&pool->idle_list는 링크드 리스트입니다. 이 링크드 리스트는 struct worker 이란 워커 구조체의 entry 주소를 가리키고 있습니다. &pool->idle_list 주소에서 struct worker.entry 주소를 빼서 struct worker 자료 구조 주소를 읽는 코드입니다.

다음은 5~6번 줄 코드를 볼 차례입니다.
5 if (likely(worker))
6 wake_up_process(worker->task);

워커 풀에서 아이들 워크를 읽어서 해당 워커 쓰레드를 깨웁니다. struct worker.task 멤버 타입은 struct task_struct으로 프로세스를 표현하는 태스크 디스크립터입니다.

wake_up_worker() 함수는 워커 쓰레드를 깨우는 중요한 함수입니다. 이 함수를 호출하면 워커 쓰레드가 깨어나는 동작을 한다고 예상하고 코드를 볼 필요가 있습니다.

여기까지 다음 워크큐 핵심 커널 함수 코드 분석을 했습니다.
find_worker_executing_work()
set_work_pwq()
get_work_pool()
wake_up_worker()

위 함수들은 워크큐를 처리하는 어느 함수에서도 자주 쓰니 잘 알아 둘 필요가 있습니다. 또한 이 함수에서 쓰는 자료구조를 잘 활용하면 세련된 커널 디버깅 코드를 작성할 수 있습니다.

다음에 워크를 누가 언제 실행하는지 살펴 보겠습니다.

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


덧글

댓글 입력 영역