Linux内核之内存管理知识以及伙伴系统

一、Linux 内核架构图

在这里插入图片描述
在这里插入图片描述

二、虚拟内存地址空间布局

2.1、用户空间

应用程序使用 malloc()申请内存,使用 free()释放内存,malloc()/free() 是 glibc 库的内存分配器 ptmalloc 提供的接口,ptmalloc 使用系统调用 brk/mmap 向内核以页为单位申请内存,然后划分成小内存块分配给用户应用程序。

2.2、内核空间

内核空间的基本功能:虚拟内存管理负责从进程的虚拟地址空间分配虚拟页,sys_brk 用来扩大或收缩堆,sys_mmap 用来在内存映射区域分配虚拟页, sys_munmap 用来释放虚拟页。

页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。内核空间提供把页划分成小内存块分配的块分配器,提供分配内存的接口 kmalloc()和 释放内存接口 kfree()。

块分配器:SLAB/SLUB/SLOB。

2.3、硬件层面

处理器包含一个称为内存管理单元(Memory Management Unit,MMU)的部 件,负责把虚拟地址转换成物理地址。内存管理单元包含一个称为页表缓存 (Translation Lookaside Buffer,TLB)的部件,保存最近使用的页表映射, 避免每次把虚拟地址转换物理地址都需要查询内存中的页表。

2.4、虚拟地址空间划分

以 ARM64 处理器为例:虚拟地址的最大宽度是 48 位。

  • 内核虚拟地址在 64 位地址空间顶部,高 16 位全是 1,范围是[0xFFFF 0000 0000 0000,0xFFFF FFFF FFFF FFFF] 。
  • 用户虚拟地址在 64 位地址空间的底部,高 16 位全是 0, 范围是[0x0000 0000 0000 0000,0x0000 FFFF FFFF FFFF]。
    在这里插入图片描述

在编译 ARM64 架构的 Linux 内核时,可以选择虚拟地址宽度:

  • 选择页长度 4KB,默认虚拟地址宽度为 39 位;
  • 选择页长度 16KB,默认虚拟地址宽度为 47 位;
  • 选择页长度 64KB,默认虚拟地址宽度为 42 位;
  • 选择 48 位虚拟地址。

在 ARM64 架构 linux 内核中,内核虚拟地址用户虚拟地址宽度相同。所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。

2.5、用户虚拟地址空间布局

进程的用户虚拟地址空间的起始地址是 0,长度是 TASK_SIZE,由每种处理器架构定义自己的宏 TASK_SIZE。ARM64 架构定义宏 TASK_SIZE 如下所示:

  • 32 位用户空间程序:TASK_SIZE 的值是 TASK_SIZE_32,即 0x10000000,等于 4GB。
  • 64 位用户空间程序:TASK_SIZE 的值是 TASK_SIZE_64,即 2 的 VA_BITS 次方字节,VA_BITS 是编译内核时选择的虚拟地址位数。

(arch/arm64/include/asm/memory.h)

/*
 * PAGE_OFFSET - the virtual address of the start of the linear map, at the
 *               start of the TTBR1 address space.
 * PAGE_END - the end of the linear map, where all other kernel mappings begin.
 * KIMAGE_VADDR - the virtual address of the start of the kernel image.
 * VA_BITS - the maximum number of bits for virtual addresses.
 */
#define VA_BITS			(CONFIG_ARM64_VA_BITS)
#define _PAGE_OFFSET(va)	(-(UL(1) << (va)))
#define PAGE_OFFSET		(_PAGE_OFFSET(VA_BITS))
#define KIMAGE_VADDR		(MODULES_END)
#define BPF_JIT_REGION_START	(KASAN_SHADOW_END)
#define BPF_JIT_REGION_SIZE	(SZ_128M)
#define BPF_JIT_REGION_END	(BPF_JIT_REGION_START + BPF_JIT_REGION_SIZE)
#define MODULES_END		(MODULES_VADDR + MODULES_VSIZE)
#define MODULES_VADDR		(BPF_JIT_REGION_END)
#define MODULES_VSIZE		(SZ_128M)
#define VMEMMAP_START		(-VMEMMAP_SIZE - SZ_2M)
#define PCI_IO_END		(VMEMMAP_START - SZ_2M)
#define PCI_IO_START		(PCI_IO_END - PCI_IO_SIZE)
#define FIXADDR_TOP		(PCI_IO_START - SZ_2M)

#if VA_BITS > 48
#define VA_BITS_MIN		(48)
#else
#define VA_BITS_MIN		(VA_BITS)
#endif

#define _PAGE_END(va)		(-(UL(1) << ((va) - 1)))

#define KERNEL_START		_text
#define KERNEL_END		_end

Linux 内核使用内存描述符 mm_struct 描述进程的用户虚拟地址空间,主要核心成员如下:

// include/linux/mm_types.h

struct mm_struct {
    
    
	struct {
    
    
		struct vm_area_struct *mmap;//		虚拟内存区域的链表指针
		struct rb_root mm_rb;       // 虚拟内存区域红黑树       
		u64 vmacache_seqnum;                   /* per-thread vmacache */
/*通过在内存映射区域查找一个没有映射的区域*/
#ifdef CONFIG_MMU
		unsigned long (*get_unmapped_area) (struct file *filp,
		unsigned long addr, unsigned long len,
		unsigned long pgoff, unsigned long flags);
#endif
                unsigned long mmap_base;//内存映射区域的起始地址
                // .......
                unsigned long task_size;//表示用户虚拟地址空间的长度大小
               // ......
                pgd_t * pgd;//指向页全局目录(第一级页表)
               // ......
		/**
		 * @mm_users: The number of users including userspace.
		 *
		 * Use mmget()/mmget_not_zero()/mmput() to modify. When this
		 * drops to 0 (i.e. when the task exits and there are no other
		 * temporary reference holders), we also release a reference on
		 * @mm_count (which may then free the &struct mm_struct if
		 * @mm_count also drops to 0).
		 */
		atomic_t mm_users;//共享同一个用户虚拟地址空间的进程数量。

		/**
		 * @mm_count: The number of references to &struct mm_struct
		 * (@mm_users count as 1).
		 *
		 * Use mmgrab()/mmdrop() to modify. When this drops to 0, the
		 * &struct mm_struct is freed.
		 */
		atomic_t mm_count;//引用计数器
                // ......
                spinlock_t arg_lock; /* protect the below fields */
		unsigned long start_code, end_code, start_data, end_data;//代码段、数据段的起始地址和结束地址
		unsigned long start_brk, brk, start_stack;//栈、堆的起始地址和结束地址
		unsigned long arg_start, arg_end, env_start, env_end;//参数字符串、环境变量的起始地址和结束地址
               // ......
                /* Architecture-specific MM context */
		mm_context_t context;//处理器架构的特殊内存管理上下文操作
               // ......
        } __randomize_layout;
        /*
	 * The mm_cpumask needs to be at the end of mm_struct, because it
	 * is dynamically sized based on nr_cpu_ids.
	 */
	unsigned long cpu_bitmap[];
}

mm和mm_user的关系
一个进程的用户虚拟地址空间包括区域:代码段、动态库的代码段、数据段、未初始化数据段、代码段、未初始化代码段、存放在栈底部的环境变量、参数字符串等等。
在这里插入图片描述

2.6、进程的进程描述和内存描述符关系

在这里插入图片描述

2.7、内核地址空间布局

ARM64 处理器架构的内核地址空间布局如下:
在这里插入图片描述
用户空间(256T)、内核空间(256T)、module区域(128M)、PCI I/O区域(16M)、vmalloc区域(123T左右)、vmemmap区域(4096G)。

KASAN影子区域:内核地址的消毒,是一个动态内存错误的检查工具。

三、SMP/NUMA 架构

3.1、SMP

SMP(Symmetric Multi-processing)对称多处理器结构。比如服务器中多个 CPU 对称运行,没有主次和从属关系,CPU 共享相同的物理内存。SMP 也称为一致存储器访问结构(UMA:Uniform Memory Access)。
在这里插入图片描述
SMP架构的处理器是对等的,通过总线连接到同一块物理内存或者同一块IO设备,这样就导致CPU、内存、IO等都是共享的;从而存在一个缺点:扩展性低。CPU的性能和有效性受总线影响,所以,SMP服务器的CPU在两个到四个之间是性能最优的。

3.2、NUMA

NUMA(Non Uniform Memory Access)非统一内存访问。内存访问时间取决于处理器的内存位置。在 NUMA 下,处理器访问它自己的本地存储器的速度比非本地存储器(存储器的地方到另一个处理器之间共享的处理器或存储器)快一些。
在这里插入图片描述
NUMA也存在一些问题:node之间资源交互会慢些,当CPU很多的情况下,它的性能提升并不是很高。

NUMA服务器适合做单点服务器,2到4个node效果是最好的。

NUMA系统提供内存互联,是一种新型动态的分析系统(MP\MMP)。

四、伙伴系统及算法

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

4.1、基本伙伴分配器

连续的物理页称为页块(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 的整数倍。

4.2、分区伙伴分配器

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

内存区域数据结构分析如下:

(include/linux/mmzone.h)

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

(include/linux/mmzone.h)

struct zone {
    
    //内存区域数据结构
	/* Read-mostly fields */

	/* zone watermarks, access with *_wmark_pages(zone) macros */
	unsigned long _watermark[NR_WMARK];//页分配器使用的水线
	unsigned long watermark_boost;

	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.
	 */
	atomic_long_t		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 */
	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];
	unsigned long		compact_init_migrate_pfn;
	unsigned long		compact_init_free_pfn;
#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];
	atomic_long_t		vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

区域水线数据结构分析:(include/linux/mmzone.h)

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

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

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

五、块分配器(Slab/Slub/Slob)

5.1、基本概念

Buddy 提供以 page 为单位的内存分配接口,这对内核来说颗粒度还太大,所以需要一种新的机制,将 page 拆分为更小的单位来管理。

Linux 中支持的主要有:slab、slub、slob。其中 slob 分配器的总代码量比较少,但分配速度不是最高效的,所以不是为大型系统设计,适合内存紧张的嵌入式系统。

5.2、slab 块分配器原理

slab 分配器的作用不仅仅是分配小块内存,更重要的作用是针对经常分配和释放的对象充当缓存。slab 分配器的核心思路是:为每种对象类型创建一个内存缓存,每个内存缓存由多个大块组成,一个大块是由一个或多个连续的物理页,每个大块包含多个对象。slab 采用面向对象的思想,基于对象类型管理内存,每种对象被划分为一类,比如进程描述符 task_struct 是一个类,每个进程描述符实例是一个对象。如下图所示为内存缓存的组成结构:
在这里插入图片描述
slab 分配器在某些情况下表现不太优先,所以 Linux 内核提供两个改进的块分配器。

  • 在配备大量物理内存的大型计算机上,slab分配器的管理数据结构的内存开销比较大,所以设计了slub分配器。
  • 在小内存的嵌入式设备上,slab 分配器的代码过多、相当复杂,所以设计一个精简 slob 分配器。

目前 slub 分配器已成为默认的块分配器。

5.3、计算 slab 长度及着色

(1)计算 slab:函数 calculate_slab_order 负责计算 slab 长度,从 0 阶到kmalloc()函数支持最大除数 KMALLOC_MAX_ORDER。

(mm/slab.c)

/**
 * calculate_slab_order - calculate size (page order) of slabs
 * @cachep: pointer to the cache that is being created
 * @size: size of objects to be created in this cache.
 * @flags: slab allocation flags
 *
 * Also calculates the number of objects per slab.
 *
 * This could be made much more intelligent.  For now, try to avoid using
 * high order pages for slabs.  When the gfp() functions are more friendly
 * towards high-order requests, this should be changed.
 *
 * Return: number of left-over bytes in a slab
 */
static size_t calculate_slab_order(struct kmem_cache *cachep,
				size_t size, slab_flags_t flags)
{
    
    
	size_t left_over = 0;
	int gfporder;

	for (gfporder = 0; gfporder <= KMALLOC_MAX_ORDER; gfporder++) {
    
    
		unsigned int num;
		size_t remainder;

		num = cache_estimate(gfporder, size, flags, &remainder);
		if (!num)
			continue;

		/* Can't handle number of objects more than SLAB_OBJ_MAX_NUM */
		if (num > SLAB_OBJ_MAX_NUM)
			break;

		if (flags & CFLGS_OFF_SLAB) {
    
    
			struct kmem_cache *freelist_cache;
			size_t freelist_size;

			freelist_size = num * sizeof(freelist_idx_t);
			freelist_cache = kmalloc_slab(freelist_size, 0u);
			if (!freelist_cache)
				continue;

			/*
			 * Needed to avoid possible looping condition
			 * in cache_grow_begin()
			 */
			if (OFF_SLAB(freelist_cache))
				continue;

			/* check if off slab has enough benefit */
			if (freelist_cache->size > cachep->size / 2)
				continue;
		}

		/* Found something acceptable - save it away */
		cachep->num = num;
		cachep->gfporder = gfporder;
		left_over = remainder;

		/*
		 * A VFS-reclaimable slab tends to have most allocations
		 * as GFP_NOFS and we really don't want to have to be allocating
		 * higher-order pages when we are unable to shrink dcache.
		 */
		if (flags & SLAB_RECLAIM_ACCOUNT)
			break;

		/*
		 * Large number of objects is good, but very large slabs are
		 * currently bad for the gfp()s.
		 */
		if (gfporder >= slab_max_order)
			break;

		/*
		 * Acceptable internal fragmentation?
		 */
		if (left_over * 8 <= (PAGE_SIZE << gfporder))
			break;
	}
	return left_over;
}

(2)着色:
slab 是一个或多个连续的物理页,起始地址总是页长度的整数倍,不同slab 中相同偏移的位置在处理器一级缓存中的索引相同。如果 slab 的剩余部分的长度超过一级缓存行的长度,剩余部分对应的一级缓存行没有被利用;如果对象的填充字节的长度超过一级缓存行的长度,填充字节对应的一级缓存行没有被利用。这两种情况导致处理器的某些缓存行被过度使用,另一些缓存行很少使用。

5.4、每处理器数组缓存

内存缓存为每个处理器创建一个数组缓存(结构体 array_cache)。释放 对象时,把对象存放到当前处理器对应的数组缓存中;分配对象的时候,先从当前处理器的数组缓存分配对象,采用后进先出(Last In First Out,LIFO) 的原则,这种做可以提高性能。
在这里插入图片描述

// include/linux/slab_def.h

struct kmem_cache {
    
    
	struct array_cache __percpu *cpu_cache;

/* 1) Cache tunables. Protected by slab_mutex */
	unsigned int batchcount;
	unsigned int limit;
	unsigned int shared;

	unsigned int size;
	struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */

	slab_flags_t flags;		/* constant flags */
	unsigned int num;		/* # of objs per slab */

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

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

	size_t colour;			/* cache colouring range */
	unsigned int colour_off;	/* colour offset */
	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;

	/*
	 * 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, while 'obj_offset'
	 * and 'object_size' 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

	unsigned int useroffset;	/* Usercopy region offset */
	unsigned int usersize;		/* Usercopy region size */

	struct kmem_cache_node *node[MAX_NUMNODES];
};

5.5、slab 分配器支持 NUMA 体系结构

内存缓存针对每个内存节点创建一个 kmem_cache_node 实例视图如下:

在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Long_xu/article/details/129417361