关于C#:brk()系统调用有什么作用?

What does the brk() system call do?

根据Linux程序员手册:

brk() and sbrk() change the location of the program break, which
defines the end of the process's data segment.

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

根据维基:

Sometimes the data, BSS, and heap areas are collectively referred to as the"data segment".

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

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

enter image description here


在您发布的图表中,"break" - 由brksbrk操纵的地址 - 是堆顶部的虚线。

simplified image of virtual memory layout

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

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

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

less simplified address space

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

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

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


最小的可运行示例

What does brk( ) system call do?

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

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

没有brk

1
2
3
4
5
6
7
8
9
10
11
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define _GNU_SOURCE
#include
#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上游。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define _GNU_SOURCE
#include
#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之前:

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

brk(p + 2)之后:

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

brk(b)之后:

1
+------+ <-- 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

1
12  common  brk         __x64_sys_brk

brk POSIX?

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

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

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

brk vs malloc

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

mmap是更新的更强大的机制,可能所有POSIX系统当前用来实现malloc

我可以混用brk和malloc吗?

如果你的malloc是用brk实现的,我不知道怎么可能不会炸毁东西,因为brk只管理一个范围的内存。

然而,我无法在glibc docs上找到任何关于它的东西,例如:

  • https://www.gnu.org/software/libc/manual/html_mono/libc.html#Resizing-the-Data-Segment

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

也可以看看:

  • 关于brk / sbrk有什么不安全/遗产?
  • 为什么两次调用sbrk(0)给出不同的值?

更多信息

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

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


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

像调用malloc一样调用sbrk。它返回一个指向当前中断的指针,并将中断增加该数量。

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

虽然您无法释放单个分配(因为没有malloc开销,请记住),您可以通过使用第一次调用sbrk返回的值调用brk来释放整个空间,从而重绕brk。

1
2
3
4
5
6
7
void *memorypool;
void initmemorypool(void){
    memorypool = sbrk(0);
}
void resetmemorypool(void){
    brk(memorypool);
}

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

还有一件事 ...

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


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


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

包括

1
2
3
4
5
int main(void){

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

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


数据段是保存所有静态数据的内存部分,在启动时从可执行文件读入,通常为零填充。


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


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