为啥 Windows 为其系统地址空间保留 1Gb(或 2 Gb)?
Posted
技术标签:
【中文标题】为啥 Windows 为其系统地址空间保留 1Gb(或 2 Gb)?【英文标题】:Why does Windows reserve 1Gb (or 2 Gb) for its system address space?为什么 Windows 为其系统地址空间保留 1Gb(或 2 Gb)? 【发布时间】:2010-11-10 01:04:36 【问题描述】:众所周知,Windows 应用程序在 32 位系统上通常具有 2Gb 的私有地址空间。这个空间可以通过 /3Gb 开关扩展到 3Gb。
操作系统会为自己保留剩余的 4Gb。
我的问题是为什么?
在内核模式下运行的代码(即设备驱动程序代码)有自己的地址空间。为什么在独占的 4Gb 地址空间之上,操作系统仍要为每个用户模式进程保留 2Gb?
我认为原因是用户模式和内核模式调用之间的转换。例如,对NtWriteFile
的调用将需要内核调度例程的地址(因此系统在每个应用程序中保留 2Gb)。但是,使用SYSENTER
,系统服务号是否不足以让内核模式代码知道正在调用哪个函数/服务?
如果您能向我解释一下为什么操作系统占用每个用户模式进程的 2Gb(或 1Gb)如此重要。
【问题讨论】:
我真的不敢相信人们投票决定将其作为 NPR 关闭。 我同意,Dave,断言操作系统设计“与编程无关”简直是疯了。 您在帖子中的任何地方都使用了代表位的小写“b”。它应该是代表字节的大写“B”。所有操作系统都遵循字节可寻址方案。因此,对于 32 位 PC,2 的 32 次方等于 4 Gb(4 Gb 唯一地址),但由于计算机实际上寻址的是一个字节而不是一个位,因此它变成了 4GB。 【参考方案1】:两个不同的用户进程有不同的虚拟地址空间。由于虚拟↔物理地址映射不同,TLB 缓存在从一个用户进程切换到另一个用户进程时失效。这是非常昂贵的,因为如果没有在 TLB 中缓存的地址,任何内存访问都会导致错误和PTEs 的遍历。
系统调用涉及两个上下文切换:用户→内核,然后内核→用户。为了加快速度,通常保留前 1GB 或 2GB 的虚拟地址空间供内核使用。因为虚拟地址空间不会在这些上下文切换之间发生变化,所以不需要 TLB 刷新。这是由每个 PTE 中的用户/主管位启用的,它确保内核内存只能在内核空间中访问;即使页表相同,用户空间也无权访问。
如果硬件支持两个独立的 TLB,其中一个专门供内核使用,那么这种优化将不再有用。但是,如果您有足够的空间来奉献,那么只制作一个更大的 TLB 可能更值得。
x86 上的 Linux 曾经支持一种称为“4G/4G 拆分”的模式。在这种模式下,用户空间可以完全访问整个 4GB 虚拟地址空间,内核也拥有完整的 4GB 虚拟地址空间。如上所述,成本是 每个 系统调用都需要 TLB 刷新,以及在用户和内核内存之间复制数据的更复杂的例程。经测量,这会导致高达 30% 的性能损失。
自从最初提出并回答这个问题以来,时代已经发生了变化:64 位操作系统现在更加普遍。在 x86-64 上的当前操作系统中,用户程序允许从 0 到 247-1 (0-128TB) 的虚拟地址,而内核永久驻留在 247 的虚拟地址中sup>×(217-1) 到 264-1(或从 -247 到 -1,如果您将地址视为有符号整数)。
如果您在 64 位 Windows 上运行 32 位可执行文件会发生什么?你会认为从 0 到 232 (0-4GB) 的所有虚拟地址都很容易获得,但是为了避免暴露现有程序中的错误,32 位可执行文件仍然限制为 0- 2GB,除非它们使用/LARGEADDRESSAWARE
重新编译。对于那些,他们可以访问 0-4GB。 (这不是一个新标志;同样适用于使用/3GB
开关运行的 32 位 Windows 内核,它将默认的 2G/2G 用户/内核拆分更改为 3G/1G,当然 3-4GB 仍然是超出范围。)
可能存在哪些错误?例如,假设您正在实现快速排序并且有两个指针,a
和 b
指向数组的开头和结尾。如果你用(a+b)/2
选择中间作为pivot,只要两个地址都在2GB以下,它就可以工作,但如果它们都在上面,那么加法会遇到整数溢出,结果会在数组之外。 (正确的表达式是a+(b-a)/2
。)
顺便说一句,具有默认 3G/1G 用户/内核拆分的 32 位 Linux 历史上运行的程序的堆栈位于 2-3GB 范围内,因此任何此类编程错误都可能很快被清除. 64 位 Linux 允许 32 位程序访问 0-4GB。
【讨论】:
这个明确的答案。因此,总而言之,从设计的角度来看,内核可以在自己的地址空间上运行并由于进程上下文切换而引入性能损失,或者被映射到用户模式进程地址空间并吃掉一些地址空间。跨度> 不一定。还有其他实现分离的方法,例如静态类型。在 Microsoft Research 的 Singularity OS 中,所有用户进程和内核都存在于一个地址空间中(实际上,MMU 实际上已关闭,在 x86 上尽可能地关闭)。用户进程之间以及内核与用户空间之间的保护由编译器保证:因为所有代码都是用内存安全和类型安全的语言 (C#) 编写的,并且不允许直接访问内存,因此不需要硬件分离。 Windows 和我所知道的所有其他主动部署的操作系统基本上都基于执行本机应用程序,并通过硬件(或根本没有)权限分离,并且与 Singularity 模型不兼容。是的,有一些研究操作系统基于编译时和虚拟机强制安全,而不是我们过去几十年习惯的硬件支持的虚拟地址空间。不幸的是,尽管这个想法很酷,但它是一个“重写一切”的场景...... @ephemient 我们所做的决定了我们将/可以做什么。 你有关于“为什么要分割虚拟空间以提高性能”的信息的参考吗?我浏览了几本计算机体系结构和操作系统教科书,一些操作系统实现书籍,但没有任何相关信息。【参考方案2】:Windows(与任何操作系统一样)远不止内核 + 驱动程序。
您的应用程序依赖于许多不仅仅存在于内核空间中的操作系统服务。 有很多缓冲区、句柄和各种资源可以映射到您的进程自己的地址空间。每当您调用一个返回窗口句柄或画笔的 Win32 API 函数时,这些东西都必须分配到您的进程中的某个位置。因此,Windows 的一部分在内核中运行,是的,其他部分在它们自己的用户模式进程中运行,还有一些,您的应用程序需要直接访问的,被映射到您的地址空间。这部分是很难避免的,但一个重要的附加因素是性能。如果每个 Win32 调用都需要上下文切换,这将是一个重大的性能损失。如果其中一些可以在用户模式下处理,因为它们所依赖的数据已经映射到您的地址空间,则可以避免上下文切换,并且可以节省相当多的 CPU 周期。
所以任何操作系统都需要留出一些数量的地址空间。 我相信 Linux 默认只为操作系统设置 1GB。
曾在 Raymond Chen 的博客中解释过 MS 选择 2GB 与 Windows 的原因。我没有链接,我不记得细节,但做出这个决定是因为 Windows NT 最初也是针对 Alpha 处理器的,而在 Alpha 上,做 50/50 确实有很好的理由分裂。 ;)
这与 Alpha 对 32 位和 64 位代码的支持有关。 :)
【讨论】:
回答您的问题。我可以从 Windows Internals 中添加:“系统服务调度程序 KiSystemService 将调用者的参数从线程的用户模式堆栈复制到其内核模式堆栈(这样用户就无法在内核访问它们时更改参数) ),然后执行系统服务。如果传递给系统服务的参数指向用户空间中的缓冲区,则必须在内核模式代码将数据复制到或从中复制数据之前探测这些缓冲区的可访问性。所以基本上,出于性能原因,内核喜欢出现在进程用户地址空间中。【参考方案3】:在内核模式下运行的代码(即设备驱动程序代码)有自己的地址空间。
不,它没有。它必须与 x86 处理器上进程的用户模式部分共享该地址空间。这就是为什么内核必须总共保留足够的空间并限制地址空间的原因。
【讨论】:
我正在阅读“使用 Windows Driver Foundation 开发驱动程序”一书。第 35 页:客户端和驱动程序在不同的地址空间中运行,因此驱动程序必须小心访问缓冲区。特别是,驱动程序不能简单地取消引用指向用户模式缓冲区的指针,而不能确定该地址处的数据是有意义的,或者该指针甚至是有效的。在阅读之前,我认为内核模式代码运行在与当前调度的任何进程相同的地址空间中。也许这本书是错的。 内核模式的东西当然有自己的地址空间。诀窍在于,Windows 不仅仅是内核模式的东西。 内核不能直接从用户空间取消引用一个指针,原因很简单,用户空间程序可能撒谎,并发送了一个错误的指针。它会使操作系统崩溃以取消引用它。另一个原因是,无论出于何种原因,指针的目标都可能被换出或未映射到内存中,因此需要格外小心,例如不要从中断处理程序或其他敏感区域触发故障。请注意,地址空间不分开的原因是,如果它们是分开的,则在每次上下文切换时都需要 TLB 刷新 - 这太昂贵而无法证明。【参考方案4】:部分答案与微处理器架构的历史有关。以下是我所知道的一些信息,其他人可以提供更新的详细信息。
Intel 8086 处理器具有用于内存的段偏移架构,提供 20 位内存地址,因此总可寻址物理内存为 1MB。
与那个时代的竞争处理器不同——比如 Zilog Z80——英特尔 8086 只有一个地址空间,它不仅要容纳电子存储器,而且还要容纳与诸如以下小型外围设备的所有输入/输出通信键盘、串行端口、打印机端口和视频显示器。 (相比之下,Zilog Z80 有一个单独的输入/输出地址空间,带有专用的汇编操作码供访问)
为不断扩大的外围扩展范围留出空间的需要导致最初决定将地址空间分段为 0-640K 的电子内存和“其他东西”(输入/输出、ROMS、视频内存等)从 640K 到 1MB。
随着 x86 产品线的发展和演变,PC 也随之演变,使用了类似的方案,以今天的 2G/2G 拆分 4G 地址空间结束。
【讨论】:
我不认为这是对任何带有 MMU 的硬件的合适解释。内存映射 I/O 存在于物理地址空间中。在任何给定时间点使用的 2GB+2GB 虚拟地址空间很可能没有的 I/O 空间被映射。 理论上我同意。从性能的角度来看,如果一个应用程序有一大块视频数据要显示,并且该应用程序的内存空间中没有任何地方可以分配缓冲区,那么您如何以有效的方式编组视频硬件?在地址空间中保留空间来执行此操作提供了一种实现此目的的方法。还要记住,关于 2G/2G 拆分的决定是很久以前做出的——它早于 Windows XP,它本身针对的是具有 64M 内存的机器。另外,我的 cmets 是关于(古代)历史的,而不是它是否是一个好主意。 一个需要直接视频缓冲区的应用程序可以请求一个映射到它的地址空间;当没有请求时,映射不应该存在。正如我在回答中所说的那样,2G/2G(或 3G/1G)拆分并不是那么根本,以至于无法更改——4G 用户空间对于上下文切换来说太昂贵了。【参考方案5】:我相信最好的答案是操作系统设计者认为,当你不得不关心的时候,人们会使用 64 位 Windows。
但这里有一个better discussion。
【讨论】:
这不仅仅是Windows的东西,Linux也为内核保留了一部分地址空间。我只是不明白为什么在内核模式下运行的代码有它自己的 adderss 空间。 嗯,硬件本身也经常喜欢拥有自己的内存,这可能是主要原因。 硬件仍然可以在内核地址空间中获得自己的内存。以上是关于为啥 Windows 为其系统地址空间保留 1Gb(或 2 Gb)?的主要内容,如果未能解决你的问题,请参考以下文章