内存映射文件

来源:互联网 发布:easybcd怎么引导linux 编辑:程序博客网 时间:2024/05/23 12:02
内存映射文件一直没弄明白,这几天在网上到处搜索,看了两篇文章,总算是弄明白了。在讲内存映射文件前,先讲讲 MMU 和内存映射到底是是什么。 

        MMU 是 Memory Management Unit 的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟内存、物理内存的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权。 

        这里提到了虚拟内存,虚拟地址,物理地址,虚拟地址和物理地址的映射。词语多了,有点晕。我们来举一个例子来说明下。 

        任何时候,计算机上都存在一个程序能够产生的地址集合,我们称之为地址范围。这个范围的大小由 CPU 的位数决定。例如一个 32 位的 CPU,它的地址范围是 0 ~ 0xFFFFFFFF(4G)。这个范围就是我们的程序能够产生的地址范围,我们把这个地址范围称为虚拟地址空间。该空间中的某一个地址我们称之为虚拟地址。与虚拟地址空间和虚拟地址相对应的则是物理地址空间和物理地址。对于这台机器,如果我们配置的是 2G 的内存,那么,它的虚拟地址空间范围是 0x00000000 ~ 0xFFFFFFFF(4G),而物理地址空间范围是 0x000000000 ~ 0x7FFFFFFF(2G)。每个进程都有自己的 4G 地址空间(32 位操作系统),从 0x00000000 ~ 0xFFFFFFFF。 

        目前大多数操作系统都会采用分页(paging)机制。虚拟地址空间的划分以页(page)为单位,而相应的物理地址空间的划分以页桢(frame)为单位。页和页桢的大小必须相同。为了简化描述,这里我们把虚拟地址空间和物理地址空间来划分空间的单位均称为页。 

        使用了分页机制之后,4G 的虚拟地址空间被分成了固定大小的页,每一页或者被映射到物理内存,或者被映射到硬盘上的交换文件(虚拟内存)中,或者没有映射任何东西。对于一般程序来说,4G 的地址空间,只有一小部分映射了物理内存,大片大片的部分是没有映射任何东西。CPU 是依据于一个叫做页目录和页表的结构来将虚拟地址转换成物理地址。CPU 在执行内存指令时,会自动根据页目录和页表中的信息,把虚拟地址转换成物理地址。 

        物理内存分页,一个物理页的大小为 4K 字节,页的索引从 0 开始,即 第 0 页,第 1 页,第 2 页,等等。因此,第 0 个物理页从物理地址 0x00000000 开始,由于一页为 4K,即 0x1000,所以,第 1 个物理页从物理地址 0x00001000 开始,第 2 个物理页从物理地址 0x00002000 开始,以此类推,第 12 个物理页从物理地址 0x0000C000 开始。注意,这里我们描述的是页的起始地址,如果要找某页的第 N(N < 4096,即 4K,因为一页 4K 个字节) 个字节,那么 N 就是所谓的偏移量。很明显,一页的偏移量,即最大 4K,用 12bit 表示即可。因此,对于 32 位 CPU,用 32bit 地址的低 12bit 表示偏移量,剩下的高 20bit 用来寻址(寻找页的起始地址)。64 位的 CPU 同理,一页同样是 4K,所以用低 12bit 表示偏移量,剩下的高 52bit 用来寻址。 

        接下来要讲的是页表页目录的概念,这两个概念比较重要,因为 CPU 就是依赖这两个信息来将虚拟地址砖为物理地址的。页表和页目录都是存放在物理页中的。 

        一个页表大小为 4K,因此一个页表是存放在一个物理页中的。一个页表由 1024 个页表项组成,所以,一个页表项大小为 4 个字节,即 32bit。页表项会存储一些信息,高 20bit 存储一个物理页的起始地址,低 12bit 存放一些标志。所以,一个页表可以记录 1024 个物理页的起始地址。如果是 64 位的 CPU,20bit 不足以存放物理页的起始地址,我也没弄明白如何处理,当然这不是讨论的范畴。我猜测,64 位 CPU,一个页表由 512 个页表项组成,一个页表项 8 个字节,这样就由足够的 bit 来寻址了。 

        一个页目录大小为 4K,因此一个页目录是存放在一个物理页中的。和页表类似,一个页目录由 1024 个页目录项组成,一个页目录项大小为 4 个字节,即 32bit。页目录项同样会存储一些信息,高 20bit 存放一个页表(页表是放在一个物理页中)所在物理页的起始地址,低 12bit 存放一些标志。对于 64 位 CPU,我依然和页表的猜测是一样的思路。 

        因此,只要能找到页目录,就能找到页表,进而找到物理页。事实上,对于 x86 系统,页目录的物理地址放在 CPU 的 CR3 寄存器中。 

        这里,我们梳理一下几个概念(以 32bit CPU 为例): 
  • 每个进程都有独立的 4G 的虚拟地址。因此,对于某个虚拟地址,请问下自己,它是属于哪个进程的。
  • 每个进程都有一个入口的物理地址。这个物理地址存放于 CR3 寄存器中,其实就是该进程的页目录表基地址(物理地址)。可以理解成:进程的入口地址 = CR3 = 页目录表的基地址。
  • 不同的进程可以有相同的虚拟地址,但它们映射成的物理地址是不一样的。因为 CR3 寄存器存的页目录基地址是不一样的。
  • 在用户进程空间中,只有一部分虚拟地址映射着实际物理地址。

        接下来,就开始讲 CPU 将虚拟地址转换成物理地址的过程了,请仔细阅读了。一个虚拟地址 32bit,我们分成三段:高 10bit,中间 10bit,低 12bit。它们依次代表着页目录的索引页表的索引物理页的偏移量。对于一个要转换成物理地址的虚拟地址(假设为 0x01AF5518),将按照以下步骤来操作: 

  1. 把虚拟地址拆分成 3 部分(高 10 bit,中 10 bit,低 12 bit),换成二进制如下:        0000 0001 1010 1111 0101 0101 0001 1000 

            按 10,10,12 来分,得到: 
            (页目录索引)00 000 00110,(页表项索引)10 1111 0101,(偏移)0101 0001 1000 

            换算成十六进制后可以得到如下结果: 
            页目录索引 = 6,页表项索引 = 0x2F5,偏移 = 0x518 
            
  2. 在 CR3 寄存器中找到该进程的入口物理地址即页目录表基地址。CR3 中存放的就是页目录表基地址。该地址是什么我们不得而知,和进程有关。我们假设页目录表基地址为 0xAA0E5000。        
  3. 计算页表项的物理地址。页表地址存放在页目录表中的第 6 个项目中(每个页目录项为 4 个字节),即:[0xAA0E5000 + 4 * 6] = [0xAA0E5018]。物理地址 0xAA0E5018 就是要找的页目录项的物理地址。回忆上面的内容,页目录项里的内容,其高 20 bit 就是页目录表的基地址(物理地址)。假设 0xAA0E5018 页目录项的内容为 0x00000867。        
  4. 计算页面的起始地址。上面得到的 0x00000867,其高 20 bit 才是页表的基地址:0x3D955000。我们要找的页面在这个页表中的第 0x2F5 项:[0x3D955000 + 4 * 0x2F5] = [0x3D955BD4]。即物理地址 0x3D955BD4 存放的内容包含了物理页的起始地址。0x3D955BD4 存放的内容时候什么我们不得而知,假设 [0x3D955BD4] = 0x7095e847。那么物理页的起始地址为 0x7095E847 的高 20 bit,即 x0x7095E000。        
  5. 计算最终的物理地址:物理地址起始地址 + 偏移量 = x0x7095E000 + 0x00000518 = 0x7095E518。
        现代的多用户多进程操作系统,需要 MMU,才能达到每个用户进程都拥有自己独立的地址空间的目标。Windows 将地址的低 2G 分给用户态空间,高 2G 分给内核态空间,这是默认的分配方式,可以通过启动参数改为 3G/1G。而 Linux 就是 3G/1G 划分:低 3G 作为用户态空间,高 1G 作为内核态空间。内核态和用户态共享 4G 的 32 位地址空间。 

        因此,上面我们说的,每个用户有独立的 4G 的用户空间并不准确,用户空间和内核空间共享 4G 地址空间。更多关于地址空间的划分请参见 http://techsingular.net/?p=1035。 

        之所以让内核态与用户态共享地址空间,其实是因为尽管每次进入内核态都可能发生进程切换,但是大多数情况下并非一定发生这样的切换。因此,共享地址空间可以避免进出内核态的时候进行地址空间(CR3)的切换。在 x86 架构下,切换地址空间要导致所有 TLB 失效,CPU 必须访问主存更新 TLB。所以,共享地址空间是一个性能 hack,仅此而已。这个性能 hack 如此历史悠久,以至于有些人如我一般完全无法想像还能有其它方式。 

        传统的文件 I/O 是通过用户进程发布 read() 和 write() 系统调用来传输数据的。为了在内核空间的文件系统页与用户空间的内存区之间移动数据,一次以上的拷贝操作几乎总是免不了的。这是因为,在文件系统页与用户缓冲区之间往往没有一一对应关系。 

        有一种大多数操作系统都支持的特殊类型的 I/O 操作,允许用户进程最大限度地利用面向页的系统 I/O 特性,并完全摒弃缓冲区拷贝。这就是内存映射 I/O。这就是将文件映射到内存(这里的内存,就是虚拟内存,不是物理内存),即磁盘上的文件数据就像是在内存中一样。这利用了操作系统的虚拟内存功能,无需在内存(物理内存)中实际保留一份文件的拷贝,就可实现文件内容的动态高速缓存。 

        虚拟内存和磁盘 I/O 是紧密关联的,从很多方面看来,它们只是同一件事物的两面。在处理大量数据时,尤其要记得这一点。如果数据缓冲区是按页对齐的,且大小是内建页大小的倍数,那么,对大多数操作系统而言,其处理效率会大幅提升。 
         
                                                             用户内存到文件系统页的映射
0 0
原创粉丝点击