brk()系统调用了什么?

nik*_*nik 173 c unix linux memory-management brk

根据Linux程序员手册:

brk()和sbrk()改变程序中断的位置,它定义了进程数据段的结束.

这里的数据段意味着什么?是仅仅将数据段或数据,BSS和堆组合在一起?

根据维基:

有时,数据,BSS和堆区域统称为"数据段".

我认为没有理由改变数据段的大小.如果它是数据,BSS和堆集合那么它是有意义的,因为堆将获得更多的空间.

这让我想到了第二个问题.在我到目前为止阅读的所有文章中,作者都​​说堆积增长,堆栈向下增长.但是他们没有解释的是当堆占用堆和堆栈之间的所有空间时会发生什么?

在此输入图像描述

zwo*_*wol 223

在您发布的图表中,"break" - 由brkand 操纵的地址和sbrk- 是堆顶部的虚线.

虚拟内存布局的简化图像

您阅读的文档将此描述为"数据段"的结尾,因为在传统的(预共享库,预先mmap)Unix中,数据段与堆连续; 在程序启动之前,内核会将"文本"和"数据"块加载到RAM中,从地址0开始(实际上略高于地址0,因此NULL指针确实没有指向任何东西)并将中断地址设置为数据段的结尾.然后第一次调用mallocsbrk用于移动分解并在数据段的顶部和新的更高的中断地址之间创建堆,如图所示,并且随后的使用malloc将使用它来使堆更大有必要的.

同时,堆栈从内存顶部开始并逐渐减少.堆栈不需要显式系统调用来使其更大; 或者它开始时分配给它的RAM尽可能多(这是传统的方法),或者堆栈下面有一个保留地址区域,内核在注意到写入时会自动分配RAM (这是现代方法).无论哪种方式,地址空间底部可能存在或可能不存在可用于堆栈的"保护"区域.如果这个区域存在(所有现代系统都这样做),它将被永久取消映射; 如果任一堆栈或堆试种到它,你就会得到一个分段错误.但是,传统上,内核并没有试图强制执行边界; 堆栈可能会成长为堆,或者堆可能会成长为堆栈,无论哪种方式,他们都会乱写彼此的数据,程序会崩溃.如果你很幸运,它会立即崩溃.

我不确定这个图中512GB的数字来自哪里.它意味着一个64位的虚拟地址空间,这与你在那里非常简单的内存映射不一致.一个真正的64位地址空间看起来更像这样:

简化的地址空间

这不是远程扩展,它不应该被解释为任何给定操作系统的确切方式(在我绘制之后我发现Linux实际上使可执行文件比我想象的更接近零地址和共享库在令人惊讶的高地址).该图的黑色区域未映射 - 任何访问都会导致立即的段错误 - 并且它们相对于灰色区域是巨大的.浅灰色区域是程序及其共享库(可以有数十个共享库); 每个都有一个独立的文本和数据段(和"bss"段,它也包含全局数据,但初始化为所有位零,而不是占用磁盘上可执行文件或库中的空间).堆不再必然与可执行文件的数据段连续 - 我这样绘制了它,但看起来Linux至少不会这样做.堆栈不再与虚拟地址空间的顶部挂钩,堆与堆栈之间的距离非常大,您无需担心跨越它.

中断仍然是堆的上限.然而,我没有表现出的是,在黑色的某个地方可能会有数十个独立的内存分配,mmap而不是brk.(操作系统会尽量远离该brk区域,以免它们发生碰撞.)

  • 这取决于具体的实现,但是IIUC当前很多`malloc使用`brk`区域进行小分配,而个人`mmap用于大型(比如说>> 128K)分配.例如,参见Linux`masteroc(3)`联机帮助页中对MMAP_THRESHOLD的讨论. (16认同)
  • +1以获得详细说明.你知道`malloc`是否仍然依赖于`brk`或者是否使用`mmap`能够"回馈"单独的内存块? (6认同)
  • @Nikhil:这很复杂.*大多数*32位系统将堆栈放在*用户模式*地址空间的最顶端,这通常只是整个地址空间的较低2或3G(剩余空间是为内核保留的).我现在不能想到一个没有但我不知道的人.大多数64位CPU实际上并不允许您使用整个64位空间; 地址的高10到16位必须为全零或全1.堆栈通常位于可用低地址的顶部附近.我不能给你一个`mmap`的规则; 它非常依赖于操作系统. (3认同)
  • @RiccardoBestetti它浪费了_address space_,但这是无害的 - 一个64位的虚拟地址空间是如此之大,以至于如果你每隔一秒钟烧掉一个千兆字节,它仍然会耗费500年才能耗尽.[\ [1 \]](https://people.cs.umass.edu/~mcorner/courses/691J/papers/VM/chase_sharing/chase_sharing.pdf)大多数处理器甚至不允许使用超过2 ^ 48到2 ^ 53位的虚拟地址(我知道的唯一例外是散列页表模式下的POWER4).它不会浪费物理RAM; 未使用的地址未分配给RAM. (3认同)
  • Linux 和其他现代操作系统以“令人惊讶的”低效方式布局不同内存段的原因之一是[地址空间布局随机化](https://en.wikipedia.org/wiki/Address_space_layout_randomization)。这是一项安全措施。如果代码和数据位于不可预测的地址,那么攻击者就很难升级缓冲区溢出以使程序执行任意代码。 (3认同)
  • 确实是很好的解释。但是正如您所说,堆栈不再位于虚拟地址空间的顶部。这是仅适用于 64 位地址空间还是适用于 32 位地址空间。如果堆栈位于地址空间的顶部,匿名内存映射会在哪里发生?它是否位于堆栈之前的虚拟地址空间的顶部。 (2认同)

Cir*_*四事件 22

最小的可运行示例

brk()系统调用做什么?

要求内核让您读取和写入称为堆的连续内存块.

如果你不问,它可能会让你陷入困境.

没有brk:

#define _GNU_SOURCE
#include <unistd.h>

int main(void) {
    /* Get the first address beyond the end of the heap. */
    void *b = sbrk(0);
    int *p = (int *)b;
    /* May segfault because it is outside of the heap. */
    *p = 1;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

brk:

#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>

int main(void) {
    void *b = sbrk(0);
    int *p = (int *)b;

    /* Move it 2 ints forward */
    brk(p + 2);

    /* Use the ints. */
    *p = 1;
    *(p + 1) = 2;
    assert(*p == 1);
    assert(*(p + 1) == 2);

    /* Deallocate back. */
    brk(b);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

即使没有brk,上面的内容也可能不会出现新页面而不是段落错误,所以这里是一个更积极的版本,分配16MiB并且很可能在没有以下情况下发生段错误brk:

#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>

int main(void) {
    void *b;
    char *p, *end;

    b = sbrk(0);
    p = (char *)b;
    end = p + 0x1000000;
    brk(end);
    while (p < end) {
        *(p++) = 1;
    }
    brk(b);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

在Ubuntu 18.04上测试过.

虚拟地址空间可视化

之前brk:

+------+ <-- Heap Start == Heap End
Run Code Online (Sandbox Code Playgroud)

之后brk(p + 2):

+------+ <-- Heap Start + 2 * sizof(int) == Heap End 
|      |
| You can now write your ints
| in this memory area.
|      |
+------+ <-- Heap Start
Run Code Online (Sandbox Code Playgroud)

之后brk(b):

+------+ <-- Heap Start == Heap End
Run Code Online (Sandbox Code Playgroud)

为了更好地理解地址空间,您应该熟悉分页:x86分页如何工作?

更多信息

brk以前是POSIX,但它在POSIX 2001中删除了,因此需要sbrk访问glibc包装器.

删除可能是由于引入brk,这是一个超集,允许分配多个范围和更多的分配选项.

在内部,内核决定进程是否可以拥有那么多内存,并为该用法指定内存页面.

sbrk并且brk是libc用于brk在POSIX系统中实现的常见底层机制.

这解释了堆栈与堆的比较:x86汇编中寄存器上使用的push/pop指令的功能是什么?

  • 既然`p`是一个指向`int`类型的指针,那么这不应该是`brk(p + 2);`? (4认同)

lus*_*oog 10

您可以使用brksbrk自己避免每个人总是在抱怨的"malloc开销".但是你不能轻易地使用这种方法,malloc因此它只适用于你没有free任何东西.因为你做不到.此外,您应该避免任何可能在malloc内部使用的库调用.IE浏览器.strlen可能是安全的,但fopen可能不是.

打电话sbrk就像你打电话一样malloc.它返回一个指向当前中断的指针,并将中断增加该数量.

void *myallocate(int n){
    return sbrk(n);
}
Run Code Online (Sandbox Code Playgroud)

虽然你不能释放单独的分配(因为没有malloc开销,请记住),你可以通过调用第一次调用返回的值释放整个空间,从而倒回brk.brksbrk

void *memorypool;
void initmemorypool(void){
    memorypool = sbrk(0);
}
void resetmemorypool(void){
    brk(memorypool);
}
Run Code Online (Sandbox Code Playgroud)

您甚至可以堆叠这些区域,通过将休息时间倒回到区域的开始来丢弃最近的区域.


还有一件事 ...

sbrk代码高尔夫中也很有用,因为它短于2个字符malloc.

  • -1因为:`malloc` /`free`肯定*可以*(并且做)将内存返回给操作系统.当你想要它们时,它们可能并不总是这样做,但这是因为你的用例未完全调整的启发式问题.更重要的是,*在任何可能调用`malloc`的程序中用非零参数*调用`sbrk`是不安全的 - 几乎所有C库函数都允许在内部调用`malloc`.唯一肯定不会是[async-signal-safe](http://pubs.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_04.html)的功能. (6认同)
  • 这太傻了.如果你想避免大量小分配的malloc开销,做一个大的分配(使用malloc或mmap,*不是*sbrk)并自己解决它.如果将二叉树的节点保存在数组中,则可以使用8b或16b索引而不是64b指针.在您准备删除*all*节点之前,如果不必删除任何节点,则此方法很有用.(例如,在运行中构建一个排序的字典.)使用`sbrk`对于code-golf只有**,因为除了源代码大小之外,手动使用`mmap(MAP_ANONYMOUS)`在各方面都更好. (3认同)

R..*_*R.. 5

有一个特殊指定的匿名私有内存映射(传统上位于 data/bss 之外,但现代 Linux 实际上会使用 ASLR 调整位置)。原则上,它并不比您可以使用 创建的任何其他映射更好mmap,但 Linux 有一些优化,可以向上扩展此映射的末尾(使用brk系统调用),同时相对于mmapmremap将产生的锁定成本降低。这使得malloc在实现主堆时使用它很有吸引力。