Linux内核设计与实现 第12章 内存管理


内核空间和用户空间不同。

页(Pages)

内核把物理页作为内存管理的基本单位。
内存管理单元MMU通常以为单位进行处理,以页大小为单位管理系统中的页表,从虚拟内存的角度看页就是最小单位。

体系结构不同,支持也大小不尽相同。通常32位体系支持4KB的页,64位体系支持8KB的页。如 在支持4KB页大小有1GB物理内存的机器上,物理内存将被划分为262144个页,
(1 * 1024 * 1024 / 4 = 262144)。

内核用结构struct page表示系统中的每个物理页,在linux/mm_types.h中,简化后,

struct page {
	unsigned long flags;
	atomic_t _count;
	atomic_t _mapcount;
	unsigned long private;
	struct address_space *mapping;
	pgoff_t index;
	struct list_head lru;
	void *virtual;
};
  • flags存放页状态,是否脏页,是否被锁定,每个位表示一个状态,至少32个状态,这些- 状态定义在linux/page-flags.h。
  • _count引用计数,-1时表示当前内核没有引用该页,内核应用page_count()检查页引用状态。
  • virtual页的虚拟地址,通常就是页在虚拟内存中的地址,高端内存并不永久映射到内核空间。

page结构与物理页相关,并非与虚拟地址相关。page对页的描述是短暂的,目的在于描述物理页本身而不是所包含的数据。
系统为每个物理页分配page结构,但是开销并不算大。假设一个page占40字节,系统物理页大小为8KB,物理内存4GB,那只需要20M空间就可以管理这524,288个物理页。

区(Zones)

由于硬件限制,内核不能对所有页一视同仁,有些页位于内存中特定的物理地址,不能将其用于某些特定任务。因此内核把页划分为不同的区。
Linux必须处理两种由于硬件存在缺陷而引起的寻址问题:

  • 一些硬件只能访问特定的内存地址来执行DMA(直接内存访问)
  • 一些体系结构的物理寻址范围比虚拟寻址范围大很多,有一些内存不能永久映射到内核空间。

linux主要使用4种区,在linux/mmzone.h中定义:

  • ZONE_DMA此区包含的页能用来执行DMA操作
  • ZONE_DMA32与ZONE_DMA类似,但只能被32位设备访问
  • ZONE_NORMAL此区包含正常映射的页
  • ZONE_HIGHMEM此区包含高端内存,其中某些页不能永久映射到内核空间
    在这里插入图片描述
    区的划分没有任何物理意义,这只是内核为管理物理页而进行的逻辑分组。

内核用结构struct zone表示每个区,在linux/mmzone.h中定义,这些结构体很大,但是只有3个区,

struct zone {
	unsigned long watermark[NR_WMARK];
	unsigned long lowmem_reserve[MAX_NR_ZONES];
	struct per_cpu_pageset pageset[NR_CPUS];
	spinlock_t lock;
	struct free_area free_area[MAX_ORDER]
	spinlock_t lru_lock;
	struct zone_lru {
	struct list_head list;
	unsigned long nr_saved_scan;
	} lru[NR_LRU_LISTS];
	struct zone_reclaim_stat reclaim_stat;
	unsigned long pages_scanned;
	unsigned long flags;
	atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
	int prev_priority;
	unsigned int inactive_ratio;
	wait_queue_head_t *wait_table;
	unsigned long wait_table_hash_nr_entries;
	unsigned long wait_table_bits;
	struct pglist_data *zone_pgdat;
	unsigned long zone_start_pfn;
	unsigned long spanned_pages;
	unsigned long present_pages;
	const char *name;
};
  • lock自旋锁,防止该结构被并发访问
  • watermark数组持有该区的最小值、最低和最高水位值,随空闲内存多少而变化
  • name以NULL结束的字符串表示该区名字, 三个区名字分别为DMA、Normal、HighMem

获得页 以页为单位分配

内核通过页、区等管理内存,通过接口在内核内分配和释放内存的。
内核提供请求内存的底层机制,提供了相应接口这些接口都以页为单位分配内存,定义在linux/gfp.h,最核心的函数是,

// 分配2^order个连续物理页,返回指向第一个page的指针,出错返回NULL
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order);

// 获取某个页的逻辑地址
void * page_address(struct page *page);

// 直接分配连续物理页 并 返回首页 逻辑地址
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

// 如果只需要一页
struct page * alloc_page(gfp_t gfp_mask);
unsigned long __get_free_page(gfp_t gfp_mask);

获得填充为0的页

所分配的页是给用户空间用时,需要把所有数据清0或其他处理,保障系统安全。

// 分配页,填充0
unsigned long get_zeroed_page(unsigned int gfp_mask)

在这里插入图片描述

释放页

释放需谨慎,只能释放自己的页,传递错误页可能导致系统崩溃,内核完全信赖自己,与用户空间不同。

void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr, unsigned int order)
void free_page(unsigned long addr)

kmalloc() (内存物理地址连续)以字节为单位分配,

与用户空间的malloc()很类似,多了一个flag参数。相对页分配,kmalloc()用的更多。在linux/slab.h中声明,

void * kmalloc(size_t size, gfp_t flags)

所分配的内存在物理上是连续的,所有虚拟地址肯定连续。 必须检查kmalloc()的返回值,如果是NULL必须处理错误。

struct dog *p;
p = kmalloc(sizeof(struct dog), GFP_KERNEL);
if (!p)
	/* handle error ... */

gfp_mask 标志

在页分配和kmalloc分配内存中都会用到分配器标志。
这些标志可分为三类:行为修饰符、区修饰符、类型

  • 行为修饰符,表示内核应该如何分配内存,特殊情况只能用特定行为修饰符。如在中断处理程序中分配内存要求不能休眠。
  • 区修饰符,从哪儿分配内存。
  • 类型修饰符,组合了行为修饰符 和 区修饰符。

这些修饰符都在linux/gfp.h中,通常包含头文件linux/slab.h就可以了,一般只用类型修饰符就够了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • GFP_KERNEL是一种类型修饰符,进程上下文中代码可以使用它。这种分配可能引起睡眠,使用的是普通优先级。因为可能阻塞,因此这个标志只用在可以重新安全调度的进程上下文中(没有锁被持有等情况)。
  • GFP_ATOMIC是一种类型修饰符,与上面截然相反。不能睡眠。在当前代码不能睡眠时只能用它。如中断处理程序、软中断和tasklet。
  • GFP_DMA 是一种类型修饰符,常用前两种。分配器从ZONE_DMA分配,用在需要DMA的内存的设备驱动程序中。一般与GFP_ATOMIC或GFP_KERNEL结合使用。

gfp_mask 使用总结

在这里插入图片描述

kfree() (与 kmalloc()对应)

必须配对使用,否则内存泄漏或bug,都在linux/slab.h中,

void kfree(const void *ptr)

vmalloc() (内存虚拟地址连续)

工作方式与kmalloc()类似,但是vmalloc()分配的虚拟地址连续,而物理地址无需连续。这也是用户空间的内存分配函数malloc()的工作方式,通过分配非连续的物理内存块,再修正页表,把内存地址映射到逻辑地址空间的连续区域中。
vmalloc()必须专门建立页表项,获取的页必须一个个映射,这就导致比直接内存映射更多的TLB抖动,所以性能没有kmalloc()好。
TLB(translation lookaside buffer),是一种硬件缓冲区,很多体系结构用它缓冲 虚拟地址到物理地址的映射关系,极大的提高了系统性能。

大多数情况下,只有硬件设备需要连续的物理内存地址空间。很多体系结构硬件设备存在于MMU之外,没有虚拟地址的概念。

什么时候用vmalloc()

为了获得大块内存时,如模块被动态插入到内核时,内核把模块装载到由vmalloc() 分配的内存上。

声明在linux/vmalloc.h中,用法与kmalloc()相同。函数可能睡眠不能在中断上下文中调用,也不能在其他不允许阻塞的代码中调用。vfree也是。

void * vmalloc(unsigned long size)

void vfree(const void *addr)

slab层

slab分配器 扮演了通用数据结构缓存层 的角色。

slab层产生原因

分配和释放数据结构是内核中最普遍的操作,为便于数据的频繁分配和回收,常常用到空闲链表,它是可供使用已经分配好的数据结构块,需要使用数据结构实例时直接拿过来用而不用分配内存,用完再放回空闲链表就行了,相当于快速存储频繁使用的对象类型。
而空闲链表的主要问题之一是不能全局控制,当内存吃紧时,内核无法使空闲链表让出内存,实际上内核根本就不知道有空闲链表。
为了弥补这一缺陷,也为了使代码更加稳固,Linux内核提供slab层。

slab层的设计

slab把不同的对象划分成所谓的高速缓存组,每个高速缓存组都存放不同类型的对象(被缓存的数据结构)。
slab由一个或者多个物理上连续的页组成,一般情况slab由一页组成。
如一个高速缓冲用于存放进程描述符(task_struct结构的一个空闲链表),另一个高速缓冲存放索引节点对象(struck inode)。kmalloc()接口建立在slab层上。
在这里插入图片描述
slab处于三种状态之一:满、部分满、空。
当内核某部分需要新对象时,先从部分满的slab中分配;如果没有部分满的slab就从空的slab中分配;如果空的slab没有就创建一个slab。

每个高速缓存都使用kmem_cache 结构表示,它包含三个链表,slabs_fullslabs_partialslabs_empty,这三个链表存放在kmem_list3结构内,这个结构在mm/slab.c定义,这些链表包含高速缓存中的所有slab。slab描述符struct slab用来描述每个slab,

struct slab {
	struct 			list_head list; /* full, partial, or empty list */
	unsigned long 	colouroff; 		/* offset for the slab coloring */
	void 			*s_mem; 		/* slab中的第一个对象 */
	unsigned int 	inuse; 			/* slab中已分配的对象数 */
	kmem_bufctl_t 	free; 			/* 第一个空闲对象 */
};

slab描述符要么在slab之外分配,要么就在slab自身开始的地方,如果slab很小或者slab内有足够的空间容纳slab描述符,那就放在里边。

slab层负责内存紧缺情况下所有底层的对齐、着色、分配、释放和回收等。如果要频繁创建很多相同类型的对象,就应该考虑使用slab高速缓存。不要自己去实现空闲链表。

在栈上的静态分配

用户空间可以轻松使用大栈,还可以在栈上为所欲为。而内核不能这么奢侈**,内核栈小而且固定**,给每个进程分配一个固定大小的小栈后,不仅可以减少内存的消耗,而且内核也无须负担太重的栈管理任务。
每个进程的内核栈大小依赖体系结构和编译器,通常是两页大小,也就是8KB或16KB。

任意的函数都必须尽量节省栈资源,只需在具体的函数中让局部变量所占空间之和不要超过几百字节。在栈上大量的静态分配是很危险的(如大型数组和结构体)。内核在内核栈管理中没有做足工作,栈溢出时会覆盖掉紧邻堆栈末端的东西。
因此进行动态分配是一种明智的选择。

高端内存映射

高端内存中的页不能永久的映射到内核地址空间上。

永久映射

在linux/highmem.h中声明的函数,在高低端内存都能用,如果page结构对应的是低端内存则返回该页虚拟地址,如果是高端内存,则会建立一个永久映射在返回地址。可以睡眠,只能用在进程上下文中。

void *kmap(struct page *page)

因为永久映射数量有限,当不再需要高端内存时应该接触映射,

void kunmap(struct page *page)

临时映射

当必须创建映射而当前上下文又不能睡眠时,内核提供临时映射(原子映射)。内核可以原子地把高端内存中的一个页映射到某个保留的映射中。可以用在不能睡眠的地方,获得此映射时绝不会阻塞。

建立临时映射,不会阻塞,因此可以用在中断上下文和其他不能重新调度的地方。它也禁止内核抢占。映射对于每个处理器都是唯一的,

void *kmap_atomic(struct page *page, enum km_type type)

参数type表示临时映射的目的,定义在asm-generic/kmap_types.h中,

enum km_type {
	KM_BOUNCE_READ,
	KM_SKB_SUNRPC_DATA,
	KM_SKB_DATA_SOFTIRQ,
	KM_USER0,
	KM_USER1,
	KM_BIO_SRC_IRQ,
	KM_BIO_DST_IRQ,
	KM_PTE0,
	KM_PTE1,
	KM_PTE2,
	KM_IRQ0,
	KM_IRQ1,
	KM_SOFTIRQ0,
	KM_SOFTIRQ1,
	KM_SYNC_ICACHE,
	KM_SYNC_DCACHE,
	KM_UML_USERCOPY,
	KM_IRQ_PTE,
	KM_NMI,
	KM_NMI_PTE,
	KM_TYPE_NR
};

取消映射,也不会阻塞,

void kunmap_atomic(void *kvaddr, enum km_type type)

每个CPU的分配

每个CPU有只属于自己的数据,因此只需要禁止内核抢占就ok,不需要繁杂的锁。在中断上下文和进程上下文中使用都很安全。但不能睡眠。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值