背景 By 魯迅 By 高爾基 說明: 1. Kernel版本:4.14 2. ARM64處理器,Contex A53,雙核 3. 使用工具:Source Insight 3.5, Visio 1. 概述 進程切換:內核將CPU上正在運行的進程掛起,選擇下一個進程來運行。 ARM架構中,CPU上一次 ...
背景
Read the fucking source code!
--By 魯迅A picture is worth a thousand words.
--By 高爾基
說明:
- Kernel版本:4.14
- ARM64處理器,Contex-A53,雙核
- 使用工具:Source Insight 3.5, Visio
1. 概述
進程切換:內核將CPU上正在運行的進程掛起,選擇下一個進程來運行。
ARM架構中,CPU上一次只能運行一個任務,內核需要為任務分配運行時間來進行調度,以便同時能處理多個任務請求。
如下圖所示:
當進行任務切換的時候,思考下兩個問題:
- 怎樣通過搶占來實現進程的切換?
- 當進程切換的時候,到底切換的什麼,是怎麼實現的?
這兩個問題,也是本文探討的主題了。
2. 搶占
2.1 用戶搶占
2.1.1 搶占觸發點
- 可以觸發搶占的情況很多,比如進程的時間片耗盡、進程等待在某些資源上被喚醒時、進程優先順序改變等。Linux內核是通過設置
TIF_NEED_RESCHED
標誌來對進程進行標記的,設置該位則表明需要進行調度切換,而實際的切換將在搶占執行點來完成。
不看代碼來講結論,那都是耍流氓。先看一下兩個關鍵結構體:struct task_struct
和struct thread_info
。我們在前邊的文章中也講過struct task_struct
用於描述任務,該結構體的首個欄位放置的正是struct thread_info
,struct thread_info
結構體中flag
欄位就可用於設置TIF_NEED_RESCHED
,此外該結構體中的preempt_count
也與搶占相關。
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
struct thread_info thread_info;
#endif
...
}
/*
* low level task data that entry.S needs immediate access to.
*/
struct thread_info {
unsigned long flags; /* low level flags */
mm_segment_t addr_limit; /* address limit */
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
u64 ttbr0; /* saved TTBR0_EL1 */
#endif
int preempt_count; /* 0 => preemptable, <0 => bug */
};
#include <asm/current.h>
#define current_thread_info() ((struct thread_info *)current) //通過該巨集可以直接獲取thread_info的信息
#endif
看看具體哪些函數過程中,設置了TIF_NEED_RESCHED
標誌吧:
- 內核提供了
set_tsk_need_resched
函數來將thread_info
中flag
欄位設置成TIF_NEED_RESCHED
; - 設置了
TIF_NEED_RESCHED
標誌,表明需要發生搶占調度;
2.1.2 搶占執行點
用戶搶占:搶占執行發生在進程處於用戶態。
搶占的執行,最明顯的標誌就是調用了schedule()
函數,來完成任務的切換。
具體來說,在用戶態執行搶占在以下幾種情況:
- 異常處理後返回到用戶態;
- 中斷處理後返回到用戶態;
- 系統調用後返回到用戶態;
如下圖:
- ARMv8有4個Exception Level,其中用戶程式運行在EL0,OS運行在EL1,Hypervisor運行在EL2,Secure monitor運行在EL3;
- 用戶程式在執行過程中,遇到異常或中斷後,將會跳到
ENTRY(vectors)
向量表處開始執行; - 返回用戶空間時進行標誌位判斷,設置了
TIF_NEED_RESCHED
則需要進行調度切換,沒有設置該標誌,則檢查是否有收到信號,有信號未處理的話,還需要進行信號的處理操作;
2.2 內核搶占
Linux內核有三種內核搶占模型,先上圖:
- CONFIG_PREEMPT_NONE:不支持搶占,中斷退出後,需要等到低優先順序任務主動讓出CPU才發生搶占切換;
- CONFIG_PREEMPT_VOLUNTARY:自願搶占,代碼中增加搶占點,在中斷退出後遇到搶占點時進行搶占切換;
- CONFIG_PREEMPT:搶占,當中斷退出後,如果遇到了更高優先順序的任務,立即進行任務搶占;
2.2.1 搶占觸發點
- 在內核中搶占觸發點,也是設置
struct thread_info
的flag
欄位,設置TIF_NEED_RESCHED
表明需要請求重新調度。 - 搶占觸發點的幾種情況,在用戶搶占中已經分析過,不管是用戶搶占還是內核搶占,觸發點都是一致的;
2.2.2 搶占執行點
內核搶占:搶占執行發生在進程處於內核態。
總體而言,內核搶占執行點可以歸屬於兩大類:
- 中斷執行完畢後進行搶占調度;
- 主動調用
preemp_enable
或schedule
等介面的地方進行搶占調度;
2.3 preempt_count
- Linux內核中使用
struct thread_info
中的preempt_count
欄位來控制搶占。 preempt_count
的低8位用於控制搶占,當大於0時表示不可搶占,等於0表示可搶占。preempt_enable()
會將preempt_count
值減1,並判斷是否需要進行調度,在條件滿足時進行切換;preempt_disable()
會將preempt_count
值加1;
此外,preemt_count
欄位還用於判斷進程處於各類上下文以及開關控制等,如圖:
3. 上下文切換
- 進程上下文:包含CPU的所有寄存器值、進程的運行狀態、堆棧中的內容等,相當於進程某一時刻的快照,包含了所有的軟硬體信息;
- 進程切換時,完成的就是上下文的切換,進程上下文的信息會保存在每個
struct task_struct
結構體中,以便在切換時能完成恢復工作;
進程上下文切換的入口就是__schedule()
,分析也圍繞這函數展開。
3.1 __schedule()
__schedule()
函數調用分析如下:
主要的邏輯:
- 根據CPU獲取運行隊列,進而得到運行隊列當前的
task
,也就是切換前的prev
; - 根據
prev
的狀態進行處理,比如pending
信號的處理等,如果該任務是一個worker線程
還需要將其睡眠,並喚醒同CPU上的另一個worker線程
; - 根據調度類來選擇需要切換過去的下一個
task
,也就是next
; context_switch
完成進程的切換;
3.2 context_switch()
context_switch()
的調用分析如下:
核心的邏輯有兩部分:
進程的地址空間切換
:切換的時候要判斷切入的進程是否為內核線程,1)所有的用戶進程都共用一個內核地址空間,而擁有不同的用戶地址空間;2)內核線程本身沒有用戶地址空間。在進程在切換的過程中就需要對這些因素來考慮,涉及到頁表的切換,以及cache/tlb
的刷新等操作。寄存器的切換
:包括CPU的通用寄存器切換、浮點寄存器切換,以及ARM處理器相關的其他一些寄存器的切換;
進程的切換,帶來的開銷不僅是頁表切換和硬體上下文的切換,還包含了Cache/TLB
刷新後帶來的miss
的開銷。在實際的開發中,也需要去評估新增進程帶來的調度開銷。