为什么Windows要保留1GB(或2GB)作为系统地址空间?

20

众所周知,在32位系统上,Windows应用程序通常有2Gb的私有地址空间。使用/3GB开关可以将此空间扩展为3Gb。

操作系统保留了剩下的4Gb。

我的问题是为什么?

运行在内核模式中(例如设备驱动程序代码)的代码具有自己的地址空间。那么,在独占的4Gb地址空间之上,为什么操作系统仍然要保留每个用户模式进程的2Gb空间?

我认为原因是从用户模式到内核模式的调用转换。例如,对NtWriteFile的调用需要内核分派例程的地址(因此操作系统会在每个应用程序中保留2Gb)。但是,使用SYSENTER,系统服务号是否足以让内核模式代码知道调用哪个函数/服务呢?

如果您能向我澄清为什么操作系统需要占用每个用户模式进程的2Gb(或1Gb),我将不胜感激。


7
我真的难以相信人们投票将这个关闭为 NPR。 - Dave Markle
9
我同意,Dave,断言操作系统设计与编程无关是有点疯狂的。 - Alex Martelli
1
你的帖子中到处都使用了小写字母'b'来表示位(bit),而应该使用大写字母'B'来表示字节(Byte)。所有操作系统都遵循字节寻址方案。因此,对于32位PC,2的32次方等于4 Gb(4 Gb独特地址),但由于计算机实际上是寻址一个字节而不是一个位,所以它变成了4GB。 - RBT
5个回答

24

两个不同的用户进程具有不同的虚拟地址空间。由于虚拟↔物理地址映射是不同的,所以在从一个用户进程切换到另一个用户进程时,TLB缓存会被使无效。这非常昂贵,因为如果TLB中未缓存地址,则任何内存访问都将导致故障和PTE的遍历。

Syscall涉及两个上下文切换:用户→内核,然后内核→用户。为了加快速度,通常会将前1GB或2GB的虚拟地址空间保留给内核使用。由于虚拟地址空间在这些上下文切换中不会改变,因此无需进行TLB清除。这是通过每个PTE中的用户/监管位实现的,该位确保内核内存仅在内核空间中可访问;即使页面表相同,用户空间也不能访问。

如果存在两个专门用于内核使用的TLBs的硬件支持,则这种优化将不再有用。但是,如果您有足够的空间可以分配,那么制作一个更大的TLB可能更值得。

在x86上,Linux曾经支持一种称为“4G/4G分裂”的模式。在此模式下,用户空间可以完全访问整个4GB虚拟地址空间,而内核也有完整的4GB虚拟地址空间。如上所述,代价是每个 syscall都需要进行TLB清除,以及更复杂的例程来在用户和内核内存之间复制数据。这已被证明会造成高达30%的性能损失。


自从这个问题最初被提出并回答以来,时代已经发生了变化:64位操作系统现在更加普遍。在x86-64上的当前操作系统中,允许用户程序使用的虚拟地址范围为0到247-1(即0-128TB),而内核永久驻留在虚拟地址从247×(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仍然超出范围。)
可能存在哪些错误?例如,假设您正在实现快速排序并有两个指针ab分别指向数组的开头和结尾。如果您选择中位数作为枢轴,使用表达式(a+b)/2,只要这两个地址都在2GB以下,它就可以正常工作,但如果它们都在2GB以上,那么加法将遇到整数溢出,并且结果将落在数组之外。(正确的表达式是a+(b-a)/2。)

顺便提一下,32位Linux系统默认采用3G/1G用户/内核分割方式,程序历史上运行在2-3GB堆栈范围内,因此任何这类编程错误很可能会很快被排除。64位Linux系统可让32位程序访问0-4GB内存。


谢谢您提供的清晰回答。因此,总结一下,从设计的角度来看,内核可以在自己的地址空间上运行,由于进程上下文切换而引入性能损失,或者映射到用户模式进程的地址空间中并占用一些地址空间。 - Rivari
不一定。有其他实现分离的方法,例如静态类型。在Microsoft Research的Singularity操作系统中,所有用户进程和内核都存在于单个地址空间中(实际上,MMU被关闭了,尽可能在x86上)。用户进程之间以及内核与用户空间之间的保护由编译器保证:因为所有代码都是用内存安全和类型安全的语言(C#)编写的,并且不允许直接访问内存,因此不需要硬件分离。 - Jörg W Mittag
1
我所知道的Windows和其他所有常用操作系统都基于硬件执行本地应用程序并通过特权分离进行操作(或完全没有特权分离),与Singularity模型不兼容。是的,有一些研究性的操作系统是基于编译时和VM强制安全性而不是我们在过去几十年中习惯的硬件支持的虚拟地址空间。不幸的是,尽管这个想法很酷,但它需要“重新编写所有内容”的情况... - ephemient
@ephemient 我们所做的决定了我们将来能够做什么。 - smwikipedia
您是否有任何关于“为什么虚拟空间会被分割以提高性能”的资料参考?我查阅了几本计算机体系结构和操作系统教材、一些操作系统实现书籍,但并没有找到相关的信息。 - piotrek

3

Windows(像任何操作系统一样)不仅仅是内核和驱动程序。

您的应用程序依赖于许多操作系统服务,这些服务不仅存在于内核空间中。 有许多缓冲区、句柄和各种资源可以映射到您的进程自己的地址空间中。每当您调用返回窗口句柄或画笔等内容的Win32 API函数时,这些内容必须在您的进程中分配。因此,Windows的某些部分确实在内核中运行,其他部分在它们自己的用户模式进程中运行,并且一些部分,即您的应用程序需要直接访问的部分,被映射到您的地址空间中。其中一部分很难避免,但重要的另一个因素是性能。如果每个 Win32 调用都需要上下文切换,那么它将是一个主要的性能影响。如果其中一些可以在用户模式下处理,因为它们所依赖的数据已经映射到您的地址空间中,则避免了上下文切换,并节省了相当多的 CPU 周期。

因此,任何操作系统都需要分配一些地址空间。 我相信 Linux 默认只为操作系统设置了 1GB。

为什么 MS 在 Windows 中定位了 2GB 的原因曾经在 Raymond Chen 的博客上解释过。我没有链接,也记不起细节,但这个决定是因为 Windows NT 最初的目标是 Alpha 处理器,而在 Alpha 上,有某些非常好的理由来进行50/50的拆分。 ;)

这与 Alpha 支持32位和64位代码有关。 :)


1
谢谢您的回答。我可以从Windows Internals中添加这个信息:“系统服务分派程序KiSystemService将调用者的参数从线程的用户模式堆栈复制到其内核模式堆栈(以便用户无法在内核访问它们时更改参数),然后执行系统服务。如果传递给系统服务的参数指向用户空间中的缓冲区,则必须在内核模式代码可以将数据复制到它们或从它们中复制数据之前对这些缓冲区进行可访问性探测。”因此,基本上内核喜欢出现在进程用户地址空间中,以提高性能。 - Rivari

2

在内核模式下运行的代码(即设备驱动程序代码)有自己的地址空间。

不是这样的。它必须与x86处理器上进程的用户模式部分共享该地址空间。这就是为什么内核必须保留足够的空间并有限制地使用地址空间。


我正在阅读《使用Windows Driver Foundation开发驱动程序》这本书。第35页:客户端和驱动程序运行在不同的地址空间中,因此驱动程序必须小心地访问缓冲区。特别是,驱动程序不能简单地引用指向用户模式缓冲区的指针,因为无法确定该地址处的数据是否有意义或者该指针是否有效。在阅读这之前,我认为内核模式代码运行在与任何进程调度的地址空间相同的地址空间中。也许这本书是错的。 - Rivari
内核模式的东西肯定有自己的地址空间。诀窍在于,Windows不仅仅是内核模式的东西。 - jalf
内核不能直接从用户空间解引用指针,原因很简单,就是因为用户空间程序可能会撒谎并传入一个错误的指针。如果解引用这个指针,操作系统将会崩溃。另一个原因是指针的目标可能已经被交换出去或者在内存中没有映射,这时需要特别小心,以免在中断处理程序或其他敏感区域触发故障。请注意,地址空间没有分离的原因是,如果分离了,每次上下文切换都需要进行TLB刷新,这样的代价太高无法合理地进行。 - nos

1

我认为最好的答案是操作系统设计师们觉得到你需要关心这个问题的时候,人们已经在使用64位Windows了。

但是这里有一个更好的讨论


1
这不仅仅是Windows的事情,Linux也会为内核保留一部分地址空间。 我只是不明白为什么,在内核模式下运行的代码有它自己的地址空间。 - Rivari
硬件本身也经常喜欢拥有自己的存储器,这可能是其中大部分原因。 - Dave Markle
硬件仍然可以在内核地址空间中获得自己的内存。 - Rivari

0

部分答案与微处理器架构的历史有关。以下是我所知道的一些内容,其他人可以提供更多最近的细节。

Intel 8086处理器采用段偏移架构来管理内存,提供20位内存地址,因此可寻址的物理内存总量为1MB。

与当时的竞争对手(如Zilog Z80)不同,Intel 8086只有一个地址空间,不仅要容纳电子内存,还要容纳所有输入/输出通信,例如键盘、串行端口、打印机端口和视频显示器。(相比之下,Zilog Z80有一个单独的输入/输出地址空间,具有专用的汇编指令进行访问)

需要为越来越多的外围扩展留出空间,这导致最初决定将地址空间分成从0-640K的电子内存和从640K到1MB的“其他东西”(输入/输出、ROMS、视频内存等)。

随着x86系列的发展和演变,以及个人电脑的发展,类似的方案已经被使用,最终形成了今天4G地址空间的2G/2G划分。


我认为这不是任何带有MMU硬件的合适解释。内存映射I/O存在于物理地址空间中。在任何给定时刻使用的2GB + 2GB虚拟地址空间可能完全没有I/O空间被映射。 - ephemient
我理论上同意。从性能的角度来看,如果一个应用程序有一大块视频数据要显示,并且在该应用程序的内存空间中甚至没有分配缓冲区的地方,那么如何以高效的方式通过到达视频硬件?为此保留地址空间中的空间提供了一种实现这一目标的方法。还要记住,2G/2G分割的决定是很久以前做出的 - 它早于Windows XP,后者针对的是具有64M内存的机器。此外,我的评论是关于(古老的)历史,而不是它是否是一个好主意。 - Bevan
需要直接视频缓冲区的应用程序可以请求将其映射到其地址空间中;当未被请求时,映射不应存在。正如我在我的答案中所说,2G/2G(或3G/1G)分割并不是不能改变的基本问题--4G用户空间的上下文切换成本太高了。 - ephemient

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接