本次圈定的性能指標(biāo)是調(diào)度延遲,那首要的目標(biāo)就是看看到底什么是調(diào)度延遲,調(diào)度延遲是保證每一個可運行進(jìn)程都至少運行一次的時間間隔,翻譯一下,是指一個task的狀態(tài)變成了TASK_RUNNING,然后從進(jìn)入 CPU 的runqueue開始,到真正執(zhí)行(獲得 CPU 的執(zhí)行權(quán))的這段時間間隔。
需要說明的是調(diào)度延遲在 Linux Kernel 中實現(xiàn)的時候是分為兩種方式的:面向task和面向rq,我們現(xiàn)在關(guān)注的是task層面。
那么runqueue和調(diào)度器的一個sched period的關(guān)系就顯得比較重要了。首先來看調(diào)度周期,調(diào)度周期的含義就是所有可運行的task都在CPU上執(zhí)行一遍的時間周期,而Linux CFS中這個值是不固定的,當(dāng)進(jìn)程數(shù)量小于8的時候,sched period就是一個固定值6ms,如果runqueue數(shù)量超過了8個,那么就保證每個task都必須運行一定的時間,這個一定的時間還叫最小粒度時間,CFS的默認(rèn)最小粒度時間是0.75ms,使用sysctl_sched_min_granularity保存,sched period是通過下面這個內(nèi)核函數(shù)來決定的:
/** 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就是可執(zhí)行task數(shù)量
那么一個疑問就產(chǎn)生了,這個不就是調(diào)度延遲scheduling latency嗎,并且每一次計算都會給出一個確定的調(diào)度周期的值是多少,但是這個調(diào)度周期僅僅是用于調(diào)度算法里面,因為這里的調(diào)度周期是為了確保runqueue上的task的最小調(diào)度周期,也就是在這段時間內(nèi),所有的task至少被調(diào)度一次,但是這僅僅是目標(biāo),而實際是達(dá)不到的。因為系統(tǒng)的狀態(tài)、task的狀態(tài)、task的slice等等都是不斷變化的,周期性調(diào)度器會在每一次tick來臨的時候檢查當(dāng)前task的slice是否到期,如果到期了就會發(fā)生preempt搶,而周期性調(diào)度器本身的精度就很有限,不考慮 hrtick 的情況下,我們查看系統(tǒng)的時鐘頻率:
$ 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是不是它應(yīng)該有的slice,更不要說保證調(diào)度周期了,外加還有wakeup、preempt等等事件。
1. atop的統(tǒng)計方法
既然不能直接使用計算好的值,那么就得通過其他方法進(jìn)行統(tǒng)計了,首先Linux kernel 本身是有統(tǒng)計每一個task的調(diào)度延遲的,在內(nèi)核中調(diào)度延遲使用的說法是run delay,并且通過proc文件系統(tǒng)暴露了出來,因此大概率現(xiàn)有的傳統(tǒng)工具提取調(diào)度延遲的源數(shù)據(jù)是來自于proc的,例如atop工具。
run delay在proc中的位置:
進(jìn)程的調(diào)度延遲:/proc//schedstat 線程的調(diào)度延遲:/proc/ /task/ /schedstat
現(xiàn)在的目標(biāo)變?yōu)楦闱宄top工具是怎么統(tǒng)計調(diào)度延遲的。
現(xiàn)有的工具atop是可以輸出用戶態(tài)每一個進(jìn)程和線程的調(diào)度延遲指標(biāo)的,在開啟atop后按下s鍵,就會看到RDELAY列,這一列就是調(diào)度延遲了。我們來看看 atop 工具是怎么統(tǒng)計這個指標(biāo)值的,cloneatop工具的代碼:
git@github.com:Atoptool/atop.git
由于目前的目標(biāo)是搞清楚atop對調(diào)度延遲指標(biāo)的統(tǒng)計方法,因此我只關(guān)心和這個部分相關(guān)的代碼片段,可視化展示的部分并不關(guān)心。
整體來說,atop 工作的大體流程是:
intmain(int argc, char *argv[]){··· // 獲取 interval interval = atoi(argv[optind]); // 開啟收集引擎 engine();··· return 0; /* never reached */}
這里的interval就是我們使用atop的時候以什么時間間隔來提取數(shù)據(jù),這個時間間隔就是interval。
所有的計算等操作都在engine()函數(shù)中完成
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 */}
代碼細(xì)節(jié)上不再詳細(xì)介紹,整體運行的大循環(huán)是在16行開始的,真正得到調(diào)度延遲指標(biāo)值的是在34行的photoproc()函數(shù)中計算的,傳入的是需要計算的task列表和task的數(shù)量
來看看最終計算的地方:
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行的函數(shù)就是在讀取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的調(diào)度延遲相加就得到了這個任務(wù)的調(diào)度延遲。
所以追蹤完atop對調(diào)度延遲的處理后,我們就可以發(fā)現(xiàn)獲取數(shù)據(jù)的思路是開啟atop之后,按照我們指定的interval,在大循環(huán)中每一次interval到來以后,就讀取一次proc文件系統(tǒng),將這個值保存,因此結(jié)論就是目前的atop工具對調(diào)度延遲的提取方式就是每隔interval秒,讀取一次proc下的schedstat文件。
因此atop獲取的是每interval時間的系統(tǒng)當(dāng)前進(jìn)程的調(diào)度延遲快照數(shù)據(jù),并且是秒級別的提取頻率。
2. proc的底層方法—面向task
那么數(shù)據(jù)源頭我們已經(jīng)定位好了,就是來源于proc,而proc的數(shù)據(jù)全部都是內(nèi)核運行過程中自己統(tǒng)計的,那現(xiàn)在的目標(biāo)就轉(zhuǎn)為內(nèi)核內(nèi)部是怎么統(tǒng)計每一個task的調(diào)度延遲的,因此需要定位到內(nèi)核中 proc 計算調(diào)度延遲的地點是哪里。
方法很簡單,寫一個讀取schedstat文件的簡單程序,使用ftrace追蹤一下,就可以看到proc里面是哪個函數(shù)來生成的schedstat文件中的數(shù)據(jù),ftrace的結(jié)果如下:
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 | }
很容易可以發(fā)現(xiàn)是第六行的函數(shù):
#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行是在判斷一個內(nèi)核配置選項,一般默認(rèn)都是開啟的,或者能看到schedstat文件有輸出,那么就是開啟的,或者可以用make menuconfig查找一下這個選項的狀態(tài)。
可以發(fā)現(xiàn)proc在拿取這個調(diào)度延遲指標(biāo)的時候是直接從傳進(jìn)來的task_struct中的sched_info中記錄的run_delay,而且是一次性讀取,沒有做平均值之類的數(shù)據(jù)處理,因此也是一個快照形式的數(shù)據(jù)。
首先說明下sched_info結(jié)構(gòu):
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函數(shù)的宏是一樣的,所以可以推測出來這個宏很有可能是用來開啟內(nèi)核統(tǒng)計task的調(diào)度信息的。每個字段的含義代碼注釋已經(jīng)介紹的比較清晰了,kernel 對調(diào)度延遲給出的解釋是在 runqueue 中等待的時間。
現(xiàn)在的目標(biāo)轉(zhuǎn)變?yōu)閮?nèi)核是怎么對這個run_delay字段進(jìn)行計算的。需要回過頭來看一下sched_info的結(jié)構(gòu),后兩個是用于計算run_delay參數(shù)的,另外這里就需要Linux調(diào)度器框架和CFS調(diào)度器相關(guān)了,首先需要梳理一下和進(jìn)程調(diào)度信息統(tǒng)計相關(guān)的函數(shù),其實就是看CONFIG_SCHED_INFO這個宏包起來了哪些函數(shù),找到這些函數(shù)的聲明點,相關(guān)的函數(shù)位于kernel/sched/stats.h中。
涉及到的函數(shù)如下:
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,調(diào)度延遲在rq中統(tǒng)計的函數(shù)是:
rq_sched_info_arrive()rq_sched_info_dequeued()rq_sched_info_depart()
注意的是這些函數(shù)的作用只是統(tǒng)計調(diào)度信息,查看這些函數(shù)的代碼,其中和調(diào)度延遲相關(guān)的函數(shù)有以下三個:
sched_info_depart(rq, t)sched_info_queued(rq, t)sched_info_arrive(rq, next)
并且一定是在關(guān)鍵的調(diào)度時間節(jié)點上被調(diào)用的:
1. 進(jìn)入runqueue
task 從其他狀態(tài)(休眠,不可中斷等)切換到可運行狀態(tài)后,進(jìn)入 runqueue 的起始時刻;
2. 調(diào)度下CPU,然后進(jìn)入runqueue
task 從一個 cpu 的 runqueue 移動到另外一個 cpu 的 runqueue 時,更新進(jìn)入新的 runqueue
的起始時刻;
task 正在運行被調(diào)度下CPU,放入 runqueue 的起始時刻,被動下CPU;
3. 產(chǎn)生新task然后進(jìn)入runqueue;
4. 調(diào)度上CPU
進(jìn)程從 runqueue 中被調(diào)度到cpu上運行時更新last_arrival;
可以這么理解要么上CPU,要么下CPU,下CPU并且狀態(tài)還是TASK_RUNNING狀態(tài)的其實就是進(jìn)入runqueue的時機(jī)。
進(jìn)入到runqueue都會最終調(diào)用到sched_info_queued,而第二種情況會先走sched_info_depart函數(shù):
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上執(zhí)行的時間戳是多少,用現(xiàn)在的時間減去last_arrival(上次被調(diào)度上CPU的時間)就可以得到,然后傳遞給了rq_sched_info_depart()函數(shù)
第2種情況下,在第8行,如果進(jìn)程這個時候的狀態(tài)還是TASK_RUNNING,那么說明這個時候task是被動下CPU的,表示該task又開始在runqueue中等待了,為什么不統(tǒng)計其它狀態(tài)的task,因為其它狀態(tài)的task是不能進(jìn)入runqueue的,例如等待IO的task,這些task只有在完成等待后才可以進(jìn)入runqueue,這個時候就有變成了第1種情況;第1種情況下會直接進(jìn)入sched_info_queued()函數(shù);因此這兩種情況下都是task進(jìn)入了runqueue然后最終調(diào)用sched_info_queued()函數(shù)記錄上次(就是現(xiàn)在)進(jìn)入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); } }
然后就到了最后一個關(guān)鍵節(jié)點,task被調(diào)度CPU了,就會觸發(fā)sched_info_arrive()函數(shù):
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); }
這個時候就可以來計算調(diào)度延遲了,代碼邏輯是如果有記錄上次的last_queued時間戳,那么就用現(xiàn)在的時間戳減去上次的時間戳,就是該 task 的調(diào)度延遲,然后保存到run_delay字段里面,并且標(biāo)記這次到達(dá)CPU的時間戳到last_arrival里面,pcount記錄的是上cpu上了多少次。
公式就是:
該task的調(diào)度延遲=該task剛被調(diào)度上CPU的時間戳-last_queued(該task上次進(jìn)入runqueue的時間戳) |
-
cpu
+關(guān)注
關(guān)注
68文章
10863瀏覽量
211763 -
Linux
+關(guān)注
關(guān)注
87文章
11304瀏覽量
209499 -
調(diào)度器
+關(guān)注
關(guān)注
0文章
98瀏覽量
5248
原文標(biāo)題:通過性能指標(biāo)學(xué)習(xí)Linux Kernel - (上)
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論