linux内核之进程地址空间

本文详细介绍了Linux系统中进程的地址空间概念、内存描述符结构及其实现机制、虚拟内存区域的数据结构与相关操作等内容。

1、地址空间

         进程的地址空间由进程可寻址的虚拟内存组成。每个进程都有自己独立的地址空间,两个进程之间没有关系,互不影响。这些虚拟内存区域包含各种对象,常见的有如下:

代码段:可执行文件代码的内存映射

数据段:可执行文件的已初始化全局变量的内存映射

Bss段:包含未初始化的全局变量,也称bss段零页的内存映射

用户进程用户空间栈的零页内存映射,栈大小编译时指定

每个共享库的代码段、数据段和bss段也会被载入进程的地址空间

任何内存映射文件

任何共享内存段

任何匿名的内存映射,比如malloc

         进程地址空间中任何有效地址都只能位于唯一的区域,这些内存区域不能相互覆盖。

2、内存描述符

(1)数据结构

内核使用内存描述符结构体标识进程的地址空间,该数据结构包含了和进行地址空间相关的全部信息。

struct task_struct {

struct mm_struct *mm,*active_mm;    //进程任务描述符指向内存描述符,mm为进程地址空间所有信息,active_mm则为当前执行的地址空间信息

}

struct mm_struct {

   struct vm_area_struct * mmap;      //内存区域链表

   struct rb_root mm_rb;                                //内存区域形成的红黑树

   struct vm_area_struct * mmap_cache; //最近使用的内存区域

#ifdef CONFIG_MMU

   unsigned long (*get_unmapped_area) (struct file *filp,

               unsigned long addr,unsigned long len,

                unsigned long pgoff, unsignedlong flags);

   void (*unmap_area) (struct mm_struct *mm, unsigned long addr);

#endif

   unsigned long mmap_base;        /*base of mmap area */

   unsigned long task_size;        /*size of task vm space */

   unsigned long cached_hole_size;    /* if non-zero, the largest hole below free_area_cache */

   unsigned long free_area_cache;     /* first hole of size cached_hole_size or larger */

   unsigned long highest_vm_end;      /* highest vma end address */

   pgd_t * pgd;

   atomic_t mm_users;          /* Howmany users with user space? */

   atomic_t mm_count;          /* Howmany references to "struct mm_struct" (users count as 1) */

   int map_count;              /*number of VMAs */

 

   spinlock_t page_table_lock;     /*Protects page tables and some counters */

   struct rw_semaphore mmap_sem;

 

struct list_headmmlist;        /* List of maybe swappedmm's.  These are globally strung

                         * together off init_mm.mmlist, andare protected

                         * by mmlist_lock

                         */

 

 

   unsigned long hiwater_rss;  /*High-watermark of RSS usage */

   unsigned long hiwater_vm;   /*High-water virtual memory usage */

 

   unsigned long total_vm;     /*Total pages mapped */

   unsigned long locked_vm;    /*Pages that have PG_mlocked set */

   unsigned long pinned_vm;    /*Refcount permanently increased */

   unsigned long shared_vm;    /*Shared pages (files) */

   unsigned long exec_vm;      /*VM_EXEC & ~VM_WRITE */

   unsigned long stack_vm;     /*VM_GROWSUP/DOWN */

   unsigned long def_flags;

   unsigned long nr_ptes;      /*Page table pages */

   unsigned long start_code, end_code, start_data, end_data;

   unsigned long start_brk, brk, start_stack;

   unsigned long arg_start, arg_end, env_start, env_end;

 

   unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

 

   /*

    * Special counters, in some configurations protected by the

    * page_table_lock, in other configurations by being atomic.

    */

   struct mm_rss_stat rss_stat;

         structlinux_binfmt *binfmt;

 

   cpumask_var_t cpu_vm_mask_var;

 

   /* Architecture-specific MM context */

   mm_context_t context;

 

   unsigned long flags; /* Must use atomic bitops to access the bits */

 

structcore_state *core_state; /* coredumping support */

}

mmap和mm_rb指向的是相同数据对象(vm_area_stuct虚拟内存区域),只是组织方式不一样。mmap以链表形式存放,利于简单、高效的遍历所有元素;mm_rb以红黑树的形式存放,更适合搜索指定元素。

pgd为页全局目录,实现虚拟地址和物理内存的映射,在switch_mm时将pgd首地址放入CPU寄存器供mmu(硬件实现)使用。TLB则是pgd的一个子集,存放已经使用过的映射,加速地址映射查找。

mm_users和mm_count:mm_users记录正在使用该内存的进程数目;mm_count域是mm_struct结构体的主引用计数。区别:mm_users一个进程创建多个线程时会增加,mm_count当内核线程共享进程地址空间时增加,初始化是1。

         所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表进程0的地址空间。

