在Linux 中测量延迟

原文地址:http://btorpey.github.io/blog/2014/02/18/clock-sources-in-linux/

他山之石,可以攻玉,该文章将详细地介绍了在Linux 系统中使用TSC 时钟精确地计算一段代码过程(中断和函数等)执行时间需要注意的内容,可以配置Intel 官方文档《How to Benchmark Code Execution Times on Intel® IA-32 and IA-64 Instruction Set Architectures》一起阅读。下面是译文。

为了在现代(操作)系统中的测量(一段过程的)延迟,我们需要(测量时间间隔的时钟)至少能够以微秒为单位,最好以纳秒为单位。好消息是,使用相对现代的硬件和软件,可以准确地测量小到几个纳秒的时间间隔。

但是,为确保您的(测量)结果是准确的,更重要的是要了解您要测量什么,以及不同情况下测量过程的边界是什么。

概要

为了(在Linux 中测量延迟)获得最佳效果,您应该使用:

  • (使用)Linux 内核 2.6.18 或更高版本 —— 这是第一个包含 hrtimers (高精度时钟)包的版本。最好的是 2.6.32 或更高版本,包括了对大多数不同时钟源的支持。
  • 具有恒定不变的 TSC(constant, invariant TSC (time-stamp counter))的 CPU。这意味着 TSC 在所有插槽和CPU核心上都以恒定速率运行,而不管电源管理(代码)对 CPU 的频率进行如何更改。如果 CPU 支持 RDTSCP 指令那就更好了(RDTSCP 会使得读取的时间更准确和稳定)。
  • TSC 应配置为Linux 内核的时钟源。
  • 您应该测量发生在同一台机器上的两个事件之间的间隔(机器内计时,即intra-machine timing)。
  • 对于机器内计时,您最好的选择使用汇编语言直接读取 TSC。在我的测试机器上,软件读取 TSC 大约需要 100ns,这是该方法准确性的(边界)限制(要测量100ns 以内的时间间隔是不实际的)。但不同机器读取TSC 开销不尽相同,这就是为什么我提供了源代码,您可以用来进行测量自己的机器。
    • 请注意,上面提到的100ns 主要是因为我的Linux 机器不支持RDTSCP 指令。所以为了获取合理准确的计时,在RDTSC 之前还执行了CPUID 指令以序列化指令执行过程。而在另一台支持RDTSCP 指令的机器(新 MacBook Air)上,开销下降了大约 14ns。

下面将讨论时钟在Linux 上的工作原理,如何从软件(角度)访问各种时钟,以及如何测量访问它们的开销。

机器内与机器间计时

在深入讨论上面(概要)建议的细节之前,首先谈谈机器内(Intra-machine)与机器间(Inter-machine)时间测量的不同。机内计时是最简单的情况,因为保证使用相同的时钟源计时一般来说非常容易。

机器间计时的问题在于,您面临着(至少)两个不同系统的时钟源。(当然,除非您是在一个机器上测量一个Ping-Pong 来回过程的间隔)。有个俗语有趣地描述了拥有两个时钟源的问题:

一个戴表的人知道现在几点了。一个有两块手表的人永远不会确定。

——西格尔定律

对于机器间计时,CLOCK_REALTIME 时钟源(gettimeofday() 的来源)几乎毫无用处,而您真正需要的是一个在两台(或更多台)机器之间同步的时钟。这样,计时准确性显然取决于您的同步时钟,并在一般情况下,(同步时钟)很难获取比微秒更精确的测量。

我们不会在本文中更多地讨论机器间计时。

Linux 如何计时

抛开(机器间计时),让我们来看看 Linux 是如何维持时间的。系统启动后, Linux 从 RTC(Real Time Clock)获取当前时间时。RTC是一个由主板纽扣电池供电的硬件时钟,因此即使机器断电,它也能继续运行。在大多数情况下,RTC 不是特别准确,因为它是由廉价的晶体振荡器驱动的,其频率会根据温度和其他环境因素变化。 Linux 系统从RTC 查询得到的启动时间,并存储在内核内存中,稍后将启动时间用作偏移量,合并TSC 保留的滴答计数推导出(当前的)挂钟时间(Wall-clock time)。

