内存问题分析(一)-内存管理基础(上)

本篇开始总结内存问题的分析,在分析之前先简单梳理下内存的基础知识。

一、虚拟内存

在早期的计算机中,程序是直接运行在物理内存上的。这样带来不少问题:
地址空间不隔离存在安全性问题、超过物理内存大小的内存需求无法得到更好满足,分配空闲内存的位置无法确定带来了重定位问题等。

为解决以上问题,引入了虚拟内存概念:
它是程序和物理内存中引入的一个中间层,属于内存管理策略的范畴。

虚拟内存

程序都有自己独立的进程地址空间,且程序认为它拥有连续的可用的内存(一个连续完整的虚拟地址空间,但不保证物理内存连续,物理内存不够的情况下,部分数据还会暂时存储在外部磁盘存储器上,在需要时进行数据交换),虚拟内存与物理内存直接建立映射关系来一一对应。

而Linux的内存管理就是建立在虚拟内存概念之上。

1.1 虚拟内存划分

从Linux操作系统层次上,可将Linux虚拟内存划分为用户空间和内核空间。以32位操作系统为例,最大寻址范围是4G,也就是整个虚拟地址空间是4G,Linux简化了分段机制,使得虚拟地址与线性地址总是一致的。Linux一般把这个4G的地址空间划分为两个部分:其中 0~3G为用户程序地址空间,虚地址0x00000000到0xBFFFFFFF,供各个进程使用;3G~4G为内核的地址空间,虚拟地址 0xC0000000到0xFFFFFFFF, 供内核使用。

这里有两点:

  • 用户进程通常情况下只能访问用户空间( 0~3G)的虚拟地址,不能访问内核空间的虚拟地址。例外情况只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。

  • 每个进程的用户空间(0-3G)完全独立、互不相干,内核空间(3G-4G)则由则由所有进程以及内核共享。

1.2 地址介绍

物理地址:内存条的单元地址。
逻辑地址:机器语言指令中用来指定一个操作数或者是一条指令的地址。
线性地址(虚拟地址):内存管理创造的一种地址。

流程:

地址转换流程

1.3 地址间映射方案

从上面流程可知,地址转换之间存在两种映射方案:分段分页

分段:使用了大小可变的块来管理内存。适合处理复杂系统的逻辑分区,映射的段表存储在线性地址空间。

分段方案

分页:使用了大小不变的块来管理内存。适合管理物理内存,映射的页表保存在物理地址空间。

分页方案

这里重点再看看虚拟地址查询物理地址过程:

虚拟地址与物理地址通过页表建立映射关系,CPU通过MMU(Memory Management Unit :内存管理单元)访问页表来查询虚拟地址对应的物理地址。

MMU内存管理

页表结构:

页表数据结构组成

依次按顺序判断:是否命中(命中:想要的数据在内存中)、是否满足RWX权限、是否满足User/Kernel权限,只要一项不满足,MMU会给CPU发出page fault,CPU自动跳到fault的代码去处理fault。全满足,那么MMU就去访问内存条上对应的地址。

二、内存组织与划分

2.1 页(page)

内核把页作为内存管理的基本单位。MMU也是以页为单位来管理页表。大多数32位体系结构支持4KB的页,而64位支持8KB的页(可通过命令来查看系统page大小:getconf -a | grep -i 'page')。内核中用struct page来表示系统中的每个物理页。

2.2 区(zone)

由于硬件限制,内核对特性不同的页是区别对待的,内核将内存按地址的顺序分成了不同的区,有的硬件只能访问有专门的区。区的划分本身没有任何物理意义,只不过是内核为了管理页而采取的一种逻辑上的分组。

主要关注的区有3个:

描述 物理内存
ZONE_DMA 直接内存访问,无需映射 <16MB
ZONE_NORMAL 一一对应映射页 16~896MB
ZONE_HIGHMEM 动态映射页 >896MB

Linux将4G的线性地址空间分为2部分,0-3G为user space,3G-4G为kernel space。以上三个区都是针对这1G的kernel space而言的。

kernel space映射方案

总结:
对0-3G的用户空间来说,其实不太关注物理地址是否连续,连续不连续都是在虚拟地址层面上谈的,区别也就是查询和插入的效率差别。

对3G-4G的内核空间来说,详细划分了三个区来满足各种物理内存需求。

DMA zone:直接访问物理内存,不需要映射,可以满足某些硬件设备的内存需求。
Normal zone:虚拟地址与物理地址是一一映射关系,如果需要连续物理内存这部分能满足。
High zone:虚拟地址与物理地址是动态映射关系,它的意义是为了能够访问所有的物理地址空间(1G空间显然无法满足,所以需要出一块动态映射区域),因此这部分内存不一定能满足连续物理内存需求,但是它提升了物理地址空间访问范围。

注:供硬件设备使用的物理内存地址必须是连续的,而供软件使用的物理内存地址则不要求必须是连续的。

三、内存分配

内存按page组织按zone划分之后,接下来看看如何分配内存。

3.1内存分配算法

1)Buddy算法

把空闲的页以2的n次方为单位进行管理,Buddy算法最主要的的特点任何时候区域里的空闲内存都能以2的n次方进行拆分或合并。整个kernel space都采用buddy算法进行管理,因此Linux最底层的内存申请都是以2n 为单位的(page)。

例如,假设ZONE_NORMAL有16页内存(24),此时有人申请一页内存,Buddy算法会把剩下的15页拆分成8+4+2+1,放到不同的链表中去。此时再申请4页,直接给4页,若再申请4页,则从8页中给4页,正好剩下4页。Buddy算法的精髓在于任何正整数都可以拆分成2的n次方之和。

通过/proc/buddyinfo可以看到内存空闲的一些情况:
buddy info
buddy order数据组织结构

Buddy算法的优点是避免了内存的外部碎片,但是长期运行后,大片的内存会比较少,而1页,2页,4页这种内存会非常多,当我们分配大片连续内存的时候就会出问题。换句话说就是以产生内部碎片为代价来避免外部碎片的产生。 Linux针对大内存的物理地址分配,采用Buddy伙伴算法,如果是针对小于一个page的内存,频繁的分配和释放,则不宜用Buddy伙伴算法。

注:所谓“内部碎片”,是指系统已经分配给用户使用、用户自己没有用到的那部分存储空间;所谓“外部碎片”,是指系统无法把它分配出去供用户使用的那部分存储空间。

2)slab算法

频繁的分配/释放内存必然导致系统性能的下降,所以有必要为频繁分配/释放的对象建立高速缓存。linux中的高速缓存是用所谓 slab 层来实现的,slab层即内核中管理高速缓存的机制。

整个slab层的原理如下:

  • 可以在内存中建立各种对象的高速缓存(比如进程描述相关的结构 task_struct 的高速缓存)。
  • 除了针对特定对象的高速缓存以外,也有通用对象的高速缓存。
  • 每个高速缓存中包含多个 slab,slab用于管理缓存的对象。
  • slab中包含多个缓存的对象,物理上由一页或多个连续的页组成。
slab组织关系

文件接口:/proc/slabinfo

cat /proc/slabinfo

上图所示为slabinfo文件的内容,第一行为表头:

Name Object name
Active_objs 已经激活的投入使用的object个数
Num_objs 为这个object分配的小内存块个数
Objsize 每一个内存块的大小
Objperslab 每一个Slab分区包含的object个数
Pagesperslab 每个Slab分区包含的page的个数
Active_slabs 已经激活的投入使用的Slab分区个数
Num_slabs 为这个object分配的Slab分区个数

最后再说一句,slab只用于分配低端内存,所分配的内存也只会被映射到物理内存映射区,所以vmalloc跟slab一毛钱关系都没有。

3.2 内存分配函数

1)按页获取(最原始方法)

以下分配内存的方法参见:<linux/gfp.h>

方法 描述
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,返回指向其逻辑地址的指针

alloc** 方法和 get** 方法的区别在于,一个返回的是内存的物理地址,一个返回内存物理地址映射后的逻辑地址。

如果无须直接操作物理页结构体的话,一般使用 get** 方法。

2)按字节获取(用的最多的获取方法)

方法 描述
kmalloc 分配的内存物理地址是连续的,虚拟地址也是连续的。分配小块内存,分配效率高。
vmalloc 分配的内存物理地址是不连续的,虚拟地址是连续的。分配大块内存,分配效率低。

尽管只有很少的硬件设备使用内存的场合需要用到连续的物理内存,但是很多内核代码还是使用kmalloc来分配内存而不是vmalloc主要还是出于性能考虑。在映射效率上,kmalloc明显高于vmalloc。kmalloc的物理地址和虚拟地址之间的映射比较简单,只需要将物理地址的第一页和虚拟地址的第一页关联起来即可。而vmalloc由于物理地址是不连续的,所以要将物理地址的每一页都和虚拟地址关联起来才行。当然除非是不得已需要大块内存时会考虑使用vmalloc。

3)slab层获取(效率最高的获取方法)

这里主要是针对高速缓存来处理。

方法 描述
kmem_cache_create 高速缓存的创建
kmem_cache_alloc 从高速缓存中分配对象
kmem_cache_free 向高速缓存释放对象
kmem_cache_destroy 高速缓存的销毁

总结:

在众多的内存分配函数中,如何选择合适的内存分配函数很重要,下面总结了一些选择的原则:

应用场景 分配函数选择
如果需要物理上连续的页 选择低级页分配器或者 kmalloc 函数
如果kmalloc分配是可以睡眠 指定 GFP_KERNEL 标志
如果kmalloc分配是不能睡眠 指定 GFP_ATOMIC 标志
如果不需要物理上连续的页 vmalloc 函数 (vmalloc 的性能不如 kmalloc)
如果需要高端内存 alloc_pages 函数获取 page 的地址,在用 kmap 之类的函数进行映射
如果频繁撤销/创建教导的数据结构 建立slab高速缓存

3.3 用户态函数

函数 描述
malloc 动态内存分配,用于在堆上申请一块连续的指定大小的内存块区域
mmap 通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
四、缺页中断

在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,然后进行故障处理,排除异常之后原先引起的异常的指令就可以继续执行,而不再产生异常。

缺页中断处理:

do_page_fault是缺页中断的核心函数,主要工作交给__do_page_fault处理,然后进行一些异常处理__do_kernel_fault和__do_user_fault。__do_page_fault主要工作交给handle_mm_fault;handle_mm_fault的核心又是handle_pte_fault。
handle_pte_fault()函数根据页表项pte所描述的物理页框是否在物理内存中,分为两大类:

  • 请求调页:被访问的页框不在主存中,那么此时必须分配一个页框,分为线性映射、非线性映射、swap情况下映射。

  • 写实复制:被访问的页存在,但是该页是只读的,内核需要对该页进行写操作,此时内核将这个已存在的只读页中的数据复制到一个新的页框中。

把缺页中断处理当成一个黑盒,就是采取一切手段让你需要访问的页面存在于内存中,并且能正常读写,显然这个过程是耗时的。

内容有点多,打算分上下两篇来总结,上篇就先总结到这吧。这里主要是梳理了下基本概念,对细节感兴趣的可以自己去撸Linux内核。

参考:
《Linux内核设计与实现》
《奔跑吧Linux内核 基于Linux4.x内核源代码问题分析》
https://www.cnblogs.com/wang_yb/archive/2013/05/23/3095907.html
https://www.cnblogs.com/wuchanming/p/4756911.html
http://www.wowotech.net/memory_management/233.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 1 内存寻址 1.1 物理地址、虚拟地址以及线性地址 物理地址: 物理内存的内存单元地址 虚拟地址: 程序员看到的...
    疯狂小王子阅读 2,795评论 3 21
  • 1、Linux内存页管理 Linux内核管理物理内存是通过分页机制实现的,它将整个内存划分成4K大小页,作为使分配...
    gbmaotai阅读 1,406评论 0 2
  • 本文转载自 https://juejin.im/post/59f8691b51882534af254317 参考:...
    xingdong阅读 2,711评论 0 3
  • 1. 用户空间 通常 32 位 Linux 虚拟地址空间划分, 0-3GB为用户空间,3GB-4GB为内核空间。每...
    Fly_Li阅读 1,696评论 0 5
  • 01. 不知道从什么时候我就开始流浪,我从来都不知道是谁生的我,我想,我大抵是被人们称为流浪狗的那个群体。 我最喜...
    遥之无忘阅读 1,020评论 0 7