linux核心分析筆記—-記憶體管理

來源:互聯網
上載者:User

      記憶體管理,不用多說,言簡意賅。在核心裡分配記憶體還真不是件容易的事情,根本上是因為核心不能想使用者空間那樣奢侈的使用記憶體。

      先來說說記憶體管理。核心把物理頁作為記憶體管理的基本單位。儘管處理器的最小可定址單位通常是字,但是,記憶體管理單元MMU通常以頁為單位進行處理。因此,從虛擬記憶體的交代來看,頁就是最小單位。核心用struct  page(linux/mm.h)結構表示系統中的每個物理頁:

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;   union{ struct pte_chain;pte_addr_t; }                  void *virtual;                  };

      flag用來存放頁的狀態,每一位代表一種狀態,所以至少可以同時表示出32中不同的狀態,這些狀態定義在linux/page-flags.h中。count記錄了該頁被引用了多少次。mapping指向與該頁相關的address_space對象。virtual是頁的虛擬位址,它就是頁在虛擬記憶體中的地址。要理解的一點是page結構與物理頁相關,而並非與虛擬頁相關。因此,該結構對頁的描述是短暫的。核心僅僅用這個結構來描述當前時刻在相關的物理頁中存放的東西。這種資料結構的目的在於描述實體記憶體本身,而不是描述包含在其中的資料。

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

1.一些硬體只能用某些特定的記憶體位址來執行DMA
2.一些體繫結構其記憶體的物理定址範圍比虛擬定址範圍大的多。這樣,就有一些記憶體不能永久地映射在核心空間上。
為瞭解決這些制約條件,Linux使用了三種區:
1.ZONE_DMA:這個區包含的頁用來執行DMA操作。
2.ZONE_NOMAL:這個區包含的都是能正常映射的頁。
3.ZONE_HIGHEM:這個區包"高端記憶體",其中的頁能不永久地映射到核心地址空間。

      區的實際使用與體繫結構是相關的。linux 把系統的頁劃分區,形成不同的記憶體池,這樣就可以根據用途進行分配了。需要說明的是,區的劃分沒有任何物理意義,只不過是核心為了管理頁而採取的一種邏輯上的分組。儘管某些分配可能需要從特定的區中獲得頁,但這並不是說,某種用途的記憶體一定要從對應的區來擷取,如果這種可供分配的資源不夠用了,核心就會佔用其他可用去的記憶體。下表給出每個區及其在X86上所佔的列表:

    

      每個區都用定義在linux/mmzone.h中的struct zone表示,如下:

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域是一個自旋鎖,這個域只保護結構,而不是保護駐留在這個區中的所有頁。沒有特定的鎖來保護單個頁。free_pages域是這個區中空閑頁的個數。核心儘可能的保護有pages_min個空閑頁可用。name域是一個以NULL結束的字串,表示這個區的名字。核心啟動期間初始化這個值,其代碼位於mm/page_alloc.h中,三個區的名字分別是"DMA","Normal","HighMem"。

