【c语言】malloc函数详解

2023-11-17

谈到malloc函数相信学过c语言的人都很熟悉,但是malloc底层到底做了什么又有多少人知道。
1、关于malloc相关的几个函数
关于malloc我们进入Linux man一下就会得到如下结果:
这里写图片描述
也可以这样认为(window下)原型:

extern void *malloc(unsigned int num_bytes);

头文件:

#include<malloc.h>或者#include<alloc.h>两者的内容是完全一样的

如果分配成功:则返回指向被分配内存空间的指针
不然返回指针NULL
同时,当内存不再使用的时候,应使用free()函数将内存块释放掉。
关于:void*,表示未确定类型的指针,c,c++规定void*可以强转为任何其他类型的指针,关于void还有一种说法就是其他任何类型都可以直接赋值给它,无需进行强转,但是反过来不可以
malloc:
malloc分配的内存大小至少为参数所指定的字节数
malloc的返回值是一个指针,指向一段可用内存的起始位置,指向一段可用内存的起始地址,多次调用malloc所分配的地址不能有重叠部分,除非某次malloc所分配的地址被释放掉malloc应该尽快完成内存分配并返回(不能使用NP-hard的内存分配算法)实现malloc时应同时实现内存大小调整和内存释放函数(realloc和free)
malloc和free是配对的,如果申请后不释放就是内存泄露,如果无故释放那就是什么也没做,释放只能释放一次,如果一块空间释放两次或者两次以上会出现错误(但是释放空指针例外,释放空指针也等于什么也没做,所以释放多少次都是可以的。)
2、malloc和new
new返回指定类型的指针,并且可以自动计算所需要的大小。

int *p;
p = new int;//返回类型为int* ,分配的大小是sizeof(int)
p = new int[100];//返回类型是int*类型,分配的大小为sizeof(int)*100

而malloc需要我们自己计算字节数,并且返回的时候要强转成指定类型的指针。

int *p;
p = (int *)malloc(sizeof(int));

(1)malloc的返回是void*,如果我们写成了:p=malloc(sizeof(int));间接的说明了(将void转化给了int*,这不合理)
(2)malloc的实参是sizeof(int),用于指明一个整型数据需要的大小,如果我们写成p=(int*)malloc(1),那么可以看出:只是申请了一个一个字节大小的空间。
(3)malloc只管分配内存,并不能对其进行初始化,所以得到的一片新内存中,其值将是随机的。一般意义上:我们习惯性的将其初始化为NULL,当然也可以使用memset函数。
简单的说:
malloc函数其实就是在内存中找一片指定大小的空间,然后将这个空间的首地址给一个指针变量,这里的指针变量可以是一个单独的指针,也可以是一个数组的首地址,这要看malloc函数中参数size的具体内容。我们这里malloc分配的内存空间在逻辑上是连续的,而在物理上可以不连续。我们作为程序员,关注的是逻辑上的连续,其他的操作系统会帮着我们处理。
下面就来看看malloc具体是怎么实现的。
首先要了解操作系统相关的知识:
虚拟内存地址和物理内存地址
为了简单,现代操作系统在处理物理内存地址时,普遍采用虚拟内存地址技术。即在汇编程序层面,当涉及内存地址时,都是使用的虚拟内存地址。采用这种技术时,每个进程仿佛自己独享一片2N字节的内存,其中N是机器位数。例如在64位CPU和64位操作系统下每个进程的虚拟地址空间为264Byte。
这种虚拟地址空间的作用主要是简化程序的编写及方便操作系统对进程间内存的隔离管理,真实中的进程不太可能如此大的空间,实际能用到的空间大小取决于物理内存的大小。
由于在机器语言层面都是采用虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运行的实际上下文将虚拟地址转化为物理内存地址,才能实现对内存数据的操作。这个转换一般由一个叫MMU的硬件完成。
页与地址构成
在现代操作系统中,不论是虚拟内存还是物理内存,都不是以字节为单位进行管理的,而是以页为单位。一个内存页是一段固定大小的连续的连续内存地址的总称,具体到Linux中,典型的内存页大小为4096 Byte
所以内存地址可以分为页号和页内偏移量。下面以64位机器,4G物理内存,4K页大小为例,虚拟内存地址和物理内存地址的组成如下:
这里写图片描述
上面是虚拟内存地址,下面是物理内存地址。由于页大小都是4k,所以页内偏移都是用低12位表示,而剩下的高地址表示页号
MMU映射单位并不是字节,而是页,这个映射通过差一个常驻内存的数据结构页表来实现。现在计算机具体的内存地址映射比较复杂,为了加快速度会引入一系列缓存和优化,例如TLB等机制,下面给出一个经过简化的内存地址翻译示意图:
这里写图片描述
内存页与磁盘页
我们知道一般将内存看做磁盘的缓存,有时MMU在工作时,会发现页表表名某个内存页不在物理内存页不在物理内存中,此时会触发一个缺页异常,此时系统会到磁盘中相应的地方将磁盘页载入到内存中,然后重新执行由于缺页而失败的机器指令。关于这部分,因为可以看做对malloc实现是透明的,所以不再详述
真实地址翻译流程:
这里写图片描述
Linux进程级内存管理
2.2.1内存排布
明白了虚拟内存和物理内存的关系及相关的映射机制,下面看一下具体在一个进程内是如何排布内存的。
以Linux 64位系统为例。理论上,64bit内存地址空间为0x0000000000000000-0xFFFFFFFFFFFFFFF,这是个相当庞大的空间,Linux实际上只用了其中一小部分
具体分布如图所示:
这里写图片描述
对用户来说主要关心的是User Space。将User Space放大后,可以看到里面主要分成如下几段:
Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)
Data:这里存放的是初始化过的全局变量
BSS:这里存放的是未初始化的全局变量
Heap:堆,这是我们本文主要关注的地方,堆自底向上由低地址向高地址增长
Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存空间,本文不考虑这种情况,这个区域由高地址像低地址增长
Stack:栈区域,自高地址像低地址增长
Heap内存模型:
一般来说,malloc所申请的内存主要从Heap区域分配,来看看Heap的结构是怎样的。
这里写图片描述
Linux维护一个break指针,这个指针执行堆空间的某个地址,从堆开始到break之间的地址空间为映射好的,可以供进程访问,而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错
brk与sbrk
由上文知道,要增加一个进程实际上的可用堆大小,就需要将break指针向高地址移动。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:

int brk(void *addr);
void *sbrk(inptr_t increment);

brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置为errno为ENOMEM,sbrk成功时返回break移动之前所指向的地址,否则返回(void*)-1;
资源限制和rlimirt
系统为每一个进程所分配的资源不是无限的,包括可映射的空间,因此每个进程有一个rlimit表示当前进程可用的资源上限,这个限制可以通过getrlimit系统调用得到,下面代码获取当前进程虚拟内存空间的rlimit
其中rlimt是一个结构体

struct rlimit
{
    rlimt_t rlim_cur;
    rlim_t rlim_max;
};

每种资源有硬限制和软限制,并且可以通过setrlimit对rlimit进行有条件限制作为软限制的上限,非特权进程只能设置软限制,且不能超过硬限制
实现malloc
(1)数据结构
首先我们要确定所采用的数据结构。一个简单可行方案是将堆内存空间以块的形式组织起来,每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址
可以使用如下结构体定义一个block

typedef struct s_block *t_block;
struck s_block{
    size_t size;//数据区大小
    t_block next;//指向下个块的指针
    int free;//是否是空闲块
    int padding;//填充4字节,保证meta块长度为8的倍数
    char data[1];//这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta
};

(2)寻找合适的block
现在考虑如何在block链中查找合适的block。一般来说有两种查找算法:
First fit:从头开始,使用第一个数据区大小大于要求size的块所谓此次分配的块
Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块
两种方式各有千秋,best fit有较高的内存使用率(payload较高),而first fit具有较高的运行效率。这里我们采用first fit算法

t_block find_block(t_block *last,size_t size){
    t_block b = first_block;
    while(b&&b->size>=size)
    {
        *last = b;
        b = b->next;
    }
    return b;
}

find_block从first_block开始,查找第一个符合要求的block并返回block起始地址,如果找不到这返回NULL,这里在遍历时会更新一个叫last的指针,这个指针始终指向当前遍历的block.这是为了如果找不到合适的block而开辟新block使用的。

(3)开辟新的block
如果现有block都不能满足size的要求,则需要在链表最后开辟一个新的block。这里关键是如何只使用sbrk创建一个struct:

#define BLOCK_SIZE 24

t_block extend_heap{
    t_block b;
    b = sbrk(0);
        if(sbrk(BLOCK_SIZE+s)==(void*)-1)
        return NULL;
        b->size = s;
        b->next - NULL;
        if(last)
        last->next = b;
        b->free = 0;
        return b;
};

(4)分裂block
First fit有一个比较致命的缺点,就是可能会让更小的size占据很大的一块block,此时,为了提高payload,应该在剩余数据区足够大的情况下,将其分裂为一个新的block

void split_block(t_block b,size_t s)
{
    t_block new;
    new = b->data;
    new->size = b->size-s-BLOCK_SIZE;
    new->next = b->next;
    new ->free = 1;
    b->size = s;
    b->next = new;
}

(5)malloc的实现
有了上面的代码,我们就可以实现一个简单的malloc.注意首先我们要定义个block链表的头first_block,初始化为NULL;另外,我们需要剩余空间至少有BLOCK_SIZE+8才执行分裂操作
由于我们需要malloc分配的数据区是按8字节对齐,所以size不为8的倍数时,我们需要将size调整为大于size的最小的8的倍数

size_t align8(size_t s)
{
    if(s&0x7 == 0)
    return s;
    return ((s>>3)+1)<<3;
}
#define BLOCK_SIZE 24
void *first_block=NULL;
void *mallloc(size_t size)
{
    t_block b,last;
    size_t s;
    //对齐地址
    s = align8(size);
    if(first_block)
    //查找适合block
    last = first_block;
    b = find_block(&last,s);
    if(b)
    {
    //如果可以则分裂
    if((b->size-s)>=(BLOCK_SIZE + 8))
    split_block(b,s);
    b->free = 0;
    }
    else
    {
        //没有合适的block,开辟一个新的
        b=extend_heap(last,s);
        if(!b)
        {
            return NULL;
        }
        else
        {
            b=extend_heap(NULL,s);
            if(!b)
            {
                return NULL;
            }
            first_block = b;
        }
    }
    return b->data;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【c语言】malloc函数详解 的相关文章

  • QT中日期和时间类

    QT中日期和时间类 QDate QTime QDateTime QDate QDate类可以封装日期信息也可以通过这个类得到日期相关的信息 包括 年 月 日 构造函数 QDate QDate QDate QDate int y int m
  • java多线程和高并发系列二 & 缓存一致性协议MESI

    目录 CPU高速缓存 Cache Memory CPU为何要有高速缓存 带有高速缓存的CPU执行计算的流程 目前流行的多级缓存结构 多核CPU多级缓存一致性协议MESI MESI协议缓存状态 MESI状态转换 多核缓存协同操作 单核读取 双

随机推荐

  • mybatis-plus 根据指定字段 批量 删除/修改

    mybatis plus 提供了根据id批量更新和修改的方法 这个大家都不陌生 但是当表没有id的时候怎么办 方案一 手写SQL 方案二 手动获取SqlSessionTemplate 就是把mybatis plus 干的事自己干了 方案三
  • Linux_查看CPU信息、机器型号等硬件信息

    查看CPU信息 型号 cat proc cpuinfo grep name cut f2 d uniq c 8 Intel R Xeon R CPU E5410 2 33GHz 看到有8个逻辑CPU 也知道了CPU型号 cat proc c
  • LeetCode刷题之路:14.最长公共前缀

    编写一个函数来查找字符串数组中的最长公共前缀 如果不存在公共前缀 返回空字符串 示例 1 输入 flower flow flight 输出 fl 示例 2 输入 dog racecar car 输出 解释 输入不存在公共前缀 说明 所有输入
  • 前后端正常交互的流程

    普及一下前后端正常交互的流程 1 评审阶段 产品召集前后端进行需求评审 前后端各自捋清楚自己的业务量以及联调之间工作量 从而进行开发时间评估 2 开发准备阶段 前后端一起商量需求中需要联调的部分 进行接口的口头协议交流 3 接口定义阶段 前
  • 几种特征选择方法的比较,孰好孰坏?

    在本文中 重点介绍特征选择方法基于评估机器学习模型的特征重要性在各种不可解释 黑盒 及可解释机器学习方法上的表现 比较了CART Optimal Trees XGBoost和SHAP正确识别相关特征子集的能力 无论使用原生特征重要性方法还是
  • mcem r语言代码_R语言面向对象编程:S3和R6

    R语言面向对象编程 S3和R6 2017 06 10 0 R语言面向对象编程 S3和R6 一 基于S3的面向对象编程 基于S3的面向对象编程是一种基于泛型函数 generic function 的实现方式 1 S3函数的创建 S3对象组成
  • 【IT之路】微信小程序之程序精简

    上一篇我们了解了下微信小程序 这次我们来给新创建出来的小程序瘦身 这里保存了日志模块部分和index页面 一 主体文件精简 app js文件精简 app js App onLaunch 展示本地存储能力 const logs wx getS
  • arduino彩灯计时器电路_Arduino UNO 制作LED节日彩灯

    假日季节来临之际 我觉得利用Arduino和全彩LED灯条制作装饰彩灯将会很有趣 这些LED不仅会亮 而且具有多种不同的颜色 能够为您带来多彩的节日气氛 目录 1 LED灯条简介 2 如何连接LED灯条并接线 3 让我们来点亮LED灯吧 1
  • win10系统:VMware无法在Windows运行 解决方法

    win10系统 VMware无法在Windows运行该怎么办 最近使用win10系统的用户反应系统中出现了无法正常运行VMware的现象 在更新了win10系统之后 首先使用的是VMware14之后发现不兼容Windows10 1903 然
  • JAVA中Bean对象的注解

    java中通常对bean对象的注解 Column 和 Id 都是javax persistence包中的 bean 表明这个属性对应数据库中主键字段 Column 是表明这个属性对应数据库中某个字段
  • 这5种必知的大数据处理框架技术,你的项目到底应该使用其中的哪几种

    大数据是收集 整理 处理大容量数据集 并从中获得见解所需的非传统战略和技术的总称 虽然处理数据所需的计算能力或存储容量早已超过一台计算机的上限 但这种计算类型的普遍性 规模 以及价值在最近几年才经历了大规模扩展 本文将介绍大数据系统一个最基
  • Hadoop学习笔记之如何运行一个MapReduce程序

    Hadoop学习笔记之如何运行一个MapReduce程序 MapReduce可以分为两个阶段来处理 一个阶段为map 另一个阶段为reduce 每个阶段都有键值对的输入和输出参数 输入输出键值对的类型由程序决定 程序同样指定了两个函数 ma
  • 笔试面试常见函数编程实现

    目录 strcpy strstr memcpy memove Strcmp atoi 最好不要用其他封装的函数 strcpy C语言标准库函数 原型声明 extern char strcpy char dest const char src
  • 使用baiduspider实现一个异步爬虫

    代码仓库 https github com Kakaluoto asnyc spider 爬虫数据收集 1 各文件说明 img 存放爬取的图片 async spider 异步爬虫类 data collector 爬虫执行脚本 从这里启动 i
  • 京东搜索EE链路演进

    导读 搜索系统中容易存在头部效应 中长尾的优质商品较难获得充分的展示机会 如何破除系统的马太效应 提升展示结果的丰富性与多样性 助力中长尾商品成长是电商平台搜索系统的一个重要课题 其中 搜索EE系统在保持排序结果基本稳定的基础上 通过将优质
  • 多工程项目打包成功,运行失败(仅参考)

    打包了好几次 成功了 但到了线上运行了 当然这样看是看不出来的 看了error log 是找不到某个类 解决方案 多工程项目打包 最好用root的package 会打包所有工程
  • 【满分】【华为OD机试真题2023 JAVA&JS】取出尽量少的球

    华为OD机试真题 2023年度机试题库全覆盖 刷题指南点这里 取出尽量少的球 时间限制 1s 空间限制 32MB 限定语言 不限 题目描述 某部门开展Family Day开放日活动 其中有个从桶里取球的游戏 游戏规则如下 有N个容量一样的小
  • vue 中 的scroll插件vuescroll

    vuescroll 官网 https vuescrolljs yvescoding org zh guide configuration html detectresize 主要配置如下 在data里面 ops vuescroll mode
  • angular 根据当前日期显示本周,上周,本月,上月,本季度,本年度时间段

    angular Date处理模块 根据当前日期获取本周 上周 本月 上月等时间段 知识准备 Date对应方法 getDate 返回月中的第几天 从 1 到 31 getDay 返回星期几 0 6 setDate day 设置 Date 对象
  • 【c语言】malloc函数详解

    谈到malloc函数相信学过c语言的人都很熟悉 但是malloc底层到底做了什么又有多少人知道 1 关于malloc相关的几个函数 关于malloc我们进入Linux man一下就会得到如下结果 也可以这样认为 window下 原型 ext