Linux內核態、用戶態以及fork進程管理

一:內核態和用戶態

內核態:

 通常一個內核由負責響應中斷的中斷服務程序,負責管理多個進程從而分享處理器時間的調度程序,負責管理進程地址空間的內存管理程序和網絡,進程間通信等系統服務程序共同組成。其獨立於普通應用程序,一般處於系統態,擁有受保護的內存空間和訪問硬件設備的所有權限,這種系統態和被保護起來的內存空間統稱爲內核空間。

用戶態:

 應用程序在用戶空間執行,它們只能看到允許它們使用的部分系統資源,並且只能使用某些特定的系統功能,不能直接訪問硬件,也不能訪問內核劃給別人的內存範圍。
 當內核運行時,系統以內核態進入內核空間執行,而執行一個普通用戶程序時,系統將以用戶態進入用戶空間執行。
 這裏寫圖片描述
 這裏我們來簡單介紹一下x86架構中的四種CPU權限,對我們理解內核態和用戶態有幫助。
 

從特權級看用戶態和內核態

 熟悉Unix/Linux系統的人都知道,fork的工作實際上是以系統調用的方式完成相應功能的,具體的工作是由sys_fork負責實施。其實無論是不是Unix或者Linux,對於任何操作系統來說,創建一個新的進程都是屬於核心功能,因爲它要做很多底層細緻地工作,消耗系統的物理資源,比如分配物理內存,從父進程拷貝相關信息,拷貝設置頁目錄頁表等等,這些顯然不能隨便讓哪個程序就能去做,於是就自然引出特權級別的概念,顯然,最關鍵性的權力必須由高特權級的程序來執行,這樣纔可以做到集中管理,減少有限資源的訪問和使用衝突。
 特權級顯然是非常有效的管理和控制程序執行的手段,因此在硬件上對特權級做了很多支持,就Intel x86架構的CPU來說一共有0~3四個特權級,0級最高,3級最低,硬件上在執行每條指令時都會對指令所具有的特權級做相應的檢查,相關的概念有CPL、DPL和RPL,這裏不再過多闡述。硬件已經提供了一套特權級使用的相關機制,軟件自然就是好好利用的問題,這屬於操作系統要做的事情,對於Unix/Linux來說,只使用了0級特權級和3級特權級。也就是說在Unix/Linux系統中,一條工作在0級特權級的指令具有了CPU能提供的最高權力,而一條工作在3級特權級的指令具有CPU提供的最低或者說最基本權力。
 現在我們從特權級的調度來理解用戶態和內核態就比較好理解了,當程序運行在3級特權級上時,就可以稱之爲運行在用戶態,因爲這是最低特權級,是普通的用戶進程運行的特權級,大部分用戶直接面對的程序都是運行在用戶態;反之,當程序運行在0級特權級上時,就可以稱之爲運行在內核態。
 雖然用戶態下和內核態下工作的程序有很多差別,但最重要的差別就在於特權級的不同,即權力的不同。運行在用戶態下的程序不能直接訪問操作系統內核數據結構和程序。
 當我們在系統中執行一個程序時,大部分時間是運行在用戶態下的,在其需要操作系統幫助完成某些它沒有權力和能力完成的工作時就會切換到內核態。
 實際上我們可以將每個處理器在任何指定時間上的活動概括歸屬爲以下三者之一:
  (1).運行於用戶空間,執行用戶進程;
  (2).運行於內核空間,處於進程上下文,代表某個特定的進程執行;
  (3).運行於內核空間,處於中斷上下文,與任何進程無關,處理某個特定的中斷;
 那麼,就有一個問題,處於用戶態的應用程序是如何切換到內核態呢?
 用戶態切換到內核態:
  從用戶態切換到內核態的三種方式:
  (1).系統調用:首先我們都知道應用程序通過系統調用界面陷入內核,這是應用程序完成其工作的基本行爲方式;
  (2).中斷進入內核:當外圍設備完成用戶的請求操作後,會像CPU發出中斷信號,此時,CPU就會暫停執行下一條即將要執行的指令,轉而去執行中斷信號對應的處理程序,如果先前執行的指令是在用戶態下,則自然就發生從用戶態到內核態的轉換。
  (3).異常進入內核: 當CPU正在執行運行在用戶態的程序時,突然發生某些預先不可知的異常事件,這個時候就會觸發從當前用戶態執行的進程轉向內核態執行相關的異常事件,典型的如缺頁異常,異常與中斷不同,它在產生時必須考慮與處理器始時鐘同步,所以異常又稱之爲同步中斷。

