type
status
slug
summary
tags
category
icon
password
new update day
Property
Oct 22, 2023 01:31 PM
created days
Last edited time
Oct 22, 2023 01:31 PM

各种地址的相关概念

在 Linux 0.11 内核中,在进行地址映射操作时,我们需要首先分清楚3种地址以及它们之间的变换概念:
  1. 程序(进程)的虚拟和逻辑地址;
  1. CPU 的线性地址
  1. 实际物理内存地址

1 虚拟地址

虚拟地址是指由程序产生的由段选择符和段内偏移地址两个部分组成的地址。因为这两部分组成的地址并没有直接用来访问物理内存,而是需要通过分段地址变换机制处理或映射后才能对应到物理内存上,因此这种地址被称为虚拟地址。
虚拟地址空间由 GDT 映射的全局地址空间和由 LDT 映射的局部地址空间组成。选择符的索引部分由 13 个比特位表示,加上区分 GDT 和 LDT 的 1 个比特位,因此 Intel 80x86 CPU 一共可以索引 16384(2^14) 个选择符。若每个段的长度都取最大值 4G ,则最大虚拟地址空间范围是 16384*4G = 64T(64*4).

2 逻辑地址

逻辑地址是指由程序产生的与段相关的偏移地址部分。在 Intel 保护模式下即是指程序执行代码段限长内的偏移地址(假定代码段、数据段完全一样)。应用程序员仅需要与逻辑地址打交道,而分段和分页机制对他来说是完全透明的,仅由系统编程人员涉及。不过有些资料并不区分逻辑地址和虚拟地址的概念,而是将它们统称为逻辑地址。

3 线性地址

线性地址(Linear Address) 是虚拟地址到物理地址变换之间的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386 的线性地址空间容量为 4G。

4 物理地址

物理地址 (Physical Address) 是指出现在 CPU 外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

5 虚拟存储(虚拟内存)

虚拟存储(虚拟内存)(Virtual Memory) 是指计算机呈现出要比实际拥有的内存大得多的内存虚拟存储 (或虚拟内存)量。因此它允许程序员编制并运行比实际系统拥有的内存大得多的程序。这使得许多大型项目也能够在具有有限内存资源的系统上实现。一个很恰当的比喻是:你不需要很长的轨道就可以让一列火车从上海开到北京。你只需要足够长的铁轨(比如说3 公里)就可以完成这个任务。采取的方法是把后面的铁轨广刻铺到火车的前面,只要你的操作足够快并能满足要求,列车就能象在一条完整的轨道上运行。这也就是虚拟内存管理需要完成的任务。在 Linux 0.11 内核中,给每个程序(进程)都划分了总容量为 64MB的虚拟内存空间。因此程序的逻辑地址范围是 0x0000000 到 0x4000000。
如上所述,有时我们也把逻辑地址称为虚拟地址。因为逻辑地址与虚拟内存空间的概念类似,并且也是与实际物理内存容量无关。

内存分段机制

