內存管理之6:虛存管理中的抽象

date: 2014-09-20 19:09linux

在軟件設計時,咱們通常要從需求中提取出抽象(類或者數據結構),而後圍繞這些抽象設計相關的算法。內存管理天然也不能例外,這一節咱們來看看爲了管理爲了內存以及整個虛存空間,linux提取哪些抽象,提取這些抽象背後的動機是什麼?這些抽象之間的關聯是什麼?算法

注:本文展現的結構體定義來自2.4.0版本的內核。數組

1. 4G虛存空間的劃分

前面講過,linux的頁式存儲管理爲虛存地址空間設置了兩種權限(段描述符中的DPL字段):最高級(0級)爲內核所使用,最低級(3級)爲用戶空間所使用。換句話說,linux區分兩種虛擬地址:系統空間的地址和用戶空間的地址,用戶空間無權訪問系統空間的地址,從而實現對系統空間的保護。另外,從linux設置的內核空間和用戶空間的段描述來看,內核空間和用戶空間均可以訪問0~4G的空間,但若是任憑內核空間與用戶空間在4G空間上隨意散佈、交織,能夠想象一下,光是管理這些空間都很費勁,更遑論地址空間保護了。緩存

將複雜的事情簡單化,linux將4G的虛存空間劃分爲兩塊,高端的1G歸內核,低端的3G歸用戶空間。這個分界線由常量TASK_SIZE表示(內核中,task表示進程,TASK_SIZE能夠理解爲進程用戶空間的大小)。每一個用戶進程都有獨立的3G用戶空間(說獨立,是說進程有本身的mm_struct結構,也有本身的目錄表pgd),全部進程共同擁有內核的1G空間。從用戶進程的視角來看,每一個進程都有4G的虛存空間,示意以下:安全

內核空間&&用戶空間

TASK_SIZE就像一道自然屏障,有了這個屏障,用戶空間和內核空間就能夠「井水不犯河水」了。藉由這個屏障,內核中相關的實現也簡單化了。最大的利好應該是內核空間與物理內存之間的映射關係簡單化了(下節將會講到)。隨着研究的深刻,你可能愈來愈體會到這一「將複雜問題簡單化」帶來的好處。數據結構

2. 內核空間的佈局

根據虛存空間與物理內存的映射關係的不一樣,內核空間還能夠細分,以下圖(偷圖自《深刻linux驅動程序內核機制》,我從新畫了下,加入4G之下的Gap):架構

內核空間按照映射的不一樣進行劃分

其中ZONE_DMA和ZONE_NORMAL這兩個zone中的物理內存直接映射到內核虛存空間中的「物理頁面直接映射區」。爲何叫直接映射區呢?這是由於在系統初始化時,已經將該區域到物理內存的映射頁表(固然包括對應的PGD和PMD了)所有創建好了,這個映射的「效果」是:app

  1. 直接映射區的大小由物理內存中ZONE_DMA和ZONE_NORMAL這兩個zone的大小決定,爲這兩個管理區的大小之和;
  2. 物理內存的0地址,對應直接映射區的起始地址(內核中用常量PAGE_OFFSET來表示這個起始地址,固然,該值爲3G);
  3. 直接映射區中的虛擬地址到物理地址的映射爲線性關係,即用虛擬地址減去PAGE_OFFSET便可獲得對應的物理地址。內核中專門爲此定義了宏__pa,相關的宏定義以下:
<page.h>
        
        #define __PAGE_OFFSET		(0xC0000000)
        ......
        #define PAGE_OFFSET		((unsigned long)__PAGE_OFFSET)
        #define __pa(x)			((unsigned long)(x)-PAGE_OFFSET)
        #define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))

  __pa將虛擬地址轉換對應的物理地址,而__va則恰好相反。框架

前面提到用戶空間大小TASK_SIZE爲3G,這裏的PAGE_OFFSET也爲3G,其實TASK_SIZE是由PAGE_OFFSET來定義的:ide

<processor.h>
    /*
     * User space process size: 3GB (default).
     */
    #define TASK_SIZE	(PAGE_OFFSET)

