UNIX下对象析构在多进程中的行为分析

本文介绍了Unix中fork函数的主要任务,即初始化要创建进程的数据结构。通过示例程序展示了fork后对象构造一次、析构两次的现象,分析其原因是子进程复制父进程内存映射信息。还给出屏蔽重复析构消息的建议及修改后的程序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;
}


附注:以上是一个很无聊的话题,但几周前被人问及,特记录于此。
评论 34
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值