【进程管理】模式切换

      Linux内核将地址空间划分为用户空间和系统空间,用户程序只能访问用户空间,而系统程序对于用户空间和系统空间;由用户空间进入系统空间主要是通过系统调用和中断来进入的,对应用户空间切换到系统空间;X86对中断的支持非常复杂,linux内核只是使用了其中的一部分,很多机制不是必须的;


中断的分类

说明几点
(1)中断有两种,一种是由CPU外部硬件产生的,另一种是由CPU本身执行程序的过程中产生的;外部中断即我们所说的中断(interrupt),外部中断是异步的,由硬件产生,我们无法预测它什么时候发生;

(2)x86软件产生的中断是由“INT n”同步产生的,有程序产生,只要CPU执行了一条INT指令,就知道在开始执行下一条指令前就会进入中断服务程序,我们又称此类中断为“陷阱”;int 80为系统调用的陷阱号
(3)异常,是被动的,如页面异常,除数为0的异常;

(4)上述异常,陷阱,以及中断,都统称为中断,CPU的相应过程基本一致,在执行完当前指令后,根据中断源提供的中断向量,在内存中找到相应的服务程序入口并调用该服务程序;

(5)外部中断向量表是由软件或硬件设置好的,陷阱向量是在自陷指令INT n中发出的,各种异常向量是由CPU的硬件结构中预先规定好的;不同的情况也就根据不同的中断向量而分开下来了;


x86的中断实现

说明几点
(1)Intel CPU支持256个向量,在早起的实地址模式中,CPU从0开始到1K字节作为一个中断向量表,每一个表项占4个字节,由两个字节的段地址和两个字节的位移组成,这样的地址构成了中断服务程序的入口地址;但是这样的机制不能构成现代意义上的操作系统,即使把16位寻址改为32位也无济于事,因为缺少对PSW的处理,无法完成运行模式的切换;在保护模式中,中断向量表的表项由单纯的入口地址改成了类似于PSW加入口地址并且更为复杂的描述项,称为gate,意思就是必须通过这些门,才能进入相应的中断服务程序;这样的门不仅为中断所用,还为切换CPU的运行状态而设置;根据不同的用途和目的,门分为任务门,中断门,陷阱门,调用门(不是与中断向量表相联系的);

(3)任务门结构:TSS段选择码,通过GDT或LDT指向特殊的系统段中的一种,实际上是用来保存任务运行“现场”的 数据结构(CPU中所有与具体进程有关的寄存器内容,包括页面目录指针CR3)和三个堆栈指针;中断发生时,CPU在中断向量表中找到相应的表项,如果此项是个任务门,并通过了优先级的检查,CPU会将当前任务的运行现场保存在相应的TSS中,并将任务门所指向的TSS作为当前任务,将其内容装入到CPU的各个寄存器,从而完成一次任务切换;为此,又增设了任务寄存器TR,用来指向当前任务的TSS。在linux内核中,一个任务就是一个进程,但Task_struct存放了更多的信息,因此linux内核并不完全是通过任务门作为切换进程的唯一手段;

(4)除任务门,其他三种门的结构基本相同,类型码标识不同的门,中断门的类型是110,陷阱门的类型是111,调用门的类型码是100;任务门不需要段内位移,因为它不需指向某一个子程序的入口,TSS作为一个段来对待的,而其他的三个门指向一个子程序,所以得结合段选择码和段内位移;

(5)Intel在i386CPU中的实现一个非常复杂的优先级别检验机制,先是门的DPL和CPU的CPL相比,CPL小于等于DPL才能通过该门(通常是中断门),然后在将目标代码段描述项中的DPL与CPL再比较;通过门后,CPU的CPL优先级只能提高;两次比较中,任何一个失败,都将引起一次全面保护异常;

(6)进入中断服务程序中,CPU要将当前EFLAGS寄存器的内容以及返回地址(CS和EIP)压入堆栈中;若中断是由异常引起的,还要将出错原因压入堆栈中;根据目标代码中的运行级别的不同,分为0,1,2,分别对应三个额外的堆栈指针,若中断发生时的CPL和目标代码中的DPL不同,就要切换堆栈指针;
(7)在Linux内核中,当中断发生在用户空间时,运行级别为3,而内核中的中断服务程序运行级别为0,所以会引起堆栈的变换,而若中断发生在系统空间时,则不会更换堆栈;


系统调用

说明几点

(1)外部中断是CPU被动的,异步的进入系统空间的一种手段,而系统调用就是CPU主动、同步的进入系统空间的手段;软件设计人员确切的知道执行指令后就一定会进入系统空间;相比之下,中断有很大的不可预测性,但是它们都使CPU的运行状态从用户态转入系统态,当然中断有可能发生在系统空间运行时,而系统调用只发生在用户空间;其实最大的原因在于CPU运行状态的变化,就是所谓的保护模式