系统启动的同时, TSC(Time-Stamp Counter 时间戳计数器)开始运行。TSC 是一个寄存器计数器,它由生成CPU 的时钟脉冲的晶体振荡器驱动,所以它和 CPU 的频率相同,例如 2GHz (CPU的 TSC)时钟每纳秒会滴答两次。

(系统也)还有许多其他时钟源,我们将在后面讨论。但在大多数情况下,TSC 是系统首选时钟源,原因有两个:它非常准确,且查询其值的开销非常小(因为对于代码来说TSC 只是一个寄存器)。但是使用TSC 作为时序源时,需要牢记几点:

  • 在较老旧的CPU 中,每个CPU 核心都有自己(独立运行)的 TSC,因此为了确保两次测量都相对彼此准确,有必要将测量代码固定到某一个CPU 核心(防止代码所在的任务被调度至其他CPU 核心)。
  • 同样在较老旧的CPU 中,TSC 将以CPU 本身的频率运行,如果该频率发生变化(例如,如果频率动态降低,或者 CPU 完全停止以进行电源管理),则该CPU 上的TSC 也会变慢下降或停止。(有时可以通过在BIOS 中禁用电源管理来解决此问题,那么所有CPU 始终以100% 频率运行,不升高也不降低)。

这两个问题在最近的(大约2010 年后) CPU 中都得到了解决:不管 CPU 频率如何变化,恒定的TSC (constant TSC)使系统中所有CPU 核心上TSC 保持同步,而不变(或不间断)的TSC (nonstop_tsc)使TSC 保持在固定速率下运行。想要看看您的CPU 是否支持以上一种或两种(特性),请执行以下操作并检查标志中输出的值:

$ cat /proc/cpuinfo | grep -i tsc
flags:... tsc rdtscp constant_tsc nonstop_tsc ...

这些(关于TSC 时钟的)标志具有以下含义:

tsc系统具有TSC 时钟。
rdtscpRDTSCP 指令可用。
constant_tscTSC 在所有CPU槽和核心之间同步。
nonstop_tscTSC 不受电源管理的影响。

其他时钟源

TSC 通常是首选时钟源,其精度高,(读取)开销相对较低。(但Linux 系统也)还可以使用其他时钟源:

  • HPET(High Precision Event Timer,高精度事件时钟)是微软和英特尔在2005年左右推出的,其精度约为100 ns,不如TSC(提供亚纳秒级精度)。查询 HPET 的成本比TSC 也高得多。
  • 由于acpi_pm 运行(频率)在 3.58MHz(每 279 ns 一个滴答),它优点是频率不会根据系统电源管理改变,acpi_pm 也没有前面的定时器那么准确。
  • jiffies 与用于调度(操作系统上进程)定时器采用相同的时钟源,因此其分辨率通常很差(大多数 Linux 发行版中的默认调度间隔是 1 ms 或 10 ms)。

查看系统上可用的时钟源:

$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm

并查看正在使用哪个:

$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc

通常,内核在引导时自动设置时钟源,但您可以通过在Linux 内核启动命令行上添加参数(例如,在 /boot/grub/grub.conf 或 /etc/default/grub 中)来强制使用特定时钟源:

ro root=/dev/... clocksource=tsc 

您还可以在系统运行时更改时钟源,例如强制使用 HPET:

$ echo hpet > /sys/devices/system/clocksource/clocksource0/current_clocksource

上面所述时钟虽然都被我们平时称为硬件时钟,但严格来说它们是一种硬件和软件的混合体。在底层有某种硬件设备可以生成周期性的定时脉冲,然后对其进行计数以创建时钟。有一些(例如TSC)计数是在硬件中完成的,也有一些(例如jiffies)计数是在软件中完成的。

壁钟时间

刚刚讨论的硬件(或硬件/软件混合)时钟都有一个共同点:它们只是计数器,与我们大多数人认为的时间(通常称为壁钟时间,Wall-clock time)没有直接关联。

如果壁钟时间要相当准确,那么需要对这些计数器进行相当复杂的软件计算。当然,采用何种合理且准确的方法取决于所需壁钟时间的精准程度。

同步多个分布式时钟的过程极其复杂,这里不做赘述。有许多不同的机制来同步分布式时钟,从相对简单的(例如网络时间协议,Network Time Protocal,NTP) 到不太简单的(例如精准时间协议,Precise Time Protocal,PTP),直至专门的专有解决方案。

将一个系统的(壁钟)时间与其他系统的时间保持同步的关键是,需用一种手段来调整当前时钟以使其与其对等系统保持同步。有两种方法:

  • 对系统时间进行(一次或多次)不连续修改(该过程也称作Stepping)。这可能会导致壁钟时间出现大幅跳跃,包括向后跳跃,尽管时间调整软件通常可以限制单次(时间)跳跃的大小。一个常见的例子就是系统在启动时从 NTP 服务器初始化其本身的系统时钟。
  • 修改驱动硬件计数器(如 TSC)的振荡器的频率(或倍频器)(该过程也称作Slewing)。这会导致时钟运行得相对更快或更慢,但(时间)不会跳跃,也不能倒退。

(Linux 中)可用时钟源

在Linux 中获取时间的最常见方法是调用gettimeofday() 系统调用,它以微秒精度(尽管不一定是微秒的准度)返回当前壁钟时间。由于gettimeofday() 调用 clock_gettime(CLOCK_REALTIME, **),因此以下讨论也适用于它。

Linux 还实现了POSIX clock_gettime() 系列函数,可让您查询不同的时钟源,包括:

CLOCK_REALTIME壁钟时间。该时间可能被时间同步协议(例如NTP 和PTP)而发生跳跃。
CLOCK_REALTIME_COARSECLOCK_REALTIME 的低分辨率版本。
CLOCK_REALTIME_HRCLOCK_REALTIME 的高分辨率版本,仅存在于实时内核。
CLOCK_MONOTONIC单调时钟。代表时间的计数器频率可能发生变化,但时间不会跳跃,即只会前移,不能后退。
CLOCK_MONOTONIC_COARSECLOCK_MONOTONIC 的低分辨率版本。
CLOCK_MONOTONIC_RAWCLOCK_MONOTONIC 的一个版本,时间的计数器频率不会变化,时间也不会跳跃 。
CLOCK_BOOTTIMECLOCK_MONOTONIC 的一个版本,它反应了自机器启动后至当前的时间。仅在较新的内核(2.6.39+)中可用。

以上可用时钟的分辨率和准确性取决于硬件以及特定的 Linux 实现。(我们实现了)一个小测试程序 (clocks.c),作为本文随附源代码的一部分,将打印有关系统上时钟的相关信息。在我的测试机上的运行结果如下:

clocks.c
                    clock	       res (ns)	           secs	          nsecs
             gettimeofday	          1,000	  1,391,886,268	    904,379,000
           CLOCK_REALTIME	              1	  1,391,886,268	    904,393,224
    CLOCK_REALTIME_COARSE	        999,848	  1,391,886,268	    903,142,905
          CLOCK_MONOTONIC	              1	        136,612	    254,536,227
      CLOCK_MONOTONIC_RAW	    870,001,632	        136,612	    381,306,122
   CLOCK_MONOTONIC_COARSE	        999,848	        136,612	    253,271,977

请注意函数clock_getres() 的返回结果,一个特定的时钟源可能返回比其clock_getres() 给出精度更高的结果(比如COARSE 时钟精度只有毫秒,但其结果可能包含微秒和微秒的信息),但超过该精度的数值可能都是没用的(唯一的例外是函数gettimeofday(),因为它返回一个以微秒为单位的时间值,低位数字全为零 )。

此外,从clock_getres() 为CLOCK_MONOTONIC_RAW 返回的值显然是不准确的,我在好几台机器上都看到过类似的结果。

最后请注意,为CLOCK_REALTIME 列出的分辨率接近但不完全是百万分之一秒——这是晶体振荡器无法产生恰好1000 Hz 频率的这一事实的产物——实际上是 1000.15 Hz。

从软件中获取时钟值

接下来简要讨论如何从软件(和程序)的角度,读取这些不同的时钟值。

汇编器

在汇编语言中,执行RDTSC 指令后,TSC 的值作为结果保存在在edx 和eax 寄存器中。但是由于现代CPU 支持乱序执行,一般要在 RDTSC 指令之前插入序列化指令(例如CPUID),以确保 RDTSC 指令的执行不会被处理器重新排序。

