hazing 2020-03-03
分析linux内存管理机制,离不了上述几个概念,在介绍上述几个概念之前,先从《深入理解linux内核》这本书中摘抄几段关于上述名词的解释:
逻辑地址(Logical Address)
包含在机器语言指令中用来指定一个操作数或一条指令的地址(有点深奥)。这种寻址方式在80x86著名的分段结构中表现得尤为具体,它促使windows程序员把程序分成若干段。每个逻辑地址都由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。
线性地址(linear address)(也称虚拟地址 virtual address)
是一个32位无符号整数,可以用来表示高达4GB的地址,线性地址通常用十六进制数字表示,值的范围从0x00000000到0xffffffff。
物理地址(physical address)
用于内存芯片级内存单元寻址。它们与从微处理器的地址引脚按发送到内存总线上的电信号相对应。物理地址由32位或36位无符号整数表示。(其实这个最好理解,就是实实在在的地址)
(PS:在下面的解释就可以看到,有时也将逻辑地址看做虚拟地址,但是《深入理解linux内核》中将线性地址看做虚拟地址)
首先说一句话:linux关于内存寻址可以分为几个阶段,首先由分段机制,然后有分页机制。
分页机制在段机制之后进行,以完成线性—物理地址的转换过程。段机制把逻辑地址转换为线性址页机制进一步把该线性地址再转换为物理地址
下面是我从网上查找资料了解到的,同时添加了自己的理解
逻辑地址(Logical Address)
是指由程序产生的与段相关的偏移地址部分。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样)。应用程序员仅需与逻辑地址打交道,而分段和分页机制对您来说是完全透明的,仅由系统编程人员涉及。应用程序员虽然自己可以直接操作内存,那也只能在操作系统给你分配的内存段操作。(也就是说,咱们应用程序中看到的地址都是逻辑地址。)
如果是程序员,那么逻辑地址对你来说应该是轻而易举就可以理解的。我们在写C代码的时候经常说我们定义的结构体首地址的偏移量,函数的入口偏移量,数组首地址等等。当我们在考究这些概念的时候,其实是相对于你这个程序而言的。并不是对于整个操作系统而言的。也就是说,逻辑地址是相对于你所编译运行的具体的程序(或者叫进程吧,事实上在运行时就是当作一个进程来执行的)而言。你的编译好的程序的入口地址可以看作是首地址,而逻辑地址我们通常可以认为是在这个程序中,编译器为我们分配好的相对于这个首地址的偏移,或者说以这个首地址为起点的一个相对的地址值。(PS:这么来看,逻辑地址就是一个段内偏移量,但是这么说违背了逻辑地址的定义,在intel段是管理中,一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量])
当我们双击一个可执行程序时,就是给操作系统提供了这个程序运行的入口地址。之后shell把可执行文件的地址传入内核。进入内核后,会fork一个新的进程出来,新的进程首先分配相应的内存区域。这里会碰到一个著名的概念叫做Copy On Write,即写时复制技术。这里不详细讲述,总之新的进程在fork出来之后,新的进程也就获得了整个的PCB结构,继而会调用exec函数转而去将磁盘中的代码加载到内存区域中。这时候,进程的PCB就被加入到可执行进程的队列中,当CPU调度到这个进程的时候就真正的执行了。
我们大可以把程序运行的入口地址理解为逻辑地址的起始地址,也就是说,一个程序的开始的地址。以及以后用到的程序的相关数据或者代码相对于这个起始地址的位置(这是由编译器事先安排好的),就构成了我们所说的逻辑地址。逻辑地址就是相对于一个具体的程序(事实上是一个进程,即程序真正被运行时的相对地址)而言的。这么理解在细节上有一定的偏差,只要领会即可。
逻辑地址产生的历史背景:
追根求源,Intel的8位机8080CPU,数据总线(DB)为8位,地址总线(AB)为16位。那么这个16位地址信息也是要通过8位数据总线来传送,也是要在数据通道中的暂存器,以及在CPU中的寄存器和内存中存放的,但由于AB正好是DB的整数倍,故不会产生矛盾!
但当上升到16位机后,Intel8086/8088CPU的设计由于当年IC集成技术和外封装及引脚技术的限制,不能超过40个引脚。但又感觉到8位机原来的地址寻址能力2^16=64KB太少了,但直接增加到16的整数倍即令AB=32位又是达不到的。故而只能把AB暂时增加4条成为20条。则
2^20=1MB的寻址能力已经增加了16倍。但此举却造成了AB的20位和DB的16位之间的矛盾,20位地址信息既无法在DB上传送,又无法在16位的CPU寄存器和内存单元中存放。于是应运而生就产生了CPU段结构的原理。Intel为了兼容,将远古时代的段式内存管理方式保留了下来,也就存在了逻辑地址
线性地址(Linear Address)
是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel
80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。
我们知道每台计算机有一个CPU(我们从单CPU来说吧。多CPU的情况应该是雷同的),最终所有的指令操作或者数据等等的运算都得由这个CPU来进行,而与CPU相关的寄存器就是暂存一些相关信息的存储记忆设备。因此,从CPU的角度出发的话,我们可以将计算机的相关设备或者部件简单分为两类:一是数据或指令存储记忆设备(如寄存器,内存等等),一种是数据或指令通路(如地址线,数据线等等)。线性地址的本质就是“CPU所看到的地址”。如果我们追根溯源,就会发现线性地址的就是伴随着Intel的X86体系结构的发展而产生的。当32位CPU出现的时候,它的可寻址范围达到4GB,而相对于内存大小来说,这是一个相当巨大的数字,我们也一般不会用到这么大的内存。那么这个时候CPU可见的4GB空间和内存的实际容量产生了差距。而线性地址就是用于描述CPU可见的这4GB空间。我们知道在多进程操作系统中,每个进程拥有独立的地址空间,拥有独立的资源。但对于某一个特定的时刻,只有一个进程运行于CPU之上。此时,CPU看到的就是这个进程所占用的4GB空间,就是这个线性地址。而CPU所做的操作,也是针对这个线性空间而言的。之所以叫线性空间,大概是因为人们觉得这样一个连续的空间排列成一线更加容易理解吧。其实就是CPU的可寻址范围。
物理地址(Physical Address)
是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。
虚拟内存(Virtual Memory)
是指计算机呈现出要比实际拥有的内存大得多的内存量。因此它允许程序员编制并运行比实际系统拥有的内存大得多的程序。这使得许多大型项目也能够在具有有限内存资源的系统上实现。一个很恰当的比喻是:你不需要很长的轨道就可以让一列火车从上海开到北京。你只需要足够长的铁轨(比如说3公里)就可以完成这个任务。采取的方法是把后面的铁轨立刻铺到火车的前面,只要你的操作足够快并能满足要求,列车就能象在一条完整的轨道上运行。这也就是虚拟内存管理需要完成的任务。在Linux
0.11内核中,给每个程序(进程)都划分了总容量为64MB的虚拟内存空间。因此程序的逻辑地址范围是0x0000000到0x4000000。
有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的。(这一点和上面的解释有一点区别,往下的解释就按照这个继续)
逻辑地址与物理地址的“差距”是0xC0000000,是由于虚拟地址->线性地址->物理地址映射正好差这个值。这个值是由操作系统指定的。
虚拟地址到物理地址的转化方法是与体系结构相关的。一般来说有分段、分页两种方式。以现在的x86 cpu为例,分段分页都是支持的。MemoryMangement Unit负责从逻辑地址到物理地址的转化。逻辑地址是段标识+段内偏移量的形式,MMU通过查询段表,可以把逻辑地址转化为线性地址。如果cpu没有开启分页功能,那么线性地址就是物理地址;如果cpu开启了分页功能,MMU还需要查询页表来将线性地址转化为物理地址:
逻辑地址 ----(段表)---> 线性地址 — (页表)—> 物理地址
不同的逻辑地址可以映射到同一个线性地址上;不同的线性地址也可以映射到同一个物理地址上;所以是多对一的关系。另外,同一个线性地址,在发生换页以后,也可能被重新装载到另外一个物理地址上。所以这种多对一的映射关系也会随时间发生变化。
程序(进程)的虚拟地址和逻辑地址
逻辑地址(logicaladdress)指程序产生的段内偏移地址。应用程序只与逻辑地址打交道,分段分页对应用程序来说是透明的。也就是说C语言中的&,汇编语言中的符号地址,C中嵌入式汇编的”m”对应的都是逻辑地址。
逻辑地址是Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说的连接器为A分配的0x08111111这个地址就是逻辑地址。不过不好意思,这样说,好像又违背了Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为[段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些”
线性地址(linear address)或也叫虚拟地址(virtual address):跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
实际物理内存地址
物理地址(physicaladdress)是CPU外部地址总线上的寻址信号,是地址变换的最终结果,一个物理地址始终对应实际内存中的一个存储单元。对80386保护模式来说,如果开启分页机制,线性地址经过页变换产生物理地址。如果没有开启分页机制,线性地址直接对应物理地址。页目录表项、页表项对应都是物理地址。
是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。
物理地址用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
Linux0.11的内核数据段,内核代码段基地址都是0,所以对内核来说,逻辑地址就是线性地址。又因为1个页目录表和4个页表完全映射16M物理内存,所以线性地址也就是物理地址。故对linux0.11内核来说,逻辑地址,线性地址,物理地址重合。
========================================================
虚拟地址是对整个内存(不要与机器上插那条对上号)的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不真实的”,“假的”内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;之所以是这样,是因为现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。这个“转换”,是所有问题讨论的关键。有了这样的抽像,一个程序,就可以使用比真实物理地址大得多的地址空间。(拆东墙,补西墙,银行也是这样子做的),甚至多个进程可以使用相同的地址。不奇怪,因为转换后的物理地址并非相同的。可以把连接后的程序反编译看一下,发现连接器已经为程序分配了一个地址,例如,要调用某个函数A,代码不是call A,而是call 0x0811111111 ,也就是说,函数A的地址已经被定下来了。没有这样的“转换”,没有虚拟地址的概念,这样做是根本行不通的。打住了,这个问题再说下去,就收不住了。
CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。
线性地址:是CPU所能寻址的空间或者范围。
物理地址:是机器中实际的内存地址。换言之,是机器中的内存容量范围。
逻辑地址:是对程序而言的。一般以Seg:Offset来表示。(程序员自己看到的地址)
因此,若要确实比较三者的话,应有以下关系:线性地址大于等于物理地址(PS:但二者的地址空间是一样的),而逻辑地址大于线性地址。逻辑地址通过段表变换成线性地址,此时如果并未开启分页机制的情况下,逻辑地址直接转换成CPU所能寻址的空间。若已开启则通过页表完成线性地址到物理地址的变换。
因此,三者最准确的关系是:逻辑地址通过线性地址完成物理地址的映射,线性地址在三者之中完全是充当"桥"的作用。
不管哪种解释,都差不多,只不过把虚拟地址归属于剩下三种的哪一个的问题