【Linux基礎系列之】內存管理(1)-buddy和slub算法

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內存管理專欄

理解linux內存管理

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