千萬不要小瞧內核在系統初始化期間爲「物理頁面直接映射起始區」所創建的映射,有了這個映射,將會帶來不少好處。首先這個區間的虛擬地址到物理地址的映射是「一步到位」的,在ZONE_DMA或ZONE_NORMAL區間分配物理頁面時,能夠直接獲得頁面對應的虛擬地址,不用再去操做頁表了,大大提升了效率。而用戶空間中堆棧的擴展則是藉由「頁面異常」一步步創建頁表,一步步進行擴張的。

圖中的其餘信息咱們經過下面的幾個問題來分析。

問題1:爲何物理內存要存在一個高端內存區ZONE_HIGHMEM?

雖然內核只有1G的虛存空間,但做爲操做系統的核心,它應該能管理到全部的物理內存。當物理內存超過1G時,顯然沒法將整個內存都作線性映射到內核的1G空間中。因此就將超出的部分單獨出來,經過其餘的方式去映射。這種映射方式就是:當內核要訪問ZONE_HIGHMEM中的一個物理頁時,先從動態映射區或者固定映射區臨時分配一個虛存頁,並經過設置頁表爲兩者創建頁面映射,這樣,內核就能夠臨時借用動態映射區或者固定映射區的虛擬地址去訪問ZONE_HIGHMEM中的內存了。訪問結束,再將虛擬地址歸還給動態映射區或者固定映射區。正由於動態映射區或者固定映射區的存在,內核不可能將整個1G虛存空間多做爲「直接映射區」,而高端內存ZONE_HIGHMEM也不只僅是物理內存中超出1G的部分。前面提到過的,物理內存區域的劃分以下:

  • ZONE_DMA First —— 16MiB of memory
  • ZONE_NORMAL —— 16MiB - 896MiB
  • ZONE_HIGHMEM —— 896 MiB - End

問題2:爲何要有vmalloc區?

與問題1的情形相反,若是系統的物理內存比較緊缺,好比嵌入式領域,物理內存一般都比較小,從「物理頁面直接映射區」中沒法得到連續的物理內存區域,那麼就能夠利用vmalloc函數來將不連續的物理內存拼湊出一塊連續(虛擬地址空間連續,最終仍是靠頁表來建議頁面映射)的內存區域。與問題1相似,vmalloc函數的實現原理分爲以下三步:

  1. 在VMALLOC區分配出一段連續的虛擬內存區域;
  2. 經過夥伴系統獲取物理頁;
  3. 經過頁表的操做將步驟①中虛擬內存映射到步驟②中得到的物理頁面上。

問題3:「物理頁面直接映射區」與vmalloc區之間、vmalloc區域動態映射區之間、以及4G空間之下都有一個Gap,是作什麼用的?

Gap區就像一個空洞,內核不會在空洞進行任何的地址映射,這主要用做安全保護,防止越界訪問。因爲這些區間沒有作地址映射,那麼訪問這些區間的地址時處理器將觸發頁面異常,內核捕獲到這個異常後就可進行相應的處理。Gap就像是內核故意設置的陷阱,而後內核說「夠膽你就踩吧,反正我已經嚴陣以待了」。

除此之外,Gap還有其餘的妙用,好比內核中ERR_PTR、PTR_ER和IS_ERR函數就是利用了4G之下的空洞來實現「將錯誤碼地址化」,並據此來判斷「究竟是錯誤碼仍是正常地址」。詳情請參考另外一篇文章《也談ERR_PTR、PTR_ERR和IS_ERR》。

問題4:若是物理內存小於1G的話將不存在高端內存區ZONE_HIGHMEM,此時整個物理內存都將會被映射到內核空間中的「物理頁面直接映射區」,那麼進程的用戶空間豈不是沒有物理內存可用了?

咱們知道,內核的鏡像也就幾十個MB,內核確定用不完整個物理內存的。咱們要明確一點,內核使用內存也是要向夥伴系統申請的,「物理頁面直接映射區」的存在並非說將整個物理內存都分配給內核使用。這裏的「物理頁面直接映射區」至關於給內核開了個「綠色通道」,或者說是夥伴系統給內核「開後門」。當內核申請內存時,固然是須要返回內存的虛擬地址了,夥伴系統從ZONE_DMA或ZONE_NORMAL區間分配物理內存頁面以後,藉由這個「綠色通道」,就能迅速的從「物理地址」轉換到「虛擬地址」了,再將虛擬地址提供給內核使用。

