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内存管理