brk() 系统调用起什么作用?
- 2024-10-12 10:28:00
- admin 原创
- 91
问题描述:
根据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 分页如何工作?。
为什么我们需要brk
和sbrk
?
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 包装器。
删除可能是由于引入了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:
您可以自己使用brk
和sbrk
来避免每个人都在抱怨的“malloc 开销”。但是您不能轻易地将此方法与 结合使用,malloc
因此它只适用于您不需要做free
任何事情的情况。因为您做不到。此外,您应该避免任何可能在内部使用的库调用malloc
。即。strlen
可能是安全的,但fopen
可能不是。
调用sbrk
就像调用 一样malloc
。它返回指向当前中断的指针,并将中断增加该量。
void *myallocate(int n){
return sbrk(n);
}
虽然您不能释放单个分配(记住,因为没有malloc-overhead),但是您可以通过调用并传入第一次调用的返回值来释放整个空间,从而倒回 brk。brk
`sbrk`
void *memorypool;
void initmemorypool(void){
memorypool = sbrk(0);
}
void resetmemorypool(void){
brk(memorypool);
}
您甚至可以堆叠这些区域,通过将断点倒回到该区域的开始处来丢弃最近的区域。
还有一件事...
sbrk
在代码高尔夫中也很有用,因为它比短 2 个字符malloc
。
解决方案 4:
有一个特殊指定的匿名私有内存映射(传统上位于数据/bss 之外,但现代 Linux 实际上会使用 ASLR 调整位置)。原则上,它并不比您可以创建的任何其他映射更好mmap
,但 Linux 有一些优化,可以brk
向上扩展此映射的末尾(使用系统调用),同时降低锁定成本(相对于mmap
或mremap
将产生的成本)。这使得malloc
在实现主堆时使用它具有吸引力。
解决方案 5:
堆位于程序数据段的最后。brk()
用于更改(扩展)堆的大小。当堆无法再增长时,任何malloc
调用都将失败。
解决方案 6:
malloc 使用 brk 系统调用来分配内存。
包括
int main(void){
char *a = malloc(10);
return 0;
}
用 strace 运行这个简单程序,它将调用 brk 系统。
解决方案 7:
我可以回答你的第二个问题。Malloc 将失败并返回一个空指针。这就是为什么在动态分配内存时总是检查空指针的原因。
解决方案 8:
数据段是保存所有静态数据的内存部分,这些数据在启动时从可执行文件读取,通常用零填充。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件