进程上下文和中断上下文,是 Linux 内核里两个最基本的概念。搞不清楚这个,很多代码你看着对,一跑就崩,还不知道为什么。
比如,内核代码里调 schedule() 是家常便饭,但同一个 schedule(),在系统调用里跑没问题,在网卡中断 handler 里跑就立刻 panic。这个差异从哪来的?
答案就在上下文里。
上下文是什么
上下文,说白了就是"这段代码手里有几张牌"。
用户态程序能调 malloc、能读写文件,是因为它活在一个完整的执行环境里:地址空间、文件描述符表、寄存器状态。这个环境,就是上下文。
内核代码也一样。它跑在 CPU 上的时候,要么带着某个进程的完整家当,要么啥也没有、只有一个孤零零的中断栈。带和不带,决定了它能干什么、不能干什么。
进程上下文:带着进程的全部家当
系统调用、缺页异常这些代码执行时,内核是在"代表某个进程"干活。current 宏指向的 task_struct,就是当前进程的身份证:
// include/linux/sched.h
struct task_struct {
volatile long state;
void *stack; // 内核栈
struct mm_struct *mm; // 用户态地址空间
struct files_struct *files; // 文件描述符表
// ...
};
内核想访问用户态内存?走 current->mm。想读写文件?走 current->files。想发信号?走 current->signal。所有上下文信息都挂在 current 上。
进程上下文最关键的特点:它能被调度。
进程 A 在内核里跑系统调用,来了个更高优先级的进程 B,调度器把 A 的寄存器、内核栈全部存进 task_struct,换 B 上 CPU 执行。所以进程上下文里可以睡、可以等锁、可以主动让出 CPU。
看个真实的读文件路径:
// fs/read_write.c
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_read(f.file, buf, count, &pos);
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
buf 上的 __user 标记说明它指向用户态地址空间。在进程上下文里能安全访问,因为 current->mm 知道用户态内存的页表结构。

中断上下文:什么都没有
网卡来了个包,硬件拉低 IRQ 线,CPU 立刻停下手里的活,跳到中断向量表。
这时候硬件说"你不管这个包就别想再干别的",所有中断被屏蔽,current 指向一个无辜的进程,这个东西叫中断上下文。
第一条铁律:不能睡。
内核里所有会睡的函数,底层都检查 preempt_count:
// kernel/sched/core.c
asmlinkage __visible void __sched notrace preempt_schedule(void)
{
if (likely(preemptible()))
__preempt_schedule();
}
// include/linux/preempt.h
#define preemptible() (preempt_count() == 0 && !irqs_disabled())
调度器一旦从 task_struct 里发现 preempt_count != 0,就拒绝调度。
在中断上下文调 wait_event、mutex_lock 这些会睡的锁,底层走到 schedule,发现 preempt_count 不为 0,直接 panic:
BUG: sleeping function called from invalid context at ...
第二条铁律:不能操作 current->mm。
中断来的时候 CPU 可能在跑任意进程,current->mm 指向的是这个随机进程的地址空间。copy_from_user 一跑,要么拿到别人的数据,要么读到的地址根本不在当前页表里。
正确的做法:在中断上下文里,只处理硬件层面的工作(确认中断源、读写寄存器、把数据从硬件搬进内核 buffer)。想访问用户态的代码,要么扔给 workqueue。
第三条铁律:锁要选对。
spin_lock 不会关中断。下半部要是也拿同一把锁,中断嵌套进来就死锁了:
unsigned long flags;
spin_lock_irqsave(&dev->lock, flags); // 关本地中断 + 拿锁
// 操作共享数据 ...
spin_unlock_irqrestore(&dev->lock, flags);
能用 spin_lock 的就别动中断,能锁的地方就锁。
中断的三层拆解
之所以要拆成三层,是因为硬中断 handler 执行期间,同 CPU 上的其他中断是被关掉的。
handler 要是磨叽了,卡住的不止是当前包——后面所有硬件中断都得排队。所以 Linux 把中断处理拆成三层,越往下越自由,越往上越快:

第一层:硬中断上下文
硬件触发的第一层。handler 必须在几百微秒内跑完,只做两件事:确认中断源,通知下半部。
// drivers/net/ethernet/intel/e1000/e1000_main.c
static irqreturn_t e1000_intr(int irq, void *data)
{
struct net_device *netdev = data;
struct e1000_adapter *adapter = netdev_priv(netdev);
u32 icr = er32(ICR); // 读中断状态寄存器,自动清除中断源
if (!napi_schedule_prep(&adapter->napi))
return IRQ_HANDLED;
e1000_irq_disable(adapter);
__napi_schedule(&adapter->napi); // 把收包工作交给下半部
return IRQ_HANDLED;
}
e1000 的 handler 一共做了三件事:读 ICR 寄存器确认中断源、关该网卡的中断、napi_schedule 启动调度。读完寄存器、交完差就走人。
第二层:软中断上下文
硬中断退出后,内核检查有没有待处理的软中断。软中断虽然能响应高优先级硬中断,但本身仍然不算进程上下文:
// include/linux/interrupt.h
enum {
HI_SOFTIRQ, // 高优先级 tasklet
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ, // 网卡收包
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ,
};
软中断虽然能响应高优先级硬中断的嵌套,但绝对不能睡。底层还是受 preempt_count 的 SOFTIRQ 段保护。
第三层:Workqueue(进程上下文)
唯一能从下半部回到进程上下文的途径。work_struct 被插入内核线程的运行队列,等调度器选中才跑:
// kernel/workqueue.c
bool queue_work_on(int cpu, struct workqueue_struct *wq,
struct work_struct *work)
{
...
insert_work(wq, work, worklist, work_flags); // 插入内核线程队列
}
在内核线程里执行意味着有 task_struct,能睡、能拿 mutex、能调 copy_from_user。代价比 tasklet 多了线程切换开销。
preempt_count:上下文识别器
Linux 用一个 preempt_count 变量,知道当前代码在哪个上下文跑。它不是 0/1,而是分段计数的:

// include/linux/preempt.h
#define preempt_count() (current_thread_info()->preempt_count)
// include/asm-generic/preempt.h
#define PREEMPT_BITS 8
#define SOFTIRQ_BITS 8
#define HARDIRQ_BITS 4
#define NMI_BITS 4
进入硬中断,HARDIRQ 段加 1;进入软中断,SOFTIRQ 段加 1;显式禁止抢占,PREEMPT 段加 1。preempt_count 不为 0,调度器就拒绝调度。
内核直接判断就行:
if (in_irq())
printk("hard irq context\n");
else if (in_serving_softirq())
printk("softirq context\n");
else
printk("process context, pid=%d\n", current->pid);
in_irq、in_serving_softirq 这些宏,全读的是 preempt_count 的对应段。
系统层面,看硬中断和软中断的分布:
$ cat /proc/interrupts # 各 CPU 硬中断统计
$ cat /proc/softirqs # 各 CPU 软中断统计
进程上下文和中断上下文,就是内核代码的两种存在状态:要么带着进程的家当干活,要么什么都别想。
前者可以睡、可以等锁、可以被调度;后者必须快、不能阻塞、栈还小。所有机制——调度器、锁、下半部——都在回答同一个问题:这段代码此刻,手里到底有几张牌。