mm_struct

Linux对于内存的管理涉及到非常多的方面,这篇文章首先从对进程虚拟地址空间的管理说起。(所依据的代码是2.6.32.60)

无论是内核线程还是用户进程,对于内核来说,无非都是 task_struct这个数据结构的一个实例 而已,task_struct被称为进程描述符(process descriptor),因为它记录了这个进程所有的context。其中有一个被称为 '内存描述符‘(memory descriptor)的数据结构 mm_struct ,抽象并描述了Linux视角下管理进程地址空间的所有信息。
mm_struct定义在include/linux/mm_types.h中,其中的域抽象了进程的地址空间,如下图所示:

struct mm_struct {
	struct vm_area_struct *mmap;		/* list of VMAs   指向虚拟区间(VMA)链表  */
	struct rb_root mm_rb;                   //指向red_black树
	u32 vmacache_seqnum;                   /* per-thread vmacache */
#ifdef CONFIG_MMU
	unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
#endif
	unsigned long mmap_base;		/* base of mmap area */
	unsigned long mmap_legacy_base;         /* base of mmap area in bottom-up allocations */
	unsigned long task_size;		/* size of task vm space */
	unsigned long highest_vm_end;		/* highest vma end address */
	pgd_t * pgd;                            //  指向进程的页目录
	atomic_t mm_users;			/* How many users with user space? */
	atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1) */
	atomic_long_t nr_ptes;			/* PTE page table pages */
#if CONFIG_PGTABLE_LEVELS > 2
	atomic_long_t nr_pmds;			/* PMD page table pages */
#endif
	int map_count;				/* number of VMAs */

	spinlock_t page_table_lock;		/* Protects page tables and some counters */
	struct rw_semaphore mmap_sem;

	struct list_head mmlist;		/* List of maybe swapped mm's.	These are globally strung
						 * together off init_mm.mmlist, and are 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 data_vm;		/* VM_WRITE & ~VM_SHARED & ~VM_STACK */
	unsigned long exec_vm;		/* VM_EXEC & ~VM_WRITE & ~VM_STACK */
	unsigned long stack_vm;		/* VM_STACK */
	unsigned long def_flags;
	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;

	struct linux_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 */

	struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
	spinlock_t			ioctx_lock;
	struct kioctx_table __rcu	*ioctx_table;
#endif
#ifdef CONFIG_MEMCG
	/*
	 * "owner" points to a task that is regarded as the canonical
	 * user/owner of this mm. All of the following must be true in
	 * order for it to be changed:
	 *
	 * current == mm->owner
	 * current->mm != mm
	 * new_owner->mm == mm
	 * new_owner->alloc_lock is held
	 */
	struct task_struct __rcu *owner;
#endif
	struct user_namespace *user_ns;

	/* store ref to file /proc/<pid>/exe symlink points to */
	struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIER
	struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
	pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_CPUMASK_OFFSTACK
	struct cpumask cpumask_allocation;
#endif
#ifdef CONFIG_NUMA_BALANCING
	/*
	 * numa_next_scan is the next time that the PTEs will be marked
	 * pte_numa. NUMA hinting faults will gather statistics and migrate
	 * pages to new nodes if necessary.
	 */
	unsigned long numa_next_scan;

	/* Restart point for scanning and setting pte_numa */
	unsigned long numa_scan_offset;

	/* numa_scan_seq prevents two threads setting pte_numa */
	int numa_scan_seq;
#endif
#if defined(CONFIG_NUMA_BALANCING) || defined(CONFIG_COMPACTION)
	/*
	 * An operation with batched TLB flushing is going on. Anything that
	 * can move process memory needs to flush the TLB when moving a
	 * PROT_NONE or PROT_NUMA mapped page.
	 */
	bool tlb_flush_pending;
#endif
	struct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGE
	atomic_long_t hugetlb_usage;
#endif
	struct work_struct async_put_work;
};


每个进程都有自己独立的mm_struct,使得每个进程都有一个抽象的平坦的独立的32或64位地址空间,各个进程都在各自的地址空间中相同的地址内存存放不同的数据而且互不干扰。如果进程之间共享相同的地址空间,则被称为线程
其中[start_code,end_code)表示代码段的地址空间范围。
[start_data,end_start)表示数据段的地址空间范围。
[start_brk,brk)分别表示heap段的起始空间和当前的heap指针。
[start_stack,end_stack)表示stack段的地址空间范围。
 mm->pgd指向容纳页表的内存,每个进程有自已的mm,每个mm有自己的页表。于是,进程调度时,页表被切换(一般会有一个CPU寄存器来保存页表的地址,比如X86下的CR3,页表切换就是改变该寄存器的值)。所以,各个进程的地址空间互不影响(因为页表都不一样了,当然无法访问到别人的地址空间上。但是共享内存除外,这是故意让不同的页表能够访问到相同的物理地址上)。
