Skip to content

시스템 콜과 인터럽트

  • 커널은 시스템콜의 일관성, 유연성, 이식성​ 3가지를 확보하는 것을 최우선 사항으로 생각하고 있다.

  • 따라서 커널은 POSIX 표준에 따라 표준 C 라이브러리 형태로 시스템콜을 제공한다.

  • 리눅스 커널에서 시스템콜은 네 가지 역할을 수행한다.

    1. 사용자 공간에 HW 인터페이스를 추상화된 형태로 제공한다.
    2. 시스템에 보안성 및 안정성을 제공한다.
    3. 인터럽트(및 트랩)와 함께 커널에 접근할 수 있는 유일한 수단이다.
    4. 사용자 공간과 기타 공간을 분리해 프로세스별 가상환경(가상메모리, 멀티태스킹 등)을 제공한다.
  • 리눅스의 시스템콜은 다른 OS보다 상대적으로 갯수가 적고 수행속도가 빠르다.

    • 리눅스는 시스템콜 핸들러 호출 흐름이 간단하기 때문이다.
    • 리눅스는 context switching 속도가 빠르기 때문이다.
  • 리눅스의 시스템콜은 아래 파일에 정의되어있다.

    • include/linux/syscalls.h: 시스템콜 선언부

    • kernel/sys.c: 시스템콜 구현부

  • 간단한 시스템콜 예시인 getpid()의 구현을 통해 조금 더 내용을 알아보자.

    #define SYSCALL_DEFINE0(name) asmlinkage long sys_##name(void)
    SYSCALL_DEFINE0(getpid) // asmlinkage long sys_getpid(void);
    {
    return task_tgid_vnr(current); // return current->tgid
    }
    • getpid() 함수는 인자를 갖지 않기 때문에 맨 끝 숫자로 0이 붙은 SYSCALL_DEFINE매크로를 사용한다.

    • asmlinkage 지시자는 함수의 인자를 프로세스 스택 메모리에서 찾을 것을 컴파일러에게 명령한다.

    • 반환형이 long인 이유는 32-bit와 64-bit 시스템 사이의 호환성을 유지하기 위함이다.

    • 모든 리눅스 시스템콜은 sys_ 라는 접두사를 명명규칙으로 따른다.

  • 커널은 시스템콜을 ‘시스템콜 번호’라는 고유번호로 식별한다.

    • 시스템콜 번호는 한 번 할당 시 변경할 수 없다.
    • 한 번 할당된 시스템콜 번호는 대응하는 시스템콜이 제거된 후라도 재사용하지 않는다.
    • 모든 시스템콜 번호와 그 핸들러는 sys_call_table 이라는 상수 함수포인터 배열에 아키텍처별로 정의되어있다. (i.e. x86은 arch/x86/kernel/syscall64.c)
  • 커널이 시스템콜을 처리할 때는 ‘프로세스 컨텍스트’라는 특수한 컨텍스트에 진입한다.

    • 프로세스 컨텍스트에서 커널과 특정 프로세스는 같은 컨텍스트로 묶여 있는 상황이다.
    • 실행은 커널이 하고 있지만, current 매크로는 여전히 특정 프로세스를 가리키고 있다.
    • 실행은 커널이 하고 있지만, 여전히 sleep 가능하고 선점도 가능한 상태​다.
  • 사용자 애플리케이션이 시스템콜을 호출 했을 때,

    1. 대응하는 시스템콜을 호출 + 특정 레지스터에 시스템콜 번호 기록 → SW 인터럽트를 발생시킴.
    2. 커널이 시스템을 커널 모드로 전환함.
    3. system_call() 함수 호출 → 레지스터값이 유효한 시스템콜 번호인지 확인함.
    4. sys_call_table의 시스템콜 번호 인덱스의 함수를 호출해 적절한 핸들러를 호출함.
  • 이때 시스템콜의 매개변수는 레지스터를 통해 전달한다. (i.e. ARM의 R0R3, x86의 ebxedx, esi, edi)

  • 사용자는 직접 시스템콜을 구현할 수도 있다. (하지만, 절대 권장하지 않는다.)

  • 시스템콜을 설계할 때 반드시 고려해야 하는 점

    • 해당 시스템콜이 수행할 정확한 하나의 목적을 정의할 것
    • 해당 시스템콜의 인자, 반환값, 오류코드를 정의할 것
    • 해당 시스템콜이 추후 새로운 기능을 추가할 여지가 있는지 생각할 것 (유연성)
    • 해당 시스템콜이 하위 호환성을 깨지 않고 버그를 쉽게 수정할 수 있는지 생각할 것 (호환성)
    • 해당 시스템콜이 특정 아키텍처의 워드나 엔디안을 가정하고 만들지는 않았는지 점검할 것 (이식성)
  • 시스템콜을 구현할 때 반드시 고려해야 하는 점

    • 인자로 전달되는 포인터의 유효성을 확인할 것

      • 포인터는 사용자 공간의 메모리 영역을 가리켜야 한다.
      • 포인터는 프로세스 주소 공간의 메모리 영역을 가리켜야 한다.
      • 해당 메모리 영역의 권한에 맞는 접근을 해야 한다.
    • 수행결과를 반드시 알맞는 방법으로 제공할 것

      • 커널 영역의 수행 결과, 특히 포인터는 절대 사용자 공간으로 내보내선 안 된다.
      • copy_to_user() 또는 copy_from_user() 두 함수를 이용해서 안전하게 정보를 전달 및 제공받아야 한다.
  • 사용자 시스템콜의 설계 및 구현이 완료됐다면 정식 시스템콜로 등록한다.

    • entry.S 파일의 시스템콜 테이블 마지막에 새로운 시스템콜을 추가한다.
    • asm/unistd.h 헤더파일에 새로운 시스템콜 번호를 추가한다.
    • 새로운 시스템콜의 핸들러를 구현한다.
    • 커널을 재컴파일 한다.

