C语言 brk()系统调用是做什么的?

xwmevbvl  于 2023-10-16  发布在  其他
关注(0)|答案(8)|浏览(133)

根据Linux程序员手册:
brk()和sbrk()改变程序中断的位置,它定义了进程数据段的结束。
这里的数据段是什么意思?它只是数据段还是数据、BSS和堆的组合?
根据wiki Data segment
有时数据、BSS和堆区域统称为“数据段”。
我认为没有理由只改变数据段的大小。如果它是数据,BSS和堆的集合,那么它是有意义的,因为堆将获得更多的空间。
这就引出了我的第二个问题到目前为止,在我读过的所有文章中,作者都说堆是向上增长的,堆栈是向下增长的。但是他们没有解释的是,当堆占据了堆和栈之间的所有空间时会发生什么?

1l5u6lss

1l5u6lss1#

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

你读过的文档将此描述为“数据段”的结束,因为在传统的(预共享库,预mmap)Unix中,数据段与堆是连续的;在程序开始之前,内核会从地址0开始将“text”和“data”块加载到RAM中(实际上是略高于地址0,这样NULL指针就不会指向任何东西),并将中断地址设置为数据段的末尾。第一次调用malloc将使用sbrk来移动break,并在数据段的顶部和新的更高的break地址之间创建堆,如图所示,随后使用malloc将根据需要使用它来使堆更大。
同时,堆栈从内存的顶部开始向下增长。堆栈不需要显式的系统调用来使其更大;要么它开始时分配给它尽可能多的RAM(这是传统的方法),要么在堆栈下面有一个保留地址区域,当内核注意到有人试图写入时,它会自动分配RAM(这是现代的方法)。无论哪种方式,在地址空间的底部可能有也可能没有可用于堆栈的“保护”区域。如果这个区域存在(所有现代系统都这样做),它将永久未Map;如果栈或堆试图增长到它里面,你会得到一个分段错误。然而,传统上,内核并不试图强制边界;堆栈可以增长为堆,或者堆可以增长为堆栈,无论哪种方式,它们都会在彼此的数据上乱涂乱画,程序就会崩溃。如果你很幸运,它会立即崩溃。
我不知道这个图中的512 GB数字是从哪里来的。它意味着一个64位的虚拟地址空间,这与您拥有的非常简单的内存Map不一致。一个真实的64位地址空间看起来更像这样:

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

这不是远程缩放,它不应该被解释为任何给定的操作系统是如何做的(在我画了它之后,我发现Linux实际上把可执行文件放在比我想象的更接近地址零的地方,共享库的地址高得惊人)。图中的黑色区域是未Map的--任何访问都会立即导致segfault --它们相对于灰色区域来说非常巨大。浅灰色区域是程序及其共享库(可能有几十个共享库);每一个都有一个独立的文本和数据段(以及“0”段,它也包含全局数据,但被初始化为全位零,而不是占用磁盘上的可执行文件或库中的空间)。堆不再需要与可执行文件的数据段保持连续--我是这样画的,但看起来至少Linux不这样做。堆栈不再与虚拟地址空间的顶部挂钩,堆和堆栈之间的距离是如此之大,以至于您不必担心跨越它。
中断仍然是堆的上限。然而,我没有展示的是,在某处可能有几十个独立的内存分配,使用mmap而不是brk。(操作系统将尝试使这些远离brk区域,以便它们不会碰撞。

svgewumm

svgewumm2#

最小可运行示例

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 upstream
上面的代码可能不会命中新页面,即使没有brk也不会发生segfault,所以这里有一个更激进的版本,它分配了16 MiB,并且很可能在没有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

为了更好地理解地址空间,您应该熟悉分页:How does x86 paging work?

为什么我们需要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

brk POSIX吗?

brk曾经是POSIX,但在POSIX 2001中被删除,因此需要_GNU_SOURCE访问glibc Package 器。
删除可能是由于引入了mmap,这是一个允许分配多个范围和更多分配选项的超集。
我认为没有有效的情况下,你应该使用brk而不是mallocmmap现在。

brk vs malloc

brk是实现malloc的一种旧的可能性。
mmap是一种更新的更强大的机制,可能所有POSIX系统目前都使用它来实现malloc。这是一个minimal runnable mmap memory allocation example

我可以混合使用brk和malloc吗?

如果你的malloc是用brk实现的,我不知道这怎么可能不把事情搞砸,因为brk只管理一个内存范围。
但是我在glibc文档中找不到任何关于它的信息,例如:

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

我想事情可能只是在那里工作,因为mmap可能用于malloc
另请参阅:

在内部,内核决定进程是否可以拥有那么多内存,并为该用途指定memory pages
这解释了堆栈与堆的比较:x86汇编中寄存器上使用的push / pop指令的功能是什么?

rqenqsqc

rqenqsqc3#

您可以自己使用brksbrk来避免每个人都在抱怨的“malloc开销”。但是你不能很容易地将这个方法与malloc结合使用,所以只有当你不需要free任何东西的时候,它才是合适的。因为你不能。此外,您应该避免任何可能在内部使用malloc的库调用。strlen可能是安全的,但fopen可能不是。
调用sbrk就像调用malloc一样。它返回一个指向当前断点的指针,并按该值递增断点。

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

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

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

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

  • 还有一件事 *

sbrkcode golf中也很有用,因为它比malloc短2个字符。

2ul0zpep

2ul0zpep4#

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

yh2wf1be

yh2wf1be5#

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

myzjeezk

myzjeezk6#

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

int main(void){

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

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

mznpcxlj

mznpcxlj7#

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

r8uurelv

r8uurelv8#

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

相关问题