在我们需要一个程序成为后台的守护进程时,一般是通过fork 来创建一个子进程,随之父进程结束,然后再通过 setsid 来使子进程脱离父进程所属的进程组和会话。(通俗一点也可以这么认为,我们需要给这个已经长大的孩子重新找个家,并且跟之前家庭断绝关系,跟人有点类似,虽然关系断绝了,但亲爹是谁这个事实是谁也改变不了的,所以不管发生什么,亲爹的ID 号是不会变的。因为之前的父母或家庭都有可能被灭亡导致这个孩子因为没有家也被饿死。)这样才能不至于当终端发送 Ctrl+c或是该会话中断后守护进程也随之结束,从而达到守护的目的。
我们一般在写程序时都有这样的习惯,先是初始化,然后再开始工作。但是在使用 fork 的时候这一点就要注意了。创建子进程的时候系统会从父进程中将数据拷贝一份到子进程中,如果 fork之前我们 new 或者 malloc 了一块内存,并且保存了一个指向该内存的指针,那么在创建子进程的时候,也会将这个指针拷贝过去,但此处只拷贝了指针并没有拷贝内存块。所以当父进程结束的时候这块内存会被释放,那么此时在子进程中的指针则指向了一块已经被释放的内存了。有一段这样的代码
void test15()
{
class A
{
public:
A():pid_(0)
{
p_malloc = (char*)malloc(100);
printf("pro p_malloc_ = %d \n",p_malloc_);
}
~A()
{
printf("free(p_malloc_) , p_malloc = %d,pid = %d\n", p_malloc_, pid_);
free(p_malloc);
}
A(const A& a)
{
printf("A(const A& a) \n");
p_malloc_ = a.p_malloc_;
}
void set_pid(int pid)
{
cout << "set pid" << endl;
printf("set pid,pid = %d\n", pid);
pid_ = pid;
}
public:
char *p_malloc_;
int pid_;
};
A a;
pid_t pid;
pid = fork();
a.set_pid(pid);
switch(pid)
{
case -1:
cout << "fork() is error" << endl;
exit(-1);
break;
case 0:
//exit(0);
break;
default:
setsid();
pid_t pre_pid = getppid();
printf("child pro pid = %d, pre_pid = %d \n",pid, pre_pid);
sleep(10);
exit(10);
break;
}
}
int main(int argc, char *argv[])
{ test15();
return 0;
}
在 Solrais 下用 CC 编译
CC Test.cpp -o Test
之后运行结果为
bash-2.03$ Test
pro p_malloc = 305952
set pid
set pid,pid = 23881
child pro pid = 23881, pre_pid = 22819
set pid
set pid,pid = 0
free(p_malloc) , p_malloc = 305952,pid = 0
我认为说明了几个问题:
1:从父进程吧数据拷贝到子进程中的时候,不按照拷贝构造函数的方式来拷贝,直接进行字节的拷贝。
2:对于指针的拷贝,只拷贝指针的值,不拷贝指针所指向的内容(malloc 和 new 方式申请的内存)。
另外,当进程以 return 的方式结束的时候,对于没有释放对象,系统会调用将其释放并调用他们的析构函数,如果以 exit 方式退出则直接释放内存不调用析构函数。有点类似于 delete 和 free 的区别。
再回到我们上面所谈的地方,此时如果有一个连接在一个类的构造函数中实现,断开连接在它的析构函数中实现。如果这个类在 fork 之前已经 new 了出来,那么当父进程以return的方式结束的时候,它的析构函数就会断开连接,所以子进程中也无法使用该连接了。这里只是比较随意的谈到了一个例子,可能还会有更多的问题因这个原因而产生,所以在使用 fork 的时候我们要想到这一点从而尽可能的避免 BUG 的产生。