谈到malloc函数相信学过c语言的人都很熟悉,但是malloc底层到底做了什么又有多少人知道。

1、关于malloc相关的几个函数

关于malloc我们进入Linux man一下就会得到如下结果:

也可以这样认为(window下)原型:

extern void *malloc(unsigned int num_bytes);

头文件:

#include或者#include两者的内容是完全一样的

如果分配成功:则返回指向被分配内存空间的指针

不然返回指针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;

}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

c语言malloc函数程序,c语言 malloc函数详解相关推荐

  1. C语言0长度数组(可变数组/柔性数组)详解

    CSDN GitHub C语言0长度数组(可变数组/柔性数组)详解 AderXCoding/language/c/zero_length_array 本作品采用知识共享署名-非商业性使用-相同方式共享 ...

  2. time库是python中处理时间的标准库_python语言time库和datetime库基本使用详解

    今天是边复习边创作博客的第三天,我今年大二,我们专业开的有这门课程,因为喜欢所以更加认真学习,本以为没人看呢,看了后台浏览量让我更加认真创作,这篇博客花了2个半小时的时间,结合自己所学,所思,所想写作 ...

  3. R语言基于forestplot包可视化森林图实战详解:美化的森林图:自定义字体设置、置信区间、坐标轴(刻度、标签、范围)、无效线去除、水平线、辅助线、box形状、色彩等

    R语言基于forestplot包可视化森林图实战详解:美化的森林图:自定义字体设置.置信区间.坐标轴(刻度.标签.范围).无效线去除.水平线.辅助线.box形状.色彩等 目录

  4. R语言中如何计算C-Statistics?几种计算方法详解

    R语言中如何计算C-Statistics?几种计算方法详解 目录 R语言中如何计算C-Statistics? #包导入 #数据加载编码

  5. java注解式开发_JAVA语言之Spring MVC注解式开发使用详解[Java代码]

    本文主要向大家介绍了JAVA语言的Spring MVC注解式开发使用详解,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助. MVC注解式开发即处理器基于注解的类开发, 对于每一个定义的处 ...

  6. 区块链用哪种语言 Java_区块链开发用什么语言呢?区块链与编程语言的关系详解...

    原标题:区块链开发用什么语言呢?区块链与编程语言的关系详解 区块链的概念就随着比特币的大热开始逐渐进入公众视野,比特币的拥有者为了使其创造更高的财富,开始疯狂炒作,因而引发购买比特币大浪潮.然而等到比 ...

  7. java调用javascript函数_[Java教程]JavaScript函数的4种调用方法详解

    [Java教程]JavaScript函数的4种调用方法详解 0 2016-08-09 00:00:12 在JavaScript中,函数是一等公民,函数在JavaScript中是一个数据类型,而非像C# ...

  8. Python函数(函数定义、函数调用)用法详解

    函数 函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,不需要编写大量重复的代码. 函数可以提前保存起来,并给它起一个独一无二的名字,只要知道它的名字就能使用这段代码.函数还可以接 ...

  9. python源程序文件的扩展名_python程序文件扩展名知识点详解

    python程序文件的扩展名称是什么 python程序的扩展名有.py..pyc..pyo和.pyd..py是源文件,.pyc是源文件编译后的文件,.pyo是源文件优化编译后的文件,.pyd是其他语言 ...

  10. 定时器 槽函数没执行_Web服务器项目详解 07 定时器处理非活动连接(上)

    点击"两猿社" 关注我们 Web服务器详解目录 00 项目概述 01 线程同步机制包装类 02 半同步/半反应堆线程池(上) 03 半同步/半反应堆线程池(下) 04 http连接 ...

最新文章

  1. python怎么写文件-来看文件处理Python怎么写?
  2. html配置ss,#WEB安全基础 : HTML/CSS | 0x8CSS进阶
  3. python语言打印菱形_Python打印菱形
  4. 业界对物联网技术最常见的三大误区解读
  5. button hover逐渐变色_两当水库界桩@产品长久不变色 - 两当安全防护
  6. 网申倒计时4天 | DJI大疆秋招独家笔试攻略
  7. Python入门--列表元素的修改
  8. 我的一百个2019(四):2019,我被坑惨了
  9. 人工智能应用-手把手教你用Python硬件编程实现打开或关闭电灯泡
  10. SolidWorks2020小金球
  11. FPGA学习: Verilog刷题记录(16)
  12. VEH+硬件断点实现无痕HOOK
  13. 经典的测试开发面试题
  14. 【桶哥的问题——吃桶-简化版】【洛谷p2671】求和
  15. 速学计算机,新手电脑配置速成学习
  16. 车载以太网转换器 100/1000BASE-T1 转换器
  17. 海南信用社计算机试题,2018海南农村信用社考试 计算机 模拟试卷六答案
  18. 通信基础篇小项目-----简单网络画板的的实现
  19. 宝塔Linux面板使用
  20. 毕业设计 基于STM32单片机的二轮平衡小车

热门文章

  1. Windows 11 正式版最低配置要求来了,你的电脑支持吗?
  2. DDoS 保护、缓解和防御:8 个基本技巧
  3. shell脚本进入某个目录返回当前目录写法
  4. DNF40级冒险团与角色升级经验计算代码
  5. 数据集的划分,验证集参与训练了吗?
  6. MYSQL中的列转行
  7. Commonly Used Afx Functions
  8. TFLearn Input_data 与 fully_connected
  9. 进程的终止方式和进程的状态
  10. 耶鲁大学教授:研究生做科研的 11 条 “军规“