问题
内存空间
说一下CPP用户空间内存分区?
介绍一下操作系统虚拟内存分区
什么时候会发生段错误?
堆区和栈区的区别?
malloc原理?
知识
内存布局
进程内存布局
内核空间
用户空间
栈:栈用于维护函数调用的上下文,离开了栈函数调用就没法实现,在10.2节中将对栈作详细的介绍。栈通常在用户空间的最高地址处分配,通常有数兆字节的大小。
堆:堆是用来容纳应用程序动态分配的内存区域,当程序使用 malloc 或 new 分配内存时,得到的内存来自堆里。堆会在10.3节详细介绍。堆通常存在于栈的下方(低地址方向),在某些时候,堆也可能没有固定统一的存储区域。堆一般比栈大很多,可以有几十至数百兆字节的容量。
可执行文件映像:这里存储着可执行文件在内存里的映像,在第 6 章已经提到过,由装载器在装载时将可执行文件的内存读取或映射到这里。在此不再详细说明。
保留区: 保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称:例如大多数操作系统中,极小的地址通常都是不允许访问的,如 NULL,C 语言将无效指针赋值为 0 也是这个考虑。
动态链接库映射区: 这个区域用于映射装载的动态链接库。在 Linux 下,如果可执行文件依赖其它共享库,那么系统就会为它在从 0x40000000 开始的地址分配相应的空间,并将共享库载入该空间。
剩下的还有以下几部份组成:
(1)代码段
(2)初始化数据段(数据段)
(3)未初始化数据段(BSS 段)
代码段(text):代码段中存放可执行的指令,在内存中,为了保证不会因为堆栈溢出被覆盖,将其放在了堆栈段下面(从上图可以看出)。通常来讲代码段是共享的,这样多次反复执行的指令只需要在内存中驻留一个副本即可,比如 C 编译器,文本编辑器等。代码段一般是只读的,程序执行时不能随意更改指令,也是为了进行隔离保护。
初始化数据段:有时就称之为数据段。数据段是一个程序虚拟地址空间的一部分,包括一全局变量和静态变量,这些变量在编程时就已经被初始化。数据段是可以修改的,不然程序运行时变量就无法改变了,这一点和代码段不同。
数据段可以细分为初始化只读区和初始化读写区。这一点和编程中的一些特殊变量吻合。比如全局变量 int global n = 1就被放在了初始化读写区,因为 global 是可以修改的。而 const int m = 2 就会被放在只读区,很明显,m 是不能修改的。
未初始化数据段:有时称之为 BSS 段,BSS 是英文 Block Started by Symbol 的简称,BSS 段属于静态内存分配。存放在这里的数据都由内核初始化为 0。未初始化数据段从数据段的末尾开始,存放有全部的全局变量和静态变量并被,默认初始化为 0,或者代码中没有显式初始化。比如 static int i; 或者全局 int j; 都会被放到BSS段。
段错误
Q:我写的程序常常出现“段错误(segment fault)”或者“非法操作,该内存地址不能read/write”的错误信息,这是怎么回事?
A:这是典型的非法指针解引用造成的错误。当指针指向一个不允许读或写的内存地址,
而程序却试图利用指针来读或写该地址的时候,就会出现这个错误。在Linux或Windows的内存布局中,有些地址是始终不能读写的,例如0地址。还有些地址是一开始不允许读写,应用程序必须事先请求获取这些地址的读写权,或者某些地址一开始并没有映射到实际的物理内存,应用程序必须事先请求将这些地址映射到实际的物理地址(commit),之后才能够自由地读写这片内存。当一个指针指向这些区域的时候,对它指向的内存进行读写就会引发错误。造成这样的最普遍原因有两种:
1.程序员将指针初始化为NULL,之后却没有给它一个合理的值就开始使用指针。
2. 程序员没有初始化栈上的指针,指针的值一般会是随机数,之后就直接开始使用指针。
因此,如果你的程序出现了这样的错误,请着重检查指针的使用情况。
栈
在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push,也可以将已经压入栈中的数据弹出(出栈, pop),但栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO),多多少少像叠成一叠的书:先叠上去的书在最下面:因此要最后才能取出。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使栈减小。
在经典的操作系统里,栈总是向下增长的。
在i386下,栈顶由称为 esp 的寄存器进行定位。压栈的操作使栈顶的地址减小,弹出的操作使栈顶地址增大。
这里栈底的地址是 0xbffff,而 esp 寄存器标明了栈顶,地址为 0xbifff4。
在栈上压入数据会导致 esp 减小,弹出数据使得 esp 增大。
栈在程序运行中具有举足轻重的地位。最重要的,栈保存了一个函数调用所需要的维护信息,这常常被称为堆栈帧(Stack Frame)或活动记录(Activate Record),堆栈帧一般包括如下几方面内容:
1、函数的返回地址和参数。
2、临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
3、保存的上下文:包括在函数调用前后需要保持不变的寄存器。
堆
光有栈,对于面向过程的程序设计还远远不够,因为栈上的数据在函数返回的时候就会被释放掉,所以无法将数据传递至函数外部。而全局变量没有办法动态地产生,只能在编译的时候定义,有很多情况下缺乏表现力,在这种情况下,堆(Heap)是一种唯一的选择。
堆是一款巨大的内存空间,常常占据整个虚拟空间的绝大部分,在这片空间里,程序可以请求一块连续的内存,并自由地使用,这块内存在程序主动放弃之前都活一直保持有效,下面是一个申请堆空间最简单的例子:
int main()
{
char* p = (char*) malloc(233);
free(p);
return 0;
}
在第 3 行用 malloc 申请了 233 个字节的空间之后,程序可以自由地使用这 233个字节,直到程序用free函数释放它。
malloc原理
malloc实现
那么 malloc 到底是怎么实现的呢?
有一种做法是,把进程的内存管理交给操作系统内核去做,既然内核管理着进程的地址空间,那么如果它提供一个系统调用,可以让程序使用这个系统调用申请内存,不就可以了吗?
当然这是一种理论上可行的做法,但实际上这样做的性能比较差,原因在于每次程序申请或者释放堆空间都需要进行系统调用。
我们知道系统调用的性能开销是很大的,当程序对堆的操作比较频繁时,这样做的结果是会严重影响程序的性能的。
比较好的做法就是:程序向操作系统申请一块适当大小的堆空间,然后由程序自己管理这块空间,而具体来讲,管理着堆空间分配的往往是程序的运行库。
运行库相当于是向操作系统 “批发” 了一块较大的堆空间,然后 “零售” 给程序用。
当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。
当然运行库在向程序零售堆空间时,必须管理它批发来的堆空间,不能把同一块地址出售两次,导致地址的冲突。
glibc的malloc函数是这样处理用户的空间请求的:对于小于128KB的请求来说,它会在现有的堆空间里面,按照堆分配算法为它分配一块空间并返回;
对于大于128KB的请求来说,它会使用 mmap()函数为它分配一块匿名空间,然后在这个匿名空间中为用户分配空间。当然我们直接使用 mmap 也可以轻而易举地实现 malloc 函数:
由于 mmap 函数与 VirtualAlloc 类似,它们都是系统虚拟空间申请函数,它们申请的空间的起始地址和大小都必须是系统页大小的整数倍,对于字节数很少的请求如果也使用 mmap 的话,无疑是浪费大量的空间的,所以上述的做法仅仅是演示而已,并不具有实用性。
Linux进程堆管理
从本章的第一节可知,进程的地址空间中,除了可执行文件、共享库和栈之外,剩余的未分配的空间都可以被用来作为堆空间。Linux下的进程堆管理稍微有些复杂,因为它提供了两种堆空间分配的方式,即两个系统调用:一个是 brk()系统调用,另外一个是 mmap()。
brk()
brk()的作用实际上就是设置进程数据段的结束地址,即它可以扩大或者缩小数据段(Linux下数据段和BSS合并在一起统称数据段)。
如果我们将数据段的结束地址向高地址移动,那么扩大的那部分空间就可以被我们使用,把这块空间拿来作为堆空间是最常见的做法之一(我们还将在第 12 章详细介绍 brk 的实现).
Glibc 中还有一个函数叫 sbrk,它的功能与 brk 类似,只不过参数和返回值略有不同。sbrk 以一个增量(Increment)作为参数,即需要增加(负数为减少)的空间大小,返回值是增加(或减少)后数据段结束地址,这个函数实际上是对brk系统调用的包装,它是通过brk()实现的。
mmap()
一段虚拟地址空间,当然这块虚拟地址空间可以映射到某个文件(这也是这个系统调用的最初的作用),当它不将地址空间映射到某个文件时,我们又称这块空为匿名(Anonymous)空间。匿名空间就可以拿来做为堆空间。它的声明如下:
mmap 的前两个参数分别用于指定需要申请的空间的起始地址和长度,如果起始地址设置为0,那么 Linux 系统会自动挑选合适的起始地址。prot/flags 这两个参数用于设置申请的空间的权限(可读、可写、可执行)以及映射类型(文件映射、匿名空间等)。
最后两个参数是用于文件映射时指定文件描述符和文件偏移的,我们在这里并不关心它们。
参考
宇宙最全面的C++面试题v2.0 - 知乎 (zhihu.com)
【高频考点】c++内存模型分类。c++内存模型到底分成几部分?为什么网上答案参差不齐,一个视频带你搞明白_哔哩哔哩_bilibili