fork是复制进程,那么首先要清楚进程是什么?
进程是一个正在运行的程序,是资源分配的最小单位,系统管理进程是依靠对进程控制块(PCB)的管理完成的,每个进程的产生分两步,一是:分配PCB,二是 :准备进程实体,如分配内存空间等 .
值得注意的是 :
- 1、fork()调用一次,返回2次,子进程的返回值是 0,父进程的返回值是新子进程的进程ID。
- 2、文件共享 . 在fork之前父进程打开的文件子进程才能使用,一个进程打开的文件描述符是在PCB中记录的,父进程调用fork()创建子进程的过程中,子进程的PCB是拷贝父进程的PCB,父进程的所有打开的文件描述符都被复制到子进程中 . 父子进程每个打开的相同的描述符共享一个文件表项 . 文件描述符的引用计数 count+1,不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均+1 .
- 3.进程被存放在一个叫做任务队列的双向循环链表中 . 链表中的每一项都是类型为task_struct称为进程描述符的结构(或者进程控制块 PCB) . (它包含一个具体进程的所有信息)
- 4.进程描述符的存放:内核通过一个唯一的进程标识值或PID来标识每个进程。(最大值默认为32768,short int短整型的最大值,它就是系统中允许同时存在的进程的最大数目 .)
有三个创建进程的函数fork(),vfork(),clone(),调用时通过中断(128号中断 , 也称0x80 中断 , 是系统调用所用的中断),从用户态进入内核态,进入系统调用,根据调用号不同进入对应系统调用函数 sys_fork() , sys_vfork() , sys_clone() ;但这三个系统函数底层调用都是do_fork();只是传的参数,和标志不同.
- fork() : 创建子进程 , 而且是全面的复制父进程的资源给子进程 .
- vfork() : 创建线程 , 参数和fork参数相同 .原因 : 值拷贝了页面表项 , 具体页面的内容还是在主进程中 . 作为创建进程的中间步骤 , 效率高 .
- clone() : 传的参数不同于前两个 . 1.创建线程 , 可以是内核线程 , 也可以是用户线程(给定子线程的堆栈位置和运行起点) . 2.创建线程 , 可以选择性的复制父进程资源.
注:通过128号终端(即0x80 中断), 从用户态切到内核态 , 进入系统调用处理
大致的流程就是 :
allco_pidmap()给子进程分配pid,task_struct分配进程描述符,调用了copy_process()来实现父进程到子进程的复制 .父进程的时间片分成了两份,父、子各占一半 .再用SET_LINKS§把子进程放入内核进程队列(插在父进程之前),即就是放到了就绪队列上等待。最后将新进程pid插到pidhash散列表中 . 这样一个进程就fork()好了 .
我们对于do_fork() 转到定义:
do_fork():
定义PCB指针struct task_struct *p;
分配PID,(cat /proc/sys/kernel/pid_max命令可以查看一个系统支持的最大进程数) ,进程数的范围0~32768,理论值.
调用alloc_pidmap方法 , 为子进程分配pid
调用copy_process方法,创建子进程的task_struct. copy_process复制进程描述符.如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址.
然后进入copy_process();
在p=dup_task_struct(current)之前都是对进程的一些判断(检查标志位合法性)和安全性检查 .
调用dup_task_struct 为子进程获取进程描述符 , 跳转进去 .
static struct task_struct *dup_task_struct(struct task_struct *orig); (为子进程获取进程描述符)
分配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 .即
然后跳回到copy_process()中
- copy_files(clone_flags, p): copy_files()复制父进程大开的文件描述符 , 这种复制只在clone_flags的标志位是CLONE_FILES且为0时才真正进行 , 为1就是共享父进程的已打开文件.
- copy_fs(clone_flags, p) : 与文件系统有关 , 只在clone_flags的标志位是CLONE_FS且为0时才进行复制
- copy_sighand(clone_flags, p) : 只在clone_flags的标志位是CLONE_SIGHAND且为0时才进行复制
- copy_mm(clone_flags, p) : 对用户空间进行复制 , 同时对深层的vm_area_struct和页面映射表也进行了复制
其中copy_mm(clone_flags, p)复制地址空间,struct mm_struct *mm,*active_mm, mm表示:进程所拥有的内存空间的描述符,对于内核线程的mm为NULL,active_mm表示:进程运行时所使用的进程描述符.
-
1、判断是否设置了CLONE_VM标志 . 如果设置,创建线程,新线程共享父进程的地址空间,将mm_users加1,然后mm=oldmm,把父进程的mm_struct指针赋给子进程的mm_struct.
如果没有设置,当前进程分配一个新的内存描述符,mm=allocate_mm(), 将它的地址放在子进程的mm中. 再把父进程(*oldmm)的内容拷进(*mm)中. -
2、dup_mmap(mm, oldmm)
复制线性区和页表,设置mm的一些属性,改变父进程的私有,可写的页为只读的,以使写时拷贝技术生效.
注意 : 在copy_mm(clone_flags,p); 中 , 对用户空间进行复制,同时利用dup_mmap(struct mm_struct* mm)对深层的vm_area_struct和页面映射表也进行了复制 .
那么创建的是进城还是线程? CLONE_VM为 0 时创建的是新进程(子进程),子进程复制了父进程的页面目录项;CLONE_VM为1时通过复制的指针共享了父进程空间,只通过copy_page_range()复制的是父进程页面表项,而不是页面目录项 .如果说这里生成了子进程,那就会有写时拷贝的存在了 .
写时拷贝 : 一种可以推迟拷贝甚至免除拷贝数据的技术 .
这里存在一个写时拷贝write_on_copy , 使用写时拷贝 , 内核并不复制整个进程地址空间,而是让父子进程共享同一个拷贝 .只有当两个进程中任何一个进程要进行写入操作时 , 此刻就会拷贝一份父进程资源给子进程 .
写时拷贝本质是将地址空间上的页的拷贝推迟到实际发生写入的时候 .
fork()的实际开销:复制父进程的页表(页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系)以及给子进程创建唯一的进程描述符。
至此 , task_struct中低端(用户空间)copy完成 .
我们接着回到copy_process()中
调用copy_thread,用发出clone系统调用时CPU寄存器的值(它们保存在父进程的内核栈中)来初始化子进程的内核栈 .不过,copy_thread把eax寄存器对应字段的值(这是fork和clone系统调用在子进程中的返回值) 强行置为 0 .子进程描述符的thread.esp字段初始化为子进程内核栈的基地址 .ret_from_fork的地址存放在thread.eip中 .如果父进程使用IO权限位图 .则子进程获取该位图的一个拷贝 .
最后,如果CLONE_SETTLS标志被置位,则子进程获取由CLONE系统调用的参数tls指向的用户态数据结构所表示的TLS段 .
注:这就是为什么父子进程沿着统一位置执行,以及子进程的返回值是0.
至此 , task_struct中高端(系统堆栈空间)copy完成 .
SET_LINKS( p ); //把新进程放入内核进程队列(就绪队列)
//把新进程描述符的PID插入pidhash散列表中
attach_pid(p, PIDTYPE_PID, p->pid);
attach_pid(p, PIDTYPE_TGID, p->tgid);
fork()结束