天天看點

如何正确地使用vfork():簡析vfork()與fork()的不同

今天看到知乎上有人問了一個由于不恰當的使用vfork()而導緻的一個奇怪現象,底下的回答非常精彩。趁此機會我也仔細了解了一下vfork()的特性。

其實對vfork()最完備、權威的表述莫過于man手冊裡面的解釋了。

簡單的說,vfork()跟fork()類似,都是建立一個子程序,這兩個函數的的傳回值也具有相同的含義。但是vfork()建立的子程序基本上隻能做一件事,那就是立即調用_exit()函數或者exec函數族成員,調用任何其它函數(包括exit())、修改任何資料(除了儲存vfork()傳回值的那個變量)、執行任何其它語句(包括return)都是不應該的。此外,調用vfork()之後,父程序會一直阻塞,直到子程序調用_exit()終止,或者調用exec函數族成員。

關于如何正确使用vfork(),上面這一段就是全部了。但是為什麼vfork()會這樣呢? 其實vfork()和fork()之間隻有兩點不同:

  1. fork()會複制父程序的頁表,而vfork()不會複制,直接讓子程序共用父程序的頁表;
  2. fork()使用了寫時複制技術,而vfork()沒有,它任何時候都不會複制父程序位址空間。

即使算上vfork()會阻塞父程序而fork()不會,也隻有三點不同,沒有更多不同了。是以vfork()産生的子程序跟父程序完全共同使用同一個位址空間,甚至共享同一個函數堆棧!也就是子程序中對任何資料變量的修改,不管是局部的還是全局的,都會影響到父程序。而任何一個函數調用都會修改棧空間,這就是為什麼vfork()的子程序不能随便調用别的函數。

但需要注意的是,由于vfork()畢竟還是産生一個新的程序,是以子程序擁有自己的程序描述符,擁有自己的寄存器,最重要的是,擁有自己的打開檔案清單!

注意擁有自己的打開檔案清單非常重要,因為如果子程序隻是簡單地共用父程序的打開檔案清單,那麼當子程序調用_exit()退出時,_exit()内部會自動關閉目前程序打開的所有檔案描述符,也就是打開檔案清單裡面的檔案,這将導緻父程序恢複執行時,無法通路到自己之前已經打開過的檔案,包括标準輸入、标準輸出和标準錯誤輸出。所幸的是這永遠不會發生,子程序會複制父程序的打開檔案清單,并增加檔案引用計數。

那為什麼vfork()子程序中可以調用_exit(),卻不可以調用exit(),也不可以直接return呢?

exit()是對_exit()的封裝,它自己在調用_exit()前會做很多清理工作,其中包括重新整理并關閉目前程序使用的流緩沖(比如stdio.h裡面的printf等),由于vfork()的子程序完全共享了父程序位址空間,子程序裡面的流也是共享的父程序的流,是以子程序裡面是不能做這些事的。

直接return就更不行了,子程序return以後,會從目前函數的外部調用點後面繼續執行,這後面子程序可能将會執行很多語句,結果就沒法預料了。

最後看一段程式,如果了解了這段程式,那麼對vfork()的了解基本上就沒什麼大問題了:

#include <stdio.h>
#include <unistd.h>

void stack1() {
    vfork();
}

void stack2() {
    _exit(0);
}

int main() {

    stack1();

    printf("%d goes 1\n", getpid());
    stack2();

    printf("%d goes 2\n", getpid());
    return 0;
}
           

如果父程序pid為1000,子程序pid為1001,那麼輸出将會是:

1001 goes 1

1000 goes 2

繼續閱讀