簡析 Linux 的 CPU 時間

2022-10-11 18:03:57

從 CPU 時間說起...

下面這個是 top 命令的介面,相信大家應該都不陌生。

top - 19:01:38 up 91 days, 23:06,  1 user,  load average: 0.00, 0.01, 0.05
Tasks: 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 st
KiB Mem :  8010420 total,  5803596 free,   341300 used,  1865524 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  6954384 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
13436 root      20   0 1382776  28040   5728 S   0.3  0.4 251:21.06 n9e-collector
    1 root      20   0   43184   3384   2212 S   0.0  0.0   5:15.64 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.28 kthreadd
    3 root      20   0       0      0      0 S   0.0  0.0   0:00.58 ksoftirqd/0
    5 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 kworker/0:0H
    7 root      rt   0       0      0      0 S   0.0  0.0   0:35.48 migration/0

%Cpu(s): 這一行表示的是 CPU 不同時間的佔比,其中大家比較熟悉的應該是 system timeuser time

  • 正常情況下 user time 佔比應該最高,這是程序執行應用程式碼的的時間佔比(CPU 密集)
  • system time 佔用率高,則意味著存在頻繁的系統呼叫(IO 密集)或者一些潛在的效能問題

不熟悉的朋友可以參考下面這張圖(來源於極客時間的課程):

接下來我們將探究隱藏在這些時間背後的操作原理。


核心態與使用者態

作業系統的核心功能就是管理硬體資源,因此不可避免會使用到一些直接操作硬體的CPU指令,這類指令我們稱之為特權指令。特權指令如果使用不當,將會導致整個系統的崩潰,因此作業系統提供了一組特殊的資源存取程式碼 —— 核心kernel 來負責執行這些指令。

作業系統將虛擬地址空間劃分為兩部分:

  • 核心空間kernel memotry:存放核心程式碼和資料(程序間共用)
  • 使用者空間user memotry:存放使用者程式的程式碼和資料(相互隔離)

通過區分核心空間和使用者空間的設計,隔離了作業系統程式碼與應用程式程式碼。即便是單個應用程式出現錯誤也不會影響到作業系統的穩定性,這樣其它的程式還可以正常的執行。

應用程式通過核心提供的介面,存取 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 出的輕量程序,它們共用父程序的記憶體空間。

Linux 的排程策略是搶佔式的,每個執行緒都有優先順序prirority的概念,並按照優先順序高低分為兩種:

  • 實時程序(優先順序 0~99)
  • 普通程序(優先順序 100~139)

每個 CPU 都有自己的執行佇列 runqueue,需要執行的執行緒會被加入到這個佇列中。

每個佇列可以進一步細分為 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 資源。


軟中斷

中斷就是一種插隊機制,可以讓作業系統優先處理一些緊急的任務。當硬體裝置(例如,網路卡)需要向 CPU 發出訊號時(例如,資料已到達),就會產生硬體中斷。

CPU 接收到中斷時,會切換到核心態執行特定的中斷服務,並且期間不允許其他中斷搶佔(關中斷)。
當中斷服務需要執行較長時間時,可能會導致且其他的中斷得不到及時的響應。

為了提高中斷處理效率,作業系統在之前的基礎上把中斷處理分成兩部分:

  • 上半部top half:在遮蔽中斷的上下文中執行,用於完成關鍵性的處理動作
  • 下半部bottom half:不在中斷服務上下文中執行,主要處理不那麼急迫但耗時的任務

核心在處理完中斷上半部後,可以延期執行下半部,該機制被稱為軟中斷softirq
軟中斷處理的過程是不會關中斷的,因此當有硬中斷到來的時候,可以及時響應。

構成軟中斷機制的核心元素包括:

  • 註冊: 軟中斷狀態暫存器 irq_stat
  • 處理: 軟中斷向量表 softirq_vec
  • 觸發: 軟中斷守護執行緒 daemon

  1. 呼叫open_softirq()將軟中斷服務程式註冊到軟中斷向量表softirq_vec(可選)

  2. 呼叫raise_softirq()觸發軟中斷事務

    • 中斷關閉的情況下,設定軟中斷狀態位irq_stat
    • 如果呼叫者不在中斷上下文(普通程序呼叫),那麼直接喚醒daemon執行緒
  3. daemon執行緒被喚醒後會執行do_softirq()處理軟中斷

    • 檢查 irq_stat 是否存發生軟中斷事件
    • 呼叫 softirq_vec 中對應的軟中斷服務程式
    • 再次檢查 irq_stat,如果發現新的軟中斷,就會喚醒ksoftrqd執行緒來處理

