24小時學通Linux內核之內存管理方式


 

 

24小時學通Linux內核之內存管理方式

  昨天分析的進程的代碼讓自己還在頭昏目眩,腦子中這幾天都是關於Linux內核的,對於自己出現的一些問題我會繼續改正,希望和大家好好分享,共同進步。今天將會講訴Linux如何追蹤和管理用戶空間進程的可用內存和內核的可用內存,還會講到內核對內存分類的方式以及如何決定分配和釋放內存,內存管理是應用程序通過軟硬件協助來訪問內存的一種方式,這里我們主要是介紹操作系統正常運行對內存的管理。插個話題,剛才和姐姐聊天,她快結婚了,說起了自己的初戀,可能是一句很搞笑的話,防火防盜防初戀,,嘎嘎,這個好像是的吧,盡管大三了,有了新的女友,也特別喜歡她,把她當作未來的伴侶,但是那個時候確實很美好,難怪哦姐姐聊起這些,這里祝福姐姐,心情好相信接下來的博客講解一定可以狀態大好,和大家一起好好分享。

  在深入了解內存管理的實現之前一些有關內存管理的高級概念我們有必要了解一下,先說虛擬內存,怎么產生的呢?現在操作系統要求能夠使多個程序共享操作系統資源,並且還要求內存對程序的開發透明,有了虛擬內存之后,依靠透明的使用磁盤空間,就可以使系統物理內存大得多,而且使得多個程序共享更加容易方便。然后再說說虛擬地址,當一個程序從內存中存取數據時,會使用地址來指出需要訪問的內存地址,這就是虛擬地址,它組成了進程虛擬地址空間,其大小取決於體系結構的字寬。內存管理在操作系統中負責維護虛擬地址和物理地址之間的關系並且實現分頁機制(將頁從內存到磁盤之間調入調出的機制), 內核把物理頁作為內存管理的基本單位;內存管理單元(MMU)把虛擬地址轉換為物理地址,通常以頁為單位進行處理。如:

       32位系統:頁大小4KB

       64位系統:頁大小8KB  

  上述這些數據都會在頁面載入內存時候得以更新,下面來看看內核是如何利用頁來實現內存管理的。

 

  作為內存管理的基本單元,頁有許多屬性需要維護,下面的結構體描述了頁描述符的各種域以及內存管理是如何使用它們的,在include/linux/mm.h中可以查看到定義。

 1 struct page
 2 {
 3         unsigned long flags;  //flags用來存放頁的狀態,每一位代表一種狀態                                                      
 4         atomic_t count;        //count記錄了該頁被引用了多少次        
 5         unsigned int mapcount;       
 6         unsigned long private;        
 7         struct address_space *mapping;  //mapping指向與該頁相關的address_space對象
 8         pgoff_t index;                  
 9         struct list_head lru;  //存放的next和prev指針,指向最近使用(LRU)鏈表中的相應結點
10         union
11        {
12             struct pte_chain;
13             pte_addr_t;
14         }         
15          void *virtual;     //virtual是頁的虛擬地址,它就是頁在虛擬內存中的地址             
16 };

  要理解的一點是page結構與物理頁相關,而並非與虛擬頁相關。因此,該結構對頁的描述是短暫的。內核僅僅用這個結構來描述當前時刻在相關的物理頁中存放的東西。這種數據結構的目的在於描述物理內存本身,而不是描述包含在其中的數據。

 

   在linux中,內核也不是對所有的也都一視同仁,內核而是把頁分為不同的區,使用區來對具有相似特性的頁進行分組。Linux必須處理如下兩種硬件存在缺陷而引起的內存尋址問題:

  • 一些硬件只能用某些特定的內存地址來執行DMA
  • 一些體系結構其內存的物理尋址范圍比虛擬尋址范圍大的多。這樣,就有一些內存不能永久地映射在內核空間上。

  為了解決這些制約條件,Linux系統使用了三種區:

  • ZONE_DMA:這個區包含的頁用來執行DMA操作。
  • ZONE_NOMAL:這個區包含的都是能正常映射的頁(用於映射非DMA)
  • ZONE_HIGHEM:這個區包"高端內存",其中的頁能不永久地映射到內核地址空間。

  每個內存區都有一個對應的描述符號zone,zone結構被定義在/linux/mmzone.h中,接下來瀏覽一下該結構的一些域:

struct zone {
         spinlock_t              lock;  //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;   //內存的所有頁鎖住時,此值置1
         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;  
};

 

  內核提供了一種請求內層的底層機制,並提供了對它進行訪問的幾個接口。所有這些接口都是以頁為單位進行操作的頁面是物理內存存儲頁的基本單元,只要有進程申請內存,內核便會請求一個頁面給它,同理,如果頁面不再使用,那么內核將其釋放,以便其他進程可以使用,下面介紹一下這些函數。

  alloc_page() 用於請求單頁,不需要描述請求內存大小的order參數

  alloc_pages() 可以請求頁面組

 
         
#define alloc_pages(gfp_mask,order)   
  alloc_pages_node(numa_node_id(),gfp_mask,order) #define alloc_page(gfp_mask)   alloc_pages_node(numa_node_id(),gfp_mask,0)

  __get_free_page() 請求單頁面操作的簡化版本

include/linux/gfp.h
    #define __get_dma_pages(gfp_mask,order) \
    __get_free_pages((gfp_mask)|GFP_DMA,(order))

  __get_dma_pages() 用於從ZONE_DMA區請求頁面

include/linux/gfp.h
    #define __get_dma_pages(gfp_mask,order) \
    __get_free_pages((gfp_mask)|GFP_DMA,(order))

   當你不再需要頁時可以用下列函數釋放它們,只是提醒:僅能釋放屬於你的頁,否則可能導致系統崩潰。內核是完全信任自己的,如果有非法操作,內核會開心的把自己掛起來,停止運行。

extern void __free_pages(struct page *page, unsigned int order);

extern void free_pages(unsigned long addr, unsigned int order);

  上面提到都是以頁為單位的分配方式,那么對於常用的以字節為單位的分配來說,內核通供的函數是kmalloc(),和mallloc很像吧,其實還真是這樣,只不過多了一個flags參數。用它可以獲得以字節為單位的一塊內核內存。

   kmalloc

kmalloc()函數與用戶空間malloc一組函數類似,獲得以字節為單位的一塊內核內存。

void *kmalloc(size_t size, gfp_t flags)

void kfree(const void *objp)

 

分配內存物理上連續。

gfp_t標志:表明分配內存的方式。如:

GFP_ATOMIC:分配內存優先級高,不會睡眠

GFP_KERNEL:常用的方式,可能會阻塞。

 

   vmalloc    

void *vmalloc(unsigned long size)

void vfree(const void *addr)

vmalloc()與kmalloc方式類似,vmalloc分配的內存虛擬地址是連續的,而物理地址則無需連續,與用戶空間分配函數一致。

vmalloc通過分配非連續的物理內存塊,在修正頁表,把內存映射到邏輯地址空間的連續區域中,虛擬地址是連續的。 是否必須要連續的物理地址和具體使用場景有關。在不理解虛擬地址的硬件設備中,內存區都必須是連續的。通過建立頁表轉換成虛擬地址空間上連續,肯定存在一些消耗,帶來性能上影響。所以通常內核使用kmalloc來申請內存,在需要大塊內存時使用vmalloc來分配。

 

  進程往往會以字節為單位請求小塊內存,為了滿足這種小內存的請求,內核特別實現了Slab分配器,Slab分配器使用三個主要結構維護對象信息,分別如下:

kmem_cache的緩存描述符

cache_sizes的通用緩存描述符

slab的slab描述符

  在最高層是 cache_chain,這是一個 slab 緩存的鏈接列表。可以用來查找最適合所需要的分配大小的緩存。cache_chain 的每個元素都是一個 kmem_cache 結構的引用。一個kmem_cache中的所有object大小都相同。這里我們首先看看緩存描述符中各個域以及他們的含義。

 

