지연 가능 함수, 커널 태스크릿 및 작업 큐

Linux 2.6의 하반부소개

스레드 작업의 빠른 처리를 위해 Linux? 커널에서는 태스크릿과 작업 큐를 제공합니다. 태스크릿과 작업 큐는 지연 가능 기능을 구현하고 드라이버의 기존 하반부 메커니즘을 대체합니다. 이 기사에서는 커널에서 태스크릿과 작업 큐를 사용하는 방법을 살펴본 후 이러한 API를 사용하여 지연 가능 함수를 작성하는 방법을 보여 줍니다.

이 기사에서는 커널 컨텍스트(특히 2.6.27.14 Linux 커널 내의 컨텍스트) 간의 처리를 지연하는 데 사용되는 두 가지 방법을 설명한다. 이러한 방법은 Linux 커널과 관련된 방법이지만 그 기본 아이디어는 아키텍처 관점에서도 유용하다. 예를 들어, 작업 스케줄링을 위한 기존 스케줄러 대신 이러한 아이디어를 기존 임베디드 시스템에 구현할 수 있다.

커널에서 함수를 지연하기 위해 사용하는 방법을 자세히 살펴보기 전에 먼저 해결 중인 문제에 대한 배경 지식을 조금 살펴보자. 하드웨어 이벤트로 인해 운영 체제가 인터럽트된 경우(예를 들어, 네트워크 어댑터를 통과하는 패킷이 있는 경우), 인터럽트에서 처리가 시작된다. 일반적으로 인터럽트에서는 상당한 양의 작업이 시작된다. 이 작업의 일부는 인터럽트의 컨텍스트에서 수행되며, 추가 처리를 위해 소프트웨어 스택 위쪽으로 전달된다(그림 1 참조).

그림 1. 상반부(top-half) 및 하반부(bottom-half) 처리

이제 인터럽트 컨텍스트에서 얼마나 많은 작업이 처리되어야 하는가라는 질문을 던져보자. 인터럽트 컨텍스트와 관련하여 이 시간 동안 일부 또는 모든 인터럽트가 비활성화될 수 있다는 문제가 있다. 이렇게 되면 다른 하드웨어 이벤트 처리의 지연 시간이 늘어난다. (그리고 처리 동작이 변경된다.) 따라서 일부 작업을 커널 컨텍스트로 넘겨서(프로세서를 효율적으로 공유할 수 있는 가능성이 높음) 인터럽트에서 수행되는 작업을 최소화하는 것이 이상적이다.

그림 1에서 보듯이 인터럽트 컨텍스트에서 수행되는 처리를 상반부(top half)라고 하고 인터럽트 컨텍스트 외부로 전달되는 인터럽트 기반 처리를 하반부(bottom half)라고 하며, 상반부에서는 하반부에서 처리할 후속 작업을 스케줄한다. 하반부 처리는 커널 컨텍스트에서 수행된다. 즉, 인터럽트가 활성화된다. 이렇게 되면 급하지 않은 작업을 지연시키고 긴급한 인터럽트 이벤트가 신속하게 처리되므로 성능이 향상된다.

하반부에 대한 간단한 역사

Linux는 스위스 군용칼과 같은 다양한 만능 기능을 제공하고 있으며 지연 기능 또한 그 중 하나이다. 커널 2.3 이후 정적으로 정의된 32개의 하반부 세트를 구현한 softirq를 사용할 수 있다. 이러한 softirq는 동적인 새 메커니즘과는 달리 정적 요소이기 때문에 컴파일 시간에 정의된다. Softirq는 커널 스레드 컨텍스트에서 시간에 민감한 처리(소프트웨어 인터럽트)에 사용되었다. softirq 기능의 소스는 ./kernel/softirq.c에서 찾을 수 있다. Linux 2.3 커널에서는 태스크릿도 도입되었다(./include/linux/interrupt.h 참조). 태스크릿은 softirq를 기반으로 작성되었으며 지연 가능 함수를 동적으로 작성할 수 있는 기능을 제공한다. 마지막으로 Linux 2.5 커널에서는 작업 큐가 도입되었다(./include/linux/workqueue.h 참조). 작업 큐를 사용하면 인터럽트 컨텍스트 외부의 작업을 커널 프로세스 컨텍스트로 지연시킬 수 있다.

