十二、内存管理
12.1 页
内核用struct page 结构表示系统中的每个物理页
struct page{
unsigned long flags;// 存放页的状态,包括页是否脏,是否锁定于内存中
acomic_t _count;// 页的引用计数。-1时,就说明当前内核没有引用这一页。
atomic_t _mapcount;
unsigned long private; //指向拥有这个页的私有数据
struct address_space *mapping; //指向和这个页关联的address_space对象即页高速缓存对象
pgoff_t index;
struct list_head lru;
void *virtual; //页的虚拟地址,页在虚拟内存中的地址
}
page结构与物理页相关,而并非与虚拟页相关。该结构对页的描述只是短暂的。即使页中包含的数据继续存在,由于交换等原因,他们也可能并不再和同一个page结构相关联。内核仅仅用这个数据结构来描述当前时刻在相关的物理页中存放的东西。这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。
12.2 区
由于硬件的限制,内核不能对所有的页一视同仁。有些页位于内存中特定的物理地址上,所以不能将其用于特定的任务。由于这种限制,所以内核把页划分为不同的区。内核使用区对具有相似特性的页进行分组:
1、一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问);
2、一些体系结构的内存的物理寻址范围比虚拟寻址范围大的多。这样,就有一些内存不能永久的映射到内核上。
四个区:
1、ZONE_DMA——执行DMA操作
2、ZONE_DMA32
3.ZONE_NORMAL——能正常映射的页
4、ZONE_HIGHEM——高端内存,其中的页不能永久映射到内核地址空间
区的划分没有任何物理意义,这只不过是内核为了管理页而采取的一种逻辑上的分组。
每个区都用struct zone表示
struct zone{
unsigned long watermark[NR_WMARY];//持有该区的最小值、最低和最高水位值。内核使用水位为每个内存区设置合适的内存消耗基准。该水位随空闲内存的多少而变化。
unsigned long lowmem_reserve[MAX_NR_ZONES];
struct per_cpu_pageset pageset[NR_CPS];
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_bits;
struct pglist_data *zone_pgdat;
unsigned long zone_start_pfn;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;
}
12.3 获得页
struct page *alloc_pages(gfp_t gfp_mask,unsigned int order);//分配2^order个连续的物理页,并返回一个指针,指向第一个页的page结构体。
void * page_address(struct page *page);//把给定的页转换成逻辑地址
unsigned long __get_free_pages(gfp_t gfp_mask,unsigned int order);//与alloc_pages()类似,不过返回所请求的第一个页的逻辑地址。
struct page *alloc_page(gfp_t gfp_mask);//分配一页
unsigned long __get_free_page(....);//分配一页
12.3.1 获得填充为0的页
unsigned long get_zerod_oage(unsigned int gfp_mask);//虽说分配好的页中应该包含的都是随机产生的垃圾信息,但其实这些信息可能并不完全随机——它很可能随机地包含某些敏感数据。用户空间的页在返回之前,所有数据必须填充为0,或做其他工作。
12.3.2 释放页
void __free_pages(struct page *page,unsigned int order);
void free_pages(unsigned long addr,unsigned int order);
void free_page(unsigned long addr);
释放页时要谨慎,只能释放属于你的页。传递了错误的struct page 或地址,用了错误的order值,都有可能导致系统的崩溃。
12.4 kmalloc() 获得以字节为单位的一块内核内存
void *kmalloc(size_t size,gfp_t flags);返回指向内存块的指针,内存块至少有size大小,所分配的内存区在物理上是连续的。
12.4.1 gfp_mask标志
标志分为三类:行为修饰符、区修饰符和类型。行为修饰符表示内核应当如何分配所需的内存。在某些特定的情况下,只能使用某些特定的方法分配内存。例如中断处理程序要求内核在分配内存时不能睡眠。区修饰符表示从哪个区进行分配。类型标识组合了行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型,简化了修饰符的使用。
12.4.2 kfree()释放由kmalloc()分配出来的内存块。如果想要释放的内存不是由kmalloc()分配的,或者想要释放的已经释放了,会导致严重后果。kfree(NULL)是安全的。
12.5 vmalloc();
分配的内存虚拟地址是连续的,物理地址不保证连续。vmalloc()函数为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。通过vmalloc()获得的页必须一个一个进行映射,这就导致比直接内存映射大得多的TLB抖动。vmalloc在不得已(分配大块内存)时才会使用。例如,当模块被动态插入到内核中时,就把模块装载到由vmalloc分配的内存上。
void vfree(const void*addr);
12.6 slab层
12.6.1 slab层的设计
slab层把不同的对象划分为高速缓存组,其中每个高速缓存组都存放不同类型的对象。每种对象类型对应一个高速缓存。例如,一个高速缓存用于存放进程描述符,另一个高速缓存存放索引节点对象。这些高速缓存又被划分为slab。slab由一个或多个物理上连续的页组成。一般就1页。每个高速缓存可由多个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之外另行分配,要么就放在slab自身开始的地方。slab分配器可以创建新的slab,这是通过__get_free_pages()低级内核页分配器进行的。
kmem_getpages();
kmem_freepages();
12.6.2 slab分配器接口
创建:struct kmem_cache *kmem_cache_create(const char *name,//名字
size_t size,//缓存中每个元素的大小
size_t align,//第一个对象的偏移量
unsigned long flags,//可选设置项,控制告诉缓存行为
void(*ctor)(void *));//告诉缓存的构造函数。linux内核的告诉缓存不使用构造函数。
撤销:int kmem_cache_destory(struct kmem_cache *cachep);撤销时必须满足:告诉缓存中所有slab都为空,调用后不能再访问此次缓存。
1、从缓存中分配对象
void * kmem_cache_alloc(struct kmem_cache *cachep,gfp_t flags)
2、释放一个对象
void kmem_cache_free(struct kmem_cache *cachep,void *objp);
实例:
struct kmem_cache *task_struct_cachep;
task_struct_cachep = kmem_cache_create("task_struct",sizeof(struct task_struct),ARCH_MIN_TASKALLGN,SLAB_PANIC|SLAB_NOTRACK,NULL);
struct task_struct *tsk;
tsk = kmem_cache_alloc(task_struct_cachep,GFP_KERNEL);
if(!tsk)
return NULL;
--------------------------------
kmem_cache_free(task_struct_cache,tsk);//释放tak
12.7 在栈上的静态分配
12.7.1 单页内核栈
12.7.2 在栈上光明正大的工作
只需要在具体的函数中让所有局部变量所占空间之和不要超过几百字节。
12.8 高端内存映射
在高端内存中的页不能永久地映射到内核空间上。因此,通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不可能有逻辑地址。在x86体系结构上,高于896m的所有物理内存的范围大都是高端内存,它不会永久的或自动的映射到内核地址空间。
12.8.1 永久映射
void *kmap(struct page *page);
void kunmap(struct page *page);
12.8.2 临时映射
当创建一个映射而当前上下文又不能睡眠时,内核提供了临时映射。
void *kmap_atomic(struct page *page,enum km_type type);
该函数不会阻塞,因此可以用于中断上下文或者其他不能重新调度的地方。它也禁止内核抢占,因为映射对每个处理器都是唯一的。
void kunmap_atomic(void *kvaddr,enum km_type type);
12.9 每个CPU的分配
支持SMP的现代操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的。一般来说,每个CPU的数据存放在一个数组中。数组中每一项对应这系统上一个存在的处理器。按当前处理器号确定这个数组的当前元素。可以声明数据如下:
unsigned long my_percpu[NR_CPUS];
然后访问:
int cpu;
cpu = get_cpu();//获得当前处理器,并禁止内核抢占
my_percpu[cpu]++;
printk("my_percpu on cpu=%d is %lu\n",cpu,my_percpu[cpu]);
put_cpu();//激活内核抢占
上面的代码中没有出现锁,这是因为所操作的数据对当前处理器来说是唯一的。除了当前处理器之外,没有其他处理器可以接触到这个数据,不存在并发访问的问题,所以当前处理器可以在不用锁的情况下安全访问它。
12.10 新的每个CPU接口
percpu,归纳了前面所述的操作行为,简化了创建和操作每个CPU的数据。
12.10.1 编译时的每个CPU数据
编译:DEFINE_PER_CPU(type,name);//为每个处理器都创建一个类型为type,名字为name的变量实例
别处声明变量:DECLARE_PER_CPU(type,name);
get_cpu_var();返回当前处理器上指定的变量
put_cpu_var();重新激活抢占
per_cpu(name,cpu);//获取指定cpu上name的变量值,该方法不会禁止内核抢占,也不会提供任何形式的锁保护
这些编译时每个cpu数据的例子不能再模块内使用,因为连接程序实际上将他们创建在一二唯一的可执行段中。
12.10.2 运行时的每个cpu数据
内核实现每个CPU数据的动态分配:
void *alloc_percpu(type);给系统中每个处理器分配一个指定类型对象的实例,是对宏__alloc_percpu()的封装,
void *__alloc_percpu(size_t size,size_t align);
void free_percpu(const void *);
无论alloc或__alloc,都返回一个指针,用来间接引用动态创建的每个CPU数据,内核提供了两个宏来利用指针获取每个CPU数据:
get_cpu_var(ptr);//返回一个void类型指针,该指针指向处理器的ptr的拷贝
put_cpu_var(ptr);//玩成,重新激活内核抢占
12.11 使用每个CPU数据的原因
1、减少了数据锁定。因为按照每个处理器访问每个CPU数据的逻辑,你可以不再需要任何锁
2、大大减少缓存失效。失效发生在处理器试图使他们的缓存保持同步时。如果一个处理器操作某个数据,而该数据又存放在其他的处理器缓存中,那么存放该数据的那个处理器必须清理或刷新自己的缓存。持续不断的缓存失效称为缓存抖动,这样对系统的性能影响较大。
12.12 分配函数的选择
浙公网安备 33010602011771号