brk()系统调用做什么?


184

根据Linux程序员手册:

brk()和sbrk()更改程序中断的位置,该位置定义了进程数据段的结尾。

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

根据Wiki:

有时,数据,BSS和堆区域统称为“数据段”。

我没有理由仅更改数据段的大小。如果是数据,BSS和堆一起使用,则有意义的是堆将获得更多空间。

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

在此处输入图片说明


1
那么,当您的空间不足时该怎么办?您交换到硬盘。使用空间后,请释放它以获取其他信息。
Igoris Azanovas 2011年

28
@Igoris:您在混淆物理内存(可以使用虚拟内存根据需要将其交换到磁盘)和地址空间。当您填满地址空间时,任何交换都不会把中间的那些地址还给您。
Daniel Pryden 2011年

7
提醒一下,brk()在汇编语言中,系统调用比在C语言中更有用。在C语言中,malloc()应使用系统调用而不是将其brk()用于任何数据分配目的-但这不会以任何方式使所提出的问题无效。
alecov 2011年

2
@Brian:堆是一个复杂的数据结构,用于处理大小和对齐方式不同的区域,空闲池等。线程堆栈始终是完整页面的连续序列(在虚拟地址空间中)。在大多数操作系统中,堆栈,堆和内存映射文件都具有页面分配器。
Ben Voigt

2
@Brian:谁说brk()和操纵了任何“堆栈” sbrk()?堆栈由页面分配器在较低级别管理。
Ben Voigt

Answers:


231

在您发布的图中,“中断”(由brk和操纵的地址)sbrk是堆顶部的虚线。

虚拟内存布局的简化图像

您已阅读的文档将其描述为“数据段”的结尾,因为在传统的(预共享库,pre mmap)Unix中,数据段与堆是连续的;因此,请参见Unix。在程序启动之前,内核将从地址零(实际上比地址零稍高一点)将“文本”和“数据”块加载到RAM中,并将NULL指针真正指向任何东西,并将中断地址设置为数据段的末尾。然后,对的第一个调用mallocsbrk用于移动分解,并在数据段的顶部和新的更高的断开地址之间创建堆,如图所示,随后使用malloc将使用它使堆变大有必要的。

同时,堆栈从内存的顶部开始,然后向下扩展。堆栈不需要显式的系统调用即可使其变大。它要么开始时分配了尽可能多的RAM(这是传统方法),要么在堆栈下方有一个保留地址区域,当内核注意到尝试写入该区域时,内核会自动向其分配RAM (这是现代方法)。无论哪种方式,在地址空间的底部可能有也可能没有“保护”区域,可用于堆栈。如果存在该区域(所有现代系统都这样做),则将永久性地将其映射;如果堆栈或堆尝试扩展到其中时,您会遇到分段错误。但是,传统上,内核不尝试强制执行边界。堆栈可能会增长到堆中,或者堆可能会增长到堆栈中,并且任一种方式它们都会在彼此的数据上乱写,并且程序将崩溃。如果您很幸运,它将立即崩溃。

我不确定此图中512GB的来源。它意味着一个64位的虚拟地址空间,这与您在那里拥有的非常简单的内存映射不一致。实际的64位地址空间看起来像这样:

简化地址空间较少

              Legend:  t: text, d: data, b: BSS

这并不是遥不可及的,它不应该被解释为任何给定OS的工作方式(我画了它之后,我发现Linux实际上使可执行文件比我认为的更接近零地址,以及共享库)。在令人惊讶的高地址)。此图的黑色区域是未映射的-任何访问都会立即导致段错误-并且相对于灰色区域而言,它们是巨大的。浅灰色区域是程序及其共享库(可以有几十个共享库);每个人都有一个独立的文本和数据段(和“ bss”段,也包含全局数据,但被初始化为全零位,而不是占用磁盘上的可执行文件或库中的空间)。堆不再必须与可执行文件的数据段连续-我是用这种方式绘制的,但看起来至少Linux并没有这样做。堆栈不再固定在虚拟地址空间的顶部,并且堆栈与堆栈之间的距离是如此之大,您不必担心会越过它。

中断仍然是堆的上限。但是,我没有显示的是,可能存在数十个独立的内存分配,这些内存以黑色mmap代替brk。(操作系统将尝试将它们远离该brk区域,以免它们发生碰撞。)


7
+1以获得详细说明。您是否知道malloc仍然依靠brk还是正在使用它mmap来“回馈”单独的存储块?
Anders Abel

18
这取决于具体的实现方式,但是IIUC的许多当前malloc使用该brk区域进行小规模分配,而使用单独mmap的进行较大(例如,> 128K)分配。例如,请参见Linux malloc(3)联机帮助页中对MMAP_THRESHOLD的讨论。
zwol 2011年

1
确实是一个很好的解释。但是正如您所说的,Stack不再位于虚拟地址空间的顶部。这仅对64位地址空间才适用,还是对32位地址空间也适用。如果堆栈位于地址空间的顶部,匿名内存映射将在哪里发生?它在堆栈之前是否位于虚拟地址空间的顶部。
尼克,

3
@Nikhil:这很复杂。大多数 32位系统都将堆栈放在用户模式地址空间的最顶端,该地址空间通常仅为整个地址空间的低2或3G(剩余空间留给内核使用)。我目前无法想到一个没有,但我不知道全部。实际上,大多数64位CPU都不允许您使用整个64位空间。地址的高10至16位必须全为零或全为1。堆栈通常放置在可用低地址的顶部附近。我不能给你规则mmap; 它与操作系统非常相关。
zwol 2011年

3
@RiccardoBestetti它浪费的地址空间,但这是无害的-一个64位的虚拟地址空间是如此之大,如果你通过它的千兆字节的每烧第二它仍然会带你500年用完。[1] 大多数处理器甚至不允许使用超过2 ^ 48到2 ^ 53位的虚拟地址(我知道的唯一例外是散列页表模式下的POWER4)。它不会浪费物理内存;未使用的地址未分配给RAM。
zwol 2016年

26

最小的可运行示例

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;
}

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;
}

GitHub上游

上面的代码即使没有,也可能不会进入新页面,也不会出现segfault brk,因此这是分配16MiB的更具侵略性的版本,并且很可能在没有brk:的情况下进行segfault :

#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;
}

在Ubuntu 18.04上测试。

虚拟地址空间可视化

之前brk

+------+ <-- Heap Start == Heap End

之后brk(p + 2)

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

之后brk(b)

+------+ <-- Heap Start == Heap End

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

为什么我们同时需要brksbrk

brk当然可以用sbrk+偏移量计算来实现,两者都是为了方便起见。

在后端,Linux内核v5.0具有一个brk用于同时实现这两个功能的系统调用:https : //github.com/torvalds/linux/blob/v5.0/arch/x86/entry/syscalls/syscall_64。 tbl#L23

12  common  brk         __x64_sys_brk

brkPOSIX吗?

brk以前是POSIX,但在POSIX 2001中已删除,因此需要_GNU_SOURCE访问glibc包装器。

删除可能是由于引入mmap,它是一个超集,它允许分配多个范围和更多分配选项。

我认为,没有有效的情况下,你应该使用brk,而不是mallocmmap现在。

brkmalloc

brk是实现的一种旧可能性malloc

mmap是较新的更强大的机制,目前所有POSIX系统都可以使用该机制来实现malloc。这是一个最小的可运行mmap内存分配示例

我可以混合brk和分配吗?

如果您使用malloc来实现brk,我不知道怎么可能不会炸毁东西,因为它brk仅管理单个范围的内存。

但是我在glibc文档中找不到关于它的任何信息,例如:

我认为事情可能会在那儿工作,因为mmap它可能用于malloc

也可以看看:

更多信息

在内部,内核决定进程是否可以具有那么多的内存,并为该用途指定内存页

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


4
既然p是类型的指针int,那不是brk(p + 2);吗?
JohanBoulé,2016年

小提示:激进版本的for循环中的表达式应该为*(p + i) = 1;
lima.sierra

顺便说一句,为什么我们需要使用a brk(p + 2)而不是简单地将其增加sbrk(2)?真的有必要吗?
Yi Lin Liu

1
@YiLinLiu我认为对于单个内核后端(brksyscall)来说,这只是两个非常相似的C前端。brk恢复先前分配的堆栈会稍微方便一些。
西罗Santilli郝海东冠状病六四事件法轮功

1
@CiroSantilli新疆改造中心996ICU六四事件考虑int的大小为4字节,int *的大小为4字节(在32位计算机上),我想知道它是否不应该仅增加4字节(而不是8-(2 * sizeof int))。它不应该指向下一个可用的堆存储-距离4个字节(而不是8个字节)。如果我在这里缺少任何东西,请纠正我。
莎凯·莎拉德

10

您可以自己使用brksbrk以避免每个人一直抱怨的“ malloc开销”。但是,您无法轻松地将这种方法与之结合使用,malloc因此仅当您不需要free任何东西时才适用。因为你不能。另外,您应避免可能在malloc内部使用的任何库调用。就是 strlen是安全的,但fopen可能不是。

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

void *myallocate(int n){
    return sbrk(n);
}

虽然你不能自由的个体分配(因为没有malloc的开销,记不清了),你可以释放整个空间通过调用brk由第一调用返回的值sbrk,从而倒带BRK

void *memorypool;
void initmemorypool(void){
    memorypool = sbrk(0);
}
void resetmemorypool(void){
    brk(memorypool);
}

您甚至可以堆叠这些区域,通过将中断倒回该区域的开头来丢弃最近的区域。