이제 작업 지연, 태스크릿 및 작업 큐에 대한 동적 메커니즘을 살펴보자.

태스크릿 소개

Softirq는 원래 다양한 소프트웨어 인터럽트 동작을 지원하는 32개의 softirq 항목으로 구성된 벡터로 설계되었다. 현재는 9개의 벡터만 softirq에 사용되고 있으며 그 중 하나가 TASKLET_SOFTIRQ(./include/linux/interrupt.h 참조)이다. 그리고 softirq가 커널에 여전히 있기는 하지만 새 softirq 벡터를 허용하기 보다는 태스크릿과 작업 큐를 사용하는 것이 좋다.

태스크릿은 등록된 함수를 나중에 실행하기 위해 스케줄할 수 있는 지연 가능 스키마이다. 상반부(인터럽트 핸들러)는 소량의 작업만 수행한 후 나중에 하반부에서 실행되도록 태스크릿을 스케줄한다.

Listing 1. 태스크릿 선언 및 스케줄하기

/* Declare a Tasklet (the Bottom-Half) */
void tasklet_function( unsigned long data );

DECLARE_TASKLET( tasklet_example, tasklet_function, tasklet_data );

...

/* Schedule the Bottom-Half */
tasklet_schedule( &tasklet_example );

지정된 태스크릿은 단 하나의 CPU(태스크릿이 스케줄된 CPU)에서만 실행되며, 동일한 태스크릿은 지정된 프로세스의 둘 이상의 CPU에서 동시에 실행되지 않는다. 하지만 서로 다른 태스크릿은 다른 CPU에서 동시에 실행될 수 있다.

태스크릿은 tasklet_struct 구조체(그림 2 참조)로 표현되며 태스크릿을 유지 관리하는 데 필요한 데이터(상태, atomic_t를 통한 활성화/비활성화, 함수 포인터, 데이터 및 연결 링크 목록 참조)를 포함한다.

그림 2. tasklet_struct 구조체의 내부

태스크릿은 softirq 메커니즘을 통해 스케줄되며, 시스템의 소프트 인터럽트 로드가 클 경우에는 ksoftirqd(CPU별 커널 스레드)를 통해 스케줄되기도 한다. 다음 섹션에서는 태스크릿 API(application programming interface)에서 사용할 수 있는 다양한 함수에 대해 살펴본다.

태스크릿은 DECLARE_TASKLET이라는 매크로(Listing 2 참조)를 사용하여 정의된다. 그 다음 이 매크로는 사용자가 제공한 정보(태스크릿 이름, 함수 및 태스크릿 관련 데이터)로 구성된 tasklet_struct 초기화를 제공한다. 태스크릿은 기본적으로 활성화되므로 스케줄할 수 있다. 또한 DECLARE_TASKLET_DISABLED 매크로를 사용하여 태스크릿을 기본적으로 비활성화되도록 선언할 수도 있다. 이 작업을 수행하려면 tasklet_enable 함수를 호출하여 태스크릿을 스케줄할 수 있도록 설정해야 한다. tasklet_enable 및 tasklet_disable 함수를 사용하여 태스크릿을 활성화 및 비활성화할 수 있다(스케줄링 관점에서). 또한 사용자가 제공한 태스크릿 데이터를 사용하여 tasklet_struct를 초기화하는 tasklet_init 함수도 있다.

Listing 2. 태스크릿 작성 및 활성화/비활성화 함수

DECLARE_TASKLET( name, func, data );
DECLARE_TASKLET_DISABLED( name, func, data);
void tasklet_init( struct tasklet_struct *, void (*func)(unsigned long),
unsigned long data );
void tasklet_disable_nosync( struct tasklet_struct * );
void tasklet_disable( struct tasklet_struct * );
void tasklet_enable( struct tasklet_struct * );
void tasklet_hi_enable( struct tasklet_struct * );

