天天看點

Linux記憶體管理a【轉】

 ​

關于Linux的記憶體管理,本文分别從核心空間和使用者空間兩個視角來闡述

一、核心空間

1.1 頁

頁(page)是核心的記憶體管理基本機關。

==> linux/mm_types.h

struct page {
       page_flags_t flags;  頁标志符
       atomic_t _count;    頁引用計數
       atomic_t _mapcount;     頁映射計數
       unsigned long private;    私有資料指針
       struct address_space *mapping;    該頁所在位址空間描述結構指針,用于内容為檔案的頁幀
       pgoff_t index;               該頁描述結構在位址空間radix樹page_tree中的對象索引号即頁号
       struct list_head lru;        最近最久未使用struct slab結構指針連結清單頭變量
       void *virtual;               頁虛拟位址
};
      
  • flags:頁标志包含是不是髒的,是否被鎖定等等,每一位單獨表示一種狀态,可同時表示出32種不同狀态,定義在<linux/page-flags.h>
  • _count:計數值為-1表示未被使用。
  • virtual:頁在虛拟記憶體中的位址,對于不能永久映射到核心空間的記憶體(比如高端記憶體),該值為NULL;需要事必須動态映射這些記憶體。

盡管處理器的最小可尋址機關通常為字或位元組,但記憶體管理單元(MMU,把虛拟位址轉換為實體位址的硬體裝置)通常以頁為機關處理。核心用struct page結構體表示每個實體頁,struct page結構體占40個位元組,假定系統實體頁大小為4KB,對于4GB實體記憶體,1M個頁面,故所有的頁面page結構體共占有記憶體大小為40MB,相對系統4G,這個代價并不高。

1.2 區

核心把頁劃分在不同的區(zone)

總共3個區,具體如下:

描述 實體記憶體(MB)
ZONE_DMA DMA使用的頁 <16
ZONE_NORMAL 可正常尋址的頁 16 ~896
ZONE_HIGHMEM 動态映射的頁 >896
  • 執行DMA操作的記憶體必須從ZONE_DMA區配置設定
  • 一般記憶體,既可從ZONE_DMA,也可從ZONE_NORMAL配置設定,但不能同時從兩個區配置設定;

1.3 頁配置設定與釋放

下面列舉所有的頁為機關進行連續實體記憶體配置設定,也稱為低級頁配置設定器:

頁配置設定函數
alloc_pages(gfp_mask, order) 配置設定2^order個頁,傳回指向第一頁的指針
alloc_pages(gfp_mask) 配置設定一頁,傳回指向頁的指針
__get_free_pages(gfp_mask, order) 配置設定2^order個頁,傳回指向其邏輯位址的指針
__get_free_pages(gfp_mask) 配置設定一頁,傳回指向其邏輯位址的指針
get_zeroed_page(gfp_mask) 配置設定一頁,并填充内容為0,傳回指向其邏輯位址的指針
  • get_zeroed_page:對于使用者空間,這個方法能保障系統敏感資料不會洩露
  • page_address: 把給定的頁轉換成邏輯位址
頁釋放函數
__free_pages(page, order) 從page開始,釋放2^order個頁
free_pages(addr, order) 從位址addr開始,釋放2^order個頁
free_page(addr) 釋放addr所在的那一頁

1.4 位元組配置設定與釋放

kmalloc,vmalloc配置設定都是以位元組為機關

(1) kmalloc

void * kmalloc(size_t size, gfp_t flags)
      

該函數傳回的是一個指向記憶體塊的指針,其記憶體塊大小至少為size,所配置設定的記憶體在實體記憶體中連續且保持原有的資料(不清零)

其中部分flags取值說明:

  • GFP_USER: 用于使用者空間的配置設定記憶體,可能休眠;
  • GFP_KERNEL:用于核心空間的記憶體配置設定,可能休眠;
  • GFP_ATOMIC:用于原子性的記憶體配置設定,不會休眠;典型原子性場景有中斷處理程式,軟中斷,tasklet等

kmalloc記憶體配置設定最終總是調用__get_free_pages 來進行實際的配置設定,故字首都是GFP_開頭。 kmalloc分最多隻能配置設定32個page大小的記憶體,每個page=4k,也就是128K大小,其中16個位元組用來記錄頁描述結構。kmalloc配置設定的是常駐記憶體,不會被交換到檔案中。最小配置設定機關是32或64位元組。

