brk() 系统调用起什么作用?

2024-10-12 10:28:00
admin
原创
91
摘要:问题描述:根据Linux程序员手册:brk() 和 sbrk() 改变程序中断的位置,它定义了进程数据段的结束。这里的数据段是什么意思?它只是数据段还是数据、BSS 和堆的组合?根据 wiki数据部分:有时数据、BSS 和堆区域统称为“数据段”。我认为没有必要只改变数据段的大小。如果是数据、BSS和堆,那么这...

问题描述:

根据Linux程序员手册:

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

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

根据 wiki数据部分:

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

我认为没有必要只改变数据段的大小。如果是数据、BSS和堆,那么这样做是有道理的,因为堆将获得更多空间。

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

在此处输入图片描述


解决方案 1:

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

虚拟内存布局的简化图像

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

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

我不确定此图中的数字 512GB 从何而来。它表示 64 位虚拟地址空间,这与您那里的非常简单的内存映射不一致。真正的 64 位地址空间看起来更像这样:

不太简化的地址空间

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

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

中断仍然是堆的上限。但是,我没有展示的是,在黑色的某个地方可能有几十个独立的内存分配,是用mmap而不是 进行的brk。(操作系统会尝试让这些内存远离该brk区域,以免发生冲突。)

解决方案 2:

最小可运行示例

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 分页如何工作?。

为什么我们需要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来代替。malloc`mmap`

brk对比malloc

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

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

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

如果您的malloc是用来实现的brk,我不知道它怎么可能不会把事情搞砸,因为brk它只管理单一范围的内存。

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

我认为事情很可能会在那里正常进行,因为mmap很可能用于malloc

参见:

  • brk/sbrk 有哪些不安全/遗留问题?

  • 为什么两次调用 sbrk(0) 会给出不同的值?

更多信息

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

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

解决方案 3:

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

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

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

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

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

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


还有一件事...

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

解决方案 4:

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

解决方案 5:

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

解决方案 6:

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

包括

int main(void){

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

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

解决方案 7:

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

解决方案 8:

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

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用