不止八股---CPP(进程内存布局,malloc原理)

问题

内存空间

说一下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)

程序员的自我修养 (豆瓣) (douban.com)

浅谈程序的内存布局 (zhihu.com)

【高频考点】c++内存模型分类。c++内存模型到底分成几部分?为什么网上答案参差不齐,一个视频带你搞明白_哔哩哔哩_bilibili

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值