- 作者: 雪山肥鱼
- 时间:20210312 06:18
- 目的:深入理解内存
# 铁三角总览
## 分页机制
### 内存和内存空间是两回事
### MMU功能简介
### PTE低12位的作用
#### rwx权限
#### kernal/kernal+user 权限
#### CPU 熔断
## 物理内存分 zone
### zone_dma 说明
## 内存管理的buddy 算法
## CMA 工作机制
铁三角总览
内存的问题是linux中最难的问题之一。CPU铁三角:
- CPU 调度:调度器调度算法
- I/O:比较独立
- I/O还没有学习,但是I/O要透过CPU MEM内存去看,不能孤立的去看
- MEM:内存在cpu和I/O肩负了一个中间交换的角色
分页机制
内存和内存空间是两回事
在一个CPU眼里,在访问外部时有两种可能性即 内存空间与IO空间
- 内存空间
凡是通过指针访问的都是内存空间 - I/O空间
-
I/O空间是x86架构中非常特殊的一块空间,不能用指针直接访问,而是用in out指令 访问外设的寄存器
x86架构.png
-
RISK处理器的内存空间
risk处理器的内存空间分为两块
- 真正的内存
-
位于内存空间的寄存器
所以内存空间和内存是两个概念,内存位于内存空间中,但内存空间并不是内存
RISK架构.png
MMU 功能介绍
- 为什么每个进程都可以有很大的地址空间,因为他们使用的页表不同,每次切换进程,页表也就切换了
- CPU 只会发出虚拟地址,MMU会帮助CPU 通过查表的方式进行虚拟地址和物理地址的转换
- 虚拟地址和物理地址的转换在前面内存系列章节已经有所阐述,这里就不再赘述。
- 但是还需要注意:Page Table 中的 每一项的PTE的PFN是 这一页物理地址的基地址,VPN 也是 虚拟地址页的基地址(不包括偏移),所以很明显页表存储的是 <VPN, PFN>的对应关系。与整个虚拟地址无关,只与虚拟地址算出的虚拟页地址相关。
常见的错误理解:
指针发出的地址非物理地址,而一定是虚拟地址
物理地址不是个指针,而是个整数!!指针是CPU发出来的,是虚拟地址。cpu不可能通过物理地址访问内存和寄存器,一切都要通过MMU进行翻译
int * p = 1M;// 这个1M是虚拟地址
int address = 1M; //linux 内核中 对应的物理地址的类型是 u32/u64 int
当然 虚拟地址 还是有可能 等于物理地址,那只是个巧合而已。
PTE 低12 位
rwx权限 读写执行权限 在linux中每一个不同的东西都会有不同的权限
代码段只有r+x,那么尝试去重写代码段,页表中查找代码段相应的地址,但是发现页表中该处并没有写权限
马上MMU会给CPU 发出一个 page fault 的中断。CPU收到后,马上去查,发现有人在写代码段,此时CPU 发出 segmentation fault信号,将程序杀掉。
相关应用举例:缓冲区攻击
- b()函数调用func() 在内存空间中会将b()中返回func()的下一条地址 压栈
- 假设我在func()通过memcpy认为破坏上面提到的下一条地址,从而让PC指针的下一跳,飞到data段
- data段可以收到网络过来的数据,我们对其进行修改,注入一段偷窃代码。
- 但是由于MMU在查看PTE时,data段只有R+W的权限,会引发page fault,linux内核一查,发现有人在修改只有R+W的data段,立刻发出segmentation fault,将程序杀掉
权限代码举例:最简单的尝试修改 const 变量
main.c:
//main.c
#include<stdio.h>
const int a = 2;
extern void add_a(void);
int main(int argc, char ** argv)
{
add_a();
return 0;
}
a.c:
extern int a;
void add_a(void)
{
g++;
}
很显然在执行函数后,程序会报sgmentation fault.
- cpu 访问 const int a , 回去查页表
- MMU 不仅会查物理地址,还会查看权限
- MMU 发现没有写权限,陷入trap: page fault
- OS 查看 page fault 原因,有人在违规操作内存
- OS 发出 signal 11: SIGSEGV -> 程序挂掉,报segmentation fault
页表中的 kernal/User+kernal 权限 什么空间可以访问
linux区别于实时操作系统的最大区别就是内核是内核,应用时应用,内核空间从中剥离出来的
内核需要提供一个无条件的环境,让应用程序跑在上面,应用程序无论怎么调内核,内核都不能挂掉。
内核的东西,应用程序时访问不了的,三环的东西,时访问不了零环的。每一个APP 需要用特权指令,才能陷入0环。
CPU 熔断介绍 Meltdown
非常生动的讲解了什么是CPU熔断:
https://blog.csdn.net/qq_27633421/article/details/106213665
理论上用户态的东西是不能访问内核态的,但是CPU熔断的出现彻底颠覆了人们对地址空间的理解。
让人可以拿到内核的数据,理论上不可能从应用态拿到内核态的东西
基本原理如下:
//可能不太合理
a[256] 每一个成员都比较大,超过cache line
//理论上读不出来C
c = *k // k是内核空间的地址 理论上这里就要收到page fault,但是intel的狂奔以及预测
for(int i = 0;i<256,++i)
开始时间
a[i]
结束时间
delta = 结束 - 开始
这些访问时间是不同的,
当执行到a[c]时,因为intel的狂奔+prefechting机制,cache 已经提前缓存了,所以会造成时间差,那么反推出来,我就知道*k 里面存的是c了。
这里的理解貌似也有点问题,举得例子貌似也不太对,原理的话,还是看链接里所举得代码例子吧
李小璐汉堡问题:背景:两个狗仔想知道李小璐在KFC里买了什么
1、李小璐KFC 买了一个汉堡
2、狗仔A 去问 李小璐买什么我就买什么
3、店员拒绝,但是后厨听到了,做了一个汉堡(CPU狂奔+prefeching)
4、狗仔B来问 你们这里最快的是什么,店员问后厨要最快做出来的东西
5、后厨送出来一个汉堡,李小璐买的是汉堡
理解页表中 kernal/kernal+user 的权限管理狠重要。
物理地址不是指针 而是整数
32位系统的虚拟地址空间肯定只有32位,但是32位的物理地址空间是可以远大于32位的,32位处理器的内存条是可以大于4G的,只要页表可以支持64位范围内即可。即32处理器的大地址扩展(arm32位处理器最后一代即支持大于16G的物理内存)。
每个进程的页表地址不相同,多个进程一定会把16G物理地址瓜分掉,这很容易理解
物理内存分zone原理
dma zone 说明
dam zone的存在是因为外设硬件的缺陷,64位处理器,也没有什么High Memory Zone 了。也就是说dam zone 有没有,也是硬件所决定的。当然也可能有 dma_32 zone
3G-4G 开机就线性映射好了,所以用两个宏搞定。其他地址的需要翻译。
可以访问内存的有CPU和 DMA(GPU等也划到这里),DMA访问内存是不通过CPU的,直接访问内存。比如网卡收到报文,DMA直接将网络包送到内存中,不需要再通过CPU,这样CPU就空闲下来,DMA最主要的功能就是让CPU有时间去做其他事情。
这样理解的话 DMA并不会让你的外设边的更快。快慢是由总线频率决定的。跟CPU访问还是DMA访问没有半毛钱关系。
有些DMA引擎是有缺陷的,比如x86的isa总线,只能访问16M以下的内存,超过16M根本就访问不到了。申请高于16M也没用,因为DMA根本访问不到
所以Linux的应对方式是将内存砍一刀,砍出一块16M的空间。这时DMA申请的内存带有GFP_DMA,那么OS在低于16M的地方给他申请内存。
这片低于16M内存不只对DMA开放,谁都可以申请,没有任何缺陷的DMA可以访问任意内存,用于DMA内存,并不一定来自DMA总(无缺陷的DMA可以在zone_high申请内存),DMA_ZONE,也不一定用于DMA(低于16M的部分,谁都可以申请)
所以 dam_alloc_coherent申请的内存,并不一定来自于zone_dma,需要考虑外设是否有缺陷,
这个函数第一个参数就是 device结构体,会填写dma的访问范围.如果dam支持访问所有MEM,那么申请的内存可以是任意位置的,并不一定在zone_dma内。
如果没有一个DMA有缺陷,那么其实本质上并不需要ZONE_DMA
举例:
内存管理的 Buddy 算法
首先要明确:申请一片内存是不可能跨zone的。
Buddy算法将每一个总分成一页一页的slot后。
将1页空闲的放在一个链表上,2页空闲放在链表上,4页放在一张链表上,2^n个页放在一张链表上。然后不停的进行拆分合并。
例如刚开机时,16页空闲内存挂在16页buddy上,此时申请了1页内存,空闲15页,不允许有15页,必须时2^n,那么会被拆成 8 4 2 1 放在各自的buddy链表上。
buddy 算法管理内存的方式:
每一个块链表上挂着2^n次方个page. 最大可申请的内存是最后1个 2^10,也就是一次性申请4M内存。
全世界任何正整数都可以分割成2^n次方的和
cat /proc/buddyinfo 就可以显示出来
但是在buddy算法不停的申请释放的时候,连续内存会很少,内存碎片化问题会导致申请连续内存失败。不停的拆分合并拆分合并,导致内存就散了,碎片化了。
那么引发出来一个经典的问题:谁会申请连续的物理内存。
- 应用程序?
错错错! 应用程序申请的内存,都可以通过MMU进行优化。都是虚拟内存,地址空间中的连续,而不是物理内存的连续。 - DMA 物理连续
绝大多数DMA是不带MMU的,所以搞不清虚拟地址和物理地址,所以只能给出连续的物理地址给DMA.
解决方案:预留内存,预留了100多M,显示器的给显示器,给GPU的给GPU。
类似当年买电脑的时候,集成显卡,从内存里预留出来一部分给显存,一样的原理。
了解CMA的工作机制 - Contiguous memory allocated
给应用程序分配 movable 页,等到dma来要连续空间的时候,应用程序的page 移走。
physical address 上的moveable页,就算移走了页关系,只要更新page table 的映射关系就行了。
- 应用程序申请了8M的堆空间
- DMA外接摄像头,申请32M空间,但此时没有连续的32M空间
- 调用 dma_alloc_coherent
- linux 满山遍野的寻找 4k大小的内存,凑出来8M
- 修改page table 映射关系
- 内存就被挤出来了
关于DMA:
CMA 也可以配置多个,分区域管理,避免单个DMA区域的碎片化
留个印象,以后再说