本次圈定的性能指標是調度延遲,那首要的目標就是看看到底什么是調度延遲,調度延遲是保證每一個可運行進程都至少運行一次的時間間隔,翻譯一下,是指一個task的狀態變成了TASK_RUNNING,然后從進入 CPU 的runqueue開始,到真正執行(獲得 CPU 的執行權)的這段時間間隔。
需要說明的是調度延遲在 Linux Kernel 中實現的時候是分為兩種方式的:面向task和面向rq,我們現在關注的是task層面。
那么runqueue和調度器的一個sched period的關系就顯得比較重要了。首先來看調度周期,調度周期的含義就是所有可運行的task都在CPU上執行一遍的時間周期,而Linux CFS中這個值是不固定的,當進程數量小于8的時候,sched period就是一個固定值6ms,如果runqueue數量超過了8個,那么就保證每個task都必須運行一定的時間,這個一定的時間還叫最小粒度時間,CFS的默認最小粒度時間是0.75ms,使用sysctl_sched_min_granularity保存,sched period是通過下面這個內核函數來決定的:
/** The idea is to set a period in which each task runs once.** When there are too many tasks (sched_nr_latency) we have to stretch* this period because otherwise the slices get too small.** p = (nr <= nl) ? l : l*nr/nl*/static u64 __sched_period(unsigned long nr_running){ if (unlikely(nr_running > sched_nr_latency)) return nr_running * sysctl_sched_min_granularity; else return sysctl_sched_latency;}
nr_running就是可執行task數量
那么一個疑問就產生了,這個不就是調度延遲scheduling latency嗎,并且每一次計算都會給出一個確定的調度周期的值是多少,但是這個調度周期僅僅是用于調度算法里面,因為這里的調度周期是為了確保runqueue上的task的最小調度周期,也就是在這段時間內,所有的task至少被調度一次,但是這僅僅是目標,而實際是達不到的。因為系統的狀態、task的狀態、task的slice等等都是不斷變化的,周期性調度器會在每一次tick來臨的時候檢查當前task的slice是否到期,如果到期了就會發生preempt搶,而周期性調度器本身的精度就很有限,不考慮 hrtick 的情況下,我們查看系統的時鐘頻率:
$ grep CONFIG_HZ /boot/config-$(uname -r)
# CONFIG_HZ_PERIODIC is not set
# CONFIG_HZ_100 is not set
CONFIG_HZ_250=y
# CONFIG_HZ_300 is not set
# CONFIG_HZ_1000 is not set
CONFIG_HZ=250
僅僅是250HZ,也就是4ms一次時鐘中斷,所以都無法保證每一個task在CPU上運行的slice是不是它應該有的slice,更不要說保證調度周期了,外加還有wakeup、preempt等等事件。
1. atop的統計方法
既然不能直接使用計算好的值,那么就得通過其他方法進行統計了,首先Linux kernel 本身是有統計每一個task的調度延遲的,在內核中調度延遲使用的說法是run delay,并且通過proc文件系統暴露了出來,因此大概率現有的傳統工具提取調度延遲的源數據是來自于proc的,例如atop工具。
run delay在proc中的位置:
進程的調度延遲:/proc//schedstat 線程的調度延遲:/proc/ /task/ /schedstat
現在的目標變為搞清楚atop工具是怎么統計調度延遲的。
現有的工具atop是可以輸出用戶態每一個進程和線程的調度延遲指標的,在開啟atop后按下s鍵,就會看到RDELAY列,這一列就是調度延遲了。我們來看看 atop 工具是怎么統計這個指標值的,cloneatop工具的代碼:
git@github.com:Atoptool/atop.git
由于目前的目標是搞清楚atop對調度延遲指標的統計方法,因此我只關心和這個部分相關的代碼片段,可視化展示的部分并不關心。
整體來說,atop 工作的大體流程是:
intmain(int argc, char *argv[]){··· // 獲取 interval interval = atoi(argv[optind]); // 開啟收集引擎 engine();··· return 0; /* never reached */}
這里的interval就是我們使用atop的時候以什么時間間隔來提取數據,這個時間間隔就是interval。
所有的計算等操作都在engine()函數中完成
engine()的工作流程如下:
static voidengine(void){··· /* ** install the signal-handler for ALARM, USR1 and USR2 (triggers * for the next sample) */ memset(&sigact, 0, sizeof sigact); sigact.sa_handler = getusr1; sigaction(SIGUSR1, &sigact, (struct sigaction *)0);··· if (interval > 0) alarm(interval);··· for (sampcnt=0; sampcnt < nsamples; sampcnt++) {··· if (sampcnt > 0 && awaittrigger) pause(); awaittrigger = 1;··· do { curtlen = counttasks(); // worst-case value curtpres = realloc(curtpres, curtlen * sizeof(struct tstat)); ptrverify(curtpres, "Malloc failed for %lu tstats ", curtlen); memset(curtpres, 0, curtlen * sizeof(struct tstat)); } while ( (ntaskpres = photoproc(curtpres, curtlen)) == curtlen); ··· } /* end of main-loop */}
代碼細節上不再詳細介紹,整體運行的大循環是在16行開始的,真正得到調度延遲指標值的是在34行的photoproc()函數中計算的,傳入的是需要計算的task列表和task的數量
來看看最終計算的地方:
unsigned longphotoproc(struct tstat *tasklist, int maxtask){··· procschedstat(curtask); /* from /proc/pid/schedstat */··· if (curtask->gen.nthr > 1) {··· curtask->cpu.rundelay = 0;··· /* ** open underlying task directory */ if ( chdir("task") == 0 ) {··· while ((tent=readdir(dirtask)) && tvalcpu.rundelay += procschedstat(curthr); ··· } ··· } } ··· return tval;}
第5行的函數就是在讀取proc的schedstat文件:
static count_t procschedstat(struct tstat *curtask){ FILE *fp; char line[4096]; count_t runtime, rundelay = 0; unsigned long pcount; static char *schedstatfile = "schedstat"; /* ** open the schedstat file */ if ( (fp = fopen(schedstatfile, "r")) ) { curtask->cpu.rundelay = 0; if (fgets(line, sizeof line, fp)) { sscanf(line, "%llu %llu %lu ", &runtime, &rundelay, &pcount); curtask->cpu.rundelay = rundelay; } /* ** verify if fgets returned NULL due to error i.s.o. EOF */ if (ferror(fp)) curtask->cpu.rundelay = 0; fclose(fp); } else { curtask->cpu.rundelay = 0; } return curtask->cpu.rundelay; }
15行是在判斷是不是有多個thread,如果有多個thread,那么就把所有的thread的調度延遲相加就得到了這個任務的調度延遲。
所以追蹤完atop對調度延遲的處理后,我們就可以發現獲取數據的思路是開啟atop之后,按照我們指定的interval,在大循環中每一次interval到來以后,就讀取一次proc文件系統,將這個值保存,因此結論就是目前的atop工具對調度延遲的提取方式就是每隔interval秒,讀取一次proc下的schedstat文件。
因此atop獲取的是每interval時間的系統當前進程的調度延遲快照數據,并且是秒級別的提取頻率。
2. proc的底層方法—面向task
那么數據源頭我們已經定位好了,就是來源于proc,而proc的數據全部都是內核運行過程中自己統計的,那現在的目標就轉為內核內部是怎么統計每一個task的調度延遲的,因此需要定位到內核中 proc 計算調度延遲的地點是哪里。
方法很簡單,寫一個讀取schedstat文件的簡單程序,使用ftrace追蹤一下,就可以看到proc里面是哪個函數來生成的schedstat文件中的數據,ftrace的結果如下:
2) 0.125 us | single_start(); 2) | proc_single_show() { 2) | get_pid_task() { 2) 0.124 us | rcu_read_unlock_strict(); 2) 0.399 us | } 2) | proc_pid_schedstat() { 2) | seq_printf() { 2) 1.145 us | seq_vprintf(); 2) 1.411 us | } 2) 1.722 us | } 2) 2.599 us | }
很容易可以發現是第六行的函數:
#ifdef CONFIG_SCHED_INFO/** Provides /proc/PID/schedstat*/static int proc_pid_schedstat(struct seq_file *m, struct pid_namespace *ns, struct pid *pid, struct task_struct *task){ if (unlikely(!sched_info_on())) seq_puts(m, "0 0 0 "); else seq_printf(m, "%llu %llu %lu ", (unsigned long long)task->se.sum_exec_runtime, (unsigned long long)task->sched_info.run_delay, task->sched_info.pcount); return 0;}#endif
第8行是在判斷一個內核配置選項,一般默認都是開啟的,或者能看到schedstat文件有輸出,那么就是開啟的,或者可以用make menuconfig查找一下這個選項的狀態。
可以發現proc在拿取這個調度延遲指標的時候是直接從傳進來的task_struct中的sched_info中記錄的run_delay,而且是一次性讀取,沒有做平均值之類的數據處理,因此也是一個快照形式的數據。
首先說明下sched_info結構:
struct sched_info {#ifdef CONFIG_SCHED_INFO /* Cumulative counters: */ /* # of times we have run on this CPU: */ unsigned long pcount; /* Time spent waiting on a runqueue: */ unsigned long long run_delay; /* Timestamps: */ /* When did we last run on a CPU? */ unsigned long long last_arrival; /* When were we last queued to run? */ unsigned long long last_queued;#endif /* CONFIG_SCHED_INFO */};
和上面proc函數的宏是一樣的,所以可以推測出來這個宏很有可能是用來開啟內核統計task的調度信息的。每個字段的含義代碼注釋已經介紹的比較清晰了,kernel 對調度延遲給出的解釋是在 runqueue 中等待的時間。
現在的目標轉變為內核是怎么對這個run_delay字段進行計算的。需要回過頭來看一下sched_info的結構,后兩個是用于計算run_delay參數的,另外這里就需要Linux調度器框架和CFS調度器相關了,首先需要梳理一下和進程調度信息統計相關的函數,其實就是看CONFIG_SCHED_INFO這個宏包起來了哪些函數,找到這些函數的聲明點,相關的函數位于kernel/sched/stats.h中。
涉及到的函數如下:
sched_info_queued(rq, t)sched_info_reset_dequeued(t)sched_info_dequeued(rq, t)sched_info_depart(rq, t)sched_info_arrive(rq, next)sched_info_switch(rq, t, next)
BTW,調度延遲在rq中統計的函數是:
rq_sched_info_arrive()rq_sched_info_dequeued()rq_sched_info_depart()
注意的是這些函數的作用只是統計調度信息,查看這些函數的代碼,其中和調度延遲相關的函數有以下三個:
sched_info_depart(rq, t)sched_info_queued(rq, t)sched_info_arrive(rq, next)
并且一定是在關鍵的調度時間節點上被調用的:
1. 進入runqueue
task 從其他狀態(休眠,不可中斷等)切換到可運行狀態后,進入 runqueue 的起始時刻;
2. 調度下CPU,然后進入runqueue
task 從一個 cpu 的 runqueue 移動到另外一個 cpu 的 runqueue 時,更新進入新的 runqueue
的起始時刻;
task 正在運行被調度下CPU,放入 runqueue 的起始時刻,被動下CPU;
3. 產生新task然后進入runqueue;
4. 調度上CPU
進程從 runqueue 中被調度到cpu上運行時更新last_arrival;
可以這么理解要么上CPU,要么下CPU,下CPU并且狀態還是TASK_RUNNING狀態的其實就是進入runqueue的時機。
進入到runqueue都會最終調用到sched_info_queued,而第二種情況會先走sched_info_depart函數:
static inline void sched_info_depart(struct rq *rq, struct task_struct *t){ unsigned long long delta = rq_clock(rq) - t->sched_info.last_arrival; rq_sched_info_depart(rq, delta); if (t->state == TASK_RUNNING) sched_info_queued(rq, t);}
第3行的代碼在計算上次在CPU上執行的時間戳是多少,用現在的時間減去last_arrival(上次被調度上CPU的時間)就可以得到,然后傳遞給了rq_sched_info_depart()函數
第2種情況下,在第8行,如果進程這個時候的狀態還是TASK_RUNNING,那么說明這個時候task是被動下CPU的,表示該task又開始在runqueue中等待了,為什么不統計其它狀態的task,因為其它狀態的task是不能進入runqueue的,例如等待IO的task,這些task只有在完成等待后才可以進入runqueue,這個時候就有變成了第1種情況;第1種情況下會直接進入sched_info_queued()函數;因此這兩種情況下都是task進入了runqueue然后最終調用sched_info_queued()函數記錄上次(就是現在)進入runqueue 的時間戳last_queued。
sched_info_queued()的代碼如下:
static inline void sched_info_queued(struct rq *rq, struct task_struct *t) { if (unlikely(sched_info_on())) { if (!t->sched_info.last_queued) t->sched_info.last_queued = rq_clock(rq); } }
然后就到了最后一個關鍵節點,task被調度CPU了,就會觸發sched_info_arrive()函數:
static void sched_info_arrive(struct rq *rq, struct task_struct *t) { unsigned long long now = rq_clock(rq), delta = 0; if (t->sched_info.last_queued) delta = now - t->sched_info.last_queued; sched_info_reset_dequeued(t); t->sched_info.run_delay += delta; t->sched_info.last_arrival = now; t->sched_info.pcount++; rq_sched_info_arrive(rq, delta); }
這個時候就可以來計算調度延遲了,代碼邏輯是如果有記錄上次的last_queued時間戳,那么就用現在的時間戳減去上次的時間戳,就是該 task 的調度延遲,然后保存到run_delay字段里面,并且標記這次到達CPU的時間戳到last_arrival里面,pcount記錄的是上cpu上了多少次。
公式就是:
該task的調度延遲=該task剛被調度上CPU的時間戳-last_queued(該task上次進入runqueue的時間戳) |
-
cpu
+關注
關注
68文章
10870瀏覽量
211874 -
Linux
+關注
關注
87文章
11310瀏覽量
209597 -
調度器
+關注
關注
0文章
98瀏覽量
5256
原文標題:通過性能指標學習Linux Kernel - (上)
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論