为什么Linux内核不允许在中断中休眠?

我以前一直以为 Linux 内核之所以不允许在中断中休眠是因为中断上下文中无法获取 thread_info,task_struct 等进程相关的结构体,从而无法调度。

今天又重新看了一下相关的代码,发现实际上不是。在最新的代码中,x86 32 使用的 irq stack 中也保存了thread_info,我们可以看 execute_on_irq_stack() 的定义:

[c]
union irqctx {
struct threadinfo tinfo;
u32 stack[THREAD_SIZE/sizeof(u32)];
} __attribute
((aligned(THREAD_SIZE)));

static inline int
execute_on_irq_stack(int overflow, struct irq_desc desc, int irq)
{
union irq_ctx
curctx, irqctx;
u32
isp, arg1, arg2;

    curctx = (union irq_ctx *) current_thread_info();
    irqctx = __this_cpu_read(hardirq_ctx);

    /*
     * this is where we switch to the IRQ stack. However, if we are
     * already using the IRQ stack (because we interrupted a hardirq
     * handler) we can't do that and just have to keep using the
     * current stack (which is the irq stack already after all)
     */
    if (unlikely(curctx == irqctx))
            return 0;

    /* build the stack frame on the IRQ stack */
    isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));
    irqctx->tinfo.task = curctx->tinfo.task;
    irqctx->tinfo.previous_esp = current_stack_pointer;

    /* Copy the preempt_count so that the [soft]irq checks work. */
    irqctx->tinfo.preempt_count = curctx->tinfo.preempt_count;

    if (unlikely(overflow))
            call_on_stack(print_stack_overflow, isp);

    asm volatile("xchgl     %%ebx,%%esp     n"
                 "call      *%%edi          n"
                 "movl      %%ebx,%%esp     n"
                 : "=a" (arg1), "=d" (arg2), "=b" (isp)
                 :  "0" (irq),   "1" (desc),  "2" (isp),
                    "D" (desc->handle_irq)
                 : "memory", "cc", "ecx");
    return 1;

}
[/c]

(注:x86 都已经使用 irq stack 了,和进程的内核栈独立开了,这样即使 irq 处理函数中占用了很多栈也不会影响外面的了。)

所以这不是问题。也就是说,技术上我们完全可以做到在中断中休眠。Linux 内核之所以没有这么做是出于设计上的原因。

如果这还不足以说明问题的话,还有个例子:do_page_fault() 也是在中断上下文中调用的(注:此处更严格的讲是 trap,而非 interrupt,但无论哪个都肯定不是普通的进程上下文,相对而言还算是中断上下文),但是它是可能休眠的,至少它明显调用了可能休眠的函数 down_read() 。

为什么要这么设计呢?因为在 Linux 内核中能让 do_page_fault() 休眠的地方基本上只有copy_from_user(),copy_to_user()(其它地方触发 page fault 会导致 oops),它们在使用用户空间的页面时可能会因为对应的页面不在物理内存中而触发 swap 等,即 handle_mm_fault() 所做的。这种情况下休眠是合理的,因为调用 copy_from_user() 的进程本身就是需要等待这个资源。休眠不就是为了等待资源吗?

为什么其它中断(注:此处是指 IRQ,或者严格意义上的 interrupt,非 trap)不能休眠?假设 CPU 上正在执行的是某个高优先级的进程 A,它本身没有使用任何网络通信,突然网卡中断来了,它被中断而且被休眠了,那么,这看起来就像是进程 A 本身在等待某网络资源,而实际上根本不是。

换句话说,如果在 IRQ 中休眠,被打断的当前进程很可能不是需要等待的进程,更进一步讲,在中断中我们根本无法知道到底是哪个进程需要休眠。更何况,这里还没有考虑到在某个中断中休眠会不会影响下一个中断之类的更复杂的问题。由此可见,如果真允许在中断中休眠的话,那么设计将会是很复杂的。

因此,这完全是一个设计的问题。中断中休眠技术上可实现,但设计上不好。

另外,Linux 内核中很多会休眠的函数会调用 might_sleep(),它是用来检测是否在中断上下文中,如果是就是触发一个警告 “BUG: sleeping function called from invalid context”。可是为什么 do_page_fault() 中的函数没有呢?这是因为普通的 IRQ 在进入处理函数之前都会调用 irq_enter(),它里面改变了进程的 preempt 的值:

[c]

define __irq_enter()

    do {                                            
            account_system_vtime(current);          
            add_preempt_count(HARDIRQ_OFFSET);      
            trace_hardirq_enter();                  
    } while (0)

void irq_enter(void)
{
int cpu = smp_processor_id();

    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_check_idle(cpu);
            _local_bh_enable();
    }

    __irq_enter();

}
[/c]

而 page fault 的话不会经过这个流程。这也从另一面证明了这是个设计的问题。

顺便说一句,比较新的 Linux 内核中已经支持中断线程化了:

[c]
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char devname, void dev_id)
[/c]

所以一部分中断处理可以在进程上下文中完成了。