인터럽트

1. 인터럽트 개요

  • OS는 인터럽트를 구별하고 인터럽트가 발생한 HW를 식별 뒤 적절한 핸들러를 이용해 인터럽트를 처리한다.

  • 요점은 장치별로 특정 인터럽트가 지정되어있으며, 커널이 이 정보를 가지고 있다는 것이다.

  • 인터럽트 처리를 위해 커널이 호출하는 함수를 인터럽트 핸들러 또는 인터럽트 서비스 루틴(ISR)라고 부른다.

  • 인터럽트는 ‘인터럽트 컨텍스트’에서 실행되며 중간에 실행을 중단할 수 없다.

  • 인터럽트는 언제라도 발생할 수 있고 그동안 원래 실행흐름은 중단되므로 최대한 빨리 인터럽트 핸들러를 처리하고 복귀하는 것이 중요하다. 따라서 인터럽트 핸들러 실행시간은 가능한 짧은 것이 중요하다.

    • 하지만, 때로는 인터럽트 핸들러에서 처리해야 하는 작업이 많거나 복잡할 수 있다.
    • 이를 해결하기 위한 전략이 **전반부 처리(top-half) + 후반부 처리(bottom-half)**다.
      • 당장 실시간으로 빠르게 처리해야 하는 부분은 인터럽트 핸들러 내에서 처리를 하고,
      • 나중에 처리해도 되는 부분은 다른 프로세스로 따로 만들어 처리한다.

2. 인터럽트 핸들러 등록

