这里的“魔力”是当进程通过mmap()系统调用向内核请求新内存时MAP_GROWSDOWN标志(由Linux内核实现)的行为,并且它通常用于初始堆栈(进程中第一个线程的堆栈,当它第一次执行时)。
因此,虽然默认情况下新进程通常会获得MAP_GROWSDOWN 堆栈,但进程也可以管理自己的堆栈。如果进程创建新线程,它必须为它们创建堆栈。 (目前,pthread_create() 创建一个固定大小的堆栈(默认最大大小,或按 pthread_attr_t 属性块中指定的大小(如果指定),而不是 MAP_GROWSDOWN 堆栈。)
Linux 内核实现MAP_GROWSDOWN 内存映射的方式是实际内存前面有一个额外的页面,称为“保护页面”。 (在 x86-64 上,页面是 4096 字节的对齐单元,但存在其他页面大小;在运行时,使用sysconf(_SC_PAGESIZE) 获取以字节为单位的大小。)
每当第一次访问保护页面时,内核都会将其转换为标准页面(与同一映射中的其他页面相同),并在其下方(在下一个较小的页面地址处)创建一个新的保护页面。如果在这些虚拟地址上已经映射了某些内容,则映射不会更改,并且该进程将收到 SIGSEGV(段违规错误)。因此,只有可用地址空间的数量(以及间接可用的内存)限制了此类堆栈的增长。
这也意味着如果依赖MAP_GROWSDOWN 自动堆栈增长,使用大于页面大小的本地数组可能会导致 SIGSEGV。因此,在 C 中使用动态内存管理——malloc()/realloc()/free(),以及 getline() 和 asprintf() 等接口——比依赖大型堆栈上固定大小的数组更可靠.
本质上,只要堆栈元素的大小最多为一页,这样的堆栈就会根据需要自动增长。
“隐式重映射”因此只适用于初始线程,因为它使用了一个使用MAP_GROWSDOWN 标志的堆栈;并且隐式重映射本身以页面大小为单位引用此自动增长工具。
如果您的进程针对不同类型的分配执行许多单独的mmap() 调用,例如将文件映射到内存等,则它们的位置可能会使得MAP_GROWSDOWN 映射的增长限制为小于过程期望什么。 (出于安全目的,内核给出的地址至少在某种程度上是随机的。)
建议将内核重新映射为可能需要的最大尺寸,这意味着可以——我不确定我是否同意“必须”——在他们程序的开头附近,使用mremap() 进行转换MAP_GROWSDOWN 映射到更大的固定大小的映射;通常,为getrlimit(RLIMIT_STACK,) 报告的大小。因为这实质上是分配地址空间,但在第一次访问之前不会使用实际 RAM 填充页面,因此主要成本是内核元数据(页表等)。
作为设置 C 运行时环境的一部分(例如,在 crt*.o 或 libgcc* 中),您的编译器提供的 C 运行时可能已经执行此操作(达到getrlimit(RLIMIT_STACK, ) 报告的大小) .我没查过。
如果想要,例如在创建一个新线程时,可以使用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() 的第二个参数。然后创建的线程将使用该堆栈。
修改当前使用的堆栈要复杂得多,最好在实际编译的 C 代码在进程中运行之前在 C 运行时(用机器代码,用汇编编写)中完成。在 C 中,它可以通过getcontext()/setcontext() 完成,通过创建一个新的上下文(就像它是一个新线程一样),为其设置一个新的堆栈,切换到新的上下文,然后释放旧的堆栈。
在许多情况下,信号处理程序通过调用sigaltstack() 设置为使用单独的堆栈。这是非常有用的,因为然后信号由于例如堆栈溢出仍然可以处理。
最后,回想一下,在 Linux 中,/proc/PID/maps 描述了进程 PID 的所有现有映射。对于流程本身,您始终可以使用/proc/self/maps。在试验这些东西时,您可能会发现以下 dump_maps() 函数很有用:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
/* Returns 0 if memory mappings printed to standard output,
an errno error code if an error occurs.
*/
int dump_maps(void)
{
FILE *in;
int ch;
in = fopen("/proc/self/maps", "r");
if (!in) {
const int saved_errno = errno;
fprintf(stderr, "Cannot open /proc/self/maps: %s.\n", strerror(saved_errno));
return errno = saved_errno;
}
printf(" MinAddress-MaxAddress Perms Offset Device Inode Pathname-or-Description\n");
/* Yes, this is the slowest possible way to copy a file to standard output,
but it should not matter for this use case. The KISS principle. */
while ((ch = getc(in)) != EOF)
putchar(ch);
putchar('\n');
fclose(in);
return 0;
}
int main(void)
{
dump_maps();
return EXIT_SUCCESS;
}
有关 /proc/self/maps 和其他 /proc 伪文件的更多信息——它们不是任何存储设备上存在的文件;它们是由内核在访问时生成的,对于这类东西来说是一个非常有效的接口——参见man 5 proc。