linux虚拟地址转物理地址

80386虚拟地址和物理地址转换

CPU的发展

之前在看malloc内存分配函数的原理时,有涉及到分配虚拟内存,然后再映射到物理内存,当初也是看得一头雾水,因为对虚拟内存和物理内存不是很了解。所以这篇文章总结下我在学习虚拟内存和物理内存的一些收获。

首先给出CPU的进化表,图片来自博客wjlkoorey的博客 cpu进化表

CPU发展从寻址物理地址;寻址段地址到物理地址转换;寻址逻辑地址转换为线性地址,再转换为物理地址。在8086之前的CPU,寻址都为物理地址,是并没有段的概念。当程序要访问内存时都是要给出内存的实际物理地址,这样在程序源代码中就会出现很多硬编码的物理地址。这样的程序可想而知,难重定位,可控性弱,结构丑陋,那个年代写这样的程序在我们现在看来是多么让人恼火的一件事儿。

后来8086引入一个非常重要的概念--段,这样就实现了分段机制;8086CPU地址总线为16,这样寻址范围为2^16=64k,而8086的寻址空间为1M,那么是怎么实现的了?原来这时候cpu给出的地址为段地址,需要加上段地址(由cs,ds,ss,es)之后才构成物理地址。物理地址为=段地址:段内偏移量;段地址左移4位+段内偏移量,即可构成20位的物理地址。例如ES=0x1000,DI=0xFFFF,那么物理地址为:

AD(Absolute Address)=(ES)*(0x10)+(DI)=0x1FFFF

0x10为16,段地址*16(2^4),即向左移动4位。这样就可以对20位的1M内存空间进行寻址。

而这这种方式的寻址最大地址为0xFFFF:0xFFFF=0x10FFEF,大于1M空间,这样如果访问大于1M的内存空间时,将会产生结果了?8086的做法是自动从物理内存0地址开始寻址,有人就是说[0x0000,0x10FFEF]地址是按0xFFFF取模寻址。下图说明这种情况: 20位寻址空间

CPU发展的下一个里程碑是1985年80386的问世,从16位到32位CPU的飞跃,这中间80286就成了这次飞跃的跳板,80286地址上升到24位并且引入了保护模式。保护模式即规定进程能访问的内存,有些内存是不能访问的,例如进程不能访问内核代码。80386继承了80286的内存保护模式和分段机制,并且引入了分页虚拟机制。首先80386继承了80286的基础上添加两个段寄存器FS和GS。很显然,为了实现保护模式,段寄存器只存储段基地址是不够的,至少还需要段地址的长度还有一些诸如访问权限之类的其他信息。所以段寄存器存储的并不是真正的段基地址,而是存储每个段描述符的选择符,通过这个选择符在GDT表格中找到这个段的基地址。

现在主流的x86CPU上的主流操作系统,Linux,FreeBSD,Windows等待都是工作在保护模式下,处理器只有在上电启动,引导阶段初始化时在会进入实时模式,实时模式任务处理之后,即进入保护模式。

80386CPU逻辑地址转换为物理地址

逻辑地址转化为线性地址

80386cpu当需要访问内存时,首先给出的逻辑地址,然后通过MMU内存管理单元分段机制转换为线性地址,然后线性地址再通过MMU内存管理单元分页机制转换为物理地址。