ksoftrqd 機制

我們知道 CPU 執行的優先順序為:硬中斷 > 軟中斷 > 普通程序。
這意味著:

  • 一個軟中斷不會去搶佔另一個軟中斷,只有硬體中斷才可以搶佔軟中斷
  • 如果軟中斷太過頻繁,使用者程序可能永遠無法獲得 CPU 時間

為了保證公平性,核心為每個 CPU 都設定一個ksoftrqd執行緒。如果所有的軟中斷在短時間內無法被處理完,核心就會喚醒ksoftrqd處理剩餘的軟中斷。以下面這張圖為例:

  • 網路卡資料就緒,通過硬中斷通知 CPU 進行處理
  • 硬中斷服務程式呼叫raise_softirq()觸發軟中斷,喚醒daemon
  • 硬中斷服務程式退出後,daemon被喚醒開始處理軟中斷
  • 遍歷過一遍向量表後,daemon發現仍有未處理的軟中斷,喚醒ksoftrqd
  • ksoftrqd獲得 CPU 時間片後,繼續處理未完成的軟中斷

由於 ksoftrqd 其實是一個 nice 值為 0 的普通執行緒,會進入 cfs_rq 參與排程,可以和普通程序公平地使用 CPU。

但如果 ksoftrirqd 長時間得不到 CPU,就會致使軟中斷的延遲變得很大,因此 ksoftirqd 的實時性是很難得到保障。

典型問題是 ping 延遲:如果 ping 包無法在軟中斷裡得到處理,就會被 ksoftirqd 處理,導致 ping 延遲變得很大。


中斷的影響

硬中斷的優先順序很高,但是需要的 CPU 時間極少。當出現大量硬中斷時,可能會引起較多的 CPU 使用者態與核心態的切換,但是interrupt time不會顯著上升。

此外,由於部分核心程式碼是不可重入的(例如,修改暫存器),其執行過程不能打斷。因此這些程式碼的執行過程中,會遮蔽掉硬中斷。

關中斷的操作在核心裡隨處可見,這反過來會給硬中斷帶來一些影響。比如,程序關中斷時間太長會導致網路報文無法及時處理,進而引起業務效能抖動。

而軟中斷的執行時間如果太長,就會給使用者執行緒帶來延遲,如果 softirq time 很大則很可能意味著使用者執行緒會受到影響。

網路 IO 頻繁的應用機器的 softirq time 通常會比較高,可能存在網路連線數過多,或者網路流量過大的情況,

ksoftirqd 的優先順序與使用者執行緒是一致的,因此,如果軟中斷處理常式是在 ksoftirqd 裡執行的,那它可能會有一些延遲。


時間竊取

在 GNU top命令中,steal time定義為 「虛擬機器器管理程序 hypervisor 從 VM 竊取的時間」。 該概念是Xen,KVM,VMware 等社群或者廠商推廣到Linux社群的。

當系統管理程序和 VM 嘗試佔用同一物理 CPU 核 pCPU 時,會導致 VM 的虛擬 CPU vCPU 可用的處理器時間減少,從而造成 VM 效能下降。

中虛擬化環境中,可能發生時間竊取的一些情況:

  • 多個高負載 VM 的 vCPU 的執行在同個 pCPU 上(公有云的 CPU 超賣)
  • VM 的 vCPU 與執行緒繫結在了某個特定的 pCPU 上,導致虛擬主機 vhost 程序處理 I/O 時從這些 vCPU 上竊取時間
  • 虛擬機器器監控程式程序(例如監視,紀錄檔記錄和I/O程序)與 VM 爭搶 pCPU

參考資料

https://www.cnblogs.com/Anker/p/3269106.html
https://zhuanlan.zhihu.com/p/69554144
https://blog.csdn.net/helloanthea/article/details/28877221
https://blog.csdn.net/lenomirei/article/details/79274073
https://www.jianshu.com/p/673c9e4817a8
https://opensource.com/article/20/1/cpu-steal-time
https://www.cnblogs.com/menkeyi/p/6732020.html
https://blog.csdn.net/zxh2075/article/details/78262568
https://kb.cnblogs.com/page/207897/
https://www.cnblogs.com/charlesblc/p/6255806.html
http://www.wowotech.net/linux_kenrel/soft-irq.html