두 개의 비활성화 함수가 있으며, 각 함수는 해당 태스크릿의 비활성화를 요청한다. 하지만 tasklet_disable만 태스크릿이 종료된 후 리턴되며, tasklet_disable_nosync는 종료되기 전에 리턴된다. 비활성화 함수를 사용하면 활성화 함수가 호출될 때까지 태스크릿을 "마스크"(실행이 아님)할 수 있다. 활성화 함수도 두 개가 있다. 하나는 일반 우선순위 스케줄링을 위한 tasklet_enable 함수이고, 다른 하나는 상위 우선순위 스케줄링을 활성화하기 위한 tasklet_hi_enable 함수이다. 일반 우선순위 스케줄은 TASKLET_SOFTIRQ 레벨 softirq를 통해 수행되는 반면 상위 우선순위는 HI_SOFTIRQ 레벨 softirq를 통해 수행된다.

일반 및 상위 우선순위 활성화 함수와 마찬가지로 일반 및 상위 우선순위 스케줄 함수도 있다(Listing 3 참조). 각 함수는 태스크릿을 특정 softirq 벡터에 저장한다(일반 우선순위의 경우에는 tasklet_vec, 상위 우선순위의 경우에는 tasklet_hi_vec). 상위 우선순위 벡터의 태스크릿이 먼저 처리된 후 일반 벡터의 태스크릿이 처리된다. 각 CPU는 고유한 일반 및 상위 우선순위 softirq 벡터를 관리한다.

Listing 3. 태스크릿 스케줄링 함수

void tasklet_schedule( struct tasklet_struct * );
void tasklet_hi_schedule( struct tasklet_struct * );

마지막으로, 태스크릿을 작성한 후에는 tasklet_kill 함수를 통해 태스크릿을 중지할 수 있다(Listing 4 참조). tasklet_kill 함수는 태스크릿이 다시 실행되지 않도록 보장하며, 태스크릿이 현재 실행되도록 스케줄되어 있는 경우에는 태스크릿이 완료될 때까지 기다린 후 태스크릿을 강제 종료한다. tasklet_kill_immediate는 지정된 CPU가 종료(dead) 상태에 있을 때만 사용된다.

Listing 4. 태스크릿 강제 종료 함수

void tasklet_kill( struct tasklet_struct * );
void tasklet_kill_immediate( struct tasklet_struct *, unsigned int cpu );

API를 보면 태스크릿 API과 그 구현이 간단하다는 것을 알 수 있다. 태스크릿 메커니즘의 구현을 ./kernel/softirq.c와 ./include/linux/interrupt.h에서 볼 수 있다.

간단한 태스크릿 예제

이제 태스크릿 API의 간단한 사용 예제를 살펴보자(Listing 5 참조). 여기에서 살펴본 것처럼 태스크릿 함수는 연관된 데이터(my_tasklet_function 및 my_tasklet_data)를 사용하여 작성되며, DECLARE_TASKLET을 사용하여 새 태스크릿을 선언하는 데 사용된다. 모듈이 삽입될 때 태스크릿이 스케줄되며 미래의 임의 시점에 실행 가능하도록 설정된다. 모듈이 언로드되면 tasklet_kill 함수가 호출되어 태스크릿을 스케줄 가능 상태에 있지 않도록 설정한다.

Listing 5. 커널 모듈의 컨텍스트에서의 간단한 태스크릿 예제

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>

MODULE_LICENSE( "GPL" );

char my_tasklet_data[]="my_tasklet_function was called";

/* Bottom Half Function */
void my_tasklet_function( unsigned long data )
{
printk( "%s\n", (char *)data );
return;
}

DECLARE_TASKLET( my_tasklet, my_tasklet_function,
(unsigned long) &my_tasklet_data );

int init_module( void )
{
/* Schedule the Bottom Half */
tasklet_schedule( &my_tasklet );

return 0;
}

void cleanup_module( void )
{
/* Stop the tasklet before we exit */
tasklet_kill( &my_tasklet );

return;
}

작업 큐 소개

