Linux内核剖析 之 内存管理
1. 内存管理区
为什么分成不同的内存管理区?
ISA总线的DMA处理器有严格的限制:仅仅能对物理内存前16M寻址。
内核线性地址空间仅仅有1G,CPU不能直接訪问全部的物理内存。
ZONE_DMA 小于16M内存页框
ZONE_NORMAL 16M~896M内存页框
ZONE_HIGHMEM 大于896M内存页框
ZONE_DMA和ZONE_NORMAL区域包括的页框,通过线性的映射到内核线性地址空间。内核能够直接訪问(对应的内核页表早已经建立)。
ZONE_HIGHMEN 动态映射到内核线性地址空间。
为什么既有线性映射又有动态映射?
线性映射。保持了内核页表的稳定性,降低了TLB miss。动态映射使得内核能够訪问到全部的物理内存。
内核将页框的信息保存在类型为struct page的页描写叙述符中。全部的页描写叙述符存放在数组mem_map(下标即为页框号)中。
struct page {
unsigned long flags; //标志,同一时候高位用来指向页框所在的管理区,包括多达32个页框的状态标志
//比如:PG_highmem 页框至于ZONE_HIGHMEM管理区
// PG_slab 页框包括在slab中
atomic_t _count; //页框引用计数
atomic_t _mapcount; //页框中的页表项数目
unsigned long private; //在不同情况下,意义不同.当页框为空暇时,由伙伴系统使用
struct address_space *mapping;
unsigned long index; //在不同情况下,意义不同
struct list_head lru; //双向链表指针
};
每一个管理区都有自己的描写叙述符:
struct zone {
unsigned long free_pages; //管理区中空暇页数目
unsigned long pages_min //保留页框的数目 (保留页框池)
unsigned long pages_low;
unsigned long pages_high; //两个watermark,与回收页框,管理区分配器有关
struct per_cpu_pageset pageset[]; //(每CPU页框快速缓存)
spinklock_t lock; //保护该描写叙述符
unsigned long lowmem_reserve[] //在内存不足的临界情况下,管理区必须保留的页框数
struct free_area free_area[]; //管理区空暇页框块(伙伴系统)
struct page *zone_mem_map; //指向管理区第一个页框
unsigned long zone_start_pfn; //第一个页框的下标
char *name; //管理区名字(DMA, NORMAL, HighMem)
..........
}
保留的页框池
有内存分配请求时,假设有足够多的空暇内存可用,请求就被立马满足,否则,必须回收一些内存,堵塞当前执行的内核代码(内核控制路径),直到有内存被释放。
可是。中断或临界区内代码不能被堵塞。原子内存分配请求。
为了降低原子内存分配请求失败,内核保留了一个页框池。
保留池大小 = [sqrt(16*直接映射内存)] (KB)
ZONE_DMA、ZONE_NORMAL按各自大小比例贡献。
struct zone中的unsigned long pages_min
//保留页框的数目 (保留页框池)。
2.分区页框分配器
被称为分区页框分配器的内核子系统,处理对连续页框组的内存分配请求。
管理区分配器:接收动态内存分配和释放的请求。(搜索能满足请求的内存管理区,触发页框回收。
。。)
每CPU页框快速缓存:快速满足本地CPU发出的单一页框请求。
伙伴系统:解决外碎片问题。
2.1 请求和释放页框
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)
gfp_mask:一组标志,指明怎样寻找空暇的页框
order:请求分配2order个页框
若成功。返回第一个分配页框的页描写叙述符地址
区修饰符:
__GFP_DMA
__GFP_HIGHMEM
行为修饰符:
__GFP_WAIT 同意内核堵塞等待空暇页框的当前内核控制路径
__GFP_HIGH 同意内核訪问保留页框池
__GFP_COLD 所请求页框可能为”冷的“(每CPU快速缓存)
__GFP_REPEAT 内核重试内存分配直到成功为止
__GFP_NOFALL 与 __GFP_REPEAT 同样
__GFP_NORETRY 一次内存分配后失败后不再重试
void __free_pages(struct page *page, unsigned int order)
释放页框函数
2.2 伙伴系统
频繁的请求和释放不同大小的连续页框,必定导致在已分配页框的块内分散了很多小块的空暇页框。
由此产生的问题是:即使有足够的空暇页框能够满足请求。但当要分配一个大块的连续页框时,无法满足请求。
这就是著名的内存管理问题:外碎片问题。
linux採用伙伴系统算法来解决外碎片的问题。
把全部的空暇页框分组为11个块链表。
每一个块链表包括大小为1,2,4,8,16,32,64,128,256,512,1024个连续的页框。每一个块的第一个页框的物理地址必须是该块大小的整数倍。
比如,大小为16个页框的块,其起始地址为16x212的倍数。
算法原理:
通过一个简单的样例说明该算法的工作原理——
假设请求一个256个页框的块,先在256个页框的链表内检查是否有一个空暇的块。
假设没有这种块,算法会查找下一个更大的块,在512个页框的链表中找一个空暇块。假设存在这种块,内核就把512的页框分成两半,一半用作满足请求,还有一半插入256个页框的链表中。假设512个页框的块链表也没有空暇块,就继续找更大的块,1024个页框的块。
假设这种块存在。内核把1024个页框的256个页框用作请求。然后从剩余的768个中拿出512个插入512个页框的链表中。把最后256个插入256个页框的链表中。
页框块的释放过程:
内核试图把大小为b的一对空暇伙伴块合并为一个大小为2b的单独块。
满足下面条件的两个块成为伙伴:
两个块具有同样的大小:b。它们的物理地址连续。
第一个页框的物理地址为2xbx212的倍数。
该算法是迭代的,假设成功合并所释放的块。它会试图合并2b的块。
数据结构:
分配块:
static struct page *__rmqueue(struct zone *zone, unsigned int order)
{
struct free_area * area;
unsigned int current_order;
struct page *page;
unsigned int index;
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = zone->free_area + current_order;
if (list_empty(&area->free_list))
continue; //从order開始,搜索第一个满足要求的页框块
page = list_entry(area->free_list.next, struct page, lru);
list_del(&page->lru); //将找到的页框块的第一个页框描写叙述符从链表删除
index = page - zone->zone_mem_map;
if (current_order != MAX_ORDER-1)
MARK_USED(index, current_order, area);
zone->free_pages -= 1UL << order; //管理区的内存页框块降低
return expand(zone, page, index, order, current_order, area);
//假设current_order>order,把剩余未分配的页框块,插入合适的链表
}
return NULL;
}
static inline struct page * expand(struct zone *zone, struct page *page, unsigned long index, int low, int high, struct free_area *area)
{//曾经面提到的样例为例
unsigned long size = 1 << high;
while (high > low) {
area--;
high--;
size >>= 1;
BUG_ON(bad_range(zone, &page[size]));
list_add(&page[size].lru, &area->free_list);
MARK_USED(index + size, high, area)。
}
return page;
}
释放块:
page—须要释放页框块的首个页框的页描写叙述符
base—管理区首个页框的页描写叙述符
order—页框块页框个数的对数
area—页数为2order的块的链表
static inline void __free_pages_bulk (struct page *page, struct page *base,
struct zone *zone, struct free_area *area, unsigned int order)
{
unsigned long page_idx, index, mask;
if (order)
destroy_compound_page(page, order);
mask = (~0UL) << order;
page_idx = page - base; //块中第一个页框的索引號,相对于管理区的首个页框
if (page_idx & ~mask) //页框块的起始物理地址必须是2orderx212 所以page_idx须要是2order的倍数
BUG();
index = page_idx >> (1 + order);
zone->free_pages += 1 << order;
while (order < MAX_ORDER-1) {
struct page *buddy1, *buddy2;
BUG_ON(area >= zone->free_area + MAX_ORDER);
if (!__test_and_change_bit(index, area->map)) //检查伙伴块是否已经被分配
/* //?? ? ? ???
* the buddy page is still allocated.
*/
break;
/* Move the buddy up one level. */
buddy1 = base + (page_idx ^ (1 << order)); //伙伴块的索引號
buddy2 = base + page_idx;
BUG_ON(bad_range(zone, buddy1));
BUG_ON(bad_range(zone, buddy2));
list_del(&buddy1->lru);
mask <<= 1;
order++;
area++;
index >>= 1;
page_idx &= mask; //合并后块中第一个页框的索引
}
list_add(&(base + page_idx)->lru, &area->free_list);
}
2.3 每CPU页框快速缓存
内核常常请求和释放单个页框。
为了提高性能。每一个内存管理区定义了每CPU页框快速缓存。当中包括一些预先分配的页框它们被用于满足本地CPU发出的单一页框请求。
每一个内存管理区为每一个CPU提供了两个快速缓存:热快速缓存和冷快速缓存。
热快速缓存:它存在的页框中所包括的内容可能就在CPU硬件快速缓存中。
假设进程在分配页框后。就向页框中写,那么从热快速缓存中获得页框对性能有利。
假设页框将被DMA操作。则从冷快速缓存中获得页框。对性能有利(节约了热快速缓存)
数据结构:
struct zone {
struct per_cpu_pageset pageset[];
}
struct per_cpu_pageset {
int count; //快速缓存中页框的个数
int low; //下界,快速缓存须要补充
int high; //上界,快速缓存须要释放到伙伴系统中
int batch; //每次加入或释放的个数
struct list_head list //快速缓存中包括的页框描写叙述符链表
}
struct page *buffered_rmqueue(struct zone *zone, int order, int gfp_flags);
释放页框到每CPU页框快速缓存:
void fastcall free_hot_cold_page(struct page *page, int cold);
2.4 管理区分配器
管理区分配器是内存页框分配器的前端。必须找到一个包括足够多空暇页框的内存区,满足内存请求。
注意:
管理区分配器保护保留的页框池;
当内存不足,且同意堵塞当前内核控制路径时,触发页框回收算法;
保存小而珍贵的ZONE_DMA区域。
管理区分配器的核心:__alloc_pages()
3.高端内存页框的内核映射
到眼下为止所介绍的函数。都是获得一块连续的内存页框,要想訪问物理内存,必需要将页框映射到线性地址空间中。
过程:(1)分配物理页框。(2)得到未映射到不论什么物理页框的线性地址;(3)更新页表,使得线性地址映射到物理页框。
对于ZONE_DMA和ZONE_NORMAL区域,线性映射到内核地址空间前896M。
依据物理地址能够直接得到对应的线性地址。而且此部分的内核页表在系统初始化时已经建立。
__va((unsigned long)(page-mem_pag) << 12)
而且这样的映射是临时的,通过反复使用线性地址,使得整个高端内存可以在不同的时间被訪问。
出于不同的目的。内核使用三种不同的机制将页框映射到高端内存。
*永久内核映射
*暂时内核映射(固定映射)
*非连续内存分配
后128M的线性地址,被分为例如以下三个部分:
3.1 永久内核映射
永久内核映射同意内核建立高端页框到内核线性地址空间的长期映射。其可能堵塞当前进程,不能在中断处理程序等不同意堵塞的代码中调用。
永久内核映射使用内核页表中的一个专门的页表。页表的地址存放在pkmap_page_table变量中。页表的表项数目:(LAST_PKMAP 1024)
该页表映射的线性地址空间PKMAP_BASE 0xff800000UL (3G+1016M) ——>0x3fe页全局文件夹的第873项。
也就是,内核通过永久内核映射。最多同一时候訪问4M高端内存。
页表中每一项都有一个计数器:
static int pkmap_count[LAST_PKMAP];
计数器为0
相应的页表项没有映射到不论什么高端内存页框,而且是可用的。
计数器为1
相应的页表项没有映射到不论什么高端内存页框,但不能使用,由于自从它最后一次使用以来,其相应的TLB表项还未被刷新。
计数器为n
相应的页表项映射到一个高端内存页框。
void *kmap(struct page *page)
{
might_sleep();
if (page < highmem_start_page)
return page_address(page); //不在高端内存中,直接返回映射的线性地址
return kmap_high(page);
}
void fastcall *kmap_high(struct page *page)
{
unsigned long vaddr;
spin_lock(&kmap_lock);
vaddr = (unsigned long)page_address(page);
if (!vaddr)
vaddr = map_new_virtual(page);
pkmap_count[PKMAP_NR(vaddr)]++;
if (pkmap_count[PKMAP_NR(vaddr)] < 2)
BUG();
spin_unlock(&kmap_lock);
return (void*) vaddr;
}
static inline unsigned long map_new_virtual(struct page *page)
{
unsigned long vaddr;
int count;
start:
count = LAST_PKMAP;
/* Find an empty entry */
for (;;) {
last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
if (!last_pkmap_nr) {
flush_all_zero_pkmaps();
count = LAST_PKMAP;
}
if (!pkmap_count[last_pkmap_nr])
break; /* Found a usable entry */
if (--count)
continue;
/*
* Sleep for somebody else to unmap their entries
*/
{
DECLARE_WAITQUEUE(wait, current);
__set_current_state(TASK_UNINTERRUPTIBLE);
add_wait_queue(&pkmap_map_wait, &wait);
spin_unlock(&kmap_lock);
schedule();
remove_wait_queue(&pkmap_map_wait, &wait);
spin_lock(&kmap_lock);
/* Somebody else might have mapped it while we slept */
if (page_address(page))
return (unsigned long)page_address(page);
/* Re-start */
goto start;
}
}
vaddr = PKMAP_ADDR(last_pkmap_nr);
set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
//建立内核页表
pkmap_count[last_pkmap_nr] = 1;
set_page_address(page, (void *)vaddr);
return vaddr;
}
3.2 暂时内核映射
FIXADDR_START (3G + 1020M)
FIXADDR_TOP (0xfffff000UL) (除去最后一页)
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_IRQ0,
KM_IRQ1,
KM_SOFTIRQ0,
KM_SOFTIRQ1,
KM_TYPE_NR
};
同一个窗体永不会被两个不同的控制路径同一时候使用
void *kmap_atomic(struct page *page, enum km_type type)
{
enum fixed_addresses idx;
unsigned long vaddr;
/* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
inc_preempt_count(); //禁止内核抢占
if (page < highmem_start_page)
return page_address(page);
idx = type + KM_TYPE_NR*smp_processor_id();
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx); //获得线性地址
set_pte(kmap_pte-idx, mk_pte(page, kmap_prot)); //建立页表
__flush_tlb_one(vaddr);
return (void*) vaddr;
}
3.3 非连续内存区管理
主要特点:通过连续的线性地址訪问非连续的页框。
长处:非连续的页框,避免了外碎片;
缺点:打乱了内核页表。
Linux在为活动的交换区分配数据结构, 为模块分配空间某些I/O驱动程序等等方面都用到了非连续内存区
从high_memory(896M)到PKMAP_BASE(1016M)之间的线性地址空间用于非连续内存区。
high_memory与VMALLOC_START之间有8M(VMALLOC_OFFSET)的安全区。为了捕获对内存的越界訪问。
vmalloc区之间的4KB的安全区也是出于相同的考虑。
数据结构:
每一个非连续内存区都相应一个类型为 vm_struct 的描写叙述符
struct vm_struct {
void *addr; //内存区起始线性地址
unsigned long size; //内存区的大小加4096(内存区之间安全区的大小)
unsigned long flags;
struct page **pages; //页描写叙述符指针数组
unsigend int nr_pages; //指针数组长度(内存区页的个数)
unsigned long phys_addr; //该字段为0
struct vm_struct *next; //指向下一个vm_struct
}
分配非连续内存区
void *vmalloc(unsigend long size):
void *__vmalloc(unsigned long size, int gfp_mask, pgprot_t prot)
{
struct vm_struct *area;
struct page **pages;
unsigned int nr_pages, array_size, i;
size = PAGE_ALIGN(size); //(((addr)+PAGE_SIZE-1)&PAGE_MASK)
if (!size || (size >> PAGE_SHIFT) > num_physpages)
return NULL;
area = get_vm_area(size, VM_ALLOC);
//在VMALLOC_START和 VMALLOC_END之间寻找大小为(szie+4096)的空暇连续地址空间
if (!area)
return NULL;
nr_pages = size >> PAGE_SHIFT;
array_size = (nr_pages * sizeof(struct page *));
area->nr_pages = nr_pages;
area->pages = pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM));
if (!area->pages) {
remove_vm_area(area->addr);
kfree(area);
return NULL;
}
memset(area->pages, 0, array_size);
for (i = 0; i < area->nr_pages; i++) {
area->pages[i] = alloc_page(gfp_mask);
//vmalloc基于伙伴系统。 每次从伙伴系统分配一页
if (unlikely(!area->pages[i])) {
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;
goto fail;
}
}
if (map_vm_area(area, prot, &pages)) //建立页表
goto fail;
return area->addr;
fail:
vfree(area->addr);
return NULL;
}
前面讲的几种动态映射都会改动内核页表。
永久内核映射与非连续内存区改动内核页表后,进程页表中内核线性地址空间部分须要与其保持一致。
那么内核怎样确保对内核页表的改动,能传递到各个进程页表中?
===>>>
当内核建立新页表。
内核态的进程訪问内核线性地址。相应的页文件夹项为空。缺页发生。
缺页处理程序检查这个缺页的线性地址是否在主内核页表中。一旦缺页处理程序发现主内核页表对应的页文件夹项不为空。
就把页文件夹项的值复制到进程的对应位置,并恢复进程的运行。
当内核撤销线性地址到物理页框的映射,把页表项设为0;
内核态进程訪问线性地址,对应页表项为0。缺页发生,缺页处理程序觉得这种訪问是一个错误。
Linux内核剖析 之 内存管理的更多相关文章
- linux内核分析之内存管理
1.struct page /* Each physical page in the system has a struct page associated with * it to keep tra ...
- linux内核--用户态内存管理
在上一篇博客“内核内存管理”中,描述的内核内存管理的相关算法和数据结构,在这里简单描述用户态内存管理的数据结构和算法. 一,相关结构体 与进程地址空间相关的全部信息都包含在一个叫做“内存描述符”的数据 ...
- 《PHP内核剖析 - 变量/内存管理》
本文总结自: <PHP7 内核剖析 - 变量的内部实现> 一:变量的实现 - 变量是一个语言实现的基础. - 在PHP中,变量的组成部分为 变量名(zval) 变量值(zend_value ...
- 初探Linux内核中的内存管理
Linux内核设计与实现之内存管理的读书笔记 初探Linux内核管理 内核本身不像用户空间那样奢侈的使用内存; 内核不支持简单快捷的内存分配机制, 用户空间支持? 这种简单快捷的内存分配机制是什么呢? ...
- [转]linux内核分析笔记----内存管理
转自:http://blog.csdn.net/Baiduluckyboy/article/details/9667933 内存管理,不用多说,言简意赅.在内核里分配内存还真不是件容易的事情,根本上是 ...
- <Linux内核源码>内存管理模型
题外语:本人对linux内核的了解尚浅,如果有差池欢迎指正,也欢迎提问交流! 首先要理解一下每一个进程是如何维护自己独立的寻址空间的,我的电脑里呢是8G内存空间.了解过的朋友应该都知道这是虚拟内存技术 ...
- linux内核源码——内存管理:段页式内存及swap
os的内存管理大概可以分成两块:1.段页式管理(虚存)2.swap in 和 swap out 段页式管理 段式管理的图像:运行时重定位 多级页表的管理图像 块表加速 用户(程序员)希望用段,物理内 ...
- Linux内核笔记:内存管理
逻辑地址由16位segment selector和offset组成 根据segment selector到GDT或LDT中去查找segment descriptor 32位base,20位limit, ...
- Linux内核高端内存 转
Linux内核地址映射模型x86 CPU采用了段页式地址映射模型.进程代码中的地址为逻辑地址,经过段页式地址映射后,才真正访问物理内存. 段页式机制如下图. Linux内核地址空间划分 通 ...
随机推荐
- adobe acrobat 无效批注对象
http://blog.csdn.net/pipisorry/article/details/40894881 adobe acrobat一直弹出 无效批注对象 检查后提示:"本页面上的全部 ...
- Win7中安装Windows PowerShell 3.0
win7内置的powershell是2.0,现在已经明显落伍了,但win系统软件更新,需要解决依赖问题,so,按下面步骤安装即可. 1. 安装Microsoft .NET Framework 4.0的 ...
- Xilinx IP核的根目录地址,有datasheet 和仿真相关的资料
C:\Xilinx\14.7\ISE_DS\ISE\coregen\ip\xilinx\dsp\com\xilinx\ip Xilinx IP核的根目录地址,有datasheet 和仿真相关的资料
- hdu 1532 最大流
#include <cstdio> #include <iostream> #include <algorithm> #include <queue> ...
- ajaxfileupload异步上传附件添加參数的方法
1.js文件 // JavaScript Document jQuery.extend({ createUploadIframe: function(id, uri) { //create frame ...
- gitbook安装与使用
废话不说,直接主题: gitbook安装 =========== 1. 安装npm 从站点 https://nodejs.org/#download 下载node.js源码(点击绿色的INSTALL ...
- 浅谈CPU,GPU,TPU,DPU,NPU,BPU
https://www.sohu.com/a/191538165_777155 A12宣传的每秒5万亿次运算,用计算机语言描述就是5Tops. 麒麟970 NPU,根据资料是 1.92Tops. 麒麟 ...
- SourceInsight-查看java中接口对应的实现类
1.双击选中需要查看的接口名称,然后右击选择“Show in Relation Window” 2.然后在右侧会弹出一个Relation的窗口 如果没有列出对应的实现类,可以在接口名上再次右击,依次选 ...
- 服务器搭建2 VSFTP搭建FTP服务器
FTP服务器是平时应用最为广泛的服务之一.VSFTP是Very Secure FTP的缩写,意指非常安全的FTP服务.VSFTP功能强大,通过结合本地系统的用户认证模块及其多功能的配置项目,可以快速有 ...
- 常用的兼容IE和火狐FF等浏览器的js方法(js中ie和火狐的一些差别)
介绍了网页上常用的IE/火狐兼容性该页的做法,并给出了代码,相当实用了.为了方便大家阅读代码,以下以 IE 代替 Internet Explorer,以 MF/FF 代替 Mozzila Firefo ...