还有一件事 ...

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


7
-1是因为:malloc/ free最肯定可以(并且确实)将内存还给操作系统。当您希望它们时,他们可能并不总是这样做,但这是针对您的用例进行的启发式调整不完善的问题。更重要的是,在任何可能会调用的程序中使用非零参数进行调用都是不安全的sbrkmalloc -几乎所有C库函数都允许在malloc内部调用。唯一绝对不会的是异步信号安全功能。
zwol

“不安全”是指“您的程序将崩溃”。
zwol

我进行了编辑以消除返回的内存夸张,并提到了内部使用库函数的危险malloc
luser droog

1
如果要进行高级内存分配,则将其基于malloc或mmap。不要碰brk和sbrk,它们是过去的遗物,弊大于利(甚至手册上也告诉您要远离它们!)
Eloff

3
真傻 如果要避免很多小分配的malloc开销,请做一个大分配(使用malloc或mmap,而不是 sbrk),然后自己分配出去。如果将二进制树的节点保留在数组中,则可以使用8b或16b索引而不是64b指针。当您在准备删除所有节点之前不必删除任何节点时,这非常有用。(例如,快速创建排序的字典。)sbrk为此使用仅对代码高尔夫球有用,因为手动使用mmap(MAP_ANONYMOUS)在除源代码大小之外的所有方面都更好。
彼得·科德斯

3

有一个专门指定的匿名私有内存映射(传统上位于数据/ bss的正上方,但是现代Linux实际上会使用ASLR调整位置)。原则上,这并不比您可以使用创建的任何其他映射更好mmap,但是Linux进行了一些优化,可以brk向上扩展该映射的末尾(使用syscall),相对于mmapmremap将要发生的锁定成本降低了。这使得malloc在实现主堆时使用的实现具有吸引力。


您的意思是可以向上扩展此映射的末尾,是吗?
zwol 2011年

是的,固定。对于那个很抱歉!
R .. GitHub停止帮助ICE,

0

我可以回答你的第二个问题。Malloc将失败并返回空指针。这就是为什么在动态分配内存时始终检查空指针的原因。


那么brk和sbrk有什么用?
尼克,

3
@NikhilRathod:malloc()将使用brk()和/或sbrk()在幕后-如果您想实现自己的定制版本,也可以使用malloc()
丹尼尔·普赖登2011年

@Daniel Pryden:brk和sbrk在堆栈和数据段之间时如何在堆上工作,如上图所示。为此工作堆应该在最后。我对吗?
尼克,

2
@Brian:Daniel说,操作系统管理堆栈,而不是堆栈指针 ……非常不同。关键是堆栈段没有sbrk / brk syscall-Linux在尝试写入堆栈段末尾时自动分配页面。
Jim Balter

1
布赖恩,你只回答了一半的问题。另一半是如果在没有可用空间的情况下尝试压入堆栈会发生什么……会出现分段错误。
Jim Balter

0

堆放在程序的数据段的最后。brk()用于更改(扩展)堆的大小。当堆不再增长时,任何malloc调用都会失败。


因此,您是说互联网上的所有图表,例如我所质疑的图表都是错误的。如果可以的话,请您指出正确的图表。
尼克,

2
@Nikkhil请记住,该图的顶部是内存的结尾。随着堆栈的增加,堆栈的顶部在图上向下移动。堆的顶部在展开时在图上向上移动。
布莱恩·戈登

0

数据段是存储所有静态数据的内存部分,这些静态数据在启动时从可执行文件中读取,通常为零。


它还保存未初始化的静态数据(不存在于可执行文件中),这可能是垃圾。
luser droog 2011年

.bss在程序启动之前,未初始化的静态数据()由OS初始化为全零。这实际上是C标准所保证的。我想有些嵌入式系统可能不会打扰(我从未见过,但是我无法在所有嵌入式系统上工作)
zwol

@zwol:Linux有一个编译时选项,以不将返回的页面归零mmap,但我认为.bss仍将归零。BSS空间可能是表达程序需要某些归零数组这一事实的最紧凑的方式。
彼得·科德斯

1
@PeterCordes C标准所说的是,如果没有初始化为声明的全局变量,则将其初始化为零。因此,将此类变量放入.bss且不为零的AC实现.bss将是不合格的。但是什么也没有强迫C实现完全使用.bss甚至没有这样的东西。
zwol

@PeterCordes同样,“ C实现”和程序之间的界线可能非常模糊,例如,实现中通常会有一小段代码静态地链接到每个可执行文件中,而这些代码之前运行main;该代码可以使.bss区域归零而不是由内核执行,这仍然是合规的。
zwol

0

malloc使用brk系统调用来分配内存。

包括

int main(void){

char *a = malloc(10); 
return 0;
}

使用strace运行这个简单的程序,它将调用brk系统。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.