fork()系统调用通过复制一个现有进程来创建一个全新的进程(进程的另外一个名字叫做任务)。

进程存放在一个叫做任务队列双向循环链表中。
链表中的每一项都是类型为task_struct称为进程描述符的结构。(它包含一个具体进程的所有信息)

进程描述符的存放:
内核通过一个唯一的进程标识值PID来标识每个进程。//最大值默认为32768,short int短整型的最大值,它就是系统中允许同时存在的进程的最大数目

进程状态
(1)TASK_RUNNING(运行)
(2)TASK_INTERRUPTIBLE(可中断)
(3)TASK_UNINTERRUPTIBLE(不可中断)
(4)TASK_ZOMBIE(僵死)
(5)TASK_STOPPED(停止)

fork():
父子进程区别
子进程和父进程的区别仅仅在于PID,PPID,某些资源统计量(挂起的信号,没有必要被继承)

写时拷贝(个人认为很重要)
linux的fork()使用写时拷贝页实现。
写时拷贝:一种可以推迟甚至免除拷贝数据的技术
使用写时拷贝,内核并不复制整个进程地址空间,而是让父子进程共享同一个拷贝。只有需要写入的时候,数据才会被复制,从而各个进程拥有各自的拷贝。
资源的复制只有在需要写入的时候才进行,在这之前只是以只读方式共享,本质是将地址空间上的页的拷贝推迟到实际发生写入的时候

fork()的实际开销:赋值父进程的页表(页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系)以及给子进程创建唯一的进程描述符。
(一般情况下,进程创建后都会马上运行一个可执行的文件。)

实现:
linux通过clone()系统调用实现fork()。
fork(),vfork(),和_clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()
do_fork()完成了创建中的大部分工作,它定义在kernel/fork.c中。该函数调用copy_process(),接下来copy_process()实现的工作如下
1.调用dup_task_struct()为新进程创建一个内核栈,threaad_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。

2.检查新创建的这个子进程后,当前用户所拥有的进程数目没有超出给他分配的资源的限制。

3.现在,子进程着手使自己与父进程区别拷来。进程描述符内的许多成员都要被清0或设为初始值。进程描述符的成员值并不是继承而来的,而主要是统计信息,进程描述符中大多数的数据都是共享的。

4.接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE(不可中断)以保证它不会投入运行。

5.copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0.表名进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。

6.调用get_pid()为新进程获取一个有效的PID

7.根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。

8.让父进程和子进程平分剩余的时间片

9.最后copy_process()做扫尾工作并返回一个指向子进程的指针

再回到do_fork()函数,如果copy_process函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

Logo

更多推荐