我对多线程进程中栈和堆的组织方式有些困惑:
- 每个线程都有自己的私有栈。
- 所有线程共享堆。
- 当程序动态创建线程(例如在Java中使用new Thread()),对象会被分配在堆上。
那么,堆是否包含用于线程对象的内存,也就是说,堆是否包含属于线程的栈?
这里的描述有意保持模糊,因为我们不想限制线程软件的实现者。
每个线程都有自己私有的堆栈。
由于每个线程执行的一组函数是互相独立的,它们需要存储返回地址等信息,因此每个线程都需要有自己的堆栈。
所有线程共享堆。
这是实现的最简单方法。这也意味着所有线程共享一个公共的内存块,因此每个线程可以通过修改内存来与其他线程通信。
当程序动态创建线程(例如Java中的new Thread())时,对象分配在堆上。
你在第一个问题中提到的堆栈。我们需要为它保留内存。因此,我们分配了一个堆的块并将其分配给线程,并说使用这个内存块来实现你的堆栈。(不是说它就是这样做的,但这是一种简单的技术方法)。
那么堆是否包含线程对象的内存,这意味着堆是否包含属于线程的堆栈?
在单线程程序中,可以将堆栈实现为堆的块。将堆栈和堆分开并向彼此增长只是一个概念。它们如何实现是未定义的,我们没有理由不能在堆内部实现堆栈。有关更多信息,请参见此问题:堆栈增长方向。
将“堆栈”视为像任何其他数据结构一样的数据结构。它可以用许多方式实现。
以下是在2000年左右之前的C和C++程序中典型的堆栈实现描述。大多数仍然以这种方式实现:
有一段连续的内存地址范围被称为“堆栈”。通常,在具有内存控制器的系统上(对于英特尔来说,这意味着80386及更高版本),此内存地址范围的页面在使用之前不会分配给物理内存。通常,这个连续的地址范围出现在地址空间的末尾。但是,这绝不是实现堆栈的唯一方法。例如,您可以将堆栈实现为链表,其中列表的每个节点都是堆栈帧。支持称为“continuations”的结构的语言经常这样做。事实上,它们通常使用DAG,因为单个堆栈帧可能会生成多个其他同时有效的堆栈帧。
还有一件事情可以做,介于两者之间,其中您的节点只是包含几个堆栈帧的大型内存区域。当创建新帧超出节点时,在幕后分配另一个节点。
或者,所有局部变量都可以使用new或类似方法分配,并在超出范围时销毁。编译器可以在幕后完成此操作。
因此,在像Java这样甚至没有C或C ++中的指针的语言中,担心堆栈的确切位置或底层内存如何分配是有点愚蠢的。它甚至可能因不同的完全兼容JVM而异。
我会说通常情况下,C++中的pthreads实现堆栈的方式是在我描述C和C ++的历史工作方式的章节的最后一段所述的多线程程序的方式。它们通常还有一个“守护页”,即为堆栈分配区域开头的一个故意未映射的页面,以便运行时由于堆栈空间不足而导致程序通常会引发SEGV错误。(实际上,这似乎是一个过于简化而错误的解释,请查看Ben Voigt的评论获取守护页的真正用途)。
VirtualAlloc
,在Unix和类Unix系统上使用mmap
获取。 - Ben Voigt