计算机的地址空间分为物理空间和虚拟空间
物理地址 (physical address): 放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就将相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。 虚拟地址 (virtual address): CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。
通过MMU进行转换
大致过程: 1)根据VA和C2找到一级页表条目
2)若是段描述符,返回物理地址
3)若该条目是二级页表描述符,继续根据虚拟地址找下一个条目
4)找到第二个页描述符,返回物理地址
取出%EBP,%ESI寄存器中的内容,再通过运算获取虚拟地址
##Linux是怎么处理Intel的分段管理机制的? 为什么要分段管理?
CPU内部的数据总线和段寄存器都是16位的,用他们做地址寄存器智能寻址64KB单元,但是8086的实际物理地址有1MB,如何解决这个问题呢?采用分段技术。
什么是分段技术?
将1MB的内存空间分成若干段,每一段的第一个地址称为段首址,并且用段首址来表示这一段的内存地址。段首址必须要能被16整除,为什么呢?这样的话,段首址就可以用16位来保存,低4位全部为零,高16位可放在段寄存器中。
Linux如何进行分段?
Linux的段式管理,事实上只是“哄骗”了一下硬件而已。从2.2版开始,Linux让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表LDT。但内核中也用到LDT,那只是在VM86模式中运行Wine,因为就是说在Linux上模拟运行Winodws软件或DOS软件的程序时才使用。
Intel微处理器的段机制是从8086开始提出的, 那时引入的段机制解决了从CPU内部16位地址到20位实地址的转换。为了保持这种兼容性,386仍然使用段机制,但比以前复杂得多。因此,Linux内核的设计并没有全部采用Intel所提供的段方案,仅仅有限度地使用了一下分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。但是,对段机制相关知识的了解是进入Linux内核的必经之路。
在 IA32 上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在IA32上设计操作系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。 但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让 Linux 具有更好的可移植性,我们需要去掉段机制而只使用分页机制。但不幸的是,IA32规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。万般无奈之下,Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说“偏移量=线性地址”。另外由于段机制规定“偏移量<4GB”,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。看来,Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。
IA32的内存寻址机制完成从逻辑地址–线性地址–物理地址的转换。其中,逻辑地址的段寄存器中的值提供段描述符,然后从段描述符中得到段基址和段界限,然后加上逻辑地址的偏移量,就得到了线性地址,线性地址通过分页机制得到物理地址。 首先,我们要明确,分段机制是IA32提供的寻址方式,这是硬件层面的。就是说,不管你是windows还是linux,只要使用IA32的CPU访问内存,都要经过MMU的转换流程才能得到物理地址,也就是说必须经过逻辑地址–线性地址–物理地址的转换。
##虚拟页面有哪几种状态? ●未分配的:VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
●缓存的:当前已缓存在物理内存中的已分配页。
●未缓存的:未缓存在物理内存中的已分配页。
##MMU怎么分配一个新的页面(VM/PM/PTE)? 当操作系统分配一个新的虚拟内存页时,调用malloc的结果。分配过程即为在磁盘上创建空间并更新PTE中对应的页表,使它指向磁盘上这个新创建的页面
##多个进程的PTE项目可能存储同一内容?
多个虚拟地址可能映射同一个物理地址,所以可能储存同一内容
##VM是怎么支持链接与进程创建、加载运行的?
虚拟内存对链接的影响
VM能够简化链接。独立的地址空间允许每个进程的内存映像使用相同的基本格式,而不管代码和数据实际存放在物理内存的何处。一个给定的Linux 系统上的每个进程都使用类似的内存格式。对于64位地址空间,代码段总是从虚拟地址0x400000开始。数据段跟在代码段之后,中间有一段符合要求的对齐空白。栈占据用户进程地址空间最高的部分,并向下生长。这样的一致性极大地简化了链接器的设计和实现,允许链接器生成完全链接的可执行文件,这些可执行文件是独立于物理内存中代码和数据的最终位置的。
虚拟内存对fork()函数
既然我们理解了虛拟内存和内存映射,那么我们可以清晰地知道fork函数是如何创建一个带有自己独立虚拟地址空间的新进程的。
当fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
虚拟内存对exeve()函数:
虚拟内存和内存映射在将程序加载到内存的过程中也扮演着关键的角色。既然已经理解了这些概念,我们就能够理解execve函数实际上是如何加载和执行程序的。假设运行在当前进程中的程序执行了如下的execve调用:
execve("a. out", NULL, NULL);
正如在第8章中学到的,execve 函数在当前进程中加载并运行包含在可执行目标文件a.out 中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要以下几个步骤:
删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些 新的区域都是私有的、写时复制的。代码和数据区域被映射为a.out文件中的. text和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。图9-31概括了私有区域的不同映射。
映射共享区城。如果a.out程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中 的共享区域内。
设置程序计数器(PC)。execve做的最后- -件事情就是设置当前进程上下文中的程 序计数器,使之指向代码区域的入口点。 下一次调度这个进程时, 它将从这个人口点开始执行。Linux将根据需要换人代码和数据页面。
PPN(物理页号)
这里不介绍32/64位系统各种变量到底占多少个字节
32位系统内存上限: 2^32=4G Bytes 64位系统内存上限同理可以计算为128GBytes
一级页表指向二级页表 末级页表中表项即为PPN
不一定
七次
Intel没有只采用物理寻址的计算机系统。