CPU切换主要有两种方式, 一种是主动释放CPU, 另外一种就是其他进程抢占CPU。
主动调度
在操作外部设备的时候,一般需要让出CPU。
主动调度,在linux源码内部调用的schedule函数来主动让出cpu
1 |
|
主要调动逻辑在__schedule函数,步骤:
- 取出当前cpu的任务队列rq,rq里面有不同的任务队列 cfs, rt, dl等。
- 获取下一个执行任务
- 如果下一个执行任务和当前任务不一致,进行上下文切换
上下文切换
上下文切换主要干两件事情,一是切换进程空间,也即虚拟内存;二是切换寄存器和 CPU 上下文。
寄存器和栈的切换,它调用到了 __switch_to_asm。这是一段汇编代码,主要用于栈的切换。
对于 32 位操作系统来讲,切换的是栈顶指针 esp。
对于 64 位操作系统来讲,切换的是栈顶指针 rsp。
在 x86 体系结构中,提供了一种以硬件的方式进行进程切换的模式,对于每个进程,x86 希望在内存里面维护一个 TSS(Task State Segment,任务状态段)结构。这里面有所有的寄存器。
另外,还有一个特殊的寄存器 TR(Task Register,任务寄存器),指向某个进程的 TSS。更改 TR 的值,将会触发硬件保存 CPU 所有寄存器的值到当前进程的 TSS 中,然后从新进程的 TSS 中读出所有寄存器值,加载到 CPU 对应的寄存器中。
所谓的进程切换,就是将某个进程的 thread_struct 里面的寄存器的值,写入到 CPU 的 TR 指向的 tss_struct,对于 CPU 来讲,这就算是完成了切换。
指令指针的保存与恢复
实际在切换的过程中,指令指针寄存器的值没有改变,只是其他寄存器切成了新进程的值,这样指令指针寄存器运行时,用的全部都是新进程相关的值,也就理所当然的成为新进程的指令指针寄存器。挺绕的,实际就是人还是同一个人,在不同环境下他的地位不一样了,是父亲,是儿子,是丈夫。。。
抢占式调度
常见的现象就是一个进程执行时间太长了,是时候切换到另一个进程了。在计算机里面有一个时钟,会过一段时间触发一次时钟中断,通知操作系统,时间又过去一个时钟周期,这是个很好的方式,可以查看是否是需要抢占的时间点。
时钟中断处理函数会调用 scheduler_tick()。函数先取出当前 CPU 的运行队列,然后得到这个队列上当前正在运行中的进程的 task_struct,然后调用这个 task_struct 的调度类的 task_tick 函数。先是调用 sched_slice 函数计算出的 ideal_runtime。判断在这个时间周期内,当前进程占用的时间是否超过了idealtime,超过就需要被抢占了。这个条件之外,还会通过 __pick_first_entity 取出红黑树中最小的进程。如果当前进程的 vruntime 大于红黑树中最小的进程的 vruntime,且差值大于 ideal_runtime,也应该被抢占了。这里只是给任务置了一个标志。
另外一种现象是当一个进程被唤醒的时候。当 I/O 到来的时候,进程往往会被唤醒。这个时候是一个时机。当被唤醒的进程优先级高于 CPU 上的当前进程,就会触发抢占。这里也只是给当前运行的任务置了一个标志。
抢占的时机
真正的抢占还需要时机,也就是需要那么一个时刻,让正在运行中的进程有机会调用一下 __schedule。
不可能某个进程代码运行着,突然要去调用 __schedule,代码里面不可能这么写,所以一定要规划几个时机,这个时机分为用户态和内核态。
用户态的抢占时机
主要是在系统调用返回和中断处理函数返回的时候,如果有标志位,则进行调用 schedule函数进行调度。可以查看函数exit_to_usermode_loop的实现。
内核态的抢占时机
对内核态的执行中,被抢占的时机一般发生在 preempt_enable() 中。在内核态的执行中,有的操作是不能被中断的,所以在进行这些操作之前,总是先调用 preempt_disable() 关闭抢占,当再次打开的时候,就是一次内核态代码被抢占的机会。
同样的,内核态处理中断返回时,也可以触发检查抢占,进行schedule调度。
实际工作中,也用了许多这种设置标记的情况。比如,相同配置连续下发的时候,处理耗时又比较长,每个处理都是不可被中断的。那么,就只能保存最新的那条配置,然后设置标记为需要更新。当配置更新完成后,检查标志位,如果需要更新,再启动新一轮的处理流程。
行动,才不会被动!
欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。