在Unix/Linux系統下進程創建時需要進行如下系統調用:fork/exec
fork()把一個進程複製成二個進程:parent (old PID), child (new PID)
exec()用新程序來重寫當前進程:PID沒有改變
接下來就重點學習這兩個系統調用:
當我們fork() 創建一個繼承的子進程將會發生如下事情:複製父進程的所有變量和內存,複製父進程的所有CPU寄存器(有一個寄存器例外)(這個寄存器是用來區分父進程和子進程的PID)
fork()的返回值:調用fork()函數成功時,將會有兩個返回值。子進程的fork()返回0,父進程的fork()返回子進程標識符。fork() 返回值可方便後續使用,子進程可使用getpid()獲取PID。
fork()執行過程對於子進程而言,是對父進程地址空間的一次複製。下面我們通過圖示來看一下這個複製過程:
注意了,圖示中的兩個childPID的值是不同的,對於父進程中的childPID當然是子進程的PID,而子進程中的childPID的值爲0。
fork()使用示例:
int main()
{
pid_t pid;
int i;
for(i=0; i<LOOP; i++)
{
/* 創建新進程*/
pid = fork();
if (pid < 0) { /*創建失敗 */
fprintf(stderr, “Fork Failed”);
exit(-1);
}
else if (pid == 0) { /* 子進程 */
fprintf(stdout, “i=%d, pid=%d, parent pid=%d\n”,I,
getpid() ,getppid());
}
}
wait(NULL);
exit(0);
}
瞭解了fork()之後我們再來看看exec()系統調用:系統調用exec( )加載新程序取代當前運行進程,也就是說exec調用成功時,它是相同的進程,但是運行了不同的程序,代碼段、堆棧和堆(heap)等也都完全重寫了。它允許進程“加載”一個完全不同的程序,並從main開始執行,而我們的fork()創建出來的子進程是從fork()之後的代碼段處開始執行的。
我們接下來討論一下fork()的實現開銷:當我們使用fork()系統調用時,首先要對子進程分配內存,然後複製父進程的內存和CPU寄存器到子進程裏。開銷昂貴!!
然而在99%的情況裏,我們在調用fork()之後調用exec(),因爲在大多數情況下我們都是不希望和父進程執行同樣的代碼,不然的話我們創建一個新的進程也就沒有多大的意義了。這時候我們就該考慮一下這個問題了:既然不想使用父進程中的代碼,在fork()操作中內存複製就是沒有作用的。那麼爲什麼不能結合它們在一個調用中?
此後,就產生了vfork():創建進程時,不再創建一個同樣的內存映像。一些時候稱之爲輕量級fork(),子進程幾乎立即調用exec()。而在子進程調用exec()之前,暫時與父進程共享地址空間。
我們注意到了,前面fork()的示例代碼中使用了wait()函數,那麼這個函數是幹什麼用的呢?這就要引出一個新的概念了:父進程等待子進程
wait()系統調用用於父進程等待子進程的結束:子進程結束時通過exit()向父進程返回一個值,父進程通過wait()接受並處理返回值
wait()系統調用的功能:有子進程存活時,父進程進入等待狀態,等待子進程的返回結果。當某子進程調用exit()時,喚醒父進程,將exit()返回值作爲父進程中wait的返回值
既然子進程結束時是通過exit()向父進程返回一個值,那麼我們又要學習一下exit()系統調用了:
進程結束執行時調用exit(),完成進程資源回收
exit()系統調用的功能:
- 將調用參數作爲進程的“結果”
- 關閉所有打開的文件等佔用資源
- 釋放內存
- 釋放大部分進程相關的內核數據結構
- 檢查是否父進程是存活着的,如存活,保留結果的值直到父進程需要它,進入殭屍(zombie/defunct)狀態。如果沒有,它釋放所有的數據結構,進程結果
- 清理所有等待的殭屍進程
說到這裏,大家可能對殭屍進程有點疑惑:所謂的殭屍進程,就是父進程沒有調用wait收集子進程的退出狀態,子進程就已經退出了,此時這個退出的子進程就成爲了殭屍進程,它的很多進程資源都還沒有釋放(例如PID在進程表中仍然存在)。