進程地址空間管理總結

前邊我已經說過了內核是如何管理物理內存。但事實是內核是操作系統的核心,不光管理本身的內存,還要管理進程的地址空間。linux操作系統採用虛擬內存技術,所有進程之間以虛擬方式共享內存。進程地址空間由每個進程中的線性地址區組成,而且更爲重要的特點是內核允許進程使用該空間中的地址。通常情況況下,每個進程都有唯一的地址空間,而且進程地址空間之間彼此互不相干。但是進程之間也可以選擇共享地址空間,這樣的進程就叫做線程。
      內核使用內存描述符結構表示進程的地址空間,由結構體mm_struct結構體表示,定義在linux/sched.h中,如下:

struct mm_struct {

        struct vm_area_struct  *mmap;               /* list of memory areas */

        struct rb_root         mm_rb;               /* red-black tree of VMAs */

        struct vm_area_struct  *mmap_cache;         /* last used memory area */

        unsigned long          free_area_cache;     /* 1st address space hole */

        pgd_t                  *pgd;                /* page global directory */

        atomic_t               mm_users;            /* address space users */

        atomic_t               mm_count;            /* primary usage counter */

        int                    map_count;           /* number of memory areas */

        struct rw_semaphore    mmap_sem;            /* memory area semaphore */

        spinlock_t             page_table_lock;     /* page table lock */

        struct list_head       mmlist;              /* list of all mm_structs */

        unsigned long          start_code;          /* start address of code */

        unsigned long          end_code;            /* final address of code */

        unsigned long          start_data;          /* start address of data */

        unsigned long          end_data;            /* final address of data */

        unsigned long          start_brk;           /* start address of heap */

        unsigned long          brk;                 /* final address of heap */

        unsigned long          start_stack;         /* start address of stack */

        unsigned long          arg_start;           /* start of arguments */

        unsigned long          arg_end;             /* end of arguments */

        unsigned long          env_start;           /* start of environment */

        unsigned long          env_end;             /* end of environment */

        unsigned long          rss;                 /* pages allocated */

        unsigned long          total_vm;            /* total number of pages */

        unsigned long          locked_vm;           /* number of locked pages */

        unsigned long          def_flags;           /* default access flags */

        unsigned long          cpu_vm_mask;         /* lazy TLB switch mask */

        unsigned long          swap_address;        /* last scanned address */

        unsigned               dumpable:1;          /* can this mm core dump? */

        int                    used_hugetlb;        /* used hugetlb pages? */

        mm_context_t           context;             /* arch-specific data */

        int                    core_waiters;        /* thread core dump waiters */

        struct completion      *core_startup_done;  /* core start completion */

        struct completion      core_done;           /* core end completion */

        rwlock_t               ioctx_list_lock;     /* AIO I/O list lock */

        struct kioctx          *ioctx_list;         /* AIO I/O list */

        struct kioctx          default_kioctx;      /* AIO default I/O context */

};

      mm_users記錄了正在使用該地址的進程數目(比如有兩個進程在使用,那就爲2)。mm_count是該結構的主引用計數,只要mm_users不爲0,它就爲1。但其爲0時,後者就爲0。這時也就說明再也沒有指向該mm_struct結構體的引用了,這時該結構體會被銷燬。內核之所以同時使用這兩個計數器是爲了區別主使用計數器和使用該地址空間的進程的數目。mmap和mm_rb描述的都是同一個對象:該地址空間中的全部內存區域。不同只是前者以鏈表,後者以紅黑樹的形式組織。所有的mm_struct結構體都通過自身的mmlist域連接在一個雙向鏈表中,該鏈表的首元素是init_mm內存描述符,它代表init進程的地址空間。另外需要注意,操作該鏈表的時候需要使用mmlist_lock鎖來防止併發訪問,該鎖定義在文件kernel/fork.c中。內存描述符的總數在mmlist_nr全局變量中,該變量也定義在文件fork.c中。

      我前邊說過的進程描述符中有一個mm域,這裏邊存放的就是該進程使用的內存描述符,通過current->mm便可以指向當前進程的內存描述符。fork函數利用copy_mm()函數就實現了複製父進程的內存描述符,而子進程中的mm_struct結構體實際是通過文件kernel/fork.c中的allocate_mm()宏從mm_cachep slab緩存中分配得到的。通常,每個進程都有唯一的mm_struct結構體。

      前邊也說過,在linux中,進程和線程其實是一樣的,唯一的不同點就是是否共享這裏的地址空間。這個可以通過CLONE_VM標誌來實現。linux內核並不區別對待它們,線程對內核來說僅僅是一個共向特定資源的進程而已。好了,如果你設置這個標誌了,似乎很多問題都解決了。不再要allocate_mm函數了,前邊剛說作用。而且在copy_mm()函數中將mm域指向其父進程的內存描述符就可以了,如下:

if (clone_flags & CLONE_VM) {

        /*

         * current is the parent process and

         * tsk is the child process during afork()

         */

        atomic_inc(&current->mm->mm_users);

         tsk->mm = current->mm;

}

      最後,當進程退出的時候,內核調用exit_mm()函數,這個函數調用mmput()來減少內存描述符中的mm_users用戶計數。如果計數降爲0,繼續調用mmdrop函數,減少mm_count使用計數。如果使用計數也爲0,則調用free_mm()宏通過kmem_cache_free()函數將mm_struct結構體歸還到mm_cachep slab緩存中。

      但對於內核而言,內核線程沒有進程地址空間,也沒有相關的內存描述符,內核線程對應的進程描述符中mm域也爲空。但內核線程還是需要使用一些數據的,比如頁表,爲了避免內核線程爲內存描述符和頁表浪費內存,也爲了當新內核線程運行時,避免浪費處理器週期向新地址空間進行切換,內核線程將直接使用前一個進程的內存描述符。回憶一下我剛說的進程調度問題,當一個進程被調度時,進程結構體中mm域指向的地址空間會被裝載到內存,進程描述符中的active_mm域會被更新,指向新的地址空間。但我們這裏的內核是沒有mm域(爲空),所以,當一個內核線程被調度時,內核發現它的mm域爲NULL,就會保留前一個進程的地址空間,隨後內核更新內核線程對應的進程描述符中的active域,使其指向前一個進程的內存描述符。所以在需要的時候,內核線程便可以使用前一個進程的頁表。因爲內核線程不妨問用戶空間的內存,所以它們僅僅使用地址空間中和內核內存相關的信息,這些信息的含義和普通進程完全相同。
      內存區域由vm_area_struct結構體描述,定義在linux/mm.h中,內存區域在內核中也經常被稱作虛擬內存區域或VMA.它描述了指定地址空間內連續區間上的一個獨立內存範圍。內核將每個內存區域作爲一個單獨的內存對象管理,每個內存區域都擁有一致的屬性。結構體如下:

struct vm_area_struct {

        struct mm_struct             *vm_mm;        /* associated mm_struct */

        unsigned long                vm_start;      /* VMA start, inclusive */

        unsigned long                vm_end;        /* VMA end , exclusive */

        struct vm_area_struct        *vm_next;      /* list of VMA's */

        pgprot_t                     vm_page_prot;  /* access permissions */

        unsigned long                vm_flags;      /* flags */

        struct rb_node               vm_rb;         /* VMA's node in the tree */

        union {         /* links to address_space->i_mmapor i_mmap_nonlinear */

                struct {

                        struct list_head        list;

                        void                    *parent;

                        structvm_area_struct   *head;

                } vm_set;

               struct prio_tree_node prio_tree_node;

        } shared;

        struct list_head             anon_vma_node;     /* anon_vma entry */

        struct anon_vma              *anon_vma;         /* anonymous VMA object */

        struct vm_operations_struct  *vm_ops;           /* associated ops */

        unsigned long                vm_pgoff;          /* offset within file */

        struct file                  *vm_file;          /* mapped file, if any */

        void                         *vm_private_data;  /* private data */

};

      每個內存描述符都對應於地址進程空間中的唯一區間。vm_mm域指向和VMA相關的mm_struct結構體。兩個獨立的進程將同一個文件映射到各自的地址空間,它們分別都會有一個vm_area_struct結構體來標誌自己的內存區域;但是如果兩個線程共享一個地址空間,那麼它們也同時共享其中的所有vm_area_struct結構體。

      在上面的vm_flags域中存放的是VMA標誌,標誌了內存區域所包含的頁面的行爲和信息,反映了內核處理頁面所需要遵循的行爲準則,如下表下述:

    

      上表已經相當詳細了,而且給出了說明,我就不說了。在vm_area_struct結構體中的vm_ops域指向域指定內存區域相關的操作函數表,內核使用表中的方法操作VMA。vm_area_struct作爲通用對象代表了任何類型的內存區域,而操作表描述針對特定的對象實例的特定方法。操作函數表由vm_operations_struct結構體表示,定義在linux/mm.h中,如下:

struct vm_operations_struct {

        void (*open) (struct vm_area_struct *);

        void (*close) (struct vm_area_struct*);

        struct page * (*nopage) (structvm_area_struct *, unsigned long, int);

       int (*populate) (structvm_area_struct *, unsigned long, unsigned long,pgprot_t, unsigned long, int);

};

open:當指定的內存區域被加入到一個地址空間時,該函數被調用。

close:當指定的內存區域從地址空間刪除時,該函數被調用。

nopages:當要訪問的頁不在物理內存中時,該函數被頁錯誤處理程序調用。

