多个堆栈和堆放在虚拟内存的哪里?

6
我正在编写一个内核,需要(并希望)将多个堆栈和堆放入虚拟内存中,但我无法有效地将它们放置在适当的位置。普通程序如何实现此功能?
在32位系统提供的有限虚拟内存中,栈和堆如何放置以尽可能具有增长空间?
例如,当一个简单程序加载到内存中时,其地址空间的布局可能如下所示:
[  Code  Data  BSS  Heap->  ...  <-Stack  ]

在这种情况下,堆可以增长到虚拟内存允许的大小(例如,堆可以增长到栈的大小),我认为这是大多数程序中堆的工作方式。没有预定义的上限。
许多程序有共享库,它们被放置在虚拟地址空间的某个位置。然后有一些多线程程序,每个线程都有一个堆栈。.NET程序有多个堆,所有这些堆都必须能够以某种方式增长。
我只是不明白,在不对所有堆和栈的大小设置预定义限制的情况下如何合理高效地完成这项工作。
2个回答

3
我假设您已经完成了内核的基础工作,具有可以将虚拟内存页面映射到RAM的页面故障陷阱处理程序。接下来,您需要一个虚拟内存地址空间管理器,用户模式代码可以从中请求地址空间。选择一个段粒度,以防止过度碎片化,64KB(16页)是一个很好的数字。允许用户模式代码同时保留空间和提交空间。使用一个简单的位图来跟踪段状态,4GB / 64KB = 64K x 2位就可以完成任务。页面故障陷阱处理程序还需要查看此位图,以知道页面请求是否有效。
堆栈是一种固定大小的虚拟内存分配,通常为1兆字节。线程通常只需要其中几个页面,具体取决于函数嵌套级别,因此预留1MB并仅提交顶部的几个页面。当线程嵌套更深时,它会触发页面故障,内核可以简单地将额外的页面映射到RAM,以允许线程继续。您需要将底部的几个页面标记为特殊页面,当线程在这些页面上出现页面故障时,您需要声明此网站的名称。
堆管理器最重要的工作是防止碎片化。最好的方法是创建一个按大小分区的lookaside列表。小于8个字节的所有内容都来自第一个段的列表。从第二个列表中获得8到16,从第三个列表中获得16到32,以此类推。随着容量的增加,您需要逐渐增加桶的大小。您需要根据实际情况调整桶的大小以达到最佳平衡。非常大的分配直接来自VM地址管理器。
第一次访问lookaside列表中的条目时,您将分配一个新的VM段。您将该段细分为较小的块,并使用链接列表。当释放这样的分配时,您将该块添加到空闲块列表中。所有块的大小相同,而不管程序请求的大小,因此不会出现任何碎片。当段完全被使用且没有可用的空闲块时,您将分配一个新段。当一个段仅包含空闲块时,您可以将其返回给VM管理器。
此方案允许您创建任意数量的堆栈和堆。

你很好地描述了堆是如何工作的,但我不明白这个“方案”如何让我创建任意数量的堆。如果我在用户代码和数据之后为第一个堆保留了16 MB,那么我应该把第二个堆放在哪里呢?放在第一个堆的后面吗?那么第一个堆就不能超过其最初的16 MB。或者在扩展时分裂堆(拆分堆),但这对于缓存局部性(例如高频堆)、最大对象大小(例如大对象堆)、垃圾回收或出于任何原因使用多个堆都是不利的。例如,.NET有许多堆,他们是如何做到的? - Daniel A.A. Pelsmaeker
你漏掉了堆分配是由多个段组成而不是固定大小的部分。当堆增长时,它将有许多段,它们可以分散在地址空间中。缓存局部性通常非常好,因为数组具有来自相同查找列表链的固定大小元素。 - Hans Passant

0

简单来说,由于系统资源总是有限的,因此您无法无限制地使用。

内存管理始终由几个层次组成,每个层次都有其明确定义的责任。从程序的角度来看,应用程序级别的管理器是可见的,通常只关注其自己的单个分配堆。上面一层可以处理从(其)一个全局堆中创建多个堆并将它们分配给子程序(每个子程序都有自己的内存管理器)。在这之上可能是标准的malloc()/free(),它使用的是操作系统处理进程页面和实际内存分配(它基本上不仅不关心多个堆,甚至不关心用户级堆)。

内存管理是昂贵的,陷入内核也是如此。将两者结合起来可能会对性能造成严重影响,因此从应用程序的角度来看,实际堆管理实际上是在用户空间(C运行时库)中实现的,以提高性能(以及其他目的,暂不讨论)。

当加载共享(DLL)库时,如果在程序启动时加载,则它很可能会被加载到CODE / DATA /等等,因此不会发生堆碎片。另一方面,如果在运行时加载,则几乎没有其他选择,只能使用堆空间。 静态库当然只是链接到CODE / DATA / BSS /等部分。

最终,您需要对堆和栈施加限制,以使它们不太可能溢出,但可以分配其他资源。 如果需要超出该限制,则可以执行以下操作之一

  • 以错误终止应用程序
  • 让内存管理器为该堆栈/堆分配/调整/移动内存块,并很可能在此之后对堆进行碎片整理(其自身级别);这就是为什么free()通常表现不佳的原因。

考虑到每个call平均有一个相当大的1KB堆栈帧(如果应用程序开发人员没有经验,则可能会发生),10MB堆栈足以支持10240个嵌套的call。顺便说一下,除此之外,每个线程几乎没有必要拥有多个堆栈和堆。


但是在线程之间切换不应该切换整个地址空间(并导致TLB被刷新),因此对于进程使用的每个线程,其堆栈必须存在于进程的地址空间中。我帖子中的链接显示了CLR进程具有非常多的堆。因此,在一个地址空间中需要多个堆和堆栈。 - Daniel A.A. Pelsmaeker
地址空间是一个非常不同的抽象层级。实际上,在这种情况下,您在同一个地址空间中拥有多个堆栈和堆。地址空间本身由操作系统管理,而堆不是;它由用户级库代码管理。 - Powerslave

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