kzalloc

​kzalloc()​

​等價于先用 ​

​kmalloc()​

​ 申請空間, 再用​

​memset()​

​來初始化,所有申請的元素都被初始化為0。

static inline void *kzalloc(size_t size, gfp_t flags)
{
    return kmalloc(size, flags | __GFP_ZERO); //通過或标志位__GFP_ZERO,初始化元素為0
}
      

(2) vmalloc

void * vmalloc(unsigned long size)
      

該函數傳回的是一個指向記憶體塊的指針,其記憶體塊大小至少為size,所配置設定的記憶體是邏輯上連續的。

kmalloc不同,該函數乜有flags,預設是可以休眠的。

小結:

配置設定函數 區域 連續性 大小 釋放函數 優勢
kmalloc 核心空間 實體位址連續 最大值128K-16 kfree 性能更佳
vmalloc 虛拟位址連續 更大 vfree 更易配置設定大記憶體
malloc 使用者空間 free

1.5 slab層

slab配置設定器的作用:

  • 對于頻繁地配置設定和釋放的資料結構,會緩存它;
  • 頻繁配置設定和回收比如導緻記憶體碎片,為了避免,空閑連結清單的緩存會連續的存放,已釋放的資料結構又會放回空閑連結清單,不會導緻碎片;
  • 讓部分緩存專屬單個處理器,配置設定和釋放操作可以不加SMP鎖;

slab層把不同的對象劃分為高速緩存組,每個高速緩存組都存放不同類型的對象,每個對象類型對應一個高速緩存。kmalloc接口監理在slab層隻是,使用一組通用高速緩存。

每個高速緩存都是用kmem_cache結構來表示

  • kmem_cache_crreate:建立高速緩存
  • kmem_cache_destroy: 撤銷高速緩存
  • kmem_cache_alloc: 從高速緩存中傳回一個指向對象的指針
  • kmem_cache_free:釋放一個對象

執行個體分析: 核心初始化期間,/kernel/fork.c的fork_init()中會建立一個名叫task_struct的高速緩存; 每當程序調用fork()時,會通過dup_task_struct()建立一個新的程序描述符,并調用do_fork(),完成從高速緩存中擷取對象。

1.6 棧的靜态配置設定

當設定單頁核心棧,那麼每個程序的核心棧隻有一頁大小,這取決于編譯時配置選項。 好處:

  • 可以減少每個程序記憶體的消耗;
  • 随着機器運作時間的增加,尋找兩個未配置設定的、連續的頁越來越困難,實體記憶體碎片化不斷加重,那麼給每個新程序配置設定虛拟記憶體的壓力也增大;
  • 每個程序的調用鍊在自己的核心棧中,當單頁棧選項被激活時,中斷處理程式可獲得自己的棧;

任意函數必須盡量節省棧資源, 方法就是所有函數讓局部變量所占空間之和不要超過幾百位元組。

1.7 高端記憶體的映射

高端記憶體中的頁不能永久地映射到核心位址空間。

  • kmap:把給定page結構映射到核心位址空間;
  • 當page位于低端記憶體,函數傳回該頁的虛拟位址
  • 當page位于高端記憶體,建立一個永久映射,再傳回位址
  • kunmap: 永久映射的數量有限,應通過kunmap及時解除映射
  • kmap_atomic: 臨時映射
  • kunmap_atomic: 解除臨時映射

1.8 每個CPU資料

  • alloc_percpu: 給系統的每個處理器配置設定一個指定類型對象的執行個體,以單位元組對齊;
  • free_percpu: 釋放每個處理器的對象執行個體;
  • get_cpu_var: 傳回一個執行目前處理器資料的特殊執行個體,同時會禁止核心搶占
  • put_cpu_var: 會重新激活核心搶占

使用每個CPU資料好處:

  • 減少了資料鎖定,每個CPU通路自己CPU資料
  • 大大減少緩存失效,失效往往發生在一個處理器操作某個資料,而其他處理器緩存了該資料,那麼必須清理或重新整理緩存。持續不斷的緩存失效稱為緩存抖動。

