linux内存分配器

介绍一下几个linux的内存分配器。

伙伴分配器

内核初始化完毕后,使用页分配器管理物理页,当前使用的页分配器就是伙伴分配器,伙伴分配器的特点是管理算法简单且高效。

基本伙伴分配器

连续的物理页称为页块(page block),阶(order)是页的数量单位,2的n次方个连续页称为n阶页块,满足如下条件的两个n阶页块称为伙伴(buddy)。

  1. 两个页块是相邻的,即物理地址是连续的;
  2. 页块的第一页的物理面页号必须是2的n次方的整数倍;
  3. 如果合并(n+1)阶页块,第一页的物理页号必须是2的括号(n+1)次方的整数倍。

伙伴分配器分配和释放物理页的数量单位也为阶(order)。以单页为说明,0号页和1号页是伙伴,2号页和3号页是伙伴。1号页和2号页不是伙伴?因为1号页和2号页合并组成一阶页块,第一页的物理页号不是2的整数倍。

分配过程

  1. 查看是否有空闲的n阶页块,如果有,则直接分配;如果没有,则进入步骤2。
  2. 查看是否有空闲的n+1阶页块,如果有,把该页块拆分为两个n阶页块,一个插入空闲n阶页块链表,另一个分配出去;如果没有,执行步骤3。
  3. 查看是否有空闲的n+2阶页块,如果有,拆分为两个n+1阶页块,一个插入空闲n+1阶页块链表,另一个执行步骤2;如果没有,继续寻找更高阶页块。

释放过程

释放过程与分配过程基本相反。

  1. 查看伙伴页块是否空闲,如果不空闲,则把这个页块插入到空闲n阶页块的链表中;如果空闲,就合并为n+1阶页块。
  2. 如果合并,寻找合并后的n+1阶页块是否空闲,如果空闲,按照步骤1执行。

分区伙伴分配器

内核在基本伙伴分配器的基础上做了进一步扩展。

  • 支持内存节点和区域,称为分区的伙伴分配器
  • 为预防内存碎片,把物理页根据可移动性分组
  • 针对分配单页做性能优化。为减少处理器之间的竞争,在内存区域增加一个每处理器页集合

内存区域的结构体struct zone成员free_area用来维护空闲页块,数组下标对应页块的阶数。结构体free_area的成员free_list是空闲页块的链表,nr_free是空闲页块的数量。内存区域的结构体成员managed_pages是伙伴分配器管理的物理页的数量。具体看下源码:

struct zone {
	/* Read-mostly fields */

	/* zone watermarks, access with *_wmark_pages(zone) macros */
    //区域水线,使用_wmark_pages(zone)宏进行访问
	unsigned long watermark[NR_WMARK];

	unsigned long nr_reserved_highatomic;

	/*
	 * 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 being tons of freeable ram on the higher zones).  This array is
	 * recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl
	 * changes.
	 */
	long lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
	int node;
#endif
	struct pglist_data	*zone_pgdat;
	struct per_cpu_pageset __percpu *pageset;

#ifndef CONFIG_SPARSEMEM
	/*
	 * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
	 * In SPARSEMEM, this map is stored in struct mem_section
	 */
	unsigned long		*pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

	/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
	unsigned long		zone_start_pfn;

	/*
	 * spanned_pages is the total pages spanned by the zone, including
	 * holes, which is calculated as:
	 * 	spanned_pages = zone_end_pfn - zone_start_pfn;
	 *
	 * present_pages is physical pages existing within the zone, which
	 * is calculated as:
	 *	present_pages = spanned_pages - absent_pages(pages in holes);
	 *
	 * managed_pages is present pages managed by the buddy system, which
	 * is calculated as (reserved_pages includes pages allocated by the
	 * bootmem allocator):
	 *	managed_pages = present_pages - reserved_pages;
	 *
	 * So present_pages may be used by memory hotplug or memory power
	 * management logic to figure out unmanaged pages by checking
	 * (present_pages - managed_pages). And managed_pages should be used
	 * by page allocator and vm scanner to calculate all kinds of watermarks
	 * and thresholds.
	 *
	 * Locking rules:
	 *
	 * zone_start_pfn and spanned_pages are protected by span_seqlock.
	 * It is a seqlock because it has to be read outside of zone->lock,
	 * and it is done in the main allocator path.  But, it is written
	 * quite infrequently.
	 *
	 * The span_seq lock is declared along with zone->lock because it is
	 * frequently read in proximity to zone->lock.  It's good to
	 * give them a chance of being in the same cacheline.
	 *
	 * Write access to present_pages at runtime should be protected by
	 * mem_hotplug_begin/end(). Any reader who can't tolerant drift of
	 * present_pages should get_online_mems() to get a stable value.
	 *
	 * Read access to managed_pages should be safe because it's unsigned
	 * long. Write access to zone->managed_pages and totalram_pages are
	 * protected by managed_page_count_lock at runtime. Idealy only
	 * adjust_managed_page_count() should be used instead of directly
	 * touching zone->managed_pages and totalram_pages.
	 */
	unsigned long		managed_pages;
	unsigned long		spanned_pages;
	unsigned long		present_pages;

