Tim*_*Tim 33 linux kernel virtual-memory
Linux 编程接口显示了进程的虚拟地址空间的布局。图中的每个区域都是一个段吗?
从了解 Linux 内核,
下面的意思是MMU中的分段单元将段和段内的偏移量映射到虚拟内存地址,然后分页单元将虚拟内存地址映射到物理内存地址是否正确?
内存管理单元(MMU)通过称为分段单元的硬件电路将逻辑地址转换为线性地址;随后,称为分页单元的第二个硬件电路将线性地址转换为物理地址(见图 2-1)。
那为什么说Linux不使用分段而只使用分页呢?
分段已包含在 80x86 微处理器中,以鼓励程序员将他们的应用程序拆分为逻辑相关的实体,例如子程序或全局和本地数据区。但是, Linux 以非常有限的方式使用分段。实际上,分段和分页是有些多余的,因为两者都可以用来分隔进程的物理地址空间:分段可以为每个进程分配不同的线性地址空间,而分页可以将相同的线性地址空间映射到不同的物理地址空间. 出于以下原因,Linux 更喜欢分页而不是分段:
• 当所有进程使用相同的段寄存器值时,即当它们共享同一组线性地址时,内存管理更简单。
• Linux 的设计目标之一是可移植到广泛的体系结构中。特别是 RISC 体系结构对分段的支持有限。
2.6 版本的 Linux 仅在 80x86 架构需要时才使用分段。
sou*_*edi 24
x86-64 架构在长模式(64 位模式)下不使用分段。
四个段寄存器:CS、SS、DS 和 ES 强制为 0,限制为 2^64。
https://en.wikipedia.org/wiki/X86_memory_segmentation#Later_developments
操作系统不再可能限制可用的“线性地址”范围。因此它不能使用分段来保护内存;它必须完全依赖分页。
不要担心 x86 CPU 的细节,这些细节仅适用于在传统 32 位模式下运行时。用于 32 位模式的 Linux 使用不多。它甚至可以被认为是“多年处于良性忽视状态”。请参阅Fedora [LWN.net, 2017] 中的32 位 x86 支持。
(碰巧 32 位 Linux 也不使用分段。但是你不需要相信我,你可以忽略它:-)。
ilk*_*chu 12
图中的每个区域都是一个段吗?
不。
虽然分段系统(在 x86 上处于 32 位保护模式)旨在支持单独的代码、数据和堆栈段,但实际上所有段都设置为相同的内存区域。也就是说,它们从 0 开始并在内存(*)的末尾结束。这使得逻辑地址和线性地址相等。
这称为“平面”内存模型,比具有不同段然后在其中包含指针的模型要简单一些。特别是,分段模型需要更长的指针,因为除了偏移指针之外还必须包括段选择器。(16 位段选择器 + 32 位偏移量,总共 48 位指针;而只是一个 32 位平面指针。)
除了平面内存模型之外,64 位长模式甚至不真正支持分段。
如果您要在 286 上以 16 位保护模式进行编程,您将需要更多的段,因为地址空间是 24 位,而指针只有 16 位。
(* 请注意,我不记得 32 位 Linux 如何处理内核/用户空间分离。分段将允许通过设置用户空间段限制来实现这一点,以便它们不包括内核空间。分页允许这样做,因为它提供了一个每页保护级别。)
那为什么说Linux不使用分段而只使用分页呢?
x86 仍然有段,你不能禁用它们。它们只是尽可能少地使用。在 32 位保护模式下,需要为平面模型设置段,即使在 64 位模式下,它们仍然存在。
因为 x86 有段,所以不可能不使用它们。但是cs(代码段)和ds(数据段)基地址都设置为零,因此实际上没有使用分段。线程本地数据是一个例外,通常未使用的段寄存器之一指向线程本地数据。但这主要是为了避免为此任务保留通用寄存器之一。
并不是说 Linux 不在 x86 上使用分段,因为这是不可能的。您已经强调了一部分,Linux 以非常有限的方式使用分段。第二部分是Linux 仅在 80x86 架构需要时才使用分段
您已经引用了原因,分页更容易且更便携。
图中的每个区域都是一个段吗?
这些是“segment”这个词的两种几乎完全不同的用法
惯例有共同的起源:如果被使用分段存储器模型(特别是在没有分页的虚拟存储器),则可能必须的数据和BSS的地址是相对于DS段的基础上,相对于SS基堆,并以相对码CS基地址。
因此,多个不同的程序可以加载到不同的线性地址,甚至可以在启动后移动,而无需更改相对于段基址的 16 位或 32 位偏移量。
但是你必须知道一个指针相对于哪个段,所以你有“远指针”等等。(实际的 16 位 x86 程序通常不需要将其代码作为数据访问,因此可以在某处使用 64k 代码段,也可能是另一个具有 DS=SS 的 64k 块,堆栈从高偏移量向下增长,数据位于底部。或者所有段基数相等的微小代码模型)。
32 / 64 位模式下的地址映射为:
页表(由 TLB 缓存)线性映射到 32(传统模式)、36(传统 PAE)或 52 位 (x86-64) 物理地址。(/sf/ask/3255640671/)。
此步骤是可选的:必须在启动期间通过在控制寄存器中设置位来启用分页。如果没有分页,线性地址就是物理地址。
注意,分割并不会让你在单个进程(或线程)使用的虚拟地址空间的超过32位或64位,这是因为扁平(线性)地址空间一切被映射到仅具有相同数目的位作为偏移量本身。(这不是 16 位 x86 的情况,其中分段实际上对于使用超过 64k 的内存以及主要是 16 位寄存器和偏移量很有用。)
CPU 缓存从 GDT(或 LDT)加载的段描述符,包括段基址。当您取消引用一个指针时,根据它所在的寄存器,它默认为 DS 或 SS 作为段。寄存器值(指针)被视为与段基址的偏移量。
由于段基数通常为零,因此 CPU 会对此进行特殊处理。或者从另一个角度来看,如果您确实有一个非零段基址,则加载会有额外的延迟,因为绕过添加基址的“特殊”(正常)情况不适用。
CS/DS/ES/SS的base和limit在32位和64位模式下都是0/-1。这被称为平面内存模型,因为所有指针都指向相同的地址空间。
(AMD CPU 架构师通过为 64 位模式强制执行平面内存模型来消除分段,因为主流操作系统无论如何都不使用它,除了 no-exec 保护,它通过使用 PAE 或 x86 分页以更好的方式提供 - 64 页表格式。)
TLS(线程本地存储):FS 和 GS在长模式下不固定在 base=0。(它们是 386 的新内容,没有被任何指令隐式使用,甚至不是rep使用 ES的-string 指令)。x86-64 Linux 将每个线程的 FS 基地址设置为 TLS 块的地址。
例如,mov eax, [fs: 16]将 16 字节中的 32 位值加载到该线程的 TLS 块中。
CS 段描述符选择 CPU 处于什么模式(16/32/64 位保护模式/长模式)。Linux 为所有 64 位用户空间进程使用单个 GDT 条目,为所有 32 位用户空间进程使用另一个 GDT 条目。(要使 CPU 正常工作,还必须将 DS/ES 设置为有效条目,SS 也是如此)。它还选择权限级别(内核(环0)vs.用户(环3)),因此即使返回64位用户空间,内核仍然要安排CS更改,使用iret或sysret代替正常跳转或 ret 指令。
在 x86-64 中,syscall入口点用于swapgs将 GS 从用户空间的 GS 翻转到内核的 GS,用于查找此线程的内核堆栈。(线程本地存储的特例)。该syscall指令不会将堆栈指针更改为指向内核堆栈;当内核到达入口点1时,它仍然指向用户堆栈。
DS/ES/SS 也必须设置为有效的段描述符,以便 CPU 在保护模式/长模式下工作,即使在长模式下忽略这些描述符的基数/限制。
因此,基本上 x86 分段用于 TLS,以及硬件要求您执行的强制性 x86 osdev 内容。
脚注 1:有趣的历史:在 AMD64 芯片发布前几年,内核开发人员和 AMD 架构师之间有邮件列表存档消息,导致对设计进行了调整,syscall因此它可以使用。有关详细信息,请参阅此答案中的链接。