解惑—Linux中的地址空间(一)

有这么一系列的问题,是否在困扰着你:用户程序编译连接形成的地址空间在什么范围内?内核编译后地址空间在什么范围内?要对外设进行访问, I/O 的地址空间又是什么样的?
先回答第一个问题。 Linux 最常见的可执行文件格式为 elf(Executable a nd Linkable Format ) 。在 elf 格式的可执行代码中, ld 总是从 0x8000000 开始安排程序的“代码段”,对每个程序都是这样。至于程序执行时在物理内存中的实际地址,则由内核为其建立内存映射时临时分配,具体地址取决于当时所分配的物理内存页面。

我们可以用 Linux 的实用程序 objdump 对你的程序进行反汇编,从而知晓其地址范围。
例如:假定我们有一个简单的 C 程序 Hello.c
  # include <stdio.h>
  greeting ( )
  {
              printf(“Hello,world!/n”);
  }
  main()
   {
         greeting();
   }
之所以把这样简单的程序写成两个函数,是为了说明指令的转移过程。我们用 gcc ld 对其进行编译和连接,得到可执行代码 hello 。然后,用 Linux 的实用程序 objdump 对其进行反汇编:
$objdump –d hello
得到的主要片段为:
08048568 <greeting>:
   8048568:     pushl  %ebp
   8048569:     movl  %esp, %ebp
   804856b:     pushl  $0x809404
   8048570:     call    8048474  <_init+0x84>
   8048575:     addl   $0x4, %esp
   8048578:     leave
   8048579:     ret
   804857a:     movl  %esi, %esi
   0804857c <main>:
   804857c:     pushl  %ebp
   804857d:     movl  %esp, %ebp
   804857f:     call    8048568  <greeting>
   8048584:     leave
   8048585:     ret
   8048586:     nop
   8048587:     nop

其中,像 08048568 这样的地址,就是我们常说的虚地址(这个地址实实在在的存在,只不过因为物理地址的存在,显得它是“虚”的罢了)。

.虚拟内存、内核空间和用户空间

    Linux 虚拟内存的大小为 2^32 (在 32 位的 x86 机器上),内核将这 4G 字节的空间分为两部分。最高的 1G 字节(从虚地址 0xC0000000 0xFFFFFFFF )供内核使用,称为“ 内核空间 ”。而较低的 3G 字节(从虚地址 0x00000000 0xBFFFFFFF ),供各个进程使用,称为“ 用户空间 ”。因为每个进程可以通过系统调用进入内核,因此, Linux 内核空间由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有 4G 字节的 虚拟地址空间 ( 也叫 虚拟内存 )
   
    每个进程有各自的私有用户空间( 0 3G ),这个空间对系统中的其他进程是不可见的。最高的 1GB 内核空间则为所有进程以及内核所共享。另外,进程的“ 用户空间 ”也叫“ 地址空间 ”,在后面的叙述中,我们对这两个术语不再区分。
用户空间不是进程共享的,而是进程隔离的。每个进程最大都可以有 3GB 的用户空间。一个进程对其中一个地址的访问,与其它进程对于同一地址的访问绝不冲突。比如,一个进程从其用户空间的地址 0x1234ABCD 处可以读出整数 8 ,而另外一个进程从其用户空间的地址 0x1234ABCD 处可以读出整数 20 ,这取决于进程自身的逻辑。
任意一个时刻,在一个 CPU 上只有一个进程在运行。所以对于此 CPU 来讲,在这一时刻,整个系统只存在一个 4GB 的虚拟地址空间,这个虚拟地址空间是面向此进程的。当进程发生切换的时候,虚拟地址空间也随着切换。由此可以看出,每个进程都有自己的虚拟地址空间,只有此进程运行的时候,其虚拟地址空间才被运行它的 CPU 所知。在其它时刻,其虚拟地址空间对于 CPU 来说,是不可知的。所以尽管每个进程都可以有 4 GB 的虚拟地址空间,但在 CPU 眼中,只有一个虚拟地址空间存在。虚拟地址空间的变化,随着进程切换而变化。
从 上面我们知道,一个程序编译连接后形成的地址空间是一个虚拟地址空间,但是程序最终还是要运行在物理内存中。因此,应用程序所给出的任何虚地址最终必须被 转化为物理地址,所以,虚拟地址空间必须被映射到物理内存空间中,这个映射关系需要通过硬件体系结构所规定的数据结构来建立。这就是我们所说的段描述符表 和页表, Linux 主要通过页表来进行映射。
于是,我们得出一个结论,如果给出的页表不同,那么 CPU 将某一虚拟地址空间中的地址转化成的物理地址就会不同。所以我们为每一个进程都建立其页表,将每个进程的虚拟地址空间根据自己的需要映射到物理地址空间上。既然某一时刻在某一 CPU 上只能有一个进程在运行,那么当进程发生切换的时候,将页表也更换为相应进程的页表,这就可以实现每个进程都有自己的虚拟地址空间而互不影响。所以,在任意时刻,对于一个 CPU 来说,只需要有当前进程的页表,就可以实现其虚拟地址到物理地址的转化。

.内核空间到物理内存的映射   

    内核空间对所有的进程都是共享的,其中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据,不管是内核程序还是用户程序,它们被编译和连接以后,所形成的指令和符号地址都是 虚地址 (参见 2.5 节中的例子),而不是物理内存中的物理地址。
虽然内核空间占据了每个虚拟空间中的最高 1GB 字节,但映射到物理内存却总是从最低地址( 0x00000000 )开始的,如图 4.2 所示,之所以这么规定,是为了在内核空间与物理内存之间建立简单的线性映射关系。其中, 3GB 0xC0000000 )就是物理地址与虚拟地址之间的位移量,在 Linux 代码中就叫做 PAGE_OFFSET
                  
我们来看一下在 include/asm/i386/page.h 头文件中对内核空间中地址映射的说明及定义:

#define __PAGE_OFFSET           (0xC0000000)
……
#define PAGE_OFFSET             ((unsigned long)__PAGE_OFFSET)
#define __pa(x)                 ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x)                 ((void *)((unsigned long)(x)+PAGE_OFFSET))
对于内核空间而言,给定一个虚地址 x ,其物理地址为“ x- PAGE_OFFSET” ,给定一个物理地址 x ,其虚地址为“ x+ PAGE_OFFSET ”。

这里再次说明,宏 __pa() 仅仅把一个内核空间的虚地址映射到物理地址,而决不适用于用户空间,用户空间的地址映射要复杂得多,它通过分页机制完成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值