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