当调度程序调度一个程序运行时,就将这个地址转成物理地址,并写入控制寄存器(CR3)。

mmap_base表示memory mapping段的起始地址。 那为什么mmap段没有结束的地址呢?
bbs段是用来干什么的呢?bbs表示的所有没有初始化的全局变量,这样只需要将它们匿名映射为‘零页’,而不用在程序load过程中从磁盘文件显示的mapping,这样既减少了elf二进制文件的大小,也提高了程序加载的效率。 在mm_struct中为什么没有bbs段的地址空间表示呢?
除此之外,mm_struct还定义了几个重要的域:
atomic_t mm_users;           /* How many users with user space? */
atomic_t mm_count;           /* How many references to "struct mm_struct" (users count as 1) */
这两个counter乍看好像差不多,那Linux使用中有什么区别呢?看代码就是最好的解释了。
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
 {
        struct mm_struct * mm, *oldmm;
        int retval;
        tsk->mm = NULL;
         tsk->active_mm = NULL;
         /*
         * Are we cloning a kernel thread?
          *
        * We need to steal a active VM for that..
         */
        oldmm = current->mm;
         if (!oldmm)
                return 0;
 
        if (clone_flags & CLONE_VM) {
                atomic_inc(&oldmm->mm_users);
                mm = oldmm;
                 goto good_mm;
   }

无论我们在调用fork,vfork,clone的时候最终会调用do_fork函数,区别在于vfork和clone会给copy_mm传入一个CLONE_VM的flag,这个标识表示父子进程都运行在同样一个‘虚拟地址空间’上面(在Linux称之为lightweight process或者线程),当然也就共享同样的物理地址空间(Page Frames)。

copy_mm函数中,如果创建线程中有CLONE_VM标识,则表示父子进程共享地址空间和同一个内存描述符,并且只需要将mm_users值+1,也就是说mm_users表示正在引用该地址空间的thread数目,是一个thread level的counter。

mm_count呢?mm_count的理解有点复杂。

对Linux来说,用户进程和内核线程(kernel thread)都是task_struct的实例,唯一的区别是kernel thread是没有进程地址空间的,内核线程也没有mm描述符的,所以内核线程的tsk->mm域是空(NULL)内核scheduler在进程context switching的时候,会根据tsk->mm判断即将调度的进程是用户进程还是内核线程。但是虽然thread thread不用访问用户进程地址空间,但是仍然需要page table来访问kernel自己的空间。但是幸运的是,对于任何用户进程来说,他们的内核空间都是100%相同的,所以内核可以’borrow'上一个被调用的用户进程的mm中的页表来访问内核地址,这个mm就记录在active_mm。

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

为了支持这个特别,mm_struct里面引入了另外一个counter,mm_count。刚才说过mm_users表示这个进程地址空间被多少线程共享或者引用,而mm_count则表示这个地址空间被内核线程引用的次数+1。

比如一个进程A有3个线程,那么这个A的mm_struct的mm_users值为3,但是mm_count为1,所以mm_count是process level的counter。维护2个counter有何用处呢?考虑这样的scenario,内核调度完A以后,切换到内核内核线程B,B ’borrow' A的mm描述符以访问内核空间,这时mm_count变成了2,同时另外一个cpu core调度了A并且进程A exit,这个时候mm_users变为了0,mm_count变为了1,但是内核不会因为mm_users==0而销毁这个mm_struct,内核只会当mm_count==0的时候才会释放mm_struct,因为这个时候既没有用户进程使用这个地址空间,也没有内核线程引用这个地址空间。

static struct mm_struct * mm_init(struct mm_struct * mm, struct task_struct *p)
  {
           atomic_set(&mm->mm_users, 1);
 4        atomic_set(&mm->mm_count, 1);
在初始化一个mm实例的时候,mm_users和mm_count都被初始化为1。

 /*
  * context_switch - switch to the new MM and the new
  * thread's register state.
  */
 static inline void
 context_switch(struct rq *rq, struct task_struct *prev,
                struct task_struct *next)
 {
         struct mm_struct *mm, *oldmm;
 
         prepare_task_switch(rq, prev, next);
        trace_sched_switch(rq, prev, next);
         mm = next->mm;
         oldmm = prev->active_mm;
 
         if (unlikely(!mm)) {
                 next->active_mm = oldmm;
                 atomic_inc(&oldmm->mm_count);
                 enter_lazy_tlb(oldmm, next);
        } else
       switch_mm(oldmm, mm, next);

上面的代码是Linux scheduler进行的context switch的一小段,从unlike(!mm)开始,next->active_mm = oldmm表示如果将要切换倒内核线程,则‘借用’前一个拥护进程的mm描述符,并把他赋给active_mm,重点是将‘借用’的mm描述符的mm_counter加1。

下面我们看看在fork一个进程的时候,是怎样处理的mm_struct的。

 /*
*  Ok, this is the main fork-routine.
*
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
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)
          {
    p = copy_process(clone_flags, stack_start, regs, stack_size, child_tidptr, NULL, trace);

do_fork调用copy_process。

 /*
   * This creates a new process as a copy of the old one,
   * but does not actually start it yet.
   *
   * It copies the registers, and all the appropriate
   * parts of the process environment (as per the clone
   * flags). The actual kick-off is left to the caller.
   */
  static struct task_struct *copy_process(unsigned long clone_flags,unsigned long stack_start,struct pt_regs *regs,
                                         unsigned long stack_size,int __user *child_tidptr,struct pid *pid,int trace)
  {
        if ((retval = copy_mm(clone_flags, p)))
                goto bad_fork_cleanup_signal;
				
copy_process调用copy_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..
         */
       oldmm = current->mm;
        if (!oldmm)
                return 0;
        if (clone_flags & CLONE_VM) {
                atomic_inc(&oldmm->mm_users);
                mm = oldmm;
                goto good_mm;
      }
 
       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;
 
         tsk->mm = mm;
        tsk->active_mm = mm;
         return 0;
 
  fail_nomem:
       return retval;
  }

对子进程或者线程的mm和active_mm初始化(NULL)。

就是我们上面说的果是创建线程,则新线程共享创建进程的mm,所以不需要进行下面的copy操作。重点就是dup_mm(tsk)。

/*
 622 * Allocate a new mm structure and copy contents from the
 623 * mm structure of the passed in task structure.
 624 */
 625   struct mm_struct *dup_mm(struct task_struct *tsk)
 626  {
 627        struct mm_struct *mm, *oldmm = current->mm;
 628        int err;
 629
 630        if (!oldmm)
 631                return NULL;
 632
 633        mm = allocate_mm();
 634        if (!mm)
 635                goto fail_nomem;
 636
 637        memcpy(mm, oldmm, sizeof(*mm));
 638
 639        /* Initializing for Swap token stuff */
 640        mm->token_priority = 0;
 641        mm->last_interval = 0;
 642
 643        if (!mm_init(mm, tsk))
 644                goto fail_nomem;
 645
 646        if (init_new_context(tsk, mm))
 647                goto fail_nocontext;
 648
 649        dup_mm_exe_file(oldmm, mm);
 650
 651        err = dup_mmap(mm, oldmm);
 652        if (err)
 653                goto free_pt;
 654
 655        mm->hiwater_rss = get_mm_rss(mm);
 656        mm->hiwater_vm = mm->total_vm;
 657
 658        if (mm->binfmt && !try_module_get(mm->binfmt->module))
 659                goto free_pt;
 660
 661        return mm;

 633行,用slab分配了mm_struct的内存对象。

637行,对子进程的mm_struct进程赋值,使其等于父进程,这样子进程mm和父进程mm的每一个域的值都相同。

在copy_mm的实现中,主要是为了实现unix COW的语义,所以理论上我们只需要父子进程mm中的start_x和end_x之类的域(像start_data,end_data)相等,而对其余的域(像mm_users)则需要re-init,这个操作主要在mm_init中完成。

static struct mm_struct * mm_init(struct mm_struct * mm, struct task_struct *p)
 450{
 451        atomic_set(&mm->mm_users, 1);
 452        atomic_set(&mm->mm_count, 1);
 453        init_rwsem(&mm->mmap_sem);
 454        INIT_LIST_HEAD(&mm->mmlist);
 455        mm->flags = (current->mm) ?
 456                (current->mm->flags & MMF_INIT_MASK) : default_dump_filter;
 457        mm->core_state = NULL;
 458        mm->nr_ptes = 0;
 459        set_mm_counter(mm, file_rss, 0);
 460        set_mm_counter(mm, anon_rss, 0);
 461        spin_lock_init(&mm->page_table_lock);
 462        mm->free_area_cache = TASK_UNMAPPED_BASE;
 463        mm->cached_hole_size = ~0UL;
 464        mm_init_aio(mm);
 465        mm_init_owner(mm, p);
 466
 467        if (likely(!mm_alloc_pgd(mm))) {
 468                mm->def_flags = 0;
 469                mmu_notifier_mm_init(mm);
 470                return mm;
 471        }
 472
 473        free_mm(mm);
 474        return NULL;
 475}

其中特别要关注的是467 - 471行的mm_alloc_pdg,也就是page table的拷贝,page table负责logic address到physical address的转换。

拷贝的结果就是父子进程有独立的page table,但是page table里面的每个entries值都是相同的,也就是说父子进程独立地址空间中相同logical address都对应于相同的physical address,这样也就是实现了父子进程的COW(copy on write)语义。

事实上,vfork和fork相比,最大的开销节省就是对page table的拷贝。

而在内核2.6中,由于page table的拷贝,fork在性能上是有所损耗的,所以内核社区里面讨论过shared page table的实现(http://lwn.NET/Articles/149888/)。






  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值