Linux進程管理

進程執行操作系統中的任務。程序是存放在磁盤上的包括一系列機器代碼指令和數據的可執行的映像,因此,是一個被動的實體。進程可以看作是一個執行中的計算機程序。它是動態的實體,在處理器執行機器代碼指令時不斷改變。處理程序的指令和數據,進程也包括程序計數器和其他CPU的寄存器以及包括臨時數據(例如例程參數、返回地址和保存的變量)的堆棧。當前執行的程序,或者說進程,包括微處理器中所有的當前的活動。Linux是一個多進程的操作系統。進程是分離的任務,擁有各自的權利和責任。如果一個進程崩潰,它不應該讓系統中的另一個進程崩潰。每一個獨立的進程運行在自己的虛擬地址空間,除了通過安全的核心管理的機制之外無法影響其他的進程。

 

在一個進程的生命週期中它會使用許多系統資源。它會用系統的CPU執行它的指令,用系統的物理內存來存儲它和它的數據。它會打開和使用文件系統中的文件,會直接或者間接使用系統的物理設備。Linux必須跟蹤進程本身和它使用的系統資源以便管理公平地管理該進程和系統中的其他進程。如果一個進程獨佔了系統的大部分物理內存和CPU,對於其他進程就是不公平的。

 

系統中最寶貴的資源就是CPU。通常系統只有一個。Linux是一個多進程的操作系統。它的目標是讓進程一直在系統的每一個CPU上運行,充分利用CPU。如果進程數多於CPU(多數是這樣),其餘的進程必須等到CPU被釋放才能運行。多進程是一個簡單的思想:一個進程一直運行,直到它必須等待,通常是等待一些系統資源,等擁有了資源,它纔可以繼續運行。在一個單進程的系統,比如DOSCPU被簡單地設爲空閒,這樣等待的時間就會被浪費。在一個多進程的系統中,同一時刻許多進程在內存中。當一個進程必須等待時操作系統將CPU從這個進程拿走,並將它交給另一個更需要的進程。是調度程序選擇了下一次最合適的進程。Linux使用了一系列的調度方案來保證公平。

Unix標準把進程定義爲:“一個其中運行着一個或多個線程的地址空間和這些線程所需要的系統資源”
除了第一個進程是"手工"建立以外,其餘的都是進程使用系統調用fork創建的新進程,被創建的進程成爲子進程(Child Process),創建者,則稱爲父進程(parent process)。內核程序使用進程標識號(process ID,pid)來標識每個進程。進程由可執行的指令代碼,數據和堆棧區組成

進程中的代碼和數據部分分別對應一個可執行文件中的代碼段,數據段。每個進程只能執行自己的代碼和訪問自己的數據及堆棧區。進程之間相互之間的通信需要通過系統調用來進行。對於只有一個CPU的系統,在某一個時刻只能有一個進程正在運行。內核通過進程調度程序分時調度各個進程運行

  Linux系統中,一個進程可以在內核態(Kernal mode)或者用戶態(user mode)下執行,因此Linux內核堆棧和用戶堆棧是分開的。用戶堆棧用於進程在用戶態下臨時保存調用函數的參數,局部變量等數據。內核堆棧則含有內核程序執行函數調用時的信息。

