进程系统调用——fork函数深入理解

原创作品 转载请注明出处http://blog.csdn.net/always2015/article/details/45008785

当我们在一个现代系统上运行一个程序的时候,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一程序。我们的程序好像是独占的使用处理器和存储器。处理器就是无间断的一条一条地执行我们程序中的指令。最后我们程序中的代码和数据显得好像是系统存储器中唯一的对象。这些假象都是通过进程的概念提供给我们的。下面我们来看一下一段程序:

#include 
#include 
#include 
int main(int argc, char * argv[])
{
    int pid;
    /* fork another process */
    pid = fork();
    if (pid < 0) 
    { 
        /* error occurred */
        fprintf(stderr,"Fork Failed!");
        exit(-1);
    } 
    else if (pid == 0) 
    {
        /* child process */
        printf("This is Child Process!\n");
    } 
    else 
    {  
        /* parent process  */
        printf("This is Parent Process!\n");
        /* parent will wait for the child to complete*/
        wait(NULL);
        printf("Child Complete!\n");
    }
}

运行结果如下:
进程系统调用——fork函数深入理解_第1张图片

在没有知道进程这个概念的时候,我们看到代码,可能会认为整个代码中的if_else语句只有一个执行,要么if,要么else。但是当我们看完结果我们会感到惊讶。为什么else if和else语句都被执行呢?是不是if_else的结构被破坏了。其实不是的,这就fork的作用。fork是干什么用的呢?如何理解父进程和子进程呢?下面我们就来看看了解一下fork的一些基本知识。

fork()知识总览

fork()函数又叫计算机程序设计中的分叉函数,fork是一个很有意思的函数,它可以建立一个新进程,把当前的进程分为父进程和子进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的PID,而子进程中的返回值则返回 0。因此,可以通过返回值来判定该进程是父进程还是子进程。还有一个很奇妙的是:fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

新创建的子进程几乎但是不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同(但是独立)的一份拷贝,包括文本,数据和bss段、堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。这就是意味着当父进程调用fork时候,子进程还可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大区别在于他们有着不同的PID。
UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。
下面我们再以一个最简单的代码来更简单说明fork()函数:

#include 
#include 
int mian(void){
   fork();
   printf("hello");
   return 0;
}

他的运行结果我们以一个图的形式展现出来,如下:
进程系统调用——fork函数深入理解_第2张图片

这个例子简单明了的把fork的作用表现出来了。在这里父进程和子进程都执行printf(),所以会输出两个hello。在这里没有用到fork函数返回值得问题。其实fork函数返回值刚才上面已经说过了。如果fork出现错误,则fork返回一个负值。当然在用户态的时候的时候我们调用的是fork,但是他在内核中是如何工作的,我们可以以到内核代码去瞧一瞧,稍微的分析一下。这也是我们这一篇博客写作的重点。

fork()内核处理过程

在这里我们会提很多疑问,比如说,当在用户态调用fork()函数的时候,系统的内核是如何执行这个函数的,子进程在内核是从哪里执行的?他的堆栈有哪些变化呢?当然说道进程,那么进程控制块PCB我们肯定是要了解的。进程控制块PCB是干什么用的呢?为了描述和控制进程的运行,系统为每一个进程定义了一个数据结构——进程控制块。它是进程实体的一部分,是操作系统中最重要的记录型数据结构。或者说,OS是根据PCB来对并发程序的进程进行控制和管理的。总而言之,PCB是进程存在的唯一标志。进程控制块中的信息包括进程标识符、处理机状态、进程调度信息、进程控制信息。然而PCB在linux中具体实现是 task_struct数据结构,由于这个数据结构是相当庞大的,我们给出把一个链接(task_struct数据结构),可以到该链接下去看看。

