이번 시간에는 create_worker() 함수에서 호출한 워커 스레드 세부 제어 함수를 살펴보겠습니다.
worker_attach_to_pool() 함수 분석하기
worker_attach_pool() 함수는 워커를 워커풀에 연결하는 역할을 수행합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/workqueue.c]
01 static void worker_attach_to_pool(struct worker *worker,
02 struct worker_pool *pool)
03 {
04 mutex_lock(&pool->attach_mutex);
05
06 set_cpus_allowed_ptr(worker->task, pool->attrs->cpumask);
07 if (pool->flags & POOL_DISASSOCIATED)
08 worker->flags |= WORKER_UNBOUND;
09
10 list_add_tail(&worker->node, &pool->workers);
11
12 mutex_unlock(&pool->attach_mutex);
13 }
이 함수의 핵심 동작은 워커풀 구조체 struct worker_pool 필드인 worker(연결리스트)에 워커 struct worker 구조체 node 필드(연결리스트 타입)를 등록하는 것입니다.
10번째 줄 코드를 보겠습니다.
10 list_add_tail(&worker->node, &pool->workers);
&worker->node와 &pool->workers 변수 모두 struct list_head 타입인 연결 리스트입니다. &pool->workers 연결 리스트에 &worker->node를 등록합니다. 이 코드가 실행하면 다음 그림과 같이 자료 구조가 업데이트됩니다.

[그림 7.12] 워커를 워커풀에 등록 후 변경되는 자료구조
다른 관점으로 &pool->workers 연결 리스트는 &worker->node 주소를 가리키고 있습니다. 이후 &pool->workers 주소에 접근해서 struct worker 구조체 node 필드 오프셋을 빼서 struct work 구조체 주소에 접근합니다.
리눅스 커널에서는 이 방식을 아주 많이 씁니다. 링크드 리스트가 다른 자료구조 링크드 리스트 필드 주소를 가르키는 동작입니다. container_of 매크로 함수도 이 방식을 적용해서 구현한 것입니다.
worker_enter_idle() 함수 분석하기
worker_enter_idle() 함수는 워커의 flags를 WORKER_IDLE로 바꾸고 워커 정보를 업데이트합니다.
이어서 worker_enter_idle() 함수를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/workqueue.c]
1 static void worker_enter_idle(struct worker *worker)
2 {
3 struct worker_pool *pool = worker->pool;
4
5 if (WARN_ON_ONCE(worker->flags & WORKER_IDLE) ||
6 WARN_ON_ONCE(!list_empty(&worker->entry) &&
7 (worker->hentry.next || worker->hentry.pprev)))
8 return;
9
10 worker->flags |= WORKER_IDLE;
11 pool->nr_idle++;
12 worker->last_active = jiffies;
13
14 list_add(&worker->entry, &pool->idle_list);
15
16 if (too_many_workers(pool) && !timer_pending(&pool->idle_timer))
17 mod_timer(&pool->idle_timer, jiffies + IDLE_WORKER_TIMEOUT);
18
19 WARN_ON_ONCE(!(pool->flags & POOL_DISASSOCIATED) &&
20 pool->nr_workers == pool->nr_idle &&
21 atomic_read(&pool->nr_running));
22 }
이 함수의 핵심 동작은 10~14번째 줄이며 나머지는 예외 처리 코드입니다.
10 worker->flags |= WORKER_IDLE;
11 pool->nr_idle++;
12 worker->last_active = jiffies;
13
14 list_add(&worker->entry, &pool->idle_list);
10번째 코드를 봅시다.
10 worker->flags |= WORKER_IDLE;
worker->flags에 WORKER_IDLE 플래그 설정해 워커를 WORKER_IDLE 타입으로 설정합니다.
다음 11번째 줄 코드를 보겠습니다.
11 pool->nr_idle++;
12 worker->last_active = jiffies;
11번째 줄 코드는 워커 풀 nr_idle 필드를 +1만큼 증감합니다. nr_idle은 워커풀에 등록된 idle 워커 개수입니다.
다음 12번째 줄 코드는 worker->last_active에 jiffies를 저장합니다. 현재 시각 정보가 있는 jiffies로 워커가 마지막에 처리된 시각을 저장하는 것입니다.
여기까지 워커를 생성하는 create_worker() 중심으로 관련 함수를 분석했습니다. 워커를 만드는 create_worker() 함수에서 다음 함수를 호출한다는 사실을 알게 됐습니다. 각 함수별 세부 동작을 요약하면 다음과 같습니다.
kthread_create_on_node() 함수: "kworker/" 이름으로 워커 스레드 만듦
worker_attach_to_pool() 함수: 워커풀에 워커를 등록
worker_enter_idle() 함수: 워커 상태를 WORKER_IDLE로 바꿈
wake_up_process() 함수: 워커 스레드를 깨움
다음 절에서는 워커 스레드 핸들 함수인 worker_thread() 분석으로 워커 스레드 동작을 살펴보겠습니다.
최근 덧글