根据Linux程序员手册:
brk()和sbrk()改变程序中断的位置,即 定义流程数据段的结束。
这里的数据段是什么意思?是仅仅将数据段或数据,BSS和堆组合在一起?
根据维基:
有时,数据,BSS和堆区域统称为“数据段”。
我认为没有理由改变数据段的大小。如果它是数据,BSS和堆集合那么它就有意义,因为堆将获得更多的空间。
这让我想到了第二个问题。在我到目前为止阅读的所有文章中,作者都说堆积增长,堆栈向下增长。但他们没有解释的是当堆占用堆和堆栈之间的所有空间时会发生什么?
答案 0 :(得分:207)
在您发布的图表中,“中断” - 由brk
和sbrk
操纵的地址 - 是堆顶部的虚线。
您阅读的文档将此描述为“数据段”的结尾,因为在传统的(预共享库,前mmap
)Unix中,数据段与堆连续;在程序启动之前,内核会将“文本”和“数据”块加载到RAM中,从地址0开始(实际上略高于地址0,因此NULL指针确实没有指向任何东西)并将中断地址设置为数据段的结尾。然后,对malloc
的第一次调用将使用sbrk
移动分解,并在数据段的顶部和新的更高的中断地址之间创建堆,如如图所示,随后使用malloc
将使用它来根据需要增加堆。
同时,堆栈从内存顶部开始并逐渐减少。堆栈不需要显式系统调用来使其更大;或者它开始时分配给它的RAM尽可能多(这是传统的方法),或者堆栈下面有一个保留地址区域,内核在注意到写入时会自动分配RAM (这是现代方法)。无论哪种方式,地址空间底部可能存在或可能不存在可用于堆栈的“保护”区域。如果这个区域存在(所有现代系统都这样做),它将被永久取消映射;如果要么堆栈或堆试图进入堆栈,就会出现分段错误。但是,传统上,内核并没有试图强制执行边界;堆栈可能会成长为堆,或者堆可能会成长为堆栈,无论哪种方式,他们都会乱写彼此的数据,程序会崩溃。如果你很幸运,它会立即崩溃。
我不确定这个图中512GB的数字来自哪里。它意味着一个64位的虚拟地址空间,这与你在那里非常简单的内存映射不一致。一个真正的64位地址空间看起来更像是这样:
这不是远程扩展,它不应该被解释为任何给定操作系统的确切方式(在我绘制之后)我发现Linux实际上使可执行文件比我想象的更接近于零,并且共享库在令人惊讶的高地址)。此图的黑色区域未映射 - 任何访问都会导致立即的段错误 - 并且相对于灰色区域它们是巨大的。浅灰色区域是程序及其共享库(可以有数十个共享库);每个都有一个独立的文本和数据段(和“bss”段,它也包含全局数据,但初始化为所有位零,而不占用磁盘上可执行文件或库中的空间)。堆不再必然与可执行文件的数据段连续 - 我这样绘制了它,但看起来Linux至少不会这样做。堆栈不再挂在虚拟地址空间的顶部,堆和堆栈之间的距离是如此之大,以至于您不必担心会越过它。
中断仍然是堆的上限。然而,我没有表明的是,在某处黑色可能存在数十个独立的内存分配,使用mmap
代替brk
。 (操作系统会尽量远离brk
区域,以免它们发生碰撞。)
答案 1 :(得分:19)
最小可运行示例
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;
}
即使没有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
为了更好地理解地址空间,您应该熟悉分页:How does x86 paging work?。
为什么我们同时需要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
vs malloc
brk
是实现malloc
的一种可能性。
mmap
是一种更新的,更强大的机制,可能所有POSIX系统目前都用来实现malloc
。
我可以混用brk
和malloc吗?
如果您的malloc
是用brk
实现的,我不知道怎么可能不会炸毁,因为brk
只管理一个内存范围。
然而,我无法在glibc文档中找到任何关于它的内容,例如:
因为mmap
可能会用于malloc
,所以事情很可能会在那里工作。
另见:
更多信息
在内部,内核决定进程是否可以拥有那么多内存,并为此用法标记memory pages。
这解释了堆栈与堆的比较:What is the function of the push / pop instructions used on registers in x86 assembly?
答案 2 :(得分:9)
您可以自己使用brk
和sbrk
来避免每个人都在抱怨的“malloc开销”。但是你不能轻易地将这个方法与malloc
结合使用,所以只有当你没有free
任何东西时它才适合。因为你做不到。此外,您应该避免任何可能在内部使用malloc
的库调用。 IE浏览器。 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);
}
你甚至可以堆叠这些区域,通过将休息时间倒回到区域的开始来丢弃最近的区域。
还有一件事......
sbrk
在code golf中也很有用,因为它比malloc
短2个字符。
答案 3 :(得分:3)
有一个特殊的指定匿名私有内存映射(传统上位于data / bss之外,但现代Linux实际上将使用ASLR调整位置)。原则上它并不比使用mmap
创建的任何其他映射更好,但Linux有一些优化可以扩展此映射的结尾(使用brk
系统调用)以降低锁定成本相对于mmap
或mremap
会产生什么。这使得在实现主堆时malloc
实现具有吸引力。
答案 4 :(得分:0)
我可以回答你的第二个问题。 Malloc将失败并返回空指针。这就是为什么在动态分配内存时总是检查空指针的原因。
答案 5 :(得分:0)
堆放在程序数据段的最后。 brk()
用于更改(扩展)堆的大小。当堆不能再增长时,任何malloc
调用都将失败。
答案 6 :(得分:0)
数据段是保存所有静态数据的内存部分,在启动时从可执行文件读入,通常为零填充。
答案 7 :(得分:0)
malloc使用brk系统调用来分配内存。
包括
int main(void){
char *a = malloc(10);
return 0;
}
用strace运行这个简单的程序,它将调用brk系统。