커널이나 드라이버 코드 리뷰를 하는 도중에 preempt_add, preempt_sub 그리고 in_interrupt 함수를 자주 마주칩니다. 이 함수들의 사용 예와 구현부에 대해서 좀 더 짚어 볼까요?
리눅스 커널에서 보는 모든 코드는 두 가지 모드에서 돌고 있어요.
process context: 우리가 보는 대부분의 코드라고 할 수 있는데, 커널 쓰레드로 돌고 있는 상태죠.
IRQ context: 어떤 디바이스던 인터럽트 전기 신호로 IRQ가 Trigger될 수 있어요. 그래서 해당 IRQ에 매핑되는 Interrupt Subroutine(ISR) 핸들러나 이 서브 루틴에서 돌고 있는 상태죠.
그럼 어떤 함수가 IRQ/process context인지는 어떻게 알 수 있을까요? 스스로 참 알기 어렵죠.
이를 식벽할 수 있는 매크로가 in_interrupt랍니다.
in_interrupt 매크로의 역할을 간단히 말하면, 현재 실행 중인 코드가 process context 혹은 interrupt context 상에서 돌고 있는 지 알려줍니다. process context 혹은 interrupt context의 개념은 여러 리눅스 커널 교재에서 마르고 닳도록 설명하고 있는데요, 아래 스택 트레이스를 보면 간단히 파악할 수 있습니다.
__irq_svc(asm) -- unwind_backtrace() 사이에 보이는 함수(붉은색으로 마킹)들은 irq context에서 실행되는 함수들,
start_kernel() --arch_cpu_idle() 구간에 보이는 함수들은 process context에서 구동이 된다고 보시면 됩니다.
[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/commit/?id=bbe097f092b0d13e9736bd2794d0ab24547d0e5d]
WARNING: CPU: 0 PID: 0 at include/linux/usb/gadget.h:405
ecm_do_notify+0x188/0x1a0
Modules linked in:
CPU: 0 PID: 0 Comm: swapper Not tainted 4.7.0+ #15
Hardware name: Atmel SAMA5
[<c010ccfc>] (unwind_backtrace) from [<c010a7ec>] (show_stack+0x10/0x14)
[<c010a7ec>] (show_stack) from [<c0115c10>] (__warn+0xe4/0xfc)
[<c0115c10>] (__warn) from [<c0115cd8>] (warn_slowpath_null+0x20/0x28)
[<c0115cd8>] (warn_slowpath_null) from [<c04377ac>] (ecm_do_notify+0x188/0x1a0)
[<c04377ac>] (ecm_do_notify) from [<c04379a4>] (ecm_set_alt+0x74/0x1ac)
[<c04379a4>] (ecm_set_alt) from [<c042f74c>] (composite_setup+0xfc0/0x19f8)
[<c042f74c>] (composite_setup) from [<c04356e8>] (usba_udc_irq+0x8f4/0xd9c)
[<c04356e8>] (usba_udc_irq) from [<c013ec9c>] (handle_irq_event_percpu+0x9c/0x158)
[<c013ec9c>] (handle_irq_event_percpu) from [<c013ed80>] (handle_irq_event+0x28/0x3c)
[<c013ed80>] (handle_irq_event) from [<c01416d4>] (handle_fasteoi_irq+0xa0/0x168)
[<c01416d4>] (handle_fasteoi_irq) from [<c013e3f8>] (generic_handle_irq+0x24/0x34)
[<c013e3f8>] (generic_handle_irq) from [<c013e640>] (__handle_domain_irq+0x54/0xa8)
[<c013e640>] (__handle_domain_irq) from [<c010b214>] (__irq_svc+0x54/0x70)
[<c010b214>] (__irq_svc) from [<c0107eb0>] (arch_cpu_idle+0x38/0x3c)
[<c0107eb0>] (arch_cpu_idle) from [<c0137300>] (cpu_startup_entry+0x9c/0xdc)
[<c0137300>] (cpu_startup_entry) from [<c0900c40>] (start_kernel+0x354/0x360)
[<c0900c40>] (start_kernel) from [<20008078>] (0x20008078)
---[ end trace e7cf9dcebf4815a6 ]---J6
in_interrupt 함수는 언제 쓰면 좋을까요? 샘플 패치를 하나 만들어 보았는데요.
__rh_alloc() 함수가 Interrupt Context 즉 IRQ가 Trigger되어 호출이 될 경우, GFP_ATOMIC: atomic operation만으로 메모리 할당을 시도하고, 반대 Process Context인 경우 메모리 할당 실패 시 Sleep되어 성공할 때 까지 반복하는 GFP_KERNEL 옵션으로 메모리 할당를 수행하는 패치입니다. IRQ context는 빨리 해당 코드를 수행하고 종료시켜야 하는 에티켓을 가져야 해요. 그런데 페이지를 할당하려다가 잠들어버리면 낭패죠.
diff --git a/drivers/md/dm-region-hash.c b/drivers/md/dm-region-hash.c
index b929fd5..1325a8a 100644
--- a/drivers/md/dm-region-hash.c
+++ b/drivers/md/dm-region-hash.c
@@ -289,7 +289,12 @@ static struct dm_region *__rh_alloc(struct dm_region_hash *rh, region_t region)
{
struct dm_region *reg, *nreg;
- nreg = mempool_alloc(rh->region_pool, GFP_ATOMIC);
+ gfp_t gfp_flag = GFP_KERNEL;
+ if (in_interrupt()) {
+ gfp_flag = GFP_ATOMIC;
+ }
+ nreg = mempool_alloc(flush_entry_pool, gfp_flag);
+
if (unlikely(!nreg))
nreg = kmalloc(sizeof(*nreg), GFP_NOIO | __GFP_NOFAIL);
이와 비슷한 개념으로 생성한 다른 패치도 있네요.
[https://patchwork.kernel.org/patch/3623051/]
diff --git a/drivers/gpu/drm/ast/ast_fb.c b/drivers/gpu/drm/ast/ast_fb.c
index 66ecc16..d56d2bf 100644
--- a/drivers/gpu/drm/ast/ast_fb.c
+++ b/drivers/gpu/drm/ast/ast_fb.c
@@ -64,7 +64,7 @@ static void ast_dirty_update(struct ast_fbdev *afbdev,
* then the BO is being moved and we should
* store up the damage until later.
*/
- if (!in_interrupt())
+ if (!drm_can_sleep())
ret = ast_bo_reserve(bo, true);
if (ret) {
if (ret != -EBUSY)
in_interrupt 매크로 구현부를 확인해보면, 실제 irq_count() 매크로를 호출하는데 preempt_count() 매크로로 리턴되는 값과
HARDIRQ_MASK | SOFTIRQ_MASK 마스크와 Oring Operation을 수행합니다.
#define in_interrupt() (irq_count())
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK
| NMI_MASK))
SOFTIRQ_MASK: 0xff00, SOFTIRQ_OFFSET: 0x100
HARDIRQ_MASK: 0xf0000, HARDIRQ_OFFSET: 0x10000
그럼, preempt_count의 정체는 뭐냐? 코드를 좀 더 살펴보면, 현재 구동 중인 함수 내에서 확인되는 스택 주소를 통해
스택 top address를 얻어 온 후 (struct thread_info *) 구조체의 preempt_count 멤버에서 얻어오는 값입니다.
static __always_inline int preempt_count(void)
{
return current_thread_info()->preempt_count;
}
[kernel/arch/arm/include/asm/thread_info.h]
static inline struct thread_info *current_thread_info(void)
{
register unsigned long sp asm ("sp");
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); // 0x1FFF = THREAD_SIZE - 1
}
예를 들어 위에서 언급된 콜스택의 경우, 아래 계산식으로 preempt_count() 값을 얻어올 수 있습니다.
preempt_count이 0x00010002이군요.
(struct thread_info*)(0xE359B908 & ~0x1fff) = 0xE359A000 -> (
flags = 0x2,
preempt_count = 0x00010002, // HARDIRQ_OFFSET
addr_limit = 0xBF000000,
task = 0xD0B5EA40, //<<-- task descriptor
exec_domain = 0xC1A1AF1C,
cpu = 0x0,
cpu_domain = 0x15,
(where)
커널 패닉 시의 Stack address: 0xE359B908
그럼, struct thread_info.preempt_count 멤버에 HARDIRQ_OFFSET 비트를 어느 함수에서 설정할까요?
아래 함수 흐름에서 __irq_enter 매크로에서 HARDIRQ_OFFSET 비트를 설정하고 있음을 알 수 있습니다.
__irq_svc
-gic_handle_irq
--__handle_domain_irq
---irq_enter
----__irq_enter
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
unsigned int irq = hwirq;
int ret = 0;
irq_enter(); //<<--
//skip
if (unlikely(!irq || irq >= nr_irqs)) {
ack_bad_irq(irq);
ret = -EINVAL;
} else {
generic_handle_irq(irq); //<<-- ISR 호출
}
irq_exit(); //<<--
set_irq_regs(old_regs);
return ret;
}
void irq_enter(void)
{
rcu_irq_enter();
if (is_idle_task(current) && !in_interrupt()) {
/*
* Prevent raise_softirq from needlessly waking up ksoftirqd
* here, as softirq will be serviced on return from interrupt.
*/
local_bh_disable();
tick_irq_enter();
_local_bh_enable();
}
__irq_enter(); //<<--
}
#define __irq_enter()
do {
account_irq_enter_time(current);
preempt_count_add(HARDIRQ_OFFSET); //<<--
trace_hardirq_enter();
} while (0)
struct thread_info.preempt_count 멤버에서 HARDIRQ_OFFSET bit는 아래 코드 흐름에서 Clear됩니다.
__handle_domain_irq
(irq 처리 완료)
-irq_exit
--__irq_exit
#define __irq_exit()
do {
trace_hardirq_exit();
account_irq_exit_time(current);
preempt_count_sub(HARDIRQ_OFFSET);
} while (0)
/*
그럼 만약 IRQ context에서 schedule되면 어떻게 될까요?
걱정할 필요가 없어요. 커널이 친철하게 커널 패닉을 유발시키죠. __sched __schedule() 이란 함수가 실행될 때 schedule_debug() 함수를 호출하거든요.
static inline void schedule_debug(struct task_struct *prev)
{
#ifdef CONFIG_SCHED_STACK_END_CHECK
if (unlikely(task_stack_end_corrupted(prev)))
panic("corrupted stack end detected inside schedulern");
#endif
/*
* Test if we are atomic. Since do_exit() needs to call into
* schedule() atomically, we ignore that path. Otherwise whine
* if we are scheduling when we should not.
*/
if (unlikely(in_atomic_preempt_off() && prev->state != TASK_DEAD)) //<<--
__schedule_bug(prev); //<<--
rcu_sleep_check();
profile_hit(SCHED_PROFILING, __builtin_return_address(0));
schedstat_inc(this_rq(), sched_count);
}
최근에 이렇게 커널 패닉이 난 이슈가 있었는데요.
커널 패닉 전 친철하게 콜 트레이스를 뿌려주세요. 아래 트레이스도 마찬가지로 IRQ context죠.
[12402.719933 / 05-13 00:44:07.295] CPU: 5 PID: 0 Comm: swapper/5 Tainted: P B W O 3.10.49-g72eb1a8 #1
[12402.719997 / 05-13 00:44:07.295] Call trace:
[12402.720151 / 05-13 00:44:07.295] [<ffffffc0002069d0>] dump_backtrace+0x0/0x270
[12402.720235 / 05-13 00:44:07.295] [<ffffffc000206c50>] show_stack+0x10/0x1c
[12402.720409 / 05-13 00:44:07.295] [<ffffffc000c90470>] dump_stack+0x1c/0x28
[12402.720663 / 05-13 00:44:07.295] [<ffffffc0002488a4>] __schedule_bug+0x44/0x60
[12402.720898 / 05-13 00:44:07.295] [<ffffffc000c96f20>] __schedule+0x90/0x750
[12402.720975 / 05-13 00:44:07.295] [<ffffffc000c97644>] schedule+0x64/0x70
[12402.721052 / 05-13 00:44:07.295] [<ffffffc000c95880>] schedule_timeout+0x210/0x258
[12402.721128 / 05-13 00:44:07.295] [<ffffffc000c96b70>] wait_for_common+0xe8/0x12c
[12402.721208 / 05-13 00:44:07.295] [<ffffffc000c96c1c>] wait_for_completion_interruptible_timeout+0xc/0x18
[12402.721297 / 05-13 00:44:07.295] [<ffffffc00083194c>] pompeii_vfe40_axi_halt+0x1cc/0x200
[12402.721372 / 05-13 00:44:07.295] [<ffffffc000823980>] pompeii_axi_halt+0x58/0x90
[12402.721445 / 05-13 00:44:07.295] [<ffffffc000823cf0>] pompeii_process_done_buf+0x338/0x550
[12402.721518 / 05-13 00:44:07.295] [<ffffffc000825488>] pompeii_process_axi_irq+0x2a4/0x4a4
[12402.721600 / 05-13 00:44:07.295] [<ffffffc000820610>] pompeii_do_tasklet+0x19c/0x1f0
[12402.721680 / 05-13 00:44:07.295] [<ffffffc0002246f0>] tasklet_action+0x90/0xf8
[12402.721755 / 05-13 00:44:07.295] [<ffffffc000223e2c>] __do_softirq+0x148/0x274
[12402.721829 / 05-13 00:44:07.295] [<ffffffc000223fec>] do_softirq+0x40/0x54
[12402.721903 / 05-13 00:44:07.295] [<ffffffc0002241f8>] irq_exit+0x70/0xa8
[12402.721981 / 05-13 00:44:07.295] [<ffffffc0002040d0>] handle_IRQ+0x80/0xa0
[12402.722057 / 05-13 00:44:07.295] [<ffffffc0002006ec>] gic_handle_irq+0x6c/0xb0
최근 덧글