天天看點

講解CPU性能名額提取及源碼分析

作者: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。

講解CPU性能名額提取及源碼分析

2.激活負責目前程序排程類的周期性排程方法。

由于排程器的子產品化結構, 主體工程其實很簡單, 在更新統計資訊的同時, 核心将真正的排程工作委 托給了特定的排程類方法。

核心先找到了就緒隊列上目前運作的程序curr, 然後調用curr所屬排程類sched_class的周期性排程 方法task_tick,即:

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

更多linux核心視訊教程文檔資料免費領取背景私信【核心】自行擷取.

講解CPU性能名額提取及源碼分析
講解CPU性能名額提取及源碼分析

Linux核心源碼/記憶體調優/檔案系統/程序管理/裝置驅動/網絡協定棧-學習視訊教程-騰訊課堂

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及正在運作的)

正常運作結果,檢視第三列的運作隊列長度:

講解CPU性能名額提取及源碼分析

壓力測試工具 stress-ng :

  • -c 2 : 生成2個worker循環調用sqrt()産生cpu壓力
  • -i 1 : 生成1個worker循環調用sync()産生io壓力
  • -m 1 : 生成1個worker循環調用malloc()/free()産生記憶體壓力
講解CPU性能名額提取及源碼分析

這裡進行壓力測試後,再次檢視運作隊列長度:

講解CPU性能名額提取及源碼分析

可以看到運作隊列長度的明顯變化,從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_name="trace_ttwu_do_wakeup")
  b.attach_kprobe(event="wake_up_new_task", fn_name="trace_wake_up_new_task")
  b.attach_kprobe(event="finish_task_switch", fn_name="trace_run")

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工具,檢視排程延遲分布情況:

講解CPU性能名額提取及源碼分析

壓力測試:

講解CPU性能名額提取及源碼分析

壓力測試後,再次檢視排程延遲:

講解CPU性能名額提取及源碼分析

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

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

講解CPU性能名額提取及源碼分析

說明:

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

平均負載:

正常情況下的top指令:

講解CPU性能名額提取及源碼分析

看1分鐘、5分鐘、15分鐘的load average分别為0.66、1.68、1.49,并且cpu基本上是空閑狀态。壓力測試後的top指令:

講解CPU性能名額提取及源碼分析

再次檢視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,已經是非常高的了。

轉載位址:一篇講解CPU性能名額提取及源碼分析 - 圈點 - 核心技術中文網 - 建構全國最權威的核心技術交流分享論壇

講解CPU性能名額提取及源碼分析

繼續閱讀