fork()的主要作用是把当前进程“复制”一份,并从下一条语句处开始执行,也就是会新产生一个相同的进程。进程是一个正在运行的程序,是资源分配的最小单位,系统管理进程是依靠对进程控制块(PCB)的管理完成的,每个进程的产生分两步,一是:分配PCB,二是准备进程实体,如分配内存空间等。
fork() 、pthread_creat()、 vfork()的系统调用分别是sys_fork()、sys_clone()、sys_vfork(),它们的底层都用的是do_fork(),只是传的参数,和标志不同。
fork() 创建进程:
fork()调用一次,返回2次,子进程的返回值是0,父进程的返回值是新子进程的进程ID。
vfork()创建一个新进程,子进程去调用exec,并不将父进程的地址空间完全复制到子进程中去,因为子进程会立即调用exec(或者exit),于是就不会访问该地址空间并保证子进程先运行,直到子进程调用exec或者exit子后,父进程才会运行。
文件共享:
在fork()之前父进程打开的文件子进程才能使用,一个进程打开的文件描述符是在PCB中记录的,父进程调用fork()创建子进程的过程中,子进程的PCB是拷贝父进程的PCB,父进程所有打开的文件描述符都被复制到子进程中。
父子进程每个相同的打开描述符共享一个文件表项。文件描述符的引用计数count+1,不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均+1
fork属于系统调用,那么调用fork时需要由用户态切入到内核态,具体的调用过程是通过中断(调用号为0x86)机制来实现。由寄存器记录需要调用的系统号,保留现场信息后,进入内核态,进入系统调用表,查找到与fork的系统号。调用和执行系统函数sys_fork()后,调用do_fork来实现fork,实现后又返回给寄存器。如图1:
图 1
fork、 vfork 、clone功能均由do_fork实现,区别在于参数不同。fork下的do_fork对应参数如图2,这个过程会传递一些信息和寄存器:
图 2
对sys_fork()的do_fork()转到定义:
do_fork():
1、定义PCB(进程控制块|进程描述符)指针struct task_struct *p;
2、为子进程分配PID,cat /proc/sys/kernel/pid_max命令可以查看一个系统支持的最大进程数,进程数的范围0~32768,类型为整型(int),最大进程数默认由宏定义为0x8000 -> 32768
3、调用p=copy_process方法(拷贝进程的方法),创建子进程的task_struct.
copy_process()函数的功能概要实现:
1、为子进程分配一个PCB(task_struct)
/*创建进程描述符以及子进程执行所需要的所有其他数据结构,它的参数与do_fork相同。外加子进程的PID。*/
/*分配PCB,继承父进程的PCB中的值,只是将特有的信息改过来。每个进程都有task_thread,thread_info结构体保存的是进程上下文的信息。要修改thread_info *info,子进程的task_struct的成员struct thread_info *info指向自己的struct thread_info,而且struct thread_info结构体的成员struct task_struct *p指向子进程自己的struct task_struct
将current进程描述符的内容复制到tsk所指向的task_struct结构中,然后把tsk_thread_info置为ti
将current进程的thread_info内容复制给ti指向的结构中,并将ti_task置为tsk.
*ti = *orig->thread_info;
*tsk = *orig;
tsk->thread_info = ti;
ti->task = tsk;
*/
调用dup_task_struct函数,为子进程获取进程描述符。一般分配8k或者4k的物理页面,1k用来保存task_struct(PCB),一部分分配thread_info,另外一部分用作子进程的内核堆栈,所以内核堆栈一般小于2个页面。内核堆栈用于存储现场信息。代码如图3:
图 3
其中current执行当前进程的PCB,也就是指向父进程的PCB。期间对current指针检查标志位的合法性。
/*在p=dup_task_struct(current)之前都是对进程的一些判断(检查标志位合法性)和安全性检查。*/
其父子进程之间指向关系如图4:
图 4
调用copy_thread,用发出clone系统调用时CPU寄存器的值(它们保存在父进程的内核栈中)来初始化子进程的内核栈。不过,copy_thread把eax寄存器对应字段的值(这是fork和clone系统调用在子进程中的返回值)强行置为0。子进程描述符的thread.esp字段初始化为子进程内核栈的基地址。ret_from_fork的地址存放在thread.eip中。(用父进程线程现场信息初始化子进程的现场信息,并将子进程中的eax寄存器的值设为0)
这就是为什么父子进程沿着统一位置执行,以及子进程的返回值是0。
2、对PCB的初始化,如自锁器,定时器,如图5:
图 5
3、复制文件描述符/*copy_files()*/,复制进程地址空间/* copy_mm()*/,将父进程的拷贝内存管理mm_struct,文件描述符files,文件系统fs,信号,信号处理函数,如图6:
/*struct mm_struct *mm,*active_mm, mm表示进程所拥有的内存空间的描述符,对于内核线程的mm为NULL; active_mm表示:进程运行时所使用的进程描述符。
判断是否设置了CLONE_VM标志,如果设置,创建线程,新线程共享父进程的地址空间,将mm_users加1,然后mm=oldmm,把父进程的mm_struct指针赋给子进程的mm_struct./*将父进程的时间分一半给子进程,状态置为就绪*/
如果没有设置,当前进程分配一个新的内存描述符,mm=allocate_mm(), 将它的地址放在子进程的mm中。再把父进程(*oldmm)的内容拷进(*mm)中
dup_mmap(mm, oldmm)复制线性区和页表,设置mm的一些属性,改变父进程的私有,可写的页为只读的,以使写时拷贝技术生效。
*/
图 6
其中copy_file()函数复制父进程打开的文件描述符,采用写时拷贝技术,以页面实现,其中copy_mm函数复制地址空间。其中copy_thread将父进程当前保存的各个寄存器的值初始化子进程保存的各个寄存器值,将eax强制设置为0(这里解释子进程为什么返回值为0),所以父子进程的返回值不一样,返回的堆栈指针不同。设置子进程开始执行位置为ret_from_fork的起始地址。
/*父进程内核栈上存放的寄存器的信息复制给子进程,如图7*/
图 7
/*
struct mm_struct{ //指向线性区对象的链表头
struct vm_area_struct*mmap; //指向线性区对象的红黑树的根
struct rb_root mm_rb;
struct mm_struct*mm; //如图8:
*/
图 8
线性地址空间如图9:
图 9
4、调用sched_fork(p)对子进程的静态优先级初始化,并设置子进程为TASK_RUNNING(进程的就绪状态或正在运行)状态。