linux进程地址空间(1) fork/clone/vfork详解(1)

来源:互联网 发布:矩阵分解的应用 编辑:程序博客网 时间:2024/06/04 00:23

开门见山,在arch/arm/kernel/sys_arm.c文件中,有这样三个函数:sys_forksys_vforksys_clone,它们都是在创建进程,分别对应系统调用fork()vfork()clone(),下面是它们在arm中的函数实现

顺便说一下,系统调用是如何对应到内核接口函数的(fork()->sys_fork),在之后会有文章专门研讨,这里重点讨论的是进程地址空间的问题;

asmlinkage int sys_fork(struct pt_regs *regs)

{

#ifdef CONFIG_MMU

         return do_fork(SIGCHLD, regs->ARM_sp, regs, 0, NULL, NULL);

#else

         /* can not support in nommu mode */

         return(-EINVAL);

#endif

}

/* Clone a task - this clones the calling program thread.

 * This is called indirectly via a small wrapper

 */

asmlinkage int sys_clone(unsigned long clone_flags, unsigned long newsp,

                             int __user *parent_tidptr, int tls_val,

                             int __user *child_tidptr, struct pt_regs *regs)

{

         if (!newsp)

                   newsp = regs->ARM_sp;

         return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);

}

 

asmlinkage int sys_vfork(struct pt_regs *regs)

{

return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->ARM_sp, regs, 0, NULL, NULL);

}

它们都是调用函数do_fork(),其中第一参数不同,fork的是SIGCHLDvfork的是CLONE_VFORK | CLONE_VM | SIGCHLDclone的由调用者决定;

函数do_fork是一个很复杂很复杂的过程,这里仅仅讨论关于进程地址空间的内容,直接它调用函数copy_process,函数copy_process将完成操作系统创建进程的各个方面的事务,函数庞大而复杂,本文只关心进程地址空间方面,直接看它又调用函数copy_mm

这个函数很重要,用于决定子进程的地址空间情况,先把task_structmmactive_mm初始化为NULL,同时取得父进程的mmcurrent->mm,然后是一个重要内容,根据参数clone_flags是否具有CLONE_VM标志界定所创建的进程是否和父进程共享一个地址空间,比如调用vfork时,或者调用clone时使用CLONE_VM标志的情况下,子进程将和父进程共有同一地址空间,具体来说就是父进程的mm就是子进程的mm,父进程的mmmm_users成员加一意为这一mm的使用者加一,子进程的task->mmtask->active_mm均指向父进程的mm;如果没有CLONE_VM标志,那么子进程就得自己创建自己独立的进程地址空间,调用函数dup_mm;整个copy_mm函数的源码如下

static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)

{

         struct mm_struct * mm, *oldmm;

         int retval;

         tsk->min_flt = tsk->maj_flt = 0;

         tsk->nvcsw = tsk->nivcsw = 0;

#ifdef CONFIG_DETECT_HUNG_TASK

         tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;

#endif

         tsk->mm = NULL;

         tsk->active_mm = NULL;

         /*

          * Are we cloning a kernel thread?

          *

          * We need to steal a active VM for that..

          */

         /*取得当前进程的mm,当前进程正在创建新进程*/

         oldmm = current->mm;

         if (!oldmm)

                   return 0;

    /*一旦有标志CLONE_VM,说明父子进程共享同一地址空间,

   这样新进程的mm就是父进程的mm,只需增加父进程的地址空间引用计数即可*/

         if (clone_flags & CLONE_VM) {

                   atomic_inc(&oldmm->mm_users);

                   mm = oldmm;

                   goto good_mm;

         }

    /*没有CLONE_VM标志,说明父子进程不要共享同一地址空间,

      换句话说就是需要子进程自己创建自己的地址空间*/

         retval = -ENOMEM;

         mm = dup_mm(tsk);

         if (!mm)

                   goto fail_nomem;

good_mm:

         /* Initializing for Swap token stuff */

         mm->token_priority = 0;

         mm->last_interval = 0;

    /*设置taskmm,active_mm字段*/

         tsk->mm = mm;

         tsk->active_mm = mm;

         return 0;

fail_nomem:

         return retval;

}

