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;
}
附注:以上是一个很无聊的话题,但几周前被人问及,特记录于此。