你不知道的 malloc 内幕

news2024/11/23 8:30:23

你不知道的 malloc 内幕

  • 1. 引言:一个例子
    • 例1
    • 例2
  • 2. 基础概念
    • 2.1 内存管理发展过程
    • 2.2 虚拟存储器
    • 2.3 内存分配机制
    • 2.4 VMA
      • 2.4.1 进程的 VMA
      • 2.4.2 vma 分析
  • 3. 实例分析
    • 3.1 malloc 到底干了啥
    • 3.2 memset 的偷天换日
      • 3.2.1 虚拟地址转物理地址
      • 3.2.2 page fault
    • 3.3 free 的无作为
  • 4. 总结
  • 5. 扩展:内存越界

1. 引言:一个例子

例1

void main()
{
	char* buf = malloc(1024*1024*1024);
	free(buf);
}

上面这段代码很简单,就做了两件事:申请1G的内存,然后释放。

思考下面几个问题:

  1. malloc调用之后,系统一定会从储存器中分配1G内存吗?
  2. 如果系统分配了1G内存,但我们什么都没干,就直接把内存还回去。申请和释放肯定会存在性能开销,操作系统会这么“傻”吗?

例2

void main()
{
	char* buf = malloc(10);
	char a = buf[11];
	free(buf);
}

思考下面几个问题:

  1. 当程序运行到 “ char a = buf[11]” 时,会报错吗?
  2. 如果a = buf[n],n 逐渐增大,会发生什么?

我们将通过下面的内容来解答这些问题。

2. 基础概念

按照我们通常的理解,malloc 调用之后操作系统应该给我分配对应的内存,但实际上并不是这样的。操作系统做了很多“不为人知”的事情,让我们产生这样的错觉。

下面,我们先简单介绍操作系统内存管理的演变过程。

2.1 内存管理发展过程

内存管理的发展经过以下几个过程:

1、早期单任务系统
CPU 直接操作存储器。由于是单任务系统,整个存储器都是由单一任务独占,CPU 直接通过物理地址访问存储器的数据。

该方式的优点是实现简单;
缺点是不支持多任务。

2、固定内存分配
在存储器划分不同任务的地址空间,每个任务只能访问各自的地址空间。

该方式的优点是实现简单;
缺点是不适用任务过多的场景,存储器利用率较低,而且不利于程序移植

3、内存分页方式
对物理存储器做了一次软件抽象,抽象出来一个虚拟存储器的概念。

每一个任务存在一个独立的虚拟存储器,应用程序只需操作虚拟存储器。虚拟存储器到物理存储器的映射关系由操作系统来保证,不需要应用程序关心。

该方式的优点是存储器利用率高,用于程序易于开发和移植;
在这里插入图片描述

2.2 虚拟存储器

下面这个图,我们都应该比较熟悉。

一个32位操作系统的虚拟地址空间。低 3G 地址范围是用户空间,可以被用户态程序访问;高 1G 地址范围是内核空间,只能被内核访问。

用户空间从低地址往高地址分别为:

  • 代码区,存放代码段的区域
  • 静态存储区,存放静态数据的区域。比如全局变量等;
  • 堆区,动态内存区域。malloc/new 申请 buffer 的区域;
  • 共享区,mmap 或 动态库所在区域
  • 栈区,临时变量区域

在这里插入图片描述

2.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)。

2.4 VMA

VMA 是指在虚拟存储器上的一段虚拟内存,一个进程就是通过多个 VMA 构建了进程的虚拟地址空间。

2.4.1 进程的 VMA

如下图所示:

  • 每一个进程在内核中都有一个 mm_struct 对象,主要用于管理该进程所有内存;
  • mm_struct 结构体中存在一个 vm_area_struct 对象的链表,其中的每个节点是一个 VMA 区域(对应虚拟地址空间中的一段虚拟内存)

在这里插入图片描述

vm_area_struct 对象中几个关键变量:

  • vm_start/vm_end 表示该 VMA 在虚拟地址空间的起始/结束地址;
  • vm_flags 表示该 VMA 的可读、可写、可执行权限;
    举几个例子,代码段的权限是可读可执行(RX),堆的权限是可读可写(RW)

2.4.2 vma 分析

Linux 系统中可以通过如下命令来查看进程的所有 VMA,其中 xxx 为进程 pid

cat /proc/xxx/maps

运行结果如下图所示,我们主要关注前两列:

  • 第一列表示该 VMA 在虚拟地址空间的起始、结束地址,对应 vm_area_struct 对象中的vm_start/vm_end
  • 第二列表示该 VMA 的 RWX 权限,对应 vm_flags
    第一个 VMA (地址范围0x10000-0x11000)权限为RX,显然它是一个代码段。
    在这里插入图片描述

3. 实例分析

下面我们通过一个简单的程序来分析整个流程。

这个程序只有三行代码:

  • 申请10个字节的内存
  • 初始化10个字节为0
  • 释放这10个字节的内存
void main()
{
	char* buf = malloc(10);
	memset(buf, 0, 10);
	free(buf);
}

3.1 malloc 到底干了啥

我们通过上一章节的命令分别在代码执行的三个位置获取 VMA 信息。

void main()
{
	// 在此获取第一次 VMA
	char* buf = malloc(10);
	// 在此获取第二次 VMA
	memset(buf, 0, 10);
	free(buf);
	// 在此获取第三次 VMA
}

1)malloc 之前的 VMA 信息
第一次 VMA 信息
2)malloc 之后的 VMA 信息
在这里插入图片描述
相比于 malloc 前的 VMA 信息,增加了 0x13000-0x34000 地址范围的 VMA,这是一个堆空间的 VMA。
注意:这里 VMA 的大小,我们申请 10 个字节,但 VMA 远大于 10.

3) free 之后的 VMA 信息
在这里插入图片描述
我们可以发现,free 调用之后,堆 VMA 仍然存在

通过对 VMA 信息的分析,我们可以得到以下信息:

malloc 之后,进程创建了一个堆空间的 VMA,但是否申请物理内存还不知道。

3.2 memset 的偷天换日

实际上,malloc 并不会真正的从存储器中申请内存,申请内存的操作是在第一次使用的时候进行的。

现代操作系统存在两大特征:局部性原理Copy On Write (写时复制)

  • 局部性原理是指在某一段时间内,CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中;
  • Copy On Write 本质上也是基于局部性原理产生的,因为局部性原理,没有必要把进程所需的所有资源都加载,需要的时候再申请使用,这样也避免了资源的浪费。

因此,对于我们分析的例子来说,malloc 并不会从存储器分配内存,memset 才会。

CPU 执行 memset 函数的调用指令,可以简化为两个步骤

  • CPU 通过 MMU 将 memset 的起始地址,由虚拟地址转成物理地址;
  • MMU 触发 page fault (缺页异常),申请一个物理内存页

3.2.1 虚拟地址转物理地址

当代 CPU 内部一般都集成了一个 MMU 硬件,它的主要功能是将虚拟地址转换成物理地址。

一般,物理页和虚拟页都是以 4K 字节为单位进行划分的,所以对于任意一个虚拟地址,低12位数据就是页内偏移。

1)CPU 拿到一个虚拟地址,先将地址分成两部分:虚拟页号 p 和 页内偏移 d;
比如 addr=0x12345678,虚拟页号为 p=0x12345,页内偏移 d=0x678

2)MMU 拿到虚拟页号 p 后,会在页表中查找虚拟页对应的物理页帧;页表包含进程内所有虚拟页和物理页的映射关系以及该页的权限。

  • 如果在页表中能够查到虚拟页号 p 对应的物理页帧 f,则可以根据物理页帧 f 和页内偏移 d 找到对应的物理地址;
  • 如果页表中不能查到虚拟页号与物理页帧的对应关系或权限不对,MMU 会产生 page fault 中断;

在这里插入图片描述

3.2.2 page fault

发生缺页中断有 4 种情况:

  • 权限对,虚拟页和物理页映射关系不存在,对应图中第一种情况;
  • 权限对,虚拟页和物理页映射关系不存在,虚拟地址不在合法的 VMA 区域,对应图中第二种情况;
  • 权限不对,对应图中第三种情况;比如,改写一个const 变量。
  • 权限对,虚拟页和物理页映射关系存在,对应图中第四种情况;

在这里插入图片描述

3.3 free 的无作为

在 3.1 章节时,我们已经发现 free 调用之后,VMA 仍然存在,这也说明了 free 可能并不会触发操作系统删除 VMA。

用户态的 malloc/free 实际上是 c 库提供的接口,是对系统调用 brk/mmap 的二次封装。c 库内部实现会有缓存,malloc 调用分为两种情况:

  • 如果缓存 buffer 足够,c 库会从缓存中分配,而不会通过系统调用进入内核;
  • 如果缓存不够,c 库会通过系统调用进入内核,并为进程创建一个 VMA;

同理,c 库实现的 free 也不会马上把释放的内存还给内核, free 调用也可以分为两种情况:

  • 如果释放的 buffer 超过设定阈值,c 库会从通过系统调用进入内核,把内存还给内核,并删除 VMA;
  • 如果释放的 buffer 没有超过设定阈值,不会进入内核,只会在 c 库中把 buffer 标记为释放状态;

注意:c 库中缓存的阈值可以通过 mallopt 函数设置。

4. 总结

void main()
{
	char* buf = malloc(10);
	memset(buf, 0, 10);
	free(buf);
}

对于上面的代码:
1、malloc
只会创建一个 VMA,但不会真正申请 buffer;

2、memset
第一次使用 buffer 时,触发 page fault 申请物理内存

3、free
可能不会释放物理内存

5. 扩展:内存越界

内存越界一直都是程序员比较头疼的问题,尤其是在大型项目中,一旦出现内存越界,产生的现象可能会比较随机,无法定位。

通过 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);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/914222.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

线程池UncaughtExceptionHandler无效?可能是使用方式不对

背景 在业务处理中,使用了线程池来提交任务执行,但是今天修改了一小段代码,发现任务未正确执行。而且看了相关日志,也并未打印结果。 源码简化版如下: 首先,自定义了一个线程池 public class NamedThrea…

iMX6ULL QT环境配置 | CMake在Linux下的交叉编译环境搭建及使用

习惯了使用cmake,再也不想回到手写makefile的年代了。相比手写makefile,使用cmake则像是实现了机动化,管理项目工程的编译变得很简单了。况且cmake很流行,linux下的很多软件源码包,很多也都使用了cmake的方式编译。因此…

大数据课程K4——Spark的DAGRDD依赖关系

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解Spark的DAG; ⚪ 掌握Spark的RDD的依赖关系; ⚪ 了解Spark对于DAG的Stage的划分; 一、DAG概念 1. 概述 Spark会根据用户提交的计算逻辑中的RDD的转换和动作来生成RDD之间的依赖关…

Android JNI系列详解之AS创建Native C++项目

一、前提 Android Studio版本:Android Studio Electric Eel | 2022.1.1 Patch 1 二、创建Native C项目 1.新建项目 2.选择新建Native C项目 3.New Project 4.选择C标准库的支持版本 5.项目自带的默认生成的代码 6.buil.gradle中也自带了CMakeList的配置(…

详细了解G1、了解G1、G1垃圾收集器详解、G1垃圾回收器简单调优

4.详细了解G1: 4.1.一:什么是垃圾回收 4.2.了解G1 4.3.G1 Yong GC 4.4.G1 Mix GC 4.5.三色标记算法 4.6.调优实践 5.G1垃圾收集器详解 5.1.G1垃圾收集器 5.2.G1的堆内存划分 5.3.G1的运行过程 5.4.三色标记 5.4.1.漏标问题 5.5.记忆集与卡表 5.6.安全点与…

开发小技巧(逐步完善)

一、验证码 1)将 大小写字母 和 数字 存储在字符数组中 2)用随机数的方式生成随机码 3)采用字符串的方式存储验证码即可 import java.util.Random;public class TEST {public static void main(String[] args) {char[] chs new char[62];//…

小程序中的全局配置以及常用的配置项(window,tabBar)

全局配置文件和常用的配置项 app.json: pages:是一个数组,用于记录当前小程序所有页面的存放路径,可以通过它来创建页面 window:全局设置小程序窗口的外观(导航栏,背景,页面的主体) tabBar:设置小程序底部的 tabBar效果 style:是否…

存储IO路径:Linux下的“快递之旅”

相信大家都有过网购的经历,当我们在电商平台上浏览心仪的商品并下单时,快递小哥会负责将物品从商家手中送至我们手中。在这个过程中,快递小哥需要经过一系列的流程才能将物品准确送达。同样,在Linux系统中,当用户下发一笔读写操作时,这些数据也需要经过一系列的流程才能最…

uniapp 回退到指定页面 保存页面状态

uniapp 历史页面回退到指定页面。 getCurrentPages() 内容如下 let delta getCurrentPages().reverse().findIndex(item > item.route "pages/popularScience/daodi") if(delta-1){uni.navigateTo({url: /pages/popularScience/daodi,success: res > {},fa…

【数据结构练习】链表面试题锦集一

目录 前言: 1. 删除链表中所有值为key的节点 方法一:正常删除,头结点另外讨论 方法二:虚拟头结点法 方法三:递归 2.反转链表 方法一:双指针迭代 方法二:递归法解析: 3.链表的中间结点 方法…

动态规划:删除并获得点数

题目来源:删除并获得点数 题目分析 题目分析: 从题目中可以获取到的条件是,如果选择了i位置,那么就必须删除与i-1和i1的位置的值相同的所有的值。 既然要删除相同的值,那么我们可以想,要不要先排序&…

捕捉现货白银短期波动办法

有的投资者认为,在现货白银市场中,投资最适合的就是使用短线投资方法。因为短线投资可以充分发挥现货白银波动灵活的特点,尤其是对资金量少的投资者更是如此。确实,现货白银确实是比较适合进行短线投资,那么下面我们就…

图像检索,目标检测map的实现

一、图像检索指标Rank1,map 参考:https://blog.csdn.net/weixin_41427758/article/details/81188164?spm1001.2014.3001.5506 1.Rank1: rank-k:算法返回的排序列表中,前k位为存在检索目标则称为rank-k命中。 常用的为rank1:首…

Linux: 使用 ssh 连接其他服务器

通过ifconfig 查看要连接的服务器地址: ubuntuubuntu1804-0172:/media/sangfor/vdc$ ssh ubuntu192.168.11.49 输入要连接的服务器密码: ubuntua192.168.1149 s password: 连接服务器成功:

二、pikachu之SQL注入(2)

文章目录 1、delete注入2、http header注入3、布尔盲注4、时间盲注 4、宽字节注入 1、delete注入 (1)寻找传参页面,在删除留言的时候,发现是get传参; (2)判断是否存在注入点,命令&…

创建Ingress实例

部署deployment和service apiVersion: apps/v1 kind: Deployment metadata:creationTimestamp: nulllabels:app: webname: webnamespace: dalong spec:replicas: 2selector:matchLabels:app: webstrategy: {}template:metadata:creationTimestamp: nullspec:containers:- ima…

LabVIEW硬件在环仿真模拟电路故障分析和特征提取

LabVIEW硬件在环仿真模拟电路故障分析和特征提取 与数字电路相比,模拟电路故障分析是一项具有挑战性的任务。这主要是由于模拟分立元件的非线性特性,以及其他因素,包括噪声和内部可访问性的限制。参数故障和灾难性故障是模拟电路中发生的两种…

写之前的项目关于使用git remote -v 找不到项目地址的解决方案

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 一、报错解析1. 报错内容2. 报错翻译3. 报错解析(1)使用git branch来查看git仓库有几个分支(2)使用git remote -v&am…

Linux 发行版 Debian 宣布支持龙芯 LoongArch 架构

近期,龙芯发布了 3A6000 桌面处理器,芯片的性能又一次大幅度提升,成为国产芯片的又一里程碑。 同期,LoongArch 架构的生态建设也迅速提升,开源网络引导固件 iPXE、QQ Linux 版、摩尔线程等软硬件都官宣支持龙芯 Loong…

SpringBoot生成和解析二维码完整工具类分享(提供Gitee源码)

前言:在日常的开发工作当中可能需要实现一个二维码小功能,我参考了网上很多关于SpringBoot生成二维码的教程,最终还是自己封装了一套完整生成二维码的工具类,可以支持基础的黑白二维码、带颜色的二维码、带Logo的二维码、带颜色和…