简析 Linux 的 CPU 时间

从 CPU 时间说起...下面这个是 top 命令的界面,相信大家应该都不陌生 。
top - 19:01:38 up 91 days, 23:06,1 user,load average: 0.00, 0.01, 0.05Tasks: 151 total,1 running, 149 sleeping,1 stopped,0 zombie%Cpu(s):0.0 us,0.1 sy,0.0 ni, 99.8 id,0.0 wa,0.0 hi,0.0 si,0.0 stKiB Mem :8010420 total,5803596 free,341300 used,1865524 buff/cacheKiB Swap:0 total,0 free,0 used.6954384 avail MemPID USERPRNIVIRTRESSHR S%CPU %MEMTIME+ COMMAND13436 root200 1382776280405728 S0.30.4 251:21.06 n9e-collector1 root2004318433842212 S0.00.05:15.64 systemd2 root200000 S0.00.00:00.28 kthreadd3 root200000 S0.00.00:00.58 ksoftirqd/05 root0 -20000 S0.00.00:00.00 kworker/0:0H7 rootrt0000 S0.00.00:35.48 migration/0%Cpu(s): 这一行表示的是 CPU 不同时间的占比,其中大家比较熟悉的应该是 system timeuser time

  • 正常情况下 user time 占比应该最高,这是进程运行应用代码的的时间占比(CPU 密集)
  • system time 占用率高,则意味着存在频繁的系统调用(IO 密集)或者一些潜在的性能问题
不熟悉的朋友可以参考下面这张图(来源于极客时间的课程):
简析 Linux 的 CPU 时间

文章插图
接下来我们将探究隐藏在这些时间背后的操作原理 。
内核态与用户态【简析 Linux 的 CPU 时间】操作系统的核心功能就是管理硬件资源,因此不可避免会使用到一些直接操作硬件的CPU指令,这类指令我们称之为特权指令 。特权指令如果使用不当,将会导致整个系统的崩溃,因此操作系统提供了一组特殊的资源访问代码 —— 内核kernel 来负责执行这些指令 。
操作系统将虚拟地址空间划分为两部分:
  • 内核空间kernel memotry:存放内核代码和数据(进程间共享)
  • 用户空间user memotry:存放用户程序的代码和数据(相互隔离)

简析 Linux 的 CPU 时间

文章插图
通过区分内核空间和用户空间的设计,隔离了操作系统代码与应用程序代码 。即便是单个应用程序出现错误也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行 。
应用程序通过内核提供的接口,访问 CPU、内存、I/O 等硬件资源,我们将该过程称为系统调用system call 。系统调用是操作系统的最小功能单位 。
每个进程处于活动状态时 , 可能处于以下两种状态之一:
  • 执行用户空间的代码时 , 处于用户态
  • 执行内核空间的代码时(系统调用) , 处于内核态
每次执行系统调用时,都需要经历以下变化:
  • CPU 保存用户态指令,切换为内核态
  • 在内核态下访问系统资源
  • CPU 恢复用户态指令 , 切换回用户态
而之前的 user timesystem time 分别就是对应 CPU 在用户态与内核态的运行时间 。
上下文切换当发生以下状况时 , 线程会被挂起,并由系统调度其他线程运行:
  • 等待系统资源分配
  • 调用sleep主动挂起
  • 被优先级更高的线程抢占
  • 发生硬件中断,跳转执行内核的中断服务程序
同个进程下的线程共享进程的用户态空间,因此当同个进程的线程发生切换时,都需要经历以下变化:
  • CPU 保存线程 A 用户态指令,切换为内核态
  • 保存线程 A 私有资源(栈、寄存器...)/li>
  • 加载线程 B 私有资源(栈、寄存器...)
  • CPU 恢复线程 B 用户态指令,切换回用户态
不同线程的用户态空间资源是相互隔离的,当不同进程的线程发生切换时,都需要经历以下变化:
  • CPU 保存线程 A 用户态指令,切换为内核态
  • 保存线程 A 私有资源(栈、寄存器...)
  • 保存线程 A 用户态资源(虚拟内存、全局变量...)
  • 加载线程 B 用户态资源(虚拟内存、全局变量...)
  • 加载线程 B 私有资源(栈、寄存器...)
  • CPU 恢复线程 B 用户态指令,切换回用户态
每次保存和恢复上下文的过程,都是在系统态进行的 , 并且需要几十纳秒到数微秒的 CPU 时间 。当切换次数较多时会耗费大量的 system time , 进而大大缩短了真正运行进程的 user time
当用户线程过多时 , 会引起大量的上下文切换,导致不必要的性能开销 。
线程调度Linux 中的线程是从父进程 fork 出的轻量进程,它们共享父进程的内存空间 。

推荐阅读