drivers/tty/tty_buffer.c
static void flush_to_ldisc(struct work_struct *work)
{
struct tty_port *port = container_of(work, struct tty_port, buf.work);
struct tty_bufhead *buf = &port->buf;

mutex_lock(&buf->lock);

// check point to get dump

이번 소절에서는 워크큐를 이루는 주요 개념을 소개합니다.

 -   워크

 -   워커스레드

 -   워커풀

 -   풀워크큐

 

먼저 워크큐의 기본 실행 단위인 워크를 배워볼까요?

 

워크란

워크는 워크큐를 실행하는 단위입니다. 워크는 누가 실행할까요? 워크는 실행하는 주인공은 워커 스레드입니다. 인터럽트 후반부 처리나 지연해야 할 작업을 워크에서 실행하는 것입니다. 

 

 

리눅스 커널에서는 워크를 work라고도 부릅니다. 이 책에서는 편의상 워크라고 명시하겠습니다. 

 

다음 그림을 보면서 워크에 대해 배워볼까요?

 

[그림 1] 워크 실행 흐름도

 

워크의 처리 흐름은 위 그림과 같이 3단계로 분류할 수 있습니다.

 

1단계부터 알아볼까요? 그림에서 ①으로 표시된 부분입니다. 

워크를 실행하려면 먼저 워크를 워크큐에 큐잉해야 합니다. 이를 위해 schedule_work() 함수를 호출해야 합니다.

 

이어서 그림에서 ②로 표시된 부분을 눈으로 따라가 볼까요? 2단계 동작입니다. wake_up_worker() 함수를 호출해 워크를 실행할 워커 스레드를 깨웁니다.  

 

마지막 3단계 동작을 알아볼까요? 그림 가장 오른쪽 부분입니다. 워커 스레드에서 워크를 실행합니다. 이렇게 워크 후반부 처리는 워크에서 지정한 워크 핸들러가 담당합니다.

 

이번엔 다른 그림을 보면서 워크에 대해 알아봅시다.

 

[그림 2] 인터럽트 후반부를 처리하기 위해 워크를 실행하는 흐름도 

 

위 그림은 인터럽트 후반부를 워크에서 처리하는 흐름도입니다.

 

그림에서 ①으로 표시된 부분을 보면 인터럽트 핸들러인 bcm2835_mbox_irq() 함수에서 워크를 워크큐에 큐잉합니다. 인터럽트를 핸들링 할 때 빨리 처리해야 할 코드는 bcm2835_mbox_irq() 함수에서 처리하고 인터럽트 후반부는 워크가 실행하는 것입니다. 

 

다음 ②번으로 표시된 부분을 볼까요?

워크를 큐잉하고 난 다음 wake_up_worker() 함수를 호출해 워커 스레드를 깨웁니다.

스케줄러는 이미 런큐에 큐잉된 다른 프로세스와 우선 순위를 체크한 다음 워커 스레드를 실행시킵니다.

 

마지막 ③ 단계입니다. 워커 스레드가 깨어나면 스레드 핸들인 worker_thread() 함수가 일을 시작합니다. 화살표 방향으로 함수가 실행되며 process_one_work() 함수에서 워크 핸들러인 bcm2835_mbox_work_callback() 함수를 호출합니다.

 

인터럽트 핸들링 관점으로 인터럽트 후반부를 처리하는 코드를 2단계로 나눌 수 있습니다. 빠르게 실행해야 할 코드는 인터럽트 핸들러에서 처리하고 인터럽트 후반부 처리 코드는 워크 핸들러에서 실행하는 것입니다. 즉, 인터럽트 핸들러에서 처리하지 못한 동작을 bcm2835_mbox_work_callback() 함수에서 실행하는 것입니다.

 

이렇게 워크는 워커 스레드를 프로세스 컨택스트에서 실행하므로 워크 핸들러 실행 도중 휴면에 진입할 수 있습니다. 그래서 커널에서 지원하는 모든 함수를 다 쓸 수 있습니다.

 

우선 워크를 다음과 같이 정리해볼까요?

 

     “커널 후반부를 처리하는 단위이며 워크 핸들러 실행 도중 휴면에 진입할 수 있다.”

     “워크는 워커 스레드가 실행한다.”

 

워크에 대해 소개했으니 이어서 워크를 실행하는 워커 스레드를 알아볼까요?

 

워커 스레드란

워커 스레드는 워크를 실행하고 워크큐 관련 자료구조를 업데이트하는 작업을 수행하는 커널 스레드입니다. 즉, 워크큐 전용 프로세스입니다. 워커 스레드 세부 동작을 보려면 어떤 함수를 봐야 할까요? 스레드 핸들인 worker_thread() 함수를 봐야 합니다. 참고로 커널 스레드의 세부 동작은 스레드 핸들 함수에 구현돼 있습니다. 

 

그러면 라즈베리파이에서 워커 스레드를 확인해볼까요? 터미널을 열고 "ps -ely | grep kworker" 명령어를 입력해봅시다. 

root@raspberrypi:/# ps -ely | grep kworker

S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD

I     0     4     2  0  60 -20     0     0 worker ?        00:00:00 kworker/0:0H

I     0    16     2  0  60 -20     0     0 worker ?        00:00:00 kworker/1:0H

I     0    21     2  0  60 -20     0     0 worker ?        00:00:00 kworker/2:0H

I     0    26     2  0  60 -20     0     0 worker ?        00:00:00 kworker/3:0H

I     0    29     2  0  80   0     0     0 worker ?        00:00:00 kworker/0:1

I     0    30     2  0  80   0     0     0 worker ?        00:00:00 kworker/1:1

 

워커 스레드 이름은 “kworker/” 으로 시작하며 뒷 쪽에 숫자가 붙습니다. 예를 들어 위에서 소개한 가장 마지막 워커 스레드 이름은 “kworker/1:1”입니다. 뒤에 1:1 이란 숫자 보입니다.

 

워커 스레드 이름은 워커 스레드를 생성하는 create_worker() 함수를 실행할 때 정해집니다.

 

그러면 워커 스레드에 대해서 다음 내용으로 소개를 마쳐 보겠습니다. 

 

     “워커 스레드는 워크를 실행하는 임무를 수행하는 커널 스레드다.”

     “워커 스레드 핸들 함수는 worker_thread() 함수이다.”

 

워커풀이란

워크를 실행하려면 먼저 워크큐에 큐잉을 해야 하고 워크를 워커 스레드가 실행한다고 설명을 드렸습니다. 워커풀은 워크와 워커 스레드를 관리하는 역할을 수행하며 워크큐의 핵심 자료 구조 중 하나입니다.

 

워커풀 자료구조는 struct worker_pool 구조체이며 선언부는 다음과 같습니다.

[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/workqueue.c]

01 struct worker_pool { 

02 spinlock_t lock; 

03 int cpu; 

...

04 struct list_head worklist; 

05 int nr_workers;

...

06 struct list_head workers;

 

04번째 줄에 보이는 worklist 필드로 큐잉한 워크 리스트를 관리합니다. 다음 06번째 줄 workers 필드로 워커 스레드를 관리합니다.

 

다음 워크큐 전체 자료구조 그림을 보면서 워커풀에 대해 알아볼까요? 

 

[그림 3] 워크큐 전체 자료구조에서 워커풀의 위치

 

위 그림 그림 가장 왼쪽 윗 부분에 표시한 struct workqueue_struct 구조체는 워크큐 전체를 제어하는 자료구조입니다. struct workqueue_struct 구조체 필드 가장 아랫 부분을 보면 cpu_pwqs 필드가 보입니다. 필드 왼쪽에 __percpu 키워드로 선언했으니 percpu 타입 변수란 사실을 알 수 있습니다. CPU 갯수만큼 struct pool_workqueue 구조체 공간이 있는 것입니다. 

 

①에 표시된 per_cpu_ptr() 함수는 percpu 오프셋을 알려줍니다. 이 오프셋으로 CPU 별로 할당된 메모리 주소를 찾을 수 있습니다. ① per_cpu_ptr() 아랫 부분을 보면 4개 struct pool_workqueue 구조체를 볼 수 있습니다. percpu 타입 변수이니 CPU 갯수만큼 메모리 공간이 있는 것입니다.

 

4개 struct pool_workqueue 구조체 박스 가장 아랫 부분을 보면 struct worker_pool 타입인 pool필드가 보입니다. ② 으로 표시된 부분을 보면 struct pool_workqueue 구조체 필드를 볼 수 있습니다.

 

③ 으로 표시된 worklist는 워크큐에 큐잉한 워크 리스트를 관리하는 필드이며 ④로 표시된 workers 필드는 워커 스레드를 관리하는 필드입니다.

 

정리하면 워커풀은 워크와 워커 스레드를 총괄하는 중요한 역할을 수행합니다.

 

풀워크큐란 

 

[그림 3] 가장 왼쪽 윗부분에서 본 struct workqueue_struct 구조체 cpu_pwqs 필드를 풀워크큐라고 부릅니다.

[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/workqueue.c]

struct workqueue_struct {

struct list_head pwqs; /* WR: all pwqs of this wq */

struct list_head list; /* PR: list of all workqueues */

...

struct rcu_head rcu;

 

/* hot fields used during command issue, aligned to cacheline */

unsigned int flags ____cacheline_aligned; 

struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */

struct pool_workqueue __rcu *numa_pwq_tbl[]; 

};

 

per-cpu 타입 변수라 CPU 갯수만큼 struct pool_workqueue 구조체가 있습니다.

 

풀워크큐 세부 내용을 이 책의 범위를 넘어서니 풀워크큐는 워커풀로 워크와 워커 스레드를 관리한다는 정도로 알아둡시다. 

 

+ Recent posts