一、進程標識 進程ID 0是調度進程,常常被稱為交換進程(swapper)。該進程並不執行任何磁碟上的程式--它是內核的一部分,因此也被稱為系統進程。進程ID 1是init進程,在自舉(bootstrapping)過程結束時由內核調用。該進程的程式文件在UNIX的早期版本中是/etc/init,在較 ...
一、進程標識
進程ID 0是調度進程,常常被稱為交換進程(swapper)。該進程並不執行任何磁碟上的程式--它是內核的一部分,因此也被稱為系統進程。進程ID 1是init進程,在自舉(bootstrapping)過程結束時由內核調用。該進程的程式文件在UNIX的早期版本中是/etc/init,在較新版本中是/sbin/init。此進程負責在內核自舉後啟動一個UNIX系統。init通常讀與系統有關的初始化(/etc/rc*文件),並將系統引導到一個狀態(例如多用戶)。init進程決不會終止。它是一個普通的用戶進程(與交換進程不同,它不是內核中的系統進程),但是它以超級用戶特權運行。
在某些UNIX的虛存實現中,進程ID 2是頁精靈進程(pagedaemon)。此進程負責支持虛存系統的請頁操作。與交換進程一樣,頁精靈進程也是內核進程。
除了進程ID,每個進程還有其他標識符。下列函數可以返回這些標識符:
#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
這些函數都沒有出錯返回
二、fork函數
一個進程調用fork函數是UNIX內核創建一個新進程的唯一方法(除了交換進程、init進程和頁精靈進程)
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回: 子進程中為0,父進程中為子進程的進程ID,出錯為-1.
子進程和父進程繼續執行fork之後的指令。子進程是父進程的複製品。例如,進程獲得父進程數據空間、堆和棧的複製品。但是這些都是進程擁有的拷貝不是與父進程共用。如果正文段是只讀的,則父、子進程共用正文段。
現在很多實現並不做一個父進程數據段和堆的完全拷貝,因為在fork之後經常跟隨著exec。作為替代,使用了在**寫時複製(Copy On Write, COW)**的技術。這些區域由父、子進程共用,而且內核將它們的存取許可權改成只讀的。如果有進程試圖修改這些區域,則內核為有關部分(典型的是虛存系統中的"頁"),做一個拷貝。
#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; }
編譯運行上面的進程:
可以看到當我們將輸出重定向到temp.out文件後多出個before fork的輸出。write函數是不帶緩存的。因為在fork之前調用write,所以其數據寫到標準輸出一次。但是標準IO是帶緩存的。如果標準輸出連到終端設備,則它是行緩存,否則它是全緩存。當以交互方式運行該程式時,只得到printf輸出的行一次,其原因是標準輸出緩存由新行符刷新。當我們將printf("before fork \n");後的換行符去掉之後即printf("before fork");來驗證這一點,修改之後輸出結果是:
可以看到before fork列印了兩次,這說明因為我們去掉了換行符所以標準輸出流的行緩存不會被flush。
但是當將標註輸出重新定向到一個文件時,卻得到printf輸出行兩次。其原因是,將標準輸出重新定向到一個文件時標準輸出流就不是行緩存而是全緩存了,在fork之前調用了printf一次,但當調用fork時,該行數據仍在緩存中,然後在父進程數據空間複製到子進程的過程中時,該緩存數據也被覆制到了子進程中。於是那時父、子進程各自有了帶該行內容的緩存。在exit之前的第二個printf將其數據添加到現存的緩衝中。當每個進程終止時,緩存中的內容將被寫到相應文件中。
文件共用 對於上面的程式需要註意:在重定向父進程的標準輸出時也重定向了子進程的標準輸出。fork的一個特性是所有由父進程打開的文件描述符都被覆制到子進程中。父、子進程每個相同的打開文件描述符共用一個文件表項。
這種共用文件的方式使父子進程對同一文件使用了一個文件位移量。對於以下情況:一個進程fork了一個子進程,然後等待子進程終止。假定,作為普通處理的一部分,父、子進程都向標準輸出執行寫操作。如果父進程使其標準輸出重定向(很可能是由shell實現的),那麼子進程寫到該標準輸出時,他將更新與父進程共用的該文件的位移量。在我們所考慮的例子中,當父進程等待子進程時,子進程寫到標準輸出;而在子進程終止後,父進程也寫入到標準輸出上,並且知道其輸出會添加在子進程所寫數據之後。如果父、子進程不共用同一文件位移量,這種形式的交互就很難實現。[為了理解這一點可參看APUE3.10節]
如果父、子進程寫到同一文件描述符文件,但又沒有任何形式的同步(例如使父進程等待子進程),那麼它們的輸出就會相互混合(假定所用的文件描述符是在fork之前打開的)。
在fork之後處理文件描述符有兩種常見的情況:
(1) 父進程等待子進程完成。這種情況下,父進程無需對其描述符做任何處理。
(2) 父、子進程各自執行不同的程式段。在這種情況下,在fork之後,父、子進程各自它們不需使用的文件描述符,並且不幹擾對方使用的文件描述符。
除了打開文件之外,很多父進程的其他性質也會由子進程繼承:
- 實際用戶ID、實際組ID、有效用戶ID、有效組ID。
- 添加組ID。
- 進程組ID。
- 對話期ID。
- 控制終端。
- 設置-用戶-ID標誌和設置-組-ID標誌。
- 當前工作目錄。
- 根目錄。
- 文件方式創建屏蔽字。(umask)
- 信號屏蔽和排列。
- 對任一打開文件描述符的在執行時關閉標誌。
- 環境。
- 連接的共用存儲段。
父、子進程之間的區別是:
- fork的返回值。
- 進程ID
- 不同的父進程iD。
- 子進程的tms_utime,tms_stime,tms_cutime以及tms_ustime設置為0。
- 父進程設置的鎖,子進程不繼承。
- 子進程的未決告警被清除。
- 子進程的未決信號集設置被清除。
三、vfork函數
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。
四、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 領養的進程。