1.9 小結

配置設定函數選擇:

  1. 連續的實體頁,使用低級頁配置設定器 或kmalloc();
  2. 高端記憶體配置設定,使用alloc_pages(),傳回page結構指針; 想擷取位址指針,應使用kmap(),把高端記憶體映射到核心的邏輯位址空間;
  3. 僅僅需要虛拟位址連續頁,使用vmalloc(),性能有所損失;
  4. 頻繁建立和撤銷大量資料結構,考慮建立slab高速緩存。

二、使用者空間

使用者空間中程序的記憶體,往往稱為程序位址空間。Linux采用虛拟記憶體技術

2.1 位址空間

每個程序都有一個32位或64位的位址空間,取決于體系結構。 一個程序的位址空間與另一個程序的位址空間即使有相同的記憶體位址,也彼此互不相幹,對于這種共享位址空間的程序稱之為線程。一個程序可尋址4GB的虛拟記憶體(32位位址空間中),但不是所有虛拟位址都有權通路。對于程序可通路的位址空間稱為記憶體區域。每個記憶體區域都具有對相關程序的可讀、可寫、可執行屬性等相關權限設定。

記憶體區域可包含的對象:

  • 代碼段(text section): 可執行檔案代碼
  • 資料段(data section): 可執行檔案的已初始化全局變量(靜态配置設定的變量和全局變量)。
  • bss段:程式中未初始化的全局變量,零頁映射(頁面的資訊全部為0值)。
  • 程序使用者空間棧的零頁映射(程序的核心棧獨立存在并由核心維護)
  • 每一個諸如C庫或動态連接配接程式等共享庫的代碼段、資料段和bss也會被載入程序的位址空間
  • 任何記憶體映射檔案
  • 任何共享記憶體段
  • 任何匿名的記憶體映射(比如由malloc()配置設定的記憶體)

這些記憶體區域不能互相覆寫,每一個程序都有不同的記憶體片段。

2.2 記憶體描述符

記憶體描述符由​

​mm_struct​

​結構體表示,

==> linux/sched.h

struct mm_struct
{
    struct vm_area_struct *mmap;
    rb_root_t mm_rb;
    ...
    atomic_t mm_users;
    atomic_t mm_count;

    struct list_head mmlist;
    ...
};
      
  • mm_users:代表正在使用該位址的程序數目,當該值為0時mm_count也變為0;
  • mm_count: 代表mm_struct的主引用計數,當該值為0說明沒有任何指向該mm_struct結構體的引用,結構體會被撤銷。
  • mmap和mm_rb:描述的對象都是相同的
  • mmap以連結清單形式存放, 利于高效地周遊所有元素
  • mm_rb以紅黑樹形式存放,适合搜尋指定元素
  • mmlist:所有的mm_struct結構體都通過mmlist連接配接在一個雙向連結清單中,該連結清單的首元素是init_mm記憶體描述符,它代表init程序的位址空間。

在程序的程序描述符(<linux/sched.h>中定義的task_struct結構體)中,mm域記錄該程序使用的記憶體描述符。故current->mm代表目前程序的記憶體描述符。

fork()函數 利用copy_mm函數複制父程序的記憶體描述符,子程序中的mm_struct結構體通過allcote_mm()從高速緩存中配置設定得到。通常,每個程序都有唯一的mm_struct結構體,即唯一的程序位址空間。

當子程序與父程序是共享位址空間,可調用clone(),那麼不再調用allcote_mm(),而是僅僅是将mm域指向父程序的mm,即 tsk->mm = current->mm。

相反地,撤銷記憶體是exit_mm()函數,該函數會進行正常的撤銷工作,更新一些統計量。

核心線程

  • 沒有程序位址空間,即核心線程對應的程序描述符中mm=NULL
  • 核心線程直接使用前一個程序的記憶體描述符,僅僅使用位址空間中和核心記憶體相關的資訊

2.3 虛拟記憶體區域(VMA)

虛拟記憶體區域由vm_area_struct結構體描述, 指定位址空間内連續區間的一個獨立記憶體範圍。 每個VMA代表不同類型的記憶體區域。