Linux下用于创建进程的API有三个fork,vfork和clone,这三个函数分别是通过系统调用sys_fork,sys_vfork以及sys_clone实现的
(这里目前讨论的都是基于x86架构的)。而且这三个系统调用,都是通过do_fork来实现的,只是传入了不同的参数。所以我们可以得出结论:所有的子进程是在do_fork实现创建和调用的。下面我们就来整理一下整个进程的在用户态到内核态的过程是怎么样的。fork系统调用如下:
进程系统调用——fork函数深入理解_第3张图片

下面我们来重点看看do_fork的代码。http://codelab.shiyanlou.com/xref/linux-3.18.6/kernel/fork.c#do_fork

long do_fork(unsigned long clone_flags,
    unsigned long stack_start,
    unsigned long stack_size,
    int __user *parent_tidptr,
    int __user *child_tidptr)
{
    struct task_struct *p;
    int trace = 0;
    long nr;

    /*
    * Determine whether and which event to report to ptracer.  When
    * called from kernel_thread or CLONE_UNTRACED is explicitly
    * requested, no event is reported; otherwise, report if the event
    * for the type of forking is enabled.
    */
    if (!(clone_flags & CLONE_UNTRACED)) {
        if (clone_flags & CLONE_VFORK)
            trace = PTRACE_EVENT_VFORK;
        else if ((clone_flags & CSIGNAL) != SIGCHLD)
            trace = PTRACE_EVENT_CLONE;
        else
            trace = PTRACE_EVENT_FORK;

        if (likely(!ptrace_event_enabled(current, trace)))
            trace = 0;
    }

    p = copy_process(clone_flags, stack_start, stack_size,
        child_tidptr, NULL, trace);
    /*
    * Do this prior waking up the new thread - the thread pointer
    * might get invalid after that point, if the thread exits quickly.
    */
    if (!IS_ERR(p)) {
        struct completion vfork;
        struct pid *pid;

        trace_sched_process_fork(current, p);

        pid = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);

        if (clone_flags & CLONE_PARENT_SETTID)
            put_user(nr, parent_tidptr);

        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        wake_up_new_task(p);

        /* forking complete and child started to run, tell ptracer */
        if (unlikely(trace))
            ptrace_event_pid(trace, pid);

        if (clone_flags & CLONE_VFORK) {
            if (!wait_for_vfork_done(p, &vfork))
                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
        }

        put_pid(pid);
    } else {
        nr = PTR_ERR(p);
    }
    return nr;
}

整段代码挺长,涉及到很多工作的处理,但是整个创建新进程是在上述代码的第29行copy_process()z这个函数实现的。我们前面已经说过,子进程是通过复制实现的。为了探个究竟,我们进入到copy_process()这个函数体里可以看到几个很重要的函数,列举如下:

复制一个PCB——task_struct

p = dup_task_struct(current);

复制当前进程的PCB描述符task_struct。我们在进入到该函数dup_task_struct体内就可以看到这个pcb是如何复制的。主要的赋值函数是

err = arch_dup_task_struct(tsk, orig);//这一句是赋值操作

当然在dup_task_struct函数体内还有其他的一次辅助操作例如:

tsk = alloc_task_struct_node(node);
ti = alloc_thread_info_node(tsk, node);
tsk->stack = ti;
setup_thread_stack(tsk, orig);//这里只是复制thread_info,而非复制内核堆栈

然而我们再 往dup_task_struct(current)函数往下看,后面是大量的修改进程的内容,也就是对复制过来的东西修改为子进程所拥有的数据。也就是初始化一个子进程。我们再往下看,在copy_process()函数http://codelab.shiyanlou.com/xref/linux-3.18.6/kernel/fork.c#copy_process的第1396行有一个非常重要的函数copy_thread,他是干什么的呢?我们点该函数,然后选择/linux-3.18.6/arch/x86/kernel/,然后点击进入copy_thread()函数体内瞧一瞧。进去之后我们可以看到,一部分重要代码如下:

struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err;

p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));

