1. 程式人生 > >Linux進程管理(一)進程的創建與銷毀

Linux進程管理(一)進程的創建與銷毀

習慣 獨立 發送信號 可執行文件 正整數 定向 除了 信號量 出錯

在進程的創建上, Unix采取了一種有趣和少見的處理方法:它將進程的創建和加載一個新二進制鏡像分離。Unix提供了兩個系統調用fork和exec。

創建進程:

缺省情況下,內核將進程ID的最大值限制為32768,2^15。系統管理員可以設置/proc/sys/kernel/pid_max的值來突破這個缺省的限制,但會犧牲一些兼容性。

創建新進程的那個進程稱為父進程,而新進程被稱為子進程。每個進程都是由其他進程創建的(除了init進程),因此每個子進程都有一個父進程。這種關系保存在每個進程的父進程ID號(ppid)中。每個進程都被一個用戶和組所擁有。這種從屬關系是用來實現訪問控制的。每個進程都是某個進程組的一部分,它簡單的表明了自己和其他進程之間的關系,但是不要和上面的用戶、組的概念混淆了。子進程通常屬於其父進程所在的那個進程組。進程組使得與管道相關的進程間發送和獲取信息變得很容易,這同樣也適用於管道中的子進程。從用戶的角度來看,進程組更像是一個任務。

在Unix中,載入內存並執行程序映像的操作與創建一個新進程的操作是分離的。Unix有一個系統調用(實際上是一系列系統調用之一)是可以將二進制文件的程序映像載入內存,替換原先進程的地址空間,並開始運行它。這個過程稱為運行一個新的程序,而相應的系統調用稱為exec系統調用。同時,另一個不同的系統調用是創建一個新的進程,它基本上就是復制父進程。通常情況下新的進程會立刻執行一個新的程序。完成創建新進程的這種行為叫做派生(fork),完成這個功能的系統調用就是fork()。

int execl (const char path, const char arg, ...);
execl()成功的調用不僅僅改變了地址空間和進程的映像,還改變了進程的一些屬性:

1,任何掛起的信號都會丟失。
2,捕捉的任何信號會還原為缺省的處理方式,因為信號處理函數已經不存在於地址空間中了。
3,任何內存的鎖定(參看第八章)會丟失。
4,多數線程的屬性會還原到缺省值。
5,多數關於進程的統計信息會復位。
6,與進程內存相關的任何數據都會丟失,包括映射的文件。
7,包括C語言庫的一些特性(例如atexit())等獨立存在於用戶空間的數據都會丟失。
然而也有很多進程的屬性沒有改變,例如pid、父進程的pid、優先級、所屬的用戶和組。
其他五個系統調用:execlp,execle,execv,execvp,execve。字 母 l 和 v 分別表示參數是以列表方式或者數組 (向量)方式提供的。字 母p意味著在用戶的PATH環境變量中尋找可執行文件。只要出現在用戶的路徑中,帶p的exec函數可以簡單的只提供文件名。最後e表示會提供給新進程以新的環境變量。

pid_t fork (void);
成功的fork()調用會返回0。在父進程中fork()返回子進程的pid。除了必要的一些方面,父進程和子進程之間在每個方面都非常相近:
1,顯然子進程的pid是新分配的,它是與父進程不同的。
2,子進程的ppid會設置為父進程的pid。
3,子進程中的資源統計信息(Resource statistics)會清零。
4,任何掛起的信號會清除,也不會被子進程繼承(參看第九章)。
5,任何文件鎖都不會被子進程所繼承。

寫時復制是一種采取了惰性優化方式來避免復制時的系統開銷。它的前提很簡單:如果有多個進程要讀取它們自己的那部分資源的副本,那麽復制是不必要的。每個進程只要保存一個指向這個資源的指針就可以了。只要沒有進程要去修改自己的”副本”,就存在著這樣的幻覺:每個進程好像獨占那個資源。從而就避免了復制帶來的負擔。如果一個進程要修改自己的那份資源“副本”,那麽就會復制那份資源,並把復制的那份提供給進程。不過其中的復制對進程來說是透明的。這個進程就可以修改復制後的資源了,同時其他的進程仍然共享那份沒有修改過的資源。
寫時復制在內核中的實現非常簡單。與內核頁相關的數據結構可以被標記為只讀和寫時復制。如果有進程試圖修改一個頁,就會產生一個缺頁中斷。內核處理缺頁中斷處理的方式就是對該頁進行一次透明復制。這時會清除頁面的COW 屬性,表示著它不再被共享。

在實現寫時復制之前, Unix 的設計者們就一直很關註在fork 後立刻執行exec所造成的地址空間的浪費。vfork()會掛起父進程直到子進程終止或者運行了一個新的可執行文件的映像。通過這種方式,vfork()避免了地址空間的按頁復制。實際上vfork()只完成了一件事:復制內部的內核數據結構。因此,子進程也就不能修改地址空間中的任何內存。