(2)Linux系统调用通过中断指令“INT 0x80”来完成,所有系统调用都要进入系统空间,在完成了所需要的服务后从系统空间返回,比如sethostname()是这样一个系统调用,设置主机名;将%eax存入0x4a(通过寄存器转入系统调用号),然后调用“INT 0x80”;如果用堆栈来传系统调用号,用户空间到系统空间要涉及到堆栈的切换,用户堆栈变为系统堆栈,虽然可以从用户空间读,但麻烦;从系统调用返回时,出错可设置好出错代码以及返回值;


模式切换

说明几点

(1)当外部中断发生时,CPU根据中断控制器取得中断向量,根据中断向量从中断向量表IDT中找到相应的表项,该表项对应的是个中断门;这样CPU就根据中断门的设置而达到了该通道的总服务程序的入口,假定为IRQ0x03_interrupt。由于中断是用户空间发生的,运行级别CPL为3,中断服务程序属于内核,其运行级别DPL为0;所以,CPU要从寄存器TR所指的当前TSS中取出内核(0级)的堆栈指针,并把堆栈切换到内核堆栈,即当前进程的系统空间堆栈;而每次从系统空间返回时要返回到用户空间时堆栈一定回到其原点;也就是说CPU进入IRQ0x03_interrupt时,堆栈中除寄存器EFLAGS的内容以及返回地址,一无所有;穿过中断门(非陷阱门)后,中断是被关闭,因为通过中断门时CPU会自动将中断关闭的

(2)对于系统调用,CPU穿过陷阱门和发生中断穿过中断门的过程是相同的,外部中断穿过中断门,是不需要检查中断门规定的级别,而INT指令穿过中断门或陷阱门时,要核对所规定的准入级别和CPU当前运行的级别;系统调用设置的陷阱门的准入级别DPL为3;寄存器IDTR指向当前的中断向量表IDT,而IDT表中对应表项0x80的表项就是为INT 0x80设置的陷阱门,其中的函数指针是system_call();

(3)

中断进入的公有代码如下:

//所有的中断都共享该代码,在这之前会将中断请求号的数值压入堆栈,这样可以确定中断源的来源,比如0x03-256,减去负数主要是为了区分系统调用
//进入中断以后,CPU会禁止中断
	.p2align CONFIG_X86_L1_CACHE_SHIFT
common_interrupt:
	addl $-0x80,(%esp)	/* Adjust vector into the [-256,-1] range */
	SAVE_ALL           //保护现场,主要是保存一些寄存器
	TRACE_IRQS_OFF
	movl %esp,%eax
	call do_IRQ        //执行中断处理程序
	jmp ret_from_intr  //恢复现场
ENDPROC(common_interrupt)
	CFI_ENDPROC


系统调用的入口

ENTRY(system_call)
	RING0_INT_FRAME			# can't unwind into user space anyway
	pushl_cfi %eax			# save orig_eax
	SAVE_ALL
	GET_THREAD_INFO(%ebp)
					# system call tracing in operation / emulation
	testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
	jnz syscall_trace_entry
	cmpl $(nr_syscalls), %eax
	jae syscall_badsys
syscall_call:
	call *sys_call_table(,%eax,4)  
	movl %eax,PT_EAX(%esp)		# store the return value
syscall_exit:
	LOCKDEP_SYS_EXIT
	DISABLE_INTERRUPTS(CLBR_ANY)	# make sure we don't miss an interrupt
					# setting need_resched or sigpending
					# between sampling and the iret
	TRACE_IRQS_OFF
	movl TI_flags(%ebp), %ecx
	testl $_TIF_ALLWORK_MASK, %ecx	# current->work
	jne syscall_exit_work         //恢复现场后会调度

异常的入口(以page_fault为例子)

ENTRY(page_fault)
	RING0_EC_FRAME
	pushl_cfi $do_page_fault
	ALIGN
error_code:
	/* the function address is in %gs's slot on the stack */
	pushl_cfi %fs
	/*CFI_REL_OFFSET fs, 0*/
	pushl_cfi %es
	/*CFI_REL_OFFSET es, 0*/
	pushl_cfi %ds
	/*CFI_REL_OFFSET ds, 0*/
	pushl_cfi %eax
	CFI_REL_OFFSET eax, 0
	pushl_cfi %ebp
	CFI_REL_OFFSET ebp, 0
	pushl_cfi %edi
	CFI_REL_OFFSET edi, 0
	pushl_cfi %esi
	CFI_REL_OFFSET esi, 0
	pushl_cfi %edx
	CFI_REL_OFFSET edx, 0
	pushl_cfi %ecx
	CFI_REL_OFFSET ecx, 0
	pushl_cfi %ebx
	CFI_REL_OFFSET ebx, 0
	cld
	movl $(__KERNEL_PERCPU), %ecx
	movl %ecx, %fs
	UNWIND_ESPFIX_STACK
	GS_TO_REG %ecx
	movl PT_GS(%esp), %edi		# get the function address
	movl PT_ORIG_EAX(%esp), %edx	# get the error code
	movl $-1, PT_ORIG_EAX(%esp)	# no syscall to restart
	REG_TO_PTGS %ecx
	SET_KERNEL_GS %ecx
	movl $(__USER_DS), %ecx
	movl %ecx, %ds
	movl %ecx, %es
	TRACE_IRQS_OFF
	movl %esp,%eax			# pt_regs pointer
	call *%edi
	jmp ret_from_exception         //返回
	CFI_ENDPROC