進程還有自己的棧空間(用來保存函數中的局部變量和控制函數的調用和返回)。
進程有自己的環境空間,包含專門爲這個進程建立的環境變量,環境僅對進程本身有效。你在程序裏面做出的改變不會反映到外部環境中,因爲變量的值不會從子進程傳遞給父進程。
進程必須維護自己的程序計數器,這個計數器用來記錄它執行到的位置。
1任務數據結構
內核程序通過進程表對進程進行管理,每個進程在進程表中佔有一項。在Linux系統中,進程表項是一個task_struct(Linuxtaskprocess互用)任務結構指針。
Linux中,每一個進程用一個task_struct(在Linuxtaskprocess互用)的數據結構來表示,用來管理系統中的進程。Task向量表是指向系統中每一個task_struct數據結構的指針的數組。這意味着系統中最大進程數受task向量表的限制,缺省是512。當新的進程創建的時候,從系統內存中分配一個新的task_struct,並增加到task向量表中。爲了更容易查找,用current指針指向當前運行的進程。
任務數據結構定義在頭文件include/linux/sched.h中。有些書上稱其爲進程控制塊PCB(Process Control Block)或者進程描述符PD(Processor Descriptor)。其中保存着用於控制和管理進程的所有信息。主要包括進程當前運行的狀態信息,信號,進程號,父進程號,運行時間累計值,正在使用的文件和本任務的局部描述符以及任務狀態段信息。該結構每個字段的含義如下所示。
當一個進程在執行時,CPU的所有寄存器中的值,進程的狀態以及堆棧中的內容被稱爲該進程的上下文。當內核需要切換(switch)至另一個進程時,它就需要保存當前進程的所有狀態,也即保存當前進程的上下文,以便在再次執行該進程時,能夠恢復到切換時的狀態執行下去。在Linux中,當前進程上下文均保存在進程的任務數據結構task_struct中。在發生中斷時,內核就在被中斷進程的上下文中,在內核狀態下執行中斷服務例程。但同時會保留所有需要用到的資源,以便中斷服務結束時能恢復被中斷進程的執行。
2 進程運行狀態
一個進程在其生存期內,可處於一組不同的狀態下,稱爲進程狀態。見下圖2-6所示。進程狀態保存在進程任務結構的state字段中。當進程正在等待系統中的資源而處於等待狀態時,則稱其處於睡眠等待狀態。在Linux系統中,睡眠等待狀態被分爲可中斷的和不可中斷的等待狀態。

運行狀態(TASK_RUNNING)

  當進程正在被CPU執行,或已經準備就緒隨時可以由調度程序執行,則稱該進程爲處於運行狀態(running)。進程可以在內核態運行,也可以在用戶態運行。當系統資源已經可用時,進程就被喚醒而進入準備運行狀態,該狀態稱爲就緒態。這些狀態在內核中表示方法相同,都被稱爲處於TASK_RUNNING狀態。


可中斷睡眠狀態(TASK_INTERRUPTIBLE)

  當進程處於可中斷等待狀態時,系統不會調度該進程執行。當系統產生一箇中斷或者釋放了進程正在等待的資源,或者進程收到一個信號,都可以喚醒進程轉換到就緒狀態(運行狀態)。


不可中斷睡眠狀態(TASK_UNINTERRUPTIBLE)

  與可中斷睡眠狀態類似。但處於該狀態的進程只有被使用wake_up()函數明確喚醒時才能被轉換到可運行就緒狀態。


暫停狀態(TASK_STOPPED)

  當進程收到信號SIGSTOP,SIGTSTP,SIGTTIN或SIGTTOU時就會進入暫停狀態。可向其發送SIGCONT信號讓進程轉換到可運行狀態。在Linux0.11中,還爲實現對該狀態的轉換處理。處於該狀態的進程將被作爲進程終止來處理。


僵死狀態(TASK_ZOMBIE)

  當進程已停止運行,但其父進程還沒有詢問其狀態時,則稱該進程處於僵死狀態。


當一個進程的運行時間片用完,系統就會使用調度程序強制切換到其他的進程去執行。另外,如果進程在內核態執行時需要等待系統的某個資源,此時該進程就會調用sleep_on()或者sleep_on_interruptible()自願放棄CPU使用權,而讓調度程序去執行其他程序。進程則進入睡眠狀態(TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE)。

  只有當進程從"內核運行態"轉移到"睡眠狀態"時,內核纔會進行進程切換操作。在內核態下運行的進程不能被其他進程搶佔,而且一個進程不能改變另一個進程的狀態。爲了避免進程切換時造成內核數據錯誤,內核在執行臨街區代碼時禁止一切中斷。

3 進程初始化

  在boot/目錄中引導程序把內核從磁盤上加載到內存中,並讓系統進入保護模式下運行後,就開始執行系統初始化程序init/main.c。該程序首先確定如何分配使用系統物理內存,然後調用內核各部分的初始化函數分別對內存管理,中斷處理,塊設備和字符設備,進程管理以及硬盤和軟盤硬件進行初始化處理。在完成了這些操作之後,系統各部分已經處於可運行狀態。此後程序把自己"手工"移動到任務0(進程0)中運行,並使用fork()調用首次創建出進程1。在進程1種程序將繼續進行應用環境的初始化並執行shell登陸程序。而原進程0則會在系統空閒時被調度執行,此時任務0僅執行pause()系統調用,並又會調用調度函數。

  "移動到任務0種執行"這個過程由宏move_to_user_mode(include/asm/system.h)完成。它把main.c程序執行流從內核態(特權級0)移動到了用戶態(特權級3)的任務0種繼續運行。在移動之前,系統在對調度程序的初始化過程(sched_init())中,首先對任務0的運行環境進行的設置。這包括人工預先設置好 任務0數據結構各字段的值(include/linux/shed.h),在全局描述符中添入任務0的任務狀態段(TSS)描述符和局部描述符表(LDT)的段描述符,並把它們分別加載到任務寄存器tr和局部描述符表寄存器ldtr中。

  這裏需要強調的是,內核初始化是一個特殊過程,內核初始化代碼也即是任務0的代碼。從任務0數據結構中設置的初始化數據可知,任務0的代碼段和數據段的基址是0,段限長是640KB。而內核代碼段和數據段的基地址時0,段限長是16MB,因此任務0的代碼段和數據段分別包含在內核代碼段和數據段中。內核初始化程序main.c也即是任務0中的代碼,只是在移動到任務0之前系統正以內核態特權級0運行着main.c程序。宏move_to_user_mode的功能就是把運行特權級內核態的0級變換到用戶態的3級,但是仍然繼續執行原來的代碼指令流。
在移動到任務0的過程中,宏move_to_user_mode使用了中斷返回指令造成特權級改變的方法。該方法的主要思想是在對站中構築中斷返回指令需要的內容,把返回地址的段選擇符設置成任務0代碼段選擇符,其特權級爲3。此後執行中斷返回指令iret時將導致系統CPU從特權級0跳轉到外層的特權級3上運行。參見下圖所示的特權級發生變化時中斷返回堆棧結構示意圖。


         宏move_to_user_mode首先往內核堆棧中壓入任務0數據段選擇符和內核堆棧指針。然後壓入標誌寄存器內容。最後壓入任務0代碼段選擇符合執行中斷返回後需要執行的下一條指令的偏移位置。該偏移位置是iret後的一條指令處。

  當執行iret指令時,CPU把返回地址送入CS:EIP中,同時探出對站中標誌寄存器內容。由於CPU判斷出墓地代碼段的特權級是3,與當前內核態的0級不同。於是CPU會把堆棧中的堆棧段選擇符合堆棧指針彈出到SS:ESP中。由於特權級發生了變化,段寄存器DS,ES,FS和GS的值變得無效,此時CPU會把這些段寄存器清零。因此在執行了iret指令後需要重新加載這些段寄存器。此後,系統就開始特權級3運行在任務0的代碼上。所使用的用戶態堆棧還是原來在移動之前使用的堆棧。而其內核態堆棧則被指定爲其任務數據解噢股所在頁面的頂端開始(PAGE_SIZE+(long)&init_task)。由於以後在創建新進程時,需要複製任務0的任務數據結構,包括其用戶堆棧指針,因此需要任務0的用戶態堆棧在創建任務1(進程1)之前保持"乾淨"狀態。

4 創建新進程

  Linux系統中創建新進程使用fork()系統調用。所有進程都是通過複製進程0而得到的,都是進程0的子進程。

  在創建新進程的過程中,系統首先在任務數組中找出一個還沒有被任何進程使用的空項(空槽)。如果系統已經有64個進程在運行,則fork()系統調用會因爲任務數組表中沒有可用空項而出錯返回。然後系統爲新建進程在主內存區中申請一頁內存來存放其任務數據結構信息,並複製當前進程任務數據結構中所有內容作爲新進程人物數據結構的模板。爲了防止這個還未處理完成的新進程被調度函數執行,此時應該立刻將新進程狀態置爲不可中斷的等待狀態(TASK_UNINTERRUPTIBLE)。

  隨後對複製的任務數據結構進行修改。把當前進程設置爲新進程的父進程,清除信號位圖並復位新進程各統計值,並設置初始運行時間片值爲15個系統嘀嗒數(150ms)。接着根據當前進程設置任務狀態段(TSS)中各寄存器的值。由於創建進程時新進程返回值應爲0,所以需要設置tss.eax=0。新建進程內核態堆棧指針tss.esp0被設置成新進程任務數據結構所在內存頁面的頂端,而堆棧段tss.ss0被設置成內核數據段選擇符。tss.ldt被設置爲局部表描述符在GDT中所引值。如果當前進程使用了協處理器,把還需要協處理器的完整狀態保存到新進程的tss.i387結構中。

  此後系統設置新任務的代碼和數據段基址,限長並輔之當前進程內存分頁管理的頁表。如果父進程中也有文件是打開的,則應將對應文件的打開次數增1。接着在GDT中設置新任務的TSS和LDT描述符項,其中基地址信息指向新進程人物結構中的tss和ldt。最後再將新任務設置成可運行狀態並返回新進程號。