작업 큐는 2.5 버전의 Linux 커널에 추가된 최신 지연 메커니즘이다. 한번에 모든 작업을 처리하는 지연 스키마를 제공하는 태스크릿과는 달리 작업 큐는 작업 큐에 대한 핸들러 함수가 대기 상태에 있을 수 있는(태스크릿 모델에서는 가능하지 않음) 제네릭 지연 메커니즘이다. 작업 큐는 태스크릿보다 높은 지연 시간을 가질 수 있고 더 다양한 작업 지연 API를 포함하고 있다. 지연은 keventd를 통해 태스크 큐에 의해 관리되었지만 이제는 events/X라는 커널 작업자 스레드에 의해 관리된다.

작업 큐는 기능을 하반부로 지연시킬 수 있는 일반적인 방법을 제공한다. 중앙에 있는 작업 큐(struct workqueue_struct)는 작업이 배치되는 구조체이다. 작업은 work_struct 구조체로 표현되며 지연시킬 작업과 사용할 지연 함수를 식별한다(그림 3 참조). events/X 커널 스레드(CPU당 한 개)는 작업 큐에서 작업을 추출하고 하반부 핸들러 중 하나를 활성화한다(struct work_struct의 핸들러 함수에 지정된 대로).

그림 3. 작업 큐 관련 프로세스

work_struct는 사용할 핸들러 함수를 나타내므로 작업 큐를 사용하여 다양한 핸들러에 대한 작업을 큐에 저장할 수 있다. 이제 작업 큐에 대한 API 함수를 살펴보자.

작업 큐 API

작업 큐 API는 수많은 옵션이 지원되기 때문에 태스크릿보다 약간 더 복잡하다. 먼저 작업 큐를 살펴본 후 작업 및 변형을 살펴보자.

그림 3에서 보았듯이 작업 큐의 핵심 구조체는 큐 자체이다. 이 구조체는 나중에 하반부에서 실행하기 위해 지연시킬 상반부의 작업을 큐에 저장하는 데 사용된다. 작업 큐는 create_workqueue라는 매크로를 통해 작성되며, 이 매크로는 workqueue_struct 참조를 리턴한다. 필요한 경우 나중에 다음과 같이 destroy_workqueue 함수를 호출하여 이 작업 큐를 제거할 수 있다.

struct workqueue_struct *create_workqueue( name );
void destroy_workqueue( struct workqueue_struct * );

작업 큐를 통해 통신할 작업은 work_struct 구조체로 정의된다. 일반적으로 이 구조체는 사용자가 작성한 작업 정의 구조체의 첫 번째 요소이다. (나중에 이에 대한 예제를 볼 수 있다.) 작업 큐 API는 할당된 버퍼에 있는 작업을 초기화할 수 있는 세 개의 함수를 제공한다(Listing 6 참조). INIT_WORK 매크로는 사용자가 전달한 핸들러 함수에 필요한 초기화 및 설정 기능을 제공한다. 작업을 작업 큐에 저장하기 전에 지연시킬 필요가 있을 경우 INIT_DELAYED_WORK 및 INIT_DELAYED_WORK_DEFERRABLE 매크로를 사용할 수 있다.

Listing 6. 작업 초기화 매크로

INIT_WORK( work, func );
INIT_DELAYED_WORK( work, func );
INIT_DELAYED_WORK_DEFERRABLE( work, func );

작업 구조체를 초기화한 후에는 작업을 작업 큐에 저장하는 단계를 수행해야 한다. 이 작업은 몇 가지 방법으로 수행할 수 있다(Listing 7 참조). 먼저, queue_work(작업을 현재 CPU에 연결)를 사용하여 작업을 작업 큐에 저장할 수 있다. 또는 queue_work_on을 사용하여 핸들러가 실행되어야 하는 CPU를 지정할 수 있다. 두 개의 추가 함수는 지연된 작업에 대해 동일한 기능을 수행한다. (해당 구조체가 work_struct 구조체와 작업 지연 시간을 캡슐화한다.)

Listing 7. 작업 큐 함수