在内存分段系统中,一个程序的逻辑地址通过分段机制自动地映射(变换) 到中间层的 4GB (232)线性地址空间中。程序每次对内存的引用都是对内存段中内存的引用。当程序引用一个内存地址时,通过把相应的段基址加到程序员看得见的逻辑地址上就形成了一个对应的线性地址。此时若没有启用分页机制,则该线性地址就被送到 CPU 的外部地址总线上,用于直接寻址对应的物理内存。见图 4-4 所示
虚拟地址(逻辑地址)到物理地址的变换过程
虚拟地址(逻辑地址)到物理地址的变换过程
CPU 进行地址变换(映射)的主要目的是为了解决虚拟内存空间到物理内存空间的映射问题。虚拟内存空间的含义是指一种利用二级或外部存储空间,使程序能不受实际物理内存量限制而使用内存的一种方法。通常虚拟内存空间要比实际物理内存量大得多。
那么虚拟存储管理是怎样实现的呢?原理与上述列车运行的比喻类似。首先,当一个程序需要使用一块不存在的内存时 (也即在内存页表项中已标出相应内存页面不在内存中),CPU 就需要一种方法来得知这个情况。这是通过 80386 的页错误异常中断来实现的。当一个进程引用一个不存在页面中的内存地址时,就会触发 CPU 产生页出错异常中断,并把引起中断的线性地址放到 CR2 控制寄存器中。因此处理该中断的过程就可以知道发生页异常的确切地址,从而可以把进程要求的页面从二级存储空间(比如硬盘上)加载到物理内存中。如果此时物理内存已经被全部占用,那么可以借助二级存储空间的一部分作为交换缓冲区 (Swapper) 把内存中暂时不使用的页面交换到二级缓冲区中,然后把要求的页面调入内存中。这也就是内存管理的缺页加载机制,在 Linux 0.11 内核中是在程序 mm/memory.c 中实现。
Intel CPU 使用段(Segment)的概念来对程序进行寻址。每个段定义了内存中的某个区域以及访问的优先级等信息。假定大家知晓实模式下内存寻址原理,现在我们根据 CPU 在实模式和保护模式下寻址方式的不同,用比较的方法来简单说明 32 位保护模式运行机制下内存寻址的主要特点。
在实模式下,寻址一个内存地址主要是使用段和偏移值,段值被存放在段寄存器中(例如 ds),并且段的长度被固定为 64KB。段内偏移地址存放在任意一个可用于寻址的寄存器中(例如 si)。因此,根据段寄存器和偏移寄存器中的值,就可以算出实际指向的内存地址,见图 5-7 (a)所示。
实模式与保护模式下寻址方式的比较
实模式与保护模式下寻址方式的比较
而在保护模式运行方式下,段寄存器中存放的不再是被寻址段的基地址,而是一个段描述符表(Scgment Descriptor Table)中某一描述符项在表中的索引值。索引值指定的段描述符项中含有需要寻址的内存段的基地址、段的长度值和段的访问特权级别等信息。寻址的内存位置是由该段描述符项中指定的段基地址值与一个段内偏移值组合而成。段的长度可变,由描述符中的内突指定。可见,和实模式下的寻址相比,段寄存器值换成了段描述符表中相应段描述符的索引值以及段表选择位和特权级,称为段选择符 (Segment Sclector),但偏移值还是使用了原实模式下的概念。这样,在保护模式下寻址一个内存地址就需要比实模式下多一道手续,也即需要使用段描述符表。这是由于在保护模式下访问一个内存段需要的信息比较多,而一个 16 位的段寄存器放不下这么多内容。示意图见图 5-7(6)所示。注意,如果你不在一个段描述符中定义一个内存线性地址空间区域,那么该地址区域就完全不能被寻址,CPU 将扣绝访问该地址区域。
每个描述符占用 8 个字节,其中含有所描述段在线性地址空间中的起始地址(基址)、段的长度、段的类型(例如代码段和数据段)、段的特权级别和其他一些信息。一个段可以定义的最大长度是 4GB.
保存描述符项的描述符表有 3 种类型,每种用于不同目的。
  • 全局描述符表 GDT (Global DescriptorTable)是主要的基本描述符表,该表可被所有程序用于引用访问一个内存段。
  • 中断描述符表IDT(InterruptDescriptor Table) 保存有定义中断或异常处理过程的段描述符。IDT 表直接替代了 8086 系统中的中断向量表。为了能在 80X86 保护模式下正常运行,我们必须为 CPU 定义一个 GDT 表和一个IDT 表。
  • 最后一种类型的表是局部描述符表 LDT (Local Descriptor Table)。该表应用于多任务系统中,通常每个任务使用一个LDT 表。作为对 GDT 表的扩充,每个 LDT 表为对应任务提供了更多的可用描述符项,因而也为每个任务提供了可寻址内存空间的范围。这些表可以保存在线性地址空间的任何地方。
