linux內核時間片調度實現詳解(基於ARM處理器)

(本文基於linux-4.5.3對內核公平調度之時間片相關函數主要流程做了詳細介紹,作者水平有限,難免有理解不到位、甚至不對之處,一切以實際爲準,其他細節請參考其他書籍或源碼。)

1、內核相關函數調用棧

1.1、時間片計數

(時間片計數主要用於對當前運行進程的時間進行統計;公平調度算法使進程都能夠公平得到cpu運行時間,調度是總是選擇運行時間最短的就緒進程來運行。以下棧是從定時器中斷到當前進程運行時間更新)

#0  update_curr (cfs_rq=0x87ec1db8) at kernel/sched/fair.c:702
#1  0x8004e10c in entity_tick (queued=<optimized out>, curr=<optimized out>, cfs_rq=<optimized out>) at kernel/sched/fair.c:3395
#2  task_tick_fair (rq=<optimized out>, curr=0x87840000, queued=<optimized out>) at kernel/sched/fair.c:8026
#3  0x8004630c in scheduler_tick () at kernel/sched/core.c:2972
#4  0x8006f9a0 in update_process_times (user_tick=0) at kernel/time/timer.c:1425
#5  0x8007c334 in tick_periodic (cpu=<optimized out>) at kernel/time/tick-common.c:92
#6  0x8007c4f0 in tick_handle_periodic (dev=0x87ec4880) at kernel/time/tick-common.c:104
#7  0x80015e28 in twd_handler (irq=<optimized out>, dev_id=<optimized out>) at arch/arm/kernel/smp_twd.c:238
#8  0x80064890 in handle_percpu_devid_irq (desc=0x87805c00) at kernel/irq/chip.c:726
#9  0x80060690 in generic_handle_irq_desc (desc=<optimized out>) at include/linux/irqdesc.h:146
#10 generic_handle_irq (irq=<optimized out>) at kernel/irq/irqdesc.c:363
#11 0x80060940 in __handle_domain_irq (domain=0x87802400, hwirq=16, lookup=<optimized out>, regs=<optimized out>) at kernel/irq/irqdesc.c:400
#12 0x80009458 in handle_domain_irq (regs=<optimized out>, hwirq=<optimized out>, domain=<optimized out>) at include/linux/irqdesc.h:164
#13 gic_handle_irq (regs=0x87ec1db8) at drivers/irqchip/irq-gic.c:339
#14 0x80013b14 in __irq_svc () at arch/arm/kernel/entry-armv.S:213

1.2、設置重新調度標誌

(當前進程時間片更新後會判斷是否有運行時間更短的進程需要調度,以及當前進程時間片是否達到最大值等。以下棧是從定時器中斷到設置重新調度標識,中斷退出的時候會判斷進程重新調度標識,有設置的話,會進行進程切換。)

#0  resched_curr (rq=0x87ec1d80) at kernel/sched/core.c:576
#1  0x8004e9c8 in check_preempt_tick (curr=<optimized out>, cfs_rq=<optimized out>) at kernel/sched/fair.c:3245
#2  entity_tick (queued=<optimized out>, curr=<optimized out>, cfs_rq=<optimized out>) at kernel/sched/fair.c:3421
#3  task_tick_fair (rq=<optimized out>, curr=0x87840000, queued=<optimized out>) at kernel/sched/fair.c:8026
#4  0x8004630c in scheduler_tick () at kernel/sched/core.c:2972
#5  0x8006f9a0 in update_process_times (user_tick=0) at kernel/time/timer.c:1425
#6  0x8007c334 in tick_periodic (cpu=<optimized out>) at kernel/time/tick-common.c:92
#7  0x8007c4f0 in tick_handle_periodic (dev=0x87ec4880) at kernel/time/tick-common.c:104
#8  0x80015e28 in twd_handler (irq=<optimized out>, dev_id=<optimized out>) at arch/arm/kernel/smp_twd.c:238
#9  0x80064890 in handle_percpu_devid_irq (desc=0x87805c00) at kernel/irq/chip.c:726
#10 0x80060690 in generic_handle_irq_desc (desc=<optimized out>) at include/linux/irqdesc.h:146
#11 generic_handle_irq (irq=<optimized out>) at kernel/irq/irqdesc.c:363
#12 0x80060940 in __handle_domain_irq (domain=0x87802400, hwirq=16, lookup=<optimized out>, regs=<optimized out>) at kernel/irq/irqdesc.c:400
#13 0x80009458 in handle_domain_irq (regs=<optimized out>, hwirq=<optimized out>, domain=<optimized out>) at include/linux/irqdesc.h:164
#14 gic_handle_irq (regs=0x87ec1d80) at drivers/irqchip/irq-gic.c:339
#15 0x80013b14 in __irq_svc () at arch/arm/kernel/entry-armv.S:213