可见如果是与父进程共享进程地址空间的话是非常简单的,它无需创建子进程的进程地址空间;同时可以发现,vfork其实是clone的一种实现,对于vfork,子进程将阻塞父进程的运行直到子进程退出或转而执行其他应用程序为止;

父子进程虽然共享同一进程地址空间,但实际上运行互不影响,这是COW即写时复制实现这一点,父子进程任何一方向写共享页时,会触发缺页异常的COW,这时会从buddy申请一个新页给触发者,并把那个共享页的内容复制到这个新页,需要注意共享页本身依然是写保护,只是当另外一方也要写共享页时,linux可以判断出只有这一个进程使用该页,就会使该页属性变为可写,具体过程在后面会详述。

下面详细描述需要子进程创建自己的mm的情况,关注函数dup_mm,在这里能看到子进程的mm_struct变量的创建和初步初始化及调用重点函数dup_mmapdup_mm的目的就是创建它的mm并把父进程的mm尤其它的vma、二级页表映射的页都放进子进程的mm,源码如下

struct mm_struct *dup_mm(struct task_struct *tsk)

{

         struct mm_struct *mm, *oldmm = current->mm;

         int err;

    /*每个进程都是由另一个父进程创建的子进程,init 1进程是所有进程的祖先

      如果没有父进程,那么失败返回NULL*/

         if (!oldmm)

                   return NULL;

    /*slab分配一个mm_struct结构,作为子进程的mm

      并把父进程的内容全都拷贝进去*/

         mm = allocate_mm();

         if (!mm)

                   goto fail_nomem;

         memcpy(mm, oldmm, sizeof(*mm));

         /* Initializing for Swap token stuff */

    /*与交换(swap)相关的初始化*/

         mm->token_priority = 0;

         mm->last_interval = 0;

    /*初始化新进程的mm*/

         if (!mm_init(mm, tsk))

                   goto fail_nomem;

    /*对于arm为空函数*/

         if (init_new_context(tsk, mm))

                   goto fail_nocontext;

         dup_mm_exe_file(oldmm, mm);

         /*继承父进程的vmavma内虚拟地址对物理地址的映射*/

         err = dup_mmap(mm, oldmm);

         if (err)

                   goto free_pt;

         mm->hiwater_rss = get_mm_rss(mm);

         mm->hiwater_vm = mm->total_vm;

         if (mm->binfmt && !try_module_get(mm->binfmt->module))

                   goto free_pt;

         return mm;

free_pt:

         /* don't put binfmt in mmput, we haven't got module yet */

         mm->binfmt = NULL;

         mmput(mm);

fail_nomem:

         return NULL;

fail_nocontext:

         /*

          * If init_new_context() failed, we cannot use mmput() to free the mm

          * because it calls destroy_context()

          */

         mm_free_pgd(mm);

         free_mm(mm);

         return NULL;

}

首先通过slab分配一个mm,然后把父进程的mm的全部内容拷贝到子进程的mm,这是一个明显的继承动作,然后调用函数mm_init对子进程的mm再进行一些初始化,最后调用函数dup_mmap把父进程的vma和二级页表映射的页全都继承过来

先看下函数mm_init,它首先初始化mmmm_users成员和mm_count成员为1mm_users的意思是这个mm被多少个进程引用,比如父进程用一个mm,这时有个子进程与父进程共享进程地址空间,那么mm_users就要加一;

mm_count的意思比较乱一些,linux来说,用户进程和内核线程都是一个个的task_struct的实例,唯一的区别是内核线程是没有进程地址空间的,也没有mm,所以内核线程的tsk->mm域是NULL(只在内核初始化时以init_mm作为内核的mm);而在切换上下文时,会根据tsk->mm判断即将调度的是用户进程还是内核线程,虽然内核线程不会去访问用户进程地址空间,但它仍然需要页表来访问它自己的空间即内核空间,不过对于任何用户进程来说,他们的内核空间都是完全相同的,所以内核可以借用上一个被调用的用户进程的mm中的页表来访问内核地址,这个mm就记录在mmactive_mm成员;