较新的CPU 包含了RDTSCP 指令,它将进行任何必要的序列化,从而避免了CPUID 指令的开销(CPUID 的开销可能相当大,且可变)。如果您的CPU 支持RDTSCP,请使用它而不是CPUID 和RDTSC 组合。

C/C++

显然,RDTSC 指令也可以直接从C 或C++ 调用,可以使用编译器支持的汇编语言方法来执行RDTSC,也可以调用C/C++封装后的接口(可以在Agner Fog 的优秀网站上找到一个示例)。

调用 gettimeofday() 或 clock_gettime() 方法也非常简单直接——请参阅随附的clocks.c 源文件以获取示例。

Java

Java 只有两种相关获取时间的方法:

  • System.currentTimeMillis() 返回自公元纪元以来至当前挂钟时间的毫秒数。它调用 gettimeofday(),后者又调用 clock_gettime(CLOCK_REALTIME, …)。
  • System.nanoTime 返回自某个未指定起点至今以来的纳秒数。它根据系统能力,要么调用 gettimeofday(),要么调用 clock_gettime(CLOCK_MONOTONIC, )。

但是如果您在Java 中需要上述时钟以外的时钟,则需要你自己动手实现,比如通过JNI 调用C。但好消息是,这样做并不比访问nanoTime 开销大多少(至少在我的测试中是这样)。

时钟查询的开销

海森堡测不准原理,简而言之就是,观察现象的行为会改变现象本身。为测量延迟而获取系统时间戳也存在类似问题,因为读取任何时钟源都需要有限的(有时是可变的)时间。换句话说,2GHz 机器上的TSC 每纳秒滴答两次,并不意味着我们可以测量出一纳秒的间隔——因为我们还需要考虑软件读取TSC 所需的时间。

那么,执行这些不同的时钟查询的成本有多大?您可以使用C/C++ 和Java(使用 JNI 调用 C 代码)测试查询各种时钟源所需的时间,或者参考一些示例代码,。

C++ 和Java 版本都采用相同的方法:在连续的循环中调用特定的(获取)时钟函数,并存储结果。循环的次数应该足够多,确保代码指令和数据在处理器中的都能够缓存命中,以保证结果的稳定和准确。

在我的机器上运行测试的结果如下(所有结果都以纳秒为单位):

ClockBench.cpp
                   Method       samples     min     max     avg  median   stdev
           CLOCK_REALTIME       255       54.00   58.00   55.65   56.00    1.55
    CLOCK_REALTIME_COARSE       255        0.00    0.00    0.00    0.00    0.00
          CLOCK_MONOTONIC       255       54.00   58.00   56.20   56.00    1.46
      CLOCK_MONOTONIC_RAW       255      650.00 1029.00  690.35  839.50   47.34
   CLOCK_MONOTONIC_COARSE       255        0.00    0.00    0.00    0.00    0.00
              cpuid+rdtsc       255       93.00   94.00   93.23   93.50    0.42
                    rdtsc       255       24.00   28.00   25.19   26.00    1.50
Using CPU frequency = 2.660000

ClockBench.java
                   Method       samples     min     max     avg  median   stdev
          System.nanoTime       255       54.00   60.00   55.31   57.00    1.55
           CLOCK_REALTIME       255       60.00   84.00   62.50   72.00    1.92
              cpuid+rdtsc       255      108.00  112.00  109.03  110.00    1.39
                    rdtsc       255       39.00   43.00   39.85   41.00    1.37
Using CPU frequency = 2.660000

关于这些结果有几点需要注意:

  • 获取两个COARSE 时钟的延迟都显示为零,这告诉我们获取该类时钟值所需的时间小于时钟的分辨率。(我们之前的测试显示 COARSE 时钟的分辨率为 1ms)。
  • 出于某种原因,CLOCK_MONOTONIC_RAW 时钟的查询成本非常高。我也无法解释这一点——你可能会认为它不会往前或往后调整时间的特性会使其获取时间更快,而不是更慢。要不是它查询开销太大,它(优良的特性)将(使得它)成为机器内计时的最佳选择。
  • CPUID 和RDTSC 的组合比RDTSCP 慢,后者比单独的RDTSC 慢。这说明一般情况下RDTSCP 只要是可用,就应该是首选,如果没有(RDTSCP),则回退到 CPUID+RDTSC。(虽然RDTSC 本身是最快的,但由于乱序执行它可能不准确,也就意味着它只对操作时间相对较长的场景合适)。
  • Java 版本比C++ 版本稍慢,大概率是JNI 的开销。

结论

我原以为这是一个简短且琐碎的研究,而结果表明, 它比我预期的要复杂得多(相关文档记录也不多)。

总之,我希望以上总结是有帮助的。

(如有问题)请随时直接与我联系,提出意见、建议、更正等。

其他资源

以下是我在研究本文时参考的内容。

http://elinux.org/Kernel_Timer_Systems

http://elinux.org/High_Resolution_Timers

http://juliusdavies.ca/posix_clocks/clock_realtime_linux_faq.html

http://en.wikipedia.org/wiki/Time_Stamp_Counter

http://stackoverflow.com/questions/10921210/cpu-tsc-fetch-operation-special-in-multicore-multi-processor-environment

http://www.citihub.com/requesting-timestamp-in-applications/

http://www.intel.com/content/www/us/en/intelligent-systems/embedded-systems-training/ia-32-ia-64-benchmark-code-execution-paper.html

利用autossh和中间主机为内网主机建立稳定ssh 连接

通常会遇到一些内网主机没有独立IP 地址,隐藏在NAT 之后,用户无法直接建立ssh 连接。

这时候就需要一个中间人机器(具有独立IP)做为跳板,内网机器反向连接至中间机器。用户登陆时,首先连接至中间机器,再反向连接至内网主机。

其步骤如下:

  1. 在内网主机,运行 ssh -R 7777:localhost:22 [email protected]
  2. 在中间主机,运行 ssh -p 7777 [email protected]

注意:步骤2的user 是内网主机user。

ssh -R 参数中7777 是远端映射的端口,连接该端口将建立起和内网22 号端口的链接;下面是man ssh 中关于-R 选项的说明

-R [bind_address:]port:host:hostport

-R [bind_address:]port:local_socket

-R remote_socket:host:hostport

-R remote_socket:local_socket

-R [bind_address:]port

Specifies that connections to the given TCP port or Unix socket on the remote (server) host are to be forwarded to the local side.

This works by allocating a socket to listen to either a TCP port or to a Unix socket on the remote side. Whenever a connection is made to this port or Unix socket, the connection is forwarded over the secure channel, and a connection is made from the local machine to either an explicit destination specified by host port hostport, or local_socket, or, if no explicit destination was specified, ssh will act as a SOCKS 4/5 proxy and forward connections to the destinations requested by the remote SOCKS client. Port forwardings can also be specified in the configuration file. Privileged ports can be forwarded only when logging in as root on the remote machine. IPv6 ad‐ dresses can be specified by enclosing the address in square brackets.

By default, TCP listening sockets on the server will be bound to the loopback interface only. This may be overridden by specifying a bind_address. An empty bind_address, or the address ‘*’, indicates that the remote socket should listen on all interfaces. Specifying a remote bind_address will only succeed if the server's GatewayPorts option is enabled (see sshd_config(5)).

If the port argument is ‘0’, the listen port will be dynamically allocated on the server and reported to the client at run time. When used together with -O forward the allocated port will be printed to the standard output.

但这样存在两个问题:1)ssh 连接超过固定时间会自动释放;2)每次连接中间机器都需要用户手动输入密码。

第一个问题通过autossh 解决

autossh 通过将ssh 命令包裹至一个循环中,并在ssh 命令断开时自动建立连接,这样就保证了即使内网机器无法访问,也会自动建立和中间主机的逆向连接。autossh 命令格式如下

autossh [autossh options] [ssh options]

即autossh 除了自身参数,其他参数直接用ssh 的即可。

第二个问题通过公钥免密码登录解决:1)内网主机执行ssh-keygen;2)ssh-copy-id -i ~/.ssh/id_rsa.pub [email protected]_machine

结合起autossh 和免密码登录,autossh 命令如下:

autossh -o "PasswordAuthentication=no" -o "PubkeyAuthentication=yes" -i ~/.ssh/id_rsa -R 7777:localhost:22 [email protected]

将该命令添加至开机启动模块中实现开机启动。

Futex 简述

