Linux內存管理(2)

進程內存的分配與回收

創建進程fork()、程序載入execve()、映射文件mmap()、動態內存分配malloc()/brk()等進程相關操作都需要分配內存給進程。不過這時進程申請和獲得的還不是實際內存,而是虛擬內存,準確的說是“內存區域”。進程對內存區域的分配最終多會歸結到do_mmap()函數上來(brk調用被單獨以系統調用實現,不用do_mmap()),

內核使用do_mmap()函數創建一個新的線性地址區間。但是說該函數創建了一個新VMA並不非常準確,因爲如果創建的地址區間和一個已經存在的地址區間相鄰,並且它們具有相同的訪問權限的話,那麼兩個區間將合併爲一個。如果不能合併,那麼就確實需要創建一個新的VMA了。但無論哪種情況, do_mmap()函數都會將一個地址區間加入到進程的地址空間中--無論是擴展已存在的內存區域還是創建一個新的區域。

同樣釋放一個內存區域使用函數do_ummap(),它會銷燬對應的內存區域。

如何由虛變實!

    從上面已經看到進程所能直接操作的地址都爲虛擬地址。當進程需要內存時,從內核獲得的僅僅時虛擬的內存區域,而不是實際的物理地址,進程並沒有獲得物理內存(物理頁框——頁的概念請大家參與硬件基礎一章),獲得的僅僅是對一個新的線性地址區間的使用權。實際的物理內存只有當進程真的去訪問新獲取的虛擬地址時,纔會由“請頁機制”產生“缺頁”異常,從而進入分配實際頁框的例程。

該異常是虛擬內存機制賴以存在的基本保證——它會告訴內核去真正爲進程分配物理頁,並建立對應的頁表,這之後虛擬地址才實實在在映射到了系統物理內存上。(當然如果頁被換出到磁盤,也會產生缺頁異常,不過這時不用再建立頁表了)

這種請頁機制把頁框的分配推遲到不能再推遲爲止,並不急於把所有的事情都一次做完(這中思想由點想涉及模式中的代理模式(proxy))。之所以能這麼做是利用了內存訪問的“局部性原理”,請頁帶來的好處是節約了空閒內存,提高了系統吞吐。要想更清楚的瞭解請頁,可以看看《深入理解linux內核》一書。

這裏我們需要說明在內存區域結構上的nopage操作,該操作是當發生訪問的進程虛擬內存而發現並未真正分配頁框時,該方法變被調用來分配實際的物理頁,併爲該頁建立頁表項。在最後的例子中我們會演示如何使用該方法。

系統物理內存管理

雖然應用程序操作的對象是映射到物理內存之上的虛擬內存,但是處理器直接操作的卻是物理內存。所以當用程序訪問一個虛擬地址時,首先必須將虛擬地址轉化成物理地址,然後處理器才能解析地址訪問請求。地址的轉換工作需要通過查詢頁表才能完成,概括的講,地址轉換需要將虛擬地址分段,使每段虛地址都作爲一個索引指向頁表,而頁表項則指向下一級別的頁表或者指向最終的物理頁面。

每個進程都有自己的頁表。進程描述符號的pgd域指向的就是進程的頁全局目錄。席面我們借用《linux設備驅動程序》中的一幅圖大致看看進程地址空間到物理頁之間的轉換關係。

     上面的過程說起簡單,做起難呀。因爲在虛擬地址映射到頁之前必須先分配物理頁——也就是說必須先從內核獲取空閒頁,並建立頁表。下面我們介紹一下內核管理物理內存的機制。

物理內存管理(頁管理)

Linux內核管理物理內存是通過分頁機制實現的,它將整個內存劃分成無數4k(i386體系結構中)大小頁,從而分配和回收內存的基本單位便是內存頁了。利用分頁管理有助於靈活分配內存地址,因爲分配時不必要求必須有大塊的連續內存[1],系統可以東一頁、西一頁的湊出所需要的內存供進程使用。雖然如此,但是實際上系統使用內存還是傾向於分配連續的內存塊,因爲分配連續內存時,頁表不需要更改,因此能降低TLB的刷新率(頻繁刷新會很大增加訪問速度)。