linux/interrupt.h
typedef irq_handler_t (*irq_handler_t)(int, void *);
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char* name,
void* dev)
  • 인터럽트 핸들러는 <linux/interrupt.h>request_irq() 함수를 통해 등록할 수 있다.

    1. irq : IRQ 번호를 의미하며 보통 기본 페리페럴은 이 값이 하드코딩 되어있다. 다른 디바이스들은 탐색을 통해 동적으로 정해진다.

    2. handler : 인터럽트를 처리할 인터럽트 핸들러의 함수 포인터다.

    3. flags :

      • IRQF_DISABLED : 인터럽트 핸들러를 실행하는 동안 모든 인터럽트를 비활성화한다.
      • IRQF_SAMPLE_RANDOM : 커널 내에 난수 발생기의 엔트로피에 이번 인터럽트 이벤트를 포함시킬지 여부를 결정한다. 포함한다면 조금 더 무작위인 난수가 생성될 것이다.
      • IRQF_TIMER : 이 핸들러가 시스템 타이머를 위한 인터럽트를 처리하는 핸들러임을 명시한다.
      • IRQF_SHARED : 이 핸들러가 여러 인터럽트가 공유하는 핸들러임을 명시한다.
    4. name : 개발자가 식별하기 위한 인터럽트 핸들러의 이름이다.

    5. dev : 같은 인터럽트를 사용하는 여러 핸들러 사이에서 특정 핸들러를 구별하기 위해 고유 쿠키값을 정의한다. 핸들러가 하나 뿐이라면 NULL을 사용해도 괜찮다.

  • 등록에 성공하면 request_irq() 함수는 0을 반환한다.

  • (참고) requeset_irq() 함수는 sleep 상태를 허용하는 함수다. 따라서 인터럽트 컨텍스트를 포함한 코드 실행이 중단돼서는 안 되는 상황에서는 호출할 수 없다. 내부적으로 proc_mkdir()proc_create()kmalloc() 함수를 호출하는데, 이 함수가 sleep이 가능하기 때문이다. ​

3. 인터럽트 핸들러 구현 및 동작순서

image
  1. 디바이스는 bus를 통해 인터럽트 컨트롤러로 전기신호를 전송한다.
  2. 인터럽트 컨트롤러는 프로세서의 특정 핀에 인터럽트를 건다.
  3. 프로세서는 하던 동작을 중단하고 작업 내용(및 context)을 스택에 저장한다.
  4. 미리 정해진 메모리 주소의 코드로 branch한다.
  5. 현재 발동된 인터럽트 라인을 비활성화 중복 인터럽트 발생을 예방하고, 유효한 핸들러가 등록돼있는지, 사용가능한지, 현재 미실행 상태인지 확인한다.
  6. <kernel/irq/handler.c>handle_IRQ_event()를 호출해 해당 인터럽트의 핸들러를 실행한다. 핸들러 실행 후 복귀했다면 정리작업을 수행하고 ret_from_intr()로 이동한다. 이 함수는 3, 4번처럼 아키텍처 특화 어셈블리로 작성되어있으며, 대기 중인 스케줄링 작업 존재 여부 확인 후 schedule() 함수를 호출해 원래 실행흐름으로 복귀한다.

4. 인터럽트 활성화/ 비활성화

  • 인터럽트 제어 기능은 아키텍처 의존적이며 동기화를 위해 인터럽트를 비활성화해 선점을 막기 위해 사용한다.
  • 보통 아래와 같은 구조를 가진다.
local_irq_disable();
/* 인터럽트가 비활성화 된 상태!
여기서 할일을 한다 ~~~ */
local_irq_enable();
  • 또는 void disable_irq(unsigned int irq), void enable_irq(unsigned int irq)로 특정 인터럽트를 마스킹하는 방법도 제공한다.

  • 하지만, 이것만으로 완전한 동기화를 보장하지는 못한다. 리눅스는 SMP 환경을 지원하므로 여기에 몇 가지 잠금 장치를 더 추가해주어야 한다.

