Linux内核——中断机制

中断分类->中断流程->中断代码实现过程

中断分类

硬件中断

硬件中断:由ARM中断控制器GIC发出的中断
参考代码:asm.s trap.c(Linux0.1.1为例)

软件中断

软件中断(异常):CPU自身产生的中断、系统调用
参考代码:system_cal.s sys.c(Linux0.1.1为例)

在这里插入图片描述

中断工作流程

常规中断流程

  1. CPU工作模式转化
  2. 进行寄存器的拷贝和压栈
  3. 设置中断异常向量表
  4. 保存正常运行的函数地址(pc寄存器中)
  5. 跳转到对应的中断服务函数运行
  6. 恢复现场(模式、寄存器等)
  7. 跳转正常工作函数地址继续运行

Linux中断流程

保存上下文->执行中断->恢复上下文

  1. 将所有寄存器值入栈(ARM中的r0~r15)
  2. 将异常码(中断号)入栈(通过中断号找到中断函数)
  3. 将当前函数地址入栈(当前指令地址)
  4. 调用对应的中断服务函数
  5. 出栈函数地址(中断处指令地址)
  6. 恢复所有入栈的寄存器

中断的代码实现过程

硬件中断处理过程

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,&divide_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,&parallel_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处理信号、恢复现场

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