Linux内核设计与实现 总结笔记(第十二章)内存管理
内核里的内存分配不像其他地方分配内存那么容易,内核的内存分配不能简单便捷的使用,分配机制也不能太复杂。
一、页
内核把页作为内存管理的基本单位,尽管处理器最小寻址坑是是字或者字节。但是内存管理单元MMU通常以页为单位进行处理。
从虚拟内存的角度来看,页就是最小单位。大多数32位系统支持4KB的页,而64位系统结构一般会支持8KB的页。
内核用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;
};
简化的struct page
flag域用来存放页的状态,flag的每一位单独表示一种状态,这些标志位定义于<linux/page-flags.h>中
_count存放页的引用计数,也就是一页被引用了多少次。如果是-1时,就说明当前内核并没有引用这一页。内核代码不应直接检查该域,而是使用page_count()函数进行检查,当页空闲时,返回0表示页空闲。
virtual域是页的虚拟地址,通常情况下,它就是页在虚拟内存中的地址。
page结构与物理页相关,而并非与虚拟页相关。
二、区
有些页位于内存中特定的物理地址上,不能用于其他特定的任务。由于这种限制,内核把页划分为不同的地区。
Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:
- 一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)
- 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。这样就有一些内存不能永久地映射到内核空间上。
因为上面的制约条件,Linux使用了四种区:
- ZONE_DMA:这个区包含的页能用来执行DMA操作
- ZONE_DMA32:和ZOME_DMA类似,该区包含的页面可用来执行DMA操作;不过只能被32位设备访问。
- ZONE_NORMAL:这个区包含的都是能正常映射的页。
- ZONE_HIGHEM:这个区包含“高端内存”并不能永久地映射到内核地址空间,在<linux/mmzone.h>中定义。
区 描述 物理内存
ZONE_DMA DMA使用的页 <16MB
ZONE_NORMAL 正常可寻址的页 16~896MB
ZONE_HIGHMEM 动态映射的页 >896MB
Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。
比如需要DMA分配所需的内存,可以在ZONE_DMA内存池分配。
区的划分没有任何物理意义,只不过是内核为了管理页而采取的一种逻辑分组。
每个区都是struct zone表示,在<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;
unsgined 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;
}
struct zone
lock域是一个自旋锁,它防止该结构被并发访问。这个域只保护结构,而不保护驻留在这个区中的所有页。
watermark数组持有该区的最小值、最低和最高水位值。内核使用水位为每个内存区设置合适的内存消耗基准。
name域是一个以NULL结束的字符串表示这个区的名字。在mm/page_alloc.c中,有名字“DMA”、“Normal”和“HighMem”
三、获得页
内核使用如下接口在内核内分配和释放内存,定义于<linux/gfp.h>中。
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
alloc_pages
函数分配2order(1<<order)个连续的物理页,并返回一个指针,指向第一个页的page结构体;如果出错,就返回NULL。
返回的页指针,可以用下面函数转换成逻辑地址。该函数返回一个指针,指向给定物理页当前所在的逻辑地址。
void *page_address(struct page *page);
page_address
如果你无须用到struct page,可以调用:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
__get_free_pages
这个函数与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。
如果只需要一页,有两个封装好的函数,只不过传递给order的值为0:
struct page *alloc_page(gfp_t gfp_mask)
unsigned long __get_free_page(gfp_t gfp_mask)
alloc_page
3.1 获得填充为0的页
函数可以让返回的页内容全为0
unsigned long get_zeroed_page(unsigned int gfp_mask);
get_zeroed_page
此函数与__get_free_pages()工作方式相同,只不过把分配好的页都填充成了0。
alloc_page(gfp_mask):只分配一页,返回指向页结构的指针
alloc_pages(gfp_mask, order):分配2^order个页,返回指向第一页页结构的指针
__get_free_page(gfp_mask):只分配一页,返回指向其逻辑地址的指针
__get_free_pages(gfp_mask, order):分配2^order页,返回指向第一页逻辑地址的指针
get_zeroed_page(gfp_mask):只分配一页,让其内容填充0,返回指向其逻辑地址的指针
底层页分配方法表
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) 例子:
unsigned long page: page = __get_free_pages(GFP_KERNEL, );
if(!page) {
/* 没有足够的内存:你必须处理这种错误 */
return -ENOMEM;
}
/* "page"现在指向8个连续页中第1个页的地址... */
用完这8个页后需要释放它们:
free_pages(page, ); /* 页现在已经被释放了,我们不应该再访问存放在"page"中的地址了 */
free_pages
四、kmalloc()
kmalloc和malloc类似,但是比malloc多了一个flags。kmalloc()函数是一个简单的接口,用它可以获得字节为单位的一块内核内存,如果需要整页,那么,前面的页分配接口可能更好的选择。
kmalloc()在<linux/slab.h>中声明:
void *kmalloc(size_t size, gfp_t flags)
size:内存块至少要有size大小,所分配的内存区在物理上是连续的。
出错返回NULL
例子:
struct dog *p;
p=kamlloc(sizeof(struct dog), GFP_KERNEL);
if(!p)
/* 处理错误 ... */
kmalloc原型
4.1 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的代码内部使用
行为修饰符列表
区修饰符:从哪儿分配内存。内核把物理内存分为多个区。
类型标志符:组合行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型,简化了修饰符的使用。
4.2 kfree()
kamlloc的另一端就是kfree(),kfree在<linux/slab.h>中:
void kfree(const void *ptr)
kfree原型
中断处理程序中分配内存的例子:
char *buf;
buf = kmalloc(BUF_SIZE, GFP_ATOMIC);
if(!buf)
/* 内存分配错误 */
kfree(buf); /* 用完释放 */
kmallco例子
五、vmalloc()
vmalloc相对于kmalloc,分配的虚拟内存地址是连续的,物理地址则无需连续。
kmalloc确保页在物理地址上是连续的。
vmalloc函数声明在<linux/vmalloc.h>中,定义在<mm/vmalloc.c>中。用法与用户空间相同。
void *vmalloc(unsgined long size);
/* 该函数返回指针,指向逻辑上连续的一块内存区,大小至少为size。 */
/* 错误时返回NULL。函数可能睡眠 */ void vfree(const void *addr)
/* 释放vmalloc获得的内存 */ /* 例子 */
char *buf;
buf = vmalloc(*PAGE_SIZE); /* get 16 pages */
if(!buf)
/* 错误!不能分配内存 */
/*
* buf现在指向虚拟地址连续的一块内存区,其大小至少为16*PAGE_SIZE
*/ vfree(buf);
/* 释放 */
vmalloc
六、slab层
分配和释放数据结构是所有内核中最普遍的操作之一。为了全局控制频繁的数据分配和回收,有了slba分配器。
- 频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。
- 频繁分配和回收必然会导致内存碎片,所以空闲链表缓存会连续地存放。因为已释放地数据接哦古又会放回空闲链表,因此不会导致碎片。
- 回收地对象可以立即投入下一次分配,因此,对于频繁分批和释放,空闲链表能够提高其性能。
- 如果分配器知道对象大小、页大小和总的高速缓存的大小这样的概念,它会做出更明智的决策。
- 如果让部分缓存专属于单个处理器,那么,分配和释放就可以在不加SMP锁的情况下进行。
- 如果分配器是与NUMA相关的,它就可以从相同的内粗节点为请求者进行分配。
- 对存放的对象进行着色,以防止多个对象映射到相同的告诉缓存行。
6.1 slab层的设计
slab把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象。比如一个存放进程描述符,一个存放索引节点。
每个高速缓存都是用kmem_cache结构来表示。包括三个链表:slabs_full、slabs_partial和slabs_empty。都存放在kmem_list3结构内,该结构在mm/slab.c中。
struct slab {
struct list_head list; /* 满、部分满或空链表 */
unsigned long colouroff; /* slab着色的偏移量 */
void *s_mem; /* 在slab中的第一个对象 */
unsigned int inuse; /* slab中已分配的对象数 */
kmem_bufctl_t free; /* 第一个空闲对象(如果有的话) */
};
struct slab
6.2 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内第一个对象的偏移,它用来确保在页内进行特定的对齐
falgs:参数是可选的设置项
ctor:高速缓存的构造函数。基本抛弃,设NULL
kmem_cache_create
falgs有各种参数:
- SLAB_HWCACHE_ALIGN:这个标志命令slab层把一个slab内所有对象按高速缓存行对齐。对齐越严格,浪费内存越多
- SLAB_POISON:slab层用已知的值(a5a5a5a5)填充slab
- SLAB_RED_ZONE:导致slab层在已分配的内存周围插入“红色警戒区”以探测缓冲越界
- SLAB_PANIC:当分配失败时提醒slab层。
- SLAB_CACHE_DMA:slab层可以执行DMA的内存给每个slab分配空间。
要撤销一个高速缓存,则调用:
int kmem_cache_destroy(struct kmem_cache *cachep);
kmem_cache_destroy
1.从缓存中分配
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t *flags)
cachep:返回一个指向对象的指针
flags:_get_free_pages()
创建缓存
释放一个对象,
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
cachep:对象objp标记为空闲
释放分配的对象
2.slab分配器的使用实例
在kernel/fork.c中,
struct kmem_cache *task_struct_cachep;
task_struct_cachep = kmem_cache_create("task_struct",
sizeof(struct task_struct),
ARCH_MIN_TASKALIGN,
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_cachep, tsk); int err;
err = kmem_cache_destroy(task_struct_cachep);
if(err)
/* 出错,撤销高速缓存 */
例子
七、在栈上的静态分配
历史上,每个进程都有两页的内核栈。因为32位和64位体系结构的页面大小分别是4KB和8KB,所以通常它们的内核栈的带线啊哦分别是8KB和16KB
7.1 单页内核栈
中断处理程序也要放在内核栈中,但同时会把更严格的约束台哦见加在这可怜的内核栈上。
所以有一个中断栈,可以为每个进程提供一个用于中断处理程序的栈。
7.2 正大光明的工作
大量的静态分配是很危险的,因此动态分配是一种明智的选择。
八、高端村内的映射
高端内存的页被映射到3GB~4GB
8.1 永久映射
要映射一个给定page结构到内核地址空间,可以使用定义在文件<linux/highmem.h>中的函数:
void *kmap(struct page *page)
这个函数在高端内存或低端内存上都能用。
kmap
8.2 临时映射
建立一个临时映射:
void *kmap_atomic(struct page *page, enum km_type type)
type是枚举类型之一
enum km_type {
KM_BOUNCE_READ,
KM_SKB_SUNRPC_DATA,
KM_USER0,
KM_USER1,
KM_BIO_SRC_IRQ,
KM_BIO_DST_IRQ,
KM_PTE0,
KM_PTE1,
KM_IRQ0,
KM_IRQ1,
KM_SOFTIRQ0,
KM_SOFTIRQ1,
KM_SYNC_ICACHE,
KM_UML_USERCOPY,
KM_IRQ_PTE,
KM_NMI,
KM_NMI_PTE,
KM_TYPE_NR
};
kmap_atomic
通过下列取消映射:
void kunmap_atomic(void *kvaddr, enum km_type type)
kunmap_atomic
九、每个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(); /* 激活内核抢占 */
cpu例子
十、新的每个CPU接口
为了方便创建和操作每个CPU数据,而引进了新的操作接口,称作percpu。
10.1 编译时的每个CPU数据
在编译时设置每个CPU的变量很简单:
DEFINE_PER_CPU(type,name);
10.2 运行时的每个CPU数据
内核实现每个CPU数据的动态分配方法类似于kmalloc()。原型在文件<linux/percpu.h>中:
void *alloc_percpu(type); /* 一个宏 */
void *__alloc_percpu(size_t size, size_t align);
void free_percpu(const void *);
宏alloc_percpu()
内核提供了两个宏来利用指针获取每个CPU数据
get_cpu_var(ptr); /* 返回一个void类型指针,该指针指向处理器的ptr拷贝 */
put_cpu_var(ptr); /* 完成:重新激活内核抢占 */
获取每个CPU数据
使用这些函数的例子:
void *percpu_ptr;
unsigned long *foo; percpu_ptr = alloc_percpu(unsigned long);
if(!ptr)
/* 内存分配错误... */ foo = get_cpu_var(percpu_ptr);
/* 操作foo ... */
put_cpu_var(percpu_ptr);
get_cpu_var例子
十一、使用每个CPU数据的原因
使用每个CPU数据有很多好处,减少了数据锁定。
第二个好处是使用每个CPU数据可以大大减少缓存失效。
每个CPU数据会省去许多数据上锁,唯一的要求是要禁止内核抢占。
十二、分配函数的选择
Linux内核设计与实现 总结笔记(第十二章)内存管理的更多相关文章
- 《Linux内核设计与实现》读书笔记(十二)- 内存管理【转】
转自:http://www.cnblogs.com/wang_yb/archive/2013/05/23/3095907.html 内核的内存使用不像用户空间那样随意,内核的内存出现错误时也只有靠自己 ...
- Linux内核设计与实现 总结笔记(第十三章)虚拟文件系统
一.通用文件系统接口 Linux通过虚拟文件系统,使得用户可以直接使用open().read().write()访问文件系统,这种协作性和泛型存取成为可能. 不管文件系统是什么,也不管文件系统位于何种 ...
- Linux内核设计与实现 总结笔记(第六章)内核数据结构
内核数据结构 Linux内核实现了这些通用数据结构,而且提倡大家在开发时重用. 内核开发者应该尽可能地使用这些数据结构,而不要自作主张的山寨方法. 通用的数据结构有以下几种:链表.队列.映射和二叉树 ...
- Linux内核设计与实现 总结笔记(第十一章)定时器和时间管理
时间管理在内核中占用非常重要的地位,内核中有大量的函数都需要基于时间驱动的,内核对相对时间和绝对时间都非常需要. 一.内核中的时间概念 内核必须在硬件的帮助下才能计算和管理时间,系统定时器以某种频率自 ...
- Linux内核设计与实现 总结笔记(第五章)系统调用
系统调用 内核提供了用户进程和内核交互的接口,使得应用程序可以受限制的访问硬件设备. 提供这些接口主要是为了保证系统稳定可靠,避免应用程序恣意妄行. 一.内核通信 系统调用在用户空间进程和硬件设备之间 ...
- Linux内核设计与实现 总结笔记(第七章)中断和中断处理
中断和中断处理 处理器的速度跟外围硬件设备的速度往往不再一个数量级上,因此,如果内核采取让处理器向硬件发出一个请求. 然后专门等待回应的办法,如果专门等待回应,明显太慢.所以等待期间可以处理其他事务, ...
- Linux内核设计与实现 总结笔记(第四章)进程调度
进程调度 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间. 调度程序没有太复杂的原理,最大限度地利用处理器时间的原则是,只要有可以执行的进程,那么就总会有进程正在执行. 一.多任务 多任 ...
- Linux内核设计与实现 总结笔记(第三章)进程
进程管理 进程:处于执行期的程序. 线程:在进程中活动的对象 虚拟机制 虚拟处理器:多个进程分享一个处理器 虚拟内存:多个线程共享虚拟内存 一.进程描述符和任务结构 进程存放在双向循环链表中(队列), ...
- 《Linux内核设计与实现》课本第十八章自学笔记——20135203齐岳
<Linux内核设计与实现>课本第十八章自学笔记 By20135203齐岳 通过打印来调试 printk()是内核提供的格式化打印函数,除了和C库提供的printf()函数功能相同外还有一 ...
- Linux内核设计与实现 读书笔记 转
Linux内核设计与实现 读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://bl ...
随机推荐
- codeforces 1186C Vus the Cossack and Strings
题目链接:https://codeforc.es/contest/1186/problem/C 题目大意:xxxxx(自认为讲不清.for instance) 例如:a="01100010& ...
- IIS7下配置web.config隐藏index.php
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.we ...
- Tomcat原理剖析
Tomcat原理学习 理解Tomcat工作原理 Tomcat的概念及启动原理浅析 Tomcat系统架构与设计模式
- Monkey学习笔记(一)
(一)adb相关命令语句: 1. 查看连接设备信息:adb devices 2.安装app到手机上:adb install [-r] [apk文件存在地址].apk 3.将文件放入设备/模拟器:ad ...
- c++多线程并发学习笔记(0)
多进程并发:将应用程序分为多个独立的进程,它们在同一时刻运行.如图所示,独立的进程可以通过进程间常规的通信渠道传递讯息(信号.套接字..文件.管道等等). 优点:1.操作系统在进程间提供附附加的保护操 ...
- mysql5.7单机多实例安装
基于之前的mysql5.7单实例安装 修改/etc/my.cnf文件如下(这里配置4个实例,可自行修改数目) # # 多实例配置文件,可以mysqld_multi --example 查看例子 # [ ...
- BZOJ 4033: [HAOI2015]树上染色题解
BZOJ 4033: [HAOI2015]树上染色题解(树形dp) 标签:题解 阅读体验:https://zybuluo.com/Junlier/note/1327400 原题地址: BZOJ 403 ...
- vue eslint 规范配置
vue eslint 规范配置 为了代码格式统一,避免一些低级或者不合理的错误,现强行使用eslint的 standard规范 项目配置 目前都是使用 vue 提供的脚手架进行开发的,虽然 vue-c ...
- 表单提交 multipart/form-data 和 x-www-form-urlencoded的区别
表单提交表单有两种提交方式,POST和GET.通常我们会使用POST方式,一是因为形式上的安全 :二是可以上传文件. 我之前经常忽略掉表单的编码类型,觉得它特别长比较难记,而且不设置也似乎不影响什么. ...
- UIScrollView学习笔记
1.如何使用UIScrollView显示一张比屏幕大的图片 //创建滚动视图的对象 UIScrollView * sv = [[UIScrollView alloc]initWithFrame:CGR ...