fork
( )的主要任務是初始化要建立進程的資料結構,其主要的步驟有
(以下內容取自joyfire筆記
):
1
)申請一個閒置頁面來儲存task_struct;
2
)尋找一個空的進程槽(find_empty_process
( ));
3
)為kernel_stack_page申請另一個閒置記憶體頁作為堆棧;
4
)將父進程的LDT表拷貝給子進程;
5
)複製父進程的記憶體映射資訊;
6
)管理檔案描述符和連結點。
那麼,若在父進程中建立了一個對象,則經過fork,對象會有什麼特殊表現呢?看看下面的程式:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
class CA
{
public:
CA(){ printf("construct CA/n"); }
~CA() { printf("destruct CA/n"); }
};
int main(int argc, char **argv)
{
pid_t pid;
pid_t wpid;
int status;
CA a;
pid = fork();
if ( pid == (pid_t)-1 )
{
fprintf(stderr, "%s: Failed to fork()/n", strerror(errno));
exit(13);
}
else if ( pid == 0)
{
printf("PID %ld: Child started, parent is %ld./n",
(long)getpid(),
(long)getppid());
return 0;
}
printf("PID %ld: Started child PID %ld./n",
(long)getpid(),
(long)pid);
wpid = wait(&status);
if ( wpid == (pid_t)-1 )
perror("wait(2)");
return 0;
}
以下是程式某次運行結果:
construct CA
PID 29423: Child started, parent is 29422.
destruct CA
PID 29422: Started child PID 29423.
destruct CA
結果顯示:對象被構造了一次,但是被析構了兩次。
為什麼會這樣呢?這正是fork使得子進程複製父進程的記憶體映射資訊的結果。對於類似這樣的代碼:
void main()
{
CA a; //CA是一個類
}
其等價的彙編代碼大概是下面這樣:
10: void main()
11: {
00401030 push ebp
00401031 mov ebp,esp
00401033 sub esp,44h
00401036 push ebx
00401037 push esi
00401038 push edi
00401039 lea edi,[ebp-44h]
0040103C mov ecx,11h
00401041 mov eax,0CCCCCCCCh
00401046 rep stos dword ptr [edi]
12: CA a;
00401048 lea ecx,[ebp-4]
0040104B call @ILT+0(CA::CA) (00401005)
13: }
00401050 lea ecx,[ebp-4]
00401053 call @ILT+5(CA::~CA) (0040100a)
00401058 pop edi
00401059 pop esi
0040105A pop ebx
0040105B add esp,44h
0040105E cmp ebp,esp
00401060 call __chkesp (00401120)
00401065 mov esp,ebp
00401067 pop ebp
00401068 ret
也就是說,何時插入構造/解構函式調用代碼,完全是在編譯期間確定的。fork後,子進程由於完全拷貝了父進程記憶體映射資訊(含程式碼片段和呼叫堆疊資訊),將繼續執行呼叫堆疊指定的指令,因此,後面的call @ILT+5(CA::~CA) (0040100a)將被兩次執行。
fork的以上特點有時被用於在父子進程間傳遞資訊(由父進程->子進程,這種傳遞是單向的)。
那麼,我們如何屏蔽上面重複輸出的析構訊息呢,通過在網上的討論,有以下建議:
1、上策:不要在fork 中使用可重新進入的語句,諸如printf;
2、下策:在CA中添加一個標誌,並在解構函式中根據該標誌進行輸出;
對於第二方案,修改後的程式如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
class CA
{
public:
CA();
~CA();
int _childFlag;
};
CA::CA()
{
_childFlag = 0;
printf("construct CA/n");
}
CA::~CA()
{
if (!_childFlag)
{
printf("destruct CA/n");
}
}
int main(int argc, char **argv)
{
pid_t pid;
pid_t wpid;
int status;
CA a;
pid = fork();
if ( pid == (pid_t)-1 )
{
fprintf(stderr, "%s: Failed to fork()/n", strerror(errno));
exit(13);
}
else if ( pid == 0)
{
a._childFlag = 1;
printf("PID %ld: Child started, parent is %ld./n",
(long)getpid(),
(long)getppid());
return 0;
}
printf("PID %ld: Started child PID %ld./n",
(long)getpid(),
(long)pid);
wpid = wait(&status);
if ( wpid == (pid_t)-1 )
perror("wait(2)");
return 0;
}
附註:以上是一個很無聊的話題,但幾周前被人問及,特記錄於此。