fork()函数是用来通过一个现有的进程创建一个新进程。
一、简述fork()函数
1、函数原型:pid_t fork(void);
2、返回值:0 返回到子进程中
>0 给父进程返回新子进程的进程ID;
-1 出错
3、两个问题:为什么子进程中返回0?为什么要向父进程中返回新子进程的进程ID?
答案:
将子进程的ID返回给父进程是因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。所以在创建时,就要将新子进程的ID返回给父进程;
Fork使子进程得到的返回值为0是因为一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID(进程ID 0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0);
4、fork的过程
子进程和父进程继续执行fork之后的命令。子进程是父进程的读本。例如子进程获得父进程数据空间、栈和堆的副本。但注意,这是子进程独有的副本。父子进程并不共享这些存储空间。父子进程共享正文段。
5、fork所用到的技术——写时拷贝技术
Fork之后,子进程会拷贝父进程的PCB结构,然后对PCB里边的数据做修改。父进程的页表直接拷贝给子进程。父子进程共享所有的数据空间(在最开始的时候)。这些区域由父子进程共享,而且内核将他们的访问权限都改为只读的。如果父子进程任意一方试图修改这些区域,则内核值为修改区域的那块内存制作一个副本 ,通常是虚拟存储器系统中的一“页”。
Fork之后,父子进程相互独立。Fork之前打开的所有文件描述符,父子进程共享fork之前的打开文件的读写偏移量。在此之后,父子进程,并不共享全局数据,堆区数据和栈区数据。
Fork时,子进程的PCB是由父进程拷贝(浅拷贝)而来,父子进程共享fork之前打开的文件。
6、fork之后谁先执行?
一般来说,fork之后是父进程还是子进程先执行是不确定的。这取决于内核所使用的调度算法。如果父子进程之间相互同步,则要求某种形式的进程间通讯;
7、文件共享
Fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。父子进程的每个相同的打开文件描述符共享一个文件表项。
以下是《UNIX高级环境编程》中给出的一种情况:
在fork之后处理文件描述符有两种常见的情况:
(1)父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已执行了相应更新;
(2)父子进程各自执行不同的程序段。在这种情况下,在fork之后,父子进程各自关闭它们不需要的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程中常用到的。
8、父子进程的区别
(1)fork的返回值
(2)进程ID不同
(3)两个进程具有不同的父进程ID;子进程的父进程ID是创建它的父进程的进程ID;父进程的父进程ID则不变;
(4)子进程的tms_utime,tms_stime,tms_cutime以及tms_ustime均被设置为0;
(5)父进程设置的文件锁不会被子进程继承;
(6)子进程的未处理的闹钟(alarm)会被清除;
(7)子进程的未处理信号集设置为空集;
9、fork失败的原因
(1)系统中已经有了太多的进程(通常意味着某个方面出了问题)
(2)该实际用户ID的进程总数超过了系统限制;CHILD_MAX规定了实际用户ID在任意时刻可具有的最大进程数;
10、fork的两种用法
(1)一个父进程希望复制自己,使父子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
(2)一个进程要执行不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec进行进程替换。
11、fork创建进程的流程,其内核实现原理
Linux通过clone系统调用实现fork()。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源、fork、vfork和_clone库函数都根据各自需要的参数标志去调用clone。然后由clone去调用do_fork。
Do_fork完成了创建中的大部分工作,它的定义是在kernel/fork.c文件中,该函数调用copy_process()函数,然后让程序开始运行。Copy_process完成的工作:
①调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
②检查新创建的这个紫禁城后,当前用户所拥有的进程数目没有超出给他分配的资源的限制。
③现在,子进程着手使自己与父进程区别开进程描述符内的许多成员都要被清0或者设为初始值。 进程描述符的成员值并不是继承而来的,而主要是统计信息。进程描述符中的大多数数据都是共享的。
④接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。
⑤copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
⑥调用get_pid()为新进程获取一个有效的PID。
⑦根据传递给clone()的参数标志,copy_process()拷贝或者共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享。否则,这些资源对每个进程是不同的,因此被拷贝到这里。
⑧让父进程和子进程平分剩余的时间片
⑨最后,copy_process()作扫尾工作并返回一个指向紫禁城的指针。
再回到do_fork()函数,如果copy_process函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec函数,这样就可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。
二、fork和vfork函数的区别
Vfork函数的调用序列和返回值与frok相同。但两者的语义不同。
区别:
1、Vfork用于创建一个新进程,而该新进程的目的是exec一个新程序。Vfork和frok一样都创建一个子进程,但是vfork并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec函数,于是也就不会存访该地址空间;
2、vfrok保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁)。