glibc中的vfork


vfork:子进程共享父进程的地址空间,对应Linux内核的task struct中的mm struct指针指向同一个mm。

在main()函数中调用vfork()时,会把返回地址IP push到堆栈,当子进程返回后,调用任何函数(例如execve),该地址将被覆盖;然而子进程结束,父进程接着运行时表现正常,这说明返回地址在vfork函数中被保存过,父进程在vfork返回前会恢复该返回地址,从而继续正常运行!

以下glibc中vfork()的代码说明了这一点:

glibc-2.7/sysdeps/unix/i386/vfork.S
ENTRY(vfork)
jmp vfork

glibc-2.7/sysdeps/unix/sysv/linux/i386/vfork.S
ENTRY (
vfork)

#ifdef __NR_vfork

/ Pop the return PC value into ECX. /
popl    %ecx

/ Stuff the syscall number in EAX and enter into the kernel. /
movl    $SYS_ify (vfork), %eax
int     $0x80

/ Jump to the return PC. Don’t jump directly since this
disturbs the branch target cache. Instead push the return
address back on the stack.
/
pushl   %ecx

cmpl    $-4095, %eax
        …



因此,运行以下代码不会有异常:

//example 1
int main()
{
int a, b;
a =1, b =100;
int pid;

write(1, "output!n", sizeof("output!n"));
printf("before forkn");

if((pid = vfork()) == 0) {
a++;
b++;
func(); //any number of function calls here is ok, stack will not be messed
}
else if(pid > 0) {
}

printf("cc:%dn", cc);
    return 0;
}



而运行下面代码将得到段误,原因就是vfork()只保存自己这一层的函数堆栈返回地址正确,而不能保证调用vfork()的上层函数的堆栈内容,
//example 2
#include <unistd.h>
#include <stdio.h>

void f1()
{
pid_t pid;
if((pid = vfork()) == 0)
;
printf("ok, …n");
}
void f2()
{
printf("f2n");
}
int main()
{
f1();   // 一旦子进程从f1()返回,父进程就再也不能从f1()返回到正确的地址了!
printf("out, …n");
f2();
return 0;
}

运行结果为:
ok, …
out, …
f2
ok, …
段错误



2009-9-10
说明:
实际上,由于main最终还是会return,从而改变堆栈内容,因此example 1仍然有错误!
在Arch Linux中运行仍然产生segment fault!!!(gcc-4.3.1 glibc-2.8 Linux-2.6.26.6)
(在子进程中调用_exit()可以避免出错)


由此可见,vfork的目的只能用于创建一个新进程,然后该新进程exec一个新进程!

vfork和fork之间的另一个区别是: vfork保证子进程先运行,在它调用exec或exit之后
父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作
,则会导致死锁。



end