首先来看下cs,ds等段寄存器的内容,如下图所示: 选择符格式

  1. INDEX为在描述符表的索引,因为总共13位,所以描述符表总共可以存储8192个描述符,处理器将INDEX*8(一个描述符占8个字节)+GDTR(全局描述符表)/LDTR(局部描述符表)即为这个段的描述符。
  2. TI(TABLE INDICATOR)指明在哪个描述符查找段描述符;如果为0,则在GDT查找,如果为1,则在LDT查找。
  3. RPL(REQUESTOR'S PRIVILEGE LEVEL)请求权限,用于保护机制,0为最高优先级,3为最低优先级,linux只使用0和3优先级,分别表示内核态和用户态。

当处理器需要访问内存时,给出的逻辑地址是:选择符+偏移量,然后通过MMU的分段机制,取出的逻辑地址的INDEX,乘以8,再加上GDTR存储的全局描述符表的基地址,即可获取这个段的描述符,然后存入描述符寄存器(一种对用户隐藏的寄存器,也称为不可编程寄存器),然后取出这个64位的段描述符的段基地址+偏移量,即可获得这个逻辑地址对应的线性地址,下图展示了转换过程: 逻辑地址转线性地址

线性地址由目录项(DIR)+页表项(PAGE)+页内偏移(OFFSET)组成。在介绍页地址转物理地址时,先介绍下段描述符的格式,如下图所示: 段描述符格式

  1. BASE:定义了4G的线程地址空间的基地址,处理器将描述符中三个BASE字段拼接成一个32位的值,这样就可以寻址4G的线性地址空间。
  2. LIMIT:定义了段的空间大小,由描述符中的两个字段拼接而成,形成一个20位的值,20位总共可以表示为1M个数值,而这个1M个数值的单位则由Granularity bit位决定:
  • 当granularity bit为0时,单位为1字节,这时limit表示的是1M的内存;
  • 当granularity bit为1时,此时单位为4kb,这时limit表示的是4g的内存;
  1. TYPE: 区别不同的描述符,即描述符的类型。
  2. DPL(Descriptor Privilege Level),描述符访问权限,用于保护机制。
  3. Segment-Present bit:如果这个bit设置为0,则这个描述符不能被使用来进行地址转换,如果这个描述符被载入段描述符,处理器将会发出一个异常,下图展示了bit=0时的描述符格式,处理器可以使用标有AVALABLE的位置存储描述符。 可用的描述符格式
  4. Accessed bit:当这个描述符被访问时,设置为1.

线性地址转化为物理地址

之前,已经将逻辑地址转换为线性地址,接下来看下是如何从线性地址转化为物理地址,先给出下面示意图: 线性地址转物理地址

一个线性地址由10位目录表+10位页表+12位偏移量组成,当给定一个线性地址时,

  1. 首先从控制寄存器CR3获取页目录基地址,然后加上线性地址的前10位页目录偏移量即可得到页表的基地址;
  2. 接着页表基地址+线性地址第二个10位的页表偏移,即可得到内存页的首地址;
  3. 最后内存页的首地址+线性地址的最后12位偏移量,即可得到最后的物理地址;

这样就完成了从线性地址转为物理地址

接下来,看下页表项的格式。先来看下空闲的页表项和存储数据的页表项的格式: 无效的页表项 页表项格式 三层页表都有着相同的格式,页帧地址为一块内存页的首地址,而内存页是4kb对齐的,所以页表项的低12位为0;而页目录项指向的是页表的首地址。

  1. Present bit表示这个页表项是否可以用于地址转换,p=1表示可以使用。当p=0时,表示这个页表项可以被进程使用。
  2. Accessed and Dirty Bits这些位表示一个页中数据的使用情况。当要对一个页写或读之前,处理器会在两个层次的页表格设置访问位;当要对一个地址进行写操作,而这个操作在第二表格某个表项指向的内存页中,这时处理器会设置第二个表格相对应的页表项的脏位,设置页目录的脏位是未定义的。操作系统可以根据这些位来决定当内存不够时,换出哪些物理内存。
  3. Read/Write和User/Supervisor Bits位在地址转换过程中,没有使用。

页转换缓存

为了提高系统,防止每次地址转换都需要访问页表,处理器还设置了TLB(Translation lookaside buffer)转换后备高速缓冲区,存储最近使用的线性地址到物理地址的映射;下面给出TLB的原理图: TLB原理图

当CPU需要访问一个一个内存地址时,给出一个虚拟地址,先是到TLB中查找是否有对应的物理地址,如果有,即命中,直接用TLB中的对应物理地址访问缓存;如果在TLB中没有对应的物理地址,即未命中,则需要到内存页表求出物理地址,并将这个物理地址存入TLB中。访问cache时,如果Cache中有需要的数据,则直接返回需要的数据,如果Cache中没有需要访问的数据,则需要到内存获取数据返回给用户,并将获取的数据存入Cache中。

所以,如果对计算机有足够的了解,那么就会发现计算机架构里面存在着好多的缓存设计,首先应用程序从磁盘获取数据时,在内核有一块内存存储最近访问的数据缓存;当CPU从主存获取数据时,也是先从缓冲区获取数据,然后在读进CPU。还有应用程序也有缓存,例如MySQL的存储引擎innodb也有一块缓冲区,存储磁盘数据;在web网站应用程序中,当从数据库获取数据时,先用memcache或者redis获取最近访问的数据等等。

缓存,让性能更美好。

参考:

  1. http://blog.chinaunix.net/uid-23069658-id-3569341.html
  2. https://pdos.csail.mit.edu/6.828/2008/readings/i386/s05_01.htm