见后面关联图说明。

 

(2)分配与撤销

A.分配

         在进程创建一节提到过,do_fork的copy_process()完成大部分工作。copy_process()中的copy_mm()实现复制父进程的内存描述符,源码解析如下:

static intcopy_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;

                   /* 把子进程初始化成NULL */

    tsk->mm = NULL;

    tsk->active_mm = NULL;

       

         /* 由于内核线程没有独立的地址空间mm=NULL,所以当前current(即父进程)mmNULL,则表示父进程是内核线程,子进程上面已经初始化为NULL,这里直接返回 */

    oldmm = current->mm;

    if (!oldmm)

        return 0;

                  

                   /* 如果设置了CLONE_VM共享地址空间标识,则子进程直接指向父进程的内存描述符,使用计数+1,一般用户创建线程使用 */

    if (clone_flags & CLONE_VM) {

        atomic_inc(&oldmm->mm_users);

        mm = oldmm;

        goto good_mm;

    }

 

    retval = -ENOMEM;

                   /* 其他情况则是通过allocate_mm()申请自己的mm_struct,拷贝父进程的内容,再进行自己独立的初始化(包括pgd页表申请等)

allocate_mm()kmem_cache_alloc的宏,从mm_cashep slab缓存中分配得到 */

    mm = dup_mm(tsk);

    if (!mm)

        goto fail_nomem;

 

good_mm:

    tsk->mm = mm;

    tsk->active_mm = mm;

    return 0;

 

fail_nomem:

    return retval;

}

 

B.撤销

当进程退出时,内核会调用exit_mm()函数,该函数执行一些常规的撤销工作,同时更新一些统计量。其中,该函数会调用mmput减少mm_users计数,如果到0,调用mmdrop函数减少mm_count计数,如果到0,那么调用free_mm()宏通过kmem_cache_free()函数将mm_struct结构体归还到mm_cachep slab缓存中。

 

(3)内核线程mm_struct

         上面分配一节已经看到,内核线程对应的进程描述符中mm域为空,这是因为内核线程在用户空间中没有任何页,所以不需要有自己的内存描述符和页表,所有内核线程共享内核地址空间。

         尽管mm域为空,即使访问内核内存,内核线程也还是需要使用一些数据的。为了避免内核线程为内存描述符和页表浪费内存,避免向新地址空间进程切换浪费处理器周期,内核线程将直接使用前一个进程的内存描述符,进程切换源码如下:

static inline void context_switch(struct rq*rq, struct task_struct *prev,

          struct task_struct *next)

{

   struct mm_struct *mm, *oldmm;

   mm = next->mm;     //下一个将执行进程的mm

   oldmm = prev->active_mm;    //当前执行进程的active_mm

       

         /*mm=NULL则为内核线程,内核线程的active_mm指向当前执行进程的active_mm,同时mm_count+1

           mm != NULL则为用户进程,通过switch_mm实现内存描述符的切换,主要完成将pgd首地址放入CPU寄存器供mmu(硬件实现)使用

         */

   if (!mm) {

       next->active_mm = oldmm;

       atomic_inc(&oldmm->mm_count);

       enter_lazy_tlb(oldmm, next);

    }else

       switch_mm(oldmm, mm, next);

       

         /*如果当前的进程为内核线程,则将active_mm=NULL,下次运行再重新赋值*/

   if (!prev->mm) {

       prev->active_mm = NULL;

       rq->prev_mm = oldmm;

    }

    …

}

        

3、虚拟内存区域

(1)数据结构

虚拟内存区域(VMA)由vm_area_struct结构体描述,指定地址空间内连续区间上的一个独立内存范围。每一个VMA作为一个单独的内存对象管理,具有一致的属性(比如访问权限等),因此,一个VMA就代表了一种类型的内存区域(如内存映射文件、进程用户空间栈等)。结构体如下:

struct vm_area_struct {

    unsignedlong vm_start;     //内存区间的收地址

unsigned longvm_end;      //内存区间的尾地址

//VMA双向链表,按照地址顺序排序

   struct vm_area_struct *vm_next, *vm_prev;

 

struct rb_nodevm_rb;  //放在红黑树上的节点

unsigned longrb_subtree_gap;

 

    struct mm_struct *vm_mm;    //指向内存描述符

    pgprot_t vm_page_prot;     //访问权限

    unsigned long vm_flags;     //标志

    union {

        struct {

            struct rb_node rb;

            unsigned long rb_subtree_last;

        } linear;

        struct list_head nonlinear;

        const char __user *anon_name;

    } shared;

 

