1、中断与系统调用的过程
1、程序控制流
正常程序的控制流只有next(执行下一条指令)和goto(跳转到另一条指令执行)。除了正常控制流之外,还有异常控制流,广义上异常控制流被称为中断。
狭义的中断是由外设产生的中断,例如按下键盘,此类中断被称为硬中断,这类中断是异步的,CPU执行指令的任何时候都有可能产生中断。另一类中断是由CPU执行中断指令产生的,属于编程中断,例如在x86系统上,执行int指令产生中断,其中int 0x80被用来实现系统调用,此类中断被称为软中断,这类中断是同步的,是执行指令的结果。还有一类中断是指令执行出错产生的,被称为异常,例如除零异常、缺页异常。无论硬中断、软中断还是异常,实现机制上是类似的,都是将CPU中断寄存器置位,以此表明产生了中断。
CPU在执行完一条指令之后,检测到有中断信号,如果中断没有屏蔽,则根据中断号,跳转到中断描述符表中指明的中断处理程序的位置(Interrupt Descriptor Table,IDT,中断描述符表是内存中的数据结构,起始位置保存在中断描述符表寄存器 IDTR中,x86系统中中断号128对应的中断处理程序是系统调用的入口),权限检查通过后执行完中断处理程序,之后,再跳转到原来程序的断点处,继续执行。
为了能够回到断点处继续执行,在跳转到中断处理程序之前,需要保存程序断点处的现场信息,也就是各个寄存器的值,包括各个段寄存器,程序计数器,各个数据寄存器,状态寄存器等。其中有硬件自动保存的,也有通过编程由软件保存的。中断处理程序执行完,恢复各个寄存器的原始值,CPU就可以从原来程序的断点处继续执行。
2、进程上下文
从硬件的角度来说,CPU无差别的执行程序流程,处理中断,并没有进程的概念。进程是从软件角度抽象出来的,进程在内存中的布局分为代码段、数据段、堆、用户栈和内核栈,不同的代码、数据、堆栈形成了不同的进程。CPU执行程序时,代码段、数据段、堆栈的内存位置存放在相应的寄存器中,改变这些寄存器的值到对应进程的代码段、数据段、堆栈就实现了进程的切换。由于内存中的进程数据不会相互干扰,而CPU的寄存器是所有进程共用的,所以进程切换时,需要保存CPU中各个寄存器的值。当切换当前正在运行的A进程时,将A进程CPU寄存器中的值保存到进程的内核栈上,恢复B进程断点时刻CPU寄存器中的值,B进程则可以从断点处继续执行。进程切换造成进程运行变慢主要原因是数据缓存失效,包括L1、L2、L3缓存以及TLB页目录缓存。线程切换受到影响较小的原因是,线程共享地址空间,不会导致缓存失效。但是频繁的进程切换会导致CPU的大部分时间都耗费在恢复进程上下文的过程中,而实际执行进程代码的时间变少。
3、中断/系统调用过程
通常可以将Linux的中断分为异常、硬件中断、系统调用。硬件中断是异步的,也就是外部硬件随时都有可能发出中断,比如按下键盘。处理中断时,CPU会关中断,也就是不允许处理中断时,再响应其他中断,发生中断套嵌。异常和系统调用是同步的,也就是执行某一条指令导致的,比如除零异常、缺页异常;系统调用是通过执行int 0x80指令导致的,对应的中断号为128。处理异常和系统调用不会关中断,也就是CPU在执行异常处理程序或系统调用时,如果产生硬件中断,则会响应中断,执行中断处理程序。
根据中断时,进程所处的特权级别,可以分为两种情况:
1、CPU在用户空间执行用户程序时,检测到中断信号进入内核空间执行系统调用/中断处理
CPU执行用户程序处于用户空间,处于用户态,执行系统调用或处理中断时处于内核空间,处于内核态。在x86系统中,用户态对应特权级ring3,内核态对应特权级ring0。由于从用户空间进入内核空间,发生了特权转换,所以进程调用栈需要从用户栈切换到内核栈,并且需要将用户栈指针ESP和程序计数EIP保存到内核栈,然后跳转到中断处理程序所在的位置。这些操作必须由硬件辅助完成,因为中断是异步产生的,软件无法完成这些操作。试想一下,CPU正常执行指令的时候,完全不知道什么时候会产生中断,也就没有办法通过指令切换调用栈,也没有办法通过指令保存程序计数。
涉及到硬件辅助,必然和硬件的体系结构相关。事实上,x86在设计上,提供硬件切换进程的方式。每个进程有一个任务状态段TSS(task state segment),这是内存中一个的数据结构,用来保存进程的寄存器上下文,包含通用寄存器的值、IO权限位图信息。
另外还有一个特殊的寄存器TR(Task Register),指向某个进程的TSS。修改TR的值将会触发硬件保存CPU所有寄存器的值到当前进程的TSS中,然后从新进程的TSS中读出所有寄存器值,加载到CPU对应的寄存器中,从而实现进程切换。
Linux没有采用x86硬件提供的进程切换方式,而是使用软件编程实现进程切换。但是,因为切换内核栈,保存程序计数是无法软件实现的,所以Linux启动的时候,初始化了一个TSS,而后,所有的进程共用一个TSS。
产生中断时,CPU检测到当前处于用户态ring3,将状态寄存器改为内核态ring0,自动从TSS中取出esp0字段(esp0代表的是特权级为0内核栈指针)赋给ESP,此时ESP指向了内核栈(内核栈位于进程结构体thread_info上方)。然后CPU将用户栈指针(ss、sp)压入内核栈,再依次压栈EFLAGS、CS、EIP、Error Code(异常时错误码)。
硬件压栈的内容和硬件体系结构相关,x86和arm是不同的。硬件压栈以后,CPU跳转到中断描述符表中对应中断号的中断处理程序的入口地址,执行中断处理程序。所有的硬件中断处理程序先压栈中断号,如果是系统调用,则先压栈系统调用号,然后通过执行SAVE_ALL宏定义的汇编代码,压栈用户程序的寄存器值,然后执行具体的系统调用/中断处理。
/* 此处已进入进程内核栈 */
ENTRY(system_call)
pushl %eax // save orig_eax,此处为系统调用号
SAVE_ALL // 保存上下文
GET_THREAD_INFO(%ebp)
// system call tracing in operation
testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax //nr_syscalls表示最大的系统调用号加1。eax存放的是用户传进来的系统调用号。
jae syscall_badsys
//系统调用号确定有效后
syscall_call:
call *sys_call_table(,%eax,4) // 在sys_call_table中查找中断服务程序并执行。
movl %eax,EAX(%esp) // 保存返回值
syscall_exit:
cli /*
make sure we don't miss an interrupt
setting need_resched or sigpending
between sampling and the iret
*/
movl TI_flags(%ebp), %ecx
testw $_TIF_ALLWORK_MASK, %cx // current->work
jne syscall_exit_work
restore_all:
RESTORE_ALL // 回复上下文
// perform work that needs to be done immediately before resumption
2、CPU在内核空间执行系统调用,检测到中断信号在内核空间执行中断处理
在内核空间发生中断,由于特权级别未改变,所以不需要切换调用栈,除此之外,处理中断的过程是一样的。
4、进程切换
发生进程切换的原因有两种:一种是进程主动放弃CPU,直接调用schedule()切换到其他进程。
另一种是进程被抢占。
1、触发抢占的时机
每个进程都包含一个TIF_NEED_RESCHED标志,内核根据这个标志判断该进程是否应该被抢占。此标志位于thread_info结构体中的flag上。此外还有preempt_count抢占计数,表示进程是否可以被抢占。
struct thread_info {
/* low level flags */
unsigned long flags;
/* address limit */
mm_segment_t addr_limit;
/* main task structure */
struct task_struct *task;
/* 抢占计数,表示进程处于内核空间时,是否可以抢占,大于0不可以抢占,等于0可以抢占,小于0是bug */
int preempt_count;
int cpu; /* cpu */
};
直接设置TIF_NEED_RESCHED标志的函数是 set_tsk_need_resched();触发抢占的函数是resched_task()。
周期性的时钟中断
时钟中断处理函数会调用scheduler_tick(),这是调度器核心层(scheduler core)的函数,它通过调度类(scheduling class)的task_tick方法 检查进程是否需要重调度,如果是,则置位TIF_NEED_RESCHED。
唤醒进程
当进程被唤醒的时候,如果优先级高于CPU上的当前进程,就会触发抢占。相应的内核代码中,try_to_wake_up()最终通过check_preempt_curr()检查是否触发抢占。
创建新进程
如果新进程的优先级高于CPU上的当前进程,会触发抢占。相应的调度器核心层代码是sched_fork(),它再通过调度类的 task_fork方法触发抢占:
修改进程nice值
如果进程修改nice值导致优先级高于CPU上的当前进程,也会触发抢占。内核代码参见 set_user_nice()。
负载均衡
在多CPU的系统上,进程调度器尽量使各个CPU之间的负载保持均衡,而负载均衡操作可能会需要触发抢占。
不同的调度类有不同的负载均衡算法,涉及的核心代码也不一样,比如CFS类在load_balance()中触发抢占
2、执行抢占的时机
用户抢占
从中断返回用户空间
检查当前进程是否设置了重调度标志TLF_NEDD_RESCHED, 如果该进程被其他进程设置了TIF_NEED_RESCHED标志, 则函数重新执行进行调度
从系统调用返回用户空间
检查当前进程是否设置了重调度标志TLF_NEDD_RESCHED, 如果该进程被其他进程设置了TIF_NEED_RESCHED标志, 则函数重新执行进行调度
内核抢占
从中断返回内核空间
当从中断返回内核空间时,内核会检preempt_count和need_resched的值(返回用户空间时只需要检查need_resched,检查是否可以抢占的原因时,内核有可能持有自旋锁,如果被抢占,可能会导致死锁),如查preempt_count为0且need_resched设置,则调用schedule(),完成任务抢占。