1.3、進程重新調度

(用戶進程被定時器中斷,調用__irq_usr函數,__irq_usr調用定時器中斷處理函數更新當前進程的時間片,檢查當前進程是否需要被搶佔,設置搶佔標識,中斷退出;退出中斷之前,先判斷_TIF_WORK_MASK標識是否被設置,該標識包括重新調度標識,有設置則調用do_work_pending函數,該函數檢查重新調度標識,然後調用schedule進行進程切換。)

#0  schedule () at kernel/sched/core.c:3306
#1  0x80012bf8 in do_work_pending (regs=0x873d9fb0, thread_flags=<optimized out>, syscall=0) at arch/arm/kernel/signal.c:576
#2  0x8000f614 in slow_work_pending () at arch/arm/kernel/entry-common.S:78
#3  0x8000f630 in ret_to_user_from_irq () at arch/arm/kernel/entry-common.S:98
#4  0x80013e1c in __irq_usr () at arch/arm/kernel/entry-armv.S:98

 

2、時間片相關函數

(以下介紹是以64位無符號數爲例的,最低位即個位索引爲0,最高位索引爲63;下標b代表二進制;有符號數最高位代表符號位,1爲負數,0爲正數。)

2.1、無符號數溢出

就如tcp報文的序號一樣,序號不斷遞增,但是呢序號位數是固定的,隨着數據的不斷增加,序號會溢出,從零開始,這個時候我們需要判斷是0大呢,還是0xffffffff大,單從數字看0xffffffff肯定比0大,時間情況應該是0更大,否則對報文解析的時候,順序就會出錯。

linux內核對進程運行時間也是不斷累計的,總會出現溢出情況,對運行時間比較是基於公平調度前提下的,即所有進程運行時間相差不大,假設進程1的運行時間爲t1、進程2的運行時間爲t2,運行時間相差不大的意思是|t1 - t2|足夠小(小到什麼程度後面會介紹)。

計算機中,對於n位無符號數運算有如下公式成立(前邊是表達式,結果是以n位表示的,代碼中只關注0~(n-1)位;後邊是成立條件):

(t1<t2時,需要向第n位借位,相當於t1 + 2^n - t2)

2.1.1 都未溢出的情況

當t1 > t2時:

公平調度會使得t1、t2之間的差值很小,使得t1-t2遠遠小於2^(n-1),舉個十進制的例子來說,距離大概是1萬和9千9百多的差距吧,使得他們之間的距離用萬位、千位、百位都爲0,距離只有幾十;

t1-t2有如下表達式成立

當我們把結果當有符號數看待時,最高位爲0,結果爲正數,即可得到t1>t2,這與實際情況是一致的。

t2-t1有如下表達式成立

把結果當作有符號數看待,最高位爲1,結果爲負數,同樣可以得到t2<t1

2.1.2、部分溢出的情況

假設t1是已經溢出,t2未溢出(都未溢出的情況2.1.1已經說明,都溢出的情況,計算比較方法與2.1.1是完全一樣的),數值上有t1<t2,因爲t1已經溢出了,就比如最大值爲1000,然後999加上10之後溢出了,就變爲9了,9的數值自然比999小,但是9所代表的時間卻比999大,下面是我們要證明的。

t1與t2之間的時間差應該是

也是說t2需要經過2^n-t2時間才能溢出,溢出後需要在經過t1的時間,才能趕上t1,不是很明白的可以畫圖演示,此次就不提供圖形描述了。另外,t1與t2間的時間間隔應該是很小的,否則就不公平了。因此有如下公式成立

 

t1-t2有如下公式成立

把最高位當符號位看待,結果爲正數,則有t1>t2(雖然數值上t2>t1,但是計算結果確是t1>t2,這也正是我們所需要的)。

 

t2-t2有如下公式成立

