Linux内存管理(八): slub分配器和kmalloc

kernel: 5.10
Arch: aarch64


上文 介绍过, 伙伴系统在分配内存时是以物理页为单位的,但在很多场景下,内存需要分配的大小是以字节为单位的,达不到一个物理页的大小。如果继续使用伙伴系统进行内存分配, 那么就会出现严重的内存内碎片问题。 内存内碎片指的是已经被分配出去却不能被利用的内存空间, 一般分配的内存空间大于请求所需的内存空间就会产生内存内碎片。

slub分配器就是为了解决小内存分配问题的。

原理

slub分配器依赖于伙伴系统,slub分配器所做的事情就是把伙伴系统分配的大块内存进一步的细分成很多份小块内存进行管理。
如下图所示:
在这里插入图片描述

至上而下:
(1) slab缓存: 一个或多个大小相同的slab页会组成一个slab缓存。主要控制slab页的布局。
(2) slab页: 一个slab由一个或多个连续的物理页组成
(3) object对象:细粒度内存分配对象。确定一个size,依照该大小将上面的 SLAB 切分成相同大小的小块内存。

这篇文章描述伙伴系统buddy和slub系统的关系就是批发商和零售商的关系。我觉得很合理。

伙伴系统是某白酒生产商,不过该生产商只按罐来卖。 然后有小的零售商去批发白酒,有的零售商批发10罐,有的零售商批发20罐, 这些零售商就是slab缓存, 批发的一罐一罐的饮白酒就是slab页。 批发好的白酒会继续进行加工,为了形成差异化的竞争,有的零售商包装成500ml每瓶, 有的零售商包装成1000ml每瓶, 这些瓶装酒就是object对象。 消费者想要喝多大容量的酒,就去对应的零售商那里买就可以了。
在这里插入图片描述

用户态

内核实现

1. 数据结构

kmem_cache是slab缓存的数据结构
kmem_cache: [include/linux/slub_def.h]

/*
 * Slab cache management.
 */
struct kmem_cache {
	struct kmem_cache_cpu __percpu *cpu_slab;
	/* Used for retrieving partial slabs, etc. */
	slab_flags_t flags;
	unsigned long min_partial;
	unsigned int size;	/* The size of an object including metadata */
	unsigned int object_size;/* The size of an object without metadata */
	unsigned int offset;	/* Free pointer offset */
#ifdef CONFIG_SLUB_CPU_PARTIAL
	/* Number of per cpu partial objects to keep around */
	unsigned int cpu_partial;
#endif
	struct kmem_cache_order_objects oo;

	/* Allocation and freeing of slabs */
	struct kmem_cache_order_objects max;
	struct kmem_cache_order_objects min;
	gfp_t allocflags;	/* gfp flags to use on each alloc */
	int refcount;		/* Refcount for slab cache destroy */
	void (*ctor)(void *);
	unsigned int inuse;		/* Offset to metadata */
	unsigned int align;		/* Alignment */
	unsigned int red_left_pad;	/* Left redzone padding size */
	const char *name;	/* Name (only for display!) */
	struct list_head list;	/* List of slab caches */
#ifdef CONFIG_NUMA
	unsigned int remote_node_defrag_ratio;
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
	unsigned int *random_seq;
#endif
	unsigned int useroffset;	/* Usercopy region offset */
	unsigned int usersize;		/* Usercopy region size */

	struct kmem_cache_node *node[MAX_NUMNODES];
};
成员描述
cpu_slabpercpu 类型的 slab, percpu 类型的 slab, 是每个CPU的本地对象缓冲池,主要是为了解决多核之间的锁竞争问题。
flagsslab 相关的标志位
min_partial每个node结点中部分空slab缓冲区数量不能低于这个值
size一个缓存块(object)所占用的内存空间,包含对齐字节
object_sizeobject 实际大小
offsetslub 中利用空闲的 object 内存,来保存下一个空闲 object 的指针,以此组成一个链表结构,该 offset 就是存放 next 指针的基地址偏移,通常情况下是 0。
cpu_partial限制 cpu_slab 上保存的 partial 链表数量
oostruct kmem_cache_order_objects 的结构体, 低16位表示object 数量,高16位表示slab的order,即slab 占用2 ^ order个页
max限定 oo 的上限
min限定 oo 的下限
allocflags从 buddy 子系统分配内存时使用的掩码
refcount引用计数, 内存回收机制会用到
ctor创建slab时的构造函数
inuse元数据的偏移量
align对齐字节数
red_left_pad用于检测左oob
nameslab缓存的名称
list链表节点,通过该节点将当前 kmem_cache 链接到 slab_caches 链表中。
remote_node_defrag_ratio用于NUMA架构,该值越小,越倾向于在本结点分配对象
useroffsetusercopy区域的偏移量
usersizeusercopy区域的大小
mode[MAX_NUMNODES]是每个内存节点的共享对象缓冲池。MAX_NUMNODES 就是 NUMA node 节点的数量

再着重看下cpu_slab和node, 分别对应kmem_cache_cpukmem_cache_node 这两个数据结构

kmem_cache_cpu: [include/linux/slub_def.h]