核心提供了一種請求內層的底層機制,並提供了對它進行訪問的幾個介面。所有這些介面都是以頁為單位進行操作的。下表給出所有底層的頁分配方法:

    

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

     

      上面提到都是以頁為單位的分配方式,那麼對於常用的以位元組為單位的分配來說,核心通供的函數是kmalloc(),和mallloc很像吧,其實還真是這樣,只不過多了一個flags參數。用它可以獲得以位元組為單位的一塊核心記憶體。如果需要的是頁----尤其是在你的需求總量接近2的冪次方的時候----那麼,前面討論的頁分配介面可能是更好的選擇。

      接下來,注意的話,可能會發現無論是頁分配介面還是kmalloc都有一個分配器標誌(如GFP_KERNEL這樣的)。這些標誌可分為三類:行為修飾符,區修飾符及類型.下面就來討論個問題.

      1.行為修飾符(linux/gfp.h):表示核心應當如何分配所需的記憶體。在某些特定的情況下,只能使用某些特定的方法分配記憶體。可以同時使用這些標誌,用|連結。列表如下:

      

      2.區分配符:它只關心去應當從何處分配。通常,分配可以從任何區開始。不過,核心優先從ZONE_NORMAL開始,這樣可以確保其他區在需要時有足夠的空閑頁可以使用。區修飾符如下:

      

       不能給_get_free_pages()指定ZONE_HIGHMEM,因為這個函數返回都是邏輯地址,而不是page結構。這兩個函數分配的記憶體當前可能有可能還沒有映射到核心的虛擬位址空間,因此,也可能根本就沒有邏輯地址。只有alloc_pages()才能分配高端記憶體。實際上,大多數ZONE_NORMAL就已經足夠了。

      3.類型標誌:指定所需的行為和區描述符以完成特殊類型的處理。正因為這點,核心代碼趨向於使用正確的類型標誌,而不是一味地指定它可能需要用到的多個描述符。下面兩個表分別給出了類型標誌的列表和每個類型標誌與哪些修飾符相關聯:

                     

      上表中,左邊是類型標誌,右邊是每種類型標誌後隱含的修飾符列表。在編寫的大多數代碼中,用到的要麼是GFP_KERNEL,要麼是GFP_ATOMIC。下表是通常情形和所用標誌的列表,不管使用那種配置類型,你都必須進行檢查,並對錯誤進行處理:

    

      有了kmalloc,當然就有kfree()(linux/slab.h),釋放由kmalloc()分配出來的記憶體塊。如果想要釋放的記憶體不是由kmalloc()分配的,或者想要釋放的記憶體早就被釋放了,在這種情況下調用這個函數會導致嚴重的後果。特別說明kfree(NULL)是安全的。

      vmalloc()和kmalloc是一樣的作用,不同在於前者分配的記憶體虛擬位址是連續的,而物理地址則無需連續。這也是使用者空間分配函數的工作方式,如malloc().kmalloc()可以保證在物理地址上都是連續的(當然,虛擬位址當然也是連續的)。vmalloc()函數只確保頁在虛擬機器地址空間內是連續的。它通過分配非聯絡的實體記憶體塊,再“修正”頁表,把記憶體映射到邏輯地址空間的連續地區中,就能做到這點。但很顯然這樣會降低處理效能,因為核心不得不做“拼接”的工作。所以這也是為什麼不得已才使用vmalloc()的原因(比如獲得大記憶體時)。大多數情況下,只有硬體裝置需要得到物理地址連續的記憶體。硬體裝置存在於記憶體管理單元以外,它根本不懂什麼是虛擬位址。因此,硬體裝置用到的任何記憶體區都必須是物理上連續的塊,而不僅僅是虛地址連續的塊。最後需要說明的是,vmalloc()可能睡眠,不能從中斷上下文中進行調用,也不能從其他不允許阻塞的情況下進行調用。釋放時必須使用vfree().

      分配和釋放資料結構是所有核心中最普遍的操作之一。為了便於資料的頻繁分配和回收,常常會用到一個空間鏈表。它就相當於對象快取以便快速儲存頻繁使用的物件類型。在核心中,空閑鏈表面臨的主要問題之一是不能全域控制。當可用記憶體變得緊張的時候,核心無法通知每個空閑鏈表,讓其收縮緩衝的大小以便釋放一些記憶體來。實際上,核心根本不知道有這樣的空閑離岸邊。為了彌補這一缺陷,也為了是代碼更加穩固,linux核心提供了slab層(也就是所謂的slab分類器),slab分類器扮演了通用資料結構緩衝層的角色。slab分配器試圖在如下幾個原則中尋求一種平衡:

1.頻繁使用的資料結構也會頻繁分配和釋放,因此應當緩衝它們。
2.頻繁分配和回收必然會導致記憶體片段。為了避免這種情況,空閑鏈表的緩衝會連續地存放。因為已釋放的資料結構又會放回空閑鏈表,不會導致片段。
3.回收的對象可以立即投入下一次分配,因此,對於頻繁的分配和釋放,空閑鏈表能夠提高其效能。
4.如果讓部分緩衝專屬於單個處理器,那麼,分配和釋放就可以在不加SMP鎖的情況下進行。
5.對存放的對象進行著色,以防止多個對象映射到相同的快取行。

      slab層把不同的對象劃分為所謂的快取組,其中每個快取都存放不同類型的對象,每種物件類型對應一個快取。kmalloc()介面建立在slab層上,使用了一組通用快取。這些緩衝又被分為slabs,slab由一個或多個物理上連續的頁組成,一般情況下,slab也就僅僅由一頁組成。每個快取可以由多個slab組成。每個slab都包含一些對象成員,這裡的對象指的是被緩衝的資料結構,每個slab處於三種狀態之一:滿,部分滿,空。當核心的某一部分需要一個新的對象時,先從部分滿的slab中進行分配。如果沒有部分滿的slab,就從空的slab中進行分配。如果沒有空的slab,就要建立一個slab了。給出快取,slab及對象之間的關係:

     

      中的每個cache由kmem_cache_s結構表示,這個結構包含三個鏈表slabs_full,slab_partial和slabs_empty,均存放在kmem_list3結構內,這些鏈表包含快取中的所有slab,slab描述符struct slab:

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

      slab描述符要麼在slab之外另行分配,要麼就在slab自身最開始的地方。如果slab很小或者slab核心有足夠的空間容納slab描述符,那麼描述符就存放在slab裡面.slab分配器建立新的slab是通過__get_free_pages()低級記憶體 Clerk進行的:

static inline void * kmem_getpages(kmem_cache_t *cachep, unsigned long flags){        void *addr;        flags |= cachep->gfpflags;        addr = (void*)__get_free_pages(flags, cachep->gfporder);        return addr;}

      上面的是一個描述原理的簡化版。接著,調用kmem_freepages()釋放記憶體,而對給定的快取頁,kmem_freepages()最終調用的是free_pages().當然,slab層的關鍵就是避免頻繁分配和釋放頁。由此可知,slab頁只有當給定的快取中既沒有部分滿也沒有空的slab時候才會調用頁分配函數。而只有在下列情況下才會調用釋放函數:當可用記憶體變得緊缺時,系統試圖釋放出更多記憶體以供使用,或者當快取顯式地被銷毀時。slab層的管理是在每個快取的基礎上,通過提供個整個核心一個簡單的介面來完成的。通過介面就可以建立和銷毀新的快取,並在快取內分配和釋放對象。快取及slab的複雜管理完全通過slab層的內部機制來處理。當建立一個快取後,slab層所起的作用就像一個專用的分配器,可以為具體的物件類型進行分配。一個新的快取是通過一下介面進行建立的:

kmem_cache_t * kmem_cache_create(const char *name, size_t size,size_t align, unsigned long flags,
                                 void (*ctor)(void*, kmem_cache_t *, unsigned long),                                 void (*dtor)(void*, kmem_cache_t *, unsigned long));
      有關這個函數的說明,我就省略了,需要的網上一大堆。這個函數成功時會返回一個執行所建立快取的指標,否則,返回空。這個函數由於會睡眠,因此不能在中斷上下文中使用。要銷毀一個快取,調用:int kmem_cache_destroy(kmem_cache_t *cachep),同樣,也是不能在中斷上下文中使用。調用該函數之前必須確儲存在以下兩個條件:
1.快取中的所有slab都必須為空白。
2.在調用kmem_cache_destory()期間不再訪問這個快取,調用者必須確保這種同步。

      建立了快取以後,就可以通過下列函數從中擷取對象:void * kmem_cache_alloc(kmem_cache_t *cachep, int flags)。該函數從快取cachep中返回一個指向對象的指標。如果快取的所有slab中都沒有閒置對象,那麼slab層必須通過kmem_getpages()擷取新的頁,flags的值傳遞給__get_free_pages().最後,釋放一個對象,並把它返回給原來的slab,可以使用下面的函數:

void kmem_cache_free(kmem_cache_t *cachep,void *objp)

      這樣就能把cachep中的對象objp標記為空白閑了,關於slab分配器的使用執行個體,參考資料上有,我就不說了。相比較以前的使用者空間棧而言,核心棧是非常小的。每個進程都有自己的核心棧進程在核心執行期間的整個調用鏈必須放在自己的核心棧上。中斷處理常式也使用被它們打斷的進程的堆棧。這就意味著,在最惡劣的情況下,8kB的核心棧可能會由多個函數的嵌套調用鏈和幾個中斷處理常式來共用。顯然,深度的嵌套會導致溢出。

      根據定義,在高端記憶體中的頁不能永久地映射到核心地址空間上。因此,通過alloc_pages()函數以__GFP_HIGHMEM標誌獲得的頁不可能有邏輯地址。一旦這些頁被分配,就必須映射到核心的邏輯地址空間上。要映射一個給定的page結構到核心地址空間,可以使用void *kmap(struct page *page) 這個函數在高端記憶體或低端記憶體上都能用。如果page結構對應的是低端記憶體中的一頁,函數只會單純地返回該頁的虛擬位址,如果頁位於高端記憶體,則會建立一個永久映射,在返回地址。這個函數可以睡眠,所以kmap()只能用在進程上下文中。當不再需要記憶體映射的時候,就用下列函數進行解除映射:

void kunmem(struct page* page)

      當必須建立一個映射而當前的上下文又不能睡眠時,核心提供了臨時睡眠(也就是原子睡眠)。只要有一組保留的永久映射,它們就可以臨時持有新建立的一個映射。核心可以原子地把高端記憶體中的一個頁映射到某個保留的映射中。因此,臨時映射可以用在不能睡眠的地方。建立臨時映射:void *kmap_atomic(struct page *page,enum km_type type).參數type是下列枚舉類型之一,描述了臨時映射的目的,如下:

    

      這個函數不會阻塞,它也禁止核心搶佔,通過函數void *kunmap_atomic(void *kvaddr,enum km_type type).這個函數還是不會映射。

      最後,我們總結一下,說說分配函數的選擇吧,總結如下:

1.如果需要連續的物理頁,就可以使用某個低級頁分配器或kmalloc().
2.如果想從高端記憶體進行分配,使用alloc_pages().
3.如果不需要物理上連續的頁,而僅僅是虛擬位址上連續的頁,那麼就是用vmalloc
4.如果要建立和銷毀很多大的資料結構,那麼考慮建立slab快取。

      好了,有關記憶體管理的也說完了,其實也不算我說,有很多都是參考書上資料的。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.