終止進程
進程成功的退出時,只需要簡單的寫上:exit(EXIT_SUCCESS);
在終止進程之前,C語言函數執行以下關閉進程的工作:
1,以在系統中註冊的逆序來調用由atexit()或on_exit()註冊的函數。
2,空所有已打開的標準I/O流。
3,刪除由tmpfile()創建的所有臨時文件。
這些步驟完成了在用戶空間中所需要做的事情,這樣exit()就可以調用_exit()來讓內核來處理終止進程的剩余工作了。
內核會清理進程所創建的、不再用到的任何資源。這包括:申請的內存、打開的文件和SystemV的信號量。清理完成後,內核摧毀進程,並告知父進程其子進程的終止。
C 語言中在main()函數返回時,編譯器會簡單的在最後的代碼中插入一個_exit()。 shell會根據這個返回值來判斷命令是否成功的執行,在 main()函數返回時明確給出一個狀態值 , 或者調用exit() , 這是一個良好的編程習慣。

atexit()的成功調用會把指定的函數註冊(無參數的,無返回值)到進程正常結束(例如一個進程以調用exit()或者從main()中返回的方式終止自己)時調用的函數中。如果進程調用了exec , 所註冊的函數列表會被清除(因為這些函數不存在於新進程的地址空間中)。如果進程是通過信號而結束的,這些註冊的函數也不會被調用。
函數調用的順序是和註冊的順序相反的。也就是這些函數存儲在棧中,以後進先出的方式調用(LIFO) 。註冊的函數不能調用exit() , 否則會引起無限的遞歸調用。

SunOS 4定義自己的一個和atexit()等價的函數on_exit(),這個函數的工作方式和atexit()—樣,只是註冊的函數原型不同。

當一個進程子進程終止時,內核會向其父進程發送SIGCHILD信號。缺省情況下會忽略此信號量,父進程也不會有任何的動作。進程也可通過signal()或sigaction()系統調用來有選擇的處理這個信號。通常情況下,父進程都希望能更多的了解到子進程的終止,或者顯式的等待子進程的終止。

用戶和組
用戶和組:Linux中通過用戶和組進行認證,每個用戶和唯一的正整數關聯,稱為用戶ID(uid)。每一個進程與一系列用戶ID關聯:
真實uid(real uid):每一個進程與一個用戶ID關聯,用來識別運行這個進程的用戶。用於辨識進程的真正所有者,且會影響到進程發送信號的權限。沒有超級用戶權限的進程僅在其RUID與目標進程的RUID相匹配時才能向目標進程發送信號,例如在父子進程間,子進程從父進程處繼承了認證信息,使得父子進程間可以互相發送信號。
有效UID(effective uid):在創建與訪問文件的時候發揮作用。具體來說,創建文件時,系統內核將根據創建文件的進程的EUID與EGID設定文件的所有者/組屬性,而在訪問文件時,內核亦根據訪問進程的EUID與EGID決定其能否訪問文件。
保留uid(saved uid):於以提升權限運行的進程暫時需要做一些不需特權的操作時使用,這種情況下進程會暫時將自己的有效用戶ID從特權用戶(常為root) 對應的UID變為某個非特權用戶對應的UID,而後將原有的特權用戶UID復制為SUID暫存;之後當進程完成不需特權的操作後,進程使用SUID的值重 置EUID以重新獲得特權。在這裏需要說明的是,無特權進程的EUID值只能設為與RUID、SUID與EUID(也即不改變)之一相同的值。
文件系統uid(filesystem uid):在Linux中使用,且只用於對文件系統的訪問權限控制,在沒有明確設定的情況下與EUID相同(若FSUID為root的UID,則SUID、RUID與EUID必至少有一亦為root的UID),且EUID改變也會影響到FSUID。設立FSUID是為了允許程序(如NFS服務器)在不需獲取向給定UID賬戶發送信號的情況下以給定UID的權限來限定自己的文件系統權限。

