Linux內存管理是一個很複雜的系統,也是linux的精髓之一,網絡上講解這方面的文檔也很多,我把這段時間學習內存管理方面的知識記錄在這裏,涉及的代碼太多,也沒有太多仔細的去看代碼,深入解算法,這篇文章就當做內存方面學習的一個入門文檔,方便以後在深入學習內存管理源碼的一個指導作用;
(一)NUMA架構
NUMA通過提供分離的存儲器給各個處理器,避免當多個處理器訪問同一個存儲器產生的性能損失來試圖解決這個問題。對於涉及到分散的數據的應用(在服務器和類似於服務器的應用中很常見),NUMA可以通過一個共享的存儲器提高性能至n倍,而n大約是處理器(或者分離的存儲器)的個數。
當然,不是所有數據都侷限於一個任務,所以多個處理器可能需要同一個數據。爲了處理這種情況,NUMA系統包含了附加的軟件或者硬件來移動不同存儲器的數據。這個操作降低了對應於這些存儲器的處理器的性能,所以總體的速度提升受制於運行任務的特點。
Linux把物理內存劃分爲三個層次來管理:
1. 存儲節點(Node): CPU被劃分爲多個節點(node), 內存則被分簇, 每個CPU對應一個本地物理內存, 即一個CPU-node對應一個內存簇bank,即每個內存簇被認爲是一個節點;
2. 管理區(Zone):每個物理內存節點node被劃分爲多個內存管理區域, 用於表示不同範圍的內存,內核可以使用不同的映射方式映射物理內存,通常管理區的類型可以分爲:ZONE_NORMAL,ZONE_DMA,ZONE_HIGHMEM三種;內核(32位爲例內核空間爲1G)空間如下:
如果物理內存超過896 MiB就爲highmem,則內核無法直接映射全部物理內存,最後的128 MiB用於其他目的,比如vmalloc就可以從這裏分配不連續的內存,最珍貴的是3GB起始的16MB DMA區域直接用於外設和系統之間的數據傳輸;
3. 頁面(Page):內存被細分爲多個頁面幀, 頁面是最基本的頁面分配的單位;
NUMA模式下,處理器被劃分成多個”節點”(node), 每個節點被分配有的本地存儲器空間。 所有節點中的處理器都可以訪問全部的系統物理存儲器,但是訪問本節點內的存儲器所需要的時間,比訪問某些遠程節點內的存儲器所花的時間要少得多。Linux通過struct pglist_data這個結構體來描述節點;
722 typedef struct pglist_data {
723 struct zone node_zones[MAX_NR_ZONES];//是一個數組,包含了結點中各內存域的數據結構;
724 struct zonelist node_zonelists[MAX_ZONELISTS];//指定備用結點及其內存域的列表,以便在當前結點沒有可用空間時,在備用結點分配內存;
725 int nr_zones;//保存結點中不同內存域的數目;
726 #ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
727 struct page *node_mem_map;//指向page實例數組的指針,用於描述結點的所有物理內存頁,它包含了結點中所有內存域的頁。
728 #ifdef CONFIG_MEMCG
729 struct page_cgroup *node_page_cgroup;
730 #endif
731 #ifdef CONFIG_PAGE_EXTENSION
732 struct page_ext *node_page_ext;
733 #endif
734 #endif
735 #ifndef CONFIG_NO_BOOTMEM
736 struct bootmem_data *bdata;//在系統啓動期間,內存管理
//子系統初始化之前,內核頁需要使用內存(另外,還需要保留部分內存用於初始
//化內存管理子系統)。bootmem分配器(bootmem allocator)的機制,這種
//機制僅僅用在系統引導時,它爲整個物理內存建立起一個頁面位圖;
737 #endif
738 #ifdef CONFIG_MEMORY_HOTPLUG
...
750 #endif
751 unsigned long node_start_pfn;////該NUMA結點第一個頁幀的邏輯編號。系統中所有的頁幀是依次編號的,每個頁幀的號碼都是全局唯一的(不只是結點內唯一)。
752 unsigned long node_present_pages; /* total number of physical pages */ //結點中頁幀的數目;
753 unsigned long node_spanned_pages; /* total size of physical page range, including holes *///該結點以頁幀爲單位計算的長度,包含內存空洞。
755 int node_id;//全局結點ID,系統中的NUMA結點都從0開始編號;
756 wait_queue_head_t kswapd_wait;//交換守護進程的等待隊列,在將頁幀換出結點時會用到。
757 wait_queue_head_t pfmemalloc_wait;
758 struct task_struct *kswapd; /* Protected by
759 mem_hotplug_begin/end() *///指向負責該結點的交換守護進程的task_struct。
760 int kswapd_max_order;//定義需要釋放的區域的長度。
761 enum zone_type classzone_idx;
762 #ifdef CONFIG_NUMA_BALANCING
...
771 #endif
772 } pg_data_t;
每個節點的內存會被分爲幾個塊,我們稱之爲管理區(zone) ,一個管理區(zone)由struct zone結構體來描述;include/linux/mmzone.h;
327 struct zone {
331 unsigned long watermark[NR_WMARK];//當系統中可用內存很少的時候,系統進程kswapd被喚醒, 開始回收釋放page, 水印這些參數(WMARK_MIN, WMARK_LOW, WMARK_HIGH)影響着這個代碼的行爲;
341 long lowmem_reserve[MAX_NR_ZONES];//爲了防止一些代碼必須運行在低地址區域,所以事先保留一些低地址區域的內存;
342
343 #ifdef CONFIG_NUMA
344 int node;
345 #endif
351 unsigned int inactive_ratio;//不活動頁的比例,很少使用或者大部分情況下是隻讀的字段;
352
353 struct pglist_data *zone_pgdat;//zone所在的節點;
354 struct per_cpu_pageset __percpu *pageset;//每個CPU的熱/冷頁幀列表,有些頁幀很可能在高速緩存中,可以快速訪問,故稱之爲熱的,反之爲冷;
360 unsigned long dirty_balance_reserve;
361
362 #ifndef CONFIG_SPARSEMEM
367 unsigned long *pageblock_flags;
368 #endif /* CONFIG_SPARSEMEM */
369
370 #ifdef CONFIG_NUMA
374 unsigned long min_unmapped_pages;
375 unsigned long min_slab_pages;
376 #endif /* CONFIG_NUMA */
377
378 /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
379 unsigned long zone_start_pfn;//內存域的第一個頁幀;
422 unsigned long managed_pages;
423 unsigned long spanned_pages;//總頁數,包含空洞;
424 unsigned long present_pages;//可用頁數,不包涵空洞;
426 const char *name;//指向管理區類型名字;
432 int nr_migrate_reserve_block;
433
434 #ifdef CONFIG_MEMORY_ISOLATION
440 unsigned long nr_isolate_pageblock;
441 #endif
442
443 #ifdef CONFIG_MEMORY_HOTPLUG
444 /* see spanned/present_pages for more description */
445 seqlock_t span_seqlock;
446 #endif
472 wait_queue_head_t *wait_table;//進程等待隊列的散列表, 這些進程正在等待管理區中的某頁;
473 unsigned long wait_table_hash_nr_entries;//等待隊列散列表中的調度實體數目;
474 unsigned long wait_table_bits;//等待隊列散列表數組大小, 值爲2^order;
475
476 ZONE_PADDING(_pad1_)
477
478 /* Write-intensive fields used from the page allocator */
479 spinlock_t lock;//對zone併發訪問的保護的自旋鎖;
480
481 /* free areas of different sizes */
482 struct free_area free_area[MAX_ORDER];//沒個bit標識對應的page是否可以分配;
483
484 /* zone flags, see below */
485 unsigned long flags;//zone flags, 描述當前內存的狀態;
486
487 ZONE_PADDING(_pad2_)
492 spinlock_t lru_lock;//LRU(最近最少使用算法)的自旋鎖;
493 struct lruvec lruvec;
494
495 /* Evictions & activations on the inactive file list */
496 atomic_long_t inactive_age;
497
503 unsigned long percpu_drift_mark;
504
505 #if defined CONFIG_COMPACTION || defined CONFIG_CMA
506 /* pfn where compaction free scanner should start */
507 unsigned long compact_cached_free_pfn;
508 /* pfn where async and sync compaction migration scanner should start */
509 unsigned long compact_cached_migrate_pfn[2];
510 #endif
511
512 #ifdef CONFIG_COMPACTION
518 unsigned int compact_considered;
519 unsigned int compact_defer_shift;
520 int compact_order_failed;
521 #endif
523 #if defined CONFIG_COMPACTION || defined CONFIG_CMA
524 /* Set to true when the PG_migrate_skip bits should be cleared */
525 bool compact_blockskip_flush;
526 #endif
528 ZONE_PADDING(_pad3_)
529 /* Zone statistics */
530 atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
531 } ____cacheline_internodealigned_in_smp;
頁我們用struct page(include/linux/mm_types.h)來表示,這裏就不貼出全部代碼,這裏列出幾個重要的成員;
virtual:對於如果物理內存可以直接映射內核的系統, 我們可以之間映射出虛擬地址與物理地址的管理, 但是對於需要使用高端內存區域的頁, 即無法直接映射到內核的虛擬地址空間, 因此需要用virtual保存該頁的虛擬地址;
_refcount:引用計數,表示內核中引用該page的次數, 如果要操作該page, 引用計數會+1, 操作完成-1. 當該值爲0時, 表示沒有引用該page的位置,所以該page可以被解除映射,這往往在內存回收時是有用的;
_mapcount:被頁表映射的次數,也就是說該page同時被多少個進程共享。初始值爲-1,如果只被一個進程的頁表映射了,該值爲0. 如果該page處於夥伴系統中,該值爲PAGE_BUDDY_MAPCOUNT_VALUE(-128),內核通過判斷該值是否爲PAGE_BUDDY_MAPCOUNT_VALUE來確定該page是否屬於夥伴系統;
mapping: 指向與該頁相關的address_space對象;
index : 在映射的虛擬空間(vma_area)內的偏移;一個文件可能只映射一部分,假設映射了1M的空間,index指的是在1M空間內的偏移,而不是在整個文件內的偏移;
lru :鏈表頭,用於在各種鏈表上維護該頁, 以便於按頁將不同類別分組, 主要有3個用途: 夥伴算法(鏈接相同階的夥伴), slab分配器(設置PG_slab標誌), 被用戶態使用或被當做頁緩存使用(連入zone中相應的lru鏈表,供內存回收時使用);
內存初始化
start_kernel() -> setup_arch() -> arm_memblock_init():在系統啓動過程期間, 內核使用了一個額外的簡化形式的內存管理模塊早期的引導內存分配器(boot memory allocator–bootmem分配器)或者memblock, 用於在啓動階段早期分配內存;
start_kernel() -> setup_arch() -> paging_init() -> bootmem_init() : 初始化分頁機制,初始化內存管理;
start_kernel() -> build_all_zonelists() : 建立並初始化結點和內存域的數據結構;
start_kernel() -> mm_init():建立了內核的內存分配器,其中mem_init調用bootmem分配器並遷移到實際的內存管理器(比如夥伴系統)然後調用kmem_cache_init函數初始化內核內部用於小塊內存區的分配器;
start_kernel() -> kmem_cache_init_late() : 在kmem_cache_init之後, 完善分配器的緩存機制, 當前3個可用的內核內存分配器(slab, slob, slub)都會定義此函數;
start_kernel() -> kmemleak_init() : Kmemleak工作於內核態Kmemleak 提供了一種可選的內核泄漏檢測,其方法類似於跟蹤內存收集器。當獨立的對象沒有被釋放時,其報告記錄在/sys/kernel/debug/kmemleak中, Kmemcheck能夠幫助定位大多數內存錯誤的上下文;
start_kernel() -> setup_per_cpu_pageset() : 初始化CPU高速緩存行, 爲pagesets的第一個數組元素分配內存, 換句話說, 其實就是第一個系統處理器分配由於在分頁情況下,每次存儲器訪問都要存取多級頁表,這就大大降低了訪問速度。所以,爲了提高速度,在CPU中設置一個最近存取頁面的高速緩存硬件機制,當進行存儲器訪問時,先檢查要訪問的頁面是否在高速緩存中.
(二)buddy內存分配算法
Linux採用著名的夥伴系統(buddy system)算法來解決外碎片問題。把所有的空閒頁框分組爲11個塊鏈表,每個塊鏈表分別包含大小爲1,2,4,8,16,32,64,128,256,512和1024個連續的頁框。對1024個頁框的最大請求對應着4MB大小的連續內存塊。每個塊的第一個頁框的物理地址是該塊大小的整數倍。例如,大小爲16個頁框的塊,其起始地址是16*2^12(4k常規頁的大小)的倍數。內核試圖把大小爲b的一對空閒夥伴塊合併爲一個大小爲2b的單獨塊。
滿足以下條件的兩個塊稱爲夥伴:
1. 兩個塊具有相同的大小,記作b;
2. 他們的物理地址是連續的;
3. 第一塊的第一個頁框的物理地址是2*b*2^12的倍數;
該算法是迭代的,如果它成功合併所釋放的塊,它會試圖合併2b的塊,以再次試圖形成更大的塊;
struct zone中的struct free_area則是用來描述該管理區夥伴系統的空閒內存塊的;管理區描述符中free_area數組的第k個元素,它標識所有大小爲2^k的空閒塊。這個元素的free_list字段是雙向循環鏈表的頭,這個雙向循環鏈表集中了大小爲2^k頁的空閒塊對應的頁描述符。
92 struct free_area {
93 struct list_head free_list[MIGRATE_TYPES];
94 unsigned long nr_free;//指定了大小爲2^k頁的空閒塊的個數;
95 };
分配出去的頁面可分爲三種類型:
不可移動頁(Non-movable pages):這類頁在內存當中有固定的位置,不能移動。內核的核心分配的內存大多屬於這種類型;
可回收頁(Reclaimable pages):這類頁不能直接移動,但可以刪除,其內容頁可以從其他地方重新生成,例如,映射自文件的數據屬於這種類型,針對這種頁,內核有專門的頁面回收處理;
可移動頁:這類頁可以隨意移動,用戶空間應用程序所用到的頁屬於該類別。它們通過頁表來映射,如果他們複製到新的位置,頁表項也會相應的更新,應用程序不會注意到任何改變。
代碼分析
paging_init(mdesc) -> bootmem_init() -> zone_sizes_init() -> free_area_init_node() : 分頁機制,內存域,節點等初始化;
內存分配:
alloc_pages(mask, order):分配2order頁並返回一個struct page的實例,表示分配的內存塊的起始頁;
alloc_page(mask): 分配一頁,order爲0;
get_zeroed_page(mask):分配一頁並返回一個page實例,頁對應的內存填充0(所有其他函數,分配之後頁的內容是未定義的);
__get_free_pages(mask, order):返回分配內存塊的虛擬地址,而不是page實例;
get_dma_pages(gfp_mask, order):用來獲得適用於DMA的頁.
gfp_mask標誌:
#define ___GFP_DMA 0x01u
#define ___GFP_HIGHMEM 0x02u
#define ___GFP_DMA32 0x04u
#define ___GFP_MOVABLE 0x08u //頁是可移動的
#define ___GFP_RECLAIMABLE 0x10u //頁是可回收的
#define ___GFP_HIGH 0x20u //應該訪問緊急分配池
#define ___GFP_IO 0x40u //可以啓動物理IO
#define ___GFP_FS 0x80u //可以調用底層文件系統?
#define ___GFP_COLD 0x100u //需要非緩存的冷頁
#define ___GFP_NOWARN 0x200u //禁止分配失敗警告
#define ___GFP_REPEAT 0x400u //重試分配,可能失敗
#define ___GFP_NOFAIL 0x800u //一直重試,不會失敗
#define ___GFP_NORETRY 0x1000u //不重試,可能失敗
#define ___GFP_MEMALLOC 0x2000u //使用緊急分配鏈表
#define ___GFP_COMP 0x4000u //增加複合頁元數據
#define ___GFP_ZERO 0x8000u //成功則返回填充字節0的頁
#define ___GFP_NOMEMALLOC 0x10000u //不使用緊急分配鏈表
#define ___GFP_HARDWALL 0x20000u //只允許在進程允許運行的CPU所關聯的結點分配內存
#define ___GFP_THISNODE 0x40000u //沒有備用結點,沒有策略
#define ___GFP_ATOMIC 0x80000u //用於原子分配,在任何情況下都不能中斷
#define ___GFP_ACCOUNT 0x100000u
#define ___GFP_NOTRACK 0x200000u
#define ___GFP_DIRECT_RECLAIM 0x400000u
#define ___GFP_OTHER_NODE 0x800000u
#define ___GFP_WRITE 0x1000000u
#define ___GFP_KSWAPD_RECLAIM 0x2000000u
也有可能是多個mask組合,如常用到的:
#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
GFP_ATOMIC :用於原子分配,在任何情況下都不能中斷, 可能使用緊急分配鏈表中的內存, 這個標誌用在中斷處理程序, 下半部, 持有自旋鎖以及其他不能睡眠的地方;
GFP_KERNEL:這是一種常規的分配方式, 可能會阻塞. 這個標誌在睡眠安全時用在進程的長下文代碼中. 爲了獲取調用者所需的內存, 內核會盡力而爲. 這個標誌應該是首選標誌;
GFP_USER:這是一種常規的分配方式, 可能會阻塞. 這個標誌用於爲用戶空間進程分配內存時使用;
上面幾個分配函數,最終都會調用到allloc_pages()來分配頁:
349 static inline struct page *
350 alloc_pages(gfp_t gfp_mask, unsigned int order)
351 {
352 return alloc_pages_current(gfp_mask, order);
353 }
2063 struct page *alloc_pages_current(gfp_t gfp, unsigned order)
2064 {
//並傳入相應節點的備用域鏈表zonelist;
2082 page = __alloc_pages_nodemask(gfp, order,
2083 policy_zonelist(gfp, pol, numa_node_id()),
2084 policy_nodemask(gfp, pol));
2090 }
2091 EXPORT_SYMBOL(alloc_pages_current);
2944 struct page *
2945 __alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
2946 struct zonelist *zonelist, nodemask_t *nodemask)
2947 {
2948 #ifdef CONFIG_ZONE_MOVABLE_CMA
2949 enum zone_type high_zoneidx = gfp_zone(gfp_mask & ~__GFP_MOVABLE);/*根據gfp_mask確定分配頁所處的管理區*/
2950 #else
2951 enum zone_type high_zoneidx = gfp_zone(gfp_mask);
2952 #endif
2953 struct zone *preferred_zone;
2954 struct zoneref *preferred_zoneref;
2955 struct page *page = NULL;
2956 int migratetype = gfpflags_to_migratetype(gfp_mask);/*根據gfp_mask得到遷移類分配頁的型*/
2957 unsigned int cpuset_mems_cookie;
2958 #if defined(CONFIG_DMAUSER_PAGES) || defined(CONFIG_ZONE_MOVABLE_CMA)
2959 int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET;
2960 #else
2961 int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
2962 #endif
3008
3009 retry_cpuset:
3010 cpuset_mems_cookie = read_mems_allowed_begin();
3011
3012 /* The preferred zone is used for statistics later */
3013 preferred_zoneref = first_zones_zonelist(zonelist, high_zoneidx,
3014 nodemask ? : &cpuset_current_mems_allowed,
3015 &preferred_zone);/*從zonelist中找到zoneidx管理區*/
3016 if (!preferred_zone)
3017 goto out;
3018 classzone_idx = zonelist_zone_idx(preferred_zoneref);
3019
3020 /* First allocation attempt */
3021 page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order,
3022 zonelist, high_zoneidx, alloc_flags,
3023 preferred_zone, classzone_idx, migratetype);//第一次分配
3024 if (unlikely(!page)) {
3025 /*
3026 * Runtime PM, block IO and its error handling path
3027 * can deadlock because I/O on the device might not
3028 * complete.
3029 */
3030 if (IS_ENABLED(CONFIG_ZONE_MOVABLE_CMA))
3031 high_zoneidx = gfp_zone(gfp_mask);
3032
3033 gfp_mask = memalloc_noio_flags(gfp_mask);
/*通過一條低速路徑來進行第二次分配,包括喚醒頁換出守護進程等等*/
3034 page = __alloc_pages_slowpath(gfp_mask, order,
3035 zonelist, high_zoneidx, nodemask,
3036 preferred_zone, classzone_idx, migratetype);
3037 }
3038
3083 return page;
3084 }
3085 EXPORT_SYMBOL(__alloc_pages_nodemask);
2083 static struct page *
2084 get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order,
2085 struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
2086 struct zone *preferred_zone, int classzone_idx, int migratetype)
2087 {
/*從認定的管理區開始遍歷,直到找到一個擁有足夠空間的管理區,
例如,如果high_zoneidx對應的ZONE_HIGHMEM,則遍歷順序爲HIGHMEM-->NORMAL-->DMA,
如果high_zoneidx對應ZONE_NORMAL,則遍歷順序爲NORMAL-->DMA*/
2106 for_each_zone_zonelist_nodemask(zone, z, zonelist,
2107 high_zoneidx, nodemask) {
2108 unsigned long mark;
2109
2110 if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
2111 !zlc_zone_worth_trying(zonelist, z, allowednodes))
2112 continue;
2113 if (cpusets_enabled() &&
2114 (alloc_flags & ALLOC_CPUSET) &&
/*檢查給定的內存域是否屬於該進程允許運行的CPU*/
2115 !cpuset_zone_allowed_softwall(zone, gfp_mask))
2116 continue;
2117 /*
2118 * Distribute pages in proportion to the individual
2119 * zone size to ensure fair page aging. The zone a
2120 * page was allocated in should have no effect on the
2121 * time the page has in memory before being reclaimed.
2122 */
2123 if (alloc_flags & ALLOC_FAIR) {
2124 if (!zone_local(preferred_zone, zone))
2125 break;
2126 if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) {
2127 nr_fair_skipped++;
2128 continue;
2129 }
2130 }
2157 if (consider_zone_dirty && !zone_dirty_ok(zone))
2158 continue;
2159
2160 mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
/*如果管理區的水位線處於正常水平,則在該管理區進行分配*/
2161 if (!zone_watermark_ok(zone, order, mark,
2162 classzone_idx, alloc_flags)) {
2163 int ret;
2164
2165 /* Checked here to keep the fast path fast */
2166 BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
2167 if (alloc_flags & ALLOC_NO_WATERMARKS)
2168 goto try_this_zone;
2169
2170 if (IS_ENABLED(CONFIG_NUMA) &&
2171 !did_zlc_setup && nr_online_nodes > 1) {
2172 /*
2173 * we do zlc_setup if there are multiple nodes
2174 * and before considering the first zone allowed
2175 * by the cpuset.
2176 */
2177 allowednodes = zlc_setup(zonelist, alloc_flags);
2178 zlc_active = 1;
2179 did_zlc_setup = 1;
2180 }
2181
2182 if (zone_reclaim_mode == 0 ||
2183 !zone_allows_reclaim(preferred_zone, zone))
2184 goto this_zone_full;
2185
2186 /*
2187 * As we may have just activated ZLC, check if the first
2188 * eligible zone has failed zone_reclaim recently.
2189 */
2190 if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
2191 !zlc_zone_worth_trying(zonelist, z, allowednodes))
2192 continue;
2193 /*針對NUMA架構的申請頁面回收*/
2194 ret = zone_reclaim(zone, gfp_mask, order);
2195 switch (ret) {
2196 case ZONE_RECLAIM_NOSCAN:/*沒有進行回收*/
2197 /* did not scan */
2198 continue;
2199 case ZONE_RECLAIM_FULL:/*沒有找到可回收的頁面*/
2200 /* scanned but unreclaimable */
2201 continue;
2202 default:
2203 /* did we reclaim enough */
2204 if (zone_watermark_ok(zone, order, mark,
2205 classzone_idx, alloc_flags))
2206 goto try_this_zone;
2207
2217 if (((alloc_flags & ALLOC_WMARK_MASK) == ALLOC_WMARK_MIN) ||
2218 ret == ZONE_RECLAIM_SOME)
2219 goto this_zone_full;
2220
2221 continue;
2222 }
2223 }
2224
2225 try_this_zone:/*分配2^order個頁*/
2226 page = buffered_rmqueue(preferred_zone, zone, order,
2227 gfp_mask, migratetype);
2228 if (page)
2229 break;
2230 this_zone_full:
2231 if (IS_ENABLED(CONFIG_NUMA) && zlc_active)
2232 zlc_mark_zone_full(zonelist, z);
2233 }
2234
2235 if (page) {
2236 /*
2237 * page->pfmemalloc is set when ALLOC_NO_WATERMARKS was
2238 * necessary to allocate the page. The expectation is
2239 * that the caller is taking steps that will free more
2240 * memory. The caller should avoid the page being used
2241 * for !PFMEMALLOC purposes.
2242 */
2243 page->pfmemalloc = !!(alloc_flags & ALLOC_NO_WATERMARKS);
2244 return page;
2245 }
2255 if (alloc_flags & ALLOC_FAIR) {
2256 alloc_flags &= ~ALLOC_FAIR;
2257 if (nr_fair_skipped) {
2258 zonelist_rescan = true;
2259 reset_alloc_batches(preferred_zone);
2260 }
2261 if (nr_online_nodes > 1)
2262 zonelist_rescan = true;
2263 }
2264
2265 if (unlikely(IS_ENABLED(CONFIG_NUMA) && zlc_active)) {
2266 /* Disable zlc cache for second zonelist scan */
2267 zlc_active = 0;
2268 zonelist_rescan = true;
2269 }
2270
2271 if (zonelist_rescan)
2272 goto zonelist_scan;
2273
2274 return NULL;
2275 }
從指定的管理區開始按照zonelist中定義的順序來遍歷管理區,如果該管理區的水位線正常,則調用buffered_rmqueue()在該管理區中分配,如果管理區的水位線過低,則在NUMA架構下會申請頁面回收;
1692 static inline
1693 struct page *buffered_rmqueue(struct zone *preferred_zone,
1694 struct zone *zone, unsigned int order,
1695 gfp_t gfp_flags, int migratetype)
1696 {
1697 unsigned long flags;
1698 struct page *page;
1699 bool cold = ((gfp_flags & __GFP_COLD) != 0);
1700
1701 again:
1702 if (likely(order == 0)) {/*order爲0,即要求分配一個頁*/
1703 struct per_cpu_pages *pcp;
1704 struct list_head *list;
1705
1706 local_irq_save(flags);
1707 pcp = &this_cpu_ptr(zone->pageset)->pcp;/*獲取本地CPU對應的pcp*/
1708 list = &pcp->lists[migratetype];/*獲取和遷移類型對應的鏈表*/
1709 if (list_empty(list)) {/*如果鏈表爲空,則表示沒有可分配的頁,需要從夥伴系統中分配2^batch個頁給list*/
1710 pcp->count += rmqueue_bulk(zone, 0,
1711 pcp->batch, list,
1712 migratetype, cold);
1713 if (unlikely(list_empty(list)))
1714 goto failed;
1715 }
1716
1717 if (cold)/*如果是需要冷頁,則從鏈表的尾部獲取*/
1718 page = list_entry(list->prev, struct page, lru);
1719 else /*如果是需要熱頁,則從鏈表的頭部獲取*/
1720 page = list_entry(list->next, struct page, lru);
1721
1722 list_del(&page->lru);
1723 pcp->count--;
1724 } else {
1725 if (unlikely(gfp_flags & __GFP_NOFAIL)) {
1736 WARN_ON_ONCE(order > 1);
1737 }
1738 spin_lock_irqsave(&zone->lock, flags);
1739 page = __rmqueue(zone, order, migratetype); /*從管理區的夥伴系統中選擇合適的內存塊進行分配*/
/*連續的頁框分配,通過調用__rmqueue()來完成分配,__rmqueue() -> __rmqueue_smallest()*/
1740 spin_unlock(&zone->lock);
1741 if (!page)
1742 goto failed;
1743 __mod_zone_freepage_state(zone, -(1 << order),
1744 get_freepage_migratetype(page));
1745 }
1746
1759 return page;
1760
1761 failed:
1762 local_irq_restore(flags);
1763 return NULL;
1764 }
1068 static inline
1069 struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
1070 int migratetype)
1071 {
1072 unsigned int current_order;
1073 struct free_area *area;
1074 struct page *page;
1075
1076 /* Find a page of the appropriate size in the preferred list */
1077 for (current_order = order; current_order < MAX_ORDER; ++current_order) {
1078 area = &(zone->free_area[current_order]);/*獲取和現在的階數對應的free_area*/
1079 if (list_empty(&area->free_list[migratetype]))
1080 continue;
1081
1082 page = list_entry(area->free_list[migratetype].next,
1083 struct page, lru); /*得到滿足要求的頁塊中的第一個頁描述符*/
1084 list_del(&page->lru);
1085 rmv_page_order(page);
1086 area->nr_free--;
1087 expand(zone, page, order, current_order, area, migratetype);/*進行拆分(在current_order > order的情況下)*/
1088 set_freepage_migratetype(page, migratetype);
1089 return page;
1090 }
1091
1092 return NULL;
1093 }
(三)slub內存分配算法
針對一些經常分配並釋放的對象,如進程描述符等,這些對象的大小一般比較小,如果用buddy system來分配會造成大量的內存碎片,而且處理速度也太慢;slab分配器是基於對象進行管理的,相同類型的對象歸爲一類,每當要申請這樣一個對象,slab分配器就從一個slab列表中分配一個這樣大小的單元出去,而當要釋放時,將其重新保存在該列表中,而不是直接返回給夥伴系統。slab分配對象時,會使用最近釋放的對象內存塊,因此其駐留在CPU高速緩存的概率較高。
slub把內存分組管理,每個組分別包含2^3、2^4、…2^11個字節,在4K頁大小的默認情況下,另外還有兩個特殊的組,分別是96B和192B,共11組。之所以這樣分配是因爲如果申請2^12B大小的內存,就可以使用夥伴系統提供的接口直接申請一個完整的頁面即可。
下圖爲各個結構體關係圖:
通過struct kmem_cache就表示這樣一個組;成員kmem_cache_node就指向了object列表;物理頁按照對象(object)大小組織成單向鏈表,對象大小時候objsize指定的。例如16字節的對象大小,每個object就是16字節,每個object包含指向下一個object的指針,該指針的位置是每個object的起始地址+offset。
void*指向的是下一個空閒的object的首地址,這樣object就連成了一個單鏈表。向slub系統申請內存塊(object)時:slub系統把內存塊當成object看待;
系統定義瞭如下這樣一個數組,每個kmem_cache 結構分配特定的內存大小:
struct kmem_cache kmalloc_caches[PAGE_SHIFT]
59 /*
60 * Slab cache management.
61 */
62 struct kmem_cache {
63 struct kmem_cache_cpu __percpu *cpu_slab;//每個CPU對應的cpu_slab;
64 /* Used for retriving partial slabs etc */
65 unsigned long flags;
66 unsigned long min_partial;//每個node節點中部分空緩衝區數量不能低於這個值;如果小於這個值,空閒slab緩衝區不能夠進行釋放
67 int size; /* The size of an object including meta data */
68 int object_size; /* The size of an object without meta data */
69 int offset; //空閒指針偏移量;/* Free pointer offset. */
70 int cpu_partial; /* Number of per cpu partial objects to keep around */ //表示的是空閒對象數量,小於的情況下要去對應的node節點部分空鏈表中獲取若干個部分空slab;
//kmem_cache_order_objects 表示保存slab緩衝區的需要的頁框數量的
//order值和objects數量的值,通過這個計算出需要多少頁框,oo是默認
//值,max是最大值,min在分配失敗的時候使用;
71 struct kmem_cache_order_objects oo;
72
73 /* Allocation and freeing of slabs */
74 struct kmem_cache_order_objects max;
75 struct kmem_cache_order_objects min;
76 gfp_t allocflags; /* gfp flags to use on each alloc */
77 int refcount; /* Refcount for slab cache destroy */
78 void (*ctor)(void *);//該緩存區的構造函數,初始化的時候調用;並設置該cpu的當前使用的緩衝區;
79 int inuse; /* Offset to metadata */
80 int align; /* Alignment */
81 int reserved; /* Reserved bytes at the end of slabs */
82 const char *name; /* Name (only for display!) */
83 struct list_head list; /* List of slab caches *///所有kmem_cache結構都會鏈入這個鏈表;
84 #ifdef CONFIG_SYSFS
85 struct kobject kobj; /* For sysfs */
86 #endif
87 #ifdef CONFIG_MEMCG_KMEM
88 struct memcg_cache_params *memcg_params;
89 int max_attr_size; /* for propagation, maximum size of a stored attr */
90 #ifdef CONFIG_SYSFS
91 struct kset *memcg_kset;
92 #endif
93 #endif
94
95 #ifdef CONFIG_NUMA
96 /*
97 * Defragmentation by allocating from a remote node.
98 */
99 int remote_node_defrag_ratio;//numa框架,該值越小,越傾向於在本結點分配對象;
100 #endif
//此高速緩存的slab鏈表,每個numa節點有一個,有可能該高速緩存有些slab處於其他幾點上;
101 struct kmem_cache_node *node[MAX_NUMNODES];
102 };
40 struct kmem_cache_cpu {
41 void **freelist;//下一個空閒對象地址/* Pointer to next available object */
42 unsigned long tid; /* Globally unique transaction id *///主要考慮併發;
43 struct page *page; /* The slab from which we are allocating */
//cpu當前使用的slab緩衝區描述符,freelist會指向此slab的下一個空閒對象;
44 struct page *partial; /* Partially allocated frozen slabs */
//cpu部分空slab鏈表,放到cpu的部分空slab鏈表中的slab會被凍結,而放入node中的部分空slab鏈表則解凍,解凍標誌放在slab緩衝區描述符中;
45 #ifdef CONFIG_SLUB_STATS
46 unsigned stat[NR_SLUB_STAT_ITEMS];
47 #endif
48 };
315 * The slab lists for all objects.
316 */
317 struct kmem_cache_node {
318 spinlock_t list_lock;
319
320 #ifdef CONFIG_SLAB
......
331 #endif
332
333 #ifdef CONFIG_SLUB
334 unsigned long nr_partial;
335 struct list_head partial;//只保留了部分空slab緩衝區;
336 #ifdef CONFIG_SLUB_DEBUG
337 atomic_long_t nr_slabs;
338 atomic_long_t total_objects;
339 struct list_head full;
340 #endif
341 #endif
342
343 };
//struct page
//物理內存被劃分成固定大小的塊,稱爲頁幀,kernel會爲每一個頁幀都創建struct page管理結構,保存在全局數組mem_map中。
include/linux/mm_types.h
44 struct page {
110 struct { /* SLUB */
111 unsigned inuse:16;
112 unsigned objects:15;
113 unsigned frozen:1;
114 };
218 };
//inuse表示page內有多少個對象在被使用,objects表示這個page中可以存放多少slab對象。
//slab 緩衝區和struct page共用一個結構;
代碼分析
slub系統的初始化:
start_kernel() -> mm_init() -> kmem_cache_init()
2952 static struct kmem_cache *kmem_cache_node;
3671 void __init kmem_cache_init(void)
3672 {
3673 static __initdata struct kmem_cache boot_kmem_cache,
3674 boot_kmem_cache_node;//聲明靜態變量,存儲臨時kmem_cache結構;
3675
3676 if (debug_guardpage_minorder())
3677 slub_max_order = 0;
3678 //臨時靜態kmem_cache 指向全局變量;
3679 kmem_cache_node = &boot_kmem_cache_node;
3680 kmem_cache = &boot_kmem_cache;
3681 //通過靜態kmem_cache申請slub緩衝區,把管理數據放在上面的靜態變量裏面;
3682 create_boot_cache(kmem_cache_node, "kmem_cache_node",
3683 sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN);
3684
3685 register_hotmemory_notifier(&slab_memory_callback_nb);
3686
3687 /* Able to allocate the per node structures */
3688 slab_state = PARTIAL;
3689
3690 create_boot_cache(kmem_cache, "kmem_cache",
3691 offsetof(struct kmem_cache, node) +
3692 nr_node_ids * sizeof(struct kmem_cache_node *),
3693 SLAB_HWCACHE_ALIGN);
3694 //把kmem_cache拷貝到新申請的對象中,完成自引導;
3695 kmem_cache = bootstrap(&boot_kmem_cache);
3696
3702 kmem_cache_node = bootstrap(&boot_kmem_cache_node);
3703
3704 /* Now we can use the kmem_cache to allocate kmalloc slabs */ //創建kmalloc常規緩存;
3705 create_kmalloc_caches(0);
3706
3707 #ifdef CONFIG_SMP
3708 register_cpu_notifier(&slab_notifier);
3709 #endif
3710
3715 }
第一次申請的時候,slub系統剛剛建立,因此只能向夥伴系統申請空閒的內存頁,通過kmem_cache中的cotr函數指針指向的構造函數並初始化這個緩衝區,並把這些頁面分成很多個object,取出其中的一個object標誌爲已被佔用,並返回給用戶,其餘的object標誌爲空閒並放在kmem_cache_cpu中保存。kmem_cache_cpu的freelist變量中保存着下一個空閒object的地址。
緩存的創建 :kmem_cache_create()
對象(object)申請:slab_alloc() -> slab_alloc_node() -> get_freepointer_safe(): 這是從本地緩存獲取;
266 static inline void *get_freepointer(struct kmem_cache *s, void *object)
267 {
268 return *(void **)(object + s->offset);
269 }
slab_alloc() -> slab_alloc_node() -> __slab_alloc() : 慢速路徑獲取。如果本地CPU緩存沒有空閒對象,則申請新的slab;如果有空閒對象,但是內存node不相符,則deactive當前cpu_slab,再申請新的slab。
分配機制:
當slub已經連續申請了很多頁,現在kmem_cache_cpu中已經沒有空閒的object了,但kmem_cache_node的partial中有空閒的object 。所以從kmem_cache_node的partial變量中獲取有空閒object的slab,並把一個空閒的object返回給用戶。
當目前分配的slab緩衝區使用完了之後,會把這個滿的slab緩衝區移除,再從夥伴系統獲取一段連續頁框作爲新的空閒slab緩衝區,而那些滿的slab緩衝區中有對象釋放的時,slub分配器優先把這些緩衝區放入該cpu對應的部分空slab鏈表;而當一個部分空slab釋放成了一個空的slab緩衝區的時候。slub分配器根據情況將此空閒slab放入到node節點的部分空slab連表中;
當部分空slab釋放一個對象後,轉變成了空閒slab緩衝區,系統會檢查node部分部分空鏈表的slab的緩衝區個數,如果這個個數小於min_partial,則將這個空閒緩衝區放入node部分空鏈表中;否則釋放這個空閒slab;將其佔用頁框返回到夥伴系統中;
當kmem_cache刷新的時候,會將kmem_cache所有的slab緩衝區放回到node節點的部分空鏈表;
詳細的slub分配規則可以參考:linux內存源碼分析 - SLUB分配器概述
slub與slab的區別:
Slab器分爲三個的每個節點分爲三個鏈表,分別是空閒slab鏈表,部分空slab鏈表,已滿slab鏈表,這三個slab維護着對應的slab緩衝區,這些slab緩衝區並不會自動返回到夥伴系統中去,而是添加到這node節點的這個三個鏈表中去,這樣就會有很多slab緩衝區是很少用到的;而slub精簡爲了一個鏈表,只保留了部分空鏈表,這樣每個CPU都維護有自己的一個部分空鏈表;
參考:
Linux內存管理專欄