Linux核心之 記憶體管理
前面幾篇介紹了程序的一些知識,從這篇開始介紹記憶體、檔案、IO等知識,發現更不好寫哈哈。但還是有必要記錄下自己的所學所思。供後續翻閱,同時寫作也是一個鞏固的過程。
這些知識以前有文件涉及過,但是角度不同,這個系列站的角度更底層,基本都是從Linux核心出發,會更深入。所以當你都讀完,然後再次審視這些功能的實現和設計時,我相信你會有種豁然開朗的感覺。
1、頁
核心把物理頁作為記憶體管理的基本單元。
儘管處理器的最小處理單位是字(或者位元組),但是MMU(記憶體管理單元,管理記憶體並把虛擬地址轉換為實體地址的硬體)通常以頁為單位進行處理。所以從虛擬記憶體看,頁也是最小單元。
體系不同,支援的頁大小不同。大多數32位體系結構支援4KB的頁,而64位體系結構一般會支援8KB的頁。
核心用struct page結構體表示系統中的每個頁,包含很多項比如頁的狀態(有沒有髒,有沒有被鎖定)、引用計數(-1表示沒有使用)等等。
page結構和物理頁相關,和虛擬記憶體無關。所以它的描述是短暫的,僅僅記錄當前的使用狀況,當然也不會描述其中的資料。
核心用這個結構來管理系統中所有的頁,所以核心知道哪些頁是空閒的,如果在使用中擁有者又是誰。
這個擁有者有四種:使用者空間程序、動態分配記憶體的核心資料、靜態核心程式碼以及頁快取記憶體。
2、區
有些頁是有特定用途的。比如記憶體中有些頁是專門用於DMA的。
核心使用區的概念將具有相似特性的頁進行分組。區是一種邏輯上的分組的概念,而沒有物理上的意義。
區的實際使用和分佈是與體系結構相關的。在x86體系結構中主要分為3個區:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA區中的頁用來進行DMA(直接記憶體訪問)時使用。
ZONE_HIGHMEM是高階記憶體,其中的頁不能永久的對映到核心地址空間,也就是說,沒有虛擬地址。
剩餘的記憶體就屬於ZONE_NORMAL區,叫低端記憶體。
不是所有體系都定義全部區,有些體系結構,比如x86-64可以對映和處理64位的記憶體空間,所以它沒有ZONE_HIGHMEM區,所有的實體記憶體都都處於ZONE_DMA和ZONE_NORMAL區。
每個區都用結構體struct zone表示。
3、介面
獲得頁
獲得頁使用的介面是alloc_pages函式與__get_free_page函式。後者也是呼叫了前者,只不過在獲得了struct page結構體後使用page_address函式獲得了虛擬地址。
我們在使用這些介面獲取頁的時候可能會面對一個問題,我們獲得的這些頁若是給使用者態用,雖然這些頁中的資料都是隨機產生的垃圾資料,不過,雖然概率很低,但是也有可能會包含某些敏感資訊。所以,更謹慎些,我們可以將獲得的頁都填充為0。這會用到get_zeroed_page函式。而這個函式又用到了__get_free_pages函式。
所以這三個函式最終都是使用了alloc_pages函式。
釋放頁
當我們不再需要某些頁時可以使用下面的函式釋放它們:
__free_pages(struct page *page, unsigned int order)
free_pages(unsigned long addr, unsigned int order)
free_page(unsigned long addr)
以上這些介面都是以頁為單位進行記憶體分配與釋放的。
kmalloc與vmalloc
在實際中核心需要的記憶體不一定是整個頁,可能只是以位元組為單位的一片區域。這兩個函式就是實現這樣的目的。
不同之處在於,kmalloc分配的是虛擬地址連續,實體地址也連續的一片區域,vmalloc分配的是虛擬地址連續,實體地址不一定連續的一片區域。
對應的釋放記憶體的函式是kfree與vfree。
4、slab層
以頁為最小單位分配記憶體對於核心管理系統中的實體記憶體來說的確比較方便,但核心自身最常使用的記憶體卻往往是很小的記憶體塊——比如存放檔案描述符、程序描述符、虛擬記憶體區域描述符等行為所需的記憶體都遠不及一頁,一個整頁中可以聚集多個這些小塊記憶體。
為了滿足核心對這種小記憶體塊的需要,Linux系統採用了一種被稱為slab分配器(也稱作slab層)的技術。slab分配器的實現相當複雜,但原理不難,其核心思想就是“儲存池”的運用。記憶體片段(小塊記憶體)被看作物件,當被使用完後,並不直接釋放而是被快取到“儲存池”裡,留做下次使用,這無疑避免了頻繁建立與銷燬物件所帶來的額外負載。
slab分配器扮演了通用資料結構快取層的角色。
slab層把不同的物件劃分為所謂快取記憶體組,其中每個快取記憶體組都存放不同型別的物件,每種物件對應一個快取記憶體。
常見的快取記憶體組有:程序描述符(task_struct結構體),索引節點物件(struct inode),目錄項物件(struct dentry),通用頁物件等等。
這些快取記憶體又被劃分為slab。slab由一個或多個物理連續的頁組成,一般僅僅由一頁組成。每個快取記憶體可以由多個slab(頁)組成。
每個快取記憶體都使用struct kmem_cache結構表示,這個結構包含三個連結串列:slabs_full、slabs_partial和slabs_empty,均放在kmem_list3結構體內。這些連結串列的每個元素為slab描述符即struct slab結構體。
每個快取記憶體需要建立新的slab即新的頁,還是通過上面提到的__get_free_page()來實現的。通過最終呼叫free_pages()釋放記憶體頁。
一個快取記憶體的建立和銷燬使用kmem_cache_create與kmem_cache_destroy。
快取記憶體中的物件的分配和釋放使用kmem_cache_alloc與kmem_cache_free。
從上看出,slab層仍然是建立在頁的基礎之上,可以總結為slab層將 空閒頁 分解成 眾多相同長度的小塊記憶體 以供 同類型的資料結構 使用。
5、程序地址空間
以上我們講述了核心如何管理記憶體,核心記憶體分配機制包括了頁分配器和slab分配器。核心除了管理本身的記憶體外,也必須管理使用者空間中程序的記憶體。
我們稱這個記憶體為程序地址空間,也就是系統中每個使用者空間程序所看到的記憶體。Linux系統採用虛擬記憶體技術,所有程序以虛擬方式共享記憶體。Linux中主要採用分頁機制而不是分段機制。
5.1 地址空間佈局
程序記憶體區域可以包含各種記憶體物件,從下往上依次為:
(1)可執行檔案程式碼的記憶體對映,稱為程式碼段。只讀可執行。
(2)可執行檔案的已初始化全域性變數的記憶體對映,稱為資料段。後續都是可讀寫。
(3)包含未初始化的全域性變數,就是bass段的零頁的記憶體對映。
(4)堆區,動態記憶體分配區域;包括任何匿名的記憶體對映,比如malloc分配的記憶體。
(5)棧區,用於程序使用者空間棧的零頁記憶體對映,這裡不要和程序核心棧混淆,程序的核心棧獨立存在並由核心維護,因為核心管理著所有程序。所以核心管理著核心棧,核心棧管理著程序。
(6)其他可能存在的:記憶體對映檔案;共享記憶體段;C庫或者動態連結庫等共享庫的程式碼段、資料段和bss也會被載入程序的地址空間。
5.2 記憶體描述符
核心使用記憶體描述符mm_struct結構體表示程序的地址空間,該結構體包含了和程序地址空間有關的全部資訊。
1 struct mm_struct { 2 struct vm_area_struct *mmap; /* list of memory areas */ 3 struct rb_root mm_rb; /* red-black tree of VMAs */ 4 struct vm_area_struct *mmap_cache; /* last used memory area */ 5 unsigned long free_area_cache; /* 1st address space hole */ 6 pgd_t *pgd; /* page global directory */ 7 atomic_t mm_users; /* address space users */ 8 atomic_t mm_count; /* primary usage counter */ 9 int map_count; /* number of memory areas */ 10 struct rw_semaphore mmap_sem; /* memory area semaphore */ 11 spinlock_t page_table_lock; /* page table lock */ 12 struct list_head mmlist; /* list of all mm_structs */ 13 unsigned long start_code; /* start address of code */ 14 unsigned long end_code; /* final address of code */ 15 unsigned long start_data; /* start address of data */ 16 unsigned long end_data; /* final address of data */ 17 unsigned long start_brk; /* start address of heap */ 18 unsigned long brk; /* final address of heap */ 19 unsigned long start_stack; /* start address of stack */ 20 unsigned long arg_start; /* start of arguments */ 21 unsigned long arg_end; /* end of arguments */ 22 unsigned long env_start; /* start of environment */ 23 unsigned long env_end; /* end of environment */ 24 unsigned long rss; /* pages allocated */ 25 unsigned long total_vm; /* total number of pages */ 26 unsigned long locked_vm; /* number of locked pages */ 27 unsigned long def_flags; /* default access flags */ 28 unsigned long cpu_vm_mask; /* lazy TLB switch mask */ 29 unsigned long swap_address; /* last scanned address */ 30 unsigned dumpable:1; /* can this mm core dump? */ 31 int used_hugetlb; /* used hugetlb pages? */ 32 mm_context_t context; /* arch-specific data */ 33 int core_waiters; /* thread core dump waiters */ 34 struct completion *core_startup_done; /* core start completion */ 35 struct completion core_done; /* core end completion */ 36 rwlock_t ioctx_list_lock; /* AIO I/O list lock */ 37 struct kioctx *ioctx_list; /* AIO I/O list */ 38 struct kioctx default_kioctx; /* AIO default I/O context */ 39 };
mmap和mm_rb描述的物件是一樣的:該地址空間中全部記憶體區域(all memory areas)。
mmap是以連結串列的形式存放,而mm_rb是以紅黑樹存放,前者有利於遍歷所有資料,而後者有利於快速搜尋定位到某個地址。所有的mm_struct結構體都通過自身的mmlist域連線在一個雙向連結串列中,該連結串列的首元素是init_mm記憶體描述符,它代表init程序的地址空間。
再往下看,可以看到地址空間幾個區(堆疊)對應的變數的定義。
我們再回顧下在核心程序管理中,程序描述符task_struct是在核心空間中快取,也就是我們上面描述的slab層。
而task_struct中有個mm域指向的就是該程序使用的記憶體描述符,再通過current->mm便可以指向當前程序的記憶體描述符。fork函式利用copy_mm()函式就實現了複製父程序的記憶體描述符,而子程序中的mm_struct結構體實際是通過檔案kernel/fork.c中的allocate_mm()巨集從mm_cachep slab快取中分配得到的。通常,每個程序都有唯一的mm_struct結構體。
因為程序描述符和程序的記憶體描述符都是處於slab層,所以它們元素的分配和釋放都由slab分配器來管理。
5.3 虛擬記憶體區域
記憶體區域由vm_area_struct結構體描述,見上面的mmap域,記憶體區域在核心中也經常被稱作虛擬記憶體區域(Virtual Memory Area,VMA)。
它描述了指定地址空間內連續區間上的一個獨立記憶體範圍。
核心將每個記憶體區域作為一個單獨的記憶體物件管理,每個記憶體區域都擁有一致的屬性。結構體如下:
1 struct vm_area_struct { 2 struct mm_struct *vm_mm; /* associated mm_struct */ 3 unsigned long vm_start; /* VMA start, inclusive */ 4 unsigned long vm_end; /* VMA end , exclusive */ 5 struct vm_area_struct *vm_next; /* list of VMA's */ 6 pgprot_t vm_page_prot; /* access permissions */ 7 unsigned long vm_flags; /* flags */ 8 struct rb_node vm_rb; /* VMA's node in the tree */ 9 union { /* links to address_space->i_mmap or i_mmap_nonlinear */ 10 struct { 11 struct list_head list; 12 void *parent; 13 struct vm_area_struct *head; 14 } vm_set; 15 struct prio_tree_node prio_tree_node; 16 } shared; 17 struct list_head anon_vma_node; /* anon_vma entry */ 18 struct anon_vma *anon_vma; /* anonymous VMA object */ 19 struct vm_operations_struct *vm_ops; /* associated ops */ 20 unsigned long vm_pgoff; /* offset within file */ 21 struct file *vm_file; /* mapped file, if any */ 22 void *vm_private_data; /* private data */ 23 };
每個記憶體描述符都對應於地址程序空間中的唯一區間。vm_mm域指向和VMA相關的mm_struct結構體。
一個記憶體區域的地址範圍是[vm_start, vm_end),vm_next指向該程序的下一個記憶體區域。
兩個獨立的程序將同一個檔案對映到各自的地址空間,它們分別都會有一個vm_area_struct結構體來標誌自己的記憶體區域;但是如果兩個執行緒共享一個地址空間,那麼它們也同時共享其中的所有vm_area_struct結構體。
在上面的vm_flags域中存放的是VMA標誌,標誌了記憶體區域所包含的頁面的行為和資訊。和物理頁訪問許可權不同,VMA標誌反映了核心處理頁面所需要遵循的行為準則,而不是硬體要求。而且vm_flags同時包含了記憶體區域中每個頁面的訊息或者記憶體區域的整體資訊,而不是具體的獨立頁面。如下表所述:
開頭三個標誌表示程式碼在該記憶體區域的可讀、可寫和可執行許可權。
第四個標誌VM_SHARD說明了該區域包含的對映是否可以在多程序間共享,如果被設定了,表示共享對映;否則未被設定,表示私有對映。
其中很多狀態在實際使用中都非常有用。
5.4 mmap()和do_mmap():建立地址空間
核心使用do_mmap()函式建立一個新的線性地址空間。但如果建立的地址區間和一個已經存在的地址區間相鄰,並且它們具有相同的訪問許可權的話,那麼兩個區間將合併為一個。如果不能合併,那麼就確實需要建立一個新的vma了,但無論哪種情況,do_mmap()函式都會將一個地址區間加入到程序的地址空間中。這個函式定義在linux/mm.h中,如下
unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot,unsigned long flag, unsigned long offset)
這個函式中由file指定檔案,具體對映的是檔案中從偏移offset處開始,長度為len位元組的範圍內的資料,如果file引數是NULL並且offset引數也是0,那麼就代表這次對映沒有和檔案相關,該情況被稱作匿名對映(file-backed mapping)。如果指定了檔案和偏移量,那麼該對映被稱為檔案對映(file-backed mapping)。
其中引數prot指定記憶體區域中頁面的訪問許可權:可讀、可寫、可執行。
flag引數指定了VMA標誌,這些標誌指定型別並改變對映的行為,請見上一小節。
如果系統呼叫do_mmap的引數中有無效引數,那麼它返回一個負值;否則,它會在虛擬記憶體中分配一個合適的新記憶體區域,如果有可能的話,將新區域和臨近區域進行合併,否則核心從vm_area_cachep長位元組(slab)快取中分配一個vm_area_struct結構體,並且使用vma_link()函式將新分配的記憶體區域新增到地址空間的記憶體區域連結串列和紅黑樹中,隨後還要更新記憶體描述符中的total_vm域,然後才返回新分配的地址區間的初始地址。
在使用者空間,我們可以通過mmap()系統呼叫獲取核心函式do_mmap()的功能。
5.5 munmap()和do_munmap():刪除地址空間
do_mummp()函式從特定的程序地址空間中刪除指定地址空間,該函式定義在檔案linux/mm.h中,如下:
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
第一個引數指定要刪除區域所在的地址空間,刪除從地址start開始,長度為len位元組的地址空間,如果成功,返回0,否則返回負的錯誤碼。
與之相對應的使用者空間系統呼叫是munmap,它是對do_mummp()函式的一個簡單封裝。
5.6 malloc()的實現
我們知道malloc()是C庫中實現的。C庫對記憶體分配的管理還有calloc()、realloc()、free()等函式。
事實上,malloc函式是以brk()或者mmap()系統呼叫實現的。
brk和sbrk主要的工作是實現虛擬記憶體到記憶體的對映。在Linux系統上,程式被載入記憶體時,核心為使用者程序地址空間建立了程式碼段、資料段和堆疊段,在資料段與堆疊段之間的空閒區域用於動態記憶體分配。我們回到記憶體結構mm_struct中的成員變數start_code和end_code是程序程式碼段的起始和終止地址,start_data和 end_data是程序資料段的起始和終止地址,start_stack是程序堆疊段起始地址,start_brk是程序動態記憶體分配起始地址(堆的起始地址),還有一個 brk(堆的當前最後地址),就是動態記憶體分配當前的終止地址。所以C庫的malloc()在Linux上的基本實現是通過核心的brk系統呼叫。brk()是一個非常簡單的系統呼叫,核心再執行sys_brk()函式進行記憶體分配,只是簡單地改變mm_struct結構的成員變數brk的值。而sbrk不是系統呼叫,是C庫函式。系統呼叫通常提供一種最小功能,而庫函式通常提供比較複雜的功能。
下面我們整理一下在程序空間堆中用brk()方式進行動態記憶體分配的流程:
C庫函式malloc()呼叫Linux系統呼叫函式brk(),brk()執行系統呼叫陷入到核心,核心執行sys_brk()函式,sys_brk()函式呼叫do_brk()進行記憶體分配
malloc()---------->brk()-----|----->sys_brk()----------->do_brk()------------>vma_merge()/kmem_cache_zalloc()
使用者空間------> | 核心空間
系統呼叫---------->
mmap()系統呼叫也可以實現動態記憶體分配功能,即5.4節我們提到的匿名對映。
那什麼時候呼叫brk(),什麼時候呼叫mmap()呢?通過閾值M_MMAP_THRESHOLD
來決定。該值預設128KB。可以通過mallopt()來進行修改設定。
所以當需要分配的記憶體大於該閾值,選擇mmap();否則小於等於該閾值,選擇brk()分配。
最後,mmap分配的記憶體在呼叫munmap後會立即返回給系統,而brk/sbrk而受M_TRIM_THRESHOLD的影響。該環境變數同樣通過mallopt()來設定,該值代表的意義是釋放記憶體的最少位元組數。
但brk/sbrk分配的記憶體是否立即歸還給系統,不僅受M_TRIM_THRESHOLD的影響,還要看高地址端(brk處)的記憶體是否已經釋放:
假如依次malloc了str1、str2、str3(str3在最上端,結束地址為brk),即使它們都是brk/sbrk分配的,如果沒有釋放str3,只釋放了str1和str2,
就算兩者加起來超過了M_TRIM_THRESHOLD,因為str3的存在,str1和str2也不能立即歸還可以系統,即這些記憶體都被str3給“拴住”了。
此時,str1和str2的記憶體只是簡單的標記為“未使用”,如果這兩處記憶體是相鄰的則會進行合併,這種演算法也稱為“夥伴記憶體演算法(buddy memory allocation scheme)”。這種演算法高速簡單,但同時也會生成碎片。包括內碎片(一次分配記憶體不夠整頁,最後一頁剩下的空間)和外碎片(多次或者反覆分配造成中間的空閒頁太小不夠後續的一次分配)。
從上可以看出,在一定條件下,假如釋放了str3的記憶體,堆的大小是可以緊縮的。
最後我們以一張圖結束今天的主題,記憶體分配流程圖:
參考資料:
《Linux核心設計與實現》原書第三版
https://www.cnblogs.com/bizhu/archive/2012/10/09/2717303.html