本文基於Linux0.11操做系統的源代碼,分析其進程模型。html
Linux0.11下載地址:https://zhidao.baidu.com/share/20396e17045cc4ce24058aa43a81bf7b.htmllinux
程序是一個可執行的文件,而進程(process)是一個執行中的程序實例。數組
進程和程序的區別:session
幾個進程能夠併發的執行一個程序數據結構
一個進程能夠順序的執行幾個程序併發
進程由可執行的指令代碼、數據和堆棧區組成。進程中的代碼和數據部分分別對應一個執行文件中的代碼段、數據段。每一個進程只能執行本身的代碼和訪問本身的數據及堆棧區。進程相互之間的通訊須要經過系統調用來進行。函數
內核程序經過進程表對進程進行管理,每一個進程在進程表中佔有一項。在Linux中,進程表項是一個task_struct任務結構指針。學習
任務數據結構定義在頭文件 include/linux/sched.h中。或稱其爲進程控制塊PCB(Process Control Block)或進程描述符PD(Processor Descriptor)。spa
其中保存着用於控制和管理進程的全部信息。操作系統
內核程序使用進程標識符(process ID,PID)來標識每一個進程。
struct task_struct { ... pid_t pid;----------進程ID pid_t tgid;---------線程ID ... }
利用分時技術,在Linux操做系統上同時能夠運行多個程序。分時技術的基本原理是把CPU的運行時間劃分紅一個個規定長度的時間片,讓每一個進程在一個時間片內運行。
當進程的時間片用完時系統就利用調度程序切換到另外一個程序去運行。所以實際上對於具備單個CPU的機器來講某一時刻只能運行一個程序。
但因爲每一個進程的時間片很短,因此表面看來好像全部進程同時運行着。
一個進程在其說生存期內,可處於一組不一樣的狀態下,成爲進程狀態。進程狀態保存在進程任務結構的state字段中。
struct task_struct { /* these are hardcoded - don't touch */ long state; /* -1 unrunnable, 0 runnable, >0 stopped */
運行狀態(TASK_RUNNING)
當進程正在被CPU執行,或已經準備就緒隨時可由調度程序執行,則稱該進程爲處於運行狀態(running)。進程能夠在內核態運行,也能夠在用戶態運行。當系統資源已經可用時,進程就被喚醒而進入準備運行狀態,該狀態稱爲就緒態。這些狀態(圖中中間一列)在內核中表示方法相同,都被成爲處於TASK_RUNNING狀態。
可中斷睡眠狀態(TASK_INTERRUPTIBLE)
當進程處於可中斷等待狀態時,系統不會調度該進行執行。當系統產生一箇中斷或者釋放了進程正在等待的資源,或者進程收到一個信號,均可以喚醒進程轉換到就緒狀態(運行狀態)。
不可中斷睡眠狀態(TASK_UNINTERRUPTIBLE)
與可中斷睡眠狀態相似。但處於該狀態的進程只有被使用wake_up()函數明確喚醒時才能轉換到可運行的就緒狀態。
暫停狀態(TASK_STOPPED)
當進程收到信號SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU時就會進入暫停狀態。可向其發送SIGCONT信號讓進程轉換到可運行狀態。
僵死狀態(TASK_ZOMBIE)
當進程已中止運行,但其父進程尚未詢問其狀態時,則稱該進程處於僵死狀態。
當進程的時間片用完時系統就利用調度程序切換到另外一個程序去運行。若是進程在內核態執行時須要等待系統的某個資源,此時該進程就會調用sleep_on()或sleep_on_interruptible()放棄CPU的使用權,進入睡眠狀態,調度程序就會去執行其餘進程。
extern void sleep_on (struct task_struct **p); // 可中斷的等待睡眠。( kernel/sched.c, 167 )
對於Linux0.11內核來說,系統最多可有64個進程同時存在,除了第一個進程是「手工」創建之外,其他的都是進程使用系統調用fork建立的新進程,被建立的進程稱爲子進程(child process),建立者稱爲父進程(parent process)。
在boot/目錄中引導程序把內核加載到內存中,並讓系統進入保護模式下運行後,就開始執行系統初始化程序init/main.c。該程序會進行一些操做使系統各部分處於可運行狀態。
此後程序把本身「手工」移動到進程0中運行,並使用fork()調用首次建立出進程1。
「移動到任務0中執行」這個過程由宏move_to_user_mode()include/asm/system.h完成。
//// 切換到用戶模式運行。 // 該函數利用iret 指令實現從內核模式切換到用戶模式(初始任務0)。 #define move_to_user_mode() \ _asm { \ _asm mov eax,esp /* 保存堆棧指針esp 到eax 寄存器中。*/\ _asm push 00000017h /* 首先將堆棧段選擇符(SS)入棧。*/\ _asm push eax /* 而後將保存的堆棧指針值(esp)入棧。*/\ _asm pushfd /* 將標誌寄存器(eflags)內容入棧。*/\ _asm push 0000000fh /* 將內核代碼段選擇符(cs)入棧。*/\ _asm push offset l1 /* 將下面標號l1 的偏移地址(eip)入棧。*/\ _asm iretd /* 執行中斷返回指令,則會跳轉到下面標號1 處。*/\ _asm l1: mov eax,17h /* 此時開始執行任務0,*/\ _asm mov ds,ax /* 初始化段寄存器指向本局部表的數據段。*/\ _asm mov es,ax \ _asm mov fs,ax \ _asm mov gs,ax \ }
Linux進程是搶佔式的。被搶佔的進程仍然處於task_running狀態,只是暫時沒有被CPU運行。進程的搶佔發生在進程處於用戶態執行階段,在內核態執行時是不能被搶佔的。
爲了能讓進程有效地使用系統資源,又能使進程有較快的響應時間,就須要對進程的切換調度採用必定的調度策略。在Linux0.11操做系統中採用了基於優先級排隊的調度策略。
Schedule()函數首先掃描任務數組。
void schedule (void) { int i, next, c; struct task_struct **p; // 任務結構指針的指針。 /* 檢測alarm(進程的報警定時值),喚醒任何已獲得信號的可中斷任務 */ // 從任務數組中最後一個任務開始檢測alarm。 for (p = &LAST_TASK; p > &FIRST_TASK; --p) if (*p) { // 若是任務的alarm 時間已通過期(alarm<jiffies),則在信號位圖中置SIGALRM 信號,而後清alarm。 // jiffies 是系統從開機開始算起的滴答數(10ms/滴答)。定義在sched.h 第139 行。 if ((*p)->alarm && (*p)->alarm < jiffies) { (*p)->signal |= (1 << (SIGALRM - 1)); (*p)->alarm = 0; } // 若是信號位圖中除被阻塞的信號外還有其它信號,而且任務處於可中斷狀態,則置任務爲就緒狀態。 // 其中'~(_BLOCKABLE & (*p)->blocked)'用於忽略被阻塞的信號,但SIGKILL 和SIGSTOP 不能被阻塞。 if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && (*p)->state == TASK_INTERRUPTIBLE) (*p)->state = TASK_RUNNING; //置爲就緒(可執行)狀態。 } /* 這裏是調度程序的主要部分 */ while (1) { c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS]; // 這段代碼也是從任務數組的最後一個任務開始循環處理,並跳過不含任務的數組槽。比較每一個就緒 // 狀態任務的counter(任務運行時間的遞減滴答計數)值,哪個值大,運行時間還不長,next 就 // 指向哪一個的任務號。 while (--i) { if (!*--p) continue; if ((*p)->state == TASK_RUNNING && (*p)->counter > c) c = (*p)->counter, next = i; } // 若是比較得出有counter 值大於0 的結果,則退出124 行開始的循環,執行任務切換(141 行)。 if (c) break; // 不然就根據每一個任務的優先權值,更新每個任務的counter 值,而後回到125 行從新比較。 // counter 值的計算方式爲counter = counter /2 + priority。[右邊counter=0??] for (p = &LAST_TASK; p > &FIRST_TASK; --p) if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } switch_to (next); // 切換到任務號爲next 的任務,並運行之。 }
經過比較每一個就緒態(task_running)任務的運行時間遞減計數counter的值來肯定哪一個進程運行的時間最少,選擇該進程運行。
若是此時全部處於task_running狀態進程的時間片都已經用完,系統就會根據進程的優先權值priority,對系統中全部(包括正在睡眠)進程從新計算每一個任務須要運行的時間片值counter。
計算的公式是
而後schedule()函數從新掃描任務數組中全部處於task_running狀態任務,重複上述過程,直到選擇出一個進程爲止。
最後調用switch_to()執行實際的進程切換操做。若是此時沒有其它進程可運行,系統就會選擇進程0運行。
當一個進程結束了運行或者在半途中終止了運行,那麼內核就須要釋放改進程所佔用的系統資源。
用戶程序調用exit()系統調用時,執行內核函數do_exit()。
//// 程序退出處理程序。在系統調用的中斷處理程序中被調用。 int do_exit (long code) // code 是錯誤碼。 { int i; // 釋放當前進程代碼段和數據段所佔的內存頁(free_page_tables()在mm/memory.c,105 行)。 free_page_tables (get_base (current->ldt[1]), get_limit (0x0f)); free_page_tables (get_base (current->ldt[2]), get_limit (0x17)); // 若是當前進程有子進程,就將子進程的father 置爲1(其父進程改成進程1)。若是該子進程已經 // 處於僵死(ZOMBIE)狀態,則向進程1 發送子進程終止信號SIGCHLD。 for (i = 0; i < NR_TASKS; i++) if (task[i] && task[i]->father == current->pid) { task[i]->father = 1; if (task[i]->state == TASK_ZOMBIE) /* assumption task[1] is always init */ (void) send_sig (SIGCHLD, task[1], 1); } // 關閉當前進程打開着的全部文件。 for (i = 0; i < NR_OPEN; i++) if (current->filp[i]) sys_close (i); // 對當前進程工做目錄pwd、根目錄root 以及運行程序的i 節點進行同步操做,並分別置空。 iput (current->pwd); current->pwd = NULL; iput (current->root); current->root = NULL; iput (current->executable); current->executable = NULL; // 若是當前進程是領頭(leader)進程而且其有控制的終端,則釋放該終端。 if (current->leader && current->tty >= 0) tty_table[current->tty].pgrp = 0; // 若是當前進程上次使用過協處理器,則將last_task_used_math 置空。 if (last_task_used_math == current) last_task_used_math = NULL; // 若是當前進程是leader 進程,則終止全部相關進程。 if (current->leader) kill_session (); // 把當前進程置爲僵死狀態,並設置退出碼。 current->state = TASK_ZOMBIE; current->exit_code = code; // 通知父進程,也即向父進程發送信號SIGCHLD -- 子進程將中止或終止。 tell_father (current->father); schedule (); // 從新調度進程的運行。 return (-1); /* just to suppress warnings */ }
若是進程有子進程,則讓init進程做爲其因此子進程的父進程。
而後把進程狀態設置爲僵死狀態task_zombie。並向其原父進程發送SIGCHLD信號,通知其某個子進程已經終止。
在進程被終止是,它的任務數據結構仍然保留着。由於其父進程還須要使用其中的信息。
在子進程執行期間,父進程一般使用wait()或waitpid()函數等待某個子進程終止。
當等待的子進程被終止並處於僵死狀態時,父進程就會把子進程運行所使用的時間累加到本身進程中,釋放子進程任務數據結構。
正如Linux系統創始人在一篇新聞組投稿上所說的,要理解一個軟件系統的真正運行機制,必定要閱讀其源代碼。
但因爲目前Linux內核整個源代碼的大小已經很是得大(例如2.2.20版具備268萬行代碼!!)因此本文基於Linux0.11操做系統的源代碼,分析其進程模型。
雖然所選擇的版本較低,各方面都有很大的提高空間,但該內核已可以正常編譯運行,其中已經包括了Linux工做原理的精髓,與目前Linux內核基本功能較爲相近,源代碼又很是短小精幹,所以會有極高的學習效率,可以作到事半功倍,快速入門。