END(page_fault)


进程切换判断

	# userspace resumption stub bypassing syscall exit tracing
	ALIGN
	RING0_PTREGS_FRAME
ret_from_exception:
	preempt_stop(CLBR_ANY)
ret_from_intr:      //返回现场
	GET_THREAD_INFO(%ebp)
check_userspace:
	movl PT_EFLAGS(%esp), %eax	# mix EFLAGS and CS
	movb PT_CS(%esp), %al
	andl $(X86_EFLAGS_VM | SEGMENT_RPL_MASK), %eax
	cmpl $USER_RPL, %eax      //如果发生在用户空间(系统调用和发生在用户空间的外部中断)
	jb resume_kernel		# not returning to v8086 or userspace   //需要调度

ENTRY(resume_userspace)
	LOCKDEP_SYS_EXIT
 	DISABLE_INTERRUPTS(CLBR_ANY)	# make sure we don't miss an interrupt
					# setting need_resched or sigpending
					# between sampling and the iret
	TRACE_IRQS_OFF
	movl TI_flags(%ebp), %ecx
	andl $_TIF_WORK_MASK, %ecx	# is there any work to be done on
					# int/exception return?
	jne work_pending
	jmp restore_all
END(ret_from_exception)

#ifdef CONFIG_PREEMPT
ENTRY(resume_kernel)
	DISABLE_INTERRUPTS(CLBR_ANY)
	cmpl $0,TI_preempt_count(%ebp)	# non-zero preempt_count ?  //是否允许抢占
	jnz restore_all    //不允许,就恢复
need_resched:
	movl TI_flags(%ebp), %ecx	# need_resched set ?
	testb $_TIF_NEED_RESCHED, %cl
	jz restore_all        //恢复
	testl $X86_EFLAGS_IF,PT_EFLAGS(%esp)	# interrupts off (exception path) ?
	jz restore_all
	call preempt_schedule_irq   //调度
	jmp need_resched
END(resume_kernel)
#endif
	CFI_ENDPROC
说明几点:

(1)在用户空间发生的外部中断以及异常以及系统调用会在返回用户空间,进行进程调度;通过cmpl $USER_RPL, %eax 来判断是否发生在用户空间;

保护现场如下:
.macro SAVE_ALL
	cld
	PUSH_GS
	pushl_cfi %fs
	/*CFI_REL_OFFSET fs, 0;*/
	pushl_cfi %es
	/*CFI_REL_OFFSET es, 0;*/
	pushl_cfi %ds
	/*CFI_REL_OFFSET ds, 0;*/
	pushl_cfi %eax
	CFI_REL_OFFSET eax, 0
	pushl_cfi %ebp
	CFI_REL_OFFSET ebp, 0
	pushl_cfi %edi
	CFI_REL_OFFSET edi, 0
	pushl_cfi %esi
	CFI_REL_OFFSET esi, 0
	pushl_cfi %edx
	CFI_REL_OFFSET edx, 0
	pushl_cfi %ecx
	CFI_REL_OFFSET ecx, 0
	pushl_cfi %ebx
	CFI_REL_OFFSET ebx, 0
	movl $(__USER_DS), %edx
	movl %edx, %ds
	movl %edx, %es
	movl $(__KERNEL_PERCPU), %edx
	movl %edx, %fs
	SET_KERNEL_GS %edx
.endm


恢复现场如下:

//恢复现场
restore_all:
	TRACE_IRQS_IRET
restore_all_notrace:
	movl PT_EFLAGS(%esp), %eax	# mix EFLAGS, SS and CS
	# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
	# are returning to the kernel.
	# See comments in process.c:copy_thread() for details.
	movb PT_OLDSS(%esp), %ah
	movb PT_CS(%esp), %al
	andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
	cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
	CFI_REMEMBER_STATE
	je ldt_ss			# returning to user-space with LDT SS
restore_nocheck:
	RESTORE_REGS 4			# skip orig_eax/error_code
irq_return:
	INTERRUPT_RETURN
.section .fixup,"ax"
ENTRY(iret_exc)
	pushl $0			# no error code
	pushl $do_iret_error
	jmp error_code
.previous
.section __ex_table,"a"
	.align 4
	.long irq_return,iret_exc
.previous

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。