• 2023. 9. 3.

    by. 도 현

    반응형

    쿼크큐는 리눅스 디바이스 드라이버와 커널의 핵심 서브시스템에서 많이 적용하는 기법이다. 워크큐는 주로 인터럽트 후반부를 처리하는 태스크 큐를 개선하기 위해 커널 2.5.41 버전부터 도입됐다. 워크큐는 특정 코드를 워크 핸들러에 위치시켜 프로세스 컨텍스트로 처리하기 위한 다양한 함수를 지원한다. 커널은 백그라운드에서 워크를 실행하기 위한 워커 스레드를 미리 생성해 놓는다.

     

    1. 워크큐의 주요개념

    - 워크

    - 워커스레드

    - 워커 풀

    - 풀워크큐

     

    워크란?

    워크(work)는 워크큐를 실행하는 단위이다. 워크를 실행하는 주인공은 워커 스레드이다. 인터럽트 후반부 처리나 지연해야 할 작업을 워크에서 실행하는 것이다. 워크의 처리 흐름은 3단계로 분류할 수 있다. 워크를 실행하려면 먼저 워크를 워크큐에 큐잉해야 한다. 이를 위해서는 schedule_work() 함수를 호출해야 한다. 2단계 동작으로서 wake_up_worker() 함수를 호출해 워크를 실행할 워커 스레드를 깨운다. 마지막으로 3단계 동작은 워커 스레드에서 워크를 실행한다. 이렇게 해서 워크 후반부 처리는 워크에서 지정한 워크 핸들러가 담당한다. 인터럽트 후반부를 워크에서 처리하는 흐름을 나타낸 것이다. 인터럽트 핸들러인 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() 함수를 봐야 한다. 참고로 커널 스레드의 세부 동작은 스레드 핸들러 함수에 구현돼 있다. 워커 스레드는 리눅스 시스템에서 백그라운드로 실행되는 친숙한 프로세스이다. 워커 스레드의 이름은 "kworker/"로 시작하며 워크큐의 종류에 따라 "kworker/" 다음에 번호를 부여한다. 

     

    워커 풀이란?

    워커 풀은 워크큐의 핵심 자료구조로서 큐잉한 워크 리스트를 관리, 워커 스레드를 생성하면서 관리하는 역할을 수행한다. 그래서 커널은 현재 큐잉한 전체 워크와 실행 중인 워커 스레드의 개수를 워커 풀에게 물어본다. worklist 필드로 큐잉한 워크 리스트를 관리한다. 디바이스 드라이버에서 워크를 큐잉하면 워커 풀 구조체의 필드 중 연결 리스트 타입인 worklist에 등록하는 것이다. workers 필드로 워커 스레드를 관리한다. 마찬가지로 워커 스레드를 이 연결 리스트로 관리한다. workqueue_struct 구조체는 워크코ㅠ 전체를 제어하는 자료구조이다. workqueue_struct 구조체의 필드 중 맨 아랫부분을 보면 cpu_pwqs 필드가 보일 것이다. 필드 왼쪽에 __percpu 키보드로 선언했으니 percpu 타입의 변수라는 사실을 알 수 있다. CPU 코어의 개수만큼 pool_workqueue 구조체 공간이 있는 것이다. per_cpu_ptr() 함수는 percpu 오프셋을 알려준다. 이 오프셋으로 CPU별로 할당된 메모리 주소를 찾을 수 있다. per_cpu_ptr()의 아랫부분을 보면 4개의 pool_workqueue 구조체를 볼 수 있는데 percpu 타입의 변수이니 CPU 코어의 개수만큼 메모리 공간이 있는 것이다. 4개의 pool_workqueue 구조체 박스 중 맨 아랫부분을 보면 struct worker_pool 타입인 pool 필드가 보일 것이다. worklist는 워크큐에 큐잉한 워크들의 리스트를 관리하는 필드이며 표시된 workers 필드는 워커 스레드를 관리하는 필드입니다. 정리하면 워커 풀은 워크와 워커 스레드를 총괄하는 중요한 역할을 수행한다.

     

    풀워크큐란?

    workqueue_struct 구조체의 cpu_pwqs 필드를 풀워크큐라고 부른다. workqueue_struct 구조체의 cpu_pwqs 필드는 percpu 타입 변수라서 CPU 코어의 개수만큼 pool, workqueue 구조체가 있다. 풀워크큐는 워커 풀을 통해 워크와 워커 스레드를 관리한다는 정도로 알아두면 된다.

    반응형