Linux内核——中断机制
Linux内核——中断机制
中断分类->中断流程->中断代码实现过程
中断分类
硬件中断
硬件中断:由ARM中断控制器GIC发出的中断
参考代码:asm.s trap.c(Linux0.1.1为例)
软件中断
软件中断(异常):CPU自身产生的中断、系统调用
参考代码:system_cal.s sys.c(Linux0.1.1为例)

中断工作流程
常规中断流程
- CPU工作模式转化
- 进行寄存器的拷贝和压栈
- 设置中断异常向量表
- 保存正常运行的函数地址(pc寄存器中)
- 跳转到对应的中断服务函数运行
- 恢复现场(模式、寄存器等)
- 跳转正常工作函数地址继续运行
Linux中断流程
保存上下文->执行中断->恢复上下文
- 将所有寄存器值入栈(ARM中的r0~r15)
- 将异常码(中断号)入栈(通过中断号找到中断函数)
- 将当前函数地址入栈(当前指令地址)
- 调用对应的中断服务函数
- 出栈函数地址(中断处指令地址)
- 恢复所有入栈的寄存器
中断的代码实现过程
硬件中断处理过程
no_error_code
参考代码:asm.s trap.c
该代码目录为:linux/kernel/asm.s
以无错误码为例:
具体代码含义见注释内容
_divide_error: # 中断处理程序的入口点。当发生除零错误时,CPU会跳转到这个标签处执行
pushl $_do_divide_error # 压入处理除零错误的C函数 from trap.c
no_error_code:
xchgl %eax,(%esp) # 将栈顶的值与%eax寄存器的值交换,用于后面调用
pushl %ebx # 寄存器入栈...保护现场
pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds
push %es
push %fs
pushl $0 # "error code"
lea 44(%esp),%edx # offset 计算中断发生时的栈帧地址
pushl %edx # 将这个地址压入栈中,作为后续C函数的参数
movl $0x10,%edx # 设置段寄存器,确保中断处理程序在内核模式下运行,内核数据段(0x10)。
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
call *%eax # 调用C函数
addl $8,%esp # 调整栈指针 +8 跳过"error code"(4字节)和函数地址(4字节) 一共8字节
pop %fs # 寄存器出栈... 恢复现场
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax
iret # 从中断返回
# 下面中断处理程序的逻辑与_divide_error 类似
# 都跳转到 no_error_code 标签处执行通用的保存上下文和调用C函数的逻辑
# 每个处理程序调用不同的C函数来处理对应的异常
_debug:
pushl $_do_int3 # _do_debug
jmp no_error_code
_nmi:
pushl $_do_nmi
jmp no_error_code
_int3:
pushl $_do_int3
jmp no_error_code
_overflow:
pushl $_do_overflow
jmp no_error_code
_bounds:
pushl $_do_bounds
jmp no_error_code
_invalid_op:
pushl $_do_invalid_op
jmp no_error_code
_coprocessor_segment_overrun:
pushl $_do_coprocessor_segment_overrun
jmp no_error_code
_reserved:
pushl $_do_reserved
jmp no_error_code
# 这是处理IRQ13(数学协处理器中断)的中断处理程序
# 它通过 outb 指令向硬件发送信号,确认中断已被处理
# 最后跳转到 _coprocessor_error 处理程序
_irq13:
pushl %eax
xorb %al,%al
outb %al,$0xF0
movb $0x20,%al
outb %al,$0x20
jmp 1f
1: jmp 1f
1: outb %al,$0xA0
popl %eax
jmp _coprocessor_error
在call *%eax执行对应的中断函数,根据处理程序的不同调用不同的执行函数
硬件中断的中断函数在开始进行中断处理时,都先入栈并xchgl %eax,(%esp)保存在eax中,后续调用过程如下:

由于上述中断执行函数的权限和处理的优先级不同,因此需要根据不同执行函数的权限和优先级进行初始化,即设置异常处理程序:
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);
outb_p(inb_p(0x21)&0xfb,0x21);
outb(inb_p(0xA1)&0xdf,0xA1);
set_trap_gate(39,¶llel_interrupt);
}
set_trap_gate权限较高,只能由系统调用
setsystem_gate权限较高,可以有系统、用户调用


(GDT:全局描述表、LDT:局部描述表、IDT:中断描述表)
软件中断处理过程
参考代码:system_cal.s sys.c
system_call
首先以系统调用为例:
在system_cal.s中
system_call:
cmpl $nr_system_calls-1,%eax # 检查系统调用号是否有效
ja bad_sys_call # 如果系统调用号大于最大值,跳转到 bad_sys_call 处理无效调用
push %ds # 保存用户数据段寄存器 ds
push %es # 保存用户附加段寄存器 es
push %fs # 保存用户附加段寄存器 fs
pushl %edx # 保存 edx 寄存器(系统调用参数)
pushl %ecx # 保存 ecx 寄存器(系统调用参数)
pushl %ebx # 保存 ebx 寄存器(系统调用参数)
movl $0x10,%edx # 将 0x10(内核数据段选择子)加载到 edx
mov %dx,%ds # 设置 ds 为内核数据段
mov %dx,%es # 设置 es 为内核数据段
movl $0x17,%edx # 将 0x17(用户数据段选择子)加载到 edx
mov %dx,%fs # 设置 fs 为用户数据段,以便访问用户空间数据
call _sys_call_table(,%eax,4) # 调用系统调用处理函数:
# - _sys_call_table 是系统调用表
# - eax 是系统调用号
# - 4 是每个表项的字节大小(函数指针占4字节)
pushl %eax # 将系统调用的返回值(eax)保存到栈中
movl _current,%eax # 将当前任务的 task_struct 指针加载到 eax
cmpl $0,state(%eax) # 检查任务状态(state 字段)是否为 0(运行态)
jne reschedule # 如果任务状态非运行态,跳转到 reschedule 进行任务调度
cmpl $0,counter(%eax) # 检查任务的时间片(counter 字段)是否为 0
je reschedule # 如果时间片用完,跳转到 reschedule 进行任务调度
系统调用(软件中断)和硬件中断的不同之处是:system_call将所有的系统调用函数放在一个系统调用表中,通过系统调用号调用,调用过程如下:
1.检查系统调用号有效后,相关寄存器入栈,保存现场
2.在call _sys_call_table(,%eax,4)执行系统调用,通过%eax中保存的系统调用号在系统调用表sys_call_table[]中查找

3.执行完对应的系统调用并处理一些信号处理工作,出栈恢复现场
ret_from_sys_call:
movl _current,%eax # 将当前任务的 task_struct 指针加载到 eax
cmpl _task,%eax # 比较当前任务是否是任务0(内核初始任务)
je 3f # 如果是任务0,跳转到标签3(任务0不能接收信号)
cmpw $0x0f,CS(%esp) # 检查保存的代码段(CS)是否是用户模式(0x0f)
jne 3f # 如果不是用户模式,跳转到标签3
cmpw $0x17,OLDSS(%esp) # 检查保存的栈段(SS)是否是用户模式(0x17)
jne 3f # 如果不是用户模式,跳转到标签3
movl signal(%eax),%ebx # 将当前任务的信号位图加载到 ebx
movl blocked(%eax),%ecx # 将当前任务的阻塞信号位图加载到 ecx
notl %ecx # 对阻塞信号位图取反,得到未阻塞的信号掩码
andl %ebx,%ecx # 计算未阻塞的信号(signal & ~blocked)
bsfl %ecx,%ecx # 找到第一个未阻塞的信号(从低位到高位)
je 3f # 如果没有未阻塞的信号,跳转到标签3
btrl %ecx,%ebx # 清除信号位图中对应的信号位
movl %ebx,signal(%eax) # 将更新后的信号位图保存回任务结构
incl %ecx # 信号号从0开始,加1得到实际的信号号
pushl %ecx # 将信号号压栈,作为 _do_signal 的参数
call _do_signal # 调用信号处理函数 _do_signal
popl %eax # 弹出信号号(清理栈)
3: popl %eax # 弹出 eax(系统调用返回值或信号号)
popl %ebx # 弹出 ebx
popl %ecx # 弹出 ecx
popl %edx # 弹出 edx
pop %fs # 弹出 fs
pop %es # 弹出 es
pop %ds # 弹出 ds
iret # 返回到用户空间
timer_interrupt
其他的中断方式,例如定时器中断的逻辑和系统调用相似:
_timer_interrupt:
push %ds # save ds,es and put kernel data space
push %es # into them. %fs is used by _system_call
push %fs
pushl %edx # we save %eax,%ecx,%edx as gcc doesn't
pushl %ecx # save those across function calls. %ebx
pushl %ebx # is saved as we use that in ret_sys_call
pushl %eax
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax
mov %ax,%fs
incl _jiffies
movb $0x20,%al # EOI to interrupt controller #1
outb %al,$0x20
movl CS(%esp),%eax
andl $3,%eax # %eax is CPL (0 or 3, 0=supervisor)
pushl %eax
call _do_timer # 'do_timer(long CPL)' does everything from
addl $4,%esp # task switching to accounting ...
jmp ret_from_sys_call
定时器中断函数执行完毕后跳转到ret_from_sys_call处理信号、恢复现场
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)