用return和exit结束fork和vfork创建的子进程的思考

fork和vfork的差别:

1、fork是创建一个子进程,并把父进程的内存数据copy到子进程中。

vfork是创建一个子进程,并和父进程的内存数据share一起。

2、vfork是这样的工作的:

(1)、保证子进程先执行。

(2)、当子进程调用exit()或exec()后,父进程往下执行。

3、fork后来采用的优化技术,这样,对于fork后并不是马上拷贝内存,而是只有你在需要改变的时候,才会从父进程中拷贝到子进程中,这样fork后立马执行exec的成本就非常小了。而vfork因为共享内存所以比较危险。


为什么要干出一个vfork这个玩意? 

原因是: 起初只有fork,但是很多程序在fork一个子进程后就exec一个外部程序,于是fork需要copy父进程的数据这个动作就变得毫无意了,而且还很重,所以,搞出了个父子进程共享的vfork。所以,vfork本就是为了exec而生。

为什么return会挂掉,exit()不会?

从上面我们知道,结束子进程的调用是exit()而不是return,如果你在vfork中return了,那么,这就意味main()函数return了,注意因为函数栈父子进程共享,所以整个程序的栈就出现问题了。如果你在子进程中return,那么基本是下面的过程:

(1)、首先子进程的main() 函数 return了。

(2)、而main()函数return后,通常会调用 exit()或相似的函数(如:exitgroup())。

(3)、这时,父进程收到子进程exit(),开始从vfork返回,但是父进程的栈都被子进程干废掉了,你让我怎么执行?(注:栈会返回一个诡异一个栈地址,对于某些内核版本的实现,直接报“栈错误”,然而,对于某些内核版本的实现,于是有可能会再次调用main(),于是进入了一个无限循环的结果,直到vfork 调用返回 error)

再回到 return 和 exit,return会释放局部变量,并弹栈,回到上级函数执行。exit直接退掉。如果你用c++ 你就知道,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用 _exit()或_exitgroup()的封装)。

可见,子进程调用exit() 没有修改函数栈,所以,父进程得以顺利执行。


内核代码分析:

linux创建子进程实际是一个复制父进程的过程。所以更贴切的说法是clone。linux一开始使用fork的原因是当时clone这个词还没有流行。 实际存在fork,clone,vfork 三个系统调用。fork是完全复制,clone则是有选择的复制,vfork则完全使用父进程的资源。可以理解vfork是创建的线程。 vfork的出现主要是为了立即就执行exec的程序考虑的。但是后来的kernel都支持copy_on_write ,所以vfork提高效率的机制也没有那么明显了。

内核中三个系统调用最后都是调用do_fork:

fork:return do_fork(SIGCHLD, regs.esp, ®s, 0);
clone:return do_fork(clone_flags, newsp, ®s, 0);
vfork: return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0);

#define CLONE_VFORK 0x00004000  /* set if the parent wants the child to wake it up on mm_release*/
#define CLONE_VM 0x00000100  /* set if VM shared between processes */

上面两个宏指出:vfork 要求子进程执行mm_release 后唤醒 父进程, 并且共享虚拟内存


为什么要求子进程先行呢?

拿虚拟内存做比方。 进程需要有结构管理自己的虚拟内存空间, 该结构在进程 结构体 task_struct 中就是一个mm_struct 类型的指针。fork的时候内核会新建结构体,将该mm_struct 本身以及下级结构都复制一份,并设置子进程的mm_struct 指向新的内存。而vfork则只是复制了task_struct 本身,并没有递归下去。简单说就是:fork复制了内存,vfork复制了指针。


do_fork:

#define DECLARE_MUTEX_LOCKED(name) __DECLARE_SEMAPHORE_GENERIC(name,0)
DECLARE_MUTEX_LOCKED(sem);
if ((clone_flags & CLONE_VFORK) && (retval > 0))
    down(&sem);

可以看到申明了信号两sem, 并初始化为0,也就是说当使用vfork时,父进程会睡眠。(需要说一下此时子进程已经进入就绪队列。并且该信号量是局部变量,子进程使用的父进程的地址空间,所以也是可以看到该局部变量的。) 子进程被调度执行时,使用的是父进程的地址空间(因为用的父进程的mm_struct 指针), 此时子进程可以该父进程的堆栈。所以此时父子进程绝对不能同时运行。 execve和exit两个系统调用是不退栈的,而是直接进入系统空间,将共享的地址空间分开,所以这两个系统调用是安全的。return是会退栈的,而子进程的退栈会导致父进程的栈也被改了(应该很好理解), 所以子进程绝对不能退到父进程当前栈顶以下的地方。

所以开发人员注意: 子进程绝对不允许在调用vfork的函数中return,vfork就是用来调用execve的。而且该系统调用在cow后就应该禁止使用了!


execve,exit两个系统调用会在内核调用mm_release函数,该函数会调用up操作。

	void mm_release(void)
	{
	    struct task_struct *tsk = current;
	    /* notify parent sleeping on vfork() */
	    if (tsk->flags & PF_VFORK) {
	        tsk->flags &= ~PF_VFORK;
	        up(tsk->p_opptr->vfork_sem);
	    }
	}

	struct task_struct {
	....
	unsigned long flags; /* per process flags, defined below */
	struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
	...
	}
p_opptr 指向父进程的task_struct 结构。分别是 生父,养父,子进程,弟弟进程,哥哥进程。



每个C程序的入口点_start处的代码用伪代码表示为

_start:

call __libc_init_first // 一些初始化

call _init
call atexit
call main
call _exit

从伪代码就看出来了,每个C程序都要在执行一些初始化函数后对main调用,若main末尾为return语句,那么控制返回,最终会call _exit,把控制返回系统。若省略return,那么也将会call _exit。如果代码中有exit函数,那么会先执行atexit注册的函数,进而执行_exit()把控制还给操作系统。总之,这些情况下,当main返回,控制会传给系统。

也可以理解成:exit是操作系统的,return是c语言函数的,不在一个层面上。






你可能感兴趣的:(Linux学习笔记,return,exit,fork,vfork)