十天學Linux內核之第三天---內存管理方式

原文: 十天學Linux內核之第三天---內存管理方式

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

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

       32位系統:頁大小4KBlinux

       64位系統:頁大小8KB  緩存

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

 

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

 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必須處理以下兩種硬件存在缺陷而引發的內存尋址問題:post

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

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

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

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

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

相關文章
相關標籤/搜索