根据Linux程序员手册:
brk() and sbrk() change the location of the program break, which
defines the end of the process's data segment.
这里的数据段意味着什么? 是仅仅将数据段或数据,BSS和堆组合在一起?
根据维基:
Sometimes the data, BSS, and heap areas are collectively referred to as the"data segment".
我认为没有理由改变数据段的大小。 如果它是数据,BSS和堆集合那么它是有意义的,因为堆将获得更多的空间。
这让我想到了第二个问题。 在我到目前为止阅读的所有文章中,作者都说堆积增长,堆栈向下增长。 但他们没有解释的是当堆占用堆和堆栈之间的所有空间时会发生什么?
-
那么当你离开太空时你会怎么做?你换成硬盘了。使用空间后,将其释放以获取其他类型的信息。
-
@Igoris:你混淆了物理内存(可以根据需要使用虚拟内存交换到磁盘)和地址空间。当你填满你的地址空间时,没有任何数量的交换会让你回到中间的那些地址。
-
提醒一下,brk()系统调用在汇编语言中比在C中更有用。在C中,应该使用malloc()代替brk()进行任何数据分配 - 但这不会使提议无效问题无论如何。
-
请人们不要再谈论堆栈了。我们生活在一个多线程的世界!
-
@Ben不要在堆上分配子线程堆栈吗?这只是pthreads?
-
@Brian:堆是一个复杂的数据结构,用于处理不同大小和对齐的区域,空闲池等。线程堆栈始终是连续的(在虚拟地址空间中)完整页面的序列。在大多数操作系统中,有一个页面分配器,它包含堆栈,堆和内存映射文件。
-
@Ben但是只有一个资本-S"堆栈"由brk()和sbrk()操纵,对吧?其他线程堆栈是由用户代码处理的较小的东西,对吗?
-
@Brian:谁说有任何"堆栈"被brk()和sbrk()操纵?堆栈由页面分配器管理,级别要低得多。
-
@Ben哦,当我回到这个问题时,我认为有一个堆栈被这些系统调用操纵,但现在我记得它实际上是堆的顶部。我当时认为必须有一个规范的堆栈,但你现在说的是有道理的。堆栈地址中发生了什么并不重要(只要你有一个堆栈指针或其他地方)因为你不应该在线程之间共享它们。
在您发布的图表中,"break" - 由brk和sbrk操纵的地址 - 是堆顶部的虚线。
您阅读的文档将此描述为"数据段"的结尾,因为在传统的(预共享库,前mmap)Unix中,数据段与堆连续;在程序启动之前,内核会将"文本"和"数据"块加载到RAM中,从地址0开始(实际上略高于地址0,因此NULL指针确实没有指向任何东西)并将中断地址设置为数据段的结尾。然后,第一次调用malloc将使用sbrk移动分解并在数据段的顶部和新的更高的中断地址之间创建堆,如图所示,并随后使用会根据需要使用它来使堆更大。
同时,堆栈从内存顶部开始并逐渐减少。堆栈不需要显式系统调用来使其更大;或者它开始时分配给它的RAM尽可能多(这是传统的方法),或者堆栈下面有一个保留地址区域,内核在注意到写入时会自动分配RAM (这是现代方法)。无论哪种方式,地址空间底部可能存在或可能不存在可用于堆栈的"保护"区域。如果这个区域存在(所有现代系统都这样做),它将被永久取消映射;如果堆栈或堆尝试增长到它,则会出现分段错误。但是,传统上,内核并没有试图强制执行边界;堆栈可能会成长为堆,或者堆可能会成长为堆栈,无论哪种方式,他们都会乱写彼此的数据,程序会崩溃。如果你很幸运,它会立即崩溃。
我不确定这个图中512GB的数字来自哪里。它意味着一个64位的虚拟地址空间,这与你在那里非常简单的内存映射不一致。一个真正的64位地址空间看起来更像这样:
1
| Legend: t: text, d: data, b: BSS |
这不是远程扩展,它不应该被解释为任何给定操作系统的确切方式(在我绘制它之后我发现Linux实际上使可执行文件比我想象的更接近零地址和共享库在令人惊讶的高地址)。该图的黑色区域未映射 - 任何访问都会导致立即的段错误 - 并且它们相对于灰色区域是巨大的。浅灰色区域是程序及其共享库(可以有数十个共享库);每个都有一个独立的文本和数据段(和"bss"段,它也包含全局数据,但初始化为所有位零,而不是占用磁盘上可执行文件或库中的空间)。堆不再必然与可执行文件的数据段连续 - 我这样绘制了它,但看起来Linux至少不会这样做。堆栈不再与虚拟地址空间的顶部挂钩,堆与堆栈之间的距离非常大,您无需担心跨越它。
中断仍然是堆的上限。然而,我没有表现出的是,黑色的某处可能存在数十个独立的内存分配,用mmap代替brk。 (操作系统会尽量远离brk区域,以免它们发生碰撞。)
-
+1以获得详细说明。你知道吗malloc是否还依赖于brk,或者它是否正在使用mmap来"回馈"单独的内存块?
-
这取决于具体的实现,但是IIUC很多当前的malloc用于小分配的brk区域和用于大(例如>> 128K)分配的单个mmap s。例如,请参阅Linux malloc(3)联机帮助页中有关MMAP_THRESHOLD的讨论。
-
确实是个很好的解释。但正如你所说堆栈不再位于虚拟地址空间的顶部。这仅适用于64位地址空间,即使对于32位地址空间也是如此。如果堆栈位于地址空间的顶部,那么匿名内存映射会在哪里发生?它是否在堆栈之前位于虚拟地址空间的顶部。
-
@Nikhil:这很复杂。大多数32位系统将堆栈放在用户模式地址空间的最顶层,通常只有整个地址空间的低2或3G(剩余空间是为内核保留的)。我现在不能想到一个没有但我不知道的人。大多数64位CPU实际上并不允许您使用整个64位空间;地址的高10到16位必须为全零或全1。堆栈通常位于可用低地址的顶部附近。我不能给你mmap的规则;它非常依赖于操作系统。
-
实际上,512GB意味着一个39位的用户地址空间 - 可能是40位地址空间的一半,而上半部分是为内核保留的。
-
为什么空白区域这么大?这不是浪费你一半的记忆吗?
-
@RiccardoBestetti它浪费了地址空间,但这是无害的 - 64位的虚拟地址空间是如此之大,以至于如果你每秒烧掉一千兆字节的空间,它仍然需要500年才能耗尽。 [ 1]大多数处理器甚至不允许使用超过2 ^ 48到2 ^ 53位的虚拟地址(我知道的唯一例外是散列页表模式下的POWER4)。它不会浪费物理RAM;未使用的地址未分配给RAM。
-
@zwol啊,我明白了!它们被映射到物理内存,彼此之间没有任何空间。现在有道理。谢谢。
-
一个很好的解释。因此,如果堆不一定与程序的数据段连续,那么如何找到堆的最低字节(让我们将此限制为没有C运行时的情况 - 只是一些汇编代码) - 是否只是调用它sbrk第一次获得一些存储空间并告诉你返回值?
-
@codeshot是的,这就是它的本质。棘手的一点是,如果你没有C运行时,你可能不会得到sbrk,只有brk,并且原始系统调用brk可能与文档中的brk函数没有相同的语义。 C库,所以你必须检查内核实际做了什么。
-
@zwol,是的我写了一些x64 linux汇编程序。 linux brk系统调用用于sbrk,在该平台上你必须同步,同时使用差异从旧计算新的brk大小。系统调用采用所需的大小并返回一个结束指针。最初你可以请求brk(0),你得到同时指向两端的指针。当brk大小已经非零时,我不确定在那种情况下会发生什么
-
很好的解释,虽然这句话:"或者堆栈下面有一个保留地址区域,内核在注意到在那里写入时会自动分配RAM(这是现代方法)。" -"RAM","它","那里"太多含糊不清。你可以再详细一点吗?
-
Linux和其他现代操作系统以"令人惊讶的"低效方式布置不同内存段的一个原因是地址空间布局随机化。这是一项安全措施。如果代码和数据处于不可预测的地址,则攻击者升级缓冲区溢出以使程序执行任意代码变得更加困难。
-
@Davislor是的,我知道,但是由于代码模型很小,我期待加载器尝试将库打包到前2GB的地址空间中。事实证明,小代码模型不会阻止共享库和位置无关的可执行文件被加载到任意基址;它只意味着它们的总大小不得超过2GB。
-
@zwol对,你做了,但我想新手可能想知道。
最小的可运行示例
What does brk( ) system call do?
要求内核让您读取和写入称为堆的连续内存块。
如果你不问,它可能会让你陷入困境。
没有brk:
1 2 3 4 5 6 7 8 9 10 11
| #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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #define _GNU_SOURCE
#include
#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的情况下发生段错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #define _GNU_SOURCE
#include
#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之前:
1
| +------+ <-- Heap Start == Heap End |
brk(p + 2)之后:
1 2 3 4 5 6
| +------+ <-- Heap Start + 2 * sizof(int) == Heap End
| |
| You can now write your ints
| in this memory area.
| |
+------+ <-- Heap Start |
brk(b)之后:
1
| +------+ <-- 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
1
| 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 docs上找到任何关于它的东西,例如:
-
https://www.gnu.org/software/libc/manual/html_mono/libc.html#Resizing-the-Data-Segment
我认为事情可能会在那里工作,因为mmap可能用于malloc。
也可以看看:
-
关于brk / sbrk有什么不安全/遗产?
-
为什么两次调用sbrk(0)给出不同的值?
更多信息
在内部,内核决定进程是否可以拥有那么多内存,并为该用法指定内存页面。
这解释了堆栈与堆的比较:x86汇编中寄存器上使用的push / pop指令的功能是什么?
-
由于p是一个指向int类型的指针,这不应该是brk(p + 2);吗?
-
@JohanBoule你是对的,修好了。
-
小注意:积极版本的for循环中的表达式应该是*(p + i) = 1;
-
@ lima.sierra是的,谢谢你的纠正
-
那么,为什么我们需要使用brk(p + 2)而不是简单地将其增加sbrk(2)? brk真的有必要吗?
-
@YiLinLiu我认为对于单个内核后端(brk系统调用)来说,它只是两个非常相似的C前端。恢复先前分配的堆栈稍微方便一些。
-
警告:确保在调用sbrk(0)之前至少使用printf()一次。导致程序上的第一次printf()调用为stdout分配一个缓冲区并且与sbrk(0)混乱。参考。
-
为什么"将它向前移动2"?
-
@SaketSharad,因为p是int *。 你期望什么是正确的代码或评论?
-
@CiroSantilli新疆改造中心996ICU六四事件考虑int的大小为4个字节,int *的大小为4个字节(在32位机器上),我想知道它不应该只增加4个字节(而不是 8 - (2 * sizeof int))。 它不应该指向下一个可用的堆存储 - 它将是4个字节(不是8个)。 如果我在这里遗漏了什么,请纠正我。
-
@SaketSharad brk获取指向区域末尾的指针,而不是像sbrk那样的delta的大小。
您可以自己使用brk和sbrk来避免每个人总是在抱怨的"malloc开销"。但是你不能轻易地将这个方法与malloc结合使用,所以它只适用于你不需要free的任何东西。因为你做不到。此外,您应该避免任何可能在内部使用malloc的库调用。 IE浏览器。 strlen可能是安全的,但fopen可能不是。
像调用malloc一样调用sbrk。它返回一个指向当前中断的指针,并将中断增加该数量。
1 2 3
| void *myallocate(int n){
return sbrk(n);
} |
虽然您无法释放单个分配(因为没有malloc开销,请记住),您可以通过使用第一次调用sbrk返回的值调用brk来释放整个空间,从而重绕brk。
1 2 3 4 5 6 7
| void *memorypool;
void initmemorypool(void){
memorypool = sbrk(0);
}
void resetmemorypool(void){
brk(memorypool);
} |
您甚至可以堆叠这些区域,通过将休息时间倒回到区域的开始来丢弃最近的区域。
还有一件事 ...
sbrk在代码高尔夫中也很有用,因为它比malloc短2个字符。
-
-1因为:malloc / free当然可以(并且确实)将内存返回给操作系统。当你想要它们时,它们可能并不总是这样做,但这是因为你的用例未完全调整的启发式问题。更重要的是,在任何可能调用malloc的程序中使用非零参数调用sbrk是不安全的 - 并且几乎所有C库函数都允许在内部调用malloc。唯一肯定不会是异步信号安全功能的。
-
而且"它不安全",我的意思是"你的程序会崩溃。"
-
我编辑了删除返回的内存夸耀,并在内部使用malloc提到了库函数的危险。
-
如果你想进行奇特的内存分配,可以将它放在malloc之上,也可以放在mmap之上。不要碰brk和sbrk,它们是过去的弊大于利的遗物(即使是联机帮助你告诉你要远离它们!)
-
同意。对于现实世界的使用,它们是禁止的。但我昨天写了一个使用它们的程序。代码高尔夫,当然。
-
这太傻了。如果你想避免大量小分配的malloc开销,做一个大的分配(使用malloc或mmap,而不是sbrk)并自己解决它。如果将二叉树的节点保存在数组中,则可以使用8b或16b索引而不是64b指针。在准备删除所有节点之前,如果不必删除任何节点,则此方法很有用。 (例如,动态构建一个排序的字典。)使用sbrk只对代码高尔夫有用,因为除了源代码大小外,手动使用mmap(MAP_ANONYMOUS)在各方面都更好。
有一个特殊的指定匿名私有内存映射(传统上位于data / bss之外,但现代Linux实际上将使用ASLR调整位置)。原则上它并不比使用mmap创建的任何其他映射更好,但Linux有一些优化可以扩展此映射的结尾(使用brk系统调用)向上,相对于< x2>或mremap会产生。这使得在实现主堆时使用malloc实现很有吸引力。
-
你的意思是可以向上扩展这个映射的结束,是吗?
-
是的,修好了。对于那个很抱歉!
malloc使用brk系统调用来分配内存。
包括
1 2 3 4 5
| int main (void){
char *a = malloc(10);
return 0;
} |
用strace运行这个简单的程序,它将调用brk系统。
数据段是保存所有静态数据的内存部分,在启动时从可执行文件读入,通常为零填充。
-
它还包含未初始化的静态数据(不存在于可执行文件中),这些数据可能是垃圾。
-
在程序启动之前,操作系统将未初始化的静态数据(.bss)初始化为全位为零;这实际上是由C标准保证的。一些嵌入式系统可能不会打扰,我想(我从来没有见过一个,但我没有工作所有嵌入式)
-
@zwol:Linux有一个编译时选项,不会将mmap返回的页面归零,但我认为.bss仍然会归零。 BSS空间可能是表达程序需要一些零点阵列这一事实的最紧凑的方式。
-
@PeterCordes C标准所说的是,没有初始化程序声明的全局变量被视为 - 如果初始化为零。因此,将这些变量置于.bss且不为零.bss的C实现将是不一致的。但是没有什么能迫使C实现完全使用.bss甚至有这样的东西。
-
@PeterCordes此外,"C实现"和程序之间的界限可能非常模糊,例如通常会有一小部分来自实现的代码,静态链接到每个可执行文件中,在main之前运行;该代码可以将.bss区域归零,而不是让内核执行此操作,这仍然符合要求。
-
@zwol:我的观点是,这样的实现会比内核的ELF-loader零初始化bss更糟糕。你不需要做任何事情,因为你需要解决它仍然有一个可用的C实现(除非你的嵌入式系统不需要任何零初始化数组)。请注意,我说"可用",而不是"符合":我们谈论的是嵌入式系统。如果内核,编译器和运行时内容的组合适用于您要运行的代码,那么它是可用的。
堆放在程序数据段的最后。 brk()用于更改(扩展)堆的大小。当堆不能再增长时,任何malloc调用都将失败。
-
所以你说互联网上的所有图表,就像我的问题中的图表都是错误的。如果可能,请指点我正确的图表。
-
@Nikkhil请记住,该图的顶部是内存的结尾。随着堆栈的增长,堆栈顶部在图表上向下移动。堆栈顶部在展开时向上移动。
我可以回答你的第二个问题。 Malloc将失败并返回空指针。这就是为什么在动态分配内存时总是检查空指针的原因。
-
那么brk和sbrk的用途是什么?
-
@NikhilRathod:malloc()将使用brk()和/或sbrk() - 如果你想实现自己的malloc()自定义版本,也可以使用brk()。
-
@Daniel Pryden:如上图所示,当堆栈和数据段之间的brk和sbrk如何在堆上工作时。为了这个工作堆应该到底。我对吗?
-
@Nikkhil你的图表上有一个有弹性的箭头,在Heap和空白之间的边界上。它可以扩展到那个空间。堆和堆栈之间有很多空间"空"。操作系统透明地给你一个巨大的内存空间的幻觉,所以你不必担心这一点。
-
@Nikhil:您的程序同时具有数据段和堆栈段。操作系统管理堆栈段 - 通常每个正在运行的线程都有一个堆栈,并且在分配堆栈时大小是固定的,尽管我模糊地回忆起Linux可能会调整堆栈大小。程序通过(直接或间接)调用brk() / sbrk()来管理数据段,以更改数据段结束的位置。所有静态数据(程序代码等)都是固定大小的,因此在调整数据段大小时唯一变大的部分是堆空间。
-
@Daniel我不确定操作系统是否管理堆栈。您可以使用推/弹指令轻松移动堆栈指针,这些指令不是系统调用。当您希望单独的线程具有单独的堆栈时,堆栈都必须在堆上的巨大块中动态分配。我记得这是一个问题,因为如果你不知道你需要多少线程,那么很难在线程堆栈中平均分配堆。
-
@Brian:Daniel说操作系统管理堆栈段,而不是堆栈指针...非常不同的东西。重点是堆栈段没有sbrk / brk系统调用 - Linux会在尝试写入堆栈段末尾时自动分配页面。
-
而Brian,你只回答了问题的一半。另一半是当没有空间可用时尝试推入堆栈时会发生的情况......您会遇到分段错误。