为了让 CPU 能定位GDT 表、IDT 表和当前的 LDT 表,需要为 CPU 分别设置GDTR、IDTR 和LDTR 三个特殊寄存器。这些寄存器中将存储对应表的 32 位线性基地址和表的限长字节值。表限长值是表的长度值-1。
当CPU 要寻址一个段时,就会使用 16 位的段寄存器中的选择符来定位一个段描述符。在 80X86 CPU中,段寄存器中的值右移 3 位即是描述符表中一个描述符的索引值。13 位的索引值最多可定位 8192(0-8191)个的描述符项。选择符中位 2(TI)用来指定使用哪个表。若该位是 0 则选择符指定的是 GDT表中的描述符,否则是 LDT 表中的描述符。
每个程序都可有若干个内存段组成。程序的逻辑地址(或称为虚拟地址) 即是用于寻址这些段和段中具体地址位置。在 Linux 0.11 中,程序逻辑地址到线性地址的变换过程使用了 CPU 的全局段描述符表GDT 和局部段描述符表 LDT。由 GDT 映射的地址空间称为全局地址空间,由 LDT 映射的地址空间则称为局部地址空间,而这两者构成了虚拟地址的空间。具体的使用方式见图 5-8 所示。
Linux 系统中虚拟地址空间分配图
Linux 系统中虚拟地址空间分配图
图中画出了具有两个任务时的情况。可以看出,每个任务的局部描述符表 LDT 本身也是由 GDT 中描述符定义的一个内存段,在该段中存放着对应任务的代码段和数据段描述符,因此 LDT 段很短,其段限长通常只要大于 24字节即可。同样,每个任务的任务状态段TSS 也是由 GDT 中描述符定义的一个内存段,其段限长也只要满足能够存放一个 TSS 数据结构就够了。
对于中断描述符表 idt,它保存在内核代码段中。由于在 Linux 0.11 内核中,内核和各任务的代码段和数据段都分别被映射到线性地址空间中相同基址处,且段限长也一样,因此内核的代码段和数据段是重叠的,各任务的代码段和数据段分别也是重叠的,参见图 5-10 或图 5-11 所示。任务状态段 TSS (TaskState Segment) 用于在任务切换时 CPU 自动保存或恢复相关任务的当前执行上下文 (CPU 当前状态)。例如对于切换出的任务,CPU 就把其寄存器等信息保存在该任务的 TSS 段中,同时 CPU 使用新切换进任务的 TSS 段中的信息来设置各寄存器,以恢复该任务的执行环境,参见图 4-37 所示。在 Linux 0.11中,每个任务的 TSS 段内容被保存在该任务的任务数据结构中。另外,Linux 0.11 内核中没有使用到 GDT表中第 4个描述符(图中 syscall 描述符项)。从ncude/linux/sched.h 文件中第 150 行上的原英文注释(如下所示)可以猜想到,Linus 当时设计内核时曾经想把系统调用的代码放在这个专门独立的段中。
Linux 0.11 线性地址空间的使用示意图
Linux 0.11 线性地址空间的使用示意图
Linux 0.11 系统任务在虚拟空间中顺序排列所占的空间范围
Linux 0.11 系统任务在虚拟空间中顺序排列所占的空间范围
notion image

内存分页管理

