说起fork,我们就会联想它的一系列函数,vfork、clone、do_fork;这些函数都有一些什么区别呢?
fork是全部复制,父进程的所有资源全部通过数据结构的复制“遗传”给子进程;
clone是一个可选择的复制,通过参数的不同,来选择性地拷贝父进程的资源,没有拷贝的则通过指针的赋值,共享父进程的资源;
vfork是一个没有参数的系统调用,除了拷贝父进程的内核栈空间和task_struct结构,其它信息都是共享的;所以,vfork出来的是一个线程;
do_fork是以上三个函数的底层实现,为什么能呈现出不同的功能,是通过传给do_fork的参数不同来实现的;
首先,fork是一个系统调用,我们需要先对于fork进行名字的拼接,找到对应的系统调用号(fork的系统调用号为2),通过eax寄存器传给内核,产生0x80号中断,将用户空间的栈信息(如esp,程序寄存器中的内容等)压入内核栈,在内核栈执行系统调用。
由于fork系统调用的底层调用的是sys_fork,所以,sys_fork带着父进程的各种寄存器的信息,又通过对于clone_flge的设置和利用父进程的寄存器信息,我们接着进入了do_fork的实现;
asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
}
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
do_fork的实现:
<1>定义一个task_struct的结构体指针,紧接着为子进程分配一个pid;
struct task_struct *p;
int trace = 0;
/**
* 通过查找pidmap_array位图,为子进程分配新的pid参数.
*/
long pid = alloc_pidmap();
<2>那么,接下来的函数copy_process就是fork的核心了,它主要是复制PCB(进程描述符),根据标志位clone_flags,来判断子进程是共享父进程中的资源还是自己也应该拥有自己的一份;当copy_process函数返回的时候,我们就获得了一个新的进程的PCB;
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
<3>如果返回的PCB信息没有错误,那么对clone_flags和标志位CLONE_STOPPED进行与操作,判断是否设置子进程为TASK_STOPPED状态,如果是就发送SIGSTOP信号并挂起它;
if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
/*
* We'll start up with an immediate SIGSTOP.
*/
sigaddset(&p->pending.signal, SIGSTOP);
set_tsk_thread_flag(p, TIF_SIGPENDING);
}
<4>如果没有设置CLONE_STOPPED,那么我们就调用wake_up_new_task()函数,它调整父进程和子进程的调度参数,如果父子进程运行在同一个CPU上,并且不能共享同一组页表(CLONE_VM为0)那么就把子进程插入父进程的运行队列中,并且让子进程插在父进程之前,这么做的目的是:如果子进程在创建之后,执行exec,执行新程序,就可以避免写时拷贝机制,执行不必要的拷贝;否则,如果不是运行在同一个CPU上或者父子进程共享同一组页表,就把子进程插入父进程运行队列的尾巴;
if (!(clone_flags & CLONE_STOPPED))
wake_up_new_task(p, clone_flags);
else/*如果CLONE_STOPPED标志被设置,就把子进程设置为TASK_STOPPED状态。*/
p->state = TASK_STOPPED;
<5>如果设置了标志位CLONE_VFORK,就把父进程插入等待队列,并挂起父进程知道子进程结束或者执行了新的程序;
执行到这里,do_fork就执行完了,最后把子进程的pid返回给父进程了,那么子进程的返回值呢,怎么最后子进程的返回值是0呢???那么就让我们进入到下面的内容。
do_fork的核心copy_process的实现:
//此为在do_fork中的调用
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
//此代码为copy_process的实现
static task_t *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
int pid)
<1>首先进行参数的检查,是否合法;
<2>调用dup_task_struct(),为子进程分配内核栈、task_struct,获取进程描述符并对thread_info进行分配和初始化;
p = dup_task_struct(current);
<3>保存新进程的PID,初始化子进程中PCB的各种信息;
p->pid = pid;
<4>接下来就是对父进程中的信息进行复制,其中copy_semundo()(信号量)、copy_files()(文件描述符)、copy_fs()(路径–文件上下文信息)、copy_sighand()(信号处理函数)、copy_signal()(信号)、copy_mm()(虚拟地址空间)、copy_namespace()(命名空间)、copy_thread()(用父进程的内核栈初始化子进程的内核栈,并将子进程的eax的值置为0);
<5>sched_fork(),该函数把新进程的状态置为TASK_RUNNING,并把thread_info结构的preempt_count字段设置为1。从而禁止抢占;此外,为了保证公平调度,父子进程共享父进程的时间片。
<6>SET_LINKS();把新进程加入进程链表;
<7>attach_pid(),把新进程描述符的PID插入pidhash散列表中;
写时拷贝的实现,明天总结!!!