if (unlikely(p->flags & PF_KTHREAD)) {
    /* kernel thread */
    memset(childregs, 0, sizeof(struct pt_regs));
    p->thread.ip = (unsigned long) ret_from_kernel_thread;
    task_user_gs(p) = __KERNEL_STACK_CANARY;
    childregs->ds = __USER_DS;
    childregs->es = __USER_DS;
    childregs->fs = __KERNEL_PERCPU;
    childregs->bx = sp; /* function */
    childregs->bp = arg;
    childregs->orig_ax = -1;
    childregs->cs = __KERNEL_CS | get_kernel_rpl();
    childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
    p->thread.io_bitmap_ptr = NULL;
    return 0;
}
*childregs = *current_pt_regs();//拷贝父进程的内核堆栈栈底,也就是已有的内核堆栈数据的拷贝
childregs->ax = 0;//给eax赋值为0,因为子进程返回的是0,系统调用是通过eax返回的,
if (sp)
    childregs->sp = sp;//修改栈顶

p->thread.ip = (unsigned long) ret_from_fork;//给ip赋值,这就是子进程执行的起点

从用户态的代码看fork();函数返回了两次,即在父子进程中各返回一次,父进程从系统调用中返回比较容易理解,子进程从系统调用中返回,那它在系统调用处理过程中的哪里开始执行的呢?这就涉及子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题,这是在哪里设定的?copy_thread in copy_process

struct pt_regs *childregs = task_pt_regs(p);
*childregs = *current_pt_regs(); //复制内核堆栈栈底
childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因!

p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址

上面赋值复制的内核堆栈并不是父进程的所有内核堆栈的内容,那复制的是哪些部分呢?我们可以看上面代码的第一句,其中复制的内容就是pt_regs里面的内容。里面的代码如下:

struct pt_regs {
    long ebx;
    long ecx;
    long edx;
    long esi;
    long edi;
    long ebp;
    long eax;
    int  xds;
    int  xes;
    int  xfs;
    int  xgs;
    long orig_eax;
    long eip;
    int  xcs;
    long eflags;
    long esp;
    int  xss;
};

父进程堆栈复制给子进程的就是上面那些参数。从copy_thread中我们就已经得出堆栈复制和子进程开始执行的起始地方。综上所述,我们对整个do_fork的分析到此就可以告一段落了。我们在回过头来总结一下,do_fork()的实现,主要是靠copy_process()完成的,这就是一环套一环。整个过程实现如下:

  1. p = dup_task_struct(current); 为新进程创建一个内核栈、thread_iofo和task_struct,这里完全copy父进程的内容,所以到目前为止,父进程和子进程是没有任何区别的。

  2. 为新进程在其内存上建立内核堆栈

  3. 对子进程task_struct任务结构体中部分变量进行初始化设置,检查所有的进程数目是否已经超出了系统规定的最大进程数,如果没有的话,那么就开始设置进程描诉符中的初始值,从这开始,父进程和子进程就开始区别开了。

  4. 把父进程的有关信息复制给子进程,建立共享关系

  5. 设置子进程的状态为不可被TASK_UNINTERRUPTIBLE,从而保证这个进程现在不能被投入运行,因为还有很多的标志位、数据等没有被设置

  6. 复制标志位(falgs成员)以及权限位(PE_SUPERPRIV)和其他的一些标志

  7. 调用get_pid()给子进程获取一个有效的并且是唯一的进程标识符PID

  8. return ret_from_fork;返回一个指向子进程的指针,开始执行

总结

linux创建一个新的进程是从复制开始的,在系统内核里首先是将父进程的进程控制块PCB进行拷贝,然后再根据自己的情况修改相应的参数,获取自己的进程号,再开始执行。我觉得整个过程重点就是理解子进程如何创建,在内核调用的几个重要的内核函数,以及子进程怎么返回开始执行的。把握这些点就OK了

你可能感兴趣的:(【linux应用】)