注意:系統調用的本質其實也是中斷,相對於外圍設備的硬中斷,這種中斷稱爲軟中斷,這是操作系統爲用戶特別開放的一種中斷,如Linux int 80h中斷。所以,從觸發方式和效果上來看,這三種切換方式是完全一樣的,都相當於是執行了一箇中斷響應的過程。但是從觸發的對象來看,系統調用是進程主動請求切換的,而異常和硬中斷則是被動的。

大體過程如下:
 用戶空間應用程序調用庫中的函數,這些庫中的函數絕大部分都運行在用戶空間,如果該庫函數需要用到一些受保護的地址空間的程序或者需要在特權級下執行某些程序,那麼就需要切換到內核空間了,而切換到內核空間絕大部分都是由調用系統調用而觸發的。
 系統調用號是可以關聯一個系統調用的唯一標識,內核中有一個系統調用表,這個表記錄了所有已經註冊過的系統調用的列表,存儲在sys_call_table中,這個表爲每一個有效的系統調用指定了一個唯一的系統調用號。在陷入內核之前,用戶空間就把相應的系統調用號放入了eax寄存器中,一旦系統調用處理程序運行,就可以在該寄存器中得到數據。
 陷入內核的過程中還需要將系統調用需要的一些外部參數從用戶空間傳遞到內核,通常這些參數也被放在寄存器中,但當參數數目比較多時,應該用一個單獨的寄存器存放指向所有參數在用戶空間地址的指針。
 在x86系統上定義的軟中斷的中斷號爲128,通過 int .$0x80指令觸發該中斷,這條指令導致系統切換到內核態並執行第128號中斷處理程序,該程序正是系統調用處理程序叫system_call()。執行中斷處理程序具體需要進行的操作步驟有:請求中斷→響應中斷→關閉中斷→保留斷點→中斷源識別→保護現場→中斷服務子程序→恢復現場→中斷返回。
  1.請求中斷
  當某一中斷源需要CPU爲其進行中斷服務時,就輸出中斷請求信號,使中斷控制系統的中斷請求觸發器置位,向CPU請求中斷。系統要求中斷請求信號一直保持到CPU對其進行中斷響應爲止。
  2.中斷響應
  CPU對系統內部中斷源提出的中斷請求必須響應,而且自動取得中斷服務子程序的入口地址,執行中斷 服務子程序。對於外部中斷,CPU在執行當前指令的最後一個時鐘週期去查詢INTR引腳,若查詢到中斷請求信號有效,同時在系統開中斷(即IF=1)的情 況下,CPU向發出中斷請求的外設回送一個低電平有效的中斷應答信號,作爲對中斷請求INTR的應答,系統自動進入中斷響應週期。
  3.關閉中斷
  CPU響應中斷後,輸出中斷響應信號,自動將狀態標誌寄存器FR或EFR的內容壓入堆棧保護起來,然後將FR或EFR中的中斷標誌位IF與陷阱標誌位TF清零,從而自動關閉外部硬件中斷。因爲CPU剛進入中斷時要保護現場,主要涉及堆棧操作,此時不能再響應中斷,否則將造成系統混亂。
  4.保護斷點
  保護斷點就是將CS和IP/EIP的當前內容壓入堆棧保存,以便中斷處理完畢後能返回被中斷的原程序繼續執行,這一過程也是由CPU自動完成。
  5.中斷源識別
  當系統中有多箇中斷源時,一旦有中斷請求,CPU必須確定是哪一個中斷源提出的中斷請求,並由中斷控制器給出中斷服務子程序的入口地址,裝入CS與IP/EIP兩個寄存器。CPU轉入相應的中斷服務子程序開始執行。
  6.保護現場
  主程序和中斷服務子程序都要使用CPU內部寄存器等資源,爲使中斷處理程序不破壞主程序中寄存器的內容,應先將斷點處各寄存器的內容壓入堆棧保護起來,再進入的中斷處理。現場保護是由用戶使用PUSH指令來實現的。
  7.中斷服務
  中斷服務是執行中斷的主體部分,不同的中斷請求,有各自不同的中斷服務內容,需要根據中斷源所要完成的功能,事先編寫相應的中斷服務子程序存入內存,等待中斷請求響應後調用執行。
  8.恢復現場
  當中斷處理完畢後,用戶通過POP指令將保存在堆棧中的各個寄存器的內容彈出,即恢復主程序斷點處寄存器的原值。
  9.中斷返回
  在中斷服務子程序的最後要安排一條中斷返回指令IRET,執行該指令,系統自動將堆棧內保存的 IP/EIP和CS值彈出,從而恢復主程序斷點處的地址值,同時還自動恢復標誌寄存器FR或EFR的內容,使CPU轉到被中斷的程序中繼續執行。
  這裏寫圖片描述
 總結以上,大致可分爲以下幾步:
  [1] 用戶空間應用程序調用庫中的函數;
  [2] 庫函數調用系統調用;
  [3] 開始進行由用戶態向內核態切換的準備工作,包括從當前進程的描述符中提取其內核棧的ss0及esp0信息,將系統調用號放入eax寄存器中,以及將系統調用所需要的外部參數傳遞給內核;
  [4] 使用ss0和esp0指向的內核棧將當前進程的cs,eip,eflags,ss,esp信息保存起來,這個過程也完成了由用戶棧到內核棧的切換過程,同時保存了被暫停執行的程序的下一條指令;
  [5] 將先前由中斷向量檢索得到的中斷處理程序的cs,eip信息裝入相應的寄存器,開始執行中斷處理程序,這時就轉到了內核態的程序執行了;
  [6] 中斷處理程序完成,恢復現場並返回用戶態。

注意:
 中斷上下文:中斷處理程序是被內核調用來響應中斷的,而它們運行於稱之爲中斷上下文的特殊上下文中,由於中斷上下文不可以睡眠,所以又稱之爲原子上下文,它有嚴格的時間限制,所以分爲上半部和下半部,由於它極有可能是打斷了其他中斷線上的另一箇中斷處理程序,所以儘量把工作從中斷處理程序中分離出來,放在下半部執行,因爲下半部可以在更合適的時間運行;
 進程上下文:它是內核所處的一種操作模式,此時內核代表進程在執行,比如執行系統調用或者內核線程,進程上下文可以睡眠。

二:進程管理

 在討論fork創建子進程之前,我們先來討論一下寫時拷貝的概念:
 寫時拷貝:傳統的fork()系統調用直接把所有的資源複製給新創建的進程,這種實現過於簡單並且效率低下。Linux的fork()使用寫時拷貝頁實現,寫時拷貝是一種可以推遲甚至是免拷貝數據的一種技術,內核此時並不複製整個進程的地址空間,而是讓父進程和子進程共享同一個拷貝。只有在需要寫入的時候,數據纔會被複制,從而使各個進程擁有各自的拷貝,資源的複製只有在需要寫入時才進行,在此之前,只是以只讀的方式共享,這種技術使得地址空間上的頁的拷貝被推遲到實際發生寫入的時候才進行。因爲fork()的實際開銷就是複製父進程的頁表以及子進程創建唯一的進程描述符。
 那麼在fork()一個子進程的流程有哪些呢?
 這裏寫圖片描述 值得注意的是vfork()不拷貝父進程的頁表項,其他與fork()功能基本相同,子進程作爲父進程的一個單獨的線程在它的地址空間中運行,父進程阻塞,直到子進程退出或執行exec(),子進程不能向地址空間寫入。
 殭屍進程:當父進程還沒有獲取到子進程的退出狀態碼,子進程就已經退出的狀態下,造成子進程成爲了一個殭屍進程。
 解決方案是讓父進程等待子進程結束時獲得子進程的退出狀態碼後再接着執行。
  1.在main函數中給SIGCHLD信號註冊一個信號處理函數(sig_chld),然後在子進程退出的時候,內核遞交一個SIGCHLD的時候就會被主進程捕獲而進入信號處理函數sig_chld,然後再在sig_chld中調用wait,就可以清理退出的子進程。這樣退出的子進程就不會成爲殭屍進程。
  2.調用waitpid而不是wait,這個辦法的方法爲:信號處理函數中,在一個循環內調用waitpid,以獲取所有已終止子進程的狀態。我們必須指定WNOHANG選項,它告知waitpid在有尚未終止的子進程在運行時不要阻塞。(我們不能在循環內調用wait,因爲沒有辦法防止wait在尚有未終止的子進程在運行時阻塞,wait將會阻塞到現有的子進程中第一個終止爲止)。
  進程一旦調用了wait,就立即阻塞自己,由wait自動分析是否當前進程的某個子進程已經退出,如果讓它找到了這樣一個已經變成殭屍的子進程,wait就會收集這個子進程的信息,並把它徹底銷燬後返回;如果沒有找到這樣一個子進程,wait就會一直阻塞在這裏,直到有一個出現爲止。
  waitpid系統調用在Linux函數庫中的原型是:
  .#include <.sys/types.h> /* 提供類型pid_t的定義 */
  .#include <.sys/wait.h>
   pid_t waitpid(pid_t pid,int *status,int options)
  從本質上講,系統調用waitpid和wait的作用是完全相同的,但waitpid多出了兩個可由用戶控制的參數pid和options,從而爲我們編程提供了另一種更靈活的方式。下面我們就來詳細介紹一下這兩個參數:
  pid:從參數的名字pid和類型pid_t中就可以看出,這裏需要的是一個進程ID。但當pid取不同的值時,在這裏有不同的意義。
  [1].pid>0時,只等待進程ID等於pid的子進程,不管其它已經有多少子進程運行結束退出了,只要指定的子進程還沒有結束,waitpid就會一直等下去。
  [2].pid=-1時,等待任何一個子進程退出,沒有任何限制,此時waitpid和wait的作用一模一樣。
  [3].pid=0時,等待同一個進程組中的任何子進程,如果子進程已經加入了別的進程組,waitpid不會對它做任何理睬。
  [4].pid<-1時,等待一個指定進程組中的任何子進程,這個進程組的ID等於pid的絕對值。
  options:options提供了一些額外的選項來控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED兩個選項,這是兩個常數,可以用”|”運算符把它們連接起來使用。如果我們不想使用它們,也可以把options設爲0,如果使用了WNOHANG參數調用waitpid,即使沒有子進程退出,它也會立即返回,不會像wait那樣永遠等下去。而WUNTRACED參數,由於涉及到一些跟蹤調試方面的知識,加之極少用到,這裏就不多費筆墨了。