把最高位當作符號位,結果爲負數,則有t2<t1(t2數值比t1大,但是計算結果卻是t2<t1,與運行時間遞增是一致的)。

 

2.1.3、全部溢出的情況

全部溢出的情況與2.1.1計算方法一致,做減法運算時,相當於都先加2^n-1再做減法,例如(1+1000)-(2+1000)與1-2計算是一樣的。

 

3、時間片調度相關函數解釋

3.1、update_curr

前面函數調用棧已經有介紹了,怎麼觸發更新當前進程的時間片,根據函數調用棧就可以分析出來,此次不做介紹,相關結構體變量參考源碼及其他書籍。

/*
 * Update the current task's runtime statistics.
 */
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr; // 獲取當前進程的sched_entity結構體指針
u64 now = rq_clock_task(rq_of(cfs_rq)); // 獲取當前時間(cpu時間?)
u64 delta_exec;


if (unlikely(!curr))
return;


delta_exec = now - curr->exec_start; // 當前時間-上一次計算時的開始時間,獲取當前進程已經在cpu上連續運行了多少時間
if (unlikely((s64)delta_exec <= 0)) // 什麼時候會爲負數?在此先不考慮,比較方法還是通過無符號運算,然後比較最高位,即將結果當作有符號數看待
return;

curr->exec_start = now; // 重置計算開始時間

schedstat_set(curr->statistics.exec_max,
     max(delta_exec, curr->statistics.exec_max));

curr->sum_exec_runtime += delta_exec; // 總的運行時間加上本次運行時間間隔
schedstat_add(cfs_rq, exec_clock, delta_exec);

curr->vruntime += calc_delta_fair(delta_exec, curr); // 虛擬運行時間加上本次運行時間(根據權重計算的,非物理時間,不同權重計算結果不一樣,同樣的物理時間,vruntime增加得越慢,表示該進程能運行的時間得到越多)
update_min_vruntime(cfs_rq); // 更新min_vruntime,函數裏面也是採用了第2節中的無符號數比較方法,更新時比較當前進程虛擬運行時間、rb樹最左端葉子節點、上次記錄的最小虛擬運行時間,取所有進程中的最小虛擬運行時間與上次記錄的最小虛擬運行時間中的較大者爲新的最小虛擬運行時間;所有進程中的最新虛擬運行時間,只需要比較當前進程虛擬運行時間與rb最左端葉子節點即可;如果上次記錄的最小虛擬運行時間小於所有進程的虛擬運行時間,取兩者中較大的,是合情合理的;如果上次記錄的最小虛擬運行時間大於所有進程的最小虛擬運行時間,保持最小虛擬運行時間不變,這種情況是存在的,因爲並不是所有進程都一直是就緒狀態,可能被臨時掛起,或者新創建進程,他們的運行時間並不是跟隨就緒進程變化的,阻塞狀態變就緒狀態時,該進程記錄的虛擬運行時間可能與其他進程運行時間相差很遠,有可能誤認爲運行了很長時間,有可能被插入到最末尾,最後纔得到調度,爲了儘快公平得到調度,會根據當前記錄的最小虛擬運行時間做調整,使之不會偏離最小運行時間太大,而最小虛擬運行時間通常介於所有進程虛擬運行之間,調整之後,就使得新的進程可能插入到就緒進程中間某個位置,而不是最末尾,這樣就有可能得到更快的調度;具體代碼可以搜索min_vruntime,看看新進程創建已經進程喚醒時的運行時間調整。

if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);

trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cpuacct_charge(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}

account_cfs_rq_runtime(cfs_rq, delta_exec);
}

3.2、check_preempt_tick

(檢查當前進程是否需要被搶佔;當前進程運行時間是否夠長,當前進程是否可以被下一個進程搶佔...)
/*
 * Preempt the current task with a newly woken task if needed:
 */
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
unsigned long ideal_runtime, delta_exec;
struct sched_entity *se;
s64 delta;

ideal_runtime = sched_slice(cfs_rq, curr); // 計算當前進程的理想運行時間
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime; // 計算當前調度已經運行的時間(在進程被調度時會記錄prev_sum_exec_runtime)
if (delta_exec > ideal_runtime) { // 當前進程的運行時間已經超過了理想的運行時間,強制設置重新調度標誌
resched_curr(rq_of(cfs_rq)); // 設置重新調度標誌
/*
* The current task ran long enough, ensure it doesn't get
* re-elected due to buddy favours.
*/
clear_buddies(cfs_rq, curr);
return;
}