物理內存是「供應方」,虛擬內存是「需求方」。「物理頁面直接映射區」是「供應方」到「需求方」的「綠色通道」,這個綠色通道很寬,可是內核不見得就要佔盡整個綠色通道,內核沒使用那部分通道,其對應的物理內存仍能夠被進程的用戶空間使用到。

3 進程用戶空間mm_struct

x86架構下,進程用戶空間的典型佈局以下:

進程用戶空間的佈局

  • 進程的命令行參數和環境變量存儲在0XC0000000下方的第一個區域,以後纔是堆棧區。
  • 在某些場合下,咱們認爲堆棧從0XC0000000地址開始,這固然是在不影響討論內容狀況下的一種粗略的描述。堆棧的增加方向是往下增加,意味着每執行一次入棧操做(push),棧頂指針esp將減4。 堆Heap的增加方向爲往上增加。在傳統佈局中,Heap的上限爲0X40000000,意味着堆的大小不可能超過1G。從2.6版本之後的內核中,引入了新式佈局,使得堆能夠突破1G。(後面可能會講到,還不肯定)。
  • 程序的代碼段(text段,也叫正文段)從0x08048000地址開始,0地址到0x08048000之間的區域保留不用,這固然也是出於安全保護的目的。

在內核中,用mm_struct來描述進程的用戶空間,結構的定義在<include/linux/sched.h>文件中:

struct mm_struct {
    	struct vm_area_struct * mmap;		/* list of VMAs */
    	struct vm_area_struct * mmap_avl;	/* tree of VMAs */
    	struct vm_area_struct * mmap_cache;	/* last find_vma result */
    	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 semaphore mmap_sem;
    	spinlock_t page_table_lock;
    
    	struct list_head mmlist;		/* List of all active mm's */
    
    	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 rss, total_vm, locked_vm;
    	unsigned long def_flags;
    	unsigned long cpu_vm_mask;
    	unsigned long swap_cnt;	/* number of pages to swap on next pass */
    	unsigned long swap_address;
    
    	/* Architecture-specific MM context */
    	mm_context_t context;
    };
  • mm_struct結構是對進程整個用戶空間的抽象。每一個進程都有一個mm_struct結構,在每一個進程的進程控制塊即task_struct結構體中,有一個指針(mm)指向該進程的mm_struct結構。

  • 雖然用戶空間多達3G,但若是不認真組織打理,最終也會混亂不堪。就像用戶空間佈局圖中展現的同樣,內核將3G的空間分紅更小粒度的虛存區間(對應結構體vm_area_struct)來管理。成員mmap用來將用戶空間中全部的虛存區間組成一個單鏈表,mmap做爲鏈表頭;成員mmap_avl用來將全部的虛存區間作成一個AVL樹,mmap_avl做爲樹的根節點;成員mmap_cache用來緩存上一次查找獲得的vm_area_struct結構,以便下一次查找時提升效率。

  • pgd成員引領用戶空間的頁面映射。每當調度一個進程進入運行的時候(意味着即將進入進程的用戶空間去運行程序),內核都要爲即將運行的進程設置好控制寄存器CR3,而MMU的硬件老是從CR3中取得當前頁面目錄表的指針。不過CPU在執行代碼是使用的是虛擬地址,而MMU硬件在進行映射時使用的是物理地址,所以須要一個從虛擬地址到物理地址的轉換。還記得__pa這個宏,這裏就要用到它了。對應的代碼在<asm/ mmu_context.h >的switch_mm函數中:

static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, 
                                        struct task_struct *tsk, unsigned cpu)
        {
              ...
	    	  /* Re-load page tables */
	    	  asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));
             ...
        }

 問題:在即將離開內核空間要進入到進程的用戶空間以前須要將CR3設置爲進程的pgd,那麼反過來,在從用戶空間陷入內核空間時,是否也須要將內核的pgd設置進CR3寄存器中呢?

 答案是不須要設置。mm_struct結構中的pgd表明着整個的4G空間,頁面目錄表其實分爲兩部分:一部分表明着內核空間的虛存區間,一部分表明着用戶空間的虛存期間。對於不一樣的進程,頁面目錄表中表明內核空間的目錄項是一致的(意味着其下屬的頁面表也是一致的),其與物理內存的映射是在系統初始化階段創建的(初始化期間存儲在swapper_pg_dir表中);而表明用戶空間的那部分則各自爲政。不信?有代碼爲證。第4章講execve系統調用時,在爲當前進程構建新的用戶空間時,會依次調用mm_alloc()-->mm_init()-->pgd_alloc()-->get_pgd_fast()-->get_pgd_slow()。咱們來看看這段代碼:

<arch/i386/kernel/head.s>
    383 /*
    384  * This is initialized to create an identity-mapping at 0-8M (for bootup
    385  * purposes) and another mapping of the 0-8M area at virtual address
    386  * PAGE_OFFSET.
    387  */
    388 .org 0x1000
    389 ENTRY(swapper_pg_dir)
    390         .long 0x00102007
    391         .long 0x00103007
    392         .fill BOOT_USER_PGD_PTRS-2,4,0
    393         /* default: 766 entries */
    394         .long 0x00102007
    395         .long 0x00103007
    396         /* default: 254 entries */
    397         .fill BOOT_KERNEL_PGD_PTRS-2,4,0
    398
    
    <include/asm/pgtable.h>
    /*TASK_SIZE爲3G, PGDIR_SIZE爲4M,所以USER_PTRS_PER_PGD爲768,
      表示目錄表中前768個目錄項表明着進程的用戶空間*/
    #define USER_PTRS_PER_PGD	 (TASK_SIZE/PGDIR_SIZE)  
    
    <include/asm/pgalloc.h>
    extern __inline__ pgd_t *get_pgd_slow(void)
    {
        //分配一個頁面即4K
    	pgd_t *ret = (pgd_t *)__get_free_page(GFP_KERNEL);
    
    	if (ret) {
           //將頁面的內容置0
    		memset(ret, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));
           //從swapper_pg_dir中拷貝表明着內核空間的目錄項(共256個)
    		memcpy(ret + USER_PTRS_PER_PGD, 
                  swapper_pg_dir + USER_PTRS_PER_PGD, 
    				(PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
    	}
    	return ret;
    }

swapper_pg_dir即初始化期間使用的頁面目錄表。第392行利用匯編語言提供的功能在當前位置填入766個目錄項,每一個目錄項的大小爲4字節,內容爲0;一樣第397行業填入254個這樣的目錄項。可見swapper_pg_dir有1024個目錄項,是一個真正的頁面目錄表。固然,該表的內容在初始化期間會逐漸被更新。

由此,咱們得出結論,將進程mm_struct結構中的pgd設置進CR3,內核空間、進程的用戶空間均可以正常進行頁面映射了。

  • 一個進程對應一個mm_struct結構,但反過來卻不成立。一個mm_struct結構可能被多個進程所共享。好比當一個進程建立(vfork或者clone)一個子進程時,其子進程就可能和父進程共享一個mm_struct結構。因此mm_struct結構體中有mm_users成員表示用戶數,mm_count成員表示引用計數。
  • start_code、 end_code等成員請參考用戶空間佈局圖。

4. 虛存區間

虛存區間對應的結構體爲vm_area_struct,定義在<linux/mm.h>中:

/*
     * This struct defines a memory VMM memory area. There is one of these
     * per VM-area/task.  A VM area is any part of the process virtual memory
     * space that has a special rule for the page-fault handlers (ie a shared
     * library, the executable area etc).
     */
    struct vm_area_struct {
    	struct mm_struct * vm_mm;	/* VM area parameters */
    	unsigned long vm_start;
    	unsigned long vm_end;
    
    	/* linked list of VM areas per task, sorted by address */
    	struct vm_area_struct *vm_next;
    
    	pgprot_t vm_page_prot;
    	unsigned long vm_flags;
    
    	/* AVL tree of VM areas per task, sorted by address */
    	short vm_avl_height;
    	struct vm_area_struct * vm_avl_left;
    	struct vm_area_struct * vm_avl_right;
    
    	/* For areas with an address space and backing store,
    	 * one of the address_space->i_mmap{,shared} lists,
    	 * for shm areas, the list of attaches, otherwise unused.
    	 */
    	struct vm_area_struct *vm_next_share;
    	struct vm_area_struct **vm_pprev_share;
    
    	struct vm_operations_struct * vm_ops;
    	unsigned long vm_pgoff;		/* offset in PAGE_SIZE units, 
                                          *not* PAGE_CACHE_SIZE */
    	struct file * vm_file;
    	unsigned long vm_raend;
    	void * vm_private_data;		/* was vm_pte (shared mem) */
    };

[vm_start, vm_end)定義了一個虛存區間的範圍,這是一個前閉後開的區間。

區間的劃分並不只僅取決於地址的連續性,還有地址的訪問權限等其餘因素。因此包含在同一個虛存區間中全部頁面具備相同的訪問權限和其餘屬性,這些由成員vm_page_prot和vm_flags來表示。

屬於同一個進程的全部區間都要按照起始地址從低到高連接在一塊兒,這就是vm_next的做用。

給定一個虛擬地址,找到它所屬的虛存區間是個頻繁調用的動做,爲了提升效率,進程的全部區間構成了一棵AVL樹,這就是vm_avl_height、vm_avl_left和vm_avl_right的做用。

在兩種狀況下,虛存頁面會和磁盤發生關聯。一種是盤區交換:將久未使用的頁面交換到磁盤上,從而騰出物理頁面供更急需的進程使用。另外一種是mmap系統調用,將磁盤上的文件映射到進程的虛擬地址空間中,此後就能夠像訪問內存中的字符數組同樣來訪問文件的內容,而沒必要使用read、lseek和write這些費時的操做。vm_next_share、vm_pprev_share和vm_file等就是用來記錄和管理這種聯繫。(後面可能會講到)。

區間結構體中另外一重要的成員是vm_operations_struct 類型的指針vm_ops,表明區間上的相關操做,vm_operations_struct定義在同一個文件中:

/*
     * These are the virtual MM functions - opening of an area, closing and
     * unmapping it (needed to keep files on disk up-to-date etc), pointer
     * to the functions called when a no-page or a wp-page exception occurs. 
     */
    struct vm_operations_struct {
    	void (*open)(struct vm_area_struct * area);
    	void (*close)(struct vm_area_struct * area);
	    struct page * (*nopage)(struct vm_area_struct * area, 
                                  unsigned long address, int write_access);
    };

這裏定義了一組函數指針,這些函數與文件操做有關。爲何要有這些函數呢?這是由於對不一樣的虛存區間可能須要一些不一樣的附加操做。其中,nopage指定了當該區間發生了頁面異常時應該執行的操做,該函數一般會嘗試申請物理內存頁面,並設置頁面表項來修復「異常頁面」。物理內存頁面的分配爲何和文件操做有關呢?首先考慮文件共享的情形,當多個進程將同一個文件映射到各自的虛存空間時,內存中只須要保留一份物理頁面便可,只有當某個進程須要寫入時,纔有必要另外複製一份獨立的副本,此即Copy On Wrtie。這種狀況下,物理頁面的分配(是否不須要從新分配,用以前的只讀副本就能夠了?仍是說有進程要進行寫操做,必需要分配新物理頁面?)顯然與文件有關。其次,進程經過mmap將文件映射到虛存區間中,當在用戶空間像讀寫內存同樣讀寫文件時,必然致使虛存區間的擴展(好比從文件頭讀到文件尾),伴隨着虛存區間的擴展,其底層必然伴隨着分配物理頁面並將文件內容讀入物理頁面的操做。此外,內存頁面與磁盤頁面的交換顯然也是和文件操做相關的。

mm_struct結構及其旗下的各個vm_area_struct結構只是代表了對虛存空間的需求。一個虛擬地址存在於某個虛存區間中,並保證該地址所在的虛存頁面已經映射到一個物理(內存或磁盤)頁面上,更不保證該頁面就在內存中。當訪問一個未經映射的頁面時,將觸發Page Falut(也成缺頁異常),那時Page Fault的異常服務程序會來處理該問題。因此,從這個意義上講,mm_struct以及vm_area_struct結構說明了對頁面的需求,前面的zone和page則說明了頁面的供應,而頁面目錄、中間目錄和頁面表則是兩者之間的橋樑。這種關係能夠描述以下:

進程虛擬內存管理框架圖(結構體之間的聯繫)

相關文章
相關標籤/搜索