CPU 性能指標提取及源碼分析

陳老師說

當 2022 級的同學考上研究生,這個暑假在雲班課開啓了 Linux 內核學習之旅,很多同學是零基礎、本科非計算機專業,通過兩個月的學習,他們逐漸踏入 Linux 內核的大門,開啓性能探索之旅。

作者介紹

南帥波,師從陳莉君老師,西安郵電大學研一在讀,剛剛踏入 Linux 內核學習的小白一枚。

內容簡介

這篇報告主要根據 CPU 性能指標——運行隊列長度、調度延遲和平均負載,對系統的性能影響進行簡單分析。

運行隊列長度

CPU 調度程序運行隊列中存放的是那些已經準備好運行、正等待可用 CPU 的輕量級進程,如果準備運行 的輕量級進程數超過系統所能處理的上限,運行隊列就會很長,運行隊列長表明系統負載可能已經飽和。 

代碼源於參考資料 1 中 map.c 用於獲取運行隊列長度的部分代碼:

// 獲取運行隊列長度
// SEC("kprobe/update_rq_clock")
int update_rq_clock(struct pt_regs *ctx) {
u32 key = 0;
u32 rqKey = 0;
struct rq *p_rq = 0;
p_rq = (struct rq *)rq_map.lookup(&rqKey);
if (!p_rq) { // 針對map表項未創建的時候,map表項之後會自動創建並初始化
return 0;
}
bpf_probe_read_kernel(p_rq, sizeof(struct rq), (void *)PT_REGS_PARM1(ctx));
u64 val = p_rq->nr_running;
runqlen.update(&key, &val);
return 0;
}

掛載點:update_rq_clock() 函數 

update_rq_clock() 被 scheduler_tick() 函數調用。

週期性調度器:

週期性調度器在 scheduler_tick 中實現. 如果系統正在活動中, 內核會按照頻率 HZ 自動調用該函數. 如果沒有進程在等待調度, 那麼在計算機電力供應不足的情況下, 內核將關閉該調度器以減少能耗. 這對於我們的 嵌入式設備或者手機終端設備的電源管理是很重要的。

週期性調度器主流程: 

scheduler_tick 函數定義在 kernel/sched/core.c,linux 內核版本:5.15:

void scheduler_tick(void)
{
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
struct task_struct *curr = rq->curr;
struct rq_flags rf;
unsigned long thermal_pressure;
u64 resched_latency;
arch_scale_freq_tick();
sched_clock_tick();
rq_lock(rq, &rf);
update_rq_clock(rq);
.....

在這個函數中主要做兩方面工作:

  1. 更新相關統計量,管理內核中的與整個系統和各個進程的調度相關的統計量. 其間執行的主要操作 是對各種計數器 + 1。

RrMgOo

  1. 激活負責當前進程調度類的週期性調度方法。

    由於調度器的模塊化結構, 主體工程其實很簡單, 在更新統計信息的同時, 內核將真正的調度工作委 託給了特定的調度類方法。

    內核先找到了就緒隊列上當前運行的進程 curr, 然後調用 curr 所屬調度類 sched_class 的週期性調度 方法 task_tick,即:

curr->sched_class->task_tick(rq, curr, 0);
task_tick 的實現方法取決於底層的調度器類, 例如完全公平調度器會在該方法中檢測是否進程已經 運行了太長的時間, 以避免過長的延遲, 注意此處的做法與之前就的基於時間片的調度方法有本質區 別, 舊的方法我們稱之爲到期的時間片, 而完全公平調度器 CFS 中則不存在所謂的時間片概念.

bpf_probe_read_kernel(): 讀取內核結構體的成員

rq 結構體:

linux 內核用結構體 rq(struct rq)將處於就緒(ready)狀態的進程組織在一起。

rq 結構體包含 cfs 和 rt 成員,分別表示兩個就緒隊列:cfs 就緒隊列用於組織就緒的普通進程 (這個隊列上 的進程用完全公平調度器進行調度);rt 就緒隊列用於用於組織就緒的實時進程 (該隊列上的進程用實時調 度器調度)。在多核系統中,每個 CPU 對應一個 rq 結構體**。**

struct rq {
/* runqueue lock: */
raw_spinlock_t lock;
/*
nr_running and cpu_load should be in the same cacheline because
remote CPUs use both these fields when doing load calculation.
*/
unsigned int nr_running;
....

nr_running:表示總共就緒的進程數(包括 cfs,rq 及正在運行的) 

正常運行結果,查看第三列的運行隊列長度:

壓力測試工具 stress-ng :

這裏進行壓力測試後,再次查看運行隊列長度:

可以看到運行隊列長度的明顯變化,從 3 左右變化到了 10 左右。

總結:

當系統運行隊列長度等於虛擬處理器的個數時,用戶不會明顯感覺到性能下降,當運行隊列長度達到虛 擬處理器的 4 倍或更多時,系統的響應就非常遲緩了。

CPU 調度程序運行隊列性能調優的一般原則: 

如果在很長一段時間裏,運行隊列的長度一直都超過虛擬處理器個數的 1 倍,就需要關注了,只是暫時不需要立即採取行動。如果在很長一段時間裏,運行隊列的長度達到虛擬處理器個數的 3~4 倍或更高,則需要立即採取行動。

解決 CPU 調用程序運行隊列過長有以下兩個方法:

  1. 增加 CPU 以分擔負載或減少處理器的負載量,從根本上減少了每個虛擬處理器上的活動線程數,從而 減少運行隊列中的輕量級進程數。 

  2. 分析系統中運行的應用,改進 CPU 使用率。程序員可以通過更有效的算法和數據結構來實現更好的性 能,性能專家通過減少代碼路徑長度或完成同樣任務更少 CPU 指令的算法來提高性能。

調度延遲

所謂調度延遲,是指一個任務具備運行的條件(進入 CPU 的 runqueue),到真正執行(獲得 CPU 的 執行權)的這段時間。 

runqlat 是一個 bcc 和 bpftrace 工具,用於測量 cpu 調度程序延遲,通常稱爲運行隊列延遲。 

runqlat.py 部分代碼:

.......
int trace_wake_up_new_task(struct pt_regs *ctx, struct task_struct *p)
{
return trace_enqueue(p->tgid, p->pid);
}
int trace_ttwu_do_wakeup(struct pt_regs *ctx, struct rq *rq, struct task_struct
*p,
int wake_flags)
{
return trace_enqueue(p->tgid, p->pid);
}
// record enqueue timestamp
static int trace_enqueue(u32 tgid, u32 pid)
{
if (FILTER || pid == 0)
return 0;
u64 ts = bpf_ktime_get_ns();
start.update(&pid, &ts);
return 0;
}
/*trace_enqueue()函數只做了一件事情,就是記錄當前這個pid進程進入 runqueue 的時間戳, 現在只
考慮最普通的情況,只記錄pid的情況,因此每有一個 task 被加入到 runqueue 的時候,就記錄這個
task  pid 和當前的納秒時間戳。*/
int trace_run(struct pt_regs *ctx, struct task_struct *prev)
{
u32 pid, tgid;
// ivcsw: treat like an enqueue event and store timestamp
if (prev->__state == TASK_RUNNING) {
tgid = prev->tgid;
pid = prev->pid;
if (!(FILTER || pid == 0)) {
u64 ts = bpf_ktime_get_ns();
start.update(&pid, &ts);
}
}
tgid = bpf_get_current_pid_tgid() >> 32;
pid = bpf_get_current_pid_tgid();
if (FILTER || pid == 0)
return 0;
u64 *tsp, delta;
// fetch timestamp and calculate delta
tsp = start.lookup(&pid);
if (tsp == 0) {
return 0; // missed enqueue
}
delta = bpf_ktime_get_ns() - *tsp;
FACTOR
// store as histogram
STORE
start.delete(&pid);
return 0;
}
.....
# load BPF program
b = BPF(text=bpf_text)
if not is_support_raw_tp:
  b.attach_kprobe(event="ttwu_do_wakeup", fn_)
  b.attach_kprobe(event="wake_up_new_task", fn_)
  b.attach_kprobe(event="finish_task_switch", fn_)
print("Tracing run queue latency... Hit Ctrl-C to end.")
.....

掛載點:

喚醒睡眠進程: process_timeout->wake_up_process->try_to_wake_up->ttwu_queue-> ttwu_do_activate()->ttwu_do_wakeup 

新進程創建後 (do_fork),也會被喚醒 (wake_up_new_task) 

wake_up 系列函數,完成兩個主要功能:

  1. 標記當前進程需要被調度;

  2. 將被喚醒的進程添加到優先級隊列,以便 schedule() 在選取下一個進程運行時,有機會選擇到。注意 此處: 對於實時進程來說, 不是添加到優先級隊列就一定會被調度選擇到,這還與進程的優先級相關,這一 點和 cfs 調度器有明顯區別, cfs 策略在一個調度週期內所有進程都有機會被調度到,只是運行時間不同, 與 nice 值有關。

進程被重新調度時無論是否爲剛 fork 出的進程都會走到 finish_task_switch 這個函數,主要工作爲:檢查回收前一個進程資源,爲當前進程恢復執行做一些準備工作。

使用 runqlat 工具:

正常情況下使用 runqlat 工具,查看調度延遲分佈情況:

壓力測試:

壓力測試後,再次查看調度延遲:

這裏觀察壓力測試前後的調度延遲,從最大延遲 511 微秒變化到了 32767 微秒,可以明顯的看到調度延遲 的變化。

以上的 runqlat 腳本只能看出延遲時間的統計結果,如果要探究延遲爲什麼會增大,得用 perf 這樣更精 細的工具。在保持 4 個 worker 線程的情況下,採樣 5 秒內和 "sched" 相關的信息:perf sched record -- sleep 5,然後用 perf sched latency 解析,可以看到每個進程的運行時間、最大延遲等 信息。像這裏,就是 stress-ng 進程有 4 個線程,總共運行了 20 秒左右,最大延遲爲 5.151 毫秒。

說明:

當 CPU 還被其他任務佔據,還沒有空出來,可能還有其他在 runqueue 中排隊的任務。就會產生調度延 遲,排隊的任務越多,調度延遲就可能越長,所以這也是間接衡量 CPU 負載的一個指標(CPU 負載通 過計算各個時刻 runqueue 上的任務數量獲得)。

平均負載:

正常情況下的 top 命令:

看 1 分鐘、5 分鐘、15 分鐘的 load average 分別爲 0.66、1.68、1.49,並且 cpu 基本上是空閒狀態。壓力測試後的 top 命令:

再次查看 1 分鐘、5 分鐘、15 分鐘的 load average 分別爲 4.98、3.17、1.98,並且 cpu 佔用率達到了 99.3%。

load average 是對 CPU 負載的評估,其值越高,說明其任務隊列越長,處於等待執行的任務越多。

說明:

多核和多處理器下的平均負載,單個四核處理器和具有四個處理器(每個處理器一個核)的服務器是否 相同?相對來說,是的。多核和多處理器的主要區別在於,前者是指單個 CPU 具有多個內核,而後者是 指多個 CPU。總結一下:一個四核等於兩個雙核,也就是四個單核。平均負載與服務器中可用內核的數 量有關,而不是它們在 CPU 上的分佈情況。這意味着最大利用率範圍是單核 0-1、雙核 0-2、四核 0- 4、八核 0-8,依此類推。在單核處理器上,負載爲 1.00 意味着容量在單核處理器上恰到好處;而在雙 核處理器上,負載爲 1.50 意味着負載已滿,另一個也要耗盡滿。同樣,四核處理器上的 5.00 負載是值 得擔心的,而在八核處理器上,5.00 意味着正在消耗,並且仍有最佳可用空間。我的虛擬機是四核的, 這裏看出,一分鐘內的平均負載已經達到 4.98,已經是非常高的了。

參考資料:

  1. eBPF_Supermarket

    https://gitee.com/linuxkerneltravel/lmp/tree/develop/eBPF_Supermarket/CPU_Subsystem/BCC_practice

  2. Linux 的調度延遲

    https://zhuanlan.zhihu.com/p/462728452

  3. 通過性能指標學習 Linux Kernel

    趙晨雨,公衆號:Linux 內核之旅通過性能指標學習 Linux Kernel - (上)

  4. 高性能:可用於 CPU 分析的 BPF 工具

    https://cloud.tencent.com/developer/article/1595327

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/GBzBl2pZzGzRvkUP5oHDdw