1.fork
fork:创建一个和当前进程映像一样的进程就可以通过fork()系统调用。其定义如下:
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
成功调用fork()会创建一个新的进程,它几乎与调用fork()的进程一模一样,这两个进程都会继续进行。
划重点!!!
fork函数每调用一次都返回两次
在子进程中,成功的fork()调用会返回0。
在父进程中fork()返回子进程的pid。
如果出现错误,fork()返回一个负值。
在fork函数执行完毕后,如果创建新进程成功,我们可以通过fork返回的值来判断当前进程是子进程还是父进程。在父进程中,fork返回新创建子进程的进程ID;在子进程中,fork函数返回0。( pid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的pid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其pid为0。)
fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的ppid被设置成原进程的pid,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。
子进程的代码与父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。数据的复制采用的是写实复制(copy on writte),即只有在任一进程(父进程或子进程)对数据执行了写操作,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。即便如此,如果我们在程序中分配了大量内存,那么使用fork时也应当十分谨慎,尽量避免没必要的内存分配和数据复制。
此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1.不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会加1。
有事我们需要在子进程中执行其他程序,即替换当前进程映像,这就需要使用exec系列函数。
下面举例看看fork的应用吧
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
1.
int main()
{
int i = 0;
for( ; i<2; i++)
{
fork();
printf("A\n");
}
}
分析屏幕上会打印出几个A?
运行结果如下:
打印出了6个A,为什么呢?
分析结果:
2.
int main()
{
int i = 0;
for( ; i<2; i++)
{
fork();
printf("A");
}
}
这段代码会打印出几个A呢?
结果是8个 ,为什么呢
分析结果
根据上面的两道题,思考打印的结果为什么不一样呢?
这是因为printf打印数据也是需要条件的:1.缓冲区放满后,会自带打印出;2.强制刷出缓冲区中的数据,可以使用"\n",fflush(stdout),3.程序结束会将缓冲区中的数据刷出。而我们在fork时也会将该进程缓冲区中的数据拷贝过来,所以导致了结果的不同。
下面还有两道题可以继续练习一下
3.
int main()
{
printf("A");
write(1,"B",1);
fork();
}
直接打印出BAA
4.
int main()
{
fork()||fork();
printf("A\n");
}
2.vfork
在实现写时复制之前,设计者们就一直很关注在fork后立刻执行exec所造成的地址空间的浪费。BSD的开发者们在3.0的BSD系统中引入了vfork()系统调用。
pid_t vfork(void)
除了子进程必须要立刻执行一次对exec的系统调用,或者调用_exec()退出,对vfork()的成功调用所昌盛的结果和fork()是一样的。vfork()会挂起父进程知道子进程终止或者停止运行了一个新的可执行文件的映像。通过这样的方式,vfork()避免了地址空间的按页复制。在这个过程中,父进程和子进程共享相同的地址空间和页表项。实际上vfork()只完成了一件事:复制内部的内核数据结构。因此,子进程也就不能修改地址空间中的任何内存。
vfork()是一个历史遗留产物,Linux本不应该去实现它。需要注意的是,即使增加了写时复制,vfork也要比fork快,因为它没有进行页表项的复制。然而,写时复制的出现减少了对于替换fork的争论。