cpu硬件管理內存是以頁(4KB)為最小顆粒度的,因為頁描述符設置內存屬性就是按照頁為單位設置的!這個顆粒度是非常大的,用戶如果只要幾十Byte的內存也分配4KB的話,再多的內存也會很快被敗光,同時帶來了內存碎片化的問題,所以迫切需要小顆粒度的內存分配方式!buddy和slab孕育而生!
1、先看看buddy內存管理方式;linux早期版本(比如0.11)管理的方式比較簡單粗暴,直接用bitmap的思路標記物理頁是否被使用,這樣做帶來最直接的問題:內存碎片化!舉例如下:
比如標黃的是已經分配的物理頁(由於是操作系統負責分配物理頁,首次是可以按照順序從低地址到高地址依次不留空隙地分配),沒標注的是剩余的物理頁。隨着時間的推移,部分進程運行完畢后釋放了物理頁,可能變成了如下情況:進程釋放了3個物理頁,但這3個物理頁還是分開的,並未連接起來;帶來的問題:
- 如果有進程需要4個連續的物理頁,要么繼續等,要么從其他內存地址開始尋找.......
- bitmap也不能直觀的快速尋址連續空閑的內存,每次都要從頭開始遍歷查找,效率也低......
這里說個題外話:應用程序調用malloc分配內存的時候,操作系統會通過各種算法在空閑的內存找一塊大小滿足應用程序需求的內存,這是比較耗時的,所以站在提高效率的角度,建議一次性調用malloc申請足夠的內存,然后反復使用;不過這樣做的也帶來了安全問題:逆向時如果用CE找到了這塊內存,就可以繼續定位關鍵的數據生成代碼了!
為了避免出現頁級別的內存碎片,Linux內核中引入了伙伴系統算法(Buddy system):把所有的空閑頁框分組為11個塊鏈表,每個塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512和1024個連續頁框的頁框塊。最大可以申請1024個連續頁框,對應4MB大小的連續內存,每個頁框塊的第一個頁框的物理地址是該塊大小的整數倍,如下:
假設要申請一個256個頁框的塊,先從256個頁框的鏈表中查找空閑塊。如果沒有,就去512個頁框的鏈表中找,找到了則將頁框塊分為2個256個頁框的塊,一個分配給應用,另外一個移到256個頁框的鏈表中。如果512個頁框的鏈表中仍沒有空閑塊,繼續向1024個頁框的鏈表查找,如果仍然沒有,則返回錯誤。頁框塊在釋放時,會主動將兩個連續的頁框塊合並為一個較大的頁框塊;和linux早期簡單粗暴的bitmap管理方式對比,buddy算法明顯是利用了鏈表結構管理物理頁,不停地對頁框做拆開合並拆開合並的動作,算法牛逼之處在於運用了世界上任何正整數都可以由2^n的和組成的原理,讓任何物理頁數量的分配都能夠滿足(只要空閑的物理頁面足夠)!
為了實現buddy算法,需要的幾個結構體:list_head是雙向循環鏈表,該鏈表包含每個空閑頁框塊(2^k)的起始頁框的page。指向鏈表中相鄰元素的指針存放在page的lru字段中(lru在頁非空閑時用於其它目的),nr_free表示空閑塊的個數;
struct free_area { struct list_head free_list; unsigned long nr_free; };
zone結構體中 struct free_area free_area[MAX_ORDER]; free_area數組就快速用來定位鏈表起點;數組的第K個元素就是2^k個頁框鏈表的起始點;
/** * 內存管理區描述符 */ struct zone { /* Fields commonly accessed by the page allocator */ /** * 管理區中空閑頁的數目 */ unsigned long free_pages; /** * Pages_min-管理區中保留頁的數目 * Page_low-回收頁框使用的下界。同時也被管理區分配器為作為閾值使用。 * pages_high-回收頁框使用的上界,同時也被管理區分配器作為閾值使用。 */ unsigned long pages_min, pages_low, pages_high; /* * We don't know if the memory that we're going to allocate will be freeable * or/and it will be released eventually, so to avoid totally wasting several * GB of ram we must reserve some of the lower zone memory (otherwise we risk * to run OOM on the lower zones despite there's tons of freeable ram * on the higher zones). This array is recalculated at runtime if the * sysctl_lowmem_reserve_ratio sysctl changes. */ /** * 為內存不足保留的頁框,分別為各種內存域指定了若干頁 * 用於一些無論如何都不能失敗的關鍵性內存分配 */ unsigned long lowmem_reserve[MAX_NR_ZONES]; /** * 用於實現單一頁框的特殊高速緩存。 * 每內存管理區對每CPU都有一個。包含熱高速緩存和冷高速緩存。 * 內核使用這些列表來保存可用於滿足實現的“新鮮”頁。 * 有些頁幀很可能在CPU高速緩存中,因此可以快速訪問,稱之為熱。 * 未緩存的頁幀稱之為冷的。 */ struct per_cpu_pageset pageset[NR_CPUS]; /* * free areas of different sizes */ /** * 保護該描述符的自旋鎖 */ spinlock_t lock; /** * 標識出管理區中的空閑頁框塊。 * 包含11個元素,被伙伴系統使用。分別對應大小的1,2,4,8,16,32,128,256,512,1024連續空閑塊的鏈表。 * 第k個元素標識所有大小為2^k的空閑塊。free_list字段指向雙向循環鏈表的頭。 * free_list是free_area的內部結構,是個雙向環回鏈表節點。 */ struct free_area free_area[MAX_ORDER]; /* * 為了cache line對齊加的pad */ ZONE_PADDING(_pad1_) /* Fields commonly accessed by the page reclaim scanner */ /** * 活動以及非活動鏈表使用的自旋鎖。 */ 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; /** * 管理區內回收頁框時使用的計數器。 */ unsigned long pages_scanned; /* since last reclaim */ /** * 在管理區中填滿不可回收頁時此標志被置位 */ int all_unreclaimable; /* All pages pinned */ /* * prev_priority holds the scanning priority for this zone. It is * defined as the scanning priority at which we achieved our reclaim * target at the previous try_to_free_pages() or balance_pgdat() * invokation. * * We use prev_priority as a measure of how much stress page reclaim is * under - it drives the swappiness decision: whether to unmap mapped * pages. * * temp_priority is used to remember the scanning priority at which * this zone was successfully refilled to free_pages == pages_high. * * Access to both these fields is quite racy even on uniprocessor. But * it is expected to average out OK. */ /** * 臨時管理區的優先級。 */ int temp_priority; /** * 管理區優先級,范圍在12和0之間。 */ int prev_priority; ZONE_PADDING(_pad2_) /* Rarely used or read-mostly fields */ /* * wait_table -- the array holding the hash table * wait_table_size -- the size of the hash table array * wait_table_bits -- wait_table_size == (1 << wait_table_bits) * * The purpose of all these is to keep track of the people * waiting for a page to become available and make them * runnable again when possible. The trouble is that this * consumes a lot of space, especially when so few things * wait on pages at a given time. So instead of using * per-page waitqueues, we use a waitqueue hash table. * * The bucket discipline is to sleep on the same queue when * colliding and wake all in that wait queue when removing. * When something wakes, it must check to be sure its page is * truly available, a la thundering herd. The cost of a * collision is great, but given the expected load of the * table, they should be so rare as to be outweighed by the * benefits from the saved space. * * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the * primary users of these fields, and in mm/page_alloc.c * free_area_init_core() performs the initialization of them. */ /** * 進程等待隊列的散列表。這些進程正在等待管理區中的某頁。 */ wait_queue_head_t * wait_table; /** * 等待隊列散列表的大小。 */ unsigned long wait_table_size; /** * 等待隊列散列表數組的大小。值為2^order */ unsigned long wait_table_bits; /* * Discontig memory support fields. */ /** * 內存節點。 */ struct pglist_data *zone_pgdat; /** * 指向管理區的第一個頁描述符的指針。這個指針是數組mem_map的一個元素。 */ struct page *zone_mem_map; /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */ /** * 管理區的第一個頁框的下標。 */ unsigned long zone_start_pfn; /** * 以頁為單位的管理區的總大小,包含空洞。 */ unsigned long spanned_pages; /* total size, including holes */ /** * 以頁為單位的管理區的總大小,不包含空洞。 */ unsigned long present_pages; /* amount of memory (excluding holes) */ /* * rarely used fields: */ /** * 指針指向管理區的傳統名稱:DMA、NORMAL、HighMem */ char *name; } ____cacheline_maxaligned_in_smp;
本質上講,buddy算法相對於原始的bitmap,優勢在於:
- 按照不同頁框個數2^n重新組織了內存,便於快速查找用戶所需大小的內存塊(free_area數組的尋址時間復雜度是O(1))
- 不停的分配、重組頁框,在一定程度上減少了頁框碎片
2、buddy算法解決了頁框顆粒度的碎片,但用戶日常使用一般申請的內存大都是幾十、頂多幾百byte,直接分配4KB太大了,需要進一步把4KB的頁框內存細分,以滿足更小顆粒度的需求,slab算法由此誕生!其解決了如下3個問題:
- 解決buddy按照頁的顆粒度分配小內存的碎片問題
- 緩存部分常用的數據結構(包括但不限於inode、dir_entry、task_struct等),減少操作系統分配、回收對象時調整內存的時間開銷
- 通過着色更好地利用cpu硬件的高速緩存cache,允許不同緩存中的對象占用相同的緩存行,從而提高緩存的利用率並獲得更好的性能
整個slab機制可以用下面的圖來概括:
圖從左往右看:
- 內存會存儲多個叫做kmem_cache的結構體實例,實例之間通過鏈表的形式連接!
- 每個kmem_cache包含3個不同的slab隊列,分別是free、partial、full,分別表示空間、部分使用和完全使用的slab隊列
- 每個slab又包含一個或多個page(一般情況是一個),這里就和buddy系統關聯起來了
- 每個slab包含多個object對象,但是不同slab包含對象的大小是不一樣的,比如上圖的第一個kmem_cache所包含object對象大小是32byte,第二個kmem_cache所包含object對象大小是128byte,最后一個是32KB;
為了實現slab機制,相應的結構體是少不了的,linux用的是kmem_cache結構體來承載和聚合各種信息的,如下:重要的屬性都用中文注釋了,比如object的大小、每個slab占用的頁面數量、slab結構體數組等;
/* * Definitions unique to the original Linux SLAB allocator. slab描述符 */ struct kmem_cache { struct array_cache __percpu *cpu_cache;/*本地cpu緩存池*/ /* 1) Cache tunables. Protected by slab_mutex */ unsigned int batchcount; unsigned int limit; unsigned int shared; unsigned int size; struct reciprocal_value reciprocal_buffer_size; /* 2) touched by every alloc & free from the backend */ unsigned int flags; /* constant flags */ unsigned int num; /* # of objs per slab:每個slab中object的數量 */ /* 3) cache_grow/shrink */ /* order of pgs per slab (2^n) :每個slab占用的頁面數量*/ unsigned int gfporder; /* force GFP flags, e.g. GFP_DMA */ gfp_t allocflags; size_t colour; /* cache colouring range:一個slab中不同cache line的數量 */ unsigned int colour_off; /* colour offset */ struct kmem_cache *freelist_cache;/*打造單向鏈表*/ unsigned int freelist_size; /* constructor func */ void (*ctor)(void *obj); /* 4) cache creation/removal */ const char *name;/*slab描述符名字*/ struct list_head list; int refcount; int object_size;/*onject對象的大小,每個kmem_cache可以個性化設置的*/ int align;/*對齊長度*/ /* 5) statistics */ #ifdef CONFIG_DEBUG_SLAB unsigned long num_active; unsigned long num_allocations; unsigned long high_mark; unsigned long grown; unsigned long reaped; unsigned long errors; unsigned long max_freeable; unsigned long node_allocs; unsigned long node_frees; unsigned long node_overflow; atomic_t allochit; atomic_t allocmiss; atomic_t freehit; atomic_t freemiss; #ifdef CONFIG_DEBUG_SLAB_LEAK atomic_t store_user_clean; #endif /* * If debugging is enabled, then the allocator can add additional * fields and/or padding to every object. size contains the total * object size including these internal fields, the following two * variables contain the offset to the user object and its size. */ int obj_offset; #endif /* CONFIG_DEBUG_SLAB */ #ifdef CONFIG_MEMCG struct memcg_cache_params memcg_params; #endif #ifdef CONFIG_KASAN struct kasan_cache kasan_info; #endif #ifdef CONFIG_SLAB_FREELIST_RANDOM unsigned int *random_seq; #endif /*slab鏈表*/ struct kmem_cache_node *node[MAX_NUMNODES]; };
上面的圖不是有3組slab鏈表么?都是在kmem_cache_node結構體中定義的,如下:前面3個slab_partial、slab_full、slab_free就是了!
#ifndef CONFIG_SLOB /* * The slab lists for all objects. */ struct kmem_cache_node { spinlock_t list_lock; #ifdef CONFIG_SLAB struct list_head slabs_partial; /* partial list first, better asm code */ struct list_head slabs_full; struct list_head slabs_free; unsigned long num_slabs; unsigned long free_objects; unsigned int free_limit; unsigned int colour_next; /* Per-node cache coloring */ struct array_cache *shared; /* shared per node */ struct alien_cache **alien; /* on other nodes */ unsigned long next_reap; /* updated without locking */ int free_touched; /* updated without locking */ #endif #ifdef CONFIG_SLUB unsigned long nr_partial; struct list_head partial; #ifdef CONFIG_SLUB_DEBUG atomic_long_t nr_slabs; atomic_long_t total_objects; struct list_head full; #endif #endif };
總的來說:
- slab機制把4KB的內存進一步切小到16byte、32byte、64byte、128byte......等不同大小的object,滿足小內存的使用的需求,由此誕生了kmem_cache結構體,里面又包含了slab結構體;所以說整個算法最核心的思路就是把4KB的頁內存進一步切小成object,然后又用kmem_cache和slab來關系object;
- kmem_cache結構體有多個實例,每個實例中包含的object大小都不同,這個思路和buddy算法的free_area數組沒有任何本質區別,只是實例之間通過鏈表的形式連接了
- 某個kmem_cache結構體中,由於包含了多個同樣大小的object,為了便於管理object,又進一步細分出了3個隊列:slab_full、slab_partial、slab_free,每個隊列都包含了多個slab;每個slab又包含多個object!所以kmem_cache、slab、objectz這3者是1對多對多的關系!
內存管理總結:
目標:
- 1)避免碎片
- 2)快速申請和釋放
解決方法:
- 1)按層級分區塊。分區塊管理,互相不污染。例如arena、chunk、run、region不同層級。這里說的污染是指碎片化
- 2)分配時拆分和釋放時合並。
- 3)充分使用各種緩沖技術,提高性能。
- 4)使用各種高效的數據結構及其算法,包括多級bitmap、鏈表、二叉樹、紅黑樹、匹配堆,等等。
- 5)減少管理數據meta data百分比。
- 6)內存划分為各個池。使用池的概念,池中對象大小都相同。不同的池,對象大小可以不同。
- 7)充分利用cpu的cache的優化。
- 8)其他機制,例如減少鎖的訪問,局部鎖代替全局鎖,從而減少競爭出現的次數。
參考:
1、https://r00tk1ts.github.io/2017/10/20/Linux%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86-%E9%A1%B5%E6%A1%86%E7%AE%A1%E7%90%86/ Linux內核學習——內存管理之頁框管理
2、https://zhuanlan.zhihu.com/p/36140017 linux內存管理算法buddy和slab
3、 https://www.bilibili.com/video/BV1wk4y1y7gL/ 頁框和伙伴算法以及slab機制
4、https://www.dingmos.com/index.php/archives/23/ slab分配器
5、https://www.bilibili.com/video/BV1My4y1e7gF?from=search&seid=15617570158469619634&spm_id_from=333.337.0.0 內存管理思想