5. 인터럽트 후반부 처리 (Bottom-half)

  • 어떤 일을 후반부로 지연시킬지 명확한 기준은 없지만, 실행시간에 민감하거나, 절대 선점되서는 안 되는 작업이 아니라면 후반부 처리로 넘길 것을 권장한다.

  • 후반부 처리의 가장 큰 요점은 모든 인터럽트가 활성화 된 상태에서, 시스템이 덜 바쁜 미래의 어떤 시점에 인터럽트의 나머지 부분을 처리해 시스템의 throughput을 극대화할 수 있다는 점이다.

  • 아랫부분에서 BH(Bottom-half), Softirq, Tasklet, WorkQueue 4가지 후반부 처리 기법에 대해서 알아볼 것이다. 결론부터 말하자면, 현재 2020년대에서는 주로 Threaded IRQ 또는 WorkQueue 2가지만 사용하고 나머지는 거의 사용하지 않는다. 리눅스 커널의 인터럽트 후반부 처리의 역사가 어떻게 발전했는지에 대해 알아보는 느낌으로 가볍게 살펴보도록 하자.

  • ① BH

    • 가장 먼저 만들어진 후반부 처리 기법으로, 정적으로 정의된 32개의 후반부 처리기가 존재했다.
    • 인터럽트 핸들러에서 32-bit 전역 변수의 bit를 조작해 나중에 실행할 후반부 처리기를 지정했다.
    • 서로 다른 프로세서가 두 개의 BH를 동시에 실행할 수 없었고, 유연성이 떨어졌고, 병목현상이 발생했다.
    • 성능이슈, 확장성 이슈, 이식성 이슈로 인해 더 이상 BH를 사용하기 어려워져 2.5버전 이후로 사라졌다.
  • ② Softirq

    • 모든 프로세서에서 동시에 사용할 수 있는 정적으로 정의된 후반부 처리기의 모음집이다.

    • 실행시간에 매우 민감하고 중요한 후반부 처리(i.e. 네트워크, Block I/O)를 해야 할 때 사용한다.

    • 같은 유형의 softirq가 다른 프로세서에서 동시에 실행될 수 있으므로 ​각별한 주의가 필요하다.

      • softirq 핸들러에서 만일 공유변수를 사용한다면, 적절한 락이 필요하다는 뜻이다.

      • 하지만, 동시 실행을 막는다면, softirq를 사용하는 의미가 상당 부분 사라지기 때문에 tasklet을 사용하는 것이 낫다.

      • 꼭 softirq를 써야만 하는 이유가 반드시 있는 것이 아니라면, 거의 모든 경우 tasklet을 사용하는 것을 권장한다.

        // https://github.com/torvalds/linux/blob/bee0e7762ad2c6025b9f5245c040fcc36ef2bde8/include/linux/interrupt.h#L588
        struct softirq_action
        {
        void (*action)(struct softirq_action *);
        };
        ...
        // https://github.com/torvalds/linux/blob/bee0e7762ad2c6025b9f5245c040fcc36ef2bde8/kernel/softirq.c#L59static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
        DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
        const char * const softirq_to_name[NR_SOFTIRQS] = {
        "HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL",
        "TASKLET", "SCHED", "HRTIMER", "RCU"
        };
    • Softirq는 <linux/intrerrupt.h>의 sortirq_action 구조체로 표현하며 관련 함수는 <kernel/softirq.c> 에 구현되어있다.

    • Softirq는 <linux/interrupt.h>에 열거형으로 우선순위 순으로 커널에 정적으로 등록되어있다.

    • Softirq의 핸들러는 open_softirq() 함수를 이용해서 런타임에 동적으로 등록할 수 있다. (open_softirq(softirq 이름, my_softirq))

    • 등록한 핸들러는 my_softirq->action(my_softirq)와 같이 실행할 수 있는데, softirq_action 구조체에 데이터를 추가하더라도 softirq 핸들러 원형을 바꿀 필요가 없기 때문에 확장성이 좋아진다는 장점이 있다.

    • 등록된 softirq는 인터럽트 핸들러가 종료 전에 raise 해줘야 실행 가능하다. raise_softirq(softirq 이름) 함수를 사용해서 raise 할 수 있다.

    • 커널은 다음 번 softirq를 처리할 때 raise 되어있는 softirq를 먼저 확인하고, 대응하는 softirq 핸들러를 실행한다

    • Softirq를 도입할 때 한 가지 딜레마가 있었다.

      • 만일 softirq의 발생 빈도가 높아질 경우 유저 공간 애플리케이션이 프로세서 시간을 얻지 못하는 starvation 문제가 발생할 가능성이 있다.
      • 커널 개발자들은 softirq를 도입하기 위해서는 적절한 ‘타협’이 필요함을 깨달았다.
      • 각 프로세서마다 nice 값 +19 (가장 낮은 우선순위)를 갖는 특수한 커널 스레드 ksoftirqd를 하나씩 만들어둔다.
      • ksoftirqd 커널 스레드는 계속 루프를 돌면서 pending 중인 softirq가 발생할 때마다, 프로세서가 여유롭다면 바로바로 do_softirq() 함수를 호출해서 softirq를 처리한다. (그리고 사용자 애플리케이션을 방해하지도 않으며 꽤 괜찮은 성능도 보여줬다.)
      • ksoftirqd 커널 스레드는 루프 한 바퀴를 돌 때마다 schedule() 함수를 호출해서 더 중요한 프로세스를 먼저 실행한다.
      • ksoftirqd 커널 스레드가 실행할 softirq가 없다면 자신을 TASK_INTERRUPTIBLE 상태로 전환해 softirq가 발생할 때 깨어난다. ​
  • ③ Tasklet

    • Softirq 기반으로 만들어진 동적 후반부 처리 방식이다. (Task와는 아무런 관련 없다)

    • 네트워크처럼 성능이 아주 중요한 경우에만 softirq를 사용하고 대부분의 후반부 처리는 tasklet을 사용하면 충분하다.

    • 같은 유형의 tasklet은 서로 다른 프로세서에서 동시 실행 불가능하다.

    • Softirq 보다 사용법이 간단하고 lock 사용 제한이 유연하다.

      // https://github.com/torvalds/linux/blob/bee0e7762ad2c6025b9f5245c040fcc36ef2bde8/include/linux/wait.h#L40
      struct tasklet_struct {
      struct tasklet_struct *next; // 다음 tasklet
      unsigned long state; // 현재 tasklet의 상태
      atomic_t count; // 참조 횟수
      bool use_callback; // 콜백 사용 여부
      union {
      void (*func)(unsigned long data); // 핸들러 함수
      void (*callback)(struct tasklet_struct *t); // 핸들러 함수 콜백
      };
      unsigned long data; // 핸들러 함수의 인자
      };
    • <linux/interrupt.h> 헤더파일의 tasklet_struct 구조체로 표현한다.

      • state는 0, TASKLET_STATE_SCHED(실행 대기 중), TASKLET_STATE_RUN(실행 중) 세 가지 중 한 가지를 가진다.
      • count는 현재 태스크릿의 참조 횟수를 뜻하며, 0이 아니면 태스크릿은 비활성화, 0이면 태스크릿은 활성화 상태다.
    • Softirq와 마찬가지로, 활성화 되기 위해서는 raising 돼야 하는데, 이를 ‘태스크릿 스케줄링’ 이라고 표현한다.

      static void __tasklet_schedule_common(struct tasklet_struct *t,
      struct tasklet_head __percpu *headp,
      unsigned int softirq_nr)
      {
      struct tasklet_head *head;
      unsigned long flags;
      local_irq_save(flags);
      head = this_cpu_ptr(headp);
      t->next = NULL;
      *head->tail = t;
      head->tail = &(t->next);
      raise_softirq_irqoff(softirq_nr);
      local_irq_restore(flags);
      }
    • 태스크릿 스케줄링은 <kernel/softrq.c> 파일에 구현되어있고 tasklet_schedule() 함수에서 처리한다.

      • 태스크릿의 상태가 TASKLET_STATE_SCHED 라면, __tasklet_schedule() 함수는 호출한다.
      • 현재 IRQ(인터럽트) 상태를 저장하고, 태스크릿을 현재 프로세서의 tasklet_vec 또는 tasklet_hi_vec 배열의 가장 뒤에 추가한다.
      • raise_softirq_irqoff() 함수로 softirq를 raise해서 do_softirq() 함수가 태스크릿을 처리하도록 만든다. (바로 이 부분에서 태스크릿이 softirq 기반으로 만들어졌음을 알 수 있다.)
    • 태스크릿이 스케줄링(활성화) 됐으니 이제 태스크릿이 핸들러 함수를 호출하고 처리되는 과정을 알아보자.

      static __latent_entropy void tasklet_action(struct softirq_action *a)
      {
      tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
      }
      static void tasklet_action_common(struct softirq_action *a,
      struct tasklet_head *tl_head,
      unsigned int softirq_nr)
      {
      struct tasklet_struct *list;
      local_irq_disable();
      list = tl_head->head;
      tl_head->head = NULL;
      tl_head->tail = &tl_head->head;
      local_irq_enable();
      while (list) {
      struct tasklet_struct *t = list;
      list = list->next;
      if (tasklet_trylock(t)) {
      if (!atomic_read(&t->count)) {
      if (tasklet_clear_sched(t)) {
      if (t->use_callback) {
      trace_tasklet_entry(t, t->callback);
      t->callback(t);
      trace_tasklet_exit(t, t->callback);
      } else {
      trace_tasklet_entry(t, t->func);
      t->func(t->data);
      trace_tasklet_exit(t, t->func);
      }
      }
      tasklet_unlock(t);
      continue;
      }
      tasklet_unlock(t);
      }
      local_irq_disable();
      t->next = NULL;
      *tl_head->tail = t;
      tl_head->tail = &t->next;
      __raise_softirq_irqoff(softirq_nr);
      local_irq_enable();
      }
      }
    • 태스크릿 핸들러 함수는 <kernel/softirq.c> 파일의 tasklet_action() 함수에서 처리한다.

    • 인터럽트 비활성화 후, 현재 프로세서의 tasklet_vec 또는 tasklet_hi_vec 배열을 copy 해온 뒤 NULL로 초기화하고, 인터럽트를 활성화 한다.

    • 다시 한 번 태스크릿의 상태가 TASKLET_STATE_SCHED 임을 확인한 뒤 핸들러를 호출해 실행한다.

    • 배열에 더 이상 대기 중인 태스크릿이 없을 때까지 반복문을 돌면서 핸들러를 호출한다.

  • ④ WorkQueue

    • 워크큐는 softirq, 태스크릿과 달리, 후반부 처리를 커널 스레드 형태로 프로세스 컨텍스트 내에서 처리한다.

    • 워크큐는 스케줄링이 가능하고, 인터럽트가 활성화 된 상태이고, 선점될 수 있고, sleep 상태로 전환될 수 있다.

    • 워크큐는 엄연히 커널 스레드이므로 사용자 공간 프로세스 메모리 영역을 접근할 수 없다.

    • 따라서 워크큐는 대용량 메모리 할당/ 세마포어 관련 작업/ 블록 I/O에 적합하다.

    • 사용 편의성 측면에서 워크큐가 가장 좋다.

    • 워크큐의 전체적인 구조는 아래와 같다. (<kernel/workqueue.c> 파일, <linux/workqueue.h> 파일 참고)

      image
      • 리눅스 커널은 프로세서별로 ‘작업 스레드’라는 events/n 이란 이름의 특별한 커널 스레드를 하나씩 가지고 있다.
      • 작업 스레드는 여러 작업 유형으로 나뉘며, 각 작업 유형마다 하나의 workqueue_struct 구조체로 표현한다.
      • 사용자가 원한다면 특정 작업 유형에 작업 스레드를 추가할 수 있으며, 작업 스레드는 cpu_workqueue_struct 구조체로 표현한다.
      • 후반부 처리 할 작업은 work_struct 구조체로 표현한다.
    • 모든 작업 스레드는 worker_thread() 라는 함수를 실행한다.

      // https://github.com/torvalds/linux/blob/9ace34a8e446c1a566f3b0a3e0c4c483987e39a6/kernel/workqueue.c#L2726
      /**
      * worker_thread - the worker thread function
      * @__worker: self
      *
      * The worker thread function. All workers belong to a worker_pool -
      * either a per-cpu one or dynamic unbound one. These workers process all
      * work items regardless of their specific target workqueue. The only
      * exception is work items which belong to workqueues with a rescuer which
      * will be explained in rescuer_thread().
      *
      * Return: 0
      */
      static int worker_thread(void *__worker)
      {
      struct worker *worker = __worker;
      struct worker_pool *pool = worker->pool;
      /* tell the scheduler that this is a workqueue worker */
      set_pf_worker(true);
      woke_up:
      raw_spin_lock_irq(&pool->lock);
      /* am I supposed to die? */
      if (unlikely(worker->flags & WORKER_DIE)) {
      raw_spin_unlock_irq(&pool->lock);
      set_pf_worker(false);
      set_task_comm(worker->task, "kworker/dying");
      ida_free(&pool->worker_ida, worker->id);
      worker_detach_from_pool(worker);
      WARN_ON_ONCE(!list_empty(&worker->entry));
      kfree(worker);
      return 0;
      }
      worker_leave_idle(worker);
      recheck:
      /* no more worker necessary? */
      if (!need_more_worker(pool))
      goto sleep;
      /* do we need to manage? */
      if (unlikely(!may_start_working(pool)) && manage_workers(worker))
      goto recheck;
      /*
      * ->scheduled list can only be filled while a worker is
      * preparing to process a work or actually processing it.
      * Make sure nobody diddled with it while I was sleeping.
      */
      WARN_ON_ONCE(!list_empty(&worker->scheduled));
      /*
      * Finish PREP stage. We're guaranteed to have at least one idle
      * worker or that someone else has already assumed the manager
      * role. This is where @worker starts participating in concurrency
      * management if applicable and concurrency management is restored
      * after being rebound. See rebind_workers() for details.
      */
      worker_clr_flags(worker, WORKER_PREP | WORKER_REBOUND);
      do {
      // pool에서 worker를 찾아서, 실행할 work가 있는 동안 동작을 계속한다.
      struct work_struct *work =
      list_first_entry(&pool->worklist,
      struct work_struct, entry);
      if (assign_work(work, worker, NULL))
      process_scheduled_works(worker);
      } while (keep_working(pool));
      worker_set_flags(worker, WORKER_PREP);
      sleep:
      /*
      * pool->lock is held and there's no work to process and no need to
      * manage, sleep. Workers are woken up only while holding
      * pool->lock or from local cpu, so setting the current state
      * before releasing pool->lock is enough to prevent losing any
      * event.
      */
      worker_enter_idle(worker);
      __set_current_state(TASK_IDLE);
      raw_spin_unlock_irq(&pool->lock);
      schedule(); // 작업이 없는 worker는 스케줄러에 의해 다시 깨어나 작업을 기다리게 된다.
      goto woke_up;
      }
    • worker_thread()에서는 raw_spin_lock_irq()raw_spin_unlock_irq() 함수를 사용하여 동기화를 유지하고, 여러 worker가 동시에 작업 목록에 접근하지 못하도록 한다.

  • 워크큐 사용하기

    • 새로운 작업 유형 작업 스레드 생성: struct workqueue_struct *create_workqueue(const char* name) 함수를 이용한다.
    • 정적 작업 생성: DECLARE_WORK(name, void (*func)(void *), void *data); 매크로를 사용한다.
    • 동적 작업 생성: 포인터를 이용해서 work_struct 구조체를 동적 생성한 뒤 INIT_WORK(struct work_struct *work, void (*func) (void *), void *data); 매크로를 사용해서 초기화한다.
    • 스케줄링: schedule_work(&work); 함수를 이용해서 작업 스레드를 깨우고 워크큐 핸들러를 실행한다.
    • 만일 당장 실행하고 싶지 않다면, schedule_delayed_word(&work, delay); 함수로 원하는 시간 이후에 활성화 할 수도 있다.

참고