	const char		*name;

#ifdef CONFIG_MEMORY_ISOLATION
	/*
	 * Number of isolated pageblock. It is used to solve incorrect
	 * freepage counting problem due to racy retrieving migratetype
	 * of pageblock. Protected by zone->lock.
	 */
	unsigned long		nr_isolate_pageblock;
#endif

#ifdef CONFIG_MEMORY_HOTPLUG
	/* see spanned/present_pages for more description */
	seqlock_t		span_seqlock;
#endif

	int initialized;

	/* Write-intensive fields used from the page allocator */
	ZONE_PADDING(_pad1_)

	/* free areas of different sizes */
    //MAX_ORDER是最大阶数,实际上是可分配的最大阶数加1
    //默认值11,意味着伙伴分配器一次最多可分配2^10页
	struct free_area	free_area[MAX_ORDER];

	/* zone flags, see below */
	unsigned long		flags;

	/* Primarily protects free_area */
	spinlock_t		lock;

	/* Write-intensive fields used by compaction and vmstats. */
	ZONE_PADDING(_pad2_)

	/*
	 * When free pages are below this point, additional steps are taken
	 * when reading the number of free pages to avoid per-cpu counter
	 * drift allowing watermarks to be breached
	 */
	unsigned long percpu_drift_mark;

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
	/* pfn where compaction free scanner should start */
	unsigned long		compact_cached_free_pfn;
	/* pfn where async and sync compaction migration scanner should start */
	unsigned long		compact_cached_migrate_pfn[2];
#endif

#ifdef CONFIG_COMPACTION
	/*
	 * On compaction failure, 1<<compact_defer_shift compactions
	 * are skipped before trying again. The number attempted since
	 * last failure is tracked with compact_considered.
	 */
	unsigned int		compact_considered;
	unsigned int		compact_defer_shift;
	int			compact_order_failed;
#endif

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
	/* Set to true when the PG_migrate_skip bits should be cleared */
	bool			compact_blockskip_flush;
#endif

	bool			contiguous;

	ZONE_PADDING(_pad3_)
	/* Zone statistics */
	atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

介绍里面两个成员

struct free_area

struct free_area {
	struct list_head	free_list[MIGRATE_TYPES];
	unsigned long		nr_free;
};

区域水线

首选的内存区域在什么情况下从备用区域借用物理页?此问题从区域水线讲解深入理解,每个内存区域有3个水线。

水线区分
  1. 高水线(HIGH):如果内存区域的空闲页数大于高水线,说明该内存区域的内存充足;
  2. 低水线(LOW):如果内存区域的空闲页数小于低水线,说明该内存区域的内存轻微不足;
  3. 最低水线(MIN):如果内存区域空闲页数小于最低水线,说明该内存区域的内存严重不足。

最低水线以下的内存称为紧急保留内存,在内存严重不足的紧急情况下才会给少量的紧急保留内存使用,或是释放更多的内存来使用。这种情况一般很少存在,现在内存一般足够大,基本不用考虑这个问题。

enum zone_watermarks {
	WMARK_MIN,
	WMARK_LOW,
	WMARK_HIGH,
	NR_WMARK
};

#define min_wmark_pages(z) (z->watermark[WMARK_MIN])
#define low_wmark_pages(z) (z->watermark[WMARK_LOW])
#define high_wmark_pages(z) (z->watermark[WMARK_HIGH])
水线计算

计算水线时,有两个重要参数
1.min_free_kbytes最小空闲字节数。默认值 4 l o w m e m _ k b y t e s 4\sqrt{lowmem\_kbytes} 4lowmem_kbytes ,并且限制范围由机器决定。其中lowmem_kbytes是低端内存大小,单位是KB,通过命令cat proc/sys/vm/min_free_kbytes查看,也可以修改。
2. watermark_scale_factor水线缩放因子。默认值10,通过命令cat proc/sys/vm/watermark_scale_factor查看,也可修改,取值范围[1,1000]。

可以通过_setup_per_zone_wmarks()负责计算每个内存区域的最低水线、低水线和高水线。

  • min_free_pages=min_free_pages所对应的页数
  • lowmem_pages=所有低端内存区域伙伴分配器管理的页数总和
  • 高端内存区域的最低水线=zone->managed_pages/1024
  • 低端内存区域的最低水线=min_free_pages*zone->managed_pages/lowmem_pages

低水线和高水线计算

增量=max(最低水线/4,managed_pages*watermark_scale_factor/10000)
低水线=最低水线+增量
高水线=最低水线+增量*2

如果最低水线/4比较大,那么计算公式简化为

低水线=最低水线*5/4
高水线=最低水线*3/2

slab分配器

Buddy提供以page为单位的内存分配接口,这对内核来说颗粒度还太大,所以需要一种新的机制,将page拆分为更小的单位来管理。
linux中支持的主要有:slab、slub、slob。其中slob分配器的总代码量比较少,但分配速度不是最高效的,所以不是为大型系统设计,适合内存紧张的嵌入式系统。在配备大量物理内存的大型计算机上,slab分配器的管理数据结构的内存开销比较大,所以设计了slub分配器。这里只介绍slab,其余两种差不多,可以类推。

/*
 * struct slab
 *
 * Manages the objs in a slab. Placed either at the beginning of mem allocated
 * for a slab, or allocated from an general cache.
 * Slabs are chained into three list: fully used, partial, fully free slabs.
 */
struct slab {
	struct list_head list;     //满,部分满或空的链表
	unsigned long colouroff;    //slab着色的偏移量
	void *s_mem;		/* including colour offset */ //在slab中的第一个对象
	unsigned int inuse;	/* num of objs active in slab */  //slab中已分配的对象数
	kmem_bufctl_t free;     //第一个空闲对象
	unsigned short nodeid;
};

内存缓存

介绍一下通用的内存缓存和专用的内存缓存编程接口

通用的内存缓存

三种分配器有统一的编程接口
分配内存kmalloc:块分配器找到一个合适的通用内存缓存,对象的长度刚好大于等于请求的长度。

static __always_inline void *kmalloc(size_t size,gfp_t flags;

参数size是需要分配内存的长度,flags是传给页分配器的分配标志位
重新分配内存krealloc:根据新的长度给对象重新分配内存,如果分配成功返回新地址,失败返回NULL。

void *krealloc(const void *p,size_t new_size,gpf_t flags);

参数p是需要重新分配的对象,new_size需要分配的新的长度,flag标志位。
释放内存kfree:释放对象

void kfree(const void *objp);

这里有个问题,内核如何知道kfree该释放哪个kmem_cache对象呢?首先,根据对象的虚拟地址来得到其物理地址,然后得到物理页号,得到page实例,注意如果是复合页,要得到首页的page实例,再根据page实例的成员slab_cache得到kmem_cache对象。
使用通用内存缓存,有缺陷。块分配器需要找到一个对象,其长度需要刚好大于等于请求的通用内存长度,如果请求的长度和内存缓存对象相差很大,就会浪费很大的空间。

专用内存缓存

为了解决上述问题,引入专用内存缓存。接口:

  • 创建内存缓存kmem_cache_create:创建专用的内存缓存,如果成功返回内存缓存地址,失败返回NULL
  • 指定内存缓存分配kmem_cache_alloc:从指定内存缓存中分配地址
  • 释放对象kmem_cache_free:释放所属的内存缓存对应的对象
  • 销毁内存缓存keme_cache_destroy

数据结构

每个内存缓存对应一个kmem_cache实例。每个slab由一个或多个连续的物理页组成,页的阶数为kmem_cache.gfporder,如果阶数大于0,则组成一个复合页。
slab被划分为多个对象,大多数情况下slab长度不是对象长度的整数倍,slab有剩余部分,可以用来给slab着色。着色指,把slab的第一个对象从slab的起始地址偏移一个数值,偏移值是处理器一级缓存行的整数倍,不同slab的偏移值不同,使不同的slab对象映射到处理器不同的缓存行。

struct kmem_cache {
    //本地高速缓存,每CPU结构,对象释放时,优先放入这个本地CPU高速缓存中
	struct array_cache __percpu *cpu_cache;
/* 1) Cache tunables. Protected by slab_mutex */
    //本地高速缓存预留数量,控制一次获得或转移对象的数量
	unsigned int batchcount; 
	unsigned int limit; //本地高速缓存中空闲对象的最大数目
	unsigned int shared; //CPU共享高速缓存标志,实际地址保存在kmem_cache_node结构中
    unsigned int size; //对象长度 + 填充字节
    
	struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */

	unsigned int flags;		/* constant flags */
    //每个slab的对象数量,同一个slab高速缓存中对象个数相同
	unsigned int num;		/* # of objs per slab */

/* 3) cache_grow/shrink */
	/* order of pgs per slab (2^n) */
	unsigned int gfporder;  //slab阶数

	/* force GFP flags, e.g. GFP_DMA */
	gfp_t allocflags;

	size_t colour;			/* cache colouring range */
	unsigned int colour_off;	/* colour offset */

    //空闲对象链表放在slab外部时使用,
    //管理用于slab对象管理结构中freelist成员的缓存,也就是又一个新缓存
	struct kmem_cache *freelist_cache;
	unsigned int freelist_size;  //空闲对象链表的大小

	/* constructor func */
	void (*ctor)(void *obj);

/* 4) cache creation/removal */
	const char *name;
	struct list_head list;
	int refcount;
	int object_size;  //对象原始长度
	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
    //每node链表,每个NUMA的结点都有该SLAB链表
	struct kmem_cache_node *node[MAX_NUMNODES];
};

每个内存节点对应一个kmem_cache_node实例

struct kmem_cache_node {
	spinlock_t list_lock;

	struct list_head slabs_partial;	//部分空闲链表
	struct list_head slabs_full; //完全用尽链表
	struct list_head slabs_free; //完全空闲链表
	unsigned long total_slabs;	// slab总数量
	unsigned long free_slabs;	// 空闲slab数量
	unsigned long free_objects; //高速缓存中空闲对象个数(包括slabs_partial、slabs_free链表中空闲对象)
	unsigned int free_limit; //高速缓存中空闲对象的上限
	unsigned int colour_next;	/* Per-node cache coloring */
	struct array_cache *shared;	// 结点上所有CPU共享的本地高速缓存
	struct alien_cache **alien;	// 其他结点上所有CPU共享的本地高速缓存
	unsigned long next_reap;	/* updated without locking */
	int free_touched;		/* updated without locking */

再来看一下整体的结构图
在这里插入图片描述
再贴一下array_cache代码

struct array_cache {
	unsigned int avail; //当前CPU上该slab可用对象数量
    //最大对象数目,和kmem_cache中一样
    //当本地对象缓冲池的空闲对象数目大于limit时就会主动释放batchcount个对象,便于内核回收和销毁slab
	unsigned int limit;
    //同kmem_cache,本地高速缓存预留数量,控制一次获得或转移对象的数量
    //表示当前CPU的本地对象缓冲池array_cache为空时,从共享的缓冲池或者slabs_partial/slabs_free列表中获取对象的数目。
	unsigned int batchcount;
	unsigned int touched; //标志位,从缓存获取对象时,touched置1,缓存收缩时, touched置0
	void *entry[];	/* 伪数组,使用时用于保存刚释放的对象
			 * Must have this definition in here for the proper
			 * alignment of array_cache. Also simplifies accessing
			 * the entries.
			 */
};

page结构体

struct page {
	/* First double word block */
    /* 标志位,每个bit代表不同的含义 */
	unsigned long flags;		/* Atomic flags, some possibly updated asynchronously */
	union {
		/*
		 * 如果mapping = 0,说明该page属于交换缓存(swap cache);当需要使用地址空间时会指定交换分区的地址空间swapper_space
		 * 如果mapping != 0,bit[0] = 0,说明该page属于页缓存或文件映射,mapping指向文件的地址空间address_space
		 * 如果mapping != 0,bit[0] != 0,说明该page为匿名映射,mapping指向struct anon_vma对象
		 */
		struct address_space *mapping;	
		void *s_mem;			/* slab first object */
	};

	/* Second double word */
	struct {
		union {
			pgoff_t index;		/* Our offset within mapping. */
			void *freelist;		/* sl[aou]b first free object */
			bool pfmemalloc;	
		};

		union {
#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \
	defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)
			/* Used for cmpxchg_double in slub */
			unsigned long counters;
#else
			/*
			 * Keep _count separate from slub cmpxchg_double data.
			 * As the rest of the double word is protected by
			 * slab_lock but _count is not.
			 */
			unsigned counters;
#endif

			struct {

				union {
					/*
					 * 被页表映射的次数,也就是说该page同时被多少个进程共享。初始值为-1,如果只被一个进程的页表映射了,该值为0 。
					 * 如果该page处于伙伴系统中,该值为PAGE_BUDDY_MAPCOUNT_VALUE(-128),
					 * 内核通过判断该值是否为PAGE_BUDDY_MAPCOUNT_VALUE来确定该page是否属于伙伴系统。
					 */
					atomic_t _mapcount;

					struct { /* SLUB */
						unsigned inuse:16;/* 这个inuse表示这个page已经使用了多少个object              */
						unsigned objects:15;
						unsigned frozen:1;/* frozen代表slab在cpu_slub,unfroze代表在partial队列或者full队列 */
					};
					int units;	/* SLOB */
				};
				/* 
				 * 引用计数,表示内核中引用该page的次数,如果要操作该page,引用计数会+1,操作完成-1。
				 * 当该值为0时,表示没有引用该page的位置,所以该page可以被解除映射,这往往在内存回收时是有用的
				 */
				atomic_t _count;		/* Usage count, see below. */
			};
			unsigned int active;	/* SLAB */
		};
	};

	/* Third double word block */
	union {
		/*
		 * page处于伙伴系统中时,用于链接相同阶的伙伴(只使用伙伴中的第一个page的lru即可达到目的)
		 * 设置PG_slab, 则page属于slab,page->lru.next指向page驻留的的缓存的管理结构,page->lru.prec指向保存该page的slab的管理结构
         * page被用户态使用或被当做页缓存使用时,用于将该page连入zone中相应的lru链表,供内存回收时使用
		 */
		struct list_head lru;	/* Pageout list, eg. active_list
					 * protected by zone->lru_lock !
					 * Can be used as a generic list
					 * by the page owner.
					 */
		/* 用作per cpu partial的链表使用 */
		struct {		/* slub per cpu partial pages */
			struct page *next;	/* Next partial slab */
#ifdef CONFIG_64BIT
			int pages;	/* Nr of partial slabs left */
			int pobjects;	/* Approximate # of objects */
#else
			/*  */
			short int pages;
			short int pobjects;
#endif
		};

		struct slab *slab_page; /* slab fields */
		struct rcu_head rcu_head;	/* Used by SLAB
						 * when destroying via RCU
						 */
		/* First tail page of compound page */
		struct {
			compound_page_dtor *compound_dtor;
			unsigned long compound_order;
		};
	};

	/* Remainder is not double word aligned */
	union {
		/*
		 * 如果设置了PG_private标志,则private字段指向struct buffer_head
		 * 如果设置了PG_compound,则指向struct page
		 * 如果设置了PG_swapcache标志,private存储了该page在交换分区中对应的位置信息swp_entry_t
		 * 如果_mapcount = PAGE_BUDDY_MAPCOUNT_VALUE,说明该page位于伙伴系统,private存储该伙伴的阶
		 */
		unsigned long private;		
		struct kmem_cache *slab_cache;	/* SL[AU]B: Pointer to slab */
		struct page *first_page;	/* Compound tail pages */
	};

#ifdef CONFIG_MEMCG
	struct mem_cgroup *mem_cgroup;
#endif

	/*
	 * On machines where all RAM is mapped into kernel address space,
	 * we can simply calculate the virtual address. On machines with
	 * highmem some memory is mapped into kernel virtual memory
	 * dynamically, so we need a place to store that address.
	 * Note that this field could be 16 bits on x86 ... ;)
	 *
	 * Architectures with slow multiplication can define
	 * WANT_PAGE_VIRTUAL in asm/page.h
	 */
#if defined(WANT_PAGE_VIRTUAL)
	void *virtual;			/* Kernel virtual address (NULL if
					   not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */

#ifdef CONFIG_KMEMCHECK
	/*
	 * kmemcheck wants to track the status of each byte in a page; this
	 * is a pointer to such a status block. NULL if not tracked.
	 */
	void *shadow;
#endif

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
	int _last_cpupid;
#endif
}

计算slab长度

calculate_slab_order函数主要计算一个slab占用的页面个数,即计算阶数。简要介绍一下基本步骤:

  1. 如果阶数大于等于最大阶数,那么选择这个阶数。尽量选择低的阶数,是因为申请高阶页块成功的概率较低。
  2. 如果剩余长度小于等于slab长度的八分之一,就选择这个阶数。
static inline size_t calculate_slab_order(struct kmem_cache *cachep, size_t size, size_t align, unsigned long flags)
{
    size_t left_over = 0;
    int gfporder;

    //首先从order为0开始尝试,直到最大order(MAX_GFP_ORDER)为止
    for (gfporder = 0 ; gfporder <= MAX_GFP_ORDER; gfporder++) 
    {
        unsigned int num;
        size_t remainder;
        //调用cache计算函数,如果该order放不下一个size大小的object,即num为0,表示order太小,需要调整。
        //计算一个slab中的object的数目和slab剩余的字节数
        cache_estimate(gfporder, size, align, flags, &remainder, &num);
        if (!num)
            continue;
        //如果Slab管理区没有和对象存放在一起,并且该order存放对象数量大于每个Slab允许存放最大的对象数量,则返回。主要针对大内存对象
        if ((flags & CFLGS_OFF_SLAB) && num > offslab_limit)
            break;
        
        //找到了合适的order,将相关参数保存。
        //Slab中的对象数量
        cachep->num = num;
        //order值
        cachep->gfporder = gfporder;
        //Slab中的碎片大小
        left_over = remainder;
        //SLAB_RECLAIM_ACCOUNT:内核检查用户态程序有没有足够内存可用,被标记为这个标志的Slab将作为通缉对象。
        if (flags & SLAB_RECLAIM_ACCOUNT)
            break;
        //slab_break_gfp_order 定义为0
        //一个slab按照最小的页面数计算,比如不超过4KB的对象,每次分配slab只需要一页即可
        if (gfporder >= slab_break_gfp_order)
            break;
        //Slab碎片的8倍大小还是小于Slab大小,那么这样的碎片是可以接受的。
        if ((left_over * 8) <= (PAGE_SIZE << gfporder))
            break;
    }
    return left_over;
}

着色

slab是一个或多个连续的物理页,起始地址总是页长度的整数倍,不同slab中相同偏移的位置在处理器一级缓存中的索引相同。如果slab的剩余部分的长度超过一级缓存行的长度,剩余部分对应的一级缓存行没有被利用;如果对象的填充字节的长度超过一级缓存行的长度,填充字节对应的一级缓存行没有被利用。这两种情况导致处理器的某些缓存行需要被反复刷新,而其余的缓存行却未被充分利用。
那么,slab着色是怎么回事呢?其实就是利用slab中的空余空间去做不同的偏移,这样就可以根据不同的偏移来区分size相同的对象了。
为什么slab中会有剩余空间?因为slab是以空间换时间。
做偏移的时候,也要考虑到缓存行中的对齐。
假如slab中有14个字节的剩余空间,cache line以4字节对齐,我们来看看有多少种可能的偏移,也就是有多少种可能的颜色。
第一种,偏移为0,也就是不偏移,那么剩余的14个字节在哪儿呢?放到结尾处,作为偏移补偿。
第二种,偏移4字节,此时偏移补偿为10字节。
第三种,偏移8字节,此时偏移补偿为6字节。
第四种,偏移12字节,此时偏移补偿为2字节。
再继续,就只能回归不偏移了,因为上一种的偏移补偿为2字节,已经不够对齐用了。
来总结一下看看有几种颜色。
第一种无偏移,后面是剩余空间 free 能满足多少次对齐 align ,就有多少种,总数: free/align +1 。
如果 free 小于 align ,slab着色就起不到作用,因为颜色只有一种,即不偏移的情况。
如果size相同的对象很多,但 free 不够大,或者 free/align 不够大,效果也不好。
因为颜色用完了,会从头再来。
继续上面的例子,如果有五个相同的对象,第五个对象的颜色与第一个相同,偏移为0。
这样一来,让一个slab移到缓存行b上面,另一个slab还是在原来的缓存行a上,两块数据分别可以在缓存行的a和b行上面进行读取,那么我们读取数据的时候就不会造成不必要的数据交换了。

每处理器数组缓存

内存缓存为每个处理器创建一个数组缓存(结构体array_cahce)。释放对象时,把对象存放到当前处理器对应的数组缓存中;分配对象的时候,先从当前处理器的数组缓存分配对象,采用后进先出(Last In First Out,LIFO)的原则,能保证本地cpu最近释放该slab cache的obj立马被下一个slab内存申请者获取到(有很大概率此obj仍然在本地cpu的硬件高速缓存中),这种做可以提高性能。
在这里插入图片描述
array_cache中的entry空数组,就是用于保存本地cpu刚释放的obj,所以该数组初始化时为空,只有本地cpu释放slab cache的obj后才会将此obj装入到entry数组。array_cache的entry成员数组中保存的obj数量是由成员limit控制的,超过该限制后会将entry数组中的batchcount个obj迁移到对应节点cpu共享的空闲对象链表中。
分配对象的时候,先从当前处理器数组缓存分配对象,如果数组缓存为空,就批量分配对象,重新填充数组缓存,批量的值是在数组缓存的成员中;释放对象时,如果数组缓存是满的,就先把数组缓存中的对象批量还给slab(因为是由slab进行分配的),批量的值就是数组缓存的一个成员,然后把正在释放的对象存到数组缓存中。

slab分配器支持NUMA体系结构

内存缓存针对每个内存节点创建一个kmem_cache_node实例。
在这里插入图片描述
slab缓存会为不同的节点维护一个自己的slab链表,用来缓存和管理自己节点的slab obj,这通过kmem_cache中node数组成员来实现,node数组中的每个数组项都为其对应的节点分配了一个struct kmem_cache_node结构。
struct kmem_cache_node结构定义的变量是一个每node变量。相比于struct array_cache定义的每cpu变量,kmem_cache_node管理的内存区域粒度更大,因为kmem_cache_node维护的对象是slab,而array_cache维护的对象是slab中的obj(一个kmem_cache可能包含一个或多个slab,而一个slab中可能包含一个或多个slab obj)。
struct kmem_cache_node对于本节点中slab的管理主要分了3个链表:slabs_partial(部分空闲slab链表),非空闲slab链表(slabs_full)和全空闲slab链表(slabs_free)。struct kmem_cache_node还会将本地节点中需要节点共享的slab obj缓存在它的shared成员中。若本地节点向访问其他节点贡献的slab obj,可以利用struct kmem_cache_node中的alien成员去获取。
struct kmem_cache_node中成员share指向共享数组缓存,alien指向远程节点数组缓存,每个节点都有一个alien。这两个成员用来分阶段释放从其他节点进入的对象,先释放到alien,再释放到share,最后再到远程节点的slab。
分配和释放本地内存节点的对象时,也会使用共享数组缓存。分配对象时,如果当前处理器的数组缓存是空的,共享数组缓存里面的对象可以用于重填;释放对象时,如果当前处理器的数组缓存是满的,并且共享数组缓存有空闲空间,可以转移一部分对象到共享数组缓存,不需要把对象批量还给slab,然后把正在释放的对象添加到当前处理器的数组缓存中。

回收内存

对于所有对象空闲的slab,没有立即释放,而是放在空闲slab链表中。只有内存节点上空闲对象的数量超过限制,才开始回收空闲slab,直到空闲对象的数量小于或等于限制。
结构体kmem_cache_node的成员slabs_free是空闲slab链表的头节点,成员free_objects是空闲对象的数量,成员free_limit是空闲对象的数量限制。
此外,每个处理器每隔2秒针对内存缓存执行:

  • 回收节点对应的远程节点数组缓存中的对象
  • 如果过去2秒没有从当前处理器的数组缓存中分配对象,那么回收数组缓存中的对象

不连续页分配器

当设备长时间运行后,内存碎片化,很难找到连续的物理页。在这种情况下,如果需要分配长度超过一页的内存块,可以使用不连续页分配器,分配虚拟地址连续但是物理地址不连续的内存块。在32位系统中不连续分配器还有一个好处:优先从高端内存区域分配页,保留稀缺的低端内存区域。

数据结构

不连续页分配器的数据结构关系如下
在这里插入图片描述
每个虚拟内存区域对应一个vmap_area实例,每个vmap_area实例关联一个vm_struct实例。看代码

struct vm_struct {
	struct vm_struct	*next;  //指向下一个vm_struct实例
	void			*addr;  //起始的虚拟地址
	unsigned long		size;  //虚拟地址长度
	unsigned long		flags;  //标志
	struct page		**pages;  //page的指针数组
	unsigned int		nr_pages;  //页数
	phys_addr_t		phys_addr;  //起始物理地址
	const void		*caller;
};

struct vmap_area {
    //起始和结束的虚拟地址
	unsigned long va_start;
	unsigned long va_end;

	unsigned long flags;  //标志位
	struct rb_node rb_node;         /* address sorted rbtree */
    //指向vm_struct的链表(按虚拟地址从小到大排序)
	struct list_head list;          /* address sorted list */
	struct llist_node purge_list;    /* "lazy purge" list */
	struct vm_struct *vm;
	struct rcu_head rcu_head;
};

函数接口

主要有以下几个重接口:

  • vmalloc:分配不连续的物理页并且把物理页映射到连续的虚拟地址空间
  • vfree:释放vmalloc分配的物理页和虚拟地址空间
  • vmap:把已经分配的不连续物理页映射到连续的虚拟地址空间
  • vunmap:释放使用vmap分配的虚拟地址空间

这里主要介绍一下vmalloc
vmalloc最终会调用__vmalloc_node_range,并把VMALLOC_START和VMALLOC_END传入,该函数是vmalloc的主要实现,用来从(start, end)中申请一段大小为size的虚拟地址空间,并给这块虚拟地址空间申请物理内存(基本是不连续的),并写入页表。

static void *__vmalloc_node(unsigned long size, unsigned long align,
                gfp_t gfp_mask, pgprot_t prot, int node, const void *caller)
{
    return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
                gfp_mask, prot, 0, node, caller);
}

static inline void *__vmalloc_node_flags(unsigned long size, int node, gfp_t flags)
{
    return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
                    node, __builtin_return_address(0));
}

/**
 *    vmalloc  -  allocate virtually contiguous memory
 *    @size:        allocation size
 *    Allocate enough pages to cover @size from the page level
 *    allocator and map them into contiguous kernel virtual space.
 *
 *    For tight control over page level allocator and protection flags
 *    use __vmalloc() instead.
 */
void *vmalloc(unsigned long size)
{
    return __vmalloc_node_flags(size, NUMA_NO_NODE, GFP_KERNEL | __GFP_HIGHMEM);
}
EXPORT_SYMBOL(vmalloc);

从上面可以看出,vmalloc虚拟地址空间的范围是(VMALLOC_START,VMALLOC_END),每种处理器架构都需要定义这两个宏。如ARM64架构定义宏如下

/*
 * VMALLOC range.
 *
 * VMALLOC_START: beginning of the kernel vmalloc space
 * VMALLOC_END: extends to the available space below vmmemmap, PCI I/O space
 *	and fixed mappings
 */
/* MODULES_END内核模块区域的结束地址
 * PAGE_OFFSET线性映射区域的起始地址
 * PUD_SIZE页目录上层表项映射的地址空间长度
 * VMEMMAP_SIZE是vmemap区域的长度
 * VMALLOC_END: extends to the available space below vmmemmap, PCI I/O space
 *
 */