/*
* Ensure that a task that missed wakeup preemption by a
* narrow margin doesn't have to wait for a full slice.
* This also mitigates buddy induced latencies under load.
*/
if (delta_exec < sysctl_sched_min_granularity) // 這個應該是爲了保證進程調度時間不會太短,太短頻繁切換會降低cpu利用率,因爲切換會引起其他一些開銷,例如內存切換、緩存失效以及進程切換本身需要消耗cpu等等。
return;

se = __pick_first_entity(cfs_rq); // 取下一個調度的進程(rb左節點),該進程的虛擬運行時間最短,下一個進程只可能是該節點
delta = curr->vruntime - se->vruntime; // 表達式右邊是無符號數運算,左邊是有符號數,比較大小方法在第2節講過,本次實際有兩個作用,一是比較大小,二是計算差值

if (delta < 0) // 符號位爲1,表示當前進程虛擬運行時間小於下一個可調度進程虛擬運行時間
return;

if (delta > ideal_runtime) // 兩個進程之間虛擬運行時間大於理想運行時間,重新調度當前進程;delta>0時,delta表示的是當前進程與下一進程虛擬運行時間之差
resched_curr(rq_of(cfs_rq)); // 設置重新調度標誌
}

3.3、ret_to_user_from_irq

上面的假設是在用戶態執行時發生的中斷,設置進程被搶佔、重新調度是在中斷裏面處理的,在返回到用戶態時,會檢查當前進程的搶佔標誌是否被設置(如何獲取當前進程thread_info在之前文章中有介紹),以確定是否需要重新調度;至於在內核態發生中斷,原理類似。
 
__irq_usr:
usr_entry // 中斷相關上下文保存
kuser_cmpxchg_check
irq_handler // 中斷處理(在這裏逐級調用到定時器函數,更新當前進程的虛擬運行時間,設置重新調度標誌...)
get_thread_info tsk // 獲取當前進程thread_info(裏面有進程重新調度標誌等)
mov why, #0
b ret_to_user_from_irq // 跳轉到返回用戶態的函數


ENTRY(ret_to_user)
ret_slow_syscall:
disable_irq_notrace @ disable interrupts
ENTRY(ret_to_user_from_irq)
ldr r1, [tsk, #TI_FLAGS]
tst r1, #_TIF_WORK_MASK // 檢查任務標誌(包含進程重新調度標誌在內)
bne slow_work_pending // 有設置,則跳轉到掛起任務處理函數(該函數包括進程重新調度處理、軟中斷處理等)
no_work_pending:
asm_trace_hardirqs_on save = 0


/* perform architecture specific actions before user return */
arch_ret_to_user r1, lr
ct_user_enter save = 0


restore_user_regs fast = 0, offset = 0 // 不需要重新調度,則恢復用戶寄存器,返回到用戶態等等
ENDPROC(ret_to_user_from_irq)
ENDPROC(ret_to_user)



asmlinkage int
do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
	/*
	* The assembly code enters us with IRQs off, but it hasn't
	* informed the tracing code of that for efficiency reasons.
	* Update the trace code with the current status.
	*/
	trace_hardirqs_off();
	do {
		if (likely(thread_flags & _TIF_NEED_RESCHED)) {
			schedule(); // 設置了重新調度標誌,進行進程調度,大概記錄了新進程的開始運行時間等等,然後進行進程上下文切換...... 該函數在後續文章中介紹......
		} else {
			if (unlikely(!user_mode(regs)))
				return 0;
			local_irq_enable();
			if (thread_flags & _TIF_SIGPENDING) {
				int restart = do_signal(regs, syscall); // 處理掛起的信號
				if (unlikely(restart)) {
					/*
					* Restart without handlers.
					* Deal with it without leaving
					* the kernel space.
					*/
					return restart;
				}
				syscall = 0;
			} else if (thread_flags & _TIF_UPROBE) {
				uprobe_notify_resume(regs);
			} else {
				clear_thread_flag(TIF_NOTIFY_RESUME);
				tracehook_notify_resume(regs);
			}
		}
		local_irq_disable();
		thread_flags = current_thread_info()->flags;
	} while (thread_flags & _TIF_WORK_MASK);
	return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章