2018-2019-1 20189206 《Linux內核原理與分析》第九周作業
#linux內核分析學習筆記 ——第八章 進程的切換和系統的一般執行過程
學習目標:重點關註進程切換的過程,進程調度的時機,操作系統的基本構成以及一般的執行過程。
進程調度的時機
因為進程的調度只發生在內核中,進程調度函數schedule()只能在內核中被調用,用戶進程無法調用。 因此,進程切換需要用到實現用戶態到內核態的切換。
除了主動讓出CPU外,進程的調度都需要在進程外進行。中斷可以實現切出進程指令流的作用,中斷處理程序是與進程無關的內核指令流。
硬中斷與軟中斷
Intel定義的中斷有以下幾種:
- 硬中斷
- CPU的兩根引腳(可屏蔽中斷和不可屏蔽中斷)。CPU在執行每條指令後檢測引腳的電平
- 一般外設都是以這種方式與CPU進行信號傳遞
- 軟中斷
- 包括零錯誤、系統調用、調試斷點等,在CPU執行指令過程中發生的各種特殊情況,稱為異常。
- 異常可以分為以下3種:
- 故障 出現問題,但是可以恢復到當前指令
- 退出 是不可恢復的嚴重故障,導致程序無法繼續運行
- 陷阱 程序主動產生的異常,在執行當前指令後發生。比如系統調用(int 0x80)等。
進程調度時機
schedule函數
linux內核通過schedule函數實現進程調度,schedule函數在運行隊列中找到一個進程。把CPU分配給他。每調用一次schedule函數就實現一次進程調度,調用schedule函數就是進程調度的時機。
調用schedule函數的兩種方式:
- 進程主動調用schedule() 進程調用阻塞的系統調用等待外設或主動睡眠,最終會在內核中調用到schedule函數。
- 松散調用,內核代碼中可以隨時調用schedule函數使當前內核路徑讓出CPU;也會根據
need_resched
標記做進程調度,內核檢測到need_resched
決定是否調用schedule函數。
上下文
CPU在任何時刻都處於以下3種情況之一:
- 運行於用戶空間,執行用戶進程上下文。
- 運行於內核空間,處於進程上下文。
- 運行於內核空間,處於中斷上下文。
中斷上下文中的get_current
可以獲取一個指向當前進程的指針,指向被中斷進程或即將運行的進程。硬件上下文切換信息也存儲於該進程的內核堆棧中。
進程調度的時機
進程調度的時機就是內核調用schedule函數的時機。當內核即將返回用戶空間時,內核會檢查need_resched標誌是否設置。如果設置,則調用schedule函數,將從中斷處理程序返回用戶空間的時間點作為一個固定的調度時機點。
- 用戶進程通過特定的系統調用主動讓出CPU
- 中斷處理程序在內核返回用戶態時進行調度
- 內核線程主動調用schedule函數讓出CPU
- 中斷處理程序主動調用schedule函數讓出CPU(包括以上兩點)
調度策略與算法
- 調度策略 根據算法的整體目標,是追求資源利用率最高還是追求相應及時或者其他目標,滿足需要的目標就是調度策略
- 調度算法 如何實現調度策略的方法並滿足設定的目標
進程的分類
第一種分類方法
- I/O消耗型進程 需要大量文件讀寫操作或者網絡讀寫操作的進程。這一類進程的特點是CPU負載不高,大量時間都在等待讀寫數據
- 處理器消耗型進程 這種進程CPU的占用率是100%,但沒有太多的硬件進行讀寫操作。
第二種分類
- 交互式進程 這類進程有大量的人機交互,進程不斷地處於睡眠狀態,等待用戶輸入
- 批處理進程 不需要人機交互,在後臺運行,但需要大量的系統資源
- 實時進程 對調度延遲要求較高,這些進程往往執行非常重要的操作,要求立即相應並執行
調度策略
進程的分類
- SCHED_FIFO 先進先出的實時進程,如果沒有其它更高優先級的可運行實時進程,就可以一直使用cpu運行。對於這種進程,時間片長度是沒有意義的。
- SCHED_RR 時間片輪轉的實時進程,所具有相同優先級(且都是當前情況下優先級最高)的SCHED_RR以時間片輪轉的方式公平使用cpu。
- SCHED_NORMAL 時間片輪轉的普通進程,時間片用完之後變成過期進程,所有進程都成為過期進程之後,再統一把過期進程轉變為活動進程。
- 時間片輪轉的普通進程(SCHED_NORMAL)優先級分為靜態優先級和動態優先級。內核用從100(最高優先級)到139(最低優先級)表示普通進程的靜態優先級。新進程總是繼承父進程的靜態優先級。
CFS調度算法 完全公平算法
- 基本原理
- 基於權重的動態優先級調度算法
- 調度順序由CPU的虛擬時間(vruntime)已使用的虛擬時間越少,進程排序就越靠前,進程再次被調度執行的概率更高。
- 調度周期(_sched_period)
某個時間周期內隊列的所有進程都會至少被調度一次
__sched_period = nr_running(進程數)*sysctl_sched_min_granularity(默認值)
理論運行時間
ideal_runtime = __sched_period * 進程權重/隊列運行總權重 每次可獲取CPU後最長可占用時間為ideal_runtime
虛擬運行時間
每個進程擁有一個vruntime,每次需要調度時就運行隊列中擁有最小的vruntime的進程來運行,最長課運行時間為ideal_runtime
vruntime = 實際運行時間 * NICE_0_LOAD / 進程權重
= 實際運行時間 * 1024 / 進程權重
NICE_0_LOAD = 1024, 表示nice值為0的進程權重
可以看到, 進程權重越大, 運行同樣的實際時間, vruntime增長的越慢
vruntime = 進程在一個調度周期內的實際運行時間 * 1024 / 進程權重
= (調度周期 * 進程權重 / 所有進程總權重) * 1024 / 進程權重
= 調度周期 * 1024 / 所有進程總權重
進程上下文的切換
內核需要有能力掛起正在CPU中運行的進程,並恢復執行以前掛起的進程,這個行為稱為進程切換。進程切換中,掛起的CPU上執行的進程與中斷時保存現場是不同的,中斷前後在同一個進程上下文中只是用戶態轉向內核態。
進程上下文包含了:
- 用戶地址空間:進程代碼、數據和用戶堆棧等
- 控制信息:進程描述符、內核堆棧
- 硬件上下文:存儲相關寄存器的值
實際代碼中進程切換由兩個步驟組成:
- 切換頁全局目錄(RC3)以安裝一個新的地址空間,這樣不同的虛擬地址可以經過不同的頁表轉為不同的物理地址。
- 切換內核態堆棧和硬件上下文
核心代碼
進程切換的函數schedule()
來選擇一個新的進程來運行,並調用context_switch
進行上下文的切換。在context_switch
中最重要的是宏switch_to
進行硬件上下文的切換。
以下是context_switch
的關鍵代碼
switch_mm
表示將下一進程的頁表地址裝入RC3,得到物理地址struct_mm
是內存描述符,其中存儲了進程地址空間中的所有信息
以下是匯編的switch_to
代碼
pushfl
pushl %ebp
註意,因為現在esp還在A的堆棧中,所以這兩個東西被保存到A進程的內核堆棧中。
pushl %%ebp 當前堆棧的棧基址保存
movl %%esp, %[prev_sp] 保存當前堆棧的棧頂
movl %[next_sp],%%esp 將esp指向next進程的棧頂
從這個時候開始,CPU當前執行的進程已經是next進程了,因為esp已經指向next的內核堆棧。
上面三行匯編代碼,實現的功能是完成內核堆棧的切換
movl $1f,%[prev_ip]
pushl %[next_ip]
jmp __switch_to
這三行代碼使用到了next進程的內核堆棧,但實際上還是在prev內核堆棧上執行。
1:
popl %%ebp
popf1
這三行代碼才是next進程開始執行的真正位置。
如果之前B也被switch_to出去過,那麽[next_ip]裏存的就是下面這個1f的標號,但如果進程B剛剛被創建,之前沒有被switch_to出去過,那麽[next_ip]裏存的將是ret_ftom_fork。
這部分代碼是內核代碼,它們跟用戶代碼不在同一個代碼段,所有進程在內核態共用這一段內核代碼。這裏涉及到的所有堆棧都是內核堆棧,而不涉及用戶堆棧。
詳細參考博客switch_to執行詳解
進程調度的源碼分析
首先還是凍結MenuOS,隨後打開調試,分別設置斷點。
gdb
file linux-3.18.6/vmlinux
target remote:1234
b schedule
b context_switch
b switch_to
b pick_next_task
開始執行後,可以看到,代碼停在了schedule
處
之後可以看到context_switch
和pick_next_task
linux系統架構
linux操作系統的整體架構如圖所示:
從下向上每一層分別是 底層硬件 --> 內核實現 --> 系統調用接口 -->基礎軟件(shell 共享庫等) --> 用戶級的應用程序
內核向上為用戶系統調用提供接口,向下調用硬件服務接口。
2018-2019-1 20189206 《Linux內核原理與分析》第九周作業