鑑於上述需求,內核分配物理頁爲了儘量減少不連續情況,採用了“夥伴”關係來管理空閒頁框。夥伴關係分配算法大家不應陌生——幾乎所有操作系統書都會提到,我們不去詳細說它了,如果不明白可以參看有關資料。這裏只需要大家明白Linux中空閒頁面的組織和管理利用了夥伴關係,因此空閒頁面分配時也需要遵循夥伴關係,最小單位只能是2的冪倍頁面大小。內核中分配空閒頁框的基本函數是get_free_page/get_free_pages,它們或是分配單頁或是分配指定的頁框(248…512頁)。

注意:get_free_page是在內核中分配內存,不同於malloc在用戶空間中分配,malloc利用堆動態分配,實際上是調用brk()系統調用,該調用的作用是擴大或縮小進程堆空間(它會修改進程的brk域)。如果現有的內存區域不夠容納堆空間,則會以頁面大小的倍數位單位,擴張或收縮對應的內存區域,但brk值並非以頁面大小爲倍數修改,而是按實際請求修改。因此Malloc在用戶空間分配內存可以以字節爲單位分配,但內核在內部仍然會是以頁爲單位分配的。

   另外需要提及的是,物理頁在系統中由頁框結構struct paga描述,系統中所有的頁框存儲在數組mem_map[]中,可以通過該數組找到系統中的每一頁(空閒或非空閒)。而其中的空閒頁框則可由上述提到的以夥伴關係組織的空閒頁鏈表(free_area[MAX_ORDER]索引。

內核內存使用

Slab

    所謂尺有所長,寸有所短。以頁爲最小單位分配內存對於內核管理系統物理內存來說的確比較方便,但內核自身最常使用的內存卻往往是很小(遠遠小於一頁)的內存塊——比如存放文件描述符、進程描述符、虛擬內存區域描述符等行爲所需的內存都不足一頁。這些用來存放描述符的內存相比頁面而言,就好比是麪包屑與麪包。一個整頁中可以聚集多個這種這些小塊內存;而且這些小塊內存塊也和麪包屑一樣頻繁地生成/銷燬。

爲了滿足內核對這種小內存塊的需要,Linux系統採用了一種被稱爲slab分配器的技術。Slab分配器的實現相當複雜,但原理不難,其核心思想就是“存儲池[2]的運用。內存片段(小塊內存)被看作對象,當被使用完後,並不直接釋放而是被緩存到“存儲池”裏,留做下次使用,這無疑避免了頻繁創建與銷燬對象所帶來的額外負載。

Slab技術不但避免了內存內部分片(下文將解釋)帶來的不便(引入Slab分配器的主要目的是爲了減少對夥伴系統分配算法的調用次數——頻繁分配和回收必然會導致內存碎片——難以找到大塊連續的可用內存,而且可以很好利用硬件緩存提高訪問速度。

   Slab並非是脫離夥伴關係而獨立存在的一種內存分配方式,slab仍然是建立在頁面基礎之上,換句話說,Slab將頁面(來自於夥伴關係管理的空閒頁框鏈)撕碎成衆多小內存塊以供分配,slab中的對象分配和銷燬使用kmem_cache_allockmem_cache_free

Kmalloc

Slab分配器不僅僅只用來存放內核專用的結構體,它還被用來處理內核對小塊內存的請求。當然鑑於Slab分配器的特點,一般來說內核程序中對小於一頁的小塊內存的求情才通過Slab分配器提供的接口Kmalloc來完成(雖然它可分配32 131072字節的內存)。從內核內存分配角度講kmalloc可被看成是get_free_pages)的一個有效補充,內存分配粒度更靈活了。

有興趣的話可以到/proc/slabinfo中找到內核執行現場使用的各種slab信息統計,其中你會看到系統中所有slab的使用信息。從信息中可以看到系統中除了專用結構體使用的slab外,還存在大量爲Kmalloc而準備的Slab(其中有些爲dma準備的)。

1] 還有些情況必須要求內存連續,比如DMA傳輸中使用的內存,由於不涉及頁機制所以必須連續分配。

[2] 這種存儲池的思想在計算機科學裏廣泛應用,比如數據庫連接池、內存訪問池等等

