地址空间
我们经常说地址,地址,其实呢,在不同的场合谈论的地址是完全不一样的,这是因为有不同的地址空间。这是抽象的结果,但是却给学习的人带来了很多误解,尤其是要涉及操作系统 ,嵌入式这些底层的人员,他们会接触到各种各样的地址空间,然后混合在一起的时候就会分不清了。
进程地址空间
在编程的情况下,很多人都会出现 error,尤其是刚开始接触学习一门新的语言,就连照着敲都会出错,这时候的错误可能还是显而易见的,让有经验的人来一看就明白了,但是随着编程能力的提升,有些错误就不能用肉眼发现,于是我们就有了 Debug 大法。
那么,在观察变量的时候,必然能看见这个变量所在的地址,这个地址也是我们最开始接触并了解地址这个概念,老师一般会解释为内存上的单元,于是我们也就这么记住了,当然这是方便我们理解,但是呢,深入到其他地址空间的时候必然要知道它们的区别。在我们 debug 时候,看见的地址,我们称这一类型的地址为 进程地址,这些也称为进程地址空间。
首先它是属于进程这一范畴,意味着所有的进程都有这么一块空间,我们以 32位 的 Linux 操作系统为例子,这一块空间就是 0~3G 这一连续空间,也就是说,每个进程都有 3G 的空间,这一空间是每个进程都会不一样的( 实际上可能一样的,但是我们先忽略 ),我们在切换进程的时候,关键的一步就是切换到不同的进程空间,所以 CPU 操作同一地址,如果针对的是不同的进程,真正访问的地方也就不一样( 这里我们忽略不同的进程利用映射,从而导致相同的情况 )。
进程空间存在的意义是什么呢,考虑如果不同的进程没有自己独立的空间,那么在我们编程之后实际上需要编译器进行处理,最终得到一个可执行的文件,这个可执行的文件我们想想,CPU 是不能识别变量名的,所有的访问变量,计算以及逻辑跳转操作都是通过寻址,然后存储到寄存器,接着运算这一过程实现的,那么我们如何预知这个地址呢?也就是说,你都不知道你这个程序最终从哪里开始,如何解决变量的寻址问题和跳转的问题,以前是通过分段来实现,也就是不同的进程,我们规定一段空间给它( 实际上,这就是一段进程空间 ),然后进程编写的时候都从默认 0 开始,实际运行的时候,我们只需要加上一个基址就好了。
现在考虑我们如果每个进程都有自己独立的空间,我们直接把程序放在自己进程空间内,那么在自己的空间, 0x100000 必然还是 a 变量,上述的代码也不需要做任何处理。如果没有这一空间,操作系统可能将程序放在了,内存 0x100000 ,那么上述所有地址,是不是都得 加上 0x100000,如果所有的程序,在执行之前都要对它进行地址修改,是不是很麻烦,并且运行起来也非常缓慢,还有一种手段,就是添加一个基址寄存器,所有的寻址都要加上这一基址寄存器,实际上就是分段的概念,比如我将基址寄存器( 每个进程都有不同的基址寄存器 )赋值为 0x100000,那么上述程序是不是也不需要修改了,实际上它和进程空间是一样的概念。
总之,进程空间的提出是方便了程序的编写和提高程序运行的效率。
内核地址空间
刚才我们说了,对于不同的进程 0~3G 是都是自己的空间,我们硬件用了一些技巧把它们隔离了,但是我们还没说,3G~4G 这块地方是什么空间呢。首先,我先说明,实际上每个进程都有自己的空间,是通过映射来实现的,简单点理解就是,CPU 执行不同进程的时候访问的相同的地址,最终是对应到不同的地址去了,也就是 CPU 看见的是假的地址,我们也称为虚拟地址。
所谓映射,就是地址转换,不同的进程,我设置不同的转换表,那么是不是相同的单元就可以转换到不同的地址了呢?最终转换的这个地址,我们称为物理地址,这个地址我们稍后再说。总之,通过这一转换,实现了不同的进程拥有"重复"的地址空间,使得我们程序的编写非常的方便。
然而,3G~4G 这一块我们映射到了相同的地址空间,什么意思呢? 也就是在为不同的进程设置的转换表,对于 3G~4G 这一块的转换,所有的进程都是相同的,也就是说,我们任何的进程,访问 3G~4G 的地址,最终转换的地址都是相同的。
这一块所有进程共享的空间,我们称之为内核地址空间,因为这块空间最终映射( 经过转换之后 )的地方是存放内核代码的地方。操作系统在切换的进程的时候,切换进程的代码你有没有考虑是存放在哪里呢?其实,就是代码运行到了某一进程( 当前正在执行 )的 3G 以上的空间,然后完成了进程的切换,又回到了 3G 以下的空间,然而这次 3G 以下的空间,是属于新的进程,也就是切换的进程的时候,切换了转换的表( 地址映射表 ),所以再次回到进程空间的时候,已经是新的进程空间了。
我们一定要了解,所谓的地址空间,就是填写一张不同的地址转换表( 映射表 ),不同的进程拥有自己的表,然后表在 3G~4G 这一块映射项填写了相同的值,所以映射到了一个地方,这一块地方就是内核映像所在的地方,所以我们将这一块空间称为内核空间。
物理地址空间
进程地址经过地址转换之后得到的就是物理地址,现在我们不妨把进程地址称之为的是虚拟地址,注意我们不考虑分段的情况,因为内存分段跟分页都是虚拟化的手段,实际上一个已经足够了,即是说,虚拟地址经过转换得到了物理地址,现在我们把实现这个转换功能的用硬件电路来实现,把这个部件称之为 MMU( Memory Management Unit)
TLB 就是我们在操作系统课程所学的快表,它利用局部性原理,使得我们地址转换的速度大大加快,现在我们也了解到了一点,就是 CPU 永远使用的都是虚拟地址,具体的转换都是由 MMU 来实现的,我们也可以说,物理地址对CPU来说是透明的。
物理地址最终到了总线,这是真正的地址信号,所以我们也把地址称为 总线地址。然而,大部分人理解的物理地址,就是 RAM 上的地址,也就是我们内存条上的单元,这其实是不准确的,物理地址空间的大部分给了内存,但是还有一部分给了设备。
我们都听过统一编址这一个概念,实际上就是同属一个物理地址空间,现在许多设备都是统一编址。
上图可以看出,32位的 CPU 实际上是不可以访问全部4G内存单元的,因为还有一部分地址空间被设备占用,这也是 32位 CPU 没有必要提升内存超过 4G 的原因。当然,现在的 CPU 已经 64位,物理地址空间不可能占用满了。可能有的读者好奇,这些物理空间的占用是固定的吗,实际上除了向前兼容的 BIOS,其他设备的占用空间起始地址都是可以由系统设置的,计算机的 BIOS 一般开机的时候都有关键的一步,就是对设备进行初始化,其中就包括这一过程,然后操作系统读取 BIOS 提供的物理空间占用表,这个表我们通常称为, memory map,中文如何翻译就见仁见智了,占用表经过操作系统的检查就可以知道哪些物理地址空间是可用的,哪些是已经被设备占用的,然后操作系统在可以利用的物理地址空间上建立内存管理系统。上面这一段话如果读者看懂了,那么在学习操作系统内存管理的时候必然是事半功倍的,必须要知道的一点,可用的地址空间不是指内存条上的内存单元,那个是固定的,而是指现在物理空间上可用的物理空间,也就是物理地址空间被内存占用的那一部分。
对于上图,可用的物理地址空间就是 0~640KB,1MB~3GB 这两块区域,注意于之间进程,内核地址空间区分,那个指的是每个进程都拥有的一块空间,而这个是总线或者说物理上只有一块的物理空间,可以理解为是总线的地址分配图。
物理地址信号在总线上出现,以上图为例子,最终地址如果属于 RAM ,也就是我们刚才所说的 物理空间中可用的部分,就会由相关的通道来处理,如果属于某一设备占用的范围,最终这个地址由对应的设备通道接收。
读者肯定会好奇,刚才不是说设备占用的物理地址空间是可以修改的吗,那么我修改了,地址如何知道由谁接收了,首先思考一下,不同的地址空间对应着不同的设备,是不是如同不同的虚拟地址对应不同的物理地址,那是不是应该有部件监控这一过程呢,上图很清楚,所有的地址数据信号的都要经过北桥,它必然要实现的一个功能就是,把地址传递到所属那个设备( RAM 也是一个设备,只是比较特殊 )以刚才物理空间那张图,因为实现设置了 PCI 设备占用的就是 3G 以上,所以地址在 3G 以上的时候,北桥就会知道这是属于 PCI 设备的,于是地址必然向下最终传递给 PCI BUS,然后又这个总线去处理这个地址信号。以此类推,比如访问 2MB,北桥查看之后 RAM 对应了这一块空间,于是地址信号到了 DDR 通道。
注意,实际上设计,可能是所有的设备都受到了地址信号,但是由北桥给每一个设备发送一个使能信号,就是来表示这个地址信号是否属于这个设备,比如为 1 表示地址有效,那么其他设备接收的信号必然就是 0,因为地址肯定只属于某一个设备,而不会是多个。我们必须要知道的一个过程,就是北桥必须实现地址空间的管理,让设备接收到属于它占用的物理地址空间的地址。
总之,物理地址来到了总线,最终会到达相关设备,然后设备可能紧接着就是接受总线传来的数据了,当你理解这一过程就是你理解物理地址空间的时候了!
这里展示的是,0~2G 的物理地址空间区域的分布,读者也可以查看自己的,获取更多的信息,来更好的掌握物理地址空间这个概念。
PCI 地址空间
PCI 几乎是现在所有的台式 PC 使用的外围总线了,当然它已经有了升级版,但是基本的原理还是没有改变的,PCI 在自己的总线区域有独立的地址空间,它连接的设备可以进行独立动态编址,很多讲有它的资料都没有涉及具体的CPU架构,而是直接说,当你在设备的 BAR(BASIC ADDRESS REGISTER)写入基址之后,那么映射就会自动完成,这是含糊不清的说法。
首先写入的基址到底是 PCI 总线空间的地址?还是整个物理空间的地址?这里我们得知道,PCI 总线有自己的地址空间,就像是一条内存条。它要让指定的设备可以接收到总线的数据,必然是建立了物理地址 -> PCI地址 这一映射关系的。
以上面 iomem 输出这个例子,这里的 0xa0000-0xbffff 说的就是物理地址空间,如果把地址写在地址总线,那么这里的地址信号不会被RAM接收,而是跑到了 PCI 总线控制器上,最终在总线上的数据会被它接受,然后转送到相关设备上。那我们想想,这个 0xa0000 在 PCI 空间的地址是多少? 答案就是 0xa0000 没错,当你在设备写入了相应的基址之后,这个映射正好是没有任何改变,意思是你写的值就是最后会在物理空间占用的值,但是是经过了地址映射的,只是正好相等。
以 X86 机构为例子,在引导的过程中,最关键的一步就是设备初始化,其中 PCI 设备,就是通过填写 BAR,来初始化设备的基址,在我们写的过程是通过 intel 预留的两个 IO 地址来沟通的,一旦我们写入了地址,主桥就会把之后在总线地址这一块地址信号传到 PCI 空间,也就是说主桥实现了 物理(总线)地址到PCI 地址的映射,只是这个映射刚好什么也不用做。
王齐老师在他 PCI 相关的书籍还以 Powe pC 为例子,详细讲述了地址映射的过程,对于那个架构,地址映射就不是简单的一一对应了,所以更好理解,而以 x86 为例子则是让很多刚入门的人误解,实际上往 PCI 设备基址寄存器写的值分配的是它在PCI总线空间的地址,但是我们的主桥又帮我们把这个地址不改变的映射在了物理地址的空间。
想了解更多PCI 的细节,推荐读者去王齐老师的博客。王老师是难得的好老师,不止是在计算机领域哟,有兴趣同学一读他的作品便知他的文笔不是一般人写的出来的拉,比如我就望尘莫及了。
Last updated