简介:futex 全称为Fast User-space Mutex,是Linux 2.5.7 内核引入的锁原语,不同于其他进程间通信IPC原语(如信号量Semaphore、信号Signal和各种锁pthread_mutex_lock),futex更轻量级、快速,一般应用开发人员可能很少用到,但可基于futex实现各类读写锁、屏障(barriers)和信号机制等。

相关背景

在Linux的早期版本(内核Linux 2.5.7 版本以前),进程间通信(Inter-Process Communication,IPC)沿用的是传统Unix系统和System V 的IPC,如信号量(Semaphores)和Socket 等,这些IPC 均基于系统调用(System Call)。这类方法的缺点是当系统竞争度较低时,每次都进行系统调用,会造成较大系统开销。

原理和做法

用户程序每次调用IPC机制都会产生系统调用,程序发生用户态和内核态的切换,futex 的基本思想是竞争态总是很少发生的,只有在竞争态才需要进入内核,否则在用户态即可完成。futex的两个目标是:1)尽量避免系统调用;2)避免不必要的上下文切换(导致的TLB失效等)。

具体而言,任务获取一个futex 将发起带锁的减指令,并验证数值结果值是否为0(加上了锁),如果成功则可继续执行程序,失败(为已经占用的锁继续加锁)则任务在内核被阻塞。为相同futex 变量的加锁的任务被阻塞后放在同一个队列,解锁任务通过减少变量(只有一个加锁且锁队列为空)或进入内核从锁队列唤醒任务。

注意:futex 在Linux 的内核实现为一个系统调用(SYS_futex),用户程序如果直接调用它肯定会进入内核态,它还需要和其他语句(如原子操作)配合使用,新手在未理解其futex 原理和并发控制机制时极易犯错,这也是为什么不推荐直接使用它的原因。

继续阅读

记一次有趣的Bug – 返回值被截断为32位

上周修改了hdrt 库文件中头文件的include 关系,结果出现了一个有趣的bug。具体表现为:当函数foo() 调用某个函数fun_called(),返回的值ret_val 总是从64位被截断为32位。

首先,在foo() 函数头和func_called 函数尾输出ret_val,确认是返回值是在调用过程中被截断了。

其次,在gdb 中查看生成的代码,发现ret_val所存储的寄存器rax (返回值一般存储在rax寄存器中)在被返回时,被cltq 指令截断了高32 位。

  0x555555565adb <foo+42> callq 0x555555569874<fun_called>
  0x555555565ae0 <foo+47> cltq
  0x555555565ae2 <foo+49> mov %rax,-0x8(%rbp)
  0x555555565ae6 <foo+53> cmpq $0x0,-0x8(%rbp)
  0x555555565aeb <foo+58> jne 0x555555565b2e <foo+125>

最后,查了相关资料https://stackoverflow.com/a/26209434/1424948 可能原因是:

1)函数声明(prototype)中没有fun_called 的声明;

2)没有引用包含fun_called 声明的头文件。

默认情况下,调用函数foo 在不知道被调用函数fun_called 的返回值类型的情况下,会按照int 类型大小的值处理,即32位。

对于这个bug 其实有两个建议:

1 非特殊情况不要用强制类型转换(即type cast),(char *)这类转换会屏蔽很多暴露问题的warning;

2 要查看/消除warning,例如这个bug 实际隐藏在了warning 中,因为返回值被编译当做默认的int 类型返回时,获取返回值变量不是int 时则告警了类型不匹配。

利用/proc精确计算Linux系统的CPU利用率

Linux 系统并未提供直接获取CPU 利用率的接口,一些应用程序通过访问/proc 文件系统中系统的状态统计从而计算得到CPU利用率。常用查看进程及其相关信息的top 命令和htop 命令即属于这一类。

本文简单介绍如何利用/proc/stat 文件计算CPU 利用率,这种经典方法在top 和htop 工具中也被采用,在StackOverflow 也有说明;接着,将上面经典方法获取的CPU 利用率和一种极端情况下的单核单任务的CPU占用率进行比较,单核单任务指的是,在隔离的单独CPU 和核心上只运行该程序(如果你对这有疑问,可以参考下core affinity 、core isolation 和non irqbalance)。

