Linux克隆调用的最小堆栈大小?

Posted

技术标签:

【中文标题】Linux克隆调用的最小堆栈大小?【英文标题】:Minimal stack size for Linux clone call? 【发布时间】:2016-12-20 17:57:03 【问题描述】:

我一直在摆弄clone 调用,我注意到不同子线程堆栈分配的三种不同结果。以下演示分配了一个 n-bytes 大的堆栈,其中 n 作为参数传递,然后尝试克隆。

foo.c

#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <errno.h>

int child(void *arg)

    (void)arg;
    write(STDOUT_FILENO, "carpe momentum\n", 15);
    return 0;


int main(int argc, char **argv)

    long stacksize;
    pid_t pid;
    void *stack;

    if (argc < 2)
        return 1;

    errno = 0;
    stacksize = strtol(argv[1], NULL, 0);
    if (errno != 0)
        return 1;

    stack = malloc(stacksize);
    if (stack == NULL)
        return 1;

    pid = clone(child, stack + stacksize, 0, NULL);
    if (pid == -1)
        return 1;

    write(STDOUT_FILENO, "success\n", 8);

    return 0;

以下是我的观察:

$ cc -o foo foo.c
$ ./foo 0
Segmentation fault
$ ./foo 23
Segmentation fault
$ ./foo 24
success
$ ./foo 583
success
$ ./foo 584
success
carpe momentum
$ ./foo 1048576 #1024 * 1024, amount suggested by man-page example
success
carpe momentum

0 到 23 之间的所有样本都有段错误,对于 24 到 583 之间的所有样本,父级成功但子级保持沉默。任何高于 584 的合理因素都会导致两者都成功。

反汇编表明child 只使用了 16 个字节的堆栈空间,再加上至少 16 个字节来调用write。但这已经超过了停止段错误所需​​的 24 个字节。

$ objdump -d foo
# ...
080484cb <child>:
 80484cb:       55                      push   %ebp
 80484cc:       89 e5                   mov    %esp,%ebp
 80484ce:       83 ec 08                sub    $0x8,%esp
 80484d1:       83 ec 04                sub    $0x4,%esp
 80484d4:       6a 0f                   push   $0xf
 80484d6:       68 50 86 04 08          push   $0x8048650
 80484db:       6a 01                   push   $0x1
 80484dd:       e8 be fe ff ff          call   80483a0 <write@plt>
 80484e2:       83 c4 10                add    $0x10,%esp
 80484e5:       b8 00 00 00 00          mov    $0x0,%eax
 80484ea:       c9                      leave  
 80484eb:       c3                      ret
# ...

这会提示几个重叠的问题。

为什么 clone 不会在 24 和 583 字节的堆栈之间出现段错误? child 如何在堆栈太少的情况下静默失败? 所有堆栈空间都用来做什么? 24 和 584 字节的意义是什么?它们在不同的系统和实施中有何不同? 我可以计算最低堆栈要求吗?我应该吗?

我在 i686 Debian 系统上:

$ uname -a
Linux REDACTED 3.16.0-4-686-pae #1 SMP Debian 3.16.7-ckt25-2+deb8u3 (2016-07-02) i686 GNU/Linux

【问题讨论】:

这是一个有趣的观察,但我想到了一堆问题:什么操作系统? CPU架构?编译器? 32 位还是 64 位?您是否尝试过平台上的细微差别?根本差异?有趣的事实:30 年前,我在 Sun Microsystems C 编译器中发现了一个 100% 可重现的错误,他们不相信我。我说,“与错误信息交谈。” @PeterRowell 添加了uname 将其输出到问题中;谢谢。我没有在任何其他系统上尝试过。 【参考方案1】:
为什么不克隆 24 到 583 字节堆栈之间的段错误?

确实如此,但因为它是一个单独的过程,所以您看不到它。在 24 岁之前,段错误不是孩子,而是试图设置孩子的父母。尝试使用 strace -ff 来查看这种情况。

如果堆栈太少,孩子如何静默失败?

当孩子去世时,会通知父母。在这种情况下,父级(执行clone() 调用的父级)对此通知不做任何事情。它在 24 岁以下不是“沉默”的原因是因为那是父母去世的时候,在这种情况下,你的 shell 会收到通知。

所有堆栈空间都用来做什么? 24 和 584 字节的意义是什么?它们在不同的系统和实施中有何不同?

前 24 个(还有一点)用于设置对child 的函数调用。因为它是一个普通函数,完成后它会返回调用函数。这意味着clone 必须设置一个调用函数来返回(一个只是干净地终止子进程的函数)。

584(还有一点)显然是调用函数、你的函数、write 和任何write 调用的局部变量所需的内存量。

我写“(和一点)”的原因是因为stack 之前可能有一点内存可用,并且在空间用完时被clonechild 滥用。尝试在克隆后添加free(stack) 以查看滥用的结果。

我可以计算最低堆栈要求吗?我应该吗?

一般而言,您可能不应该这样做。它需要对你的函数和那些使用的外部函数进行相当深入的分析。就像“普通”程序一样,我建议使用默认值(如果我没记错的话,Linux 上是 8MB)。只有当你有严格的内存要求(或堆栈溢出问题)时,你才应该开始担心这些事情。

【讨论】:

child 在对write 的调用中使用了大部分 584 字节吗?另外,为什么设置 child 需要 24 个字节而不是 16 个字节? @nebuch 可能;正如您所展示的,孩子本身并没有使用太多。尽管“调用函数”也可以进行一些额外的调用,这些调用会导致这种用法。必须进行更多调试才能准确找出答案。 假设我确实有严格的内存要求,有没有办法从调用树或类似的东西计算最大堆栈深度? @nebuch 看起来这里已经回答了:***.com/a/6390984/1306666 8mb 仅适用于 glibc,musl 在 Linux 上使用的默认堆栈大小要小得多,为 80kb。见git.musl-libc.org/cgit/musl/tree/src/internal/…

以上是关于Linux克隆调用的最小堆栈大小?的主要内容,如果未能解决你的问题,请参考以下文章

数据结构之堆栈

数据结构&算法_堆栈(堆栈)队列链表

-Xms:初始堆大小或最小堆大小?

Android 版本是不是有最小堆大小?

为啥这个递归函数超过调用堆栈大小?

线程堆栈大小 pthread_attr_setstacksize 的使用