struct kmem_cache_cpu {
	void **freelist;	/* Pointer to next available object */
	unsigned long tid;	/* Globally unique transaction id */
	struct page *page;	/* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
	struct page *partial;	/* Partially allocated frozen slabs */
#endif
#ifdef CONFIG_SLUB_STATS
	unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
成员描述
freelist指向下一个可用的object。
tid这是一个用作校验的字段,主要用来判断tid和kmem_cache是否由同一个CPU访问。
page指向当前使用的slab
partial指向当前cpu上缓存的部分空闲slab链表

kmem_cache_node: [mm/slab.h]

struct kmem_cache_node {
	spinlock_t list_lock;
	
	unsigned long nr_partial;
	struct list_head partial;
};
成员描述
list_lock自旋锁,保护数据
nr_partial当前 node 上保留的 partial slab 的数量
partial连接 partial slab 的链表头

page中描述Slub信息的字段:

struct page {
     /* 如果flag设置成PG_slab,表示页属于slub分配器 */
      unsigned long flags;
      union {
        struct address_space *mapping;  
        /* 指向当前slab中第一个object */
        void *s_mem;      /* slab first object */
        atomic_t compound_mapcount;  /* first tail page */
      };
      union {
        pgoff_t index;    /* Our offset within mapping. */
        /* 指向当前slab中第一个空闲的object */
        void *freelist;    /* sl[aou]b first free object */
      };
      union {
        unsigned counters;
        struct {
          union {
            atomic_t _mapcount;
            unsigned int active;    /* SLAB */
            struct {      /* SLUB */
              /* 该slab中已经分配使用的object数量 */
              unsigned inuse:16;
              /* 该slab中的所有object数量 */
              unsigned objects:15;
              /* 
               * 如果slab在kmem_cache_cpu中,表示处于冻结状态;
               * 如果slab在kmem_cache_node的部分空闲slab链表中,表示处于解冻状态
        */
              unsigned frozen:1;
            };
            int units;      /* SLOB */
          };
          atomic_t _refcount;
        };
      };
      union {
        /* 作为链表节点加入到kmem_cache_node的部分空闲slab链表中
        struct list_head lru;  /* Pageout list   */
        struct dev_pagemap *pgmap; 
        struct {    /* slub per cpu partial pages */
          struct page *next;  /* Next partial slab */
          int pages;  /* Nr of partial slabs left */
          int pobjects;  /* Approximate # of objects */
        };
        struct rcu_head rcu_head;
        struct {
          unsigned long compound_head; /* If bit zero is set */
          unsigned int compound_dtor;
          unsigned int compound_order;
        };
      };
      union {
        unsigned long private;
        struct kmem_cache *slab_cache;  /* SL[AU]B: Pointer to slab */
      };
    ......
    }

在这里插入图片描述

2. API

2.1 kmem_cache_create

内核通过kmem_cache_create()接口来创建一个slab缓存。

struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
		slab_flags_t flags, void (*ctor)(void *))

一共有5个参数。

  • name: 要创建的slab对象的名称
  • size: slab对象的大小
  • align: slab对象的对齐大小
  • flags: slab内存分配器的掩码和标志位, 比如常用的SLAB_HWCACHE_ALIGN标志位,创建的kmem_cache管理的object按照硬件cache 对齐
  • ctor: 对象的构造函数

kmem_cache_create()函数的流程如下图所示:
在这里插入图片描述

  • kmem_cache_alias()函数用于查找是否有现成的slab描述符可以使用, 如果有直接退出。
  • create_cache()函数创建slab描述符
  • kmem_cache_zalloc()函数分配一个kmem_cache数据结构
  • kmem_cache_open()函数是核心函数, 对结构体的成员进行初始化。
    calcute_sizes() 用于初始化object数目, 大小,分配order等值;
    set_min_partial()设置kmem_cache中的min_partial,它表示kmem_cache_node中partial链表可挂入的slab数量;
    set_cpu_partial()设置kmem_cache中的cpu_partial,它表示per cpu partial上所有slab中free object总数;
    init_kmem_cache_nodes()为每个节点分配kmem_cache_node;
    alloc_kmem_cache_cpus()为kmem_cache_cpu变量创建每CPU副本;
2.2 kmem_cache_alloc

在这里插入图片描述

  • get_freelist() 从percpu缓存的页面中获取freelist, 获取成功则返回
  • new_slab_object()会先调用get_partial()从Node的partial链表中获取slab页,获取成功则返回; 如果没有获取到,则allocate_slab()从伙伴系统分配slab页面,并初始化slab页面中的空闲对象
  • get_free_pointer_safe() 获取下一个object对象
  • this_cpu_cmpxhcg_double()比较object和next object并进行交换处理
  • prefetch_freepointer() 将next_object地址放到cacheline,提高命中率

