【问题标题】:UNIX: What should be Stack Size (ulimit -s) in UNIX? [closed]UNIX:UNIX 中的堆栈大小 (ulimit -s) 应该是多少? [关闭]
【发布时间】:2013-08-31 06:56:51
【问题描述】:

如何计算我的程序在 UNIX 中所需的最小堆栈大小,以使我的程序永远不会崩溃。

假设我的程序是

int main()
{
         int number;
         number++;
         return 0;
}

1) 运行此程序所需的堆栈大小是多少?是怎么计算的?

2) 我的 Unix 系统给出了ulimit -s 512000。我的小程序真的需要这个值512MB吗?

3) 如果我有一个包含多线程、大约 500 个函数(包括一些库、宏、动态分配的内存等)的大型程序怎么办。为此需要多少堆栈大小?

【问题讨论】:

  • 由于你的程序不做任何事情,它真的根本不需要任何堆栈...
  • @CodeCodeCode 您没有在硬盘中创建整数大小的内存,堆栈在 RAM 中而不是在硬盘上。是的,你的程序什么也没做,因为编译器已经优化了它。使用 godbolt 查看代码的 ASM 翻译。
  • @KerrekSB 必须调用函数main,并且必须在堆栈上为number 分配空间。在实践中,甚至在输入main 之前使用数百字节的堆栈并不罕见。 (我不知道目前的做法,但我过去使用过在调用main 之前将整个环境放在堆栈上的系统。)
  • @CodeCodeCode 这是permalink
  • 这是一个有效的问题。有时了解使用了多少堆栈空间很重要,例如在为堆栈有限的内核编写代码或编写复杂的递归算法时。编译器应该通过提供在例程或块中使用的量来帮助计算堆栈空间(没有 VLA 等),并且链接器应该提供非递归调用树的总数。

标签: c++ c multithreading unix


【解决方案1】:
  1. 您的程序本身使用了几个字节 - 1 个 int,但当然还有在 main 之前出现的运行时部分需要考虑在内。但它不可能超过几十个字节,可能一次有几百个字节。由于任何现代操作系统中的最小堆栈大小是“一页”= 4KB,这应该很容易适应。
  2. 51200 = 51.2MB,但这似乎相当高。在我的 Linux Fedora 16 x86-64 机器上,它是 8192。
  3. 线程并不重要,因为每个线程都有自己的堆栈。函数的数量本身并不是堆栈使用的巨大贡献者。用完堆栈几乎总是由大型局部变量和/或深度递归引起的。对于任何有点复杂的程序,计算精确的堆栈使用量可能非常棘手。通常,它涉及大量运行程序并查看堆栈是否“爆炸”。如果没有,你有足够的筹码。一般来说,库函数往往不会使用大量的堆栈,但总会有例外。

举例:

void func()
{
   int x, y, z;
   float w;
   ...
}

此函数占用大约 16 字节的堆栈,加上调用函数的一般开销,通常为 1-3 个“机器字”(32 位机器上为 4-12 字节,64 位机器上为 8-24 字节)位机)。

void func2()
{
    int x[10000];
    ...
}

此函数将占用 40000 字节的堆栈空间。显然,您不需要对该函数进行多次递归调用即可耗尽堆栈。

【讨论】:

  • OP的问题是,在如此庞大的默认堆栈下,他/她的32位虚拟内存空间在创建很少的线程后就耗尽了。
  • @MartinJames - 每个线程真的预先分配了最大堆栈大小吗?不知道具体的操作系统很难说。 LINUX NPTL 确实使用它作为新线程堆栈的默认大小(如果它不是 unlimited),但即使这样也不是全部错误。
  • @Useless:正确,堆栈没有预先分配。但是,如果您使用虚拟空间的每个线程 50+MB,您将很快用完用户代码可以使用的 2-3GB(请记住,还有其他“块”可以放入其中,例如共享库,应用程序代码本身等)
  • 哎呀,阅读理解失败。谢谢。
【解决方案2】:

没有什么神奇的方法可以判断您的程序在堆栈上需要多少空间。这取决于代码实际上在做什么。即使程序似乎没有做任何事情,无限(或非常深)递归也会导致堆栈溢出。

作为示例,请参见以下内容:

$ ulimit
unlimited
$ echo "foo(){foo();} main(){foo();}" | gcc -x c -
$ ./a.out 
Segmentation fault (core dumped)

【讨论】:

  • 从技术上讲,这可能会崩溃,因为在你的代码中调用 main 是 UB。
  • 修改了上面的代码,避免调用UB。
  • 调用 main 是 C++ 中的 UB。它在 C 中定义明确。
  • @nouney 尾递归可能会或可能不会被优化掉,具体取决于编译器和编译器选项。
  • @MatsPetersson 没有未定义的行为,即使在原始版本中也是如此,因为他将其编译为 C(而不是 C++)。在 C 中,您可以递归调用 main
