fork vfork区别
进程描述符及任务结构
内核把进程的列表存放在任务队列的双向循环链表中,链表中的每一项都是类型为task_struct,称之为进程描述符或者叫做进程控制块的结构,该结构包含着一个具体进程的全部信息。
task_struct在32位操作系统上大小约为1.7KB,看着挺大,但是要考虑到该结构体内包含着一个进程的所有信息,那么也就挺小的了。task_struct包含的信息包括:打开的文件,进程的地址空间,挂起的信号,进程的状态,等等。
通过slab分配器动态分配task_struct,所以只需要在内核栈底开辟一个新的结构struct thread_info,这个thread_info结构体内会存在一个指向task_struct的指针。
进程创建
首先调用fork()通过拷贝当前进程创建一个子进程,然后再调用exec()函数负责读取可执行文件并将其加载到地址空间上进行运行。传统的fork()系统调用是直接把所有的资源复制给新的进程,这种方式是简单但是效率很低,因为要复制的数据可能并不共享。更极端的是,如果创造出来的新进程立刻就需要执行一个新的映像,那么之前的拷贝一点意义也不存在。所以Linux的fork()系统调用使用了写时拷贝技术,写时拷贝技术顾名思义,就以一种写入时才进行拷贝的技术,从而再让父子进程拥有各自的拷贝,也就是说资源的拷贝是在进行写入的时候才进行,在此之前,只是以只读的方式进行共享。
这种技术的实现使得地址空间上的页的拷贝推迟到了写入的时候,在页根本就不会被写入的情况下,就不会重复拷贝了。(比如:fork()之后立即调用exec(),这种优化可以避免拷贝大量根本就不会被使用的数据)
那么fork()的实际开销就是复制父进程的页表,以及给子进程创建唯一的进程描述符pcb。
系统调用fork(),vfork()的区别:
- fork():无参数,资源全部复制,父进程所有的资源都全部通过数据结构的复制,传递给子进程。
- vfork():无参数,除了task_struct结构和系统空间堆栈外,其他的资源全部通过数据结构指针的方式进行复制遗传,所以vfork()出来的是线程而不是进程。vfork()是出于效率的考虑而设计的。
- fork与vfork都可以创建一个进程,但vfork是由fork封装得来的。
其实fork创建的子进程相对独立,当用fork后子进程与父进程同时各自进行自己的程序互不影响,但只有一个终端接受他们两个的输出,所以并不是子进程与父进程随机调用,而只是在同时执行父进程和子进程,只是随机输出,子进程与父进程仍各自有序。
(1)使用fork创建一个进程时,子进程只是完全复制父进程的资源。这样得到的子进程独立于父进程,具有良好的并发性。而使用vfork创建一个子进程时,操作系统并不将父进程的地址空间完全复制到子进程,用vfork创建的子进程共享父进程的地址空间,也就是说子进程完全运行在父进程的地址空间上。子进程对该地址空间中任何数据的修改同样为父进程所见。
(2)使用fork创建一个子进程是哪个进程先运行取决于系统的调度算法。而vfork一个进程时,vfork保证子进程先运行,当他调用exec或exit之后,父进程才可能被调读运行。如果在调用exec或exit之前子进程要依赖父进程的某个行为,就会导致死锁。
因为使用fork创建一个进程时,子进程需要将父进程几乎每种资源都复制,所以fork是一个开销很大的系统调用,这些开销并不是所有情况都需要的。比如fork一个进程后,立即调用exec执行另一个应用程序,那么fork过程中子进程对父进程地址空间的复制将是一个多余的过程。vfork不会拷贝父进程的地址空间,这大大减小了系统的开销。
当用vfork创建进程时,若以return 0 结束则释放局部变量,以exit(0)结束则不会释放。
(p185)fork一个子进程,该子进程中的var和globvar记录的是父进程中var和globvar中的值,而fork之后父进程对变量的改变则不对子进程产生影响。
vfork一个子进程,先执行子进程,子进程沿用父进程中的变量,当以exit(0)结束后,父进程可仍沿用子进程中的变量。当以return 0 结束则释放局部变量,父进程再引用时则会为系统给的随机值。
fork
fork()是创建进程函数。c程序一开始,就会产生 一个进程,当这个进程执行到fork()的时候,会创建一个子进程。此时父进程和子进程是共存的,它们俩会一起向下执行c程序的代码。需要注意!!!子进程创建成功后,fork是返回两个值,一个代表父进程,一个代表子进程:代表父进程的值是一串数字,这串数字是子进程的ID(地址);一个代表子进程,值为0。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
fork()执行的流程
Linux通过clone()系统调用来实现fork(),由于clone()可以自主选择需要复制的资源,所以这个系统调用需要传入很多的参数标志用于指明父子进程需要共享的资源。
fork(),vfork(),_clone()函数都需要根据各自传入的参数去底层调用clone()系统调用,然后再由clone()去调用do_fork()。
do_fork()完成了创建的大部分工作,该函数调用copy_process()函数,然后让进程开始运行。
copy_process()函数完成的工作分为这几步:
-
调用dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct,这些值和当前进程的值相同。也就是说,当前子进程和父进程的进程描述符是一致的。
-
检查一次,确保创建新进程后,拥有的进程数目没有超过给它分配的资源和限制。所有进程的task_struct结构中都有一个数组rlim,这个数组中记载了该进程对占用各种资源的数目限制,所以如果该用户当前拥有的进程数目已经达到了峰值,则不允许继续fork()。这个值为PID_MAX,大小为0x8000,也就是说进程号的最大值为0x7fff,即短整型变量short的大小32767,其中0~299是为系统进程(包括内核线程)保留的,主要用于各种“保护神进程”。
-
子进程为了将自己与父进程区分开来,将进程描述符中的许多成员全部清零或者设为初始值。不过大多数数据都未修改。
-
子进程的状态设置为TASK_UNINTERRUPTIBLE深度睡眠,不可被信号唤醒,以保证子进程不会投入运行。
-
copy_process()函数调用copy_flags()以更新task_struct中的flags成员。其中表示进程是否拥有超级用户管理权限的PF_SUPERPRIV标志被清零,表示进程还没有调用exec()函数的PF_FORKNOEXEC标志也被清零。
-
调用alloc_pid为子进程分配一个有效的PID
-
根据传递给clone()的参数标志,调用do_fork()->copy_process()拷贝或共享父进程打开的文件,信号处理函数,进程地址空间和命名空间等。一般情况下,这些资源会给进程下的所有线程共享。
-
最后,copy_process()做扫尾工作并返回一个指向子进程的指针。
do_fork()函数
如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行,内核一般有意让子进程首先运行,因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间内写入。
①:当系统调用fork()通过sys_fork()进入到do_fork()中的时候,其clone_flags标志为SIGCHILD,也就意味着所有的标志位为0,所以其copy_files(),copy_fs(),copy_sighand(),copy_mm()这四个分别为已打开文件,文件系统信息,信号处理函数,用户存储空间的拷贝工作已经完成。
②:而系统调用vfork(),经过sys_vfork()进入do_fork()中的时候,其clone_flags为VFORK|CLONE_VM|SIGCHILD,所以只会执行copy_files(),copy_fs(),copy_sighand()这三个函数,而copy_mm()函数则因为标志位CLONE_VM为1,所以不会执行,只会通过指针共享的方式共享其父进程的mm_struct结构体,这也就是说,vfork()复制的是个线程,只能靠共享其父进程的存储空间生存,包括用户空间堆栈在内。
③:至于__clone(),则取决于调用时的参数,当然前提是父进程要有,要是父进程都没有,那么即使调用相应函数复制了,也还是空的。
④:前面我们知道在fork一个新进程的时候,第一步dup_task_struct()函数会给新进程创建新的系统空间堆栈,task_struct以及thread_info结构,其中alloc_task_struct()则是创建task_struct用的,并且aa在进程内核栈中创建了thread_info结构体,thread_info结构体中有一个task_struct类型的结构体指针task指向task_struct这个结构体。