Linux C编程--fork 详解

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

               

在Linux系统下学习一个系统函数最好的方法就是阅读其源码,首先,给出fork函数的源码

/**  linux/kernel/fork.c*                                //--fork()用于创建子进程*  (C) 1991  Linus Torvalds*//**  'fork.c' contains the help-routines for the 'fork' system call* (see also system_call.s), and some misc functions ('verify_area').* Fork is rather simple, once you get the hang of it, but the memory* management can be a bitch. See 'mm/mm.c': 'copy_page_tables()'*/#include <errno.h>#include <linux/sched.h>#include <linux/kernel.h>#include <asm/segment.h>#include <asm/system.h>                                //--写页面验证,若页面不可写,则复制页面extern void write_verify(unsigned long address);long last_pid=0;                                //--进程空间区域写前验证函数void verify_area(void * addr,int size){    unsigned long start;    start = (unsigned long) addr;    size += start & 0xfff;    start &= 0xfffff000;    start += get_base(current->ldt[2]);        //--逻辑地址到线性地址的转换    while (size>0) {        size -= 4096;        write_verify(start);        start += 4096;    }}int copy_mem(int nr,struct task_struct * p)        //--复制内存页表{                                                //--由于采用写时复制技术,这里只复制目录和页表项,不分配内存    unsigned long old_data_base,new_data_base,data_limit;    unsigned long old_code_base,new_code_base,code_limit;    code_limit=get_limit(0x0f);                    //--取段限长    data_limit=get_limit(0x17);    old_code_base = get_base(current->ldt[1]);    old_data_base = get_base(current->ldt[2]);    if (old_data_base != old_code_base)        panic("We don't support separate I&D");    if (data_limit < code_limit)        panic("Bad data_limit");    new_data_base = new_code_base = nr * TASK_SIZE;    p->start_code = new_code_base;    set_base(p->ldt[1],new_code_base);    set_base(p->ldt[2],new_data_base);    if (copy_page_tables(old_data_base,new_data_base,data_limit)) {        //--复制页表        free_page_tables(new_data_base,data_limit);        return -ENOMEM;    }    return 0;}/**  Ok, this is the main fork-routine. It copies the system process* information (task[nr]) and sets up the necessary registers. It* also copies the data segment in it's entirety.*/                                    //--fork()子程序,它复制系统进程信息,设置寄存器,复制数据段(代码段)int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,        long ebx,long ecx,long edx, long orig_eax,         long fs,long es,long ds,        long eip,long cs,long eflags,long esp,long ss)        //--复制进程{    struct task_struct *p;    int i;    struct file *f;    p = (struct task_struct *) get_free_page();                //--为新任务数据结构分配内存    if (!p)        return -EAGAIN;    task[nr] = p;    *p = *current;    /* NOTE! this doesn't copy the supervisor stack */    p->state = TASK_UNINTERRUPTIBLE;    p->pid = last_pid;    p->counter = p->priority;    p->signal = 0;    p->alarm = 0;    p->leader = 0;        /* process leadership doesn't inherit */    p->utime = p->stime = 0;    p->cutime = p->cstime = 0;    p->start_time = jiffies;    p->tss.back_link = 0;    p->tss.esp0 = PAGE_SIZE + (long) p;    p->tss.ss0 = 0x10;    p->tss.eip = eip;    p->tss.eflags = eflags;    p->tss.eax = 0;    p->tss.ecx = ecx;    p->tss.edx = edx;    p->tss.ebx = ebx;    p->tss.esp = esp;    p->tss.ebp = ebp;    p->tss.esi = esi;    p->tss.edi = edi;    p->tss.es = es & 0xffff;    p->tss.cs = cs & 0xffff;    p->tss.ss = ss & 0xffff;    p->tss.ds = ds & 0xffff;    p->tss.fs = fs & 0xffff;    p->tss.gs = gs & 0xffff;    p->tss.ldt = _LDT(nr);    p->tss.trace_bitmap = 0x80000000;    if (last_task_used_math == current)        __asm__("clts ; fnsave %0 ; frstor %0"::"m" (p->tss.i387));    if (copy_mem(nr,p)) {        task[nr] = NULL;        free_page((long) p);        return -EAGAIN;    }    for (i=0; i<NR_OPEN;i++)                    //--如果父进程中有文件是打开的,则将对应文件的打开次数增1        if (f=p->filp[i])            f->f_count++;    if (current->pwd)        current->pwd->i_count++;    if (current->root)        current->root->i_count++;    if (current->executable)        current->executable->i_count++;    if (current->library)        current->library->i_count++;    set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));    //--在GDT表中设置新任务的TSS和LDT    set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));    p->p_pptr = current;    p->p_cptr = 0;    p->p_ysptr = 0;    p->p_osptr = current->p_cptr;    if (p->p_osptr)        p->p_osptr->p_ysptr = p;    current->p_cptr = p;    p->state = TASK_RUNNING;    /* do this last, just in case */    return last_pid;}int find_empty_process(void)                        //--为新进程取得不重复的进程号last_pid{    int i;    repeat:        if ((++last_pid)<0) last_pid=1;        for(i=0 ; i<NR_TASKS ; i++)            if (task[i] && ((task[i]->pid == last_pid) ||                        (task[i]->pgrp == last_pid)))                goto repeat;    for(i=1 ; i<NR_TASKS ; i++)        if (!task[i])            return i;    return -EAGAIN;}

