linux 内存管理 (一) 基本的一些概念

该文章参考宋宝华老师的内存管理课程,详细可以去听阅码场宋老师的课程。

本文将从 从硬件原理  到内核实现   配置内存的参数(内存什么时候回收,脏数据什么时候收回?)  从应用程序上的内存泄漏 还有工程中一些常遇见的问题 ,比如 DMA  的内存从哪里来?连续内存分配器?来理解内存管理。

1、硬件原理和分页管理

几个概念:

内存空间和内存的区别

CPU 访问外面的资源,有两种方法,第一种是通过 指针地址的方式去访问内存空间,另外一种是通过 IN/OUT 的方式去访问 IO空间(有一些寄存器),这一种方式是X86所特有的,在RISC中没有。

内存空间分为两部分,内存和寄存器,寄存器就是cpu 里常见的 usb gpio  i2c  spi 等控制器,由此可见,内存和内存空间不是一回事。如果通过 i2c 又接一个外设,那外设的寄存器和 内存没有半毛钱的关系,因为它的寄存器是通过 cpu 的i2c 控制器访问的,和 内存没有直接的关系。

虚拟地址 / 物理地址

mmu 是一个硬件,负责把虚拟地址转化为物理地址。

所谓一花一世界,一叶一菩提,相同的事物在不同的角度可能会有不同的看法,对于物理地址,虚拟地址的概念也是如此。

物理地址是MMU的视角所看到的内存地址。虚拟地址是存在MMU的前提下CPU和程序员所看到的内存地址,我们实际编程的时候操作的就是虚拟地址。

 内存里面有很多页表,这个页表的基地址都会存在 MMU 里的寄存器上,当cpu 切到 QQ 程序的时候,MMU 就会把页表切换到 QQ的页表,

当cpu 切到 wechat 程序的时候,MMU 就会把页表切换到 wechat 的页表 , 每次切换的时候,MMU里的基地址都会被重写,MMU 就是负责查找页表,将cpu的虚拟地址 和 物理地址的映射关系找出来,由此可见,同样的虚拟地址,int a ; 的地址是 1G, QQ 和 Wechat都有一个1G的虚拟地址,但是它们的物理地址是不一样的。这也是为什么每个进程都有一个4G的虚拟地址空间,那是因为进程切换的时候,页表就切换了。总之,一旦开启了MMU , cpu 就只知道虚拟地址,不知道物理地址,谁来做物理地址和虚拟地址的转换呢?这是硬件MMU来做的。一定是硬件来做的,不是cpu来做的,这个查找页表的过程一定是硬件来做的,软件是做不了的,CPU来做,负担太重了,CPU 每看到一个变量a , 都要去查它的页表吗,这是不可能的,都是MMU硬件帮它去查的。

最简单的页表就是一个1维数组,页表一般都是2维 3 4 维的,为了方便理解原理,我们把页表想象成1维的,当我们CPU去访问一个地址   0x1234560 (这里注意,CPU发出的地址一定是 虚拟地址)的时候,一般的页表里面,每一页都是4k,

所以 560正好12位,显然是页内偏移,CPU在访问的时候,会直接把页内偏移直接删掉,那MMU就去查这个一维数组的第 0x1234行,每一行4个字节(32位处理器),比如说查到的物理地址是 1M ,那么实际的物理地址就是

1M + 页内偏移560 .  

注意:CPU发出的地址一定是 虚拟地址 , int * p =1M . 指针一定是虚拟地址,物理地址不是一个指针,是一个整数!大家可以看内核里面,关于物理地址的定义,都是一个 u32 或者 u64类型的一个整数。指针都是cpu的角度去发出的一个地址,

CPU 不肯能直接通过物理地址访问到寄存器,也不可能直接通过物理地址访问到内存,反正内存空间的东西,CPU 只能通过MMU来访问,一切都要通过虚拟地址。当然虚拟地址是可以等于物理地址的,这只是在页表里填的地址和物理地址一样而已。

MMU 通过查页表就将虚拟地址转化为了物理地址。

页表的结构,一个32位的处理器,高20位是来存储物理地址的,比如我们去访问一个 0x1234560的虚拟地址的时候,页内偏移是 560 ,也就是 页表中只需要存储 高20位的物理地址就行,那页表中剩余的12位存储什么呢?是非常重要的东西。

存储物理地址的权限,RWX , 还有 kernel/ user + kernel 权限 。当你非法操作的时候,就会导致一个 page_fault ,segment fault , 这里的内存管理系统中的 权限管理是非常重要的,这里举一个栈溢出,产生page fault的例子,也是黑客进行缓存攻击的一个案例

我的主程序 一直在接收报文,其中的一个函数 func 中,定义了一个数组a, 它最多能够存储40个字节,如果下面的操作,往a地址拷贝了更多的数据,因为栈是向上增长的,可能会改变到 func的返回值,如果这个写的缓存够大,

覆盖到你的报文 data 中去了,当然你写的缓存可能是一串可执行的代码,可能是盗取密码,盗取密钥的代码,你想去执行,但是 data 是存在数据段上的,数据段在内存上的权限是 RW , 没有可执行的权限,这时内核就会报一个 pagefault

你的程序就挂掉了,这是内存管理安全性的一个保障。

另外一个例子,两个文件 main.c    g.c 

main.c 如下

g.c 如下

 编译  gcc  main.c  g.c 

这个结果是 main.c 里对一个 const变量进行操作,被编译器拦截了。如果把  main.c 的 g=1 注释掉  重新编译,就可以编译通过(g.c 中不知道 g是const的)。但是在执行的时候会有 segment fault.

后面是内存管理来做的。segment falut 就是 应用程序收到了内核发出的 信号11  SIGGEGV  (kill -l 可以查看)

如果要证实程序就是死在这一句呢,可以用 gdb来跑一下,要学会自己调试。

这里总结一下:

在linux 里面 不同于实时系统的 非常重要的一点就是  内核就是内核,应用就是应用 ,内核空间是用户空间里剥离出来的。

用户空间的程序在跑的时候,你是不能访问内核里面的东西的,如果应用程序能够随便进到内核的话,很容易把内核搞挂,影响到别的程序。

内核相当于给应用提供了一个虚拟的环境,所以在linux里面,一定要坚信一点,只要kernel 挂了,一定是kernel 的 bug , 有的时候,我极其的牛逼啊,我写个应用程序把内核搞挂了,请问这是不是我应用程序的bug?

还有我写了一个驱动,人家应用程序一调用,驱动挂了,他说人家应用层调用的不对,这些都是不对的。你写了一个内核程序,就要无条件保证,无论人家以多变态的方法去调用你,挂的都是那个应用。只要挂的是kenel ,就是kernel的bug。

因为你内核要提供一个无条件,透明的一个环境,来保证应用程序可以跑。1个进程跑100个线程,只要有1个线程挂掉了,整个进程就挂掉了。用户态是跑在三环的,内核是在0环的,如果要交互,只能通过特定的方法,系统调用  proc  netlink  等方法。

所以在页表里,不仅会标注 RWX, 也会标注 user / kernel 的权限。

2 案例 - meldown 熔断漏洞

理论上来讲,我从一个应用层的程序,去访问一个内核空间的值,这是非法的。

基于时间的旁路攻击 Side-channel attack 

c=*k, 其实c 是拿不到具体的真实值的,因为访问的是一个内核空间的地址,但是由于CPU的这种狂奔的特点,它还是会继续往下执行的,执行完 c=*k 之后,我们会访问一下 a[c] ,

因为a[256] 中的值都比 cacheline 大,所以 cache 会命中 a[c] , 我们知道 cpu 访问cache的时间要比访问内存的时间要少很多,这样的话  ,我们就可以挨个访问a[256],然后记录访问的时间,根据时间来判断出c的具体值。

具体的原理呢,江湖中有个李小璐买汉堡问题的传说。可以看出CPU 的漏洞。

这就是基于时间的旁路攻击,大家写代码的时候,千万不要这样写,是很不安全的,这是基本的常识

 像这个对比密码的函数,攻击者可以一个一个试,因为第一个字母不对了,你就返回了,这个时间是很短的,对的时候你才会比较第二个字母,时间就更长,这很容易遭到旁路攻击。同样的方法,人家就可以探测出你的每一个密码是什么。

Meltdown 漏洞怎么修复呢?

注意:

在内核中 物理地址是一个整数!不是指针,一般来说 32位的处理器,物理地址是 32位的,64位的处理器,物理地址是64位的,但是不一定都是这样!

32位的处理器,它的虚拟地址空间肯定是32位的,但是它的物理地址可以远大于32位,只要 MMU 里的页表支持可以映射到更多位的物理地址就可以。这叫大的地址扩展。一个32位的处理器,它的内存条是可以大于4G的,

3 内存分ZONE的问题

 比如有一个大小4G的内存条,它的物理地址是 0-4G , 我们经常把它分为三个区域,

ZONE_HIGH  高端内存( 896M - 4G) ,在内核中一般不使用,如果要使用,通过 kmap映射。

ZONE_NORMAL

ZONE_DMA    

我们要理解原理,像现在很多64位的处理器,已经没有 ZONE_HIGH  , ZONE_DMA   也不一定有,这是有硬件决定的,有的硬件DMA引擎无法access 所有的内存,所以才制造出一个 ZONE_DMA的概念。

在64位处理器里面,可能还有 ZONE_DMA32 等等,

这里注意,如果你的DDR是 256M  512M ,那么是没有 高端内存的概念的。

当系统一上电,就会把一段低端内存 0 - 896M 线性映射到  虚拟地址 3G+0 -- 3G+896M 的内核空间中。这里注意,为什么映射一段,是因为整个4G我是映射不了的,这个896M 也不一定非得是896M , ZONE_HIGH的大小是可以配置的。

现在很多64位的处理器,已经没有896M的问题了,ZONE_HIGH 并不是浪费掉了,只是内核一般不访问 ZONE_HIGH,当然也有办法访问,通过kmap映射。它不像低端内存  ZONE_NORMAL  ZONE_DMA    一开机就线性映射好了,

CPU访问这些物理地址很简单的啊,直接通过虚拟地址就好了。在内核里的两个宏,virt_to_phys  和 phys_to_virt ,这两个都是简单的线性偏移,只对部分的低端内存可以用,ZONE_HIGH是不行的。

ZONE_DMA    也不一定有,它存在的原因就是  DMA硬件有缺陷,有的硬件DMA引擎无法access 所有的内存,比如只可以访问0-16M的内存,或者 0-32M的内存。所以才有了 ZONE_DMA    或者 ZONE_DMA16 或者 ZONE_DMA32的概念。

一般来说DMA可以与内存直接交互的,但是也有一些比较牛逼的DMA引擎是有MMU的,注意 DMA的出现就是可以让CPU闲出来,可以干别的事情,它不会让你的外设变快,快不快是由于你的总线频率决定的,跟你CPU访问 还是DMA访问没有半毛钱的关系。

有缺陷的DMA在申请内存的时候 ,会用到 GFP_DMA 的标志,但是 并不是只要 DMA引擎才可以申请 ZONE_DMA, 鬼都可以申请,而且 DMA申请的内存 也不一定是在ZONE_DMA,这是由硬件决定的。

void *dma_alloc_coherent( struct device *dev, size_t size,dma_addr_t *dma_handle,gfp_t gfp);  

这个函数申请的内存 也不一定来自 ZONE_DMA, 别看它名字里面有个DMA,那到底是来自哪里,完全取决你的外设, 它第一个参数就是device结构体 ,里面你会填写DMA的访问范围的啊,由这个决定的。

这里注意,CPU 和 程序员的视角是一样的,所以CPU 只关注虚拟地址连续就可以了,因为有了MMU,我们只要关系虚拟地址的连续就可以,物理地址连续不连续对我们来说不重要。

只有 DMA 会关注 物理地址的连续性,因为一般的DMA引擎是没有MMU的!

4 Buddy算法

理解完这些之后,我们要了解一个算法,Buddy算法

Buddy 算法直面物理内存,32位的处理器,可能内存条上来就分为了三个zone ,Buddy 的算法就管理这个ZONE,你不可能跨越两个ZONE去申请内存。

每个页是4K,每一个ZONE都有很多的4k页, buddy算法会把这个空闲的页都管理起来,把1页空闲的都放在一个链表上,2页空间的都放在一个链表上,

4页空间的都放在一个链表上,8页空间的都放在一个链表上,然后不停的进行拆分合并。

 比如现在有 16页的空闲页,2^4 , 这个时候会有一个人来申请1页,现在就只有15个页,就分成了 8 + 4 + 2 + 1 ,就这样不停的拆分 合并,

Buddy 牛逼的地方就在于,全世界的正整数都可以拆分成2^n 的和的。那如何在系统里查看这些 空闲的页呢 , cat /proc/buddyinfo 

分别代表的是 DMA_ZONE中,1页空闲的还有6个,2页空闲的还有4个,4页空闲的还有1个,8页空闲的还有0............

Buddy 牛逼但是无法避免一个情况。你的内存还有很大,但是你的连续空闲的内存确很小。因为随着buddy算法的不断的拆分合并,会出现 buddyinfo 中前面很多,后面很少的情况,这会影响到会使用物理连续的情况。

但是谁会申请连续物理内存呢?有人说了应用程序,malloc(10M), 这是不对的!CPU 和 程序员的视角是一样的,所以CPU 只关注虚拟地址连续就可以了,因为有了MMU,我们只要关系虚拟地址的连续就可以,物理地址连续不连续对我们来说不重要。

只有 DMA 会关注 物理地址的连续性,因为一般的DMA引擎是没有MMU的!

比如有一个摄像头,拍摄了一张1080p的图像 8M,想通过DMA引擎传过去,但是这个DMA引擎是没有MMU的,它就无法搞清楚虚拟地址和物理地址的关系,它只能给它一个连续的物理地址,好了,我现在cpu上有100M空闲的内存,但是没有一个8M连续的内存,

怎么办?

早期人们是怎么解决的呢?是通过预留内存的方法解决的,比如你买的手机是1G的内存,但是看起来只要800M ,其实是有一部分是预留起来了,可能20M给摄像头,20M给显示器,50M给GPU啊等等。但是这样会有浪费的情况存在啊。

5 CMA连续内存分配器

后来三星公司有一个工程师,提出了一个非常天才的设计,叫CMA 连续内存分配器

还是原来的例子,我开辟了一个32M的CMA的区域,本来是要给摄像头用的,平时用不到,就分配给了其他app使用,现在摄像头突然要传输一个8M的图片,但是32M的区域里并没有一个连续的空间,怎么办呢?

MMU会在内存空间里疯狂的寻找空闲的页面,将原来分配给其他app的页面交换出去,让CMA的区域空闲下来,这里注意,这个交换只需要MMU 更改一下虚拟地址和物理地址的映射关系即可,CPU和程序员是感受不到这个变化的。这样的话,预留的内存就不会浪费掉了。

关于如何申请一个预留的CMA内存呢,可以参考  linux/Documentation/devicetree/bindings/reserved-memory 

这里可以为一个单独的DMA设备,指定一块CMA区域,也可以几个DMA设备,公用一块CMA区域,都是可以在devicetree里配置的。

配置好了 dma_alloc_coherent 就会为你分配到这个区域

一定要读一下宋宝华老师的下面的文章,

宋宝华:那些年你误会的Linux DMA(关于Linux DMA ZONE和API最透彻的一篇)

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Linux内存管理是一种复杂且功能强大的技术,它基于一系列算法和策略来有效地使用和管理系统的内存资源。以下是Linux内存管理基本概念: 1. 分页和段管理:Linux采用分页机制来管理物理内存,它将内存划分为固定大小的页或帧。同时,它还采用分段机制来管理进程的虚拟地址空间,将进程的地址空间划分为一系列段(如代码段、数据段和堆段)。 2. 内存映射:Linux使用内存映射来将文件或设备直接映射到内存中,这样可以直接访问文件内容或设备寄存器,而无需进行系统调用或访问文件系统。 3. 交换和回收:当系统内存不足时,Linux使用交换机制将不再活跃的进程或数据页从内存中移除,并将其存储在交换空间(也称为交换分区)中。当需要更多内存时,可以从交换空间中恢复这些页面。回收机制则用于回收不再需要的内存页面,以供其他进程使用。 4. 缓存:Linux使用缓存来提高内存使用效率。缓存通常包括页替换缓存(用于存储不再使用的页面)和缓存数据结构(如TLB,用于加速虚拟到物理的转换)。 5. 动态内存分配:Linux提供了多种动态内存分配机制,如brk()和sbrk()系统调用来调整堆的大小,mmap()系统调用用于映射文件或创建新的内存区域。 6. 内存使用和监控:Linux提供了多种工具和接口来监控和管理系统的内存使用情况,如free、top、vmstat等命令,以及/proc文件系统中的信息。 总的来说,Linux内存管理策略旨在实现高效的内存使用、动态调整、缓存优化和故障恢复等功能,以确保系统在各种负载和场景下的稳定运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值