RISC-V 기반의 리눅스 커널에서는,
do_irq() 함수에서 인터럽트에 대한 처리를 수행한다:
arch/riscv/kernel/traps.c
asmlinkage void noinstr do_irq(struct pt_regs *regs)
{
irqentry_state_t state = irqentry_enter(regs);
if (IS_ENABLED(CONFIG_IRQ_STACKS) && on_thread_stack())
call_on_irq_stack(regs, handle_riscv_irq);
else
handle_riscv_irq(regs);
irqentry_exit(regs, state);
}
대부분 64 아키텍처 기반의 리눅스 커널에서는 call_on_irq_stack() 함수가 호출되어서, 인터럽트 핸들러를 호출한다. 인터럽트에 대한 처리를 마무리하면 결국 irqentry_exit() 함수가 호출된다.
인터럽트 처리를 마무리한 후 유저 스페이스로 복귀하는 코드는 이제 모두 kernel/entry 디렉토리에 위치한다.
irqentry_exit() 함수의 구현부는 다음과 같다:
https://elixir.bootlin.com/linux/v6.17.13/source/kernel/entry/common.c
noinstr void irqentry_exit(struct pt_regs *regs, irqentry_state_t state)
{
lockdep_assert_irqs_disabled();
/* Check whether this returns to user mode */
if (user_mode(regs)) {
irqentry_exit_to_user_mode(regs);
유저 모드에서 코드가 실행 중이다가 인터럽트가 유발되면 커널 공간으로 접근한다.
이 시점의 유저 공간에서 실행 중인 레지스터는 struct pt_regs *regs 구조체로 표현할 수 있다.
user_mode(regs) 코드는 인터럽트가 유발되는 시점이 유저 공간인지를 체크한다.
이 조건에서 irqentry_exit_to_user_mode() 함수를 호출한다.
irqentry_exit_to_user_mode() 함수의 구현부이다.
kernel/entry/common.c
noinstr void irqentry_exit_to_user_mode(struct pt_regs *regs)
{
instrumentation_begin();
exit_to_user_mode_prepare(regs);
instrumentation_end();
exit_to_user_mode();
}
exit_to_user_mode_prepare() 함수는 유저 공간으로 복귀하기 전에,
프로세스의 상태를 체크한다. task_struct 구조체를 통해 프로세스가 시그널
펜딩인지, 혹은 처리해야 할 동작이 있는지 점검한다.
아래는 exit_to_user_mode_prepare() 함수의 구현부이다.
include/linux/irq-entry-common.h
static __always_inline void exit_to_user_mode_prepare(struct pt_regs *regs)
{
unsigned long ti_work;
lockdep_assert_irqs_disabled();
/* Flush pending rcuog wakeup before the last need_resched() check */
tick_nohz_user_enter_prepare();
ti_work = read_thread_flags();
if (unlikely(ti_work & EXIT_TO_USER_MODE_WORK))
ti_work = exit_to_user_mode_loop(regs, ti_work);
arch_exit_to_user_mode_prepare(regs, ti_work);
/* Ensure that kernel state is sane for a return to userspace */
kmap_assert_nomap();
lockdep_assert_irqs_disabled();
lockdep_sys_exit();
}
가장 마지막 코드에서 lockdep_sys_exit() 함수를 호출한다.
lockdep_sys_exit() 함수의 구현부는 다음과 같다:
https://elixir.bootlin.com/linux/v6.17.13/source/kernel/locking/lockdep.c
asmlinkage __visible void lockdep_sys_exit(void)
{
struct task_struct *curr = current;
if (unlikely(curr->lockdep_depth)) {
if (!debug_locks_off())
return;
nbcon_cpu_emergency_enter();
pr_warn("\n");
pr_warn("================================================\n");
pr_warn("WARNING: lock held when returning to user space!\n");
print_kernel_ident();
pr_warn("------------------------------------------------\n");
pr_warn("%s/%d is leaving the kernel with locks still held!\n",
curr->comm, curr->pid);
lockdep_print_held_locks(curr);
nbcon_cpu_emergency_exit();
}
무엇인가 lock을 획득하고 릴리즈하지 않는다면 이를 커널 코드로 출력한다.
lockdep_print_held_locks() 함수의 구현부이다.
https://elixir.bootlin.com/linux/v6.17.13/source/kernel/locking/lockdep.c
static void lockdep_print_held_locks(struct task_struct *p)
{
int i, depth = READ_ONCE(p->lockdep_depth);
if (!depth)
printk("no locks held by %s/%d.\n", p->comm, task_pid_nr(p));
else
printk("%d lock%s held by %s/%d:\n", depth,
str_plural(depth), p->comm, task_pid_nr(p));
/*
* It's not reliable to print a task's held locks if it's not sleeping
* and it's not the current task.
*/
if (p != current && task_is_running(p))
return;
for (i = 0; i < depth; i++) {
printk(" #%d: ", i);
print_lock(p->held_locks + i);
}
}
이미 held하고 처리하지 않는 락의 정보를 출력한다.
로그 리뷰
사실 여기까지 커널 코드를 분석한 이유는, 아래와 같은 커널 에러 로그를 확인했기 때문이다.
아래 코드를 분석하면서, 어느 커널 코드에서 에러 로그를 출력하는지 궁금했다.
[ 94.930394] <6>lkdtm: Performing direct entry SPINLOCKUP
[ 94.936805] <4>
[ 94.938349] <4>================================================
[ 94.944069] <4>WARNING: lock held when returning to user space!
[ 94.949789] <4>6.17.0+ #3 Not tainted
[ 94.953245] <4>------------------------------------------------
[ 94.958962] <4>bash/976 is leaving the kernel with locks still held!
[ 94.965121] <4>1 lock held by bash/976:
[ 94.968751] <4> #0: ffffffff81f86298 (lock_me_up){+.+.}-{3:3}, at: lkdtm_SPINLOCKUP+0x18/0x20
[ 94.977345] <3>BUG: sleeping function called from invalid context at kernel/task_work.c:229
[ 94.985623] <3>in_atomic(): 1, irqs_disabled(): 0, non_block: 0, pid: 976, name: bash
[ 94.993377] <3>preempt_count: 1, expected: 0
[ 94.997462] <4>INFO: lockdep is turned off.
다음 포스트에서는 커널 함수와 crash utility 분석을 함께 진행하겠다.