populate:該函數被系統調用remap_pages調用來爲將要發生的缺頁中斷預映射一個新映射。

      記性好的你一定記得內存描述符中的mmap和mm_rb域都獨立地指向與內存描述符相關的全體內存區域對象。它們包含完全相同的vm_area_struct結構體的指針,僅僅組織方式不同而已。前者以鏈表的方式進行組織,所有的區域按地址增長的方向排序,mmap域指向鏈表中第一個內存區域,鏈中最後一個VMA結構體指針指向空。而mm_rb域採用紅--黑樹連接所有的內存區域對象。它指向紅--黑輸的根節點。地址空間中每一個vm_area_struct結構體通過自身的vm_rb域連接到樹中。關於紅黑二叉樹結構我就不細講了,以後可能會詳細說這個問題。內核之所以採用這兩種結構來表示同一內存區域,主要是鏈表結構便於遍歷所有節點,而紅黑樹結構體便於在地址空間中定位特定內存區域的節點。我麼可以使用/proc文件系統和pmap工具查看給定進程的內存空間和其中所包含的內存區域。這裏就不細說了。

      內核也爲我們提供了對內存區域操作的API,定義在linux/mm.h中:

(1)find_vma<定義在mm/mmap.c>中,該函數在指定的地址空間中搜索一個vm_end大於addr的內存區域。換句話說,該函數尋找第一個包含
    addr或者首地址大於addr的內存區域,如果沒有發現這樣的區域,該函數返回NULL;否則返回指向匹配的內存區域的vm_area_struct結構
   體指針。
(2)find_vma_prev().
函數定義和聲明分別在文件mm/mmap.c中和文件linux/mm.h中,它和find_vma()工作方式相同,但返回的是第一個小於 
    addr的VMA.
(3)find_vma_intersection().
定義在文件linux/mm.h中,返回第一個和指定地址區間相交的VMA,該函數是一個內斂函數。

      接下來要說的兩個函數就非常重要了,它們負責創建和刪除地址空間。
      內核使用do_mmap()函數創建一個新的線性地址空間。但如果創建的地址區間和一個已經存在的地址區間相鄰,並且它們具有相同的訪問權限的話,那麼兩個區間將合併爲一個。如果不能合併,那麼就確實需要創建一個新的vma了,但無論哪種情況,do_mmap()函數都會將一個地址區間加入到進程的地址空間中。這個函數定義在linux/mm.h中,如下:

unsigned long do_mmap(struct file*file, unsigned long addr, unsigned long len, unsigned long prot,unsigned longflag, unsigned long offset)

      這個函數中由file指定文件,具體映射的是文件中從偏移offset處開始,長度爲len字節的範圍內的數據,如果file參數是NULL並且offset參數也是0,那麼就代表這次映射沒有和文件相關,該情況被稱作匿名映射。如果指定了文件和偏移量,那麼該映射被稱爲文件映射(file-backed mapping),其中參數prot指定內存區域中頁面的訪問權限,這些訪問權限定義在asm/mman.h中,如下:

    

      flag參數指定了VMA標誌,這些標誌定義在asm/mman.h中,如下:

    

      如果系統調用do_mmap的參數中有無效參數,那麼它返回一個負值;否則,它會在虛擬內存中分配一個合適的新內存區域,如果有可能的話,將新區域和臨近區域進行合併,否則內核從vm_area_cach
ep長字節緩存中分配一個vm_area_struct結構體,並且使用vma_link()函數將新分配的內存區域添加到地址空間的內存區域鏈表和紅黑樹中,隨後還要更新內存描述符中的total_vm域,然後才返回新分配的地址區間的初始地址。在用戶空間,我們可以通過mmap()系統調用獲取內核函數do_mmap()的功能,這個在unix環境高級編程中講的很詳細,我就不好意思繼續說了。我們繼續往下走。
我們說既然有了創建,當然要有刪除了,是不?do_mummp()函數就是幹這事的。它從特定的進程地址空間中刪除指定地址空間,該函數定義在文件linux/mm.h中,如下:

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

      第一個參數指定要刪除區域所在的地址空間,刪除從地址start開始,長度爲len字節的地址空間,如果成功,返回0,否則返回負的錯誤碼。與之相對應的用戶空間系統調用是munmap。

      下面開始最後一點內容:頁表

      我們知道應用程序操作的對象是映射到物理內存之上的虛擬內存,但是處理器直接操作的確實物理內存。所以當應用程序訪問一個虛擬地址時,首先必須將虛擬地址轉化爲物理地址,然後處理器才能解析地址訪問請求。這個轉換工作需要通過查詢頁面才能完成,概括地講,地址轉換需要將虛擬地址分段,使每段虛地址都作爲一個索引指向頁表,而頁表項則指向下一級別的頁表或者指向最終的物理頁面。linux中使用三級頁表完成地址轉換。多數體系結構中,搜索頁表的工作由硬件完成,下表描述了虛擬地址通過頁表找到物理地址的過程:

    

      在上面這個圖中,頂級頁表是頁全局目錄(PGD),二級頁表是中間頁目錄(PMD).最後一級是頁表(PTE),該頁表結構指向物理頁。上圖中的頁表對應的結構體定義在文件asm/page.h中。爲了加快查找速度,在linux中實現了快表(TLB),其本質是一個緩衝器,作爲一個將虛擬地址映射到物理地址的硬件緩存,當請求訪問一個虛擬地址時,處理器將首先檢查TLB中是否緩存了該虛擬地址到物理地址的映射,如果找到了,物理地址就立刻返回,否則,就需要再通過頁表搜索需要的物理地址。

 

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