struct vm_area_struct {
    struct mm_struct * vm_mm;  //記憶體描述符
    unsigned long  vm_start;   //區域的首位址
    unsigned long vm_end;      //區域的尾位址
    struct vm_area_struct * vm_next; //VMA連結清單
    pgrot t_vm_page_prot;   //通路控制權限
    unsigned long vm_flags;   //保護标志位和屬性标志位
    struct rb_node_ vm_rb;   //VMA的紅黑樹結構
    ...
    struct vm_operations_struct * vm_ops; //相關的操作表
    struct file * vm_file; //指向被映射的檔案的指針
    void * vm_private_data; //裝置驅動私有資料,與記憶體管理無關。
}
      

每個記憶體描述符對應于程序位址空間的唯一區間,vm_end - vm_start便是記憶體區間的長度。

VMA操作

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);
    ...
}
      

檢視程序記憶體空間

cat /proc/<pid>/maps
      

每行資料格式: 開始-結束 通路權限 偏移 主裝置号:次裝置号 i節點 檔案

  • 裝置表示為00:00, 索引節點标示頁為0,這個區域就是零頁(所有資料全為零)
  • 資料段和bss具有可讀、可寫但不可執行權限;而堆棧可讀、可寫、甚至可執行

也可通過工具pmap

pmap <pid>
      

2.4 記憶體區域操作

find_vma 檢視mm_struct所屬于的VMA,搜尋第一個vm_end大于addr的記憶體區域

struct vm_area_struct *find_vma(struct mm_struct *mm, usigned long addr)
      
  1. 檢查mmap_cache,檢視緩存VMA是否包含所需位址,如果沒有找到,進入2
  2. 通過紅黑樹搜尋;

find_vma_prev 檢視mm_struct所屬于的VMA,搜尋第一個vm_end小于addr的記憶體區域

struct vm_area_struct * find_vma_prev(struct mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev)
      

mmap

  • 核心使用do_mmap()建立一個新的線性位址區間,如果建立的位址區間和一個已存在的相鄰位址區間有相同的通路權限,則将兩個區間合并為一個。
  • mmap()系統調用擷取核心函數do_mmap()的功能。
  • do_mummap()從特定的程序位址空間中删除指定位址區間
  • mummap()與 mmap功能相反。

2.5 頁表

應用程式操作的對象時映射到實體記憶體之上的虛拟記憶體,而處理器直接操作的是實體記憶體。故應用程式通路一個虛拟位址時,需要将虛拟位址轉換為實體位址,然後處理器才能解析位址通路請求,這個轉換工作通過查詢頁表完成。

Linux使用三級頁表完成位址轉換。

Linux記憶體管理a【轉】
  1. 頂級頁表:頁全局目錄(PGD),指向二級頁目錄;
  2. 二級頁表:中間頁目錄(PMD),指向PTE中的表項;
  3. 最後一級:頁表(PTE),指向實體頁面。

多數體系結構,搜尋頁表工作由硬體完成。每個程序都有自己的頁表(線程會共享頁表)。為了加快搜尋,實作了翻譯後緩沖器(TLB),作為将虛拟位址映射到實體位址的硬體緩存。還有寫時拷貝方式共享頁表,當fork()時,父子程序共享頁表,隻有當子程序或父程序試圖修改特定頁表項時,核心才建立該頁表項的新拷貝,之後父子程序不再共享該頁表項。可見,利用共享頁表可以消除fork()操作中頁表拷貝所帶來的消耗。

三、程序與記憶體

所有程序都必須占用一定數量的記憶體,這些記憶體用來存放從磁盤載入的程式代碼,或存放來自使用者輸入的資料等。記憶體可以提前靜态配置設定和統一回收,也可以按需動态配置設定和回收。

對于普通程序對應的記憶體空間包含5種不同的資料區:

  • 代碼段
  • 資料段
  • BSS段
  • 堆:動态配置設定的記憶體段,大小不固定,可動态擴張(malloc等函數配置設定記憶體),或動态縮減(free等函數釋放);
  • 棧:存放臨時建立的局部變量;
Linux記憶體管理a【轉】

3.1 程序記憶體空間