5 進程調度

  由前面描述可知,Linux進程是搶佔式的。被搶佔的進程仍然處於TASK_RUNNING狀態,只是暫時沒有被CPU運行。進程的搶佔發生在進程處於用戶態執行階段,在內核執行時是不能被搶佔的。

  爲了能讓進程有效地使用系統資源,又能使進程有較快的響應時間,就需要對進程的切換調度採用一定的調度策略。在Linux0.11中採用了基於優先級排隊的調度策略。

  調度程序:  

  schedule()函數首先掃描任務數組。通過比較每個就緒態(TASK_RUNNING)任務的運行時間遞減滴答計數counter的值來確定當前哪個進程運行時間最少。哪一個的值大,就表示運行時間還不長,於是就選中該進程,並使用任務切換宏函數切換到該進程運行。

  如果此時所有處於TASK_RUNNING狀態進程的時間片已經用完,系統就會根據每個進程的優先權值priority,對系統中所有進程(包括正在睡眠的進程)重新計算每個任務需要運行的時間片值counter。

  計算公式是:  counter= counter/2 + priority

  然後schedule()函數重新掃描人物數組中所有處於TASK_RUNNING狀態,重複上述過程,直到選擇出一個進程位置。最後調用switch_to()執行實際的進程切換操作。

  如果此時沒有其他進程可運行,系統就會選擇進程0運行。對於linux0.11來說,進程0會調用pause()把自己置爲可中斷的睡眠狀態並在此調用schedule()。不過在調度進程運行時,schedule()並不在意進程0處於什麼狀態。只要系統空閒就調度進程0運行。

  進程切換:

  執行實際進程切換的任務由switch_to()宏定義的一段彙編代碼完成。在進行切換之前,switch_to()首先檢查要切換到的進程是否就是當前進程,如果是則什麼也不做,直接退出。否則就首先把內核全局變量current置爲新任務的指針,然後長跳轉到新任務的任務狀態段TSS組成的地指處,造成CPU執行任務切換操作。此時CPU會把其所有寄存器的轉改保存到當前人物寄存器TR中TSS段選擇符所指向的當前進程任務數據結構的tss結構中,然後把新任務狀態段選擇符所指向的新任務數據結構中tss結構中的寄存器信息恢復到CPU中,系統就正式開始運行新切換的任務了。這個過程可參考下圖



6 終止進程

  當一個進程結束了運行或在半途中終止了運行,那麼內核就需要釋放該進程所佔用的系統資源。這包括進程運行時打開的文件,申請的內存等。

  當一個用戶程序調用exit()系統調用時,就會執行內核函數do_exit()。該函數會首先釋放進程代碼段和數據段佔用的內存頁面,關閉進程打開着的所有文件,對進程使用的當前工作目錄,根目錄和運行程序的i節點進行同步操作。如果進程有子進程,則讓init進程作爲其所有子進程的父進程。如果進程是一個會話頭進程並且有控制終端,則釋放控制終端,並向屬於該會話的所有進程發送掛斷信號SIGHUP,這通常會終止該會話中的所有進程。然後把進程狀態置爲僵死狀態TASK_ZOMBIE。並向其原父進程發送SIGCHILD信號,通知其某個子進程已經終止。最後do_exit()調用調度函數去執行其他進程。由此可見在進程終止時,它的task_struct任務數據結構仍然保留着。因爲其父進程還需要使用其中的信息。

  在子進程在執行期間,父進程通常使用wait()或waitpid()函數等待其某個子進程終止。當子進程被終止並處於僵死狀態時,父進程就會把子進程運行所使用的時間累加到自己進程中。最終釋放已終止子進程任務數據結構所佔用的內存頁面,並置空子進程在任務數組中佔用的指針項。

參考:

http://www.cnblogs.com/hongzg1982/articles/2112224.html
發佈了41 篇原創文章 · 獲贊 43 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章