void flush_scheduled_word() 함수에 대해서 찾다가 발견한 문서인데,
work queue 에 대해서 개념 설명이 잘 되어있다.
태스크 큐와 워크 큐의 필요성
특정 I/O에 대한 지속적인 감시가 필요한 경우
디바이스 드라이버의 주기적인 I/O 감시 방법
타이머 인터럽트 이용 => 시스템에서 사용하지 않는 여분의 타이머 인터럽트 필요.
시스템이 폭주할 수 있다.
커널 타이머 이용 => 정확한 주기로 동작, 1/HZ 이하의 주기에 서는 사용 불가능.
인터럽트 한계는 그대로 남아 있다.
태스크 큐와 워크 큐 이용 => 동작 시점 예측 힘듬, 커널 타이머보다 자주 혹은 빠르게 호출될 가능성
이 있음, 디바이스 드라이버 전용 워크 큐를 사용할 경우 훨씬 다양한
처리가 가능.
bottomhalf 이용(ver 2.4 이하) => 제한적인 방법. 주로 인터럽트 처리의 신속성을 보완하기 위해 사용.
인터럽트 핸들러 함수에서의 신속한 처리가 힘든 경우
인터럽트에 의해 동작하는 함수는 가급적 신속하게 처리하고 종료하는 구조로 설계되어야 한다.
ex) 이더넷 디바이스, 사운드 디바이스
인터럽트 서비스 함수(ISR)가 가지는 제약 이외의 처리를 해야할 경우
인터럽트 서비스 함수(ISR)는 제약이 많음
ISR내의 긴 지연은 시스템 전체 성능 저하를 유발함
vmalloc이나 파일 입출력과 같이 sleep 상태를 유발할 수 있는 함수는 사용이 금지됨
커널 내부 구조와 연계 되어야 할 경우
주기적인 호출의 구현이 필요하거나 인터럽트에서 발생한 데이터를 커널과 연계된 데이터 구조로 변환하여 전달하거나 인터럽트 함수로서의 한계를 벗어나야 할 경우 등과 같은 상황을 극복하기 위해 태스크 큐와 워크 큐가 구현된 것이다.
워크 큐
워크 큐의 특징
태스크 큐와 기본 설계 이념 동일
태스크 큐의 문제점 보완
우선 순위가 높은 전용의 kernel thread를 이용하여 태스크 특성 부여
(선점형 커널 도입으로 성능이 보장되었음)
2.6에서 태스크 큐를 대체함
워크 큐가 동작하는 핵심 개념은 워크 큐 구조체를 이용해 등록된 함수의 수행을 keventd와 같은
커널 스레드가 수행해 준다.
선점형 커널에서 구현 할 수 있다.
워크 큐가 만들어진 이유
* 등록된 함수가 수행될 때 인터럽트 허용
* 일반 사용자 프로세스와 같은 태스크의 특성 포함.
스케줄 워크 큐
디바이스 드라이버에서 워크 큐라고 하면 보통 스케줄 워크 큐를 지칭한다. 스케줄 워크 큐는 커널에서 워크 큐를 소비하는 스레드인 event를 만들고 처리하는 방식을 지칭하는데, 디바이스 드라이버에서 자체적으로 만들어 등록할 수 있다. 그러나 이런 방식은 프로그래머 입장에서 보면 매우 귀찮고 버그가 발생할 소지가 있으므로 커널에서 지원하는 스케줄 워크 큐를 그냥 사용하는 편이 좋다. 그래서 워크 큐라고 지칭하면 스케줄 워크 큐를 이용한다고 생각해도 무방하다.
워크 큐를 이용해 함수를 수행하려면 work_struct 구조체 변수를 선언해야 한다. 이 구조체 변수의 형식은 다음과 같으며, #include <linux/workqueue.h>를 포함시켜야 한다.
struct work_struct
{
unsigned longpending;
struct list_headentry;
void(*func)(void *);
void*data;
void*wq_data;
struct timer_listtimer;
}
func : 수행할 함수
data : func 필드에 정의된 함수가 수행될 때 전달해야 할 정보를 지정한다.
이 구조체 중 디바이스 드라이버 작성자가 알아야 할 필드는 func 필드와 data 필드이다. func 필드는 수행되어야 할 함수를 지정하고, data 필드는 func 필드에 정의된 함수가 수행될 때 전달해야 할 정보를 지정한다. 이 구조체에는 여러 필드가 있지만, 초기화를 위한 매크로 함수가 다음과 같이 정의되어 있다. 이 매크로 함수는 #include <linux/workqueue.h>를 포함시켜야 사용할 수 있다.
DECLARE_WORK : 워크 큐에 등록될 작업 구조체 변수를 선언하고 초기화하는 매크로 함수 INIT_WORK : 워크 큐 구조체 변수를 초기화하는 매크로 함수 |
DECLARE_WORK 매크로 함수는 워크 큐 구조체 변수를 선언하고 초기화하는 매크로 함수로, 사용빈도가 가장 높다. 이 매크로 함수의 형식은 다음과 같다.
DECLARE_WORK ( name, void (*function)(void *), void *data);
name은 선언하려고 하는 변수명을, function은 수행되어야 하는 워크 큐 함수를 지정한다. data는 function에 지정된 함수가 전달받아야 하는 데이터를 지정한다. 이 매크로를 다음과 같이 사용할 수 있다.
extern void *xxx_func(void *data);
:
DECLARE_WORK (xxx_workqueue, xxx_func, NULL);
이 예에서는 xxx_func() 함수에서 데이터를 처리할 필요가 없다고 가정하고, NULL을 data 매개변수에 전달한다. DECLARE_WORK 매크로 함수가 워크 큐 구조체 변수를 선언하는 반면 INIT_WORK 매크로 함수는 이미 선언된 워크 큐 구조체 변수를 초기화할 경우에 사용한다. 프로세스에 따라서 디바이스 드라이버가 워크 큐를 따로 관리하는 경우나 디바이스 파일을 열었을 때 워크 큐 구조체를 사용하는 경우에 이 매크로를 사용한다. 이 매크로의 형식은 다음과 같다.
INIT_WORK(struct *work, vod(*function)(void *), void *data);
DECLARE_WORK 매크로 함수와 다른 것은 work라는 매개변수에 이미선언된 워크 큐 구조체 변수의 주소를 대입한다는 점이다. 이 매크로는 다음과 같이 사용할 수 있다.
struct work_struct xxx_work;
extern void *xxx_func(void *data);
struct xxx_data_t
{
:
// 워크 큐 처리 함수에서 사용할 데이터 정의
:
} xxx_data;
int xxx_open(struct inode *inode, struct file *filp)
{
:
INIT_WORK(&xxx_work, xxx_func, &xxx_data);
:
}
워크 큐를 통해 커널에 등록하여 수행할 함수의 형식은 다음과 같다.
void xxx_workqueuefunc(void *data)
{
struct xxx_data_t *pmydata;
pmydata = (struct xxx_data_t *) data;
:
}
워크 큐는 변수 선언을 한 뒤에 schedule_work()함수나 schedule_delayed_work() 함수를 이용해 work_struct 구조체 변수를 커널에 등록한다.
schedule_work() : 워크 큐 구조체 등록 schedule_delayed_work() : 지정된 시간 후에 워크 큐 구조체 등록 |
keventd커널 스레드에서 소모하는 워크 큐에 수행할 함수와 데이터를 등록하고 해당 함수의 수행을 요청하는 schedule_work() 함수의 형식은 다음과 같다.
int schedule_work(struct work_struct *work)
이 함수가 정상적으로 등록되면 1을 반환하고, 실패하면 0을 반환한다. 등록에 실패하는 경우는 이미 동일한 구조체 변수가 워크 큐에 등록되었기 때문이다. 이 함수를 이용해 앞의 예에 선언된 워크 큐 구조체를 등록하여 함수를 수행하려면 다음과 같이 사용하면 된다.
schedule_work(&xxx_work);
schedule_work() 함수와 달리 schedule_delayed_work() 함수는 일정한 시간 뒤에 워크 큐가 등록되도록 예약하며, 형식은 다음과 같다.
int schedule_delayed_work(struct work_struct *work, unsigned long delay)
이 함수는 delay에 정의된 시간 (jiffies값과 연관되어 있으며, 1/Hz단위)이 지나면 keventd 커널 스레드가 소모하는 워크 큐 관리 구조체에 수행할 함수와 데이터 정보를 포함하는 work구조체 변수를 등록한 후 커널 스레드로 다시 복귀하여 스케줄러에게 프로세스가 커널 스레드로 전환되었음을 알린다. 이 함수는 즉시 수행되지 않고, delay에 선언된 시간 이후에 워크 큐에 선언된 함수가 호출되는 구조다.
이 함수 역시 schedule_work()함수와 마찬가지로 정상적으로 등록되면 1을 반환하고, 실패하면 0을 반환한다. 등록에 실패하는 경우는 이미 동일한 구조체 변수가 워크 큐에 등록되어 있기 때문이다. 이 함수를 이용해 앞의 예에서 선언된 워크 큐 구조체를 200밀리초 이후에 등록하여 함수를 수행하려면 다음과 같이 사용하면 된다.
schedule_delayed_work(&xxx_work, 200);
schedule_work()함수나 schedule_delayed_work()함수를 이용해 등록된 함수는 keventd커널 스레드가 수행되어야 비로소 등록된 내용이 제거되는데, 디바이스 드라이버가 모듈 형태로 작성되거나 디바이스 드라이버의 수행 구조상의 이유로 등록된 워크 큐가 처리되기를 기다릴 필요가 있을 수 있다. 이때 사용되는 함수가 flush_scheduled_work() 함수며, 사용 형식은 다음과 가탇.
void flush_scheduled_word(void);
이 함수는 keventd커널 스레드가 소모하는 워크 큐 구조체에 등록된 함수가 모두 처리될 수 있도록 스케줄 전환을 수행하고, 워크 큐에 정의된 함수가 모두 수행되어 처리될 함수가 없을 때까지 기다린다. 등록된 함수가 끝나는 것을 보장하기 위해 이 함수를 사용한다. 그 때문에 디바이스 드라이버를 완전히 제거할 때나 커널이 종료될 경우에만 호출되어야 한다. 여기 함 가지 주의할 점이 있다면 flush_scheduled_work()함수는 디바이스 드라이버가 등록한 워크 큐 전체가 소비될 때까지 기다린다는 점이다. 커널 2.6에서 지원하는 워크 큐의 동작 구조를 좀더 자세하게 살펴보면 커널은 초기 부팅
kworker thread 와 cpu 의 상관관계
원문 : http://studyfoss.egloos.com/5626173
아래와 같은 컨샙으로 보았을때 무수히 많은 kworker 가 살아있다면 work 들이 모두 pending 상태에 있음을 의심해 볼 수 있겠다.
workqueue는 특정 작업을 별도의 process context에서 실행하고 싶은 경우 사용하는
커널 API로 커널 내의 다양한 위치에서 널리 사용된다. 이는 일종의 thread pool의
개념으로 볼 수 있으며, workqueue 시 생성 시에 작업용 thread들을 미리 만들어 둔
뒤, 이후에 필요한 work가 발생되었을 때 해당 thread를 이용하여 수행하는 방식이다.
기존의 구현 방식은 (singlethread 모드가 아닌) workqueue 생성 시 시스템에
존재하는 CPU 수 만큼의 worker_thread를 만들어두고, 이후 (queue_work 혹은
schedule_work 등의 API를 통해) work가 생성되었을 때 해당 CPU에 배정된
worker_thread가 그 work를 수행하는 식이었다.
가령 4개의 CPU 코어를 가진 시스템에서 workqueue는 다음 그림과 같은 형태로
구성될 것이다.
하지만 이러한 방식은 매우 많은 수의 CPU를 가진 시스템에서, 많은 수의 workqueue가
사용될 경우 worker thread를 위해 너무 많은 task가 만들어진다는 단점이 있다.
특히 task를 생성하려면 pid를 부여해야 하는데 미리 만들어둔 worker thread로
인해 pid가 모자라게 되는 상황이 발생할 수도 있다.
사실 2.6.35 이후에는 시스템 내의 CPU 수를 파악하여 가능한 최대 pid 번호를 부팅
시 동적으로 조정할 수 있게 되었지만 어쨌든 이렇게 많은 수의 task가 생성된다는
것은 그많큼 많은 자원이 사용된다는 의미이므로 그리 바람직하지 않다고 볼 수 있다.
그보다 더 큰 문제는 이렇게 많은 worker thread들이 생성되었음에도 불구하고 이들
thread들 간에 협업이 불가능하기 때문에 생각만큼 충분한 병렬성(concurrency)을
제공하지 못한다는 점이었다.
이러한 문제를 해결하기 위해 2.6.36 버전에서 도입된 새로운 workqueue의 구현이
바로 cmwq이다. cmwq는 각 CPU별로 공유되는 gcwq (global per-cpu wq)를 도입하여
worker thread를 관리하며, 각 wq는 여전히 내부적으로 cwq(per-cpu wq)를 통해
work를 관리하긴 하지만 이를 gcwq로 보낼 때의 동작을 제어할 수 있게 된다.
cmwq의 대략적인 구조는 다음과 같다.
기본적인 차이점으로는 앞서 말했듯이 worker thread가 각각의 workqueue 단위가
아닌 CPU (gcwq) 단위로 관리된다는 것인데, wq로 보내진 work는 다시 gcwq로
전달되어 처리되도록 변경되었다. wq에서 gcwq로 보낼 수 있는 work의 수는 wq 할당
시 max_active 인자로 지정할 수 있으며, 기존의 create_workqueue() API를 사용하는
경우 1로 지정된다. 만약 max_active 인자로 0을 주면 기본값인 256으로 지정된다.
이 active 카운트 값은 cwq 마다 따로 관리되며, gcwq로 work를 전달 시에 증가되고
해당 work가 수행되고나면 감소한다. 만약 max_active 보다 많은 work가 wq로 보내진
경우 해당 cwq의 delayed_works 리스트에 보관된다.
gcwq는 각각의 cwq에서 보내진 work들을 queue로 관리하며 차례대로 worker thread를
통해 수행한다. 이 worker thread들의 이름은 "kworker/0:0" 형태가 되는데 첫번째
숫자는 gcwq의 CPU 번호에 해당하고 두번째 숫자는 해당 thread의 ID이다.
worker thread는 시스템 부팅 초기에 각 gcwq 마다 하나씩 할당되며, 시스템 동작
중에 필요에 따라 할당/제거될 수 있다. 기본적으로는 gcwq는 주어진 work를 하나씩
순서대로 처리하며 하나의 work가 완료된 후에 다음 work를 처리한다. 하지만 work는
process context에서 처리되기 때문에 수행 도중 sleep될 수 있는데 이 때 (기존
workqueue 구현에서처럼) 단순히 이전 work가 완료되기를 기다리며 다음 work를
수행하지 않는 것은 CPU 자원을 낭비하는 결과가 되므로 이 경우 새로운 worker
thread를 생성하여 바로 다음 work를 처리하도록 하는 것이 cmwq의 아이디어이다.
(cmwq에서 말하는 concurrency는 바로 이것을 의미한다.)
worker thread는 주어진 work들을 처리하기 전후에 worker thread의 수를 관리해야
할 지를 검사하여 이를 조정한다. 만약 gcwq 내에 주어진 work thread가 있지만 현재
실행 중인 worker thread가 없다면 새로운 worker를 생성하고, 주어진 work가 없고
3개 이상의 worker thread가 (또한 실행 중인 worker thread가 많지 않아야 한다)
idle 상태라면 5분 이상 idle 상태인 worker thread를 차례로 제거하게 되므로 보통
실행 환경에서는 CPU마다 2개의 worker thread가 존재한다고 볼 수 있다.
이러한 여러 동작들을 제어하기 위해 workqueue 생성 시 추가적으로 flags 인자를
넘길 수 있는데 이는 다음과 같은 값들의 bitmask이다.
- WQ_NON_REENTRANT: 주어진 work가 동시에 여러 번 실행되지 않는다는 것을 보장해준다. 만약 이 플래그가 없다면 해당 work가 실행되는 도중 다른 CPU에서 동시에 동일한 work를 실행할 수도 있다.
- WQ_UNBOUND: 이 workqueue는 CPU마다 work를 배정하지 않고 그때그때 현재 CPU에서 work를 실행한다. 이 경우 worker thread는 WORKER_NOT_RUNNING 상태이므로 idle worker가 없다면 항상 새로운 worker를 생성하여 작업을 수행하게 될 것이다.
- WQ_FREEZABLE: 이 workqueue를 통해 수행되는 work는 시스템이 절전 상태로 빠지는 경우 실행이 중단된다.
- WQ_MEM_RECLAIM: 이 workqueue를 통해 수행되는 work는 메모리 할당/해제 과정에서 사용될 수 있음을 뜻한다. 따라서 시스템의 메모리가 부족한 상황에서 기존의 worker thread들이 모두 실행 중이라면 새로운 work를 수행하기 위한 worker thread를 할당하지 못할 수 있다. 하지만 시스템의 메모리를 확보하기 위해 해당 work를 반드시 수행해야만 한다면 (예를 들어, dirty page를 swap 영역에 write해야 하는 경우) 이는 시스템 전체를 마비시키는 결과를 가져올 것이다. 이를 방지하기 위해 새로운 worker thread를 할당하지 못한 경우 rescuer thread를 미리 만들어두고 비상 시에 이를 이용하여 work를 수행할 수 있도록 해 준다.
- WQ_HIGHPRI: 일반적으로 work는 queue로 관리되므로 주어진 순서대로 차례로 처리되지만 이 플래그가 설정된 wq를 통해 보내진 work는 그렇지 않은 work보다 먼저 처리되도록할 수 있다. 또한 WQ_HIGHPRI가 설정된 work 간에는 FIFO 순서가 보장된다.
- WQ_CPU_INTENSIVE: 이 workqueue를 통해 수행되는 work는 cmwq의 worker thread 관리 대상에 포함되지 않으므로 gcwq는 이에 상관없이 다른 work를 수행할 수 있다. 하지만 WQ_HIGHPRI와는 달리 해당 work가 실행되는 순서에는 영향을 주지 않는다.
schedule_work() API등을 통해 기본적으로 사용되는 system_wq (keventd) 외에도
이러한 다양한 플래그를 지원하는 여러 system-level wq들이 추가적으로 많이 제공되므로
GPL을 사용하는 모듈의 경우 필요에 따라 적절한 wq를 선택하여 사용할 수 있을 것이다.
'Programming > Linux_Kernel' 카테고리의 다른 글
linux kernel 2.6.30, 31, 32 change list (0) | 2010.08.03 |
---|---|
Ram Memory 설정하기 (0) | 2010.08.02 |
wait queue (0) | 2010.08.02 |
restart_syscall 의 호출 경로 - 작성중 (1) | 2010.07.13 |
process의 signal 처리 상태 보기 (0) | 2010.07.09 |