Linux2.6.33内存管理


在内核之中获取内存

  • 内核把物理页作为内存管理的基本单元
  • 内存管理单元(MMU)以页page大小为单位来管理系统中的页表
  • 体系结构不同,支持的页大小页不一样,大多数32位体系结构支持4KB的页,而64位体系结构一般支持8KB的页
  • 内核使用struct page结构体表示系统中的每个物理页
  • page结构体与物理页相关,而并非虚拟页相关
  • 内核仅仅使用这个数据结构描述当前时刻在相关的物理页中存放的东西,也就是说该数据结构目的在于描述物理内存本身,而不是藐视包含其中的数据
  • 内核使用page数据结构管理系统所有的页,因为内核需要知道一个页是否空闲,如果页已经分配,内核还需要知道谁拥有该页,拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或页高速缓冲等
#include <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:用于存放页的状态,包括页是不是脏的,是不是被锁定在内存中等。flags的每一个位单独表示一种状态,这些标志定义在<linux/page-flags.h>
  • _count:存放页的引用计数。当计数值变为-1时,表示当前内核没有引用该页,在新的分配中就可以使用该页。内核代码不应当直接检查该域,而是调用page_count()函数检查,该函数唯一参数是page结构体,返回0表示页空闲,返回一个整数表示页在使用。一个页可以由页缓存使用(mapping域指向和这个页关联的address_space对象),或者作为私有数据(private指向),或者作为进程页表中的映射
  • virtual:页的虚拟地址。也就是说页在虚拟内存中的地址,有些内存(高端内存)并不永久地映射到内核地址空间上,此种情况下,该值为NULL,需要的时候必须动态地映射这些页

  • 由于硬件的限制,内核并不能对所有的页一视同仁,有些页位于内存中特定的物理地址上,不能将其用于一些特定的任务
  • 内核把页划分为不同的区(zone),内核使用zone对具有相似特性的页进行分组
  1. ZONE_DMA:该区包含的页能用来执行DMA操作
  2. ZONE_DMA32:和ZONE_DMA类似,可用来执行DMA操作,不同之处,这些页面只能被32位设备访问,在某些体系结构中,该区比ZONE_DMA更大
  3. ZONE_NORMAL:该区包含的页都是能正常映射的页
  4. ZONE_HIGHMEM:该区包含“高端内存”,其中的页并不能永久地映射到内核地址空间,这些区在<linux/mmzone.h>中定义
  • 区的实际使用和分布是与体系结构相关的,在x86体系,ISA设备不能32位地址空间执行DMA,因为ISA设备只能访问物理内存的前16MB,因此ZONE_DMA在x86上包含的页都在0-16MB的内存范围
  • ZONE_HIGHMEM能否直接映射也取决体系结构,在32位的x86系统上,ZONE_HIGHMEM为高于896的所有物理内存,在其他体系结构上,由于所有内存都被直接映射,所以ZONE_HIGHMEM为空
  • ZONE_HIGHMEM所在的内存是所谓的高端内存,系统其余内存就是低端内存
  • 区的划分只不是为了管理页而采取的一种逻辑上的分组
#include <linux/mmzone.h>
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结束的字符串表示的这个区的名字。内核启动期间初始化该值,其代码位于mm/page_alloc.c中,三个区的名字分布为“DMA”、“Normal”和“HighMem”

获取页

#include <linux/gfp.h>
//该函数分配2order(1 << order)个连续的物理页,并返回指向第一个页的page结构体指针,如果出错,返回NULL
struct page* alloc_pages(gfp_t gfp_mask, unsigned int order);
//该函数把给定的page结构体页转换成逻辑地址,该函数返回指针,指向给定物理页当前所在的逻辑地址
void* page_address(struct page* page);
//该函数与alloc_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的一页内存空间
unsigned long get_zone_zeroed_page(unsigned int gfp_mask);
//释放页需要谨慎,传递错误的struct page或地址,错误的order,都可能导致系统崩溃
void __free_pages(struct page* page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
void free_page(unsigned long addr);
unsigned long page;
page = __get_free_pages(GFP_KERNEL, 3);
if (!page) {
	return _ENOMEM;
}
....
free_pages(page, 3);

gfp_mask标志

  • 行为修饰符表示内核应当如何分配所需的内存
  • 区修饰符表示从哪分配内存
  • 类型标志组合行为修饰符和区修饰符
  • 描述符都在<linux/gfp.h>中声明,在<linux/slab.h>中有包含该头文件

行为修饰符

标志描述
__GFP_WAIT分配器可以睡眠
__GFP_HIGH分配器可以访问紧急事件缓冲池
__GFP_IO分配器可以启动磁盘I/O
__GFP_FS分配器可以启动文件系统I/O
__GFP_COLD分配器应该使用高速缓存中快要淘汰出去的页
__GFP_NOWARN分配器将不打印失败警告
__GFP_REPEAT分配器在分配失败时重复进行分配,但是这次分配还存在失败的可能
__GFP_NOFALL分配器将无限地重复进行分配,分配不能失败
__GFP_NORETRY分配器在分配失败时绝不会重新分配
__GFP_NO_GROW由slab层内部使用
__GFP_COMP添加混合页元数据,在hugetlb的代码内部使用
//可以同时指定多种分配标志,比如分配时可以阻塞、执行I/O、必要时还可以执行文件系统操作
ptr = kmalloc(size, __GFP_WAIT | __GFP_IO | __GFP_FS);

区修饰符

  • 区修饰符表示内存区应当从何处分配,通常分配可以从任何区开始,内核优先从ZONE_NORMAL开始
  • 实际只有两个区修饰符,因为除了ZONE_NORMAL之外只有两个区
  • 在大多数情况下,不必指定修饰符,默认ZONE_NORMAL
标志描述
__GFP_DMA从ZONE_DMA分配
__GFP_DMA32只在ZONE_DMA32分配
__GFP_HIGHMEM从ZONE_HIGHMEM或ZONE_NORMAL分配
  • 不能给_get_free_pages()或kalloc()指定ZONE_HIGHMEM,因为这两个函数返回的都是逻辑地址,而不是page结构体,这两个函数分配的内存当前由可能还没有映射到内核的虚拟地址空间,因此,也可能根本没有逻辑地址
  • 只有alloc_pages()才能分配高端内存

类型标志

  • 类型标志指定所需的行为和区描述符以完成特殊类型的处理
标志描述修饰符标志
GFP_ATOMIC用在中断处理程序、下半部、持有自旋锁以及其他不能睡眠的地方__GFP_HIGH
GFP_NOWAIT与GFP_ATOMIC类似,不同之处,调用不会退给紧急内存池,这就增加了内存分配失败的可能性0
GFP_NOIO可以阻塞,但不会启动磁盘I/O,这个标志在不能引发更多磁盘I/O时能阻塞I/O代码,可能导致令人不愉快的递归__GFP_WAIT
GFP_NOFS分配在必要时可能阻塞,页可能启动磁盘I/O,但是不会启动文件系统操作,这个标志在不能启动另一个文件系统的操作时,用在文件系统部分代码中__GFP_WAIT | __GFP_IO
GFP_KERNEL常规分配方式,可能阻塞,这个标志在睡眠安全时用在进程上下文代码中,为了获得调用者所需的内存,内核会尽力而为__GFP_WAIT | __GFP_IO | __GFP_FS
GFP_USER常规分配方式,可能会阻塞,这个标志用于用户空间进程分配内存__GFP_WAIT | __GFP_IO | __GFP_FS
GFP_HIGHUSER从ZONE_HIGHMEM进行分配,可能会阻塞,这个标志用于为用户空间进行分配内存__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HIGHMEN
GFP_DMA从ZONE_DMA进行分配,需要获取能供DMA使用内存的设备驱动程序使用这个标志,通常与以上的某个标志组合在一起使用__GFP_DMA
使用情形相应标志
进程上下文,可以睡眠使用GFP_KERNEL
进程上下文,不可以睡眠使用GFP_ATOMIC,或在睡眠之前或之后以GFP_KERNEL执行内存分配
中断处理程序使用GFP_ATOMIC
软中断使用GFP_ATOMIC
tasklet使用GFP_ATOMIC
需要用于DMA的内存,可以睡眠使用GFP_DMA | GFP_KERNEL
需要用于DMA的内存,不可以睡眠使用GFP_DMA | GFP_ATOMIC,或在睡眠之前执行内存分配

kmalloc

  • 获得以字节为单位的一块内核内存,其空间在物理地址上是连续的
#include <linux/slab.h>
//该函数返回一个指向内存块的指针,如果出错返回NULL
void* kmalloc(size_t size, gfp_t flags);
//释放由kmalloc()分配出来的内存块
void kfree(const void* ptr);

vmalloc

  • 获得以字节为单位的一块内核内存,其空间在内存虚拟地址是连续的,物理地址无需连续
  • vmalloc()函数为了把物理上不连续的页转换为虚拟地址空间上的连续的页,必须专门建立页表项
  • vmalloc()仅在不得已才会使用,如为了获取大块内存,例如,当模块被动态插入内核中时,就把模块装载到由vmalloc()分配的内存上
#include <linux/vmalloc.h>
void* vmalloc(unsigned long size);
void vfree(const void* addr);

slab层

struct slab {
	struct list_head list;//满、部分满或空链表
	unsigned long colouroff;//slab着色偏移量
	void *s_mem;//在slab中的第一个对象
	unsigned int inuse;//slab中已分配的对象数
	kmem_bufctl_t free;//第一个空闲对象
}
//slab分配器可以创建新的slab,通过__get_free_pages()低级内核页分配器进行
static void* kmem_getpages(struct kmem_cache* cachep, gfp_t flags, int nodeid)
{
	struct page* page;
	void *addr;
	int i;

	flags |= cachep->gfpflags;
	if (likely(nodeid == -1)) {
		addr = (void*)__get_free_pages(flags, cachep->gfporder);
		if (!addr)
			return NULL;
		page = virt_to_page(addr);
	} else {
		page = alloc_pages_node(nodeid, flags, cachep->gfporder);
		if (!page)
			return NULL;
		addr = page_address(page);
	}
	
	i = (1 << cachep->gfporder);
	if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
		atomic_add(i, &slab_reclaim_pages);
	add_page_state(nr_slab, i);
	while (i--) {
		SetPageSlab(page);
		page++;
	}
	return addr;
}

创建一个新的高速缓存slab

struct kmem_cache* kmem_cache_create(const char*name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *));
  • name:一个字符串,存放高速缓存的名字
  • size:是高速缓存中每个元素的大小,即对象的大小
  • align:slab内第一个对象的偏移,用来保证在页内进行特定的对齐,通常情况下为0
  • flags:是可选的设置项,用来控制高速缓存的行为,0表示无特殊的行为,页可以多个标志进行“或”运算
  1. SLAB_HWCACHE_ALIGN:把一个slab内的所有对象按高速缓存行对齐,防止错误共享(两个或多个对象尽管位于不同的内存地址,但映射到相同的高速缓存行),虽然提供性能,但是以增加内存开销为代价
  2. SLAB_POISON:市slab使用已知的值(a5a5a5a5)填充slab,就是所谓的“中毒”,有利于对未初始化内存的访问
  3. SLAB_RED_ZONE:这个标志导致slab层在以分配的内存周围插入“红色警戒区”以探测缓冲越界
  4. SLAB_PANIC:这个标志当分配失败时提醒slab层,这要求分配只能成功的时候非常有用,如在系统启动时分配一个VMA结构的高速缓冲
  5. SLAB_CACHE_DMA:这个标志命令slab层使用可以执行DMA的内存给每个slab分配空间,只有分配的对象用于DMA,而且必须驻留在ZONE_DMA区时才需要这个标志,否则不应该设置该标志
  • ctor:高速缓冲的构造函数,只有新页追加到高速缓存时,构造函数才被调用,实际上,Linux内核的高速缓存不使用构造函数,因此可以赋值NULL

撤销一个高速缓存

//成功返回0,否则返回非0值
int kmem_cache_destroy(struct kmem_cache* cachep);
  • 撤销给定的高速缓存,通常在模块的注销代码中调用,也不能从中断上下文中调用该函数,可能睡眠
  • 高速缓存中所有的slab都必须为空
  • 在调用kmem_cache_destroy()过程中,不能访问该高速缓存

从缓存中分配一个对象

void* kmem_cache_alloc(struct kmem_cache* cachep, gfp_t flags);
  • 从给定的高速缓存cachep中返回一个指向对象的指针
  • 如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmem_getpages()获取新的页
  • flags的值传递给__get_free_pages(),一般为GFP_KERNRL或GFP_ATOMIC

从缓存中释放一个对象

void kmem_cache_free(struct kmem_cache* cachep, void *objp);

slab分配器的使用实例

//实例使用task_struct进程描述符结构体
//首先,内核定义一个全局变量存放指向task_struct高速缓存的指针
struct kmem_cache* task_struct_cachep;
//在内核初始化期间,定义于kernel/fork.c的fork_init()中创建高速缓存
//该对象被创建后存放在slab中偏移量为ARCH_MIN_TASKALIGN个字节的地方
//ARCH_MIN_TASKALIGN预定义值与体系结构相关,通常定义为L1高速缓存的字节大小
//因为SLAB_PANIC标志,如果分配失败,slab分配器调用panic()函数
//如果没有SLAB_PANIC标志,就必须自己检查返回值
task_struct_cachep = kmem_cache_create("task_struct",
										sizeof(struct task_struct),
										ARCH_MIN_TASKALIGN,
										SLAB_PANIC | SLAB_NOTRACK,
										NULL);
//每当进程调用fork()时,一定创建一个新的进程描述符,在dup_task_struct()中完成,该函数被do_fork()调用
struct task_struct* tsk;
tsk = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL);
if (!tsk)
	return NULL;
