進程地址空間[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域,使其指向前一個進程的內存描述符,在需要時,內核線程可以使用前一個進程的頁表。因爲內核線程不訪問用戶空間的內存,所以它們僅僅使用地址空間中的和內核內存相關的信息。

  

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章