【解决方案3】:

大多数人依赖于堆栈“大”并且他们的程序没有使用所有堆栈,这仅仅是因为大小已设置得如此之大以至于程序很少会因为堆栈空间不足而失败,除非他们使用非常大的数组自动存储期限。

这是一个工程故障,从某种意义上说它不是工程:一个已知且很大程度上可预防的完全故障源是不受控制的。

一般来说,计算程序的实际堆栈需求可能很困难。特别是当存在递归时,编译器通常无法预测一个例程将被递归调用多少次,因此它无法知道该例程需要多少次堆栈空间。另一个复杂之处是调用在运行时准备的地址,例如调用虚函数或通过其他指向函数的指针。

但是,编译器和链接器可以提供一些帮助。对于使用固定数量的堆栈空间的任何例程,编译器理论上可以提供该信息。例程可能包括执行或未执行的块,并且每个块可能具有不同的堆栈空间要求。这会干扰为例程提供固定编号的编译器,但编译器可能会单独提供有关每个块的信息和/或例程的最大值。

理论上,链接器可以检查调用树,如果它是静态的且不是递归的,则可以为链接程序提供最大的堆栈使用量。他们还可以提供沿特定调用子链的堆栈使用(例如,从一个例程到导致递归调用同一例程的调用链),以便人类可以将算法知识应用于多个堆栈使用子链被递归调用的最大次数)。

我还没有看到具有这些功能的编译器或链接器。这表明开发这些功能几乎没有经济动机。

有时堆栈使用信息很重要。操作系统内核可能有一个比用户进程更有限的堆栈,因此应该计算内核代码的最大堆栈使用量(作为一种良好的工程实践),以便可以适当地设置堆栈大小(或重新设计代码使用更少的堆栈)。

如果您迫切需要计算堆栈空间要求,您可以检查编译器生成的汇编代码。在许多计算平台上的许多例程中,在例程开始时从堆栈指针中减去一个固定数字。在没有额外的减法或“推”指令的情况下,这是例程的堆栈使用,不包括它调用的子例程使用的进一步堆栈。但是,例程可能包含包含额外堆栈分配的代码块,因此您必须小心检查生成的汇编代码以确保您已找到所有堆栈调整。

例程也可能包含在运行时计算的堆栈分配。在计算堆栈空间至关重要的情况下,您可能会避免编写导致此类分配的代码(例如,避免使用 C 的可变长度数组功能)。

一旦您确定了每个例程的堆栈使用,您可以通过沿各种例程调用路径添加每个例程的堆栈使用(包括之前运行的启动例程的堆栈使用)来确定程序的总堆栈使用main 被调用)。

这种对完整程序的堆栈使用的计算通常很困难,很少执行。

您通常可以通过了解程序“需要”多少数据来完成其工作来估计程序的堆栈使用情况。每个例程通常都需要堆栈空间来存储它使用的对象,并具有自动存储持续时间以及一些用于保存处理器寄存器、将参数传递给子例程、一些临时工作等的开销。许多事情可以改变堆栈的使用,因此只能通过这种方式获得估计。例如,您的示例程序不需要任何空间用于number。由于不会打印声明或使用 number 的结果,因此编译器中的优化器可以消除它。您的程序只需要用于启动例程的堆栈空间; main 例程除了返回零之外不需要做任何事情。

【讨论】:

  • 感谢 Eric 的详细解释。这对我很有帮助。
  • 这是一个工程故障,在某种意义上它不是工程:一个已知且在很大程度上可预防的完全故障源不受控制。 不是工程故障:一个已知且在具有由有能力的人员编程的 MMU 的系统上,完全故障的来源在很大程度上是可预防的,发生的可能性约为 0。我只在非 MMU 系统(包括 DOS)和 Java 程序(但典型的 Java 程序员完全是另一个问题)上看到堆栈空间不足的程序。
  • @ninjalj:(1) 声称产品故障很少见并不能证明没有执行适当的工程。 (2) 你将你的反例限制在由有能力的人编程的 MMU 系统上。这个答案不限于那个子集;它通常解决编程问题。 (2) 您对堆栈空间不足的程序缺乏观察并不是其发生的有力证据。 (4) 我的回答给出了堆栈限制是一个重要因素(例如内核编程)的真实示例。
猜你喜欢
  • 2015-12-31
  • 2019-10-20
  • 2014-04-19
  • 2014-01-11
  • 1970-01-01
  • 2015-01-02
  • 1970-01-01
  • 1970-01-01
  • 2012-12-09
相关资源
最近更新 更多