Linux 0.11源码阅读笔记-中断过程

  • A+
所属分类:linux技术
摘要

中断发生时,计算机会停止当前运行的程序,转而执行中断处理程序,然后再返回原被中断的程序继续运行。中断包括硬件中断和软件中断,硬中断是由外设自动产生的,软中断是程序通过int指令主动调用。中断产生时,会有一个中断号,根据中断号可在中断向量表中选择对应的中断处理程序执行。


Linux 0.11源码阅读笔记-中断过程

是什么中断

中断发生时,计算机会停止当前运行的程序,转而执行中断处理程序,然后再返回原被中断的程序继续运行。中断包括硬件中断和软件中断,硬中断是由外设自动产生的,软中断是程序通过int指令主动调用。中断产生时,会有一个中断号,根据中断号可在中断向量表中选择对应的中断处理程序执行。

中断在linux当中非常重要,是用户态代码与和心态代码相互切换运行的桥梁。进程调度依赖于时钟中断进入内核,系统调用也是依赖int 80软中断进入内核执行。

中断处理过程

以int 80中断为例。system_call过程代码是 int 80中断处理程序,是所有系统调用的入口。位于linux-0.11/kernel/system_call.s文件中。

sytem_call执行过程

  1. 保存运行环境:保存被中断程序的运行环境,包括指令地址(PC)、段等寄存器的值。
  2. 执行系统调用:根据系统调用号,查找系统调用函数地址表,并执行系统调用函数
  3. 执行调度程序:判断进程时间片是否处于不可运行状态(时间片用完或者处于阻塞状态),若不可运行,则执行调度程序。
  4. 进行信号处理:若被中断程序为用户态进程,会先判断信号是否产生,并执行相应的信号处理程序。
  5. 恢复运行环境:恢复被中断程序的运行环境。

system_call代码及注释

### int 0x80 - linux系统调用入口点(调用中断int 0x80,eax 中是调用号) system_call: 	cmpl $nr_system_calls-1,%eax    # 调用号如果超出范围的话就在eax中置-1并退出 	ja bad_sys_call 	 # 保存原段寄存器值 	push %ds                         	push %es 	push %fs  # edx、ecx、ebx作为系统调用的参数 	pushl %edx 	pushl %ecx		# push %ebx,%ecx,%edx as parameters 	pushl %ebx		# to the system call 	 # 设置内核段 	movl $0x10,%edx	# set up ds,es to kernel space 	mov %dx,%ds 	mov %dx,%es # fs指向用户程序的数据段。 	movl $0x17,%edx		# fs points to local data space 	mov %dx,%fs  # 调用系统调用号对应的系统调用函数 	call sys_call_table(,%eax,4)	# 间接调用指定功能C函数 	pushl %eax						# 把系统调用返回值入栈 	 # 如果进程时间片(counter)用完或者状态(state)非就绪,则执行调度程序 	movl current,%eax		# 取当前任务(进程)数据结构地址→eax 	cmpl $0,state(%eax)		# state 	jne reschedule 	cmpl $0,counter(%eax)	# counter 	je reschedule  # 中断处理程序后半段,返回被中断程序继续执行。 ret_from_sys_call: # 若被中断程序为0号进程,直接返回。 	movl current,%eax		# task[0] cannot have signals 	cmpl task,%eax 	je 3f                   # 向前(forward)跳转到标号3处退出中断处理  # 若被中断程序运行在内核态(例如其它可被中断中断程序),则直接返回 	cmpw $0x0f,CS(%esp)		# was old code segment supervisor ? 	jne 3f 	cmpw $0x17,OLDSS(%esp)	# was stack segment = 0x17 ? 	jne 3f  # 若被中断程序为用户进程,则先处理进程的信号 # 通过信号位图,判断产生的信号,然后调用do_signal执行对应的信号处理程序 	movl signal(%eax),%ebx	# 取信号位图→ebx,每1位代表1种信号,共32个信号 	movl blocked(%eax),%ecx	# 取阻塞(屏蔽)信号位图→ecx 	notl %ecx				# 每位取反 	andl %ebx,%ecx			# 获得许可信号位图 	bsfl %ecx,%ecx			# 从低位(位0)开始扫描位图,看是否有1的位,若有,则ecx保留该位的偏移值 	je 3f					# 如果没有信号则向前跳转退出 	btrl %ecx,%ebx			# 复位该信号(ebx含有原signal位图) 	movl %ebx,signal(%eax)	# 重新保存signal位图信息→current->signal. 	incl %ecx				# 将信号调整为从1开始的数(1-32) 	pushl %ecx				# 信号值入栈作为调用do_signal的参数之一 	call do_signal			# 调用C函数信号处理程序(kernel/signal.c) 	popl %eax				# 弹出入栈的信号值 	 # 返回被中断程序 3:	popl %eax				# eax中含有上面入栈系统调用的返回值 	popl %ebx 	popl %ecx 	popl %edx 	pop %fs 	pop %es 	pop %ds 	iret					# 特权级中断返回指令 

用户栈与内核栈之间的切换

特权级发生变化时,会涉及到内核栈和用户栈的切换。linux具有两个特权级,内核态(0)和用户态(3)。int指令可以从用户态转入内核态,iret指令可以从内核态返回用户态。

Linux 0.11源码阅读笔记-中断过程

用户栈到内核栈

中断引起CPU特权级从3级到0级的变化,此时CPU会进行用户栈到内核栈的切换操作。

  1. 获取内核栈内存地址信息。CPU从当前任务状态段TSS(PCB)中取得新栈的段选择符和偏移值,内核栈指针从TSS的ss0(选择符)和esp0字段中获得。
  2. 将用户栈地址信息保存在内核栈中。将用户态栈指针和代码地址信息ss、esp、cs、eip压入内核栈中。
  3. 将ss、esp、cs、eip指向内核栈和代码地址。

内核栈到用户栈

iret指令引起CPU特权级从0级到3级的变化,此时CPU会进行内核栈到用户栈的切换操作。

iret指令将先前压入内核栈的用户进程栈和代码地址信息cs、esp、ss、esp信息从栈里弹出,加载到相应的寄存器中,重新执行用户进程。

进程调度的实现机制

每个用户进程有自己的内核栈,进程调度时,将栈寄存器指向需要运行的进程的内核栈,执行iret指令即可继续运行该进程。

内核栈的切换

进程调度的通过内核栈之间的切换,实现进程之间的切换运行。为什么每个进程都需要一个内核栈?进程需要保存运行状态信息(各种寄存器信息),等待被调度程序选中运行。

Linux 0.11源码阅读笔记-中断过程

进程切换

进程切换的实质在于进程状态(上下文)的切换,即将CPU的当前进程状态替换成新进程的状态。被替换进程的进程状态会保存在其对应的tss数据结构中,等待恢复运行。任务寄存器TR会保存当前任务的TSS指针,TSS数据结构总保存进程的运行状态。

具体过程为:

  1. 当前进程通过中断进入内核,用户态ss、esp、cs、ip寄存器信息保存在内核栈中,切换到内核态运行,使用内核堆栈
  2. schedule调度程序时,将当前进程内核态运行信息保存在当前任务的tss数据结构中,然后切换到新进程的内核运行状态,在内核中运行新的进程。
  3. 新运行的进程通过iret指令从内核态转到用户态运行,用户态的运行状态信息在其内核栈中。

Linux 0.11源码阅读笔记-中断过程

参考

  • Linux 内核完全注释 内核版本0.11 - 赵炯