kmem_cache_alloc() 首先会通过local_irq_save()函数关闭本地中断,防止在处理percpu的slabs 不会因为内核调度而产生变化。
和Buddy System中分配页面类似,slub分配器存在快速路径和慢速路径两种,所谓的快速路径就是per-CPU缓存,可以无锁访问,因而效率更高。
大致流程是:会先判断本地缓冲池有没有空闲的对象,有的话直接获取slab对象,—> 如果没有就会则从Node管理的slab页面中迁移slab页到per-CPU缓存中,再重新分配---->当Node管理的slab页面也不足的情况下,则从Buddy System中分配新的页面,添加到per-CPU缓存中。

3. kmalloc

内核中常用的kmalloc()函数的核心实现就是slub机制.

static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
	if (__builtin_constant_p(size)) {                     ---------------1if (size > KMALLOC_MAX_CACHE_SIZE)                ---------------2return kmalloc_large(size, flags);
		index = kmalloc_index(size);                      -------------------3if (!index)
			return ZERO_SIZE_PTR;

		return kmem_cache_alloc_trace(
				kmalloc_caches[kmalloc_type(flags)][index],
				flags, size);
	}
	return __kmalloc(size, flags);
}

(1) __builtin_constant_p编译器内联函数,判断传入参数是否为常量。如果是变量,直接调用__kmalloc()函数。
(2) 如果分配的size大于KMALLOC_MAX_CACHE_SIZE , 直接调用kmalloc_large()函数

#ifdef CONFIG_SLUB
#define KMALLOC_SHIFT_HIGH	(PAGE_SHIFT + 1)
#define KMALLOC_MAX_CACHE_SIZE	(1UL << KMALLOC_SHIFT_HIGH)
#endif

可以看出KMALLOC_MAX_CACHE_SIZE 大小为2 * PAGE_SIZE = 8k.

(3) 系统启动初期会创建多个管理不同大小对象的kmem_cache。 通过`kmalloc_index()函数查找符合满足分配大小的最小kmem_cache。

static __always_inline unsigned int kmalloc_index(size_t size)
{
	if (!size)
		return 0;

	if (size <= KMALLOC_MIN_SIZE)
		return KMALLOC_SHIFT_LOW;

	if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
		return 1;
	if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
		return 2;
	if (size <=          8) return 3;
	if (size <=         16) return 4;
	if (size <=         32) return 5;
	if (size <=         64) return 6;
	if (size <=        128) return 7;
	if (size <=        256) return 8;
	if (size <=        512) return 9;
	if (size <=       1024) return 10;
	if (size <=   2 * 1024) return 11;
	if (size <=   4 * 1024) return 12;
	if (size <=   8 * 1024) return 13;
	if (size <=  16 * 1024) return 14;
	if (size <=  32 * 1024) return 15;
	if (size <=  64 * 1024) return 16;
	if (size <= 128 * 1024) return 17;
	if (size <= 256 * 1024) return 18;
	if (size <= 512 * 1024) return 19;
	if (size <= 1024 * 1024) return 20;
	if (size <=  2 * 1024 * 1024) return 21;
	if (size <=  4 * 1024 * 1024) return 22;
	if (size <=  8 * 1024 * 1024) return 23;
	if (size <=  16 * 1024 * 1024) return 24;
	if (size <=  32 * 1024 * 1024) return 25;
	if (size <=  64 * 1024 * 1024) return 26;
	BUG();

	/* Will never be reached. Needed because the compiler may complain */
	return -1;
}

比如通过kmalloc(20, GFP_KERNEL)申请内存,系统会从名称“kmalloc-32”管理的slab缓存池中分配一个对象, 即使浪费了12Byte内存。
但是从这个函数来看,最大可以到64MB, 这里应该写的有问题, slab最大宏定义为32M, 64M这个分支多少显得有些多余。

5. 参考资料

图解slub
Slab Memory Allocator
linux内存子系统 - slub 分配器0 - slub原理

  • 0
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Linux内存管理中的页面分配器是负责分配和管理物理页面(Page)的一种机制。在内核中,物理页面是以固定大小的块进行管理的,通常被称为页面帧(Page Frame)。页面分配器的主要任务是从可用的物理内存中分配页面帧,并在需要时释放这些页面帧。 Linux中有多种页面分配器,其中最常用的是伙伴系统(Buddy System)。伙伴系统将可用的物理内存划分为不同大小的块,每个块都是2的幂次方大小。当需要分配一块n个页面帧大小的内存时,伙伴系统会找到一个大小为2的幂次方,并且大于等于n的最小块。如果找到的块大于n,则将该块分裂成两个小块,其中一个块将被分配,另一个块将被留作备用。如果找到的块恰好是n,则将该块分配出去。如果没有合适的块可用,则会尝试从内存中释放一些页面帧来获得可用的内存。 伙伴系统的优点是效率高,分配和释放内存的速度都很快。它还可以避免内存碎片的问题,因为它只分配大小为2的幂次方的块,这些块可以非常有效地组合在一起,而不会留下任何碎片。但是,伙伴系统的缺点是它会浪费一些内存。当一个块被分割成两个小块时,其中一个块可能永远不会被使用,从而浪费了一些内存。 除了伙伴系统之外,Linux还提供了其他的页面分配器,如SLAB和SLUB。这些页面分配器适用于不同的场景,具有不同的优缺点。在实际应用中,应该根据具体的需求选择适合的页面分配器

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值