堆栈增长和mmap堆栈?

HCS*_*CSF 1 c linux kernel

我正在阅读有关内存过度使用的页面,其中提到

The C language stack growth does an implicit mremap. If you want absolute
guarantees and run close to the edge you MUST mmap your stack for the 
largest size you think you will need. For typical stack usage this does
not matter much but it's a corner case if you really really care
Run Code Online (Sandbox Code Playgroud)

当程序编译时(例如通过gcc),定义了堆栈的大小限制(我记得有一个gcc参数可以调整它)。然后,在程序内部,我们可以继续在堆栈上分配。

几个问题:

  1. 在这种情况下,“堆栈增长”意味着什么?这是否意味着如果 C 程序不断在堆栈上分配/释放,有时mremap()会在幕后调用?为什么如果在编译时就定义了堆栈的大小限制呢?
  2. 我们怎样才能mmap堆栈呢?

小智 5

这里的“魔法”是当进程通过mmap()MAP_GROWSDOWN系统调用从内核请求新内存时 flag 的行为(由 Linux 内核实现),并且它通常用于初始堆栈(第一个线程的堆栈)在一个进程中,当它第一次执行时)。

\n

因此,虽然新进程通常会MAP_GROWSDOWN默认获得堆栈,但进程也可以管理自己的堆栈。如果进程创建新线程,则必须为它们创建堆栈。(目前,pthread_create()创建一个固定大小的堆栈(具有默认的最大大小,或者根据 pthread_attr_t 属性块中的指定大小(如果指定)),而不是堆栈MAP_GROWSDOWN。)

\n

Linux 内核实现内存映射的方式MAP_GROWSDOWN是在实际内存之前有一个额外的页面,称为“保护页面”。(在 x86-64 上,页面以 4096 字节为对齐单位,但也存在其他页面大小;在运行时,使用sysconf(_SC_PAGESIZE)来获取以字节为单位的大小。)

\n

每当首次访问保护页时,内核都会将其转换为标准页(与同一映射中的其他页相同),并在下面(在下一个较小的页地址处)创建一个新的保护页。如果这些虚拟地址已经映射了某些内容,则映射不会更改,并且进程将收到 SIGSEGV(段违规错误)。因此,只有可用地址空间的数量(以及间接可用的内存)限制了此类堆栈的增长。

\n

这也意味着,如果依赖于自动堆栈增长,则使用大于页面大小的本地数组可能会导致 SIGSEGV MAP_GROWSDOWN。因此,在 C \xe2\x80\x93\xe2\x80\x93 malloc()/realloc()/free() 中使用动态内存管理以及像 getline() 和 asprintf() 这样的接口 \xe2\ x80\x93\xe2\x80\x93 比依赖大型堆栈上固定大小数组。

\n

本质上,只要堆栈元素最多为一页大小,此类堆栈就会根据需要自动增长。

\n

因此,“隐式重映射”仅适用于初始线程,因为它使用使用该标志的堆栈MAP_GROWSDOWN;隐式重映射本身引用了这种以页面大小为单位的自动增长功能。

\n

如果您的进程mmap()对不同类型的分配进行多次单独的调用,例如将文件映射到内存等,则它们的位置可能会导致映射的增长MAP_GROWSDOWN限制为小于进程的预期。(出于安全目的,内核给出的地址至少在某种程度上是随机的。)

\n

将内核重新映射为可能需要的最大大小的建议意味着可以 \xe2\x80\x93\xe2\x80\x93 我不确定我是否同意“必须”\xe2\x80\x93\xe2 \x80\x93,在程序的开头附近,使用mremap()将映射转换MAP_GROWSDOWN为更大的固定大小的映射;通常,达到getrlimit(RLIMIT_STACK,)报告的大小。因为这本质上是分配地址空间,但在第一次访问之前不会用实际 RAM 填充页面,所以主要成本是内核元数据(页表等)。

\n

作为设置 C 运行时环境(例如在 crt*.o 或 libgcc* 中)的一部分,编译器提供的 C 运行时可能已经执行此操作(达到getrlimit(RLIMIT_STACK, )报告的大小)。我没查过。

\n

如果有人愿意,例如在创建一个新线程时,可以使用mmap()(例如,mmap((void *)0, size_in_bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK | MAP_GROWSDOWN, -1, 0))来分配想要的任何堆栈,然后使用pthread_attr_init()来初始化线程属性集,使用 ,将堆栈的地址和大小放入该线程属性中pthread_attr_setstack(),并提供指向该线程属性集的指针作为第二个参数pthread_create()。创建的线程将使用该堆栈。

\n

修改当前使用的堆栈要困难得多,最好在进程中运行实际编译的 C 代码之前在 C 运行时(以机器代码,用汇编语言编写)中完成。在 C 中,可以通过getcontext()/setcontext()来完成,方法是创建一个新上下文(就好像它是一个新线程),为其设置一个新堆栈,切换到新上下文,然后释放旧上下文堆。

\n

在许多情况下,通过调用sigaltstack()将信号处理程序设置为使用单独的堆栈。这是非常有用的,因为这样由于例如堆栈溢出而导致的信号仍然可以被作用。

\n

最后,回想一下,在 Linux 中,/proc/PID/maps描述了进程 PID 的所有现有映射。对于流程本身,您始终可以使用/proc/self/maps. dump_maps()在尝试这些东西时,您可能会发现以下函数很有用:

\n
#include <stdlib.h>\n#include <string.h>\n#include <stdio.h>\n#include <errno.h>\n\n/* Returns 0 if memory mappings printed to standard output,\n   an errno error code if an error occurs.\n*/\nint dump_maps(void)\n{\n    FILE *in;\n    int   ch;\n\n    in = fopen("/proc/self/maps", "r");\n    if (!in) {\n        const int saved_errno = errno;\n        fprintf(stderr, "Cannot open /proc/self/maps: %s.\\n", strerror(saved_errno));\n        return errno = saved_errno;\n    }\n\n    printf("  MinAddress-MaxAddress  Perms Offset  Device   Inode                    Pathname-or-Description\\n");\n\n    /* Yes, this is the slowest possible way to copy a file to standard output,\n       but it should not matter for this use case.  The KISS principle. */\n    while ((ch = getc(in)) != EOF)\n        putchar(ch);\n\n    putchar(\'\\n\');\n\n    fclose(in);\n    return 0;\n}\n\nint main(void)\n{\n    dump_maps();\n\n    return EXIT_SUCCESS;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

有关 /proc/self/maps 和其他 /proc 伪文件 \xe2\x80\x93\xe2\x80\x93 的更多信息,它们不是存在于任何存储设备上的文件;它们是由内核在访问时生成的,并且是此类内容的非常有效的接口 \xe2\x80\x93\xe2\x80\x93,请参阅man 5 proc

\n