看下图htop 工具的截图:该系统有8个CPU核心,每个核心的CPU利用率位于图上方横向柱状图。8个核心中1-7都是被隔离的,1/3/4/5/6/7每个核心都分配了一个名称为grt_simple_conc_ispc 的线程,每个线程CPU 占用率(CPU%)位于图下方第5 列,可以看出CPU利用率(上方)和每个程序占CPU利用率(下方)结果是有差异的。

方法一

一种最直接计算程序占用CPU 使用率的计算方法是,采样N 次,如果有R 次该程序正在执行,则其占用CPU使用率为 R/N × 100%。

Linux 在/proc/{pid}/stat 文件中记录进程ID 为pid 的进程统计信息(按照类别分为几十个字段,详见 https://man7.org/linux/man-pages/man5/proc.5.html )。如果进程状态字符是R ,则表示进程正在运行(Running),如果状态字符是S ,则表示进程正在睡眠(Sleeping)。

将该方法作为CPU 利用率的前提是:该CPU 核心只运行该进程,尽量少地被中断。

计算CPU 利用率经典方法(方法二)

工具(如top 和htop)定时采样读取/proc/stat 文件内容,该文件记录了每个CPU核心所处不同状态的累计时间,再通过以下公式可计算得到指定核心CPU利用率:

previdle   = previdle + previowait
idle     = idle + iowait
prevnoidle  = prevuser + prevnice + prevsystem + previrq + prevsoftirq + prevsteal
noidle    = user + nice + system + irq + softirq + steal
prevtotal   = previdle + prevnonidle
total     = idle + nonidle
total_delta  = total - prevtotal
idle_delta  = idle - previdle
CPU_usage    = (total_delta - idle_delta)/total_delta

    以prev开头的变量是上一次采用数据,否则是当前采样数据,通过计算两次采样之间的CPU空闲时间(idle_delta)和总计时间(total_delta)得到CPU利用率,这是procs/top 工具默认使用的方法,也是最常见CPU利用率统计方法。

方法三

方法和约束情况类似于方法一,周期性读取/proc/{pid}/stat文件,获取进程用户态时间utime 和核态时间stime,按照如下公式计算CPU利用率

    CPU_usage = (utime+stime)/sample_interval

其中,utime和stime代表进程用户态时间和核态时间,sample_interval是采样间隔。

下面是负载由轻到高,三种方法得到的CPU 利用率:

Linux 内核文档PDF版(Linux kernel v5.8 documents pdf version)

accounting.pdf

doc-guide.pdf

ide.pdf

maintainer.pdf

process.pdf

timers.pdf

admin-guide.pdf

driver-api.pdf

iio.pdf

mhi.pdf

RCU.pdf

trace.pdf

arm64.pdf

fault-injection.pdf

infiniband.pdf

mips.pdf

riscv.pdf

translations.pdf

arm.pdf

fb.pdf

input.pdf

misc-devices.pdf

s390.pdf

usb.pdf

block.pdf

filesystems.pdf

isdn.pdf

netlabel.pdf

scheduler.pdf

userspace-api.pdf

bpf.pdf

firmware-guide.pdf

kbuild.pdf

networking.pdf

scsi.pdf

virt.pdf

cdrom.pdf

fpga.pdf

kernel-hacking.pdf

openrisc.pdf

security.pdf

vm.pdf

core-api.pdf

gpu.pdf

leds.pdf

parisc.pdf

sh.pdf

w1.pdf

cpu-freq.pdf

hid.pdf

PCI.pdf

sound.pdf

watchdog.pdf

crypto.pdf

hwmon.pdf

livepatch.pdf

pcmcia.pdf

sparc.pdf

x86.pdf

devicetree.pdf

i2c.pdf

locking.pdf

powerpc.pdf

spi.pdf

xtensa.pdf

dev-tools.pdf

ia64.pdf

m68k.pdf

power.pdf

target.pdf

你也可以在自己的Linux kernel 源码目录执行以下命令生成自己的pdf,但何必自己造轮子呢。

sudo apt-get install sphinxsearch
sudo apt-get install python-sphinx-rtd-theme
sudo apt-get install texlive-latex-recommended
sudo apt-get install texlive-base
sudo apt-get install graphviz
sudo apt-get install imagemagick
/usr/bin/virtualenv ~/sphinx_version
. ~/sphinx_version/bin/activate
pip install -r Documentation/sphinx/requirements.txt
make pdfdocs