以下给出说明:

 fork函数
#include <sys/types.h> 
#include <unistd.h>  pid_t fork(void);

fork调用失败则返回-1,调用成功的返回值见下面的解释。我们通过一个例子来理解fork是怎样创建新进程的。

这个程序的运行过程如下图所示。


  1. 父进程初始化。

  2. 父进程调用fork,这是一个系统调用,因此进入内核。

  3. 内核根据父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态代码和数据也相同。因此,子进程现在的状态看起来和父进程一样,做完了初始化,刚调用了fork进入内核,还没有从内核返回

  4. 现在有两个一模一样的进程看起来都调用了fork进入内核等待从内核返回(实际上fork只调用了一次),此外系统中还有很多别的进程也等待从内核返回。是父进程先返回还是子进程先返回,还是这两个进程都等待,先去调度执行别的进程,这都不一定,取决于内核的调度算法。

  5. 如果某个时刻父进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是子进程的id,是一个大于0的整数,因此执下面的else分支,然后执行for循环,打印"This is the parent\n"三次之后终止。

  6. 如果某个时刻子进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是0,因此执行下面的if (pid == 0)分支,然后执行for循环,打印"This is the child\n"六次之后终止。fork调用把父进程的数据复制一份给子进程,但此后二者互不影响,在这个例子中,fork调用之后父进程和子进程的变量messagen被赋予不同的值,互不影响。

  7. 父进程每打印一条消息就睡眠1秒,这时内核调度别的进程执行,在1秒这么长的间隙里(对于计算机来说1秒很长了)子进程很有可能被调度到。同样地,子进程每打印一条消息就睡眠1秒,在这1秒期间父进程也很有可能被调度到。所以程序运行的结果基本上是父子进程交替打印,但这也不是一定的,取决于系统中其它进程的运行情况和内核的调度算法,如果系统中其它进程非常繁忙则有可能观察到不同的结果。另外,读者也可以把sleep(1);去掉看程序的运行结果如何。

  8. 这个程序是在Shell下运行的,因此Shell进程是父进程的父进程。父进程运行时Shell进程处于等待状态,当父进程终止时Shell进程认为命令执行结束了,于是打印Shell提示符,而事实上子进程这时还没结束,所以子进程的消息打印到了Shell提示符后面。最后光标停在This is the child的下一行,这时用户仍然可以敲命令,即使命令不是紧跟在提示符后面,Shell也能正确读取。

fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。从上图可以看出,一开始是一个控制流程,调用fork之后发生了分叉,变成两个控制流程,这也就是“fork”(分叉)这个名字的由来了。子进程中fork的返回值是0,而父进程中fork的返回值则是子进程的id(从根本上说fork是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当fork函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。

fork的返回值这样规定是有道理的。fork在子进程中返回0,子进程仍可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无它法。

fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。


           

给我老师的人工智能教程打call!http://blog.csdn.net/jiangjunshow

这里写图片描述

猜你喜欢

转载自blog.csdn.net/yffhhffv/article/details/84076826
今日推荐