6.內存管理

內存管理

在內核中分配內存不像在其他地方分配內存那么容易。造成這種局面的因素很多,根本原因是內核本身不能像用戶空間那樣奢侈地使用內存。

1.頁

內核把物理頁作為內存管理的基本單位。內存管理單元(MMU,管理內存并把虛擬地址轉換為物理地址的硬件)通常以頁為單位。體系結構不同,支持的頁大小也不同。內核用struct page結構表示每個物理頁:

struct page {
         unsigned long flags;                                                      
         atomic_t count;                
         unsigned int mapcount;          
         unsigned long private;          
         struct address_space *mapping;  
         pgoff_t index;                  
         struct list_head lru;  
         void *virtual;                  
};

對上面重要變量說明:

  • flag的每一位單獨表示一個狀態,標志定義在<linux/page-flags.h>
  • count存放頁的引用次數,為0則是空閑頁
  • virtual是頁的虛擬地址

2.區

由于硬件限制,內核對頁不能一視同仁。有些頁位于內存特定的物理地址上,不能用于一些特定的任務,因此內核把頁劃分為不同的區(zone)。Linux必須處理如下兩種由于硬件缺陷而引起的內存尋址問題:

  • 一些硬件只能用某些特定內存來執行DMA(直接內存訪問)
  • 一些體系結構的內存物理尋址范圍比虛擬尋址范圍大的多,因此部分內存永遠無法映射到內核空間

因此Linux主要存在四種區:

  • ZONE_DMA,包含的頁可以執行DMA
  • ZONE_DMA32,和ZONE_DMA不同在于,這些頁面只能被32位設備訪問,某些體系下該區比ZONE_DMA更大
  • ZONE_NORMAL,能夠正常映射的頁
  • ZONE_HIGHMEM,不能永久被映射到內核空間地址的區

每個區都用struct zone表示,定義在<linux/mmzone.h>

struct zone {
         spinlock_t              lock;
         unsigned long           free_pages;
         unsigned long           pages_min, pages_low, pages_high;
         unsigned long           protection[MAX_NR_ZONES];
         spinlock_t              lru_lock;       
         struct list_head        active_list;
         struct list_head        inactive_list;
         unsigned long           nr_scan_active;
         unsigned long           nr_scan_inactive;
         unsigned long           nr_active;
         unsigned long           nr_inactive;
         int                     all_unreclaimable; 
         unsigned long           pages_scanned;    
         struct free_area        free_area[MAX_ORDER];
         wait_queue_head_t       * wait_table;
         unsigned long           wait_table_size;
         unsigned long           wait_table_bits;
         struct per_cpu_pageset  pageset[NR_CPUS];
         struct pglist_data      *zone_pgdat;
         struct page             *zone_mem_map;
         unsigned long           zone_start_pfn;
 
         char                    *name;
         unsigned long           spanned_pages;  
         unsigned long           present_pages;  
};

其中,lock是自旋鎖防止該結構被并發訪問;watermark數組持有該區的最小值、最低和最高水位值;name是以NULL結尾的區名字,三個區名字為DMANormalHighMem

3.獲得頁

前面了解了頁和區的概念,下面講述如何請求和釋放頁。

請求頁

標志 描述
alloc_page(gfp_mask) 只分配一頁,返回指向頁結構的指針
alloc_pages(gfp_mask, order) 分配2^order頁,返回指向第一頁頁結構的指針
__get_free_page(gfp_mask) 只分配一頁,返回指向其邏輯地址的指針
__get_free_pages(gfp_mask, order) 分配2^order頁,返回指向第一頁邏輯地址的指針
get_zeroed_page(gfp_mask) 只分配一頁,讓其內容填充0,返回指向邏輯地址的指針

釋放頁

釋放頁需要謹慎,只能釋放屬于你的頁。傳遞了錯誤的struct page或地址,,用了錯誤的order值都可能導致系統崩潰。

例如釋放8個頁:

free_pages(page, 3)

可以看到釋放過程與C語言的釋放內存很相似的。

4.kmalloc()

上述的方法是對以頁為單位的連續物理頁,而以字節為單位的分配,內核提供的函數是kmalloc()。使用方法和malloc()類似,只是多了一個flags參數,其在<linux/slab.h>中聲明:

void * kmalloc(size_t size, gfp_t flags)

kmalloc()對應的函數就是kfree()kfree()聲明于<linux/slab.h>中:

void kfree(const void *ptr)

5.vmalloc()

vmalloc()kmalloc()工作方式類似,但是kmalloc()使用的連續的物理地址。vmalloc()使用非連續的物理地址,該函數為了把物理上不連續的頁轉換為虛擬地址空間上連續的頁,必須專門建立頁表項。

大多數情況下,一般硬件設備需要使用連續的物理地址,而軟件可以使用非連續的物理地址,但是大多數情況,為了性能提升,內核往往用kmalloc()更多。

vmalloc()函數聲明在<linux/vmalloc.h>中,定義在<mm/vmalloc.c>中。用法和用戶空間的malloc()相同:

void * vmalloc(unsigned long size)

釋放通過vmalloc()所獲得的內存,使用下面函數:

void vfree(const void *addr)

6.slab層

分配和釋放數據結構是所有內核中最常用操作之一。為了便于數據的頻繁分配和回收,編程人員常常會用到空閑鏈表空閑鏈表包含可供使用的、已經分配好的數據結構塊。當代名需要一個新的數據結構實例時,就可以從空閑鏈表中抓取一個,而不需要分配內存,再把數據存放進去。不需要這個數據結構的實例時,就放回空閑鏈表,而不是釋放它。空閑鏈表相對于對象的高速緩存——快速存儲頻繁使用的對象類型(這個策略簡直是awesome!)。

沒有免費的蛋糕,對于空閑鏈表存在的主要問題是無法全局控制。當內存緊缺時,內核無法通知每個空閑鏈表,讓其收縮緩存的大小,以便釋放部分內存。實際上,內核根本就不知道任何空閑鏈表。因此未來彌補這個缺陷,Linux內核提供了slab層(也就是所謂的slab分配器)。slab分配器扮演了通用數據結構緩存層的角色。對于slab分配器設計需要考慮一下幾個原則:

  • 頻繁使用的數據結構也會頻繁分配和釋放,因此應當緩存它們。
  • 頻繁分配和回收必然會導致內存碎片。為了避免這種情況,空閑鏈表的緩存會連續地存放。因為已釋放的數據結構又會放回空閑鏈表,不會導致碎片。
  • 回收的對象可以立即投入下一次分配,因此,對于頻繁的分配和釋放,空閑鏈表能夠提高其性能。
  • 如果讓部分緩存專屬于單個處理器,那么,分配和釋放就可以在不加SMP鎖的情況下進行。
  • 對存放的對象進行著色,以防止多個對象映射到相同的高速緩存行。

slab層把不同的對象劃分為所謂的高速緩存組,其中每個高速緩存都存放不同類型的對象,每種對象類型對應一個高速緩存,例如一個高速緩存用于task_struct,一個用于struct inode。kmalloc()接口建立在slab層上,使用了一組通用高速緩存。這些緩存又被分為slabs,slab由一個或多個物理上連續的頁組成,一般情況下,slab也就僅僅由一頁組成。每個高速緩存可以由多個slab組成。每個slab都包含一些對象成員,這里的對象指的是被緩存的數據結構,每個slab處于三種狀態之一:滿,部分滿,空。當內核的某一部分需要一個新的對象時,先從部分滿的slab中進行分配。如果沒有部分滿的slab,就從空的slab中進行分配。如果沒有空的slab,就要創建一個slab了。下圖給出高速緩存,slab及對象之間的關系:

高速緩存、slab和對象關系

每個緩存都使用kmem_catche結構表示,結構中包含3個鏈表。這些鏈表包含高速緩存所有的slab。slab描述符struct slab用來描述每個slab:

struct slab {
        struct list_head  list;       /*滿,部分滿或空鏈表*/
        unsigned long     colouroff;  /*slab著色的偏移量*/
        void              *s_mem;     /*在slab中的第一個對象*/
        unsigned int      inuse;      /*已分配的對象數*/
        kmem_bufctl_t     free;       /*第一個空閑對象*/
};

slab層負責內存緊缺情況下所有底層的對齊、著色、分配、釋放和回收等。

7.棧上的靜態分配

在前面討論的分配例子,不少可以分配到棧上。用戶空間可以奢侈地負擔很大的棧,而且棧空間還可以動態增長,相反內核空間不能——棧小而固定。給每個進程分配一個固定小棧,可以減小內存消耗和棧管理任務負擔。

進程的內核棧大小既依賴體系結構,也和編譯時的選項有關。在任何一個函數中,都必須盡量節省棧資源。讓函數所有局部變量之后不要超過幾百字節(棧上分配大量的靜態分配是不理智的),棧溢出就會覆蓋掉臨近堆棧末端的數據。首先就是前面講的thread_info

8.每個CPU使用數據

支持SMP的操作系統使用每個CPU上的數據,對于給定的處理器其數據是唯一的。一般而言,每個CPU的數據存放在一個數組內,數組中的每一項對應著系統上一個存在的處理器,安裝當前處理器號就能確定這個數組的當前元素。

在Linux中引入了新的操作接口稱為percpu,頭文件<linux/percpu.h>聲明了所有接口操作例程,可以在文件mm/slab.c<asm/percpu.h>找到定義。

使用每個CPU數據的好處是:

  • 減少了數據鎖定
  • 大大減少了緩存失效,一個CPU操作另一個CPU的數據時,必須清理另一個CPU的緩存并刷新,存在不斷的緩存失效。持續不斷的緩存失效稱為緩存抖動

這種方式的唯一安全要求就是禁止內核搶占,同時注意進程在訪問每個CPU數據過程中不能睡眠——否則,喚醒之后可能已經到其他處理器上了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容