当前位置:首页>Linux>Linux 内核中的上下文:从一个问题说起

Linux 内核中的上下文:从一个问题说起

  • 2026-10-11 05:37:07
Linux 内核中的上下文:从一个问题说起

为什么网卡收到一个数据包,系统要分成两步处理,而不是一步到位?

因为第一步(硬件中断)必须在中断上下文里跑,有严格限制;第二步(协议栈处理)可以慢慢来,但已经出了中断上下文。这两个"场景"就是 Linux 里说的上下文(Context)。

搞懂上下文这个概念,才明白为什么有些代码能睡,有些不能。

  

01 上下文是什么

上下文就是 CPU 执行代码时的"状态快照":寄存器值、程序计数器、内核栈指针、当前进程信息。

Linux 里最重要的两条线:

  • • 进程上下文(Process Context):CPU 在执行某个进程的内核态代码——比如进程 A 在跑 read() 系统调用。进程上下文属于某个进程,可以睡,可以阻塞。
  • • 中断上下文(Interrupt Context):CPU 在响应硬件中断,不属于任何进程。没有进程可以睡,因为没有人可以被调度进来接替。
  

02 陷入:用户态怎么进内核

用户态代码(Ring 3)想进内核态(Ring 0),靠陷入指令(Trap)。x86_64 上这条指令叫 syscall,不是软中断(那是 int 0x80,32 位时代的东西)。

// arch/x86/entry/entry_64.S (Linux v6.6)syscall:    swapgs                          // 切换到内核GS基址    movq %rsp, PER_CPU_VAR(rsp_scratch)    movq PER_CPU_VAR(kernel_stacks), %rsp    ...    call do_syscall_64

CPU 执行 syscall,自动跳到内核,跳转路径由 MSR 寄存器指定,不经过中断向量表,比 int 0x80 快得多。Linux 64 位统一走这条路径。

do_syscall_64 里查 sys_call_table[nr],分发到具体函数,返回后 syscall_exit_to_user_mode() 把 CPU 切回 Ring 3:

$ cat /proc/kallsyms | grep sys_call_tableffffffffabc00000 T sys_call_table

还有一条进内核的路:硬件中断。外设(网卡、磁盘)通过 CPU 的 INT 引脚发起中断,异步打断当前执行,CPU 自动切换到内核的中断处理入口。跟 syscall 的区别是:中断是外设主动打的,跟用户代码没关系。

03 中断上下文里不能干什么

中断上下文的核心约束:不能睡、不能阻塞、不能拿可能导致阻塞的锁。

原因很简单:中断打断的是一段正在执行的代码,中断处理完还要回到被打断的地方继续跑。如果在中断处理函数里调用了 schedule()(会触发调度),CPU 去跑另一个进程了,那被打断的进程谁来恢复?

所以中断处理程序必须:

  • • 执行要快(atomic)
  • • 不能调用 copy_to_user()(涉及用户态内存,可能触发缺页中断→阻塞)
  • • 不能拿 down(&semaphore)(信号量可能直接让当前进程等住)

Linux 解决这个矛盾的思路是:拆分——把必须在中断上下文跑的工作(最少、最急)跑完,剩下的扔给"可以睡"的环境去处理。

04 Bottom Half:延后处理的三条路

拆出来的后半段,就是 Bottom Half。Linux 历史上用过多种实现,现在稳定的三条路:

  

Softirq(软中断)

最高优先级的延后处理,在中断退出路径上执行。多 CPU 可以并行跑,同一个类型的 softirq 在每个 CPU 上独立实例运行。Linux 内核为特定场景静态注册了软中断向量:

// kernel/softirq.c (Linux v6.6)enum {    HI_SOFTIRQ=0,    TIMER_SOFTIRQ,    NET_TX_SOFTIRQ,    NET_RX_SOFTIRQ,    BLOCK_SOFTIRQ,    IRQ_POLL_SOFTIRQ,    TASKLET_SOFTIRQ,    SCHED_SOFTIRQ,    HRTIMER_SOFTIRQ,    ...};

网络收包走 NET_RX_SOFTIRQ,块设备请求走 BLOCK_SOFTIRQ,定时器到期走 TIMER_SOFTIRQ。

Tasklet(任务队列)

基于 softirq 实现,比 softirq 更晚引入,API 更友好。同一 CPU 上同一类型的 tasklet 串行执行,不同类型之间不保证顺序。驱动开发者常用 tasklet 而不是直接注册 softirq。

Workqueue(工作队列)

最灵活的一种。Workqueue 把延后处理扔到进程上下文里——在内核线程中执行。内核线程是可以被 schedule() 调度走的,所以 workqueue 的回调函数可以睡、可以阻塞、可以拿信号量、可以调用 copy_to_user()。

$ ps aux | grep kworkerroot     kworker/0:1H   // 高优先级工作队列线程root     kworker/0:2   // 普通工作队列线程root     ksoftirqd/0   // 处理软中断的工作线程

05 进程上下文:可以睡在内核里

进程上下文属于某个进程,核心特点是可以睡、可以阻塞。代表场景有两个:

系统调用

用户态 read() → syscall 指令 → do_syscall_64(),内核在进程上下文中执行 sys_read()。如果磁盘数据未就绪,sys_read() 里会调用 io_schedule() 让当前进程睡等。

内核线程

内核线程是没有对应用户态进程的"纯内核进程",有自己的 task_struct,参与调度,可以被 schedule() 换出。kworker/* 系列是最常见的,每秒处理大量工作队列任务。

上下文选择的核心逻辑

用这张表收尾:

需要做什么
选哪种上下文
必须立即响应外设(毫秒级)
Top Half,中断上下文
响应要快但可以稍等(软中断级)
Softirq,中断上下文
驱动级延后处理,不想注册软中断
Tasklet,中断上下文
处理逻辑复杂/需要睡/需要阻塞
Workqueue,进程上下文
用户请求到达内核(读文件、发网络包)
系统调用,进程上下文
内核后台周期性任务
内核线程,进程上下文

回到开头的问题:网卡数据包为什么要分两步处理?因为中断处理函数必须在原子上下文里跑——它要快,不能等。而协议栈处理(TCP 重传、socket 缓冲、应用层拷贝)逻辑重、耗时长,必须扔到工作队列里,让内核线程在进程上下文里慢慢处理。

最新文章

随机文章