浅谈linux 内核网络 sk_buff 之克隆与复制
深入linux内核架构--进程&线程
了解Docker 依赖的linux内核技术
浅析linux内核网络协议栈--linux bridge
深入理解SR-IOV和IO虚拟化
如图 6.11 所示为中断或异常发生时,CPU 所要做的工作。
确定中断或异常的中断向量 i(在 0~255 之间)。可屏蔽中断的中断向量从中断控制器获得,不可屏蔽中断和异常的中断向量是固定的。
通过 IDTR 寄存器找到 IDT,读取第 i 项(即第 i 个门描述符)。
进行特权级检查。将 CPU 的当前特权级 CPL 与门描述符的 DPL 相比较,如果 CPL 小于 DPL,则能够通过门,否则产生通用保护异常(中断向量 13)
。通过门之后,获取门描述符中段选择符所指
向代码段的 DPL,将其与 CPL 相比较,如果小于,则执行中断处理程序,否则产生一个通用保护异常。
若上述过程检测到特权级发生了变化,则需要进行堆栈的切换。因为中断或异常服务程序运行在内核态(特权级为 0),当中断或异常在 CPU 处于用户态(特权级为 3)时发生,将引起堆栈的切换。
也就是说,从用户态堆栈切换到内核态堆栈。如果当前是异常,则 CPU 将异常码压入当前的堆栈。
如果当前是中断,CPU 清零 EFLAGS 的 IF 位,即关闭所有的可屏蔽中断;如果是异常,CPU 不会清零该位。进入中断或异服务程序并执行。
内核对中断的处理
图 6.12 所示为中断在内核中的处理过程。
所有中断(除去不可屏蔽中断)的服务程序在 init_IRQ 函数中都被初始化为 interrupt[i],interrupt 数组的每一项指向一个代码片段,该代码片段除了将中断向量号压入堆栈外,还要调用到一个公共的处理程序common_interrupt。
613 common_interrupt:
614 SAVE_ALL
615 TRACE_IRQS_OFF
616 movl %esp,%eax
617 call do_IRQ
618 jmp ret_from_intr
common_interrupt 函数首先保存现场,将中断发生前所有寄存器的值(属于被中断的进程)保存在堆栈中,然后调用 do_IRQ 函数。
do_IRQ 函数在 arch/i386/kernel/irq.c 文件中定义为:
071 fastcall unsigned int do_IRQ(struct pt_regs *regs)
072 {
073 ......
/* 取得中断向量号 */
075 int irq = ~regs->orig_eax;
076 ......
/* 进入中断上下文 */
089 irq_enter();
090 ......
/* 调用该 IRQ 的公共处理程序对中断进行处理 */
143 desc->handle_irq(irq, desc);
144 /* 退出中断上下文 */
145 irq_exit();
147 ......
148 }
从堆栈中取出中断向量号之后,do_IRQ 函数调用 irq_enter 进入中断上下文。irq_enter 函数主要的工作是增加被中断进程的 preempt_count 计数器的值,表示禁止抢占。在中断处理结束调用 irq_exit 退出中断上下文时,会相应地减少该计数器的值。
do_IRQ 函数的工作重心在第 143 行,调用每个 IRQ 线的公共服务程序。
在通用 IRQ 层引入之前,每个 IRQ 线并没有自己单独的服务程序,而是所有的 IRQ 线都共用一个公共的服务程序__do_IRQ 对中断进行处理。
在通用 IRQ 层引入之后,对不同类型中断(边沿触发、电平触发等)的处理流程做出了区分,并分别设 置 了 一 些 对 应 的 服 务 程 序 , 比 如 边 沿 触 发 中 断 对 应 handle_edge_irq 函 数 , 电 平 触 发 中 断 对 应handle_level_irq 函数等。
无论是旧的__do_IRQ 函数还是新的各种类型中断单独的服务程序,它们的基本原理还是一样的。都要首先屏蔽当前的 IRQ,禁止该中断线上的中断传递。
然后,会判断该 IRQ 线的中断请求队列上是否为空,即是否有一个或多个有效的中断服务程序,如果有的话,就调用 handle_IRQ_event 函数来遍历并执行挂载在该 IRQ 线中断请求队列上的所有中断处理程序。
handle_IRQ_event 函数在 arch/kernel/irq/handle.c 文件中定义为:(该函数可以被所有体系共用,而do_IRQ是不同体系定义不同的,可见这个handle_IRQ_event的通用性)
129 irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
130 {
131 irqreturn_t ret, retval = IRQ_NONE;
132 unsigned int status = 0;
133
134handle_dynamic_tick(action);
135
/* 因为 CPU 在通过中断门时自动将中断关闭,如果这里希望将它们打开,即希望
* 中断服务程序能够在中断打开的情况下执行,就需要在使用 request_irq 函数
* 注册中断处理程序时,不要设置 IRQF_DISABLED 标志。
*/
136if (!(action->flags & IRQF_DISABLED))
137local_irq_enable_in_hardirq();
/* 遍历该 IRQ 线的中断请求队列,并调用其中的所有中断服务程序。通常每个具体的
* 中断服务程序中都会一开始检查各自的中断源,判断是否有来自该设备的中断请求,
* 如果没有则马上退出。而且每个 IRQ 线的中断请求队列一般也不会很大,所以这个
* 过程并不会耗费很长的时间。
*/
139do {
140 ret = action->handler(irq, action->dev_id);
141 if (ret == IRQ_HANDLED)
142status |= action->flags;
143 retval |= ret;
144 action = action->next;
145} while (action);
146
/* 如果注册 IRQ 时设置了 IRQF_SAMPLE_RANDOM 标志,则需要调用
* add_interrupt_randomness 函数,以中断间隔时间为随机数产生熵。
*/
147if (status & IRQF_SAMPLE_RANDOM)
148add_interrupt_randomness(irq);
/* 最后关闭中断,函数返回。*/
149local_irq_disable();
150
151return retval;
152 }
定义了do_IRQ的文档,不同体系都有自己的定义
# line filename / context / line
1 58 arch/avr32/mach-at32ap/intc.c <>
asmlinkage void do_IRQ(int level, struct pt_regs *regs)
2 86 arch/cris/kernel/irq.c <>
asmlinkage void do_IRQ(int irq, struct pt_regs * regs)
3 141 arch/frv/kernel/irq.c <>
asmlinkage void do_IRQ(void )
4 171 arch/h8300/kernel/irq.c <>
asmlinkage void do_IRQ(int irq)
5 72 arch/m32r/kernel/irq.c <>
asmlinkage unsigned int do_IRQ(int irq, struct pt_regs *regs)
6 21 arch/m68knommu/kernel/irq.c <>
asmlinkage void do_IRQ(int irq, struct pt_regs *regs)
7 36 arch/microblaze/kernel/irq.c <>
void __irq_entry do_IRQ(struct pt_regs *regs)
8 159 arch/mips/kernel/irq.c <>
void __irq_entry do_IRQ(unsigned int irq)
9 156 arch/mn10300/kernel/irq.c <>
asmlinkage void do_IRQ(void )
10 384 arch/powerpc/kernel/irq.c <>
void do_IRQ(struct pt_regs *regs)
11 48 arch/score/kernel/irq.c <>
-- More --
中断处理结束之后,do_IRQ 函数返回到 common_interrupt(公共的处理程序),然后调用汇编函数 ret_from_intr 从中断返回。
如果内核正在从中断返回到用户空间,并且被中断进程设置了标志 TIF_NEED_RESCHED,则ret_from_intr 会调用 schedule 函数进行重新调度。
current->thread_info.flags中TIF_NEED_RESCHED为1,表示当前进程需要执行schedule()释放CPU控制权.
如果内核正在返回内核空间,即中断了内核本身,则ret_from_intr 会检查被中断进程的 preempt_count 计数器是否为 0,只有为 0 时,schedule 函数才会被调用(不为0表示此中断服务加锁,抢占这个中断不安全,不允许抢占;而schedule函数作用是即刻中断当前服务,对于加锁进程来说,是不安全的,所以只有为0的时候允许调用schedule)。