fork底层实现
1 | pid_t fork(void); |
- fork()系统调用通过复制一个现有进程来创建一个全新的进程。进程被存放在一个叫做任务队列的双向循环链表当中,链表当中的每一项都是类型为 task_struct 称为进程描述符的结构,也就是进程PCB
- 内核通过进程 PID 来标识每一个进程,PID 的最大值默认为 32768 (
2^15
) ,也就是 short int 短整形的最大值。
查看系统允许同时存在的进程数目:cat /proc/sys/kernel/pid_max
当进程调用 fork 后,控制权转移到内核时,内核会:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程(COW 写时拷贝)
- 添加子进程到系统进程列表当中
- fork返回,调度器开始调度
Linux 中,系统调用 fork 、vfork 底层都通过系统调用 clone 函数实现,clone 函数会调用 do_fork 函数,然后 do_fork 函数又会调用 copy_process 函数。这个函数会做如下事情:
- 调用 dup_task_struct() 为新进程创建一个内核栈、thread_info 结构和 task_struct 结构等。此时,子进程和父进程的文件描述符是完全相同的
- 检查前所拥有的进程数目有没有超过分配资源的限制
- 区分父子进程描述符,将子进程所拥有的一些结构体成员清零或者重置
- 子进程状态设置为 TASK_UNINTERRUPTIBLE(不可中断)以保证它不会被运行
- 调用 copy_flags() 更新 task_struct 的 flags 成员。将进程是否拥有超级用户权限的 PF_SUPERPRIV 标志清 0
- 调用 get_pid() 为新进程获取一个有效的 PID
- 根据 clone() 参数,拷贝或共享打开的文件、进程地址空间等
- 处理善后,并返回指向子进程的指针
写时拷贝:
写时复制:是一种采取了惰性优化方法来避免复制时的系统开销。Linux采用了写时复制的方法,以减少fork时对父进程空间进程整体复制带来的开销。它的前提很简单:如果有多个进程要读取它们自己的那部门资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。只要没有进程要去修改自己的“副本”,就存在着这样的幻觉:每个进程好像独占那个资源。从而就避免了复制带来的负担。如果一个进程要修改自己的那份资源“副本”,那么就会复制那份资源,并把复制的那份提供给进程。不过其中的复制对进程来说是透明的。这个进程就可以修改复制后的资源了,同时其他的进程仍然共享那份没有修改过的资源。所以这就是名称的由来:在写入时进行复制。
- 创建子进程时,将父进程的
虚拟内存
与物理内存
映射关系复制到子进程中,并将内存设置为只读(设置为只读是为了当对内存进行写操作时触发缺页异常
)。 - 当子进程或者父进程对内存数据进行修改时,便会触发
写时复制
机制:将原来的内存页复制一份新的,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写。
vfork:
vfork的实现原理非常简单,就是父子进程完全共用一份资源. 就是是有人修改了内容,甚至main()函数退出了也不会新开辟一个空间. 所以这里里会有问题的,如果你的一个子进程没有使用exit()退出,那么程序就会出现段错误.
- 在实现写时复制之前,Unix的设计者们就一直很关注在fork后立刻执行exec所造成的地址空间的浪费。BSD的开发者们在3.0的BSD系统中引入了vfork( )系统调用。(很多情况下 fork 之后会紧接着exec,而exec 的执行相当于之前 fork 复制空间这个过程全部变成了无用功)
- 除了子进程必须要立刻执行一次对exec的系统调用,或者调用_exit( )退出,对vfork( )的成功调用所产生的结果和fork( )是一样的。vfork( )会挂起父进程直到子进程终止或者运行了一个新的可执行文件的映像。通过这样的方式,vfork( )避免了地址空间的按页复制。在这个过程中,父进程和子进程共享相同的地址空间和页表项。实际上vfork( )只完成了一件事:复制内部的内核数据结构。因此,子进程也就不能修改地址空间中的任何内存。
- vfork( )是一个历史遗留产物,Linux本不应该实现它。需要注意的是,即使增加了写时复制,vfork( )也要比fork( )快,因为它没有进行页表项的复制。然而,写时复制的出现减少了对于替换fork( )争论。实际上,直到2.2.0内核,vfork( )只是一个封装过的fork( )。因为对vfork( )的需求要小于fork( ),所以vfork( )的这种实现方式是可行的。
- fork和vfork的区别
- fork( )的子进程拷贝父进程的数据段和代码段;vfork( )的子进程与父进程共享数据段
- fork( )的父子进程的执行次序不确定;vfork( )保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。
- vfork( )保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
- 当需要改变共享数据段中变量的值,则拷贝父进程。