mqy+ 原創做品轉載請註明出處 + 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000linux
操做系統原理中介紹了大量進程調度算法,這些算法從實現的角度看僅僅是從運行隊列中選擇一個新進程,選擇的過程當中運用了不一樣的策略而已。算法
對於理解操做系統的工做機制,反而是進程的調度時機與進程的切換機制更爲關鍵。shell
中斷處理過程(包括時鐘中斷、I/O中斷、系統調用和異常)中,直接調用schedule(),或者返回用戶態時根據need_resched標記調用schedule();網絡
內核線程能夠直接調用schedule()進行進程切換,也能夠在中斷處理過程當中進行調度,也就是說內核線程做爲一類的特殊的進程能夠主動調度,也能夠被動調度;架構
用戶態進程沒法實現主動調度,僅能經過陷入內核態後的某個時機點進行調度,即在中斷處理過程當中進行調度。函數
爲了控制進程的執行,內核必須有能力掛起正在CPU上執行的進程,並恢復之前掛起的某個進程的執行,這叫作進程切換、任務切換、上下文切換;spa
掛起正在CPU上執行的進程,與中斷時保存現場是不一樣的,中斷先後是在同一個進程上下文中,只是由用戶態轉向內核態執行;操作系統
進程上下文包含了進程執行須要的全部信息線程
用戶地址空間:包括程序代碼,數據,用戶堆棧等指針
控制信息:進程描述符,內核堆棧等
硬件上下文(注意中斷也要保存硬件上下文只是保存的方法不一樣)
schedule()函數選擇一個新的進程來運行,並調用context_switch進行上下文的切換,這個宏調用switch_to來進行關鍵上下文切換
next = pick_next_task(rq, prev);//進程調度算法都封裝這個函數內部
context_switch(rq, prev, next);//進程上下文切換
switch_to利用了prev和next兩個參數:prev指向當前進程,next指向被調度的進程
正在運行的用戶態進程X
發生中斷——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).
SAVE_ALL //保存現場
中斷處理過程當中或中斷返回前調用了schedule(),其中的switch_to作了關鍵的進程上下文切換
標號1以後開始運行用戶態進程Y(這裏Y曾經經過以上步驟被切換出去過所以能夠從標號1繼續執行)
restore_all //恢復現場
iret - pop cs:eip/ss:esp/eflags from kernel stack
繼續運行用戶態進程Y
操做系統:任何計算機系統包含的一個基本的程序集合
操做系統的目的
理解Linux系統中進程調度的時機,能夠在內核代碼中搜索schedule()函數,看都是哪裏調用了schedule(),判斷咱們課程內容中的總結是否準確;
使用gdb跟蹤分析一個schedule()函數 ,驗證您對Linux系統進程調度與進程切換過程的理解;推薦在實驗樓Linux虛擬機環境下完成實驗。
特別關注並仔細分析switch_to中的彙編代碼,理解進程上下文的切換機制,以及與中斷上下文切換的關係;
實驗:
1. 實驗目的
本次實驗選擇fork系統調用,其系統調用號爲:
2 i386 fork sys_fork stub32_fork
一個進程,包括代碼、數據和分配給進程的資源。fork()函數經過系統調用建立一個與原來進程幾乎徹底相同的進程,也就是兩個進程能夠作徹底相同的事,但若是初始參數或者傳入的變量不一樣,兩個進程也能夠作不一樣的事。一個進程調用fork()函數後,系統先給新的進程分配資源,例如存儲數據和代碼的空間。而後把原來的進程的全部值都複製到新的新進程中,只有少數值與原來的進程的值不一樣。至關於克隆了一個本身。
fork調用的一個奇妙之處就是它僅僅被調用一次,卻可以返回兩次,它可能有三種不一樣的返回值:
在fork函數執行完畢後,若是建立新進程成功,則出現兩個進程,一個是子進程,一個是父進程。在子進程中,fork函數返回0,在父進程中,fork返回新建立子進程的進程ID。咱們能夠經過fork返回的值來判斷當前進程是子進程仍是父進程。 引用一位網友的話來解釋fpid的值爲何在父子進程中不一樣。「其實就至關於鏈表,進程造成了鏈表,父進程的fpid(p 意味point)指向子進程的進程id, 由於子進程沒有子進程,因此其fpid爲0。
fork.c的代碼以下:
#include <unistd.h> #include <stdio.h> int main () { pid_t fpid; int count = 0; fpid = fork(); if (fpid < 0) printf("error in fork!"); else if (fpid == 0) { printf("i am the child process, my process id is %d\n",getpid()); count++; } else { printf("i am the parent process, my process id is %d\n",getpid()); count++; } printf("count: %d\n",count); return 0; }
分別用API和嵌入式彙編代碼調用fork,結果如圖:
建立新進程成功後,系統中出現兩個基本徹底相同的進程,這兩個進程執行沒有固定的前後順序,哪一個進程先執行要看系統的進程調度策略。 每一個進程都有一個獨特(互不相同)的進程標識符(process ID),能夠經過getpid()函數得到,還有一個記錄父進程pid的變量,能夠經過getppid()函數得到變量的值。 fork執行完畢後,出現兩個進程,進程1的變量爲count=0,fpid!=0(父進程)。進程2的變量爲count=0,fpid=0(子進程),這兩個進程的變量都是獨立的,存在不一樣的地址中,不是共用的,這點要注意。能夠說,咱們就是經過fpid來識別和操做父子進程的。 還有人可能疑惑爲何不是從#include處開始複製代碼的,這是由於fork是把進程當前的狀況拷貝一份,執行fork時,進程已經執行完了int count=0;fork只拷貝下一個要執行的代碼到新的進程。
下面重點分析嵌入式彙編代碼的執行,fork-asm.c源代碼以下:
#include <unistd.h> #include <stdio.h> int main () { pid_t fpid; int count = 0; asm volatile ( "mov $0, %%ebx\n\t" "mov $0x2, %%eax\n\t" "int $0x80\n\t" "mov %%eax, %0\n\t" : "=m" (fpid) ); if (fpid < 0) printf("error in fork!"); else if (fpid == 0) { printf("i am the child process, my process id is %d\n",getpid()); count++; } else { printf("i am the parent process, my process id is %d\n",getpid()); count++; } printf("count: %d\n",count); return 0; }
以上程序與fork.c的主要區別就是用asm彙編代替了fpid = fork();語句。其主要過程是:
asm volatile ( "mov $0, %%ebx\n\t" // 因爲fork函數調用不須要參數,可直接將當即數0賦值給ebx,表明NULL。沒有這條語句應該也能夠。 "mov $0x2, %%eax\n\t" // 系統調用號默認經過eax傳遞,所以將fork的系統調用號0x2賦值給eax "int $0x80\n\t" // 經過0x80中斷向量,執行系統調用。系統由eax此時的值可知,用戶請求fork調用。 "mov %%eax, %0\n\t" // 系統返回的pid號默認儲存在eax中,將eax的值賦給第一個輸出操做數,即下面的fpid。 : "=m" (fpid) // =表明操做數在指令中是隻寫的,m表明內存變量。即輸出操做數0爲內存中的fpid。 );
除了系統調用號之外,大部分系統調用都還須要一些外部的參數輸人。因此,在發生異常的時候,應該把這些參數從用戶空間傳給內核。最簡單的辦法就是像傳遞系統調用號同樣把這些參數也存放在寄存器裏。在x86系統上,ebx, ecx, edx, esi和edi按照順序存放前五個參數。須要六個或六個以上參數的狀況很少見,此時,應該用一個單獨的寄存器存放指向全部這些參數在用戶空間地址的指針。 給用戶空間的返回值也經過寄存器傳遞。在x86系統上,它存放在eax寄存器中。接下來許多關於系統調用處理程序的描述都是針對x86版本的。但不用擔憂,全部體系結構的實現都很相似。