linux的内存管理
① linux进程内存管理
对于包含MMU的处理器而言,Linux系统提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。在Linux系统中,进程的4GB内存空间被分为两个部分——用户空间与内核空间。用户空间的地址一般分布为0~3GB(即PAGE_OFFSET,在Ox86中它等于OxC0000000),这样,剩下的3~4GB为内核空间,用户进程通常只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。用户进程只有通过系统调用(代表用户进程在内核态执行)等方式才可以访问到内核空间。
每个进程的用户空间都是完全独立、互不相干的,用户进程各自有不同的页表。而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间的虚拟地址到物理地址映射是被所有进程共享的,内核的虚拟空间独立于其他程序。
Linux中1GB的内核地址空间又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区这几个区域。
对于x86系统而言,一般情况下,物理内存映射区最大长度为896MB,系统的物理内存被顺序映射在内核空间的这个区域中。当系统物理内存大于896MB时,超过物理内存映射区的那部分内存称为高端内存(而未超过物理内存映射区的内存通常被称为常规内存),内核在存取高端内存时必须将它们映射到高端页面映射区。Linux保留内核空间最顶部FIXADDR_TOP~4GB的区域作为保留区。
当系统物理内存超过4GB时,必须使用CPU的扩展分页(PAE)模式所提供的64位页目录项才能存取到4GB以上的物理内存,这需要CPU的支持。加入了PAE功能的Intel Pentium Pro及以后的CPU允许内存最大可配置到64GB,它们具备36位物理地址空间寻址能力。
由此可见,对于32位的x86而言,在3~4GB之间的内核空间中,从低地址到高地址依次为:物理内存映射区→隔离带→vmalloc虚拟内存分配器区→隔离带→高端内存映射区→专用页面映射区→保留区。
② Linux 内核的内存管理 - 概念
Concepts overview — The Linux Kernel documentation
Linux中的内存管理是一个复杂的系统,经过多年的发展,它包含越来越多的功能,以支持从 MMU-less microcontrollers 到 supercomputers 的各种系统。
没有MMU内存管理的系统被称为 nommu ,它值得写一份专门的文档进行描述。
尽管有些概念是相同的,这里我们假设MMU可用,CPU可以将虚拟地址转换为物理地址。
计算机系统中的物理内存是有限资源,即便支持内存热插拔,其可以安装的内存也有限的。物理内存不一定必须是连续的;它可以作为一组不同的地址范围被访问。此外,不同的CPU架构,甚至同架构的不同实现对如何定义这些地址范围都是不同的。
这使得直接处理物理内存异常复杂,为了避免这种复杂性,开发了 虚拟内存 (virtual memory) 的概念。
虚拟内存从应用软件中抽象出物理内存的细节,只允许在物理内存中保留需要的信息 (demand paging) ,并提供一种机制来保护和控制进程之间的数据共享。
通过虚拟内存,每次内存访问都访问一个 虚拟地址 。当CPU对从系统内存读取(或写入)的指令进行解码时,它将该指令中编码的虚拟地址转换为内存控制器可以理解的物理地址。
物理内存被切分为 页帧 page frames 或 页 pages 。页的大小是基于架构的。一些架构允许从几个支持的值中选择页大小;此选择在内核编译时设置到内核配置。
每个物理内存页都可以映射为一个或多个 虚拟页(virtual pages) 。映射关系描述在 页表(page tables) 中,页表将程序使用的虚拟地址转换为物理内存地址。页表以层次结构组织。
最底层的表包含软件使用的实际内存页的物理地址。较高层的表包含较低层表页的物理地址。顶层表的指针驻留在寄存器中。
当CPU进行地址转换的时候,它使用寄存器访问顶级页表。
虚拟地址的高位,用于顶级页表的条目索引。然后,通过该条目访问下级,下级的虚拟地址位又作为其下下级页表的索引。虚拟地址的最低位定义实际页内的偏移量。
地址转换需要多次内存访问,而内存访问相对于CPU速度来说比较慢。为了避免在地址转换上花费宝贵的处理器周期,CPU维护着一个称为 TLB (Translation Lookaside Buffer)的用于地址转换缓存(cache)。通常TLB是非常稀缺的资源,需要大内存工作应用程序会因为TLB未命中而影响性能。
很多现代CPU架构允许页表的高层直接映射到内存页。例如,x86架构,可以通过二级、三级页表的条目映射2M甚至1G内存页。在Linux中,这些内存页称为 大页 (Huge) 。大页的使用显着降低了TLB的压力,提高了TLB命中率,从而提高了系统的整体性能。
Linux提供两种机制开启使用大页映射物理内存。
第一个是 HugeTLB 文件系统,即 hugetlbfs 。它是一个伪文件系统,使用RAM作为其存储。在此文件系统中创建的文件,数据驻留在内存中,并使用大页进行映射。
关于 HugeTLB Pages
另一个被称为 THP (Transparent HugePages) ,后出的开启大页映射物理内存的机制。
与 hugetlbfs 不同,hugetlbfs要求用户和/或系统管理员配置系统内存的哪些部分应该并可以被大页映射;THP透明地管理这些映射并获取名称。
关于 Transparent Hugepage Support
通常,硬件对不同物理内存范围的访问方式有所限制。某些情况下,设备不能对所有可寻址内存执行DMA。在其他情况下,物理内存的大小超过虚拟内存的最大可寻址大小,需要采取特殊措施来访问部分内存。还有些情况,物理内存的尺寸超过了虚拟内存的最大可寻址尺寸,需要采取特殊措施来访问部分内存。
Linux根据内存页的使用情况,将其组合为多个 zones 。比如, ZONE_DMA 包含设备用于DMA的内存, ZONE_HIGHMEM 包含未永久映射到内核地址空间的内存, ZONE_NORMAL 包含正常寻址内存页。
内存zones的实际层次架构取决于硬件,因为并非所有架构都定义了所有的zones,不同平台对DMA的要求也不同。
多处理器机器很多基于 NUMA (Non-Uniform Memory Access system - 非统一内存访问系统 )架构。 在这样的系统中,根据与处理器的“距离”,内存被安排成具有不同访问延迟的 banks 。每个 bank 被称为一个 node ,Linux为每个 node 构造一个独立的内存管理子系统。 Node 有自己的zones集合、free&used页面列表,以及各种统计计数器。
What is NUMA?
NUMA Memory Policy
物理内存易失,将数据放入内存的常见情况是读取文件。读取文件时,数据会放入 页面缓存(page cache) ,可以在再次读取时避免耗时的磁盘访问。同样,写文件时,数据也会被放入 页面缓存 ,并最终进入存储设备。被写入的页被标记为 脏页(dirty page) ,当Linux决定将其重用时,它会将更新的数据同步到设备上的文件。
匿名内存 anonymous memory 或 匿名映射 anonymous mappings 表示没有后置文件系统的内存。这些映射是为程序的stack和heap隐式创建的,或调用mmap(2)显式创建的。通常,匿名映射只定义允许程序访问的虚拟内存区域。读,会创建一个页表条目,该条目引用一个填充有零的特殊物理页。写,则分配一个常规物理页来保存写入数据。该页将被标记为脏页,如果内核决定重用该页,则脏页将被交换出去 swapped out 。
纵贯整个系统生命周期,物理页可用于存储不同类型的数据。它可以是内核内部数据结构、设备驱动DMA缓冲区、读取自文件系统的数据、用户空间进程分配的内存等。
根据内存页使用情况,Linux内存管理会区别处理。可以随时释放的页面称为 可回收(reclaimable) 页面,因为它们把数据缓存到了其他地方(比如,硬盘),或者被swap out到硬盘上。
可回收页最值得注意的是 页面缓存 和 匿名页面 。
在大多数情况下,存放内部内核数据的页,和用作DMA缓冲区的页无法重用,它们将保持现状直到用户释放。这样的被称为 不可回收页(unreclaimable) 。
然而,在特定情况下,即便是内核数据结构占用的页面也会被回收。
例如,文件系统元数据的缓存(in-memory)可以从存储设备中重新读取,因此,当系统存在内存压力时,可以从主内存中丢弃它们。
释放可回收物理内存页并重新调整其用途的过程称为 (surprise!) reclaim 。
Linux支持异步或同步回收页,取决于系统的状态。
当系统负载不高时,大部分内存是空闲的,可以立即从空闲页得到分配。
当系统负载提升后,空闲页减少,当达到某个阈值( low watermark )时,内存分配请求将唤醒 kswapd 守护进程。它将以异步的方式扫描内存页。如果内存页中的数据在其他地方也有,则释放这些内存页;或者退出内存到后置存储设备(关联 脏页 )。
随着内存使用量进一步增加,并达到另一个阈值- min watermark -将触发回收。这种情况下,分配将暂停,直到回收到足够的内存页。
当系统运行时,任务分配并释放内存,内存变得碎片化。
虽然使用虚拟内存可以将分散的物理页表示为虚拟连续范围,但有时需要分配大的连续的物理内存。这种需求可能会提升。例如,当设备驱动需要一个大的DMA缓冲区时,或当THP分配一个大页时。
内存地址压缩(compaction ) 解决了碎片问题。
该机制将占用的页从内存zone的下部移动到上部的空闲页。压缩扫描完成后,zone开始处的空闲页就并在一起了,分配较大的连续物理内存就可行了。
与 reclaim 类似, compaction 可以在 kcompactd守护进程中异步进行,也可以作为内存分配请求的结果同步进行。
在存在负载的机器上,内存可能会耗尽,内核无法回收到足够的内存以继续运行。
为了保障系统的其余部分,引入了 OOM killer 。
OOM killer 选择牺牲一个任务来保障系统的总体健康。选定的任务被killed,以期望在它退出后释放足够的内存以继续正常的操作。
③ 内存管理:一文读懂Linux内存组织结构及页面布局
1、内存是什么?
1) 内存又称主存,是 CPU 能直接寻址的存储森郑空间,由半导体器件制成;
2) 内存的特点是存取速率快,断电一般不保存数据,非持久化设备;
2、内存的作用
1) 暂时存放 cpu 的运算数据
2) 硬盘等外部存储器交换的数据
3) 保障 cpu 计算机的稳定性和高性能
1、linux 内存地址空间 Linux 内存管理全貌
2、内存地址——用户态&内核态
3、内存地址——MMU 地址转换
4、内存地址——分段机制
1) 段选择符
更多Linux内核视频教程文档资料免费领取后台私信【 内核 】自行获取。
内核学习网站:
Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂
2) 分段实现
5、内存地址——分页机制(32 位)
6、用户态地址空间
7、内核态地址空间
8、进程内存空间
内存管理算法 ——对讨厌自己管理内存的人来说是天赐的礼物
1、内存碎片
1) 基本原理
2) 如何避免内存碎片
2、伙伴系统算法——组织结构
1) 概念
2) 外部碎片
3、伙伴系统算法——申请和回收
1) 申请算法
2) 回收算法
3) 条件
4、如何分配 4M 以上内存?
1) 为何限制大旦春盯块内存分配
2) 内核中获取 4M 以上大内存的方法
5、伙伴系统——反碎片机制
1) 不可移动页
2) 可回收页
6、slab 算法——基本原理
1) 基本概念
2) 内部碎片
7、slab 分配器的结构
详细参考:
经典|图解Linux内存性能优化核心思想
8、slab 高速缓存
1) 普通高速缓存
2) 专用高速缓存
9、内核态内存池
1) 基本原理
2) 内核 API
10、用户态内存池
1) C++ 实例
11、DMA 内存
1) 什么是 DMA
2) DMA 信号
out of memory 的时代过去了吗?no,内存再充足也不可任性使用。
1、内存的使用场景
2、用户态内存分配函数
a) 如果当前连续内存块足够 realloc 的话,只是将 p 所指向的空间扩大,并返回模和 p 的指针地址。这个时候 q 和 p 指向的地址是一样的
b) 如果当前连续内存块不够长度,再找一个足够长的地方,分配一块新的内存,q,并将 p 指向的内容 到 q,返回 q。并将 p 所指向的内存空间删除
3、内核态内存分配函数
4、malloc 申请内存
5、缺页异常
6、用户进程访问内存分析
7、共享内存
1) 原理
2) shm 接口
1、C 内存泄露
2、C 野指针
3、C 资源访问冲突
4、STL 迭代器失效
错误示例:删除当前迭代器,迭代器会失效
正确示例:迭代器 erase 时,需保存下一个迭代器
5、C++ 11 智能指针
(1)原理分析:
(2)数据结构:
(3)使用方法:
6、C++ 11 更小更快更安全
六、 如何查看内存
可以通过 cat /proc/slabinfo 命令查看
可以通过 /proc/sys/vm/drop_caches来释放
④ Linux存储管理方式
这种方式中,将用户程序的地址空间,注意,是 用户程序的地址空间 分为若干个固定大小的区域,成为“页”或“页面”。我们可以知道,这也页其实是不存在的,只是一种划分内存空间的方法。也就是说,这种方式将用户的程序 “肢解” 了,分成很多个小的部分,每个部分称为一个“页”。
将逻辑地址的前n位作为页号,后面32-n位作为页内偏移量。
由于进程的最后一页经常装不满一个块,从而形成了不可利用的碎片,称之为 “页内碎片” 。
作用:实现页号到物理号的地址映射。
页表是记录逻辑空间(虚拟内存)中每一页在内存中对应的物理块号。但并非每一页逻辑空间都会实际对应着一个物理块,只有实际驻留在物理内存空间中的页才会对应着物理块。
系统会为每一个进程建立一张页表,页表是需要一直驻留在物理内存中的(多级页表除外),另外页表的起址和长度存放在 PCB(Process Control Block)进程控制结构体中。
可以在页表的表项中设置相关的权限控制字段,例如设置存取控制字段,用于保护该存储块的读写;若存取控制字段为2位,则可以设置读/写、只读和只执行等存取方式。
物理块是实实在在存在于内存中的:
由于执行频率高,要求效率比较高,需要使用硬件实现。
在系统中设置一个 页表寄存器(PTR) ,其中存放页表在内存的起始地址和页表的长度。平时进程未执行的时候,页表的起始地址和页表长度放在本进程的PCB中。当调度程序调度到某个进程的时候,才将这两个数据装入 页表寄存器 。
变换过程:
快表的变换机构
为了提高地址变换速度,可在地址变换机构中增设一个具有并行查询能力的特殊高速缓冲寄存器,又称为"联想寄存器"或者“快表”。俗称TLB。
快表与页表的功能类似,其实就是将一部分页表存到 CPU 内部的高速缓冲存储器 Cache。CPU 寻址时先到快表查询相应的页表项形成物理地址,如果查询不到,则到内存中查询,并将对应页表项调入到快表中。但,如果快表的存储空间已满,则需要通过算法找到一个暂时不再需要的页表项,将它换出内存。
由于成本的关系,快表不可能做得很大,通常只存放 16~512 个页表项,这对中、小型作业来说,已有可能把全部页表项放在快表中;但对于大型作业而言,则只能将其一部分页表项放入其中。由于对程序和数据的访问往往带有局限性,因此,据统计,从快表中能找到所需页表项的概率可达 90% 以上。这样,由于增加了地址变换机构而造成的速度损失可减少到 10% 以下,达到了可接受的程度。
我们可以采用这样两个方法来解决这一问题:
① 对于页表所需的内存空间,可采用离散分配方式,以解决难以找到一块连续的大内存空间的问题;
② 只将当前需要的部分页表项调入内存,其余的页表项仍驻留在磁盘上,需要时再调入。
二级页表的页表项:
过程:
在采用两级页表结构的情况下,对于正在运行的进程,必须将其外层页表调入内存,而对于内页表则只需调入一页或几页。为了表征某页的页表是否已经调入内存,还应在外层页表项中增设一个状态位 S,其值若为 0,表示该页表分页不在内存中,否则说明其分页已调入内存。进程运行时,地址变换机构根据逻辑地址中的 P1去查找外层页表;若所找到的页表项中的状态位为 0,则产生一个中断信号,请求 OS 将该页表分页调入内存。
多级页表和二级页表类似。多级页表和二级页表是为了节省物理内存空间。使得页表可以在内存中离散存储。(单级页表为了随机访问必须连续存储,如果虚拟内存空间很大,就需要很多页表项,就需要很大的连续内存空间,但是多级页表不需要。)
为什么引入分段存储管理?
引入效果:
它将用户程序的地址空间分为若干个大小不同的的段,每个段可以定义一组完整的信息。
段号表示段名,每个段都从0开始编址,并且采用一段连续的地址空间。
在该地址结构中,允许一个作业最长有64K个段,每个段的最大长度为64KB。
在分段式存储管理系统中,为每一个分段分配一个连续的分区。进程的各个段,可以离散地装入内存中不同的分区中。
作用:实现从逻辑地址到物理内存区的映射。
为了保证程序能够正常运行,就必须能够从物理内存中找出每个逻辑段所对应的位置。为此在系统中会为每一个进程建立一张 段表 。每个段在表中有一个表项,其中记录了该段在内存中的起始地址和段的长度。一般将段表保存在内存中。
在配置了段表之后,执行的过程可以通过查找段表,找到每一个段所对应的内存区。
为了实现进程从逻辑地址到物理地址的变换功能,在系统设置了段表寄存器,用于存放段表的起始地址和段表长度TL。
在进行地址变换时,系统将逻辑地址中的段号与段表长度TL 进行比较。若 S > TL,表示段号太大,是访问越界,于是产生越界中断信号。若未越界,则根据段表的始址和该段的段号,计算出该段对应段表项的位置,从中读出该段在内存的起始地址。然后,再检查段内地址 d 是否超过该段的段长 SL。若超过,即 d>SL,同样发出越界中断信号。若未越界,则将该段的基址 d 与段内地址相加,即可得到要访问的内存。
分页和分段系统相似之处:两者都采用离散分配方式,且都是通过地址映射机构实现地址变换。
但在概念上两者完全不同,主要表现在下述三个方面:
分页系统以页面作为内存分配的基本单位,能有效地提高内存利用率,而分段系统以段作为内存分配的基本单位,它能够更好地满足用户多方面的需要。
段页式地址结构由段号、段内页号及页内地址三部分所组成
段页式系统的基本原理是分段和分页原理的结合,即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。如下图展示了一个作业地址空间的结构。该作业有三个段:主程序段、子程序段和数据段;页面大小为 4 KB:
在段页式系统中,为了实现从逻辑地址到物理地址的变换,系统中需要同时配置段表和页表。段表的内容与分段系统略有不同,它不再是内存始址和段长,而是页表始址和页表长度。下图展示出了利用段表和页表进行从用户地址空间到物理(内存)空间的映射。
在段页式系统中,为了便于实现地址变换,须配置一个段表寄存器,其中存放段表始址和段长 TL。进行地址变换时,首先利用段号 S,将它与段长 TL 进行比较。若 S < TL,表示未越界,于是利用段表始址和段号来求出该段所对应的段表项在段表中的位置,从中得到该段的页表始址,并利用逻辑地址中的段内页号 P 来获得对应页的页表项位置,从中读出该贝所在的物理块号 b,再利用块号 b 和页内地址来构成物理地址。
在段页式系统中,为了获得一条指令或数据,须三次访问内存。第一次访问是访问内存中的段表,从中取得页表始址;第二次访问是访问内存中的页表,从中取出该页所在的物理块号,并将该块号与页内地址一起形成指令或数据的物理地址;第三次访问才是真正从第二次访问所得的地址中取出指令或数据。
显然,这使访问内存的次数增加了近两倍。为了提高执行速度,在地址变换机构中增设一个高速缓冲寄存器。每次访问它时,都须同时利用段号和页号去检索高速缓存,若找到匹配的表项,便可从中得到相应页的物理块号,用来与页内地址一起形成物理地址:若未找到匹配表项,则仍需第三次访问内存。
参考链接: