3

为什么不alloca检查它是否可以分配内存?

来自man 3 alloca

如果分配导致堆栈溢出,则程序行为未定义。… 如果堆栈帧无法扩展,则没有错误指示。

为什么alloca不/不能检查它是否可以分配更多内存?

我理解它alloca在堆栈上分配内存时在堆栈上分配内存的方式(s)brk。来自https://en.wikipedia.org/wiki/Data_segment#Heap

堆区由 malloc、calloc、realloc 和 free 管理,可以使用 brk 和 sbrk 系统调用来调整其大小

来自man 3 alloca

alloca() 函数在调用者的堆栈帧中分配 size 个字节的空间。

并且堆栈和堆正朝着收敛的方向增长,如下图所示:

在此处输入图像描述

(上图来自Dougct 在CC BY-SA 3.0下发布的Wikimedia Commons

现在两者都alloca返回(s)brk一个指向新分配内存开头的指针,这意味着它们都必须知道当前堆栈/堆在哪里结束。确实,来自man 2 sbrk

以 0 为增量调用 sbrk() 可用于查找程序中断的当前位置。

因此,按照我的理解,检查是否alloca可以分配所需的内存基本上归结为检查堆栈的当前端和堆的当前端之间是否有足够的空间。如果在堆栈上分配所需的内存会使堆栈到达堆,则分配失败;否则,它会成功。

那么,为什么不能使用这样的代码来检查是否alloca可以分配内存呢?

void *safe_alloca(size_t size)
{
    if(alloca(0) - sbrk(0) < size) {
        errno = ENOMEM;
        return (void *)-1;
    } else {
        return alloca(size);
    }
}

这对我来说更令人困惑,因为显然(s)brk可以进行此类检查。来自man 2 sbrk

brk() 将数据段的末尾设置为 addr 指定的值,当该值合理时,系统有足够的内存,并且进程不超过其最大数据大小(参见 setrlimit(2))。

所以如果(s)brk可以做这样的检查,那为什么不能alloca呢?

4

2 回答 2

4

alloca是一个非标准的编译器内在特性,它的卖点是它可以编译成非常轻量级的代码,甚至可能是一条指令。它基本上使用局部变量在每个函数的开头执行操作 - 将堆栈指针寄存器移动指定的数量并返回新值。不像sbrk,alloca完全在用户空间中,无法知道还有多少可用堆栈。

堆栈向堆增长的图像是学习内存管理基础知识的有用心理模型,但在现代系统上并不准确:

  • 正如 cmaster 在他的回答中解释的那样,堆栈大小将主要受到内核强制限制的限制,而不是真正与堆碰撞的堆栈,尤其是在 64 位系统上。
  • 在多线程进程中,栈不是一个栈,而是每个线程一个栈,显然它们不可能都向堆增长。
  • 堆本身不是连续的。现代malloc实现使用多个领域来提高并发性能,并将大量分配卸载到匿名mmap,确保free 将它们返回给操作系统。后者也在传统描述的单一竞技场“堆”之外。

可以想象一个版本alloca从操作系统查询此信息并返回正确的错误条件,但随后它的性能优势将丢失,甚至很可能与malloc(仅偶尔需要转到操作系统以获取更多内存)相比对于进程,通常在用户空间工作)。

于 2017-10-14T11:28:05.720 回答
3

图片有点过时了:在现代系统上,堆内存区域和包含调用堆栈的内存区域是完全独立的实体,在 64 位系统上它们相距甚远。内存中断的概念是为具有小地址空间的系统设计的。

因此,堆栈空间的限制不是它可以增长到堆的顶部,而是内核可能没有任何内存来支持它。或者内核可能认为您的堆栈增长过多(达到了某个限制),从而终止了您的进程。

您的进程仅通过递减堆栈指针并在其中存储一些数据来增加堆栈。如果该内存访问当前未映射到您的地址空间,则硬件会立即将此情况发送给操作系统内核,操作系统内核会检查发生失败的内存访问的位置,如果该内存访问位于堆栈内存区域下方,它将立即扩展该内存映射,在那里映射一个新的内存页面,并将控制权交还给您的进程以重试其内存访问。该过程没有看到任何这些。它只是看到它对堆栈内存的访问成功。

alloca()不会以任何方式偏离这一点:您要求它在堆栈上分配一些内存,并且它通过相应地递减堆栈指针来做到这一点。但是,如果您的分配如此之大,以至于操作系统不会将对其的内存访问视为有效的堆栈内存访问,它会(很可能并且希望)使用 SEGFAULT 终止您的进程。然而,由于行为是未定义的,任何事情都可能发生。

于 2017-10-14T11:17:09.783 回答