內核非連續內存分配(Vmalloc

夥伴關係也好、slab技術也好,從內存管理理論角度而言目的基本是一致的,它們都是爲了防止“分片”,不過分片又分爲外部分片和內部分片之說,所謂內部分片是說系統爲了滿足一小段內存區(連續)的需要,不得不分配了一大區域連續內存給它,從而造成了空間浪費;外部分片是指系統雖有足夠的內存,但卻是分散的碎片,無法滿足對大塊“連續內存”的需求。無論何種分片都是系統有效利用內存的障礙。slab分配器使得含與一個頁面內衆多小塊內存可獨立被分配使用,避免了內部分片,節約了空閒內存。夥伴關係把內存塊按大小分組管理,一定程度上減輕了外部分片的危害,因爲頁框分配不在盲目,而是按照大小依次有序進行,不過夥伴關係只是減輕了外部分片,但並未徹底消除。你自己筆畫一下多次分配頁框後,空閒內存的剩餘情況吧。

所以避免外部分片的最終思路還是落到了如何利用不連續的內存塊組合成“看起來很大的內存塊”——這裏的情況很類似於用戶空間分配虛擬內存,內存邏輯上連續,其實影射到並不一定連續的物理內存上。Linux內核借用了這個技術,允許內核程序在內核地址空間中分配虛擬地址,同樣也利用頁表(內核頁表)將虛擬地址影射到分散的內存頁上。以此完美地解決了內核內存使用中的外部分片問題。內核提供vmalloc函數分配內核虛擬內存,該函數不同於kmalloc,它可以分配較Kmalloc大得多的內存空間(可遠大於128K,但必須是頁大小的倍數),但相比Kmalloc來說Vmalloc需要對內核虛擬地址進行重影射,必須更新內核頁表,因此分配效率上要低一些(用空間換時間)

與用戶進程相似內核也有一個名爲init_mmmm_strcut結構來描述內核地址空間,其中頁表項pdg=swapper_pg_dir包含了系統內核空間(3G-4G)的映射關係。因此vmalloc分配內核虛擬地址必須更新內核頁表,而kmallocget_free_page由於分配的連續內存,所以不需要更新內核頁表。

vmalloc分配的內核虛擬內存與kmalloc/get_free_page分配的內核虛擬內存位於不同的區間,不會重疊。因爲內核虛擬空間被分區管理,各司其職。進程空間地址分佈從0到3G(其實是到PAGE_OFFSET,0x86中它等於0xC0000000),從3Gvmalloc_start這段地址是物理內存映射區域(該區域中包含了內核鏡像、物理頁框表mem_map等等)比如我使用的系統內存是64M(可以用free看到),那麼(3G——3G+64M)這片內存就應該映射物理內存,而vmalloc_start位置應在3G+64M附近(說附近因爲是在物理內存映射區與vmalloc_start期間還回存在一個8M大小的gap來防止躍界),vmalloc_end的位置接近4G(說接近是因爲最後位置系統會保留一片128k大小的區域用於專用頁面映射,還由可能會由高端內存映射區,這些都是細節,這裏我們不做糾纏)

上圖是內存分佈的模糊輪廓

  get_free_pageKmalloc函數所分配的連續內存都陷於物理映射區域,所以它們返回的內核虛擬地址和實際物理地址僅僅是相差一個偏移量(PAGE_OFFSET),你可以很方便的將其轉化爲物理內存地址,同時內核也提供了virt_to_phys()函數將內核虛擬空間中的物理影射區地址轉化爲物理地址。要知道,物理內存映射區中的地址與內核頁表是有序對應,系統中的每個物理頁框都可以找到它對應的內核虛擬地址(在物理內存映射區中的)。

vmalloc分配的地址則限於vmalloc_startvmalloc_end之間。每一塊vmalloc分配的內核虛擬內存都對應一個vm_struct結構體(可別和vm_area_struct搞混,那可是進程虛擬內存區域的結構),不同的內核虛擬地址被4k打大小空閒區的間隔,以防止越界——見下圖)。與進程虛擬地址的特性一樣,這些虛擬地址可與物理內存沒有簡單的位移關係,必須通過內核頁表纔可轉換爲物理地址或物理頁。它們有可能尚未被映射,在發生缺頁時才真正分配物理頁框。

這裏給出一個小程序幫助大家認請上面幾種分配函數所對應的區域。

#include<linux/module.h>

#include<linux/slab.h>

#include<linux/vmalloc.h>

unsigned char *pagemem;

unsigned char *kmallocmem;

unsigned char *vmallocmem;

int init_module(void)

{

pagemem = get_free_page(0);

printk("<1>pagemem=%s",pagemem);

kmallocmem = kmalloc(100,0);

printk("<1>kmallocmem=%s",kmallocmem);

vmallocmem = vmalloc(1000000);

printk("<1>vmallocmem=%s",vmallocmem);

}

void cleanup_module(void)

{

free_page(pagemem);

kfree(kmallocmem);

vfree(vmallocmem);

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章