若采用了分页机制,则此时线性地址只是一个中间结果,还需要使用分页机制进行变换,再最终映射到实际物理内存地址上。与分段机制类似,分页机制允许我们重新定向(变换)每次内存引用,以适应我们的特殊要求。使用分页机制最普遍的场合是当系统内存实际上被分成很多凌乱的块时。它可以建立个大而连续的内存空间映像,好让程序不用操心和管理这些分散的内存块。分页机制增强了分段机制的性能。另外,页地址变换建立在段变换基础之上,任何分页机制的保护措施并不会取代段变换的保护措施而只是进行更进一步的检查操作。
内存分页管理机制的基本原理是将 CPU 整个线性内存区域划分成 4096 字节为1页的内存页面。程序申请使用内存时,系统就以内存页为单位进行分配。内存分页机制的实现方式与分段机制很相似,但并不如分段机制那么完善。因为分页机制是在分段机制之上实现的,所以其结果是对系统内存具有非常灵活的控制权,并且在分段机制的内存保护上更增加了分页保护机制。为了在 80X86 保护模式下使用分页机制,需要把控制寄存器 CRO 的最高比特位(位 31) 置位。
在使用这种内存分页管理方法时,每个执行中的进程(任务) 可以使用比实际内存容量大得多的连续地址空间。为了在使用分页机制的条件下把线性地址映射到容量相对很小的物理内存空间上,80386审用了页目录表和页表。页目录表项与页表项格式基本相同,都占用4个字节,并日每个页目录表或页表必须只能包含 1024 个页表项。因此一个页目录表或一个页表分别共占用1页内存。页目录项和页表项的小区别在于页表项有个已写位 D (Dirty),而页目录项则没有。
线性地址到物理地址的变换过程见图 5-9 所示。图中控制寄存器 CR3 保存着是当前页目录表在物理内存中的基地址(因此 CR3 也被称为页目录基地址寄存器 PDBR)。32 位的线性地址被分成三个部分分别用来在页目录表和页表中定位对应的页目录项和页表项以及在对应的物理内存页面中指定页面内的偏移位置。因为1个页表可有 1024 项,因此一个页表最多可以映射 内存:又因为一人页目录表最多有 1024项,对应1024 个二级页表,因此一个页目录表最多可以映射 容量的内存。即一个页目录表就可以映射整个线性地址空间范围。
线性地址到物理地址的变换示意图
线性地址到物理地址的变换示意图
由于 Linux 0.1x 系统中内核和所有任务都共用同一个页目录表,使得任何时刻处理器线性地址空间到物理地址空间的映射函数都一样。因此为了让内核和所有任务都不互相重叠和干扰,它们都必须从虚拟地址空间映射到线性地址空间的不同位置,即占用不同的线性地址空间范围。
对于 Intel 80386 系统,其 CPU 可以提供多达 4G 的线性地址空间。一个任务的虚拟地址需要首先通过其局部段描述符变换为 CPU 整个线性地址空间中的地址,然后再使用页目录表 PDT (一级页表)和页表 PT(二级页表)映射到实际物理地址页上。为了使用实际物理内存,每个进程的线性地址通过二级内存页表动态地映射到主内存区域的不同物理内存页上。由于 Linux 0.11 中把每个进程最大可用虚拟内存空间定义为 64MB,因此每个进程的逻辑地址通过加上(任务号)*64MB,即可转换为线性空间中的地址。不过在注释中,在不至于搞混的情况下我们有时将进程中的此类地址简单地称为逻辑地址或线性地址。
对于 Linux 0.11 系统,内核设置全局描述符表 GDT 中的段描述符项数最大为 256,其中 2 项空闲、2项系统使用,每个进程使用两项。因此,此时系统可以最多容纳(256-4)/2 =126 个任务,并且虚拟地范围是 ((256-4)/2)* 64MB 约等于 8G。但0.11内核中人工定义最大任务数NR TASKS = 64个,每务逻辑地址范围是 64M,并且各个任务在线性地址空间中的起始位置是 (任务号)64MB。因此全部任所使用的线性地址空间范围是 64MB64 =4G,见图 5-10 所示。图中示出了当系统具有 4 个任务时的情况。内核代码段和数据段被映射到线性地址空间的开始 16MB 部分,并且代码和数据段都映射到同一个区域,完全互相重叠。而第 1 个任务(任务 0) 是由内核“人工”启动运行的,其代码和数据包含在内核代码和数据中,因此该任务所占用的线性地址空间范围比较特殊。任务 0的代码段和数据段的长度是从线性地址 0 开始的 640KB 范围,其代码和数据段也完全重叠,并且与内核代码段和数据段有重叠的部分。实际上,Linux 0.11 中所有任务的指今空间I (Instruction) 和数据空间 D (Data) 都合用一块内存即一个进程的所有代码、数据和堆栈部分都处于同一内存段中,也即是 I&D 不分离的一种使用方式。
Linux 0.11 线性地址空间的使用示意图
Linux 0.11 线性地址空间的使用示意图
任务1的线性地址空间范围也只有从 64MB 开始的 640KB 长度。它们之间的详细对应关系见后面说明。任务 2 和任务 3 分别被映射线性地址 128MB 和 192MB 开始的地方,并且它们的逻辑地址范围均是64MB。由于 4G 地址空间范围正好是 CPU 的线性地址空间范围和可寻址的最大物理地址空间范围,而且在把任务 0 和任务 1 的逻辑地址范围看作 64MB 时,系统中同时可有任务的逻辑地址范围总和也是4GB,因此在 0.11 内核中比较容易混淆三种地址概念。
如果也按照线性空间中任务的排列顺序排列虚拟空间中的任务,那么我们可以有图 5-11 所示的系统同时可拥有所有任务在虚拟地址空间中的示意图,所占用虚拟空间范围也是 4GB。其中没有考虑内核代码和数据在虚拟空间中所占用的范围。另外,在图中对于进程2 和进程3 还分别给出了各自逻辑空间中代码段和数据段(包括数据和堆栈内容) 的位置示意图。
Linux 0.11 系统任务在虚拟空间中顺序排列所占的空间范围
Linux 0.11 系统任务在虚拟空间中顺序排列所占的空间范围
请还需注意,进程逻辑地址空间中代码段 (Code Section) 和数据段 (Data Section)的概念与 CPU分段机制中的代码段和数据段不是同一个概念。CPU 分段机制中段的概念确定了在线性地址空间中一个段的用途以及被执行或访问的约束和限制,每个段可以设置在 4GB 线性地址空间中的任何地方,它们可以相互独立也可以完全重叠或部分重叠。而进程在其逻辑地址空间中的代码段和数据段则是指由编译器在编译程序和操作系统在加载程序时规定的在进程逻辑空间中顺序排列的代码区域、初始化和未初始化的数据区域以及堆栈区域。进程逻辑地址空间中代码段和数据段等结构形式见图所示。有关逻辑地址空间的说明请参见内存管理一章内容。
进程代码和数据在其逻辑地址空间中的分布
进程代码和数据在其逻辑地址空间中的分布
Linux 0.11 中的 put_page 函数linux 0.11 修改时间片大小后的表现