waitpid的返回值比wait稍微複雜一些,一共有3種情況:
  1、當正常返回的時候,waitpid返回收集到的子進程的進程ID;
  2、如果設置了選項WNOHANG,而調用中waitpid發現沒有已退出的子進程可收集,則返回0;
  3、如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在;
 當pid所指示的子進程不存在,或此進程存在,但不是調用進程的子進程,waitpid就會出錯返回,這時errno被設置爲ECHILD;
 孤兒進程:當父進程在子進程還未結束時就已經退出時,此時的子進程就成爲了一個孤兒進程,孤兒進程將被init進程(進程號爲1)所收養,並由init進程對它們完成狀態收集工作。
 守護進程:守護進程就是在後臺運行,不與任何終端關聯的進程,通常情況下守護進程在系統啓動時就在運行,它們以root用戶或者其他特殊用戶(apache和postfix)運行,並能處理一些系統級的任務.習慣上守護進程的名字通常以d結尾(sshd),但這些不是必須的.
 下面介紹一下創建守護進程的步驟
 1.調用fork(),創建新進程,它會是將來的守護進程;
 2.在父進程中調用exit,保證子進程不是進程組長;
 3.調用setsid()創建新的會話區;
 4.將當前目錄改成跟目錄(如果把當前目錄作爲守護進程的目錄,當前目錄不能被卸載他作爲守護進程的工作目錄);
 5.將標準輸入,標註輸出,標準錯誤重定向到/dev/null。
 注意:Linux系統最多可以運行的進程數爲默認是 32768 (2^11),最大值不能超過 2^22 (4194304) 400萬。可以用cat /proc/sys/kernel/pid_max命令來查看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章