2018-2019-1 20189203《Linux核心原理與分析》第九周作業
第一部分 課本學習
程序的切換和系統的一般執行過程
程序排程的時機
Linux核心系統通過schedule函式實現程序排程,程序排程的時機就是核心呼叫schedule函式的時機。當核心即將返回使用者空間時,核心會檢查need_resched標誌是否設定。如果設定,則呼叫schedule函式,此時是從中斷(異常/系統呼叫)處理程式返回使用者空間的時間點作為一個固定的排程時機點。
簡單總結程序排程時機如下:
使用者程序通過特定的系統呼叫主動讓出CPU
中斷處理程式在核心返回使用者態時進行排程。
核心執行緒主動呼叫schedule函式讓出CPU。
中斷處理程式主動呼叫schedule函式讓出CPU,涵蓋以上第一種和第二種情況。排程策略與演算法
排程策略:Linux系統中常用的幾種排程策略為SCHED_NORMAL、SCHED_FIFO、SCHED_RR。其中SCHED_NORMAL是用於普通程序的排程類,而SCHED_FIFO和SCHED_RR是用於實時程序的排程類,優先順序高於SCHED_NORMAL。核心中根據程序的優先順序來區分普通程序和實時程序,Linux核心程序優先順序為0~139,數值越高,優先順序越低,0為最高有限級。實時程序的優先順序取值為0~99;而普通程序只具有nice值,nice值對映到優先順序為100~139。子程序會繼承父程序的優先順序。
CFS排程演算法:CFS即為完全公平排程演算法,其基本原理是基於權重的動態優先順序排程演算法。每個程序使用CPU的順序由程序已使用的CPU虛擬時間(vruntime)決定,已使用的虛擬時間越少,程序排序就越靠前,程序再次被排程執行的概率就越高。每個程序每次佔用CPU後能夠執行的時間(ideal_runtime)由程序的權重決定,並且保證在某個時間週期(__sched_period)內執行佇列裡的所有程序都能夠至少被排程執行一次。程序上下文切換
在實際程式碼中,每個程序切換基本由兩個步驟組成。
切換頁全域性目錄(CR3)以安裝一個新的地址空間,這樣不同程序的虛擬地址如0x8048400就會經過不同的頁錶轉換為不同的實體地址。
切換核心態堆疊和硬體上下文,因為硬體上下文提供了核心執行新程序所需要的所有資訊,包含CPU暫存器狀態Linux系統的執行過程
最基本和一般場景是:正在執行的使用者態程序X切換到使用者態程序Y的過程。具體過程如下。
1、 正在執行的使用者態程序X。
2、發生中斷(包括異常、系統呼叫等)。
3、SAVE_ALL,儲存現場,此時完成了中斷上下文切換,即從程序X的使用者態到程序X的核心態。
4、中斷處理過程中或中斷返回前呼叫了schedule函式,其中的switch_to做了關鍵的程序上下文切換。
5、標號1,之後開始執行使用者態程序Y(這裡Y曾經通過以上步驟被切換出去過因此可以從標號1繼續執行)。
6、restore_all,恢復現場,與3中儲存現場相對應。
7、iret - pop cs:eip/ss:esp/eflags,從Y程序的核心堆疊中彈出2中硬體完成的壓棧內容。此時完成了中斷上下文的切換,即從程序Y的核心態返回到程序Y的使用者態。
8、繼續執行使用者態程序Y。Linux系統架構與執行過程概覽
Linux系統的整體架構如圖所示:
Ls命令執行過程示意圖如下:
第二部分 程式碼分析
- context_switch程式碼
static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);
if (!mm) { //如果被切換進來的程序的mm為空切換,核心執行緒mm為空
next->active_mm = oldmm; //將共享切換出去的程序的active_mm
atomic_inc(&oldmm->mm_count); //有一個程序共享,所有引用計數加一
enter_lazy_tlb(oldmm, next); //普通mm不為空,則呼叫switch_mm切換地址空間
} else
switch_mm(oldmm, mm, next);
if (!prev->mm) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler it's an obvious special-case), so we
* do an early lockdep release here:
*/
spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
context_tracking_task_switch(prev, next);
// 這裡切換暫存器狀態和棧
switch_to(prev, next, prev);
barrier();
/*
* this_rq must be evaluated again because prev may have moved
* CPUs since it called schedule(), thus the 'rq' on its stack
* frame will be invalid.
*/
finish_task_switch(this_rq(), prev);
}
- switch_to程式碼
#define switch_to(prev, next, last) //prev指向當前程序,next指向被排程的程序
do {
unsigned long ebx, ecx, edx, esi, edi;
asm volatile("pushfl\n\t" //把prev程序的flag儲存到prev程序的核心堆疊中
"pushl %%ebp\n\t" //把prev程序的基址ebp儲存到prev程序的核心堆疊中
"movl %%esp,%[prev_sp]\n\t"//儲存ESP
"movl %[next_sp],%%esp\n\t"//更新ESP,將下一棧頂儲存到ESP中
"movl $1f,%[prev_ip]\n\t"//儲存當前程序EIP*
"pushl %[next_ip]\n\t"//把next程序起點壓入next程序的核心堆疊棧頂
__switch_canary
"jmp __switch_to\n"//prev程序中設定next程序堆疊
//jmp不同於call,是通過暫存器傳遞引數,而不是通過堆疊傳遞引數,所以ret時彈出的是之前壓入棧頂的next程序起點
//wancheng EIP的切換
"1:\t"
"popl %%ebp\n\t"
"popfl\n"
/* output parameters */
: [prev_sp] "=m"(prev->thread.sp), //儲存prev程序的esp
[prev_ip] "=m"(prev->thread.ip), //儲存prev程序的eip
"=a" (last),
/* clobbered output registers: */
"=b" (ebx), "=c"(ecx), "=d" (edx),
"=S" (esi), "=D"(edi)
__switch_canary_oparam
/* input parameters: */
: [next_sp] "m" (next->thread.sp), //next程序核心堆疊棧頂地址,即esp
[next_ip] "m" (next->thread.ip), //next程序的原eip
/* regparm parameters for __switch_to():*/
//jmp通過eax暫存器和edx暫存器傳遞引數
[prev] "a" (prev),
[next] "d" (next)
__switch_canary_iparam
: /* 重新載入段暫存器
"memory");
} while (0)
第三部分 實驗樓實驗
在實驗樓中配置執行MenuOS系統:
配置gdb斷點:
開始執行,分別停在各斷點處:
Schedule函式的作用非常重要,是程序排程的主體函式。其中pick_next_task函式是schedule函式中重要的函式,負責根據排程策略和排程演算法選擇下一個程序,context_switch函式是schedule函式中實現程序切換的函式,switch_to是context_switch函式中進行程序關鍵上下文切換的函式。由於switch_to內部是內嵌彙編程式碼,無法跟蹤除錯。