void main()
{
char* buf = malloc(1024*1024*1024);
free(buf);
}
上面这段代码很简单,就做了两件事:申请1G的内存,然后释放。
思考下面几个问题:
void main()
{
char* buf = malloc(10);
char a = buf[11];
free(buf);
}
思考下面几个问题:
我们将通过下面的内容来解答这些问题。
按照我们通常的理解,malloc 调用之后操作系统应该给我分配对应的内存,但实际上并不是这样的。操作系统做了很多“不为人知”的事情,让我们产生这样的错觉。
下面,我们先简单介绍操作系统内存管理的演变过程。
内存管理的发展经过以下几个过程:
该方式的优点是实现简单;
缺点是不支持多任务。
该方式的优点是实现简单;
缺点是不适用任务过多的场景,存储器利用率较低,而且不利于程序移植
3、内存分页方式
对物理存储器做了一次软件抽象,抽象出来一个虚拟存储器的概念。
每一个任务存在一个独立的虚拟存储器,应用程序只需操作虚拟存储器。虚拟存储器到物理存储器的映射关系由操作系统来保证,不需要应用程序关心。
该方式的优点是存储器利用率高,用于程序易于开发和移植;
下面这个图,我们都应该比较熟悉。
从用户态的 malloc 调用到实际从存储器中申请到内存,需要经过3 层。我们从下到上介绍:
1、Buddy 系统
Buddy 系统是操作系统实现的物理内存分配机制。它最重要的特性是:
以页为单位,划分物理内存,一般一页为 4K 字节,所以 Buddy 申请内存最小粒度为 4K 字节。
2、C 库
由于 Buddy 申请内存以页为单位,如果我只申请 1 个字节,他也给我分配 4K 字节,那会造成浪费。因此在 Buddy 之上还需要对内存进行二次管理。
在用户态中,C 库就是这样一个内存二次管理者。它向下通过 brk/mmap 系统调用申请物理内存,向上层应用提供 malloc/free 接口。
3、中间的未知层
该层主要是通过 Buddy 系统申请物理内存,由 page fault 实现,请参考第 3.2 章。
疑问:这里的内存分配机制好像和前面的虚拟存储器没有什么关联啊?
答案是 VMA (Virtual Memory Area)。
如下图所示:
vm_area_struct 对象中几个关键变量:
Linux 系统中可以通过如下命令来查看进程的所有 VMA,其中 xxx 为进程 pid
cat /proc/xxx/maps
运行结果如下图所示,我们主要关注前两列:
下面我们通过一个简单的程序来分析整个流程。
这个程序只有三行代码:
void main()
{
char* buf = malloc(10);
memset(buf, 0, 10);
free(buf);
}
我们通过上一章节的命令分别在代码执行的三个位置获取 VMA 信息。
void main()
{
// 在此获取第一次 VMA
char* buf = malloc(10);
// 在此获取第二次 VMA
memset(buf, 0, 10);
free(buf);
// 在此获取第三次 VMA
}
1)malloc 之前的 VMA 信息
3) free 之后的 VMA 信息
通过对 VMA 信息的分析,我们可以得到以下信息:
malloc 之后,进程创建了一个堆空间的 VMA,但是否申请物理内存还不知道。
实际上,malloc 并不会真正的从存储器中申请内存,申请内存的操作是在第一次使用的时候进行的。
现代操作系统存在两大特征:局部性原理和 Copy On Write (写时复制)。
因此,对于我们分析的例子来说,malloc 并不会从存储器分配内存,memset 才会。
CPU 执行 memset 函数的调用指令,可以简化为两个步骤:
2)MMU 拿到虚拟页号 p 后,会在页表中查找虚拟页对应的物理页帧;页表包含进程内所有虚拟页和物理页的映射关系以及该页的权限。
发生缺页中断有 4 种情况:
在 3.1 章节时,我们已经发现 free 调用之后,VMA 仍然存在,这也说明了 free 可能并不会触发操作系统删除 VMA。
用户态的 malloc/free 实际上是 c 库提供的接口,是对系统调用 brk/mmap 的二次封装。c 库内部实现会有缓存,malloc 调用分为两种情况:
同理,c 库实现的 free 也不会马上把释放的内存还给内核, free 调用也可以分为两种情况:
注意:c 库中缓存的阈值可以通过 mallopt 函数设置。
void main()
{
char* buf = malloc(10);
memset(buf, 0, 10);
free(buf);
}
对于上面的代码:
1、malloc
只会创建一个 VMA,但不会真正申请 buffer;
2、memset
第一次使用 buffer 时,触发 page fault 申请物理内存
3、free
可能不会释放物理内存
内存越界一直都是程序员比较头疼的问题,尤其是在大型项目中,一旦出现内存越界,产生的现象可能会比较随机,无法定位。
通过 3.2.2 章,我们可以知道发生内存越界会有两种可能:对应 page fault 的情况 2 和情况 4。
如果是出现情况2,和直接触发 segment fault,可以根据程序崩溃产生的 coredump 信息定位到具体代码位置。
如果是情况4,则问题很难定位排查。比如下面的代码,操作 buf1 时,错误的处理了 buf2 的数据。
void main()
{
char* buf1 = malloc(10);
char* buf2 = malloc(60);
.....
memset(buf1, 5, 100); // 内存越界
free(buf1);
free(buf2);
}
因篇幅问题不能全部显示,请点此查看更多更全内容