   struct list_head anon_vma_chain;  //匿名VMA对象链表

   struct anon_vma *anon_vma;  //匿名VMA对象

 

    const structvm_operations_struct *vm_ops;        //相关的操作表

 

    unsigned long vm_pgoff;     //文件中的偏移量

    struct file * vm_file;      //被映射的文件

    void * vm_private_data;     //私有数据

 

#ifndefCONFIG_MMU

    struct vm_region *vm_region;    /* NOMMU mapping region */

#endif

};

         在同一个地址空间内的不同内存区间不能重叠。

 

struct vm_operations_struct {

void(*open)(struct vm_area_struct * area);

当指定的内存区域被加入到一个地址空间时,该函数被调用

void(*close)(struct vm_area_struct * area);

当指定的内存区域从地址空间删除时,该函数被调用

   int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);

     当没有出现在物理内存中的页面被访问时,该函数被页面故障处理调用

   int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);

         当某个页面为只读页面时,该函数被页面故障处理调用

   int (*access)(struct vm_area_struct *vma, unsigned long addr,

              void *buf, int len, int write);

         当get_user_pages()函数调用失败时,该函数被access_process_vm()函数调用

    int(*remap_pages)(struct vm_area_struct *vma, unsigned long addr,

               unsigned long size, pgoff_tpgoff);

};

 

(2)相关操作

         内核时常需要在某个内存区域上执行一些操作,比如某个指定地址是否包含在某个内存区域中。这类操作非常频繁,也是mmap例程的基础。为了方便执行这类对内存区域的操作,内核定义了许多的辅助函数。

A. find_vma

structvm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)

为了找到一个给定的内存地址属于哪一个内存区域。该函数在指定的地址空间搜索第一个vm_end大于addr的内存区域。如果没有发现这样的区域,返回NULL,否则指向匹配的内存区域vm_area_struct结构体指针。返回的结果被缓存在内存描述符mmap_cache域中,有相当好的命中率,提高速度。该函数通过红黑树查找。

B.find_vma_prev

         和find_vma类似,但是它返回第一个小于addr的VMA。

C.find_vma_intersection

         返回第一个和指定地址区间相交的VMA,也是调用find_vma

D.mmap

         创建一个新的线性地址区间。如果新创建的VMA和已经存在的地址区间相邻,并且相同的访问权限,两个区域将合并为一个;如果不能合并就算是一个新的VMA。创建之后,会将该地址空间加入到进程的地址空间中。

Mmap系统调用对应的内核函数do_mmap_pgoff:

unsigned long do_mmap_pgoff(struct file*file, unsigned long addr,

           unsigned long len, unsigned long prot,

           unsigned long flags, unsigned long pgoff,

           unsigned long *populate)

file为NULL且pgoff为0,代表没有和文件相关,称为匿名映射

如果指定了文件名和偏移量,称为文件映射

 

E.munmap

从特定的进程地址空间中删除指定地址空间,munmap系统调用对应的内核函数do_munmap:

int do_munmap(struct mm_struct *mm,unsigned long start, size_t len)

 

4、缺页异常处理

         Linux缺页异常处理程序必须区分两种情况:

由编程错误引起的异常

由引用属于进程地址空间但还尚未分配物理页框的页所引起的异常。

内核由static int __kprobes do_page_fault(unsigned long addr, unsigned intfsr, struct pt_regs *regs)实现。

总体方案如下图:

5、关联图


进程、线程之间内存描述符关系

进程A和B是独立进程,进程B由A创建,T1是进程A的一个线程。上图展示说明如下:

A.进程A fork进程B,进程B会复制父进程A的task_struct和mm_struct,进行相应的初始化,有各自的页表目录。

B.进程A pthread_create 线程T1,复制父进程A的task_struct,但是共享mm_struct,使用同一个页表目录

C.进程A和进程B的虚拟地址通过mm_struct的pgd页表映射到实际物理内存,对于可读页(如C库等),两个进程是共享实际内存,对于可写页(如堆栈、变量等)则有各自的物理内存

D.进程A和线程T1共享mm_struct,因此也共享pgd页表,当线程需要自己的可写页时,就会加入到进程A的共享页表里。

 

进程的虚拟内存管理

VMA:虚拟内存区域

mm_rb:为红黑树,图中用链表示意,表示mmap和mm_rb都指向虚拟内存区域

每个进程都有一个task_struct,所有进程通过链表连接起来,链表头为init_task。每个task_struct又都有一个mm_struct结构,所有的mm_struct通过mmlist连接在一个双向链表中,该链表的首元素是init_mm。每个进程的各个虚拟内存区域通过mmap和mm_rb组织起来,然后通过pgd页表,映射到实际的物理内存。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值