目錄
文章目錄
內存分配算法
Linux 系統把物理內存劃分 4K 大小的內存頁(Page),也稱做頁框(Page Frame),物理內存的分配和回收都是基於內存頁進行,把物理內存分頁管理有不少好處。假如系統請求小塊內存,能夠預先分配一頁給它,避免了反覆的申請和釋放小塊內存帶來頻繁的系統開銷。假如系統須要大塊內存,則能夠用多頁內存拼湊,而沒必要要求大塊連續內存。前端
注意,若是就直接這樣把內存分頁使用,再也不加額外的管理仍是存在一些問題,下面咱們來看下,系統在屢次分配和釋放物理頁的時候會遇到哪些問題。node
在內核態申請內存比在用戶態申請內存要更爲直接,它沒有采用用戶態那種延遲分配內存技術。內核認爲一旦有內核函數申請內存,那麼就必須馬上知足該申請內存的請求,而且這個請求必定是正確合理的。相反,對於用戶態申請內存的請求,內核老是儘可能延後分配物理內存,用戶進程老是先得到一個虛擬內存區的使用權,最終經過缺頁異常得到一塊真正的物理內存。linux
IA32(Intel x86 32 位)架構支持 4GB 內存的尋址,其中的內核虛擬地址空間只有 1GB 大小(從 3GB 到 4GB),所以能夠直接將 1GB 大小的物理內存(即常規內存)映射到內核地址空間,但超出 1GB 大小的物理內存(即高端內存)就不能映射到內核空間。爲此,內核採起了下面的方法使得內核可使用全部的物理內存。算法
-
高端內存不能所有映射到內核空間,也就是說這些物理內存沒有對應的線性地址。不過,內核爲每一個物理頁框都分配了對應的頁框描述符,全部的頁框描述符都保存在 mem_map 數組中,所以每一個頁框描述符的線性地址都是固定存在的。內核此時可使用 alloc_pages() 和 alloc_page() 來分配高端內存,由於這些函數返回頁框描述符的線性地址。api
-
內核地址空間的後 128MB 專門用於映射高端內存,不然,沒有線性地址的高端內存不能被內核所訪問。這些高端內存的內核映射顯然是暫時映射的,不然也只能映射 128MB 的高端內存。當內核須要訪問高端內存時就臨時在這個區域進行地址映射,使用完畢以後再用來進行其餘高端內存的映射。數組
因爲要進行高端內存的內核映射,所以直接可以映射的物理內存大小隻有 896MB,該值保存在 high_memory 中。內核地址空間的線性地址區間以下圖所示:緩存
從圖中能夠看出,內核採用了三種機制將高端內存映射到內核空間:永久內核映射、固定映射和 vmalloc 機制。數據結構
物理內存分配
內存碎片
基於物理內存在內核空間中的映射原理,物理內存的管理方式也有所不一樣。架構
物理頁管理面臨問題:物理內存頁分配會出現外部碎片和內部碎片問題,所謂的內部和外部是針對 「頁框內外」 而言的,一個頁框內的內存碎片是內部碎片,多個頁框間的碎片是外部碎片。async
- 外部碎片:當須要分配大塊內存的時候,要用好幾頁組合起來纔夠,而系統分配物理內存頁的時候會盡可能分配連續的內存頁面,頻繁的分配與回收物理頁致使大量的小塊內存夾雜在已分配頁面中間,造成外部碎片。
- 內部碎片:物理內存是按頁來分配的,這樣當實際只須要很小內存的時候,也會分配至少是 4K 大小的頁面,而內核中有不少須要以字節爲單位分配內存的場景,這樣原本只想要幾個字節而已卻不得不分配一頁內存,除去用掉的字節剩下的就造成了內部碎片。
內核中物理內存的管理機制主要有夥伴算法,Slab 高速緩存和 vmalloc 機制。其中夥伴算法和 Slab 高速緩存都在物理內存映射區分配物理內存,而 vmalloc 機制則在高端內存映射區分配物理內存。
基本原理:內存分配較小,而且分配的這些小的內存生存週期又較長,反覆申請後將產生內存碎片的出現。
- 優勢:提升分配速度,便於內存管理,防止內存泄露。
- 缺點:大量的內存碎片會使系統緩慢,內存使用率低,浪費大。
如何避免內存碎片:
- 少用動態內存分配的函數(儘可能使用棧空間)。
- 分配內存和釋放的內存儘可能在同一個函數中。
- 儘可能一次性申請較大的內存,而不要反覆申請小內存。
- 儘量申請大塊的 2 的指數冪大小的內存空間。
- 外部碎片避免 — 夥伴系統算法。
- 內部碎片避免 — slab 算法。
- 本身進行內存管理工做,設計內存池。
夥伴(Buddy)分配算法
外部碎片:的是尚未被分配出去(不屬於任何進程),但因爲過小了沒法分配給申請內存空間的新進程的內存空閒區域3) 組織結構。
夥伴系統算法(Buddy system),顧名思義,就是把相同大小的頁框塊用鏈表串起來,頁框塊就像手拉手的好夥伴,也是這個算法名字的由來。夥伴算法負責大塊連續物理內存的分配和釋放,以頁框爲基本單位。該機制能夠避免外部碎片。
具體的,全部的空閒頁框分組爲 11 個塊鏈表,每一個塊鏈表分別包含大小爲 1,2,4,8,16,32,64,128,256,512 和 1024 個連續頁框的頁框塊。最大能夠申請 1024 個連續頁框,對應 4MB 大小的連續內存。
由於任何正整數均可以由 2^n 的和組成,因此總能找到合適大小的內存塊分配出去,減小了外部碎片產生 。
如此的,假設須要申請 4 個頁框,可是長度爲 4 個連續頁框塊鏈表沒有空閒的頁框塊,夥伴系統會從連續 8 個頁框塊的鏈表獲取一個,並將其拆分爲兩個連續 4 個頁框塊,取其中一個,另一個放入連續 4 個頁框塊的空閒鏈表中。釋放的時候會檢查,釋放的這幾個頁框先後的頁框是否空閒,可否組成下一級長度的塊。
命令查看:
[root@c-dev ~]# cat /proc/buddyinfo Node 0, zone DMA 1 0 0 0 2 1 1 0 1 1 3 Node 0, zone DMA32 189 209 119 80 38 17 11 1 1 2 627 Node 0, zone Normal 1298 1768 1859 661 743 461 275 133 68 61 2752
申請和回收
申請算法:
- 申請 2i 個頁塊存儲空間,若是 2i 對應的塊鏈表有空閒頁塊,則分配給應用。
- 若是沒有空閒頁塊,則查找 2**(i 1) 對應的塊鏈表是否有空閒頁塊,若是有,則分配 2i 塊鏈表節點給應用,另外 2i 塊鏈表節點插入到 2**i 對應的塊鏈表中。
- 若是 2**(i 1) 塊鏈表中沒有空閒頁塊,則重複步驟 2,直到找到有空閒頁塊的塊鏈表。
- 果仍然沒有,則返回內存分配失敗。
回收算法:
- 釋放 2i 個頁塊存儲空間,查找 2i 個頁塊對應的塊鏈表,是否有與其物理地址是連續的頁塊,若是沒有,則無需合併。
- 若是有,則合併成 2**(i1) 的頁塊,以此類推,繼續查找下一級塊連接,直到不能合併爲止。
條件:
- 兩個塊具備相同的大小。
- 它們的物理地址是連續的.
- 頁塊大小相同。
如何分配 4M 以上內存?
-
爲什麼限制大塊內存分配?
- 分配的內存越大, 失敗的可能性越大。
- 大塊內存使用場景少。
-
內核中獲取 4M 以上大內存的方法
- 修改 MAX_ORDER,從新編譯內核。
- 內核啓動選型傳遞 「mem=」 參數, 如:「mem=80M」,預留部份內存;
- 而後經過 request_mem_region 和 ioremap_nocache 將預留的內存映射到模塊中。須要修改內核啓動參數,無需從新編譯內核。但這種方法不支持 x86 架構, 只支持 ARM、PowerPC 等非 x86 架構。
- 在 start_kernel 中 mem_init 函數以前調用 alloc_boot_mem 函數預分配大塊內存,須要從新編譯內核。
- vmalloc 函數,內核代碼使用它來分配在虛擬內存中連續但在物理內存中不必定連續的內存。
反碎片機制
不可移動頁:
- 這些頁在內存中有固定的位置,不可以移動,也不可回收。
- 內核代碼段,數據段,內核 kmalloc() 出來的內存,內核線程佔用的內存等。
可回收頁:
- 這些頁不能移動,但能夠刪除。內核在回收頁佔據了太多的內存時或者內存短缺時進行頁面回收
可移動頁:
- 這些頁能夠任意移動,用戶空間應用程序使用的頁都屬於該類別。它們是經過頁表映射的。
- 當它們移動到新的位置,頁表項也會相應的更新。
Slab 算法
Linux 所使用的 slab 分配器的基礎是 Jeff Bonwick 爲 SunOS 操做系統首次引入的一種算法。它的基本思想是:將內核中常用的對象放到高速緩存中,而且由系統保持爲初始的可利用狀態。好比進程描述符,內核中會頻繁對此數據進行申請和釋放。
通常來講,內核對象的生命週期是:分配內存 -> 初始化 -> 釋放內存,內核中有大量的小對象,好比:文件描述結構對象、任務描述結構對象,若是按照夥伴系統按頁分配和釋放內存,就會對小對象頻繁的執行分配內存 -> 初始化 -> 釋放內存的過程,會很是消耗性能。
夥伴系統分配出去的內存仍是以頁框爲單位,而對於內核的不少場景都是分配小片內存,遠用不到一頁內存大小的空間。Slab 分配器最初是爲了解決物理內存的內部碎片而提出的,它將內核中經常使用的數據結構看作對象。Slab 分配器爲每一種對象創建高速緩存,經過將內存按使用對象不一樣再劃分紅不一樣大小的空間,應用於內核對象的緩存,內核對該對象的分配和釋放均是在這塊高速緩存中操做。
可見,Slab 內存分配器是對夥伴分配算法的補充。Slab 緩存負責小塊物理內存的分配,而且它也做爲高速緩存,主要針對內核中常常分配並釋放的對象。
- 減小夥伴算法在分配小塊連續內存時所產生的內部碎片。
- 將頻繁使用的對象緩存起來,減小分配、初始化和釋放對象的時間開銷。
- 經過着色技術調整對象以更好的使用硬件高速緩存。
對於每一個內核中的相同類型的對象,如:task_struct、file_struct 等須要重複使用的小型內核數據對象,都會有個 Slab 緩存池,緩存住大量經常使用的「已經初始化」的對象,每當要申請這種類型的對象時,就從緩存池的 Slab 列表中分配一個出去;而當要釋放時,將其從新保存在該列表中,而不是直接返回給夥伴系統,從而避免內部碎片,同時也大大提升了內存分配性能。
主要優勢:
- Slab 內存管理基於內核小對象,不用每次都分配一頁內存,充分利用內存空間,避免內部碎片。
- Slab 對內核中頻繁建立和釋放的小對象作緩存,重複利用一些相同的對象,減小內存分配次數。
slab 分配器的結構
- 因爲對象是從 slab 中分配和釋放的,所以單個 slab 能夠在 slab 列表之間進行移動。
- slabs_empty 列表中的 slab 是進行回收(reaping)的主要備選對象。
- slab 還支持通用對象的初始化,從而避免了爲同一目而對一個對象重複進行初始化。
kmem_cache 是一個cache_chain 的鏈表組成節點,表明的是一個內核中的相同類型的「對象高速緩存」,每一個kmem_cache 一般是一段連續的內存塊,包含了三種類型的 slabs 鏈表:
- slabs_full:徹底分配的 slab 鏈表
- slabs_partial:部分分配的 slab 鏈表
- slabs_empty:沒有被分配對象的 slab 鏈表
kmem_cache 中有個重要的結構體 kmem_list3 包含了以上三個數據結構的聲明。
slab 是 Slab 分配器的最小單位,在實現上一個 slab 由一個或多個連續的物理頁組成(一般只有一頁)。單個 slab 能夠在 slab 鏈表之間移動,例如:若是一個半滿 slabs_partial 鏈表被分配了對象後變滿了,就要從 slabs_partial 中刪除,同時插入到全滿 slabs_full 鏈表中去。內核 slab 對象的分配過程是這樣的:
- 若是 slabs_partial 鏈表還有未分配的空間,分配對象,若分配以後變滿,移動 slab 到 slabs_full 鏈表。
- 若是 slabs_partial 鏈表沒有未分配的空間,進入下一步。
- 若是 slabs_empty 鏈表還有未分配的空間,分配對象,同時移動 slab 進入 slabs_partial 鏈表。
- 若是 slabs_empty 爲空,請求夥伴系統分頁,建立一個新的空閒 slab, 按步驟 3 分配對象。
查看 Slab 內存信息:
[root@c-dev ~]# cat /proc/slabinfo slabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail> isofs_inode_cache 50 50 640 25 4 : tunables 0 0 0 : slabdata 2 2 0 kvm_async_pf 0 0 136 30 1 : tunables 0 0 0 : slabdata 0 0 0 kvm_vcpu 0 0 14976 2 8 : tunables 0 0 0 : slabdata 0 0 0 xfs_dqtrx 0 0 528 31 4 : tunables 0 0 0 : slabdata 0 0 0 xfs_dquot 0 0 488 33 4 : tunables 0 0 0 : slabdata 0 0 0 xfs_ili 40296 40296 168 24 1 : tunables 0 0 0 : slabdata 1679 1679 0 xfs_inode 41591 41616 960 34 8 : tunables 0 0 0 : slabdata 1224 1224 0 xfs_efd_item 1053 1053 416 39 4 : tunables 0 0 0 : slabdata 27 27 0
實時顯示內核中的 Slab 內存緩存信息:
$ slabtop
slab 高速緩存
Slab 高速緩存分爲如下兩類:
-
通用高速緩存:slab 分配器中用 kmem_cache 來描述高速緩存的結構,它自己也須要 slab 分配器對其進行高速緩存。cache_cache 保存着對高速緩存描述符的高速緩存,是一種通用高速緩存,保存在 cache_chain 鏈表中的第一個元素。另外,slab 分配器所提供的小塊連續內存的分配,也是通用高速緩存實現的。通用高速緩存所提供的對象具備幾何分佈的大小,範圍爲 32 到 131072 字節。內核中提供了 kmalloc() 和 kfree() 兩個接口分別進行內存的申請和釋放。slab 分配器所提供的小塊連續內存的分配是經過通用高速緩存實現的。通用高速緩存所提供的對象具備幾何分佈的大小,範圍爲 32 到 131072 字節。內核中提供了 kmalloc() 和 kfree() 兩個接口分別進行內存的申請和釋放。
-
專用高速緩存:內核爲專用高速緩存的申請和釋放提供了一套完整的接口,根據所傳入的參數爲指定的對象分配 Slab 緩存。內核爲專用高速緩存的申請和釋放提供了一套完整的接口,根據所傳入的參數爲具體的對象分配 slab 緩存。kmem_cache_create() 用於對一個指定的對象建立高速緩存。它從 cache_cache 普通高速緩存中爲新的專有緩存分配一個高速緩存描述符,並把這個描述符插入到高速緩存描述符造成的 cache_chain 鏈表中。kmem_cache_alloc() 在其參數所指定的高速緩存中分配一個 slab。相反, kmem_cache_free() 在其參數所指定的高速緩存中釋放一個 slab。
分區頁框分配器
分區頁框分配器(Zoned Page Frame Allocator),處理對連續頁框的內存分配請求。分區頁框管理器分爲兩大部分:前端的管理區分配器和夥伴系統,以下圖:
管理區分配器負責搜索一個能知足請求頁框塊大小的管理區。在每一個管理區中,具體的頁框分配工做由夥伴系統負責。爲了達到更好的系統性能,單個頁框的申請工做直接經過 per-CPU 頁框高速緩存完成。
per-CPU 頁框高速緩存:內核常常請求和釋放單個頁框,該緩存包含預先分配的頁框,用於知足本地 CPU 發出的單一頁框請求。
該分配器經過幾個函數和宏來請求頁框,它們之間的封裝關係以下圖所示。
這些函數和宏將核心的分配函數 __alloc_pages_nodemask() 封裝,造成知足不一樣分配需求的分配函數。其中,alloc_pages() 系列函數返回物理內存首頁框描述符,__get_free_pages() 系列函數返回內存的線性地址。
非連續內存區內存的分配
內核經過 vmalloc() 來申請非連續的物理內存,若申請成功,該函數返回連續內存區的起始地址,不然,返回 NULL。vmalloc() 和 kmalloc() 申請的內存有所不一樣,kmalloc() 所申請內存的線性地址與物理地址都是連續的,而 vmalloc() 所申請的內存線性地址連續而物理地址則是離散的,兩個地址之間經過內核頁表進行映射。
vmalloc 機制使得內核經過連續的線性地址來訪問非連續的物理頁框,這樣能夠最大限度的使用高端物理內存。
vmalloc() 的工做方式理解起來很簡單:
- 尋找一個新的連續線性地址空間;
- 依次分配一組非連續的頁框;
- 爲線性地址空間和非連續頁框創建映射關係,即修改內核頁表;
vmalloc() 的內存分配原理與用戶態的內存分配類似,都是經過連續的虛擬內存來訪問離散的物理內存,而且虛擬地址和物理地址之間是經過頁表進行鏈接的,經過這種方式能夠有效的使用物理內存。可是應該注意的是,vmalloc() 申請物理內存時是當即分配的,由於內核認爲這種內存分配請求是正當並且緊急的;相反,用戶態有內存請求時,內核老是儘量的延後,畢竟用戶態跟內核態不在一個特權級。
虛擬內存的分配
虛擬內存的分配,包括用戶空間虛擬內存和內核空間虛擬內存。
注意,分配的虛擬內存尚未映射到物理內存,只有當訪問申請的虛擬內存時,纔會發生缺頁異常,再經過上面介紹的夥伴系統和 slab 分配器申請物理內存。
內核空間內存分配
先來回顧一下內核地址空間。
kmalloc 和 vmalloc 分別用於分配不一樣映射區的虛擬內存。
基本原理:
- 先申請分配必定數量的、大小相等(通常狀況下)的內存塊留做備用。
- 當有新的內存需求時,就從內存池中分出一部份內存塊,若內存塊不夠再繼續申請新的內存。
- 這樣作的一個顯著優勢是儘可能避免了內存碎片,使得內存分配效率獲得提高。
內核 API:
- mempool_create 建立內存池對象
- mempool_alloc 分配函數得到該對象
- mempool_free 釋放一個對象
- mempool_destroy 銷燬內存池
kmalloc
kmalloc() 分配的虛擬地址範圍在內核空間的直接內存映射區。
按字節爲單位虛擬內存,通常用於分配小塊內存,釋放內存對應於 kfree ,能夠分配連續的物理內存。函數原型在 <linux/kmalloc.h> 中聲明,通常狀況下在驅動程序中都是調用 kmalloc() 來給數據結構分配內存。
kmalloc 是基於 Slab 分配器的,一樣能夠用 cat /proc/slabinfo 命令,查看 kmalloc 相關 slab 對象信息,下面的 kmalloc-八、kmalloc-16 等等就是基於 Slab 分配的 kmalloc 高速緩存。
vmalloc
vmalloc 分配的虛擬地址區間,位於 vmalloc_start 與 vmalloc_end 之間的動態內存映射區。
通常用分配大塊內存,釋放內存對應於 vfree,分配的虛擬內存地址連續,物理地址上不必定連續。函數原型在 <linux/vmalloc.h> 中聲明。通常用在爲活動的交換區分配數據結構,爲某些 I/O 驅動程序分配緩衝區,或爲內核模塊分配空間。
用戶空間內存分配(malloc)
用戶態內存分配函數:
- alloca 是向棧申請內存,所以無需釋放。
- malloc 所分配的內存空間未被初始化,使用 malloc() 函數的程序開始時(內存空間尚未被從新分配)能正常運行,但通過一段時間後(內存空間已被從新分配)可能會出現問題。
- calloc 會將所分配的內存空間中的每一位都初始化爲零。
- realloc 擴展示有內存空間大小。
- 若是當前連續內存塊足夠 realloc 的話,只是將 p 所指向的空間擴大,並返回 p 的指針地址。這個時候 q 和 p 指向的地址是同樣的。
- 若是當前連續內存塊不夠長度,再找一個足夠長的地方,分配一塊新的內存,q,並將 p 指向的內容 copy 到 q,返回 q。並將 p 所指向的內存空間刪除。
malloc 申請內存
malloc 用於申請用戶空間的虛擬內存,當申請小於 128KB 小內存的時,malloc 使用 sbrk 或 brk 分配內存;當申請大於 128KB 的內存時,使用 mmap 函數申請內存;
存在的問題:因爲 brk/sbrk/mmap 屬於系統調用,若是每次申請內存都要產生系統調用開銷,CPU 在用戶態和內核態之間頻繁切換,很是影響性能。並且,堆是從低地址往高地址增加,若是低地址的內存沒有被釋放,高地址的內存就不能被回收,容易產生內存碎片。
解決:所以,malloc 採用的是內存池的實現方式,先申請一大塊內存,而後將內存分紅不一樣大小的內存塊,而後用戶申請內存時,直接從內存池中選擇一塊相近的內存塊分配出去。
-
調用 malloc 函數時,它沿 free_chuck_list 鏈接表尋找一個大到足以知足用戶請求所須要的內存塊。
-
free_chuck_list 鏈接表的主要工做是維護一個空閒的堆空間緩衝區鏈表。
-
若是空間緩衝區鏈表沒有找到對應的節點,須要經過系統調用 sys_brk 延伸進程的棧空間。
DMA 內存
直接內存訪問是一種硬件機制,它容許外圍設備和主內存之間直接傳輸它們的 I/O 數據,而不須要系統處理器的參與
DMA 控制器的功能:
- 能向 CPU 發出系統保持(HOLD)信號,提出總線接管請求。
- 當 CPU 發出容許接管信號後,負責對總線的控制,進入 DMA 方式。
- 能對存儲器尋址及能修改地址指針,實現對內存的讀寫操做。
- 能決定本次 DMA 傳送的字節數,判斷 DMA 傳送是否結束。
- 發出 DMA 結束信號,使 CPU 恢復正常工做狀態。
DMA 信號:
- DREQ:DMA 請求信號。是外設向 DMA 控制器提出要求,DMA 操做的申請信號。
- DACK:DMA 響應信號。是 DMA 控制器向提出 DMA 請求的外設表示已收到請求和正進行處理的信號。
- HRQ:DMA 控制器向 CPU 發出的信號,要求接管總線的請求信號。
- HLDA:CPU 向 DMA 控制器發出的信號,容許接管總線的應答信號。
缺頁異常
- 經過 get_free_pages 申請一個或多個物理頁面。
- 換算 addr 在進程 pdg 映射中所在的 pte 地址。
- 將 addr 對應的 pte 設置爲物理頁面的首地址。
- 系統調用:Brk—申請內存小於等於 128kb,do_map—申請內存大於 128kb。