會話和進程組
每個進程都屬於某個進程組。進程組是由一個或多個相互間有關聯的進程組成的,它的目的是為了進行作業控制。進程組的主要特征就是信號可以發送給進程組中的所有進程:這個信號可以使同一個進程組中的所有進程終止、停止或者繼續運行。每個進程組都由進程組ID(pgid)唯一的標識,並且有一個組長進程。進程組ID就是組長進程的pid。只要在某個進程組中還有一個進程存在,則該進程組就存在。即使組長進程終止了,該進程組依然存在。
當有新的用戶登陸計算機,登陸進程就會為這個用戶創建一個新的會話。這個會話中只有用戶的登陸shell—個進程。登陸shell做為會話首進程(session leader)。會話首進程的pid就被作為會話的ID。一個會話就是一個或多個進程組的集合。會話囊括了登陸用戶的所有活動,並且分配給用戶一個控制終端(controling terminal)。控制終端是一個用於處理用戶I/O的tty設備。因此,會話的功能和shell差不多。沒有誰刻意去區分它們。
會話中的進程組分為一個前臺進程組和零個或多個後臺進程組。當用戶退出終端時,向前臺進程組中的所有進程發送SIGQUIT信號。當出現網絡中斷的情況時,向前臺進程組中的所有進程發送SIGHUP信號。當 用戶敲入了終止鍵(一般是Ctrl+C) ,向前臺進程組中的所有進程發送SIGINT信號。

相關系統調用:setsid,getsid,setpgid,getpgid。

特殊進程:
Init 進程
Idle進程
空閑進程,當沒有其他進程在運行時,內核所運行的進程—它的pid是0。
init進程,在啟動後,內核運行的第一個進程稱為init進程,它的pid是1。除非用戶顯式地指定內核所要運行的程序(通過內核啟動的init參數),否則內核依次尋找一個init程序,第一被發現的就會當做init運行。如果所有的都失敗了,內核就會發出panic,掛起系統。在內核交出控制後,init會接著完成後續的啟動過程。典型的情況是init會初始化系統,啟動各種服務和啟動登陸進程。

Orphan Process孤兒進程

Zombie Process僵屍進程
等待終止的子進程
用信號通知父進程是可以的,但是很多的父進程想知道關於子進程終止的更多信息——例如子進程的返回值。如果在終止過程中,子進程完全消失了,就沒有給父進程留下任何可以來了解子進程的東西。Unix的設計者們做出了這樣的決定:如果一個子進程在父進程之前結束,內核應該把子進程設置為一個特殊的狀態。處於這種狀態的進程叫做僵死(zombie)進程。進程只保留最小的概要信息一一些保存著有用信息的內核數據結構(進程號,退出狀態,運行時間等)。僵死的進程等待這父進程來查詢自己的信息(這叫做在僵死進程上等待)。只要父進程獲取了子進程的信息,子進程就會消失,否則一直保持僵死狀態。
為避免僵死進程,如進程可以顯示等待子進程結束、處理或者忽略SIGCHLD信號。

pid_t wait (int status);
pid_t waitpid (pid_t pid, int
status, int options);
int waitid (idtype_t idtype, id_t id, siginfo_t *infop, int options);
wait()返回已終止子進程的pid , 或者返回-1表示出錯。如果沒有子進程終止,調用者會被阻塞,直到一個子進程終止。

int system (const char *command);
ANSI和 POSIX 都定義了一個用於創建新進程並等待它結束的函數—— 可以把它想象成是同步的創建進程。

只要有進程結束了,內核就會遍歷它的所有子進程,並且把它們的父進程重新設為init進程(pid為1的那個進程)。這保證了系統中不存在沒有父進程的進程。init進程會周期性的等待所有子進程,確保不會有長時間存在的僵死進程。

Daemon Process守護進程
守護進程運行在後臺,不與任何控制終端相關聯。守護進程通常在系統啟動時就運行,它們以root用戶運行或者其他特殊的用戶(例如apache和postfix),並處理一些系統級的任務。習慣上守護進程的名字通常以d結尾(就像crond和sshd),但這不是必須的,甚至不是通用的。對於守護進程有兩個基本要求:它必須是init進程的子進程,並且不與任何控制終端相關聯。

一般來講,進程可以通過以下步驟成為守護進程:
1,調用fork(),創建新的進程,它會是將來的守護進程。
2,在守護進程的父進程中調用exit()。這保證了守護進程的祖父進程確認父進程已經結束。還保證了父進程不再繼續運行,守護進程不是組長進程。最後一點是順利完成以下步驟的前提。
3,調用setsid(),使得守護進程有一個新的進程組和新的會話,兩者都把它作為首進程。這也保證它不會與控制終端相關聯(因為進程剛剛創建了新的會話,同時也就不會為其關聯一個控制終端)。
4,用chdir()將當前工作目錄改為根目錄。因為前面調用fork()創建了新進程,它所繼承來的當前工作目錄可能在文件系統中任何地方。而守護進程通常在系統啟動時運行,同時不希望一些隨機目錄保持打開狀態,也就阻止了管理員卸載守護進程工作目錄所在的那個文件系統。
5,關閉所有的文件描述符。不需要繼承任何打開的文件描述符,對於無法確認的文件描述符,讓它們繼續處於打開狀態。
6,打開0、1和2號文件描述符(標準輸入、標準輸出和標準錯誤),把它們重定向到/dev/null。

Linux進程管理(一)進程的創建與銷毀