简而言之就是,对于内核线程,tsk->mm == NULL表示自己内核线程的身份,而tsk->active_mm是借用上一个用户进程的mm,用这个mm的页表来访问内核空间;对于用户进程,tsk->mm == tsk->active_mm

为了这个原因,mm_struct里面引入了另外一个countermm_count;刚才说过mm_users表示这个进程地址空间被多少进程共享,则mm_count表示这个地址空间被内核线程引用的次数,比如一个进程A3个线程,那么这个Amm_structmm_users值为3mm_count1mm_countprocess levelcounter

维护2个计数的用处呢?考虑这样的情况,内核调度完用户进程A以后,切换到内核内核线程BB借用Amm描述符以访问内核空间,这时mm_count变成了2,同时另外一个cpu core调度了A并且进程A exit,这个时候mm_users变为了0mm_count变为了1,但是内核不会因为mm_users==0而销毁这个原先用户进程Amm_struct,因为内核线程B还在使用!只会当mm_count==0的时候才会释放这个mm_struct,因为这个时候既没有用户进程使用这个地址空间,也没有内核线程引用这个地址空间。

回到函数mm_init,注意下这个初始化,“set_mm_counter(mm, file_rss, 0);set_mm_counter(mm, anon_rss, 0);”,这是说这个mm的匿名映射和文件映射的个数,所谓匿名映射是相对于文件映射说的,简单的说就是不是文件映射的就是匿名映射,本质都是对vma线性区的映射;然后注意下free_area_cache成员和cached_hole_size的初始化,这两个成员是用于查找空闲的vma地址空间用的,后面会看到它们的用途;

最后看一下mm_init的最重要的初始化,mm_alloc_pgd,这是创建子进程的内存页表,我们知道内核有一个内存页表,确切的说是一个一级页表,用户进程也一样,每个进程都有一个自己的内存一级页表,函数调用顺序是:mm_alloc_pgd->pgd_alloc->get_pgd_slow,看下函数get_pgd_slow,只需看该函数的前几行即可

pgd_t *get_pgd_slow(struct mm_struct *mm)

{

         pgd_t *new_pgd, *init_pgd;

         pmd_t *new_pmd, *init_pmd;

         pte_t *new_pte, *init_pte;

 /*buddy获取4个连续的物理页,返回虚拟起始地址new_pgd作为一级页表条目*/

         new_pgd = (pgd_t *)__get_free_pages(GFP_KERNEL, 2);

         if (!new_pgd)

                   goto no_pgd;

         memset(new_pgd, 0, FIRST_KERNEL_PGD_NR * sizeof(pgd_t));

         ……………………….

}

调用函数__get_free_pagesbuddy获取4页连续物理内存,这就是子进程的内存一级页表,4页内存共16K空间,每个一级页表条目占8字节,这样就是2048个条目,每个一级页表条目可寻址2MB,所以一共可以寻址4G的空间,这就是子进程的内存页表。页表的地址将存在子进程mmpgd成员。

回到dup_mm,现在已经拷贝了父进程的mm的全部内容并又初始化了一部分内容,接下来调用函数dup_mmap,把父进程的vma和里边映射的物理页都继承下来,这是最重要的内容,这个函数的本质内容是:

1、 父进程的mm有多少个vma,子进程就创建多少个vma

2、 并把父进程的所有vma管理结构变量的内容都全盘复制到子进程的所有vma

3、 父进程的每个vma都映射了哪些物理页,子进程的vma也映射哪些物理页;

4、 父进程和子进程对这些物理页的二级映射都设置为写保护,这是为了实现写时复制COW(后面会看到)

5、 此外子进程的vma相关成员需要做一些初始化;

 

dup_mmap函数描述见下一篇......