我正在阅读这个关于内存过度提交的页面,它提到
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当程序被编写(例如gcc编写的)时,就定义了堆栈的大小限制(我记得有一个gcc参数来调整它)。然后,在程序内部,我们可以继续在堆栈上分配。
很少有问题:
mremap()吗?如果在编译时定义了堆栈的大小限制,为什么?mmap堆栈?发布于 2021-10-22 12:59:05
这里的“魔力”是MAP_GROWSDOWN标志(由Linux内核实现)在进程通过mmap()系统调用从内核请求新内存时的行为,并且它通常用于初始堆栈(进程中第一个线程的堆栈,当它第一次执行时)。
因此,虽然新进程通常在默认情况下得到一个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映射转换为更大的、固定大小的映射;通常,将其转换为堆栈,)报告的大小。因为这实际上分配了地址空间,但是直到第一次访问时才用实际的RAM填充页面,所以主要的成本是内核元数据(页面表等等)。
您的编译器提供的C运行时可能已经这样做了(按照堆栈,)报告的大小),作为为C设置运行时环境的一部分(例如,在crt*.o或libgcc*中)。我还没查过。
例如,如果您想要创建一个新线程,可以使用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()创建一个新上下文(就像一个新线程一样),为它设置一个新堆栈,切换到新上下文,然后释放旧堆栈。
在许多情况下,通过调用Sigalt堆栈()将信号处理程序设置为使用单独的堆栈。这是非常有用的,因为随后由于堆栈溢出而产生的信号仍然可以被处理。
最后,回顾一下在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/map和其他/proc伪文件的更多信息--它们不是任何存储设备上存在的文件;它们是在访问时由内核生成的,并且是这类文件非常有效的接口--参见man 5 proc。
https://stackoverflow.com/questions/69671164
复制相似问题