当前位置:首页>Linux>理解 Linux 代码的“两副牌”:探秘进程上下文与中断上下文

理解 Linux 代码的“两副牌”:探秘进程上下文与中断上下文

  • 2026-09-10 15:31:53
理解 Linux 代码的“两副牌”:探秘进程上下文与中断上下文

进程上下文和中断上下文,是 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 软中断统计

进程上下文和中断上下文,就是内核代码的两种存在状态:要么带着进程的家当干活,要么什么都别想。

前者可以睡、可以等锁、可以被调度;后者必须快、不能阻塞、栈还小。所有机制——调度器、锁、下半部——都在回答同一个问题:这段代码此刻,手里到底有几张牌。

最新文章

随机文章