Linux采用虛拟記憶體管理技術,每個程序都有各自獨立的程序位址空間(即4G的線性虛拟空間),無法直接通路實體記憶體。這樣起到保護作業系統,并且讓使用者程式可使用比實際實體記憶體更大的位址空間。

  • 4G程序位址空間被劃分兩部分,核心空間和使用者空間。使用者空間從0到3G,核心空間從3G到4G;
  • 使用者程序通常情況隻能通路使用者空間的虛拟位址,不能通路核心空間虛拟位址。隻有使用者程序進行系統調用(代表使用者程序在核心态執行)等情況可通路到核心空間;
  • 使用者空間對應程序,是以當程序切換,使用者空間也會跟着變化;
  • 核心空間是由核心負責映射,不會跟着程序變化;核心空間位址有自己對應的頁表,使用者程序各自有不同額頁表。
Linux記憶體管理a【轉】

3.2 記憶體配置設定

程序配置設定記憶體,陷入核心态分别由brk和mmap完成,但這兩種配置設定還沒有配置設定真正的實體記憶體,真正配置設定在後面會講。

  • brk: 資料段的最高位址指針_edata往高位址推
  • 當malloc需要配置設定的記憶體<M_MMAP_THRESHOLD(預設128k)時,采用brk;
  • brk配置設定的記憶體需高位址記憶體全部釋放之後才會釋放。(由于是通過推動指針方式)
  • 當最高位址空間的空閑記憶體大于M_TRIM_THRESHOLD時(預設128k),執行記憶體緊縮操作;
  • do_mmap:在堆棧中間的檔案映射區域找空閑的虛拟記憶體
  • 當malloc需要配置設定的記憶體>M_MMAP_THRESHOLD(預設128k)時,采用do_map();
  • mmap配置設定的記憶體可以單獨釋放

3.3 實體記憶體

  • 實體記憶體隻有程序真正去通路虛拟位址,發生缺頁中斷時,才配置設定實際的實體頁面,建立實體記憶體和虛拟記憶體的映射關系。
  • 應用程式操作的是虛拟記憶體;而處理器直接操作的卻是實體記憶體。當應用程式通路虛拟位址,必須将虛拟位址轉化為實體位址,處理器才能解析位址通路請求。
  • 實體記憶體是通過分頁機制實作的
  • 實體頁在系統中由也結構struct page描述,所有的page都存儲在數組mem_map[]中,可通過該數組找到系統中的每一頁。

虛拟記憶體 轉化為 真實實體記憶體:

  • 虛拟程序空間:通過查詢程序頁表,擷取實際實體記憶體位址;
  • 虛拟核心空間:通過查詢核心頁表,擷取實際實體記憶體位址;
  • 實體記憶體映射區:實體記憶體映射區與實際實體去偏移量僅PAGE_OFFSET,通過通過virt_to_phys()轉化;

虛拟記憶體與真實實體記憶體映射關系:

Linux記憶體管理a【轉】

其中實體位址空間中除了896M(ZONE_DMA + ZONE_NORMAL)的區域是絕對的實體連續,其他記憶體都不是實體記憶體連續。在虛拟核心位址空間中的安全保護區域的指針都是非法的,用于保證指針非法越界類的操作,vm_struct是連續的虛拟核心空間,對應的實體頁面可以不連續,位址範圍(3G + 896M + 8M) ~ 4G;另外在虛拟使用者空間中 vm_area_struct同樣也是一塊連續的虛拟程序空間,位址空間範圍0~3G。

3.4 碎片問題

  • 外部碎片:未被配置設定的記憶體,由于太多零碎的不連續小記憶體,無法滿足目前較大記憶體的申請要求;
  • 原因:頻繁的配置設定與回收實體頁導緻大量的小塊記憶體夾雜在已配置設定頁面中間;
  • 解決方案:夥伴算法有所改善
  • 内部碎片:已經配置設定的記憶體,卻不能被利用的記憶體空間;
  • 緣由:所有記憶體配置設定必須起始可被4、8或16(體系結構決定)整除的位址或者MMU分頁機制限制;
  • 解決方案:slab配置設定器有所改善
  • 執行個體:請求一個11Byte的記憶體塊,系統可能會配置設定12Byte、16Byte等稍大一些的位元組,這些多餘空間就産生碎片

繼續閱讀