int queue_work( struct workqueue_struct *wq, struct work_struct *work );
int queue_work_on( int cpu, struct workqueue_struct *wq, struct work_struct *work );

int queue_delayed_work( struct workqueue_struct *wq,
struct delayed_work *dwork, unsigned long delay );

int queue_delayed_work_on( int cpu, struct workqueue_struct *wq,
struct delayed_work *dwork, unsigned long delay );

이 작업 큐를 지원하는 네 개의 함수가 있는 글로벌 커널-글로벌 작업 큐를 사용할 수 있다. 이러한 함수(Listing 8 참조)는 작업 큐 구조체를 정의하지 않아도 된다는 점을 제외하면 Listing 7의 함수와 유사하다.

Listing 8. 커널-글로벌 작업 큐 함수

int schedule_work( struct work_struct *work );
int schedule_work_on( int cpu, struct work_struct *work );

int scheduled_delayed_work( struct delayed_work *dwork, unsigned long delay );
int scheduled_delayed_work_on(
int cpu, struct delayed_work *dwork, unsigned long delay );

또한 작업 큐의 작업을 비우거나 취소하는 데 사용할 수 있는 여러 헬퍼 함수도 있다. 작업이 완료될 때까지 특정 작업 항목 및 블록을 비우려는 경우 flush_work를 호출할 수 있다. flush_workqueue를 호출하여 지정된 작업 큐의 모든 작업을 완료할 수도 있다. 두 경우 모두 작업이 완료될 때까지 호출자가 차단된다. 커널-글로벌 작업 큐를 비우려면 flush_scheduled_work를 호출한다.

int flush_work( struct work_struct *work );
int flush_workqueue( struct workqueue_struct *wq );
void flush_scheduled_work( void );

핸들러에서 아직 실행되지 않고 있을 경우 작업을 취소할 수 있다. cancel_work_sync를 호출하면 콜백이 완료될 때까지 큐 또는 블록의 작업이 종료된다(작업이 핸들러에서 처리 중인 경우). 작업이 지연된 경우에는 cancel_delayed_work_sync를 호출할 수 있다.

int cancel_work_sync( struct work_struct *work );
int cancel_delayed_work_sync( struct delayed_work *dwork );

마지막으로, work_pending 또는 delayed_work_pending 호출을 사용하여 작업 항목이 보류 중(핸들러에서 아직 실행되지 않음)인지 여부를 확인할 수 있다.

work_pending( work );
delayed_work_pending( work );

이 점이 바로 작업 큐 API의 핵심이다. 작업 큐 API의 구현은 ./kernel/workqueue.c에서 볼 수 있으며 API 정의는 ./include/linux/workqueue.h에 있다. 이제 작업 큐 API의 간단한 예제를 살펴보자.

간단한 작업 큐 예제

다음 예제에서는 몇 가지 핵심 작업 큐 API 함수를 보여 준다. 태스크릿 예제와 마찬가지로 이 예제도 단순하게 설명하기 위해 커널 모듈의 컨텍스트에서 구현한다.

먼저 작업 구조체와 하반부를 구현하는 데 사용할 핸들러 함수를 살펴보자(Listing 9 참조). 여기서 먼저 살펴볼 부분은 작업 큐 구조체 참조(my_wq)와 my_work_t 정의이다. my_work_t typedef에는 work_struct 구조체가 앞에 있고, 그 뒤에 작업 항목을 나타내는 정수가 있다. 핸들러(콜백 함수)는 work_struct 포인터를 my_work_t 유형으로 역참조한다. 작업 항목(구조체의 정수)을 내보낸 후 작업 포인터의 참조가 해제된다.

Listing 9. 작업 구조체 및 하반부 핸들러

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/workqueue.h>

MODULE_LICENSE( "GPL" );

static struct workqueue_struct *my_wq;

typedef struct {
struct work_struct my_work;
int x;
} my_work_t;

my_work_t *work, *work2;


static void my_wq_function( struct work_struct *work)
{
my_work_t *my_work = (my_work_t *)work;

printk( "my_work.x %d\n", my_work->x );

kfree( (void *)work );

return;
}