#define VMALLOC_START		(MODULES_END)
#define VMALLOC_END		(PAGE_OFFSET - PUD_SIZE - VMEMMAP_SIZE - SZ_64K)

vmalloc函数执行过程:分配虚拟内存区域、分配物理页、在内核的页表中把虚拟页映射到物理页。
具体介绍一下。vmalloc函数分配的单位是页,如果请求分配的长度不是页的整数倍,就把长度向上对齐到页的整数倍。一般情况下,建议在申请内存长度超过一页时,一定要使用vmalloc函数,因为它的执行非常简单:

  1. 分配虚拟内存区域:在vm_struct实例和vmap_area实例中遍历已存在的对应vmap_area实例,在两个实例之间找到一个足够大的空洞。如果找到,就把起始地址和结束地址保存到新的地址空间中,然后把新的vmap_area加入到红黑树或链表中,最后把新的实例关联到vm_struct中。
  2. 分配物理页:从页分配器中分配一个物理页,把物理地址存放到pages指针数组中,再给nr_pages加1。
  3. 在内核的页表中把虚拟页映射到物理页:将物理内存与虚拟内存一一建立映射,然后返回虚拟起始地址给用户,完成分配动作。

最后说一下进程内核页表的更新。从进程被创建时,除了复制一份父进程的页表还会复制一份内核页表。而内核页表必须保证每个进程使用的都是一样的,在 vmalloc 中,已经触发了内核页表的修改(所谓内核页表就是 init 进程的 mm_struct),那其他进程如何同步呢?

答案是通过缺页中断来实现延时同步。因为当其他进程想要访问新映射的 vmalloc 区虚拟地址时,由于其内核页表没更新,因此会匹配不到,触发缺页中断。进而此时再将修改的内核页表同步到该进程中即可。意味着如果没有使用到这部分地址,也不会触发内核页表同步。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值