• 好好学习,天天向上~
  • 欢迎欢迎~

linux学习15,内核是如何切换进程的

linux笔记 lcc 113℃ 0评论

前面两节讨论了 linux 中进程的睡眠与唤醒机制,也介绍了 linux 内核的 cfs 进程调度方法,知道了哪些进程会被挂起,哪些进程会被投入运行。

不过,我们还不知道 linux 内核在进程调度时,是如何切换进程的的。例如,原本进程 A 正在睡眠,进程 B正在运行,现在要将 A 投入运行,将 B 设置为睡眠状态,这一过程是如何实现的呢?本节将讨论这个问题。

抢占和上下文切换

我们将进程运行时的资源(栈信息、寄存器信息等)称作“上下文”,这么一来,任务抢占就可看作是 linux 内核在切换上下文,上下文切换完毕时,内核也就完成了从一个可执行进程切换到另一个可执行进程。

“上下文”这个名字其实挺贴切的,内核在执行进程时,可以看做是内核在阅读一篇文章,如果有进程需要调度,就相当于内核换了一篇文章读。

那么,linux 内核是如何实现上下文切换的呢?这其实可以从进程调度获得入口,因为上下文切换一定发生在进程调度时,查看 schedule() 函数的 C语言代码:

4141 asmlinkage void __sched schedule(void)
-   4142 {
|   4143     struct task_struct *prev, *next;
|   4144     unsigned long *switch_count;
|   4145     struct rq *rq;
|   4146     int cpu;
|   4147 
|   4148 need_resched:
|   4149     preempt_disable();
|   4150     cpu = smp_processor_id();
|   4151     rq = cpu_rq(cpu);
|   4152     rcu_qsctr_inc(cpu);
|   4153     prev = rq->curr;
|   4154     switch_count = &prev->nivcsw;
|   4155     
|   4156     release_kernel_lock(prev);
    ....
|-  4190     if (likely(prev != next)) {
||  4191         sched_info_switch(prev, next);
||  4192     
||  4193         rq->nr_switches++;
||  4194         rq->curr = next;
||  4195         ++*switch_count;
||  4196         
||  4197         context_switch(rq, prev, next); /* unlocks the rq */
...


发现 context_switch() 函数,显然它就是负责上下文切换的函数。它的 C语言代码如下:

    2447 static inline void
    2448 context_switch(struct rq *rq, struct task_struct *prev,
    2449            struct task_struct *next)
-   2450 {
|   2451     struct mm_struct *mm, *oldmm;
|   2452 
|   2453     prepare_task_switch(rq, prev, next);
|   2454     mm = next->mm;
|   2455     oldmm = prev->active_mm;
...
|-  2463     if (unlikely(!mm)) {
||  2464         next->active_mm = oldmm;
||  2465         atomic_inc(&oldmm->mm_count);
||  2466         enter_lazy_tlb(oldmm, next);
||  2467     } else
|   2468         switch_mm(oldmm, mm, next);
...
|   2484     /* Here we just switch the register state and the stack. */
|   2485     switch_to(prev, next, prev);
...
|   2493     finish_task_switch(this_rq(), prev);
|   2494 }


容易看出,核心就是 switch_mm() 函数和 switch_to() 函数。switch_mm() 函数的 C语言代码如下,请看:

     35 static inline void switch_mm(struct mm_struct *prev,
     36                  struct mm_struct *next,
     37                  struct task_struct *tsk)
-    38 {
|    39     int cpu = smp_processor_id();
|    40 
|-   41     if (likely(prev != next)) {
...
||   51         load_cr3(next->pgd);
||   56         if (unlikely(prev->context.ldt != next->context.ldt))
||   57             load_LDT_nolock(&next->context);
||   58     }
...
|    73 }


switch_mm() 函数切换了页表,它的主要作用就是把虚拟内存(前面的文章曾经介绍过,linux 中的进程都是运行在虚拟系统中的)从上一个进程映射到新进程中。switch_to() 函数负责切换新进程的栈和寄存器,因为涉及到寄存器的操作,C语言无法方便的完成,所以 linux 内核是使用汇编代码(而且比C语言代码效率更高)实现该函数的,请看:

从汇编代码也能看出,switch_to() 函数在切换新进程之前,将上一个进程的信息都压栈保存了,所以以后再切换回来的时候,进程能够接着上一次的状态继续运行。

进一步调度

现在已经清楚 linux 内核是如何切换进程的了。之前我们说过 linux 的进程是有优先级的概念的,高优先级的进程总是优先运行。假设某次调度后,进程 A 即将被投入运行,但是这时优先级比进程 A 更高的进程 B 也处于可运行状态了,linux 内核如何处理这种情况呢?

事实上,内核提供了 need_resched 标志位来表明是否需要重新执行一次调度。

该标志位可以通过以下三个C语言 inline 函数修改和查询:

进一步跟踪,发现 need_resched 标志位其实记录在进程的 thread_info 结构体的 flag 成员里,该结构体之前有文章专门介绍过。

现在就清楚了,当优先级较高的进程进入可执行状态的时候,linux 内核会设置 need_resched 标志位。而内核在进程调度后返回用户空间时,会检查 need_resched 标志,如果已被设置,则内核会重新执行一次调度。

那么,linux 内核什么时候重新调度才是安全的呢?只要进程没有持有锁,内核就可以抢占它。所以每个进程的 thread_info 结构体有一个 preempt_count 计数器,它的初始值为 0,每使用一次锁就加 1,释放一次锁就减 1,当该值为 0 的时候,linux 内核就能够抢占它。

linux 的实时调度策略

再啰嗦一下 linux 的两种实时调度策略:SCHED_FIFO 和 SCHED_RR。不特殊指定调度策略的进程一般都是 SCHED_NORMAL 策略。

SCHED_FIFO 调度策略不使用时间片,它使用先进先出的调度算法,处于可运行状态的 SCHED_FIFO 进程会比任何 SCHED_NORMAL 几的进程都先得到调度。一旦一个 SCHED_FIFO 级的进程处于可执行状态,就会一直运行,除非执行完毕或者它自己主动让出 cpu,否则就只有优先级更高的 SCHED_FIFO 和 SCHED_RR 级进程才能抢占它。

SCHED_RR 调度策略与 SCHED_FIFO 调度策略总体相同,只不过 SCHED_RR 调度策略也使用时间片,SCHED_RR级进程消耗完自己的时间片时,由同优先级的其他实时进程抢占。

SCHED_FIFO 和 SCHED_RR 调度策略,高优先级的进程总是立刻抢占低优先级的进程。低优先级进程不会抢占 SCHED_RR 进程,即使它的时间片已经使用完毕。


linux学习15,内核是如何切换进程的 - 刘冲的博客(https://blog.popkx.com) 原创版权所有,转载请注明出处。
点赞 (0)分享 (0)
发表我的评论
取消评论
表情     4 + 6 = ? (必填)