fork,vfork
转自 http://blog.csdn.net/todd911/article/details/14062103
1.fork函数
一个现有的进程可以调用fork函数创建一个新的子进程。
#include <unitsd.h> pid_t fork(void); //子进程返回0,父进程返回子进程ID,出错返回-1。
关于fork函数的常规用法这边不说了,下面说明下父子进程的文件共享。子进程是父进程的副本,例如,子进程获得父进程数据空间、栈和堆的副本,这是子进程所拥有的副本,父子进程并不共享这些存储空间部分。由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全复制,作为替代,使用了写时复制(copy on write,COW)技术。
一个进程具有三个不同的打开文件,它们是标准输入,标准输出和标准出错,从fork返回时,我们有了下面的结构。
这种共享文件的方式使父子进程对同一文件使用了一个文件偏移量,如果父子进程都向标准输出进行写操作,如果父进程的标准输出已经重定向,那么子进程写到标准输出时,它将更新与父进程共享的该文件的偏移量。比如当父进程等待子进程时,子进程写到标准输出,而在子进程终止后,父进程也写到标准输出,并且其输出会添加在子进程所写数据之后,如果父子进程不共享同一文件偏移量,这种形式的交互很难实现。
除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:
- 实际用户ID,实际组ID,有效用户ID,有效组ID。
- 附加组ID。
- 进程组ID。
- session ID。
- 控制终端。
- 设置用户ID标志和设置组ID标志。
- 当前工作目录。
- 根目录。
- 文件模式创建屏蔽字。
- 信号屏蔽和安排。
- 针对任意开打文件描述符的在执行时关闭(close-on-exec)标志。
- 环境。
- 连接的共享存储段。
- 资源映射。
- 资源限制。
父子进程的区别是:
- fork返回值。
- 进程ID不同。
- 两个进程具有不同的父进程ID。
- 子进程的tms_utime,tms_stime,tms_cutime以及tms_ustime均设置为0.
- 父进程设置的文件锁不会被子进程继承。
- 子进程的未处理alarm被清除。
- 子进程的未处理信号设置为空集。
实践:
#include <stdio.h> #include <unistd.h> int g_a = 1; int main(void){ int l_b = 1; pid_t p; if((p = fork()) < 0){ perror("fork"); }else if(p == 0){ g_a++; l_b++; } printf("pid = %d,g_a = %d,l_b = %d\n", getpid(), g_a, l_b); return 0; }
运行结果:
[yan@yanPC apue]$ ./a.out
pid = 15869,g_a = 2,l_b = 2
pid = 15868,g_a = 1,l_b = 1
[yan@yanPC apue]$ ./a.out
pid = 15870,g_a = 1,l_b = 1
pid = 15871,g_a = 2,l_b = 2
pid = 15869,g_a = 2,l_b = 2
pid = 15868,g_a = 1,l_b = 1
[yan@yanPC apue]$ ./a.out
pid = 15870,g_a = 1,l_b = 1
pid = 15871,g_a = 2,l_b = 2
从运行结果来看,子进程的变量进行了改变,而父进程中的变量没有,因为他们是不同的地址空间,不会相互影响。二是父子进程到底哪个先运行是随机的。
2.vfork函数
vfork函数的调用序列和返回值与fork相同,但是两者的语义不同。
1.vfork和fork都是创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会存访该地址空间,相反在子进程调用exec或exit之前,它在父进程的空间中运行。这种优化工作方式在某些unix的页虚拟存储器实现中提高了效率。
2.vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。
实践:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int g_a = 1; int main(void){ int l_b = 1; pid_t p; if((p = vfork()) < 0){ perror("fork"); }else if(p == 0){ g_a++; l_b++; } printf("ppid = %d,pid = %d,g_a = %d,l_b = %d\n", getppid(), getpid(), g_a, l_b); return 0; }
运行结果:
[yan@yanPC apue]$ ./a.out
ppid = 16090,pid = 16091,g_a = 2,l_b = 2
ppid = 15773,pid = 16090,g_a = 2,l_b = 3908224
Segmentation fault
ppid = 16090,pid = 16091,g_a = 2,l_b = 2
ppid = 15773,pid = 16090,g_a = 2,l_b = 3908224
Segmentation fault
出现了段错误,因为父子程序在同一内存空间运行,子程序结束后肯定会释放相关的内存,此时父进程再去访问相关的内存,则出现未知的段错误。将程序修改为如下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int g_a = 1; int main(void){ int l_b = 1; pid_t p; if((p = vfork()) < 0){ perror("fork"); }else if(p == 0){ g_a++; l_b++; _exit(0); //为什么现在好了?? } printf("ppid = %d,pid = %d,g_a = %d,l_b = %d\n", getppid(), getpid(), g_a, l_b); return 0; }
运行结果:
ppid = 15773,pid = 16166,g_a = 2,l_b = 2
在子进程中修改的变量在父进程中打印出来也被进行了修改,说明父子进程使用的是同一块内存空间。
在子进程调用了_exit()函数,使子进程退出,但是不关闭标准的IO流等清理操作,如果使用了exit()函数,那么等到父进程执行时可能会出现意料以外的结果。
下面是vfork的一段官方说明:
vfork() differs from fork(2) in that the parent is suspended until the
child terminates (either normally, by calling _exit(2), or abnormally,
after delivery of a fatal signal), or it makes a call to execve(2).
Until that point, the child shares all memory with its parent, includ‐
ing the stack. The child must not return from the current function or
call exit(3), but may call _exit(2).
child terminates (either normally, by calling _exit(2), or abnormally,
after delivery of a fatal signal), or it makes a call to execve(2).
Until that point, the child shares all memory with its parent, includ‐
ing the stack. The child must not return from the current function or
call exit(3), but may call _exit(2).