Listing 10은 사용자의 init_module 함수이며 create_workqueue API 함수를 사용하여 작업 큐를 작성하는 작업부터 시작한다. 작업 큐를 성공? 통해 할당됨)을 작성한다. 그런 다음 각 작업 항목이 INIT_WORK를 통해 초기화되고 작업이 정의된 후 queue_work 호출을 통해 작업 큐에 저장된다. 상반부 프로세스(여기에서 시뮬레이트한 프로세스)가 이제 완료되었다. 그런 다음 작업이, 때로는 나중에, 핸들러에 의해 처리된다(Listing 10 참조).

Listing 10. 작업 큐 및 작업 작성

int init_module( void )
{
int ret;

my_wq = create_workqueue("my_queue");
if (my_wq) {

/* Queue some work (item 1) */
work = (my_work_t *)kmalloc(sizeof(my_work_t), GFP_KERNEL);
if (work) {

INIT_WORK( (struct work_struct *)work, my_wq_function );

work->x = 1;

ret = queue_work( my_wq, (struct work_struct *)work );

}

/* Queue some additional work (item 2) */
work2 = (my_work_t *)kmalloc(sizeof(my_work_t), GFP_KERNEL);
if (work2) {

INIT_WORK( (struct work_struct *)work2, my_wq_function );

work2->x = 2;

ret = queue_work( my_wq, (struct work_struct *)work2 );

}

}

return 0;
}

Listing 11에서는 마지막 요소를 보여 준다. 이 정리 모듈에서는 특정 작업 큐를 비우고(핸들러가 작업 처리를 완료할 때까지 차단됨) 작업 큐를 삭제한다.

Listing 11. 작업 큐 비우기 및 삭제

void cleanup_module( void )
{
flush_workqueue( my_wq );

destroy_workqueue( my_wq );

return;
}

태스크릿과 작업 큐의 차이점

태스크릿과 작업 큐를 간단하게 소개하는 이 기사에서는 작업을 상반부에서 하반부로 지연시키는 두 가지 스키마를 살펴보았다. 태스크릿은 간단하고 직접적이기 때문에 지연 시간이 짧은 메커니즘을 제공하는 반면 작업 큐는 여러 작업 항목을 큐에 저장할 수 있는 유연한 API를 제공한다. 두 스키마 모두 인터럽트 컨텍스트의 작업을 지연시키지만 태스크릿만 실행부터 완료까지 자동으로 실행되며 작업 큐는 필요한 경우 핸들러를 대기 상태로 설정할 수 있다. 두 방법 모두 작업 지연에 유용하므로 사용자의 특정 요구에 따라 선택해서 사용하면 된다.

추가 주제

여기에서 살펴본 작업 지연 방법은 Linux 커널에 사용되는 과거와 현재의 방법을 나타낸다(후속 기사에서 다룰 타이머 제외). 이러한 방법은 실제로 과거에도 다른 형태로 사용되었기 때문에 분명 새로운 기능은 아니지만 Linux를 비롯한 여러 운영 체제에 유용한 흥미로운 아키텍처 패턴을 보여 준다. softirq부터 시작하여 태스크릿과 작업 큐를 거쳐서 지연된 작업 큐에 이르기까지 Linux는 커널의 모든 영역에서 지속적으로 발전하고 있으며, 그와 동시에 일관되고 호환성 높은 사용자 환경도 제공하고 있다.

필자소개

M. Tim Jones는 임베디드 펌웨어 아키텍트이자 Artificial Intelligence: A Systems Approach, GNU/Linux Application Programming(현재 2판), AI Application Programming(현재 2판) 및 BSD Sockets Programming from a Multilanguage Perspective의 저자이다. 정지 위성을 위한 커널 개발에서 시작해 임베디드 시스템 아키텍처와 네트워크 프로토콜 개발에 이르기까지 다양한 분야에 대한 공학 지식을 가지고 있다. 콜로라도주 롱몬트 소재의 Emulex Corp.에서 컨설턴트 엔지니어로 활약하고 있다.

출처 : 한국 IBM

Posted by 단세포소년

댓글을 달아 주세요