简析 Linux 的 CPU 时间( 二 )


  • CPU 保存线程 A 用户态指令,切换为内核态
  • 保存线程 A 私有资源(栈、寄存器...)
  • 保存线程 A 用户态资源(虚拟内存、全局变量...)
  • 加载线程 B 用户态资源(虚拟内存、全局变量...)
  • 加载线程 B 私有资源(栈、寄存器...)
  • CPU 恢复线程 B 用户态指令,切换回用户态
每次保存和恢复上下文的过程,都是在系统态进行的,并且需要几十纳秒到数微秒的 CPU 时间 。当切换次数较多时会耗费大量的 system time,进而大大缩短了真正运行进程的 user time
当用户线程过多时,会引起大量的上下文切换,导致不必要的性能开销 。
线程调度Linux 中的线程是从父进程 fork 出的轻量进程,它们共享父进程的内存空间 。
Linux 的调度策略是抢占式的,每个线程都有优先级prirority的概念,并按照优先级高低分为两种:
  • 实时进程(优先级 0~99)
  • 普通进程(优先级 100~139)
每个 CPU 都有自己的运行队列 runqueue,需要运行的线程会被加入到这个队列中 。
简析 Linux 的 CPU 时间

文章插图
每个队列可以进一步细分为 3 个队列以及 5 种调度策略:
  • dl_rq
    • SCHED_DEADLINE 选择 deadline 距离当前时间点最近的任务执行
  • rt_rq —— 可以互相抢占的实时任务
    • SCHED_FIFO 一旦抢占到 CPU 资源,就会一直运行直到退出,除非被高优先级抢占
    • SCHED_RR 当 CPU 时间片用完,内核会把它放到队列末尾,可以被高优先级抢占
  • cfs_rq —— 公平占用 CPU 时间的普通任务
    • SCHED_NORMAL 普通进程
    • SCHED_BATCH 后台进程
Linux 内核在选择下一个任务执行时,会按照该顺序来进行选择,也就是先从 dl_rq 里选择任务,然后从 rt_rq 里选择任务,最后从 cfs_rq 里选择任务 。所以实时任务总是会比普通任务先得到执行 。
实时进程的优先级总是高于普通进程,因此当系统中有实时进程运行时,普通进程几乎是无法分到时间片的 。
nice 值为了保证 cfs_rq 队列的公平性,Linux 采用完全公平调度算法 CFS Completely Fair Scheduler进行调度,保证每个普通进程都尽可能被调度到 。
CFS 引入了 vruntime 作为衡量是否公平的依据:
  • vruntime 与任务占用的 CPU 时间成正比
  • vruntime 与任务优先级成反比(优先级越高vruntime增长越慢)
如果一个任务的 vruntime 较小,说明它以前占用 CPU 的时间较短,受到了不公平对待,因此该进程会被优先调度,从而到达所谓的公平性 。
为了实现可控的调度,Linux 为普通进程引入了 nice 值的概念 。其的取值其范围是 -20 ~ +19,调整该值会改变进程的优先级:prirority += nice
与此同时 vruntime 计算也会受到影响:
进程的 nice 值越小, 优先级越高, 所能分到的运行时间也越多当用户进程设置了一个大于 0 的 nice 值时,其用户态的运行时间将被统计为nice time 而不是 user time 。简单来说,nice time 表示 CPU 花了多少时间用于运行低优先级的任务 。
nice time 占比比较高时,通常是某些定时任务调度器导致的:它们会为后台任务进程设置一个较大的 nice 值,避免这些进程与其他线程争抢 CPU 资源 。

经验总结扩展阅读