簡(jiǎn)單講解Linux進(jìn)程調(diào)度器-進(jìn)程切換
說明:
Kernel版本:4.14
ARM64處理器,Contex-A53,雙核
使用工具:Source Insight 3.5, Visio
1. 概述
進(jìn)程切換:內(nèi)核將CPU上正在運(yùn)行的進(jìn)程掛起,選擇下一個(gè)進(jìn)程來運(yùn)行。ARM架構(gòu)中,CPU上一次只能運(yùn)行一個(gè)任務(wù),內(nèi)核需要為任務(wù)分配運(yùn)行時(shí)間來進(jìn)行調(diào)度,以便同時(shí)能處理多個(gè)任務(wù)請(qǐng)求。如下圖所示:

當(dāng)進(jìn)行任務(wù)切換的時(shí)候,思考下兩個(gè)問題:
怎樣通過搶占來實(shí)現(xiàn)進(jìn)程的切換?
當(dāng)進(jìn)程切換的時(shí)候,到底切換的什么,是怎么實(shí)現(xiàn)的?
這兩個(gè)問題,也是本文探討的主題了。
2. 搶占
2.1 用戶搶占
2.1.1 搶占觸發(fā)點(diǎn)
可以觸發(fā)搶占的情況很多,比如進(jìn)程的時(shí)間片耗盡、進(jìn)程等待在某些資源上被喚醒時(shí)、進(jìn)程優(yōu)先級(jí)改變等。Linux內(nèi)核是通過設(shè)置
TIF_NEED_RESCHED
標(biāo)志來對(duì)進(jìn)程進(jìn)行標(biāo)記的,設(shè)置該位則表明需要進(jìn)行調(diào)度切換,而實(shí)際的切換將在搶占執(zhí)行點(diǎn)來完成。
不看代碼來講結(jié)論,那都是耍流氓。先看一下兩個(gè)關(guān)鍵結(jié)構(gòu)體:struct task_struct
和struct thread_info
。我們?cè)谇斑叺奈恼轮幸仓v過struct task_struct
用于描述任務(wù),該結(jié)構(gòu)體的首個(gè)字段放置的正是struct thread_info
,struct thread_info
結(jié)構(gòu)體中flag
字段就可用于設(shè)置TIF_NEED_RESCHED
,此外該結(jié)構(gòu)體中的preempt_count
也與搶占相關(guān)。
看看具體哪些函數(shù)過程中,設(shè)置了TIF_NEED_RESCHED
標(biāo)志吧:

內(nèi)核提供了
set_tsk_need_resched
函數(shù)來將thread_info
中flag
字段設(shè)置成TIF_NEED_RESCHED
;設(shè)置了
TIF_NEED_RESCHED
標(biāo)志,表明需要發(fā)生搶占調(diào)度;
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ?


2.1.2 搶占執(zhí)行點(diǎn)
用戶搶占:搶占執(zhí)行發(fā)生在進(jìn)程處于用戶態(tài)。搶占的執(zhí)行,最明顯的標(biāo)志就是調(diào)用了schedule()
函數(shù),來完成任務(wù)的切換。具體來說,在用戶態(tài)執(zhí)行搶占在以下幾種情況:
異常處理后返回到用戶態(tài);
中斷處理后返回到用戶態(tài);
系統(tǒng)調(diào)用后返回到用戶態(tài);
如下圖:

ARMv8有4個(gè)Exception Level,其中用戶程序運(yùn)行在EL0,OS運(yùn)行在EL1,Hypervisor運(yùn)行在EL2,Secure monitor運(yùn)行在EL3;
用戶程序在執(zhí)行過程中,遇到異?;蛑袛嗪?,將會(huì)跳到
ENTRY(vectors)
向量表處開始執(zhí)行;返回用戶空間時(shí)進(jìn)行標(biāo)志位判斷,設(shè)置了
TIF_NEED_RESCHED
則需要進(jìn)行調(diào)度切換,沒有設(shè)置該標(biāo)志,則檢查是否有收到信號(hào),有信號(hào)未處理的話,還需要進(jìn)行信號(hào)的處理操作;
2.2 內(nèi)核搶占
Linux內(nèi)核有三種內(nèi)核搶占模型,先上圖:

CONFIG_PREEMPT_NONE:不支持搶占,中斷退出后,需要等到低優(yōu)先級(jí)任務(wù)主動(dòng)讓出CPU才發(fā)生搶占切換;
CONFIG_PREEMPT_VOLUNTARY:自愿搶占,代碼中增加搶占點(diǎn),在中斷退出后遇到搶占點(diǎn)時(shí)進(jìn)行搶占切換;
CONFIG_PREEMPT:搶占,當(dāng)中斷退出后,如果遇到了更高優(yōu)先級(jí)的任務(wù),立即進(jìn)行任務(wù)搶占;
2.2.1 搶占觸發(fā)點(diǎn)
在內(nèi)核中搶占觸發(fā)點(diǎn),也是設(shè)置
struct thread_info
的flag
字段,設(shè)置TIF_NEED_RESCHED
表明需要請(qǐng)求重新調(diào)度。搶占觸發(fā)點(diǎn)的幾種情況,在用戶搶占中已經(jīng)分析過,不管是用戶搶占還是內(nèi)核搶占,觸發(fā)點(diǎn)都是一致的;
2.2.2 搶占執(zhí)行點(diǎn)
內(nèi)核搶占:搶占執(zhí)行發(fā)生在進(jìn)程處于內(nèi)核態(tài)。

總體而言,內(nèi)核搶占執(zhí)行點(diǎn)可以歸屬于兩大類:
中斷執(zhí)行完畢后進(jìn)行搶占調(diào)度;
主動(dòng)調(diào)用
preemp_enable
或schedule
等接口的地方進(jìn)行搶占調(diào)度;
2.3 preempt_count
Linux內(nèi)核中使用
struct thread_info
中的preempt_count
字段來控制搶占。preempt_count
的低8位用于控制搶占,當(dāng)大于0時(shí)表示不可搶占,等于0表示可搶占。preempt_enable()
會(huì)將preempt_count
值減1,并判斷是否需要進(jìn)行調(diào)度,在條件滿足時(shí)進(jìn)行切換;preempt_disable()
會(huì)將preempt_count
值加1;
此外,preemt_count
字段還用于判斷進(jìn)程處于各類上下文以及開關(guān)控制等,如圖:

3. 上下文切換
進(jìn)程上下文:包含CPU的所有寄存器值、進(jìn)程的運(yùn)行狀態(tài)、堆棧中的內(nèi)容等,相當(dāng)于進(jìn)程某一時(shí)刻的快照,包含了所有的軟硬件信息;
進(jìn)程切換時(shí),完成的就是上下文的切換,進(jìn)程上下文的信息會(huì)保存在每個(gè)
struct task_struct
結(jié)構(gòu)體中,以便在切換時(shí)能完成恢復(fù)工作;
進(jìn)程上下文切換的入口就是__schedule()
,分析也圍繞這函數(shù)展開。
3.1?__schedule()
__schedule()
函數(shù)調(diào)用分析如下:

主要的邏輯:
根據(jù)CPU獲取運(yùn)行隊(duì)列,進(jìn)而得到運(yùn)行隊(duì)列當(dāng)前的
task
,也就是切換前的prev
;根據(jù)
prev
的狀態(tài)進(jìn)行處理,比如pending
信號(hào)的處理等,如果該任務(wù)是一個(gè)worker線程
還需要將其睡眠,并喚醒同CPU上的另一個(gè)worker線程
;根據(jù)調(diào)度類來選擇需要切換過去的下一個(gè)
task
,也就是next
;context_switch
完成進(jìn)程的切換;
3.2?context_switch()
context_switch()
的調(diào)用分析如下:

核心的邏輯有兩部分:
進(jìn)程的地址空間切換
:切換的時(shí)候要判斷切入的進(jìn)程是否為內(nèi)核線程,1)所有的用戶進(jìn)程都共用一個(gè)內(nèi)核地址空間,而擁有不同的用戶地址空間;2)內(nèi)核線程本身沒有用戶地址空間。在進(jìn)程在切換的過程中就需要對(duì)這些因素來考慮,涉及到頁表的切換,以及cache/tlb
的刷新等操作。寄存器的切換
:包括CPU的通用寄存器切換、浮點(diǎn)寄存器切換,以及ARM處理器相關(guān)的其他一些寄存器的切換;
進(jìn)程的切換,帶來的開銷不僅是頁表切換和硬件上下文的切換,還包含了Cache/TLB
刷新后帶來的miss
的開銷。在實(shí)際的開發(fā)中,也需要去評(píng)估新增進(jìn)程帶來的調(diào)度開銷。
原文作者:LoyenWang
