Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1025875
  • 博文数量: 26
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 437
  • 用 户 组: 普通用户
  • 注册时间: 2019-09-08 12:19
个人简介

关于个人介绍,既然你诚心诚意的看了 我就大发慈悲的告诉你 为了防止世界被破坏 为了维护世界的和平 贯彻爱与真实的邪恶 可爱又迷人的反派角色 我们是穿梭在银河的火箭队 白洞,白色的明天在等着我们

文章分类

分类: 嵌入式

2019-11-08 17:26:20

做嵌入式底层的朋友都应该知道,在Linux当中,我们会发现存在多进程和多线程的概念,但是在以cortex-m系列为代表的单片机中却只有多任务的概念。通常,多任务和多线程其实是一个意思,只有进程和线程之间才会存在差异。那么是什么导致这种差异呢?单片机能不能跑多进程呢?今天,本篇主要是用于讲解导致这个差异的硬件模块:MMU(内存管理单元)。

通常,我们的ARMv7 Cortex-A 一般采用MMU来管理内存,而单片机所使用的ARMv7 Cortex-M则是采用MPU来管理内存。两者之间有明确的差异。MMU是支持虚拟内存的,而MPU则不支持虚拟内存,只支持简单的内存权限管理。我们可以理解为MPU就是MMU的阉割版(不知道这么理解合不合适)。然后多进程是建立在虚拟内存上的,因此,不支持MMU的芯片就不支持多进程,故单片机通常不支持多进程。下面我们详细聊聊什么是MMU。

在《cortex_A_series_PG.pdf》中有所描述:MMU是用于让进程可以工作在自己的私有内存空间的处理单元。MMU的一个关键技术在于 地址映射(虚拟地址到物理地址转换),这种地址转换对于应用程序是透明的,另外,MMU还管理了内存访问的权限,内存order,cache策略等等。在多进程的嵌入式系统当中,我们通常用MMU去映射一段内存,并授予访问该内存的权限和一些其他属性。

当MMU禁用的时候,所有的虚拟地址访问都是直接访问到对应数字上的物理内存上面(1:1) ( a flat mapping)(即VA=PA)。当MMU使能的时候,MMU则需要通过存放到TTB寄存器中的映射表来查询对应的物理地址(映射表我们可以看成是一个数组,虚拟地址是数组的索引,数组的内容就是物理地址)。如果MMU通过映射表查询到虚拟地址是未映射的(转换是通过映射表完成),那么CPU会产生一个abort异常,并提供详细的问题异常信息(比如0地址,通常我们不会在0(NULL)地址上设置映射关系,因此程序一旦访问0地址,就会触发page falut,从而产生segment fault)。

MMU允许操作系统使用多张虚拟映射表即一个进程一张映射表,转换过程大概如下:

如果上图(MMU和cache的关系见《ARMv7 L1 cache详解》),当MMU使能后,CPU能见到的地址就是虚拟地址了(对CPU而言,它是不知道的,它以为它使用的是物理地址。当它发出地址访问的时候,由于MMU使能,MMU就会截获CPU发出来的地址,并进行地址转换,最后把获得的数据返回给CPU),CPU的一切操作都在虚拟地址上。当CPU去访问虚拟地址的时候,会首先通过MMU,MMU通过虚拟地址映射表去查询该虚拟地址对应的物理地址,最后通过物理地址获取到DDR上面的数据。在Linux当中,通常我们配置的MMU能够处理的最小单元是一个page,大小4k,因此,如果我们要能够处理4G大小的地址空间,那么就会存在256*4096个4k。假如这个虚拟映射表是一个数组,那么vtop[256*4096],可见这个数组是很大的,如果每次去查表都要遍历这么大的数组,很明显是不可取的。因此,MMU将这个数组做成了二级分页查询,也就是我们说的页式管理,通常Linux是2级,第一级是有4096个元素的数组即vtop1[4096]用虚拟地址的[31:20]索引,第二级是有256个元素的数组即vtop2[256]用虚拟地址的[19:12]索引,最后[11:0]作为页内偏移([11:0]的大小正好是4k,即一个页大小为4k,虚拟地址的低12和物理地址的低12位相同)=》则物理地址PA =(*(vtop1[VA31:30]))[VA19:12] + va[11:0]。其中第一级vtop1[4096]上的每个结点存放的是vtop2的地址,即我们有4096个vtop2表和1个vtop1表(vtop1和vtop2表都存放到内存地址上面,以后Linux系统启动章节将会说明)。

在实际应用当中,CPU会提供一个寄存器(X86为CR3,ARM为协处理器cp15 TTB寄存器),要求操作系统先将vtop1的地址填入后,然后再使能MMU。这样可以保证MMU一旦使能,MMU就可以从寄存器中获取到映射表的vtop1地址,便可以进行虚拟地址到物理地址的翻译,也就是说翻译是MMU自动完成的,但是映射表需要操作系统来创建。注:为了减少进程切换带来的映射表变动,从而造成的额外开销,ARM提供两个TTB寄存器存放映射表基地址,即TTB0,TTB1。TTB0可用于用户空间映射表,TTB1可用于内核空间映射表。其中,CP15协处理器中的TTB控制寄存器有一个N位用于控制TTB0和TTB1的使用,如当N=7的时候,则如果虚拟地址的最高7位位全0的时候,即小于32M的内存访问,采用TTB0指向的映射表,否则采用TTB1指向的映射表;当N=0的时候,表示只是用TTB0所指向的映射表。Linux好像只使用了一个TTB0寄存器(N=0), 因为不管怎么变换,Linux所有进程的内核空间的映射表都是一样的,所以采用一张4G映射表,不需要这种分离设计。

当然,如上所说的映射表是存放到DDR的内存里面的,我们知道DDR相对于CPU而言是一个慢速设备(《ARMv7 L1 cache详解》),因此,如果CPU每次访问虚拟内存的时候都要到DDR中去查询这个2级表,那么这个效率就会显的比较低,故正如DDR引入cache的概念一样,MMU也引入了TLB。所谓TLB(Translation Lookaside Buffer)就是用于存储最近执行过虚拟地址转换的地址和对应的物理地址。这样当CPU发出地址访问的时候,MMU会截获CPU的地址,并先尝试在TLB中去查询,如果查到(TLB hit),TLB直接返回物理地址给MMU;如果没有查找到(TLB miss),则MMU就去内存translation table walk,并将walk到的物理地址存放到TLB中。TLB的处理和《ARMv7 L1 cache详解》相似,结构如下:

如上图,是TLB中数据的格式,这个对用户是透明的,VA表示的是虚拟地址,ASID表示的是虚拟映射表的tag(多进程使用0-255),PA是VA对应的物理地址。

一级映射表内容格式(vtop1[n]的值),如下:

从上图可以知道,一级映射表支持2种段:1M大小的段(vtop1的位[1:0] == 10, [18] == 0)和16M大小的段vtop1的位[1:0] == 10, [18] == 1。如果是1M大小的段,[31:20]存放二级段表的基址。如果是16M大小的段,[31:24]存放二级段表的基址。vtop1的位[1:0] == 01,表示位[31:10]直接存放的2级映射表地址;vtop1的位[1:0] == 00,表示地址没有映射,如果此时是启用了MMU的情况,访问该地址会产生page fault异常。(Xn位表示是否能执行的位,ng位表示page是否为全局,domain不常用,就是domain ID,可以根据domain来控制权限访问,即cp15的c3寄存器Domain Access Control Register


二级映射表格式(vtop2[n]的值)(1k对齐),如下:

从上图可以知道,二级映射表支持两种页,大小64K的Large Page和大小4K的small page(通过最低两位[1:0]决定),[1:0]如果为00表示没有映射,如果访问会产生异常;01表示64K大页,1x表示4K小页。(Xn位表示是否能执行的位,ng位表示page是否为全局)

虚拟内存访问权限的位的含义如下:

从上可以知道,我们可以通过修改映射表里面的ap属性,来控制虚拟地址的对应段或者页的读写状态,Xn来设置执行状态(这个方法,Linux用于进程的写时复制COW机制)(同样我也将它用来解决一些野指针的问题),内存类型和cache属性相关,如下:

上图,主要是用于处理内存cache和order的问题,即我们可以配置一些内存不能被cache,如给DMA或者外设寄存器地址用的内存。什么叫内存order(memory order)?由于cache的引入,会导致某些时候,将要执行的一系列指令 一部分指令在cache里面,而另一部分可能在DDR里面,因此CPU在执行指令的时候,是可以根据对应指令是否被cache来打乱访问顺序的,以提升CPU性能(即使在DDR中的指令理论上应该最先执行,CPU也会先把cache中的先执行,不需要等待DDR中的指令被执行了才执行)。注意:CPU乱序问题或者memory order问题。=》 引入下面指令(memory barrier)。

MSB:数据存储器隔离指令。指令保证: 仅当所有在它前面的存储器访问操作
都执行完毕后,才提交(commit)在它后面的存储器访问操作

DSB:数据同步隔离质量。比 DMB 严格: 仅当所有在它前面的存储器访问操作
都执行完毕后,才执行在它后面的指令

ISB:指令同步隔离质量。最严格:它会清洗流水线,以保证所有它前面的指令都执
行完毕之后,才执行它后面的指令。

asm volatile("" ::: "memory");:内存栅栏,防止CPU乱序执行


MMU操作例子

@ 初始化MMU

MOV r1,#0x0

MCR p15, 0, r1, c2, c0, 2 @ 配置TTB控制寄存器

LDR r1, ttb_address

MCR p15, 0, r1, c2, c0, 0 @ 将ttb_address指向的映射表写入TTB0寄存器中

@ 配置 Domain域访问权限

LDR r1, =0x55555555

MCR p15, 0, r1, c3, c0, 0 

@ 使能 MMU

MRC p15, 0, r1, c1, c0, 0 @ 读取MMU使能控制寄存器

ORR r1, r1, #0x1            @ 位0是MMU使能位

MCR p15, 0, r1, c1, c0, 0 @ 将数据写回到控制寄存器

其中ttb_address的数据格式,按照本章给出的数据格式填写,即可。

注:Linux多进程实现为在每个进程描述结构task_struct中都会保存一份映射表,当在Linux的进程切换中,switch_to函数会在切换的时候,将task_struct里记录的映射表写入到cp15的ttb寄存器中,然后进行寄存器恢复的操作。如果新进程的task_struct是内核线程,则不会切换映射表,因为内核的映射表在所有进程中都是一样的,这叫做lazy模式。

Linux的进程切换将在以后分析,敬请期待。

MMU在虚拟化(hypervisor)中的妙用以后分析,敬请期待。


阅读(7317) | 评论(2) | 转发(0) |
给主人留下些什么吧!~~

BugMan2019-11-25 09:52:47

SaHae:博主,图都看不到呢

cu出问题了,我也是醉了

回复 | 举报

SaHae2019-11-23 16:19:07

博主,图都看不到呢