struct kmem_cache_s{

    struct kmen_list3 lists;  //lists域中包含三個鏈表頭,每個鏈表頭均對應了slab所處的三種狀態(滿,未滿,空閑)之一,

    unsigned int objsize;  //objsize域中持有緩存中對象的大小
    unsigned int flags;  //flags持有標志掩碼,其描述了緩存固有特性
    unsigned int num;  //num域中持有緩存中每個slab所包含的對象數目

    unsigned int gfporder;  //緩存中每個slab所占連續頁面數的冪,該值默認0

    size_t color;   

    unsigned int color_off;
    unsigned int color_next;
    kmem_cache_t *slabp_cache;  //可存儲在自身緩存中也可以存在外部其他緩存中
    unsigned int dflags;

    void (*ctor) (void *,kmem_cache_t*,unsigened long);

    void (*dtor)(void*,kmem_cache_t *,unsigend long);

    const char *name;  //name持有易於理解的名稱
    struct list_head next;  //next域指向下個單向緩存描述符鏈表上的緩存描述符

};

 

  如我們所講,作為通用目的的緩存大小都是被定義好的,且成對出現,一個為從DMA內存分配對象,另一個從普通內存中分配,結構cache_sizes包含了有關通用緩存大小的所有信息。代碼解釋如下:

struct cache_sizes{
    size_t cs_size;  //持有該緩存中容納的內存對象大小
    kmem_cache_t *cs_cachep;  //持有指向普通內存緩存描述符飛指針
    kmem_cache_t *cs_dmacachep;  //持有指向DMA內存緩存描述符的指針,分配自ZONE_DMA
};

  最后介紹一下Slab狀態和描述符域的值,如下表(N=slab中的對象數目,X=某一變量的正數)

  Free Partial Full
Slab->inuse 0 X N
Slab->free 0 X N

 

 

   

 

  現在我們再內核運行的整個生命周期范圍內觀察緩存和slab分配器第如何交互的,內核需要某些特殊結構以支持進程的內存請求和動態可加載模塊來創建特定緩存,內核函數 kmem_cache_create 用來創建一個新緩存。這通常是在內核初始化時執行的,或者在首次加載內核模塊時執行.

struct kmem_cache *kmem_cache_create (

  const char *name,  //定義了緩存名稱

  size_t size,  //指定了為這個緩存創建的對象的大小

  size_t align,  //定義了每個對象必需的對齊。

  unsigned long flags,  //指定了為緩存啟用的選項

  void (*ctor)(void *))  //定義了一個可選的對象構造器和析構器。構造器和析構器是用戶提供的回調函數。當從緩存中分配新對象時,可以通過構造器進行初始化。

  當緩存被創建之后,其中的slab都是空的,事實上slab在請求對象前都不會分配,當我們在創建slab時,不僅僅分配和初始化其描述符,而且還需要和伙伴系統交互請求頁面。從一個命名的緩存中分配一個對象,可以使用 kmem_cache_alloc 函數,這個函數從緩存中返回一個對象。注意如果緩存目前為空,那么這個函數就會調用 cache_alloc_refill 向緩存中增加內存。

void kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
//cachep是需要擴充的緩存描述符
//flags這些標志將用於創建slab

  緩存和slab都可被銷毀,其步驟與創建相逆,但是對齊問題在銷毀緩存時候不需要關心,只需要刪除緩存描述符和釋放內存即可,其步驟有三如下:

  • 從緩存鏈表中刪除緩存
  • 刪除slab描述符
  • 刪除緩存描述符
mm/slab.c
int kmem_cache_destroy(kmem_cache_t *cachep)
{
    int i;
    
    if(!cache || in_interrupt())
    BUG();  //完成健全性檢查

    down(&cache_chain_sem);

    list_del(&cachep->next);
    up(&cache_chain_sem);  //獲得cache_chain信號量從緩存中刪除指定緩存,釋放cache_chain信號量

    if(_cache_shrink(cachep)){
        slab_error(cachep,"Can't free all objects");
        down(&cache_chain_sem);
        list_add(&cache->next,&cache_chain);
        up(&cache_chain_sem);
        return 1;    //該段負責釋放為使用slab
    }
    ...
    kmem_cache_free(&cache_cache,cachep);  //釋放緩存描述符
    
    return 0;
}

 

  目前為止,我們討論完了slab分配器,那么實際的內存請求是怎么樣的呢,slab分配器是如何被調用的呢?這里我粗略講解一下。當內核必須獲得字節大小的內存塊時,就需要使用函數kmalloc(),它實際上會調用函數kmem_getpages完成實際分配,調用路徑如下:kmalloc()->__cache_alloc()->kmem_cache_grow()->kmem_getpages().kmalloc和get_free_page申請的內存位於物理內存映射區域,而且在物理上也是連續的,它們與真實的物理地址只有一個固定的偏移,因此存在較簡單的轉換關系,virt_to_phys()可以實現內核虛擬地址轉化為物理地址:

1 #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
2 extern inline unsigned long virt_to_phys(volatile void * address)
3 {
4 return __pa(address);
5 }

  那么內核是如何管理它們使用內存的呢,用戶進程一旦創建便要分配一個虛擬地址空間,其地址范圍可以通過增加或者刪除線性地址間隔得以擴大或者縮減,在內核中進程地址空間的所有信息都被保存在mm_struct結構中,mm_struct和vm_area_struct結構之間的關系如下圖:

 

struct mm_struct {

  struct vm_area_struct * mmap; /* 指向虛擬區間(VMA)鏈表 */

  rb_root_t mm_rb; /*指向red_black樹*/

  struct vm_area_struct * mmap_cache; /* 指向最近找到的虛擬區間*/

  pgd_t * pgd; /*指向進程的頁目錄*/ 

  atomic_t mm_users; /* 用戶空間中的有多少用戶*/

  atomic_t mm_count; /* 對"struct mm_struct"有多少引用*/

  int map_count; /* 虛擬區間的個數*/

  struct rw_semaphore mmap_sem;

  spinlock_t page_table_lock; /* 保護任務頁表和 mm->rss */

  struct list_head mmlist; /*所有活動(active)mm的鏈表 */

  unsigned long start_code, end_code, start_data, end_data; /*start_code 代碼段起始地址,end_code 代碼段結束地址,start_data 數據段起始地址, start_end 數據段結束地址*/

  unsigned long start_brk, brk, start_stack; /*start_brk 和brk記錄有關堆的信息, start_brk是用戶虛擬地址空間初始化時,堆的結束地址, brk 是當前堆的結束地址, start_stack 是棧的起始地址*/

  unsigned long arg_start, arg_end, env_start, env_end; /*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_address;
....
};

 

  最后簡單講一下進程映象分布於線性地址空間的相關重點,當用戶程序被載入內存之后,便被賦予 了自己的線性空間,並且被映射到進程地址空間,下面需要注意。

永久映射:可能會阻塞

  映射一個給定的page結構到內核地址空間:

  void *kmap(struct page *page)

  解除映射:

  void kunmap(struct page *page) 

臨時映射:不會阻塞     

void *kmap_atomic(struct page *page)

 

  小結

  這次講了內存管理的大部分內容,介紹了頁是如何在內核中被跟蹤,然后討論了內存區,之后討論了小於一頁的小塊內存分配,即slab分配器管理。在內核管理結構和眾多代碼分析完了之后,繼續討論了用戶空間進程管理特殊方式,最后簡單介紹了進程映象分布於線性地址空間的相關重點。里面肯定有些內容比較散亂,代碼有補全的狀況,希望大家能夠多家批評改正,一起討論,今天發生了很多事情,到現在才更新完,晚上還有些時間,還需要好好理解體會,共勉。

 

  版權所有,轉載請注明轉載地址:http://www.cnblogs.com/lihuidashen/p/4242645.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM