并发编程,C/C++中的堆栈

7
抱歉,如果这看起来像是旧问题的重复,我很抱歉。我在Stack Overflow上查看了几个问题,阅读了tanenbaum的《现代操作系统》等书籍,但仍有疑问。
首先,我希望能提供更详细的书籍或资源,以更好地理解这个结构。我不明白这些是否是OS书籍、编程语言或架构书籍中通常解释的概念。
在我提出问题之前,我将根据有关堆栈/堆的阅读结果列出我的发现。
堆:
- 包含所有实例变量、动态分配(new/malloc)和全局变量。 - 不再使用堆数据结构,而使用更复杂的结构。 - 通过内存位置访问,各个进程负责其分配的内存。 - 碎片整理和分配由操作系统执行(请回答谁管理堆,操作系统还是运行时环境)。 - 在进程中共享,可以被拥有其引用的所有线程访问。
栈:
- 仅包含所有局部变量(在函数调用时推入)。 - 使用实际的堆栈数据结构进行操作。 - 由于连续性更快访问。
现在,以下是一些关于此主题的问题。
  1. 全局变量是在哪里分配的?我认为它们分配在堆上。如果是这样,那么它们是在运行时还是编译时分配的?另外一个问题是,这块内存是否可以被清除(使用delete)?
  2. 堆的结构是什么样的?堆是如何组织的(由操作系统管理还是由C/C++编译器设置的运行时环境管理)?
  3. 栈只保存方法及其本地变量吗?
  4. 每个应用程序(进程)都拥有单独的堆,但如果超过堆分配限制,这意味着操作系统无法分配更多内存吗?(我假设缺乏内存会导致操作系统重新分配来避免碎片化)
  5. 堆可以从进程内所有线程中访问(我认为是这样的)。是的,所有线程都可以访问实例变量、动态分配的变量和全局变量(如果它们引用它)。
  6. 不同的进程无法访问彼此的堆(即使传递了地址)。
  7. 栈溢出会崩溃
    • 仅当前线程
    • 当前进程
    • 所有进程
  8. 在C/C++中,块变量是否在函数运行时在堆栈上分配内存(例如,如果代码的子块(例如For循环)创建一个新变量,那么它是在运行时在堆栈上分配的还是预先分配的?) 什么时候删除它们(块级作用域,如何维护)?我认为所有对堆栈的添加都是在块开始之前在运行时进行的,当达到该块的结束点时,所有添加到那一点的元素都被弹出。
  9. CPU对栈寄存器的支持仅限于可以通过正常访问内存递增(pop)和递减(push)的堆栈指针。这是真的吗?
  10. 最后,堆栈结构由OS/Runtime环境生成,存在于主存储器中(作为抽象)吗?
2个回答

8
  1. 全局变量在编译时分配在静态内存段中。这些值在进入main函数之前进行初始化。当然,初始化可能会在堆上分配(例如,静态分配的std::string结构本身位于静态分配的内存中,但其包含的字符串数据在启动期间在堆上分配)。这些内容在程序正常关闭时被删除。在此之前无法释放它们,如果您希望释放它们,可以将该值包装在指针中,并在程序启动时初始化该指针。

  2. 堆由一个分配器库管理。其中一个是与C运行时一起提供的,但也有自定义的分配器,如tcmallocjemalloc,可用于替代标准分配器。这些分配器使用系统调用从操作系统获取大型内存页,然后在调用malloc时为您提供这些页面的部分。堆的组织方式有些复杂,因为不同的分配器之间存在差异,您可以在它们的网站上查找它们的工作方式。

  3. 是的,有点类似。虽然您可以使用库函数(如alloca)在堆栈上创建一块空间,并将其用于任何目的。

  4. 每个进程都有单独的内存空间,即它认为自己是孤立的,不存在其他进程。通常,操作系统会在请求更多内存时提供更多内存,但它也可以强制执行限制(例如Linux上的ulimit),此时它可能拒绝为您提供更多内存。对于操作系统来说,内存碎片不是问题,因为它以的形式提供内存。但是,进程中的碎片可能会导致您的分配器请求更多的页面,即使存在空闲空间。

  5. 是的。

  6. 是的,但通常有特定于操作系统的方法来创建多个进程可以访问的共享内存区域。

  7. 堆栈溢出本身不会导致崩溃,它会导致在可能保存其他值的位置写入内存值,从而破坏它。在破坏的内存上进行操作会导致崩溃。当进程访问未映射内存(参见下面的注释)时,它会崩溃,不仅是线程,而是整个进程。它不会影响其他进程,因为它们的内存空间是隔离的。(这在旧的操作系统如Windows 95中不成立,因为所有进程共享相同的内存空间)。

  8. 在C++中,堆栈分配的对象在进入块时创建,在退出块时销毁。实际的堆栈空间可能被分配得不太精确,但构造和析构将在那些特定点发生。

  9. x86进程的堆栈指针可以任意操纵。编译器通常会生成代码,仅添加堆栈指针的空间量,然后设置堆栈上的值的内存,而不是进行一堆推操作。

  10. 了解内存组织的概述可能会很有帮助:

    • 您拥有物理内存,内核可以看到。
    • 当进程请求时,内核将物理内存页面映射到虚拟内存页面。
    • 一个进程在其自己的虚拟内存空间中操作,不知道系统上的其他进程。
    • 当进程启动时,它将可执行文件的部分内容(代码、全局变量等)放入其中一些虚拟内存页面中。
    • 分配器从进程中请求页面,以满足 malloc 调用,这些内存构成堆。
    • 当线程启动时(或进程的初始线程),它向操作系统请求形成栈的几个页面。(您也可以向堆分配器请求,并将其给出的空间用作栈。)
    • 程序运行时,可以自由访问其地址空间中的所有内存,包括堆、栈等。
    • 当您尝试访问未映射的内存空间时,程序会崩溃。(更具体地说,您会收到来自操作系统的信号,您可以选择处理该信号。)
    • 由于堆栈溢出 tend to 导致程序访问此类未映射的区域,因此堆栈溢出 tend to 导致程序崩溃。

2
  1. 全局变量的分配实际上取决于系统。有些系统会将它们静态地放在二进制文件中,有些会在堆上分配它们,有些会在栈上分配它们。如果全局变量是一个指针,你可以delete它所指向的值,但没有其他方法来清除那个内存。当应用程序退出时,全局变量的析构函数将自动调用(好吧,也许不会用SIGTERM)。
  2. 我不确定,但我想它是由操作系统管理的,具体来说是内核。
  3. 是的,只有到一定程度。例如,你不能做无限递归,因为值会(别介意这个双关语)堆积起来。你最终会遇到一个,等待它,stack overflow(啊,它出现了,他说了!)。
  4. 一些操作系统可能通过单独的进程强制限制堆大小,但通常如果你无法分配内存,那是因为没有剩余内存。
  5. 所有线程共享一个公共堆,因此是的,它们都可以访问全局变量、动态分配的内存等。
  6. 通常正确,尽管在一些非常基本的架构上可能不是这样。在大多数情况下,操作系统在虚拟表的上下文中执行进程,因此你正在使用的指针值实际上指向一个不同的内存地址,而不是它们看起来应该指向的地址。
  7. 如果你所说的进程是指操作系统级别的进程,那么就是当前进程。
  8. 我假设这是正确的,但我自己不知道。
  9. 这个超出了我的能力范围。
  10. 是的,有点像。正如我之前提到的,大多数操作系统使用虚拟表将进程指针映射到主内存。此外,请考虑页面交换到磁盘(交换)。

Yiding的回答更加详尽和准确,但无论如何我很乐意帮助。 - Soup d'Campbells

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