-
[리눅스 커널] Interrupt - TaskletLinux/kernel 2023. 10. 28. 20:47
글의 참고
- https://lwn.net/Articles/830964/
- http://www.wowotech.net/irq_subsystem/tasklet.html
- https://lwn.net/Articles/239633/
- https://stackoverflow.com/questions/7106050/what-context-does-the-scheduler-code-run-in
글의 전제
- 밑줄로 작성된 글은 강조 표시를 의미한다.
- 그림 출처는 항시 그림 아래에 표시했다.
글의 내용
- Overview
" 리눅스에서 인터럽트 처리 과정인 2가지로 나뉜다.
1. top-half : 하드웨어 인터럽트 핸들러를 의미한다.
2. bottom-half : 인터럽트가 발생하면 일단 모든 인터럽트를 비활성화한다. 그리고 모든 일을 처리하고 나서 다시 인터럽트를 활성화한다. 이때 인터럽트 서비스 루틴(ISR)의 처리가 길어지면 길어질 수록, 다른 인터럽트를 놓칠 수 있는 가능성이 커진다. 그래서 덜 중요한 일들은 나중에 실행될 수 있도록 별도의 핸들러에게 위임하도록 한다." 리눅스에서는 인터럽트 후반부 처리 기법을 bottom-half 라고 부르지만, 대개 OS 이론서들 에서는 deferable work 혹은 deferred work 와 같은 용어를 더 많이 사용한다. 이와같이 실행을 뒤로 미루는 기법은 대표적으로 3가지가 있다.
1. top-half 가 끝날때까지 대기한다. 즉, top-half 가 끝나고 나서 실행된다.
2. 특정 시간뒤에 실행(예를 들면, 10ms 뒤에 실행).
3. 커널 스레드가 스케줄링될 때 까지 대기한다. 즉, 스케줄러에게 선택될 때 까지 대기." 첫 번째 케이스는 softirq 및 tasklet 에 해당된다. 두 번째 케이스는 타이머에 속한다. 세 번째는 threaded IRQ, 일반적인 workqueue, 드라이버에서 자시만의 kthread 를 생성하는 경우를 들 수 있다(그러나, 마지막 케이스인 드라이버에서 kthread 를 직접해서 deferred work 를 수행하는 것은 권장되지 않는다). 이 글에서는 첫 번째 케이스중에 `tasklet` 에 대해 다뤄본다. softirq 는 이 글을 참고하자.
- Why do we need tasklet ?
1. Basic knowledge [참고1]
" 만약, deferred work 를 크게 나눈다면 2 가지로 분류할 수 있다.
1. delay time 정해진 경우
2. delay time 이 정해지지 않은 경우" 위에서 `delay time 이 정해지지 않은 경우` 는 다시 2가지로 나뉜다.
2.1. 빨리 실행될 수 록 좋은 케이스다. 이 경우는 퍼포먼스가 굉장히 중요한 경우를 의미한다. 이 케이스에서는 top-half 를 제외한, 즉 실제 하드웨어 인터럽트를 제외한 그 어떤 컨택스트(프로세스 컨택스트)도 현재 컨택스트(인터럽트 컨택스트)를 절대 선점할 수 없다. 아무리 높은 우선 순위의 프로세스가 있더라도 이 컨택스트(인터럽트 컨택스트)를 선점할 수 없다.
2.2. 그냥 때되면 실행되것지 케이스다. 이 케이스는 퍼포먼스는 신경쓰지 않는다. 스케줄러에 의해서 때 되면 실행되는 경우를 의미한다." (2.1) 번 케이스를 bottom-half 에 적용할 경우, 너무 많은 일을 수행해서는 안된다. 그리고, 너무 오랫동안 실행되서도 안된다. 왜 그럴까? 인터럽트 컨택스트에서는 process scheduling 을 할 수 없기 때문이다. 만약, 인터럽트 컨택스트에서 너무 시간을 잡아먹어 버리면, 프로세스 스케줄링 딜레이가 너무 지연되고, 심지어 언제 다른 프로세스를 실행할 수 있을지도 불확실해진다. 이러한 구조는 real-time 시스템에서는 최악이다.
2. bottom-half in linux
" 리눅스에서는 (2.1)번 타입에 softirq & tasklet 이 속하고, (2.2)번 타입에 workqueue & threaded irq 가 속한다. tasklet 은 softirq 와 비교해서 아래의 2가지 이점을 가지고 있다.
1. softirq 는 개수가 정해져있고, driver engineer 가 임의의로 만들 수 없다(할 수 있지만, 거의 불가능하다). 즉, softirq 는 커널에서 관리하는 메커니즘이다. 그러나, softirq 와 달리 tasklet 은 driver engineer 에 의해 자신만의 tasklet 을 생성할 수 있고 개수 또한 정적 및 동적으로 할당될 수 있다. 생성할 수 있는 tasklet 의 개수에는 제한이 없다.
2. softirq 와 달리 동일 tasklet 은 여러 CPU`s 에서 병렬적으로 실행될 수 없다. 이 특징 덕분에 con-currency 에 대한 부담을 덜어준다(물론, 병렬적으로 실행되지 않기 때문에 softirq 보다 퍼포먼스를 떨어질 수 있다)." 그러나, 현재는 위의 2가지 이점이 사실은 단점이 되고있다. (1) 번 조건은 tasklet 을 오남용하기 좋게 만든다. 왜냐면, 드라이버 개발자들은 자기 맘대로 tasklet 을 만들 수 있기 때문이다. 문제는 tasklet 이 interrupt context 에서 동작하는 high-prioiry task 이기 때문에, 드라이버 자체에는 좋은 퍼포먼스를 제공할 수 있어도, 시스템 전체 퍼포먼스에는 악영향을 줄 수 있다는 것이다. 커널은 이런 부분 때문에 드라이버 개발자들에게 softirq 를 직접 생성할 수 없게 만든것이다.
" (2) 조건 덕분에 확실히 softirq 에서 발생할 수 있는 concurrency 문제를 덜 수 있게 되었다. 그러나, tasklet 처리 메커니즘은 실행 중인 일반 프로세스들에게 CPU 선점권을 무분별하게 뺏어버린다. softirq 는 reentrant 특성이 있기 때문에, 한 번 실행될 때 병렬적으로 처리되서 이런 문제가 덜 하다.
softirq tasklet con-currency reentrant 를 고려해야 한다. 즉, 다른 프로세서들과 병렬적으로 실행함. 1. reentrant 를 허용되지 않는다. 즉, 다른 프로세서들과 병렬적으로 실행될 수 없다. " 이상적인 시나리오를 놓고 softirq 와 tasklet 의 포퍼먼스를 비교하면 당연히 softirq 가 더 좋은 퍼포먼스를 낸다.
" 결국 tasklet 의 이점은 하나뿐인 것 같다. tasklet 은 softirq 를 기반으로 packaging 했기 때문에, softirq 보다는 사용하기가 더 편리하다. 특히, 동기화에 대한 고민이 사라지는 것이다. 이점이 이것뿐일까? 내 개인적인 생각으로는 이것뿐인 것 같다.
- Removal tasklet [참고1 참고2]
" 리눅스 커널 커뮤니티에서 tasklet 사용에 대한 논쟁은 꾀나 오래전 부터 이어져 왔다. 2007 년도에 LWN 에 기재된 `Eliminating tasklets` 에서는 `tasklet 은 softirq context 에서 동작하기 때문에, 시스템에 존재하는 그 어떤 프로세스보다 우선 순위가 높다. 결국, 이것 때문에 unbouned latencies 를 생성할 가능성이 매우 높다` 라는 부분에 대해 우려했다. 물론, 반대 의견도 나왔다. `빠른 이벤트 처리가 필요한 드라이버에서 tasklet 을 제거하면 퍼포먼스 손실이 있을 수 있다` 는 반대 의견이 나왔다. 그러나, 이 시점에는 threaded irq 가 아직 mainline 에 커밋되기 전이었다.
" 현재 커널에서는 tasklet 을 대체할 수 있는 기법들이 많이 존재한다. 예를 들어, workqueue, timer, threaded irq 를 사용할 수 있다. 예를 들어, threaded irq 를 이용하면 deferred work 가 인터럽트 핸들러 자체에서 수행된다. 이 기법은 이전 tasklet 이 가지고 있는 단점이 존재하지 않으면서 동일한 포퍼먼스를 만들어 낼 수 있다. 즉, 드라이버 개발자들은 더 이상 tasklet 의 필요성에 대해 느끼지 못하게 될 것 이다.
- Tasklet in detail
1. data structure
" 각 CPU 는 자신이 처리해야 할 tasklet 을 관리하는 연결 리스트를 가지고 있다.
// include/linux/interrupt.h - v6.5 /* Tasklets --- multithreaded analogue of BHs. This API is deprecated. Please consider using threaded IRQs instead: https://lore.kernel.org/lkml/20200716081538.2sivhkj4hcyrusem@linutronix.de Main feature differing them of generic softirqs: tasklet is running only on one CPU simultaneously. Main feature differing them of BHs: different tasklets may be run simultaneously on different CPUs. Properties: * If tasklet_schedule() is called, then tasklet is guaranteed to be executed on some cpu at least once after this. * If the tasklet is already scheduled, but its execution is still not started, it will be executed only once. * If this tasklet is already running on another CPU (or schedule is called from tasklet itself), it is rescheduled for later. * Tasklet is strictly serialized wrt itself, but not wrt another tasklets. If client needs some intertask synchronization, he makes it with spinlocks. */ struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; bool use_callback; union { void (*func)(unsigned long data); void (*callback)(struct tasklet_struct *t); }; unsigned long data; };
// include/linux/interrupt.h - v6.5 enum { TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */ TASKLET_STATE_RUN /* Tasklet is running (SMP only) */ };
1. next : 이걸 통해서 연결 리스트에 연결된다. 다음 tasklet 을 가리킨다.
2. state : tasklet 의 상태를 나타낸다. 뒤에서 보겠지만, tasklet synchronization 은 이 값을 통해 이루어진다.
- TASKLET_STATE_SCHED(0) : 현재 시점에 tasklet 이 실행되기 위해서 스케줄링 되었음을 의미한다.
- TASKLET_STATE_RUN(1) : 현재 tasklet 이 특정 CPU 에서 실행 중임을 의미한다.
3. count : 이 값이 0 이면, tasklet 은 enabled 되어있음을 의미한다. 그렇지 않으면, disabled 를 의미한다.
4. use_callback : 기존에 자료 구조 형태는 아래와 같다.
struct tasklet_struct{struct tasklet_struct *next;unsigned long state;atomic_t count;void (*func)(unsigned long);unsigned long data;};
5. func & data : 콜백 함수(func)와 func 에 전달되는 인자(data0를 나타낸다. 새로운 구조에서는 이 필드들은 사용하지 않는다.2. How to enable / disable tasklet ?
" softirq 에서 우리는 bottom-half synchronization 에 대해서 알아봤다. 특히, local_bh_disable 함수는 process context 의 critical section 에서 bottom-half 가 진입하지 못하도록 한다. 그런데, local_bh_disable 함수는 softirq 와 tasklet 모두에 적용된다. 경우에 따라서는 tasklet 만 막고 싶을 수 가 있다. 이럴 때는 tasklet_disable 함수를 사용할 수 있다. 그런데, tasklet 은 Local CPU 전체에 적용되는 API(local_irq_disable / local_bh_disable) 가 없다. 즉, 개별 tasklet 마다 tasklet_disable 함수를 호출해야 한다. tasklet count 은 nesting 을 지원하기 때문에, 반드시 pair 로 동작해야 한다.
// include/linux/interrupt.h - v6.5 static inline void tasklet_disable_nosync(struct tasklet_struct *t) { atomic_inc(&t->count); smp_mb__after_atomic(); } static inline void tasklet_disable(struct tasklet_struct *t) { tasklet_disable_nosync(t); tasklet_unlock_wait(t); smp_mb(); } static inline void tasklet_enable(struct tasklet_struct *t) { smp_mb__before_atomic(); atomic_dec(&t->count); }
" tasklet_unlock_wait 함수는 tasklet 이 동작중일 경우(TASKLET_STATE_RUN) 끝날 때 까지 기다린다. 즉, TASKLET_STATE_SCHED 상태가 될 때 까지 기다려준다.
3. How to manage tasklets ?
" 자료 구조를 설명할 때 언급했었지만, tasklet 은 각 CPU 마다 별도의 연결 리스트를 통해서 관리된다.
// kernel/softirq.c - v6.5 /* * Tasklets */ struct tasklet_head { struct tasklet_struct *head; struct tasklet_struct **tail; }; static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec); static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
" softirq 에서 tasklet 을 위해서 2개의 softirq vector 가 할당되어있다. HI_SOFTIRQ 는 우선 순위가 높은 tasklet 에게, TASKLET_SOFTIRQ 는 일반 tasklet 에게 할당된다. softirq 같은 경우는 priorty 는 `순서` 를 의미했다. 즉, __softirq_pending 멤버 변수에 0번째 비트가 가장 높은 우선 순위를 의미하고, 숫자가 커질 수 록 우선 순위가 낮아진다. 이 말은 동시에 다수의 softirq 요청이 들어올 경우에, softirq 처리 순서는 __softirq_pending 의 비트 위치에 의존한다는 뜻이다.
// include/linux/interrupt.h - v6.5 enum { HI_SOFTIRQ=0, .... TASKLET_SOFTIRQ, .... };
" 주의할 점은 HI_SOFTIRQ 는 TIMER_SOFTIRQ 보다 우선 순위가 높다. 그렇기 때문에, HI_SOFTIRQ 사용에 대해서 반드시 주의가 필요하다(참고로, v6.5 에서 HI_SOFTIRQ 를 사용하는 코드는 존재하지 않는다). HI_SOFTIRQ 와 TASKLET_SOFTIRQ 의 동작 메커니즘이 동일하기 때문에, 이 글에서는 TASKLET_SOFTIRQ 를 위주로 다룬다.
4. How to define a tasklet ?
" 아래의 매크로를 사용해서 정적으로 tasklet 을 생성할 수 있다. 접두사로 `_DISABLED` 가 같은 매크로들은 `->count`를 1 로 초기화해서 tasklet 을 비활성화 상태로 생성한다. `_OLD` 는 기존 func & data 의 호환성을 위해 존재하는 매크로들이다.
// include/linux/interrupt.h - v6.5 - v6.5 #define DECLARE_TASKLET(name, _callback) \ struct tasklet_struct name = { \ .count = ATOMIC_INIT(0), \ .callback = _callback, \ .use_callback = true, \ } #define DECLARE_TASKLET_DISABLED(name, _callback) \ struct tasklet_struct name = { \ .count = ATOMIC_INIT(1), \ .callback = _callback, \ .use_callback = true, \ } .... #define DECLARE_TASKLET_OLD(name, _func) \ struct tasklet_struct name = { \ .count = ATOMIC_INIT(0), \ .func = _func, \ } #define DECLARE_TASKLET_DISABLED_OLD(name, _func) \ struct tasklet_struct name = { \ .count = ATOMIC_INIT(1), \ .func = _func, \ }
" 동적 할당을 통해서 tasklet 을 생성할 수 두 있다. 정적 생성과 마찬가지로 old(tasklet_init) 와 new(tasklet_setup) 로 나뉜다. 주의할 점은 동적으로 초기화를 진행할 경우, count 가 0 으로 초기화된다는 점이다. 즉, tasklet 이 enabled 상태로 초기화된다.
// kernel/softirq.c - v6.5 - v6.5 void tasklet_setup(struct tasklet_struct *t, void (*callback)(struct tasklet_struct *)) { t->next = NULL; t->state = 0; atomic_set(&t->count, 0); t->callback = callback; t->use_callback = true; t->data = 0; } EXPORT_SYMBOL(tasklet_setup); void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data) { t->next = NULL; t->state = 0; atomic_set(&t->count, 0); t->func = func; t->use_callback = false; t->data = data; } EXPORT_SYMBOL(tasklet_init);
5. How to schedule a tasklet ?
" tasklet 은 softirq 를 기반으로 하기 때문에, 스케줄링 요청 방법이 동일하다. 즉, 최종적으로 `raise_softirq_xxx` 계열의 함수를 사용한다.
// include/linux/interrupt.h - v6.5 static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t); }
// kernel/softirq.c - v6.5 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); // --- 1 head = this_cpu_ptr(headp); // --- 1 t->next = NULL; *head->tail = t; // --- 2 head->tail = &(t->next); // --- 2 raise_softirq_irqoff(softirq_nr); // --- 3 local_irq_restore(flags); // --- 1 } void __tasklet_schedule(struct tasklet_struct *t) { __tasklet_schedule_common(t, &tasklet_vec, TASKLET_SOFTIRQ); }
1. tasklet 을 요청하는 작업은 최종적으로 Per-CPU 연결 리스트에 연결되어야 한다. Per-CPU 이기 때문에, 멀티 프로세서끼리의 선점 문제는 걱정하지 않아도 된다. 그렇다면, 동일 프로세서내에서 스레드간의 경쟁 관계만 주의하면 된다. 동일 프로세서내에서 스레드의 교체는 컨택스트 스위칭을 통해서 발생한다. 컨택스트 스위칭은 일반적인 경우에 타이머 인터럽트로 인해 발생한다. 즉, 타이머 인터럽트만 Off 하면 critical sectiion 을 보호할 수 있게 된다.
2. 리스트 자료 구조를 사용하지 않고 직접 tasklet 끼리 연결 리스트 구조를 이루고 있다. 코드가 작은 만큼 내용이 압축되어 있어서 이해하기가 조금 어려울 수 있다. 2가지에 초점을 맞춰야 한다.
1. struct tasklet_struct 들 끼리 직접 연결되는 구조가 아니다. 전역적으로 존재하는 struct tasklet_vec 를 통해 struct tasklet_struct 끼리 서로 연결된다.
2. 위의 과정을 이중 포인터를 통해서 진행한다. 여기서 중요한 점은 이중 포인터를 단순히 struct tasklet_struct 들을 연결시키는 용 사용하므로, 별도의 동적 할당은 하지 않는다.
3. 당연히 tasklet 을 요청했기 때문에, 해당 요청을 저장하는 과정도 필요하다. tasklet 은 softirq 가 등록 절차가 동일하기 때문에, raise_softirq_irqoff(TASKLET_SOFTIRQ) 함수를 사용하면 된다. 그런데, 왜 raise_softirq_irqoff 함수를 사용했을까? raise_softirq 함수는 사용할 수 없나? caller 측에서 인터럽트를 비활성화한 경우 raise_softirq_irqoff 함수를 사용한다." 그런데, `struct tasklet_head->tail` 은 어떻게 `struct tasklet_head->head` 를 가리키고 있을까? tasklet 이 초기화되는 과정을 확인해보자.
// kernel/softirq.c - v6.5 void __init softirq_init(void) { int cpu; for_each_possible_cpu(cpu) { // --- 1 per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; } open_softirq(TASKLET_SOFTIRQ, tasklet_action); // --- 2 open_softirq(HI_SOFTIRQ, tasklet_hi_action); }
1. tasklet 이 초기화될 때, `Per-CPU tail = &head` 를 확인할 수 있다.
2. softirq 에 등록된 2개의 tasklet 이 tasklet_action / tasklet_hi_action 으로 초기화되는 것을 확인할 수 있다.6. When will the tasklet be executed ?
" 스케줄링된 tasklet 은 언제 실행될까? tasklet 은 결국 softirq 기반이기 때문에, softirq 가 실행되는 시점에 tasklet 도 실행된다. 그렇다면, softirq 가 실행되는 시점은 언제일까? 크게 2가지 케이스로 나눌 수 있다. (1) 하드웨어 인터럽트가 반환되는 시점, (2) softirq 를 re-enable 하는 시점(local_bh_enable, spin_unlock_bh).
There are two places where software interrupts can "fire" and preempt the current thread. One of them is at the end of the processing for a hardware interrupt; it is common for interrupt handlers to raise softirqs, so it makes sense (for latency and optimal cache use) to process them as soon as hardware interrupts can be re-enabled. The other possibility is anytime that kernel code re-enables softirq processing (via a call to functions like local_bh_enable() or spin_unlock_bh()). The end result is that the accumulated softirq work (which can be substantial) is executed in the context of whichever process happens to be running at the wrong time; that is the "randomly chosen victim" aspect that Thomas was talking about.
- 참고 : https://lwn.net/Articles/520076/" softirq 를 re-enable 한다는 게 무슨 뜻일까? 커널 스페이스의 process context 에서 bottom-half handler 로 부터 critical section 을 보호하기 위해 local_bh_disable 함수를 사용하는 경우가 있다. 그런데, 이건 bottom-half 는 막더라도 hardware interrupt 는 막지못한다. 만약 크리티컬 섹션에서 하드웨어 인터럽트가 발생하면 process context 는 즉각적으로 선점되버린다. 이후에 인터럽트가 반환될 시점에 softirq 가 처리될 수 있을까? 당연히 안된다. 왜냐면, 이전에 process context 에서 softirq context 를 disable 했기 때문이다.
" 결론적으로, 인터럽트가 리턴되는 시점에 softirq 가 처리되지 못한다. 그러나, bottom-half 는 본질적으로 process context 보다 우선 순위가 훨씬 높다. 그래서 기회만 된다면, 즉각적으로 process context 를 선점해서 ksoftirqd 를 실행시켜 버린다. 여기서 `기회가 되다면` 이 바로 `local_bh_enable` 함수다. process context 에서 critical section 작업이 끝나고 local_bh_enable 함수를 호출하면, pending softirq 가 있는지를 확인한다. 있다면, ksoftirqd 를 스케줄링하고, 실행중 인 process 를 선점해버린다." 이제 스케줄링된 tasklet 를 실제로 실행하는 함수를 분석해보자.
// kernel/softirq.c - v6.5 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(); // --- 1 list = tl_head->head; tl_head->head = NULL; tl_head->tail = &tl_head->head; local_irq_enable(); // --- 1 while (list) { // --- 2 struct tasklet_struct *t = list; // --- 2 list = list->next; // --- 2 if (tasklet_trylock(t)) { // --- 3 if (!atomic_read(&t->count)) { // --- 4 if (tasklet_clear_sched(t)) { // --- 5 if (t->use_callback) { // --- 6 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(); // --- 7 t->next = NULL; *tl_head->tail = t; tl_head->tail = &t->next; __raise_softirq_irqoff(softirq_nr); local_irq_enable(); } } static __latent_entropy void tasklet_action(struct softirq_action *a) { tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ); }
1. tasklet 도 softirq 처럼, 한 번 처리될 때, pending tasklets 를 모두 처리한다. 이 때, 처리할 pending tasklets 들을 모두 별도로 빼내고, 기존 tasklets 을 모두 제거한다. 제거하는 이유 또한 softirq 와 비슷하다. 처리중에 새로운 요청이 들어오면 무한 루프로 처리해 할 수 도 있기 때문이다. 그리고, tasklet_vec 는 Per-CPU 이기 때문에 인터럽트만 비활성화하면 atomic context 가 보장되므로 리스트를 안전하게 제거할 수 있다. 리스트를 리셋한 뒤, tasklet 또한 bottom-half 이므로 인터럽트를 다시 활성화한다.
2. 모든 pending tasklets 을 탐색한다.
3. 이 글은 리얼 타임 리눅스 커널에 대해서는 다루지 않는다. 상위 4개는 SMP 혹은 리얼 타임 리눅스 커널에서 지원하는 함수들이고, 하위 4개는 UP 구조에서 사용하는 API 다. 현대의 대부분의 프로세서는 일반적으로 SMP 구조를 따르므로, 우리는 위쪽 API 들에 집중하면 된다.// include/linux/interrupt.h - v6.5 #if defined(CONFIG_SMP) || defined(CONFIG_PREEMPT_RT) static inline int tasklet_trylock(struct tasklet_struct *t) { return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state); } void tasklet_unlock(struct tasklet_struct *t); void tasklet_unlock_wait(struct tasklet_struct *t); void tasklet_unlock_spin_wait(struct tasklet_struct *t); #else static inline int tasklet_trylock(struct tasklet_struct *t) { return 1; } static inline void tasklet_unlock(struct tasklet_struct *t) { } static inline void tasklet_unlock_wait(struct tasklet_struct *t) { } static inline void tasklet_unlock_spin_wait(struct tasklet_struct *t) { } #endif
// kernel/softirq.c - v6.5 #if defined(CONFIG_SMP) || defined(CONFIG_PREEMPT_RT) void tasklet_unlock(struct tasklet_struct *t) { smp_mb__before_atomic(); clear_bit(TASKLET_STATE_RUN, &t->state); smp_mb__after_atomic(); wake_up_var(&t->state); } .... #endif
" tasklet_trylock 함수는 현재 tasklet 이 TASKLET_STATE_RUN 상태인지를 검사한다(1번째 비트를 검사). 맞다면, 0을 반환한다. 그렇지 않다면, 양수를 반환한다. 그리고, 현재 어떤 상태인지에 관계없이 TASKLET_STATE_RUN 상태로 변경한다(이 때, t->state 는 `2` 가 된다). 즉, CPU[0] 에서 tasklet A 가 TASKLET_STATE_RUN 상태라면, 동일한 tasklet A 는 CPU[X] 에서 실행될 수 없다는 것을 의미한다(2개의 사건이 동시에 발생했음을 전제로 한다).
4. 처리하려는 tasklet 이 enabled(0) 상태인지를 체크한다. enabled 상태라면, tasklet 을 실행할 수 있다는 것을 의미한다.
5. `tasklet_clear_sched` 함수는 인자로 전달된 tasklet 의 상태가 TASKLET_STATE_SCHED 인지를 검사한다. 맞다면, CLEAR 하고 true 를 반환한다. 그런데, TASKLET_STATE_SCHED 가 아니라면, false 반환한다.
// kernel/softirq.c - v6.5 static bool tasklet_clear_sched(struct tasklet_struct *t) { if (test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) { wake_up_var(&t->state); return true; } WARN_ONCE(1, "tasklet SCHED state not set: %s %pS\n", t->use_callback ? "callback" : "func", t->use_callback ? (void *)t->callback : (void *)t->func); return false; }
" 그런데, 이 함수가 호출되기전에 t->state 를 TASKLET_STATE_SCHED 로 변경한 것으로 보였다. 그렇다면, 이 함수에서는 무조건 false 를 반환하지 않을까? t->state 의 구조에 대해서 알 필요가 있다. TASKLET_STATE_SCHED & TASKLET_STATE_RUN 은 각 비트를 나타내고 있다. 그래서 TASKLET_STATE_SCHED 는 0번째 비트를, TASKLET_STATE_SCHED 는 1번째 비트를 검사한다. 즉, 상태가 병렬적으로 컨트롤된다(비트를 2개 사용한다는 뜻).
" 이쯤오니 의문점이 2가지가 생긴다.
1. 왜 `if (!atomic_read(&t->count)` 부터 검사하지 않을까? 최적화를 위해서 이렇게 짠 것으로 보인다. 즉, tasklet 이 disabled 상태보다 다른 CPU 에서 처리중인 케이스가 더 많은 것이다. 예를 들어, A 와 B 조건이 있는데 2개의 조건 중 하나만 false 여도 함수가 종료된다고 가정하자. 여기서 A와 B가 각각 false 가 되는 빈도가 `1123 : 8` 이라고 가정하자. 그렇면 조건문을 어떻게 짜야할까? 당연히 정답은 `1` 이다. A 를 먼저 검사하면, B 까지 검사할 필요가 없을 경우가 많을 것 이기 때문이다.
1. if (A && B)
2. if (B && A)
2. 왜 상태를 병렬적으로 변경 가능하도록 만들었을까? 이걸 이해하기 위해서는 상태가 나올 수 있는 경우의 수를 따져봐야 한다. 총 4가지 경우의 수가 존재한다.
TASKLET_STATE_RUN TASKLET_STATE_SCHED 설명 0 0 tasklet 을 처리중도 아니면서, tasklet 실행 요청도 없는 상태. 0 1 tasklet 을 처리중은 아니지만, tasklet 실행 요청이 있는 상태(Per-CPU tasklet_vec 에 연결된 상태). 1 0 tasklet 을 처리중인 상태. 추가요청은 없음. 1 1 tasklet 이 실행 중에 하드웨어 인터럽트가 발생해서 동일한 tasklet 을 또 요청할 수 도 있다. " 아래 tasklet 의 상태 변화도를 나타낸다. 함수의 호출 관계를 보면 틀린 그림이다. 단지, tasklet 의 상태가 schedule / handle 에서 시간 흐름상 어떻게 변하는지만 참고하자.
6. New tasklet 자료 구조는 콜백 함수의 파라미터가 변경되었다. use_callback 이 true 면, new tasklet 을 사용한다는 것을 의미한다. false 면, 기존 tasklet 을 의미한다. 참고로, v6.5 trace_tasklet_entry / trace_tasklet_exit 함수는 소스상에서 아예 존재하지 않는다.7. 여기까지 왔다는 것은 아래의 2가지 케이스 중에 하나에 속한다.
1. 현재 tasklet 이 다른 CPU 에서 이미 실행중이다(TASKLET_STATE_RUN).
2. 현재 tasklet 이 disabled 상태다." 위에 2 개의 케이스 모두 처리되지 못한 tasklet 를 어떻게 처리 할까? 폐기하지 않는다. 해당 tasklet 은 다시 Per-CPU tasklet_vec 뒤쪽에 연결한다. 즉, retry 하는 것이다. 정리하면, tasklet 이 매핑된 CPU 에서 처리될 때 까지 계속 retry 를 반복하는 구조다.
" 그런데, tasklet 이 disabled 상태에서도 retry 를 하는 것이 맞는 구조일까? 다른 CPU 에서 동일 tasklet 이 처리 중일 경우에 retry 는 납득할 수 있지만, disabled 상태인 tasklet 은 언제 enabled 될지 알 수 없다. 이건 `unbounded latencies` 가 발생하기 아주 좋은 구조다.
'Linux > kernel' 카테고리의 다른 글
[리눅스 커널] cache - basic (1) 2023.11.04 [리눅스 커널] Scheduler - Basic (0) 2023.10.30 [리눅스 커널] Synchronization - Per-CPU Overview (0) 2023.10.18 [리눅스 커널] Interrupt - Softirq (0) 2023.10.18 [리눅스 커널] Synchronization - RCU Overview (0) 2023.10.16