進程ID 0是調度進程,經常被稱爲交換進程(swapper)。該進程並不執行任何磁盤上的程序--它是內核的一部分,所以也被稱爲系統進程。進程ID 1是init進程,在自舉(bootstrapping)過程結束時由內核調用。該進程的程序文件在UNIX的早期版本中是/etc/init,在較新版本中是/sbin/init。此進程負責在內核自舉後啓動一個UNIX系統。init一般讀與系統有關的初始化(/etc/rc*文件),並將系統引導到一個狀態(例如多用戶)。init進程決不會終止。它是一個普通的用戶進程(與交換進程不一樣,它不是內核中的系統進程),可是它以超級用戶特權運行。
在某些UNIX的虛存實現中,進程ID 2是頁精靈進程(pagedaemon)。此進程負責支持虛存系統的請頁操做。與交換進程同樣,頁精靈進程也是內核進程。
除了進程ID,每一個進程還有其餘標識符。下列函數能夠返回這些標識符:linux
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); 返回: 調用進程的進程ID pid_t getppid(void); 返回: 調用進程的父進程ID uid_t getuid(void); 返回: 調用進程的實際用戶ID uid_t geteuid(void); 返回: 調用進程的有效用戶ID gid_t getgid(void); 返回: 調用進程的實際組ID gid_t getegid(void); 返回: 調用進程的有效阻ID
這些函數都沒有出錯返回git
一個進程調用fork函數是UNIX內核建立一個新進程的惟一方法(除了交換進程、init進程和頁精靈進程)shell
#include <sys/types.h> #include <unistd.h> pid_t fork(void); 返回: 子進程中爲0,父進程中爲子進程的進程ID,出錯爲-1.
子進程和父進程繼續執行fork以後的指令。子進程是父進程的複製品。例如,進程得到父進程數據空間、堆和棧的複製品。可是這些都是進程擁有的拷貝不是與父進程共享。若是正文段是隻讀的,則父、子進程共享正文段。
如今不少實現並不作一個父進程數據段和堆的徹底拷貝,由於在fork以後常常跟隨着exec。做爲替代,使用了在**寫時複製(Copy On Write, COW)**的技術。這些區域由父、子進程共享,並且內核將它們的存取權限改爲只讀的。若是有進程試圖修改這些區域,則內核爲有關部分(典型的是虛存系統中的"頁"),作一個拷貝。bootstrap
#include <sys/types.h> #include <stdio.h> #inlcude <unistd.h> int glob = 6; char buf[] = "a write to stdout\n"; int main(void) { int var; pid_t pid; var = 88; if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1) { fprintf(stderr, "write error"); } printf("before fork \n"); if ((pid = fork()) < 0) { fprintf(stderr, "fork error"); } else if (pid == 0) { glob++; var++; } else { sleep(2); } printf("pid=%d,glob=%d,var=%d\n", gitpid(), glob, var); return 0; }
編譯運行上面的進程:
centos
能夠看到當咱們將輸出重定向到temp.out文件後多出個before fork的輸出。write函數是不帶緩存的。由於在fork以前調用write,因此其數據寫到標準輸出一次。可是標準IO是帶緩存的。若是標準輸出連到終端設備,則它是行緩存,不然它是全緩存。當以交互方式運行該程序時,只獲得printf輸出的行一次,其緣由是標準輸出緩存由新行符刷新。當咱們將printf("before fork \n");後的換行符去掉以後即printf("before fork");來驗證這一點,修改以後輸出結果是:緩存
能夠看到before fork打印了兩次,這說明由於咱們去掉了換行符因此標準輸出流的行緩存不會被flush。app
可是當將標註輸出從新定向到一個文件時,卻獲得printf輸出行兩次。其緣由是,將標準輸出從新定向到一個文件時標準輸出流就不是行緩存而是全緩存了,在fork以前調用了printf一次,但當調用fork時,該行數據仍在緩存中,而後在父進程數據空間複製到子進程的過程當中時,該緩存數據也被複制到了子進程中。因而那時父、子進程各自有了帶該行內容的緩存。在exit以前的第二個printf將其數據添加到現存的緩衝中。當每一個進程終止時,緩存中的內容將被寫到相應文件中。
文件共享 對於上面的程序須要注意:在重定向父進程的標準輸出時也重定向了子進程的標準輸出。fork的一個特性是全部由父進程打開的文件描述符都被複制到子進程中。父、子進程每一個相同的打開文件描述符共享一個文件表項。
這種共享文件的方式使父子進程對同一文件使用了一個文件位移量。對於如下狀況:一個進程fork了一個子進程,而後等待子進程終止。假定,做爲普通處理的一部分,父、子進程都向標準輸出執行寫操做。若是父進程使其標準輸出重定向(極可能是由shell實現的),那麼子進程寫到該標準輸出時,他將更新與父進程共享的該文件的位移量。在咱們所考慮的例子中,當父進程等待子進程時,子進程寫到標準輸出;而在子進程終止後,父進程也寫入到標準輸出上,而且知道其輸出會添加在子進程所寫數據以後。若是父、子進程不共享同一文件位移量,這種形式的交互就很難實現。[爲了理解這一點可參看APUE3.10節]函數
若是父、子進程寫到同一文件描述符文件,但又沒有任何形式的同步(例如使父進程等待子進程),那麼它們的輸出就會相互混合(假定所用的文件描述符是在fork以前打開的)。
在fork以後處理文件描述符有兩種常見的狀況:
(1) 父進程等待子進程完成。這種狀況下,父進程無需對其描述符作任何處理。
(2) 父、子進程各自執行不一樣的程序段。在這種狀況下,在fork以後,父、子進程各自它們不需使用的文件描述符,而且不干擾對方使用的文件描述符。ui
除了打開文件以外,不少父進程的其餘性質也會由子進程繼承:spa
父、子進程之間的區別是:
vfork函數的調用序列和返回值與fork相同,但兩個函數的語義不一樣。
vfork用於建立一個新進程,而該進程的目的是exec一個新程序。vfork與fork同樣都建立一個子進程,可是它並不將父進程的地址空間徹底複製到子進程中,由於子進程會當即調用exec(或exit),因此就不會用到此地址空間。
vfork和fork之間的另外一個區別是:vfork保證子進程先運行,在它調用exec/exit以後父進程纔可能被調度運行。(若是在調用exec/exit以前子進程依賴於父進程的進一步動做,則會致使死鎖。) 對於如下示例:
#include <sys/types.h> #include <stdio.h> #include <unistd.h>
int glob = 6; int main(void) { int var; pid_t pid; var = 88; printf("before vfork\n"); if ((pid = vfork()) < 0) { fprintf(stderr, "vfork error\n"); } else if (pid == 0) { glob++; var++; _exit(0); } printf("pid=%d,glob=%d,var=%d\n", getpid(), glob, var); return 0; }
編譯運行該程序:
須要注意在上面的程序咱們調用了_exit而不是exit。_exit並不執行IO緩存的刷新操做。若是是調用exit而不是_exit,則該程序的輸出是:
(上圖是APUE原書, 下圖是我在centos上實驗結果
之因此結果不一樣是由於在linux中子進程關閉的是本身的, 雖然他們共享標準輸入、標準輸出、標準出錯等 「打開的文件」, 子進程exit時,也不過是遞減一個引用計數,不可能關閉父進程的,因此父進程仍是有輸出的。
)
可見父進程的printf輸出消失了。其緣由是子進程調用了exit,它刷新開關閉了全部標準IO流,這包括標準輸出。雖然這是由子進程執行的,但倒是在父進程的地址空間中進行的,因此全部受到影響的標準IO FILE對象都是在父進程中。當父進程調用prinf時,標準輸出已經被關閉了,因而printf返回-1。
4、exit函數
進程有三種正常終止法和兩種異常終止法。
(1) 正常終止:
(a) 在main函數內執行return語句,這至關於調用exit。
(b) 調用exit函數。
(c) 調用_exit函數。
(2) 異常終止:
(a) 調用abort。它產生SIGABRT信號,因此是下一種異常終止的特例。
(b) 當進程接收到某個信號時。進程自己(例如調用abort函數)、其餘進程和內核都能產生傳送到某一進程的信號。例如:進程越出其地址空間訪問存儲單元,或者除以0,內核都會爲該進程產生相應的信號。
對上述任意一種終止情形,咱們都但願終止進程可以通知其父進程它是如何終止的。對於e x i t和_ e x i t,這是依靠傳遞給它們的退出狀態( exit status)參數來實現的。在異常終止狀況,內核(不是進程自己)產生一個指示其異常終止緣由的終止狀態( termination status) 。在任意一種狀況下,該終止進程的父進程都能用 w a i t或w a i t p i d函數(在下一節說明)取得其終止狀態。(退出狀態是傳給exit/_exit的參數,或main返回值。在最後調用_exit時內核將其退出狀態轉爲終止狀態,若是子進程正常終止那父進程能夠獲取子進程的退出狀態)。
必定是一個父進程生成一個子進程。上面又說明了子進程將其終止狀態返回給父進程。可是若是父進程在子進程以前終止,則將如何呢 ?其回答是對於其父進程已經終止的全部進程,它們的父進程都改變爲init進程。咱們稱這些進程由init進程領養。其操做過程大體是:在一個進程終止時,內核逐個檢查全部活動進程,以判斷它是不是正要終止的進程的子進程,若是是,則該進程的父進程 I D就更改成1 ( i n i t進程的I D )。這種處理方法保證了每一個進程有一個父進程。
另外一個咱們關心的狀況是若是子進程在父進程以前終止,那麼父進程又如何能在作相應檢查時獲得子進程的終止狀態呢?對此問題的回答是內核爲每一個終止子進程保存了必定量的信息,因此當終止進程的父進程調用 w a i t或waitpid 時,能夠獲得有關信息。這種信息至少包括進程I D、該進程的終止狀態、以反該進程使用的 C P U時間總量。內核能夠釋放終止進程所使用的全部存儲器,關閉其全部打開文件。在 U N I X術語中,一個已經終止、可是其父進程還沒有對其進行善後處理(獲取終止子進程的有關信息、釋放它仍佔用的資源)的進程被稱爲僵死進程。
最後一個要考慮的問題是:一個由i n i t進程領養的進程終止時會發生什麼 ?它會不會變成一個僵死進程?對此問題的回答是「否」 ,由於i n i t被編寫成只要有一個子進程終止, i n i t就會調用一個w a i t函數取得其終止狀態。這樣也就防止了在系統中有不少僵死進程。當說起「一個 i n i t的子進程」時,這指的是 i n i t直接產生的進程(例如,將在9 . 2節說明的g e t t y進程),或者是其父進程已終止,由init 領養的進程。