新的疑问:
一,对于linux可执行文件(磁盘上)与进程中相关页的映射关系保存在哪里?
涉及虚拟空间和可执行文件的映射关系;是建立进程的其中一个步骤。
进程的建立:
首先是创建虚拟地址空间,由一组页映射函数将虚拟空间的各个页映射至相应的物理空间;在i386的linux下,创建虚拟地址空间实际只是分配一个页目录(Page Directory);
然后,读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系:就是虚拟空间与可执行文件的映射关系;当操作系统捕获到缺页时,它应知道程序当前所需要的页在可执行文件中的哪一个位置;VMA(Virtual Memory Area),在进程的相应的数据结构中设置一个.text段的VMA, 它对应ELF文件中偏移为0的.text,它的属性为只读。
最后,将CPU指令寄存器设置成可执行文件入口,启动运行,就是ELF文件头中保存的入口地址。
二,共享对象,通过什么数据结构,何种机制让所有进程共享?
三,32位CPU下,程序使用的空间能不能超过4GB?
若是指,虚拟地址空间,答案: "否",32位CPU只能使用32位指针;
若是指,计算机的内存空间,答案: "是",硬件层面,intel扩展至36位,并修改了页映射方式,使得新的映射方式可以访问更多的物理内存;Intel把这个地址扩展方式叫做PAE(Physical Address Extension).
深入理解计算机系统(原书第三版3)
Linux内核源代码分析-p53
1.硬件
由于大多数RISC机器以分页为主,linux内核设计时巧妙的绕过段机制(线性地址=段的起始地址+偏移量),把段的起始地址赋为0;
1.1.分段分页
CPU给出一个逻辑地址,通过此逻辑地址中的段选择符查表(段描述符表),GDTR中保存了GDT的基地址(Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B, 3C & 3D): System Programming Guide, chapter 2.4.1中明确了这是一个线性地址)。
寄存器GDTR中存的是线性地址,因为段描述符表保存在内存中,那么,GDTR在提供地址后是否需进行一次转换,把这个线性地址变为物理地址???
毛德操老师的《linux内核源代码情景分析》第一章-预备知识,有提到:
一旦启用页式管理,所有的线性地址,都要经过页式映射,连GDTR与LDTR中给出的段描述符表起始地址也不例外。
也就意味着,GDTR中的线性地址,要先去查页表(CR3中存放页目录的基地址),找到对应的物理地址;再去查段描述符表;(这样确实绕圈子???)
这里即使需要映射为物理地址,也只需要一次。(GDTR里面存储的线性地址,在保护模式启动之前就是物理地址,之后自然就是虚拟地址。GDTR的虚拟地址在内核里面,你切换N个进程环境,这个虚拟地址对应的PTE指向的都是同一块物理页面)。那么存在一种什么机制,使GDTR中存的线性地址转换成对应的物理地址,而不用每次去查页表?
为了避免每次访问内存时都去引用描述符表,去读和解码一个段描述符,每个段寄存器都有一
个“可见”部分和一个“隐藏”部分(隐藏部分也被称为“描述符缓冲”或“影子寄存器”)。当一个段选
择符被加载到一个段寄存器可见部分中时,处理器也同时把段选择符指向的段描述符中的段地址、段限长以及访问控制信息加载到段寄存器的隐藏部分中。缓冲在段寄存器(可见和隐藏部分)中的信息使得处理器可以在进行地址转换时不再需要花费时间从段描述符中读取基地址和限长值。
每个系统必须定义一个 GDT,并可用于系统中所有程序或任务。
那么,同样,对于IDTR中存的基址……
对比:控制寄存器 CR3 保存着是当前页目录表在物理内存中的基地址(因此 CR3 也被称为页目录基地址寄存器 PDBR)。
为了把逻辑地址转换成一个线性地址,处理器会执行以下操作:
1. 使用段选择符中的偏移值(段索引)在 GDT 或 LDT 表中定位相应的段描述符。(仅当一个新的段选择符加载到段寄存器中时才需要这一步。)
2. 利用段描述符检验段的访问权限和范围,以确保该段是可访问的并且偏移量位于段界限内。
3. 把段描述符中取得的段基地址加到偏移量上,最后形成一个线性地址。
段选择符中索引值(Index)用于选择指定描述符表中 8192(2^13)个描述符中的一个。处理器将该索引值乘上 8(本身这个索引值就是在高13位,低3位清零,就是GDT中对应表项的偏移值)。GDT中第 2 个是内核代码段描述符,其选择符是 0x08;第 3 个是内核数据段描述符,其选择符是 0x10。
对应用程序来说段选择符是作为指针变量的一部分而可见,但选择符的值通常是由链接编辑器或链接加载程序进行设置或修改,而非应用程序。段描述符通常由编译器、链接器、加载器或者操作系统来创建,但绝不是应用程序。
图截取自赵炯老师《Linux 内核 0.12 完全注释》。
《Linux 内核 0.12 完全注释》p98:分段和分页是两种不同的地址变换机制,它们都对整个地址变换操作提供独立的处理阶段。尽管两种机制都使用存储在内存中的变换表,但所用的表结构不同。实际上,段表存储在线性地址空间,而页表则保存在物理地址空间。因而段变换表可由分页机制重新定位而无需段机制的信息或合作。段变换机制把虚拟地址(逻辑地址)变换成线性地址,并且在线性地址空间中访问自己的表,但是并不知晓分页机制把这些线性地址转换到物理地址的过程。类似地,分页机制也不知道程序产生地址的虚拟地址空间。分页机制只是简单地把线性地址转换成物理地址,并且在物理内存中访问自己的转换表。
LGDT使用内存中一个6字节操作数来加载GDTR寄存器。头两个字节代表描述符表的长度,后4个字节是描述符表的基地址。然而请注意,访问LDTR寄存器的指令LLDT所使用的操作数却是一个2字节的操作数,表示全局描述符表GDT中一个描述符项的选择符。该选择符所对应的GDT表中的描述符项应该对应一个局部描述符表。
如果没有开启分页,那么处理器直接把线性地址映射到物理地址(即线性地址被送到处理器地址总线上)。如果对线性地址空间进行了分页处理,那么就会使用二级地址转换把线性地址转换成物理地址。
GDT 和 IDT 在内核数据段中,因此它们的线性地址也同样等于它们的物理地址.
对于 Linux 0.12 内核代码和数据来说,在 head.s 程序的初始化操作中已经把内核代码段和数据段都设置成为长度为 16MB 的段。在线性地址空间中这两个段的范围重叠,都是从线性地址 0 开始到地址0xFFFFFF 共 16MB 地址范围。在该范围中含有内核所有的代码、内核段表(GDT、IDT、TSS)、页目录和内核的二级页表、内核局部数据以及内核临时堆栈.
此时的GDT在0-16MB空间中,一一对应物理内存,GDTR中的线性地址等于物理地址;
默认情况下 Linux 0.12 内核最多可管理 16MB 的物理内存,共有 4096 个物理页面(页帧),
每个页面 4KB。通过上述分析可以看出:①内核代码段和数据段区域在线性地址空间和物理地址空间中是一样的。这样设置可以大大简化内核的初始化操作。②GDT 和 IDT 在内核数据段中,因此它们的线性地址也同样等于它们的物理地址。
物理空间仓库:Linux内核中有个全局量mem_map,是一个指针,指向一个page数据结构的数组,每个page结构代表着一个物理页面,整个数组就代表着系统中的全部物理页面。
毛德操老师的《linux内核源代码情景分析》-p70
交换设备(通常是磁盘,也可是普通文件)的每个物理页面也要在内存中有个相应的数据结构。swap_info_struct数据结构,用以描述和管理用于页面交换的文件或设备。
虚拟空间仓库:vm_area_struct数据结构;
mm_struct结构,在每个进程的“进程控制块”,即task_struct结构中,有一个指针指向该进程的mm_struct结构。mm_struct是进程整个用户空间的抽象。
缺页中断时操作系统怎么知道磁盘地址?
深入理解计算机系统(原书第三版),第九章,虚拟内存;
页表就是一个页表条目(Page Table Entry,PTE)的数组,假设每个PTE是一个有效位(valid bit)和一个n位地址字段组成。有效位表明是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配,否则,这个地址就指向该虚拟页在磁盘上的起始位置。
以上,在linux中是对应实现如下:
当物理页面在内存中时,页面表项是一个叫pte_t结构, 指向一个内存页面;当物理页面不在内存中时,则是一个swap_entry_t结构,指向一个盘上页面;
CPU产生一次"页面异常(Page Fault)", 页面异常处理程序handle_pte_fault(),pte_present(), 检查表项中的P标志位,看看物理页面是否在内存中,如果不在,则通过pte_none()检查表项是否为空,即全0.如果非空,说明映射已经建立,只是物理页面不在内存中,所以要通过do_swap_page(),从交换设备上换入这个页面。
1.2.堆栈切换
当特权级 3 的程序在执行时,特权级 3 的堆栈的段选择符和栈指针会被分别存放在 SS 和 ESP 中,并且在发生堆栈切换时被保存在被调用过程的堆栈上。
特权级 0、1 和 2 的堆栈的初始指针值都存放在当前运行任务的 TSS 段中。
1.3.任务切换(所谓上下文切换);
2.软件