222

根据Linux程序员手册:

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

这里的数据段是什么意思?只是数据段还是数据、BSS、堆结合?

根据维基数据段

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

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

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

在此处输入图像描述

4

8 回答 8

278

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

虚拟内存布局的简化图

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

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

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

不太简化的地址空间

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

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

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

于 2011-08-09T01:19:37.913 回答
40

最小可运行示例

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 上游.

即使没有 ,上述内容也可能不会出现新页面并且不会出现段错误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;
}

在 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 分页是如何工作的?.

为什么我们同时需要brkand sbrk

brk当然可以用sbrk+ 偏移计算来实现,两者都只是为了方便而存在。

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

12  common  brk         __x64_sys_brk

brkPOSIX吗?

brk曾经是 POSIX,但在 POSIX 2001 中被删除,因此需要_GNU_SOURCE访问 glibc 包装器。

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

我认为没有有效的案例可以brk代替mallocmmap现在使用。

brk对比malloc

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

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

我可以混合brk和malloc吗?

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

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

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

也可以看看:

更多信息

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

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

于 2015-06-26T21:23:28.620 回答
10

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

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

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

虽然您不能释放单个分配(因为没有malloc-overhead,请记住),但您可以通过使用第一次调用返回的值来释放整个空间to ,从而倒回 brkbrksbrk

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

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


还有一件事 ...

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

于 2011-08-09T06:10:49.570 回答
4

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

于 2011-08-08T22:32:16.330 回答
1

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

包括

int main(void){

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

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

于 2013-03-12T16:44:12.937 回答
0

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

于 2011-08-08T20:59:57.910 回答
0

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

于 2011-08-08T21:00:19.227 回答
0

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

于 2011-08-08T21:02:13.993 回答