进程地址空间[1]

 

     内核除了管理本身的内存外,还必须管理进程的地址空间,即系统中每个用户空间进程所看到的内存。Linux采用虚拟内存技术,系统中的所有进程之间以虚拟方式共享内存。对每个进程来说,它们好像都可以访问整个系统的所有物理内存;即使单独一个进程,它拥有的地址空间也可以远远大于系统物理内存。

    进程地址空间由每个进程中的线性地址区组成,而且更为重要的特点是内核允许进程使用该空间中的地址。每个进程都有一个32或64位的flat地址空间,空间的具体大小取决于体系结构。flat描述的是地址空间范围是一个独立的连续区间。通常情况下,每个进程都有惟一的这种flat地址空间,进程地址空间之间彼此互不相干。两个不同的进程可以在鸽子地址空间的相同地址内存放不同的数据。进程之间也可以选择共享地址空间,称这样的进程为线程。

    内存地址是一个给定的值,它要在地址空间范围之内。在地址空间中,我们更为关心的是进程有权访问的虚拟内存地址区间,这些可被访问的合法地址区间被称为内存区域(memory area),通过内核,进程可以给自己的地址空间动态地添加或减少内存区域。

   进程只能访问有效范围内的内存地址。每个内存区域也具有相应进程必须遵循的特定访问属性,如果一个进程访问了不在有效范围中的地址,或以不正确的方式访问有效地址,那么内核就会终止该进程,并返回“段错误”信息。

   内存区域可以包含各种内存对象,比如:

1. 可执行文件代码的内存映射,称为代码段(text section)

2. 可执行文件的已初始化全局变量的内存映射,称为数据段(data section)

3. 包含未初始化全局变量,也就是bss段的零页(页面中的信息全部为0值,可用于映射bss段等目的)的内存映射。

   术语“BSS”是block started by symbol的缩写。因为未初始化的变量没有对应的值,所以不需要存放在可执行对象中。但是因为C标准强制规定未初始化的全局变量要被赋予特殊的默认值,所以内核要将未赋值的变量从可执行代码载入到内存中,然后将零页映射到该片内存上,于是这些未初始化的变量就被赋予了0值,这样避免了在目标文件中显式地进行初始化,减少空间浪费。

4. 用于进程用户空间栈(不要和进程内核栈混淆,进程的内核栈独立存在并由内核维护)的零页的内存映射。

5. 每一个诸如C库或动态连接程序等共享库的代码段,数据段和bss也会被载入进程的地址空间。

6. 任何内存映射文件

7. 任何共享内存段

8. 任何匿名的内存映射,比如由malloc()分配的内存。

   进程地址空间中的任何有效地址都只能位于惟一的区域。这些内存区域不能相互覆盖。在执行的进程中,每个不同的内存片段都对应一个独立的内存区域:栈、对象代码、全局变量、被映射的文件等。

  • 内存描述符

  内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间相关的全部信息:

 

    • 在<include/linux/sched.h>中
    • struct mm_struct {
    •     struct vm_area_struct * mmap;       /* list of VMAs */
    •     struct rb_root mm_rb;  /* 虚拟内存区域红黑树 */
    •     struct vm_area_struct * mmap_cache; /* last find_vma result */
    •     unsigned long (*get_unmapped_area) (struct file *filp,
    •                 unsigned long addr, unsigned long len,
    •                 unsigned long pgoff, unsigned long flags);
    •     void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
    •     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 */
    •     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) */
    •     int map_count;              /* number of VMAs */
    •     struct rw_semaphore mmap_sem;
    •     spinlock_t page_table_lock;     /* Protects page tables and some counters */
    •     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
    •                          */
    •     /* Special counters, in some configurations protected by the
    •      * page_table_lock, in other configurations by being atomic.
    •      */
    •     mm_counter_t _file_rss;
    •     mm_counter_t _anon_rss;
    •     unsigned long hiwater_rss;  /* High-watermark of RSS usage */
    •     unsigned long hiwater_vm;   /* High-water virtual memory usage */
    •     unsigned long total_vm, locked_vm, shared_vm, exec_vm;
    •     unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
    •     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 */
    •     cpumask_t cpu_vm_mask;
    •     /* Architecture-specific MM context */
    •     mm_context_t context; 
    •     /* Swap token stuff */
    •     /*
    •      * Last value of global fault stamp as seen by this process.
    •      * In other words, this value gives an indication of how long
    •      * it has been since this task got the token.
    •      * Look at mm/thrash.c
    •      */
    •     unsigned int faultstamp;
    •     unsigned int token_priority;
    •     unsigned int last_interval;
    •     unsigned char dumpable:2;
    •     /* coredumping support */
    •     int core_waiters;    /*内核转储等待线程 */
    •     struct completion *core_startup_done,/*core 开始完成 */ core_done/*core结束完成 */;
    •     /* aio bits */
    •     rwlock_t        ioctx_list_lock;   /* AIO IO链表锁*/
    •     struct kioctx       *ioctx_list;  /* AIO IO链表*/
    • };

   内核同时使用mm_count和mm_users这两个计数器是为了区别主使用计数器(mm_count)和使用该地址空间的进程数目(mm_users)。

   mmap和mm_rb这两个不同的数据结构体描述的对象是相同的:该地址空间中的全部内存区域。mmap是以链表形式存放的,这样利于简单高效地遍历所有元素;而mm_rb以红黑树形式存放,适合搜索指定元素。

   所有mm_struct结构体通过自身的mmlist域连接成一个双向链表中,首元素是init_mm内存描述符,它代表init进程的地址空间。操作该链表的时候,需要使用mmlist_lock锁(定义在kernel/fork.c中)来防止并发访问。

   内存描述符的总数存放在mmlist_nr全局变量(定义在kernel/fork.c中)中。

    • 分配内存描述符

   进程的进程描述符中,mm域存放着该进程使用的内存描述符,所以current->mm便指向当前进程的内存描述符。

   fork()函数利于copy_mm()函数复制父进程的内存描述符,就是将current->mm域给子进程,子进程中的mm_struct结构实际上是通过文件kernel/fork.c中的allocate_mm()宏从mm_cachep slab缓存中分配得到的。通常,每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间。

   如果父进程希望和子进程共享地址空间,可以在调用clone()时,设置CLONE_VM标志。我们把这样的进程称为线程。

      1. /* SLAB cache for mm_struct structures (tsk->mm) */
      2. static struct kmem_cache *mm_cachep;
      3. #define allocate_mm()   (kmem_cache_alloc(mm_cachep, GFP_KERNEL))
      4. static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
      5. {
      6.     struct mm_struct * mm, *oldmm;
      7.     int retval;
      8.     tsk->min_flt = tsk->maj_flt = 0;
      9.     tsk->nvcsw = tsk->nivcsw = 0;
      10.     tsk->mm = NULL;
      11.     tsk->active_mm = NULL;
      12.     /*
      13.      * Are we cloning a kernel thread?
      14.      *
      15.      * We need to steal a active VM for that..
      16.      */
      17.     oldmm = current->mm;
      18.     if (!oldmm)
      19.         return 0;
      20.     if (clone_flags & CLONE_VM) {
      21.         atomic_inc(&oldmm->mm_users);
      22.         mm = oldmm;
      23.         goto good_mm;
      24.     }
      25.     retval = -ENOMEM;
      26.     mm = dup_mm(tsk);
      27.     if (!mm)
      28.         goto fail_nomem;
      29. good_mm:
      30.     /* Initializing for Swap token stuff */
      31.     mm->token_priority = 0;
      32.     mm->last_interval = 0;
      33.     tsk->mm = mm;
      34.     tsk->active_mm = mm;
      35.     return 0;
      36. fail_nomem:
      37.     return retval;
      38. }
    • 销毁内存描述符

  当进程退出时,内核会调用exit_mm()函数,该函数执行一些常规的销毁工作,同时更新一些统计量。

    • 在<kernel/Exit.c>中
    • /*
    •  * Turn us into a lazy TLB process if we
    •  * aren't already..
    •  */
    • static void exit_mm(struct task_struct * tsk)
    • {
    •     struct mm_struct *mm = tsk->mm;
    •     mm_release(tsk, mm);
    •     if (!mm)
    •         return;
    •     /*
    •      * Serialize with any possible pending coredump.
    •      * We must hold mmap_sem around checking core_waiters
    •      * and clearing tsk->mm.  The core-inducing thread
    •      * will increment core_waiters for each thread in the
    •      * group with ->mm != NULL.
    •      */
    •     down_read(&mm->mmap_sem);
    •     if (mm->core_waiters) {
    •         up_read(&mm->mmap_sem);
    •         down_write(&mm->mmap_sem);
    •         if (!--mm->core_waiters)
    •             complete(mm->core_startup_done);
    •         up_write(&mm->mmap_sem);
    •         wait_for_completion(&mm->core_done);
    •         down_read(&mm->mmap_sem);
    •     }
    •     atomic_inc(&mm->mm_count);
    •     BUG_ON(mm != tsk->active_mm);
    •     /* more a memory barrier than a real lock */
    •     task_lock(tsk);
    •     tsk->mm = NULL;
    •     up_read(&mm->mmap_sem);
    •     enter_lazy_tlb(mm, current);
    •     task_unlock(tsk);
    •     mmput(mm);  //减少内存描述符中的mm_users用户计数
    • }
    • 在fork.c中
    • /* Please note the differences between mmput and mm_release.
    •  * mmput is called whenever we stop holding onto a mm_struct,
    •  * error success whatever.
    •  *
    •  * mm_release is called after a mm_struct has been removed
    •  * from the current process.
    •  *
    •  * This difference is important for error handling, when we
    •  * only half set up a mm_struct for a new process and need to restore
    •  * the old one.  Because we mmput the new mm_struct before
    •  * restoring the old one. . .
    •  * Eric Biederman 10 January 1998
    •  */
    • void mm_release(struct task_struct *tsk, struct mm_struct *mm)
    • {
    •     struct completion *vfork_done = tsk->vfork_done;
    •     /* Get rid of any cached register state */
    •     deactivate_mm(tsk, mm);
    •     /* notify parent sleeping on vfork() */
    •     if (vfork_done) {
    •         tsk->vfork_done = NULL;
    •         complete(vfork_done);
    •     }
    •     /*
    •      * If we're exiting normally, clear a user-space tid field if
    •      * requested.  We leave this alone when dying by signal, to leave
    •      * the value intact in a core dump, and to save the unnecessary
    •      * trouble otherwise.  Userland only wants this done for a sys_exit.
    •      */
    •     if (tsk->clear_child_tid
    •         && !(tsk->flags & PF_SIGNALED)
    •         && atomic_read(&mm->mm_users) > 1) {
    •         u32 __user * tidptr = tsk->clear_child_tid;
    •         tsk->clear_child_tid = NULL;
    •         /*
    •          * We don't check the error code - if userspace has
    •          * not set up a proper pointer then tough luck.
    •          */
    •         put_user(0, tidptr);
    •         sys_futex(tidptr, FUTEX_WAKE, 1, NULL, NULL, 0);
    •     }
    • }
    • /*
    •  * Decrement the use count and release all resources for an mm.
    •  */
    • void mmput(struct mm_struct *mm)
    • {
    •     might_sleep();
    •     if (atomic_dec_and_test(&mm->mm_users)) {
    •         exit_aio(mm);
    •         exit_mmap(mm);
    •         if (!list_empty(&mm->mmlist)) {
    •             spin_lock(&mmlist_lock);
    •             list_del(&mm->mmlist);
    •             spin_unlock(&mmlist_lock);
    •         }
    •         put_swap_token(mm);
    •         mmdrop(mm);
    •     }
    • }
    • /*
    •  * Called when the last reference to the mm
    •  * is dropped: either by a lazy thread or by
    •  * mmput. Free the page directory and the mm.
    •  */
    • void fastcall __mmdrop(struct mm_struct *mm)
    • {
    •     BUG_ON(mm == &init_mm);
    •     mm_free_pgd(mm);
    •     destroy_context(mm);
    •     free_mm(mm);
    • }
    • 在sched.h中
    • static inline void mmdrop(struct mm_struct * mm)
    • {
    •     if (atomic_dec_and_test(&mm->mm_count))
    •         __mmdrop(mm);
    • }

 

    • mm_struct与内核线程

  内核线程没有进程地址空间,也没有相关的内存描述符。所以内核线程对应的进程描述符中mm域为空。这正式内核线程的真正含义,它们没有用户上下文。

  内核线程并不需要访问任何用户空间的内存,以为内核线程在用户空间没有任何页,所以它们并不需要有自己的内存描述符和页表。尽管如此,即使访问内核内存,内核线程饿还是需要使用一些数据的,比如页表。为了避免内核线程为内存描述符和页表浪费内存,也为了当新内核线程运行时,避免浪费处理器周期向新地址空间进行切换,内核系拿出将直接使用前一个进程的内存描述符。

  当一个进程被调度时,该进程的mm域执行的地址空间被装载到内存,进程描述符中的active_mm域会被更新,指向新的地址空间。内核线程没有地址空间,所以mm为空。当内核线程被调度时,内核发现它的mm为空,就会保留前一个进程的地址空间,随后内核更新内核线程对应的进程描述符中的active_mm域,使其指向前一个进程的内存描述符,在需要时,内核线程可以使用前一个进程的页表。因为内核线程不访问用户空间的内存,所以它们仅仅使用地址空间中的和内核内存相关的信息。

  

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值