//进程执行完后,如果没有子进程等待,该进程描述符就被释放,并返回task_struct_cachep slab高速缓存,在free_task_struct()中执行
kmem_cache_free(task_struct_cachep, tsk);
//由于进程描述符是内核的核心组成部分,因此task_struct_cachep高速缓存绝不会被撤销,即使真能撤销,也要通过下列函数阻止其被撤销
int err;
err = kmem_cache_destroy(task_struct_cachep);
if (err)
	/*错误,撤销高速缓存*/

在栈上静态分配

  • 每个进程的内核栈大小依赖体系结构,也与编译时的选项有关
  • 因为32位和64位体系结构的页面大小分别是4KB和8KB,所有通常内核栈的大小分别是8KB和16KB
  • 中断栈为每个进程提供一个用于处理程序的栈,因此中断程序不要再和被中断程序共享一个内核栈
  • 在栈上进行大量的静态分配(比如分配大型数组和大型结构体)是危险的,容易导致栈溢出

高端内存的映射

  • 根据定义,在高端内存中的页不能永久地映射到内核地址空间上,因此通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不可能有逻辑地址
  • 在x86体系结构上,高于896MB的所有物理内存的范围都是高端内存,并不会永久地或自动地映射到内存地址空间
  • 在x86上,高端内存中的页被映射到3GB~4GB

永久映射

//要映射一个给定的page结构到内核地址空间,可以定义在<linux/highmem.h>的函数kmap()
//该函数在高端内存或低端内存都能用
//如果page结构对应低端内存中的一页,函数单纯返回该页的虚拟地址
//如果page结构位于高端内存,则会建立一个永久映射,再返回地址
//这个函数可以睡眠,因此kmap()只能用在进程上下文中
#include <linux/highmem.h>
void* kmap(struct page* page);

//因为运行永久映射的数量有限,当不需要高端内存时,应当接触映射
void kunmap(struct page* page);

临时映射

//当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时映射
//该函数不会阻塞,因此可以再中断上下文和其他不能重新调度的地方使用
//禁止内核抢占,因为映射对每个处理器都是唯一的
void* kmap_atomic(struct page* page, enum km_type type);
//取消映射
void kunmap_atomic(void* kvaddr, enum km_type type);
//其中type是下列枚举类型之一,其目的用于描述临时映射,定义与<asm/kmap_types.h>
#include <asm/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
};

每个CPU的分配

  • 支持SMP的现代操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的
  • 一般来说,每个CPU的数据存放在一个数组中,数组中的每一项对应着系统上一个存在的处理器,按当前处理器号确定这个数组的当前元素
  • 2.4内核处理每个CPU数据的方式,在2.6内核中一人存在,如下面
unsigned long my_percpu[NR_CPUS];
//按如下方式访问
int cpu;
cpu = get_cpu();//获取当前处理器,并禁止内核抢占
my_percpu[cpu] ++; 
printk("my_percpu on spu=%d is %lu\n", cpu, my_percpu[cpu]);
put_cpu();//激活内核抢占

新的每个CPU接口

  • 2.6内核为了方便创建和操作每个CPU数据,引进新的接口,称为percpu
  • 头文件<linux/percpu.h>声明了所有接口,可以在mm/slab.c和<asm/percpu.h>找到定义

编译时的每个CPU数据

//为系统中每一个处理器都创建一个类型为type,名字为name的变量实例
DEFINE_PER_CPU(type, name);
//如果需要在别处声明变量,以防编译时警告,则使用
DECLARE_PER_CPU(type, name);
//返回当前处理器上的指定变量,同时禁止抢占
get_cpu_var(name)++;//增加该处理器上的name变量的值
//将当前处理器重新激活抢占
put_cpu_var(name);
//获取别的处理器上的CPU数据
//per_cpu()函数既不会禁止内核抢占,也不提供任何形式的锁保护
per_cpu(name, cpu)++; //增加指定处理器上的name变量的值

运行时的每个CPU数据

//内核实现每个CPU数据动态分配的方法类似kmalloc()
//该例程为系统上的每个处理器创建所需内存的实例,其原型在文件<linux/percpu.h>中
#include <linux/percpu.h>
void* alloc_percpu(type);//一个宏
void* __alloc_perspu(size_t size, size_t align);
void free_percpu(const void*);
//宏alloc_percpu()给系统的每个处理器分配一个指定类型对象的实例,其实是__alloc_perspu()一个封装
//调用free_percpu()将释放所有处理器上指定的每个CPU数据

//利用指针获取每个CPU数据
get_cpu_var(ptr);//返回一个void类型指针,该指针指向处理器的ptr的拷贝,同时禁用内核抢占
put_cpu_var(ptr);//重新激活内核抢占
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值