malloc底层实现

news2024/12/23 12:32:52

xv6实现的动态内存分配虽然只有不到100行代码,但却体现了动态内存分配的精髓,非常值得学习

xv6的内存布局

一谈到C语言的动态内存分配,就会想到以及malloc()和free()这两个函数。先来回顾一下XV6中的内存布局是怎样的,我们动态分配的内存就在下图的heap部分:在这里插入图片描述

总体思路

在看源码之前,我们先来想想自己会如何设计malloc()和free()。

设计malloc(),我们需要给malloc()传入一个参数表示需要申请的内存块的大小,同时返回申请成功的内存块的首地址,在xv6中的系统调用sbrk()刚好就能满足这一需求。

设计free(),我们需要给free()传入一个内存块地址,让他能自动地释放掉对应的内存块,然而如果仅仅只有内存块地址的信息是不够的,删多少就是一个问题,解决办法就是给每一个内存块添加size这一属性;同时释放掉的内存我们也需要处理,不然那块内存就会被浪费,解决办法是使用链表管理所有可用的内存块

在这里插入图片描述

照着上面的思路,我们再来看看源码是如何实现这些功能的。

umalloc.c:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"

// Memory allocator by Kernighan and Ritchie,
// The C programming Language, 2nd ed.  Section 8.7.

typedef long Align;

union header {
  struct {
    union header *ptr;
    uint size;
  } s;
  Align x;
};

typedef union header Header;

static Header base;
static Header *freep;

void
free(void *ap)
{
  Header *bp, *p;

  bp = (Header*)ap - 1;
  for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
    if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
      break;
  if(bp + bp->s.size == p->s.ptr){
    bp->s.size += p->s.ptr->s.size;
    bp->s.ptr = p->s.ptr->s.ptr;
  } else
    bp->s.ptr = p->s.ptr;
  if(p + p->s.size == bp){
    p->s.size += bp->s.size;
    p->s.ptr = bp->s.ptr;
  } else
    p->s.ptr = bp;
  freep = p;
}

static Header*
morecore(uint nu)
{
  char *p;
  Header *hp;

  if(nu < 4096)
    nu = 4096;
  p = sbrk(nu * sizeof(Header));
  if(p == (char*)-1)
    return 0;
  hp = (Header*)p;
  hp->s.size = nu;
  free((void*)(hp + 1));
  return freep;
}

void*
malloc(uint nbytes)
{
  Header *p, *prevp;
  uint nunits;

  nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;
  if((prevp = freep) == 0){
    base.s.ptr = freep = prevp = &base;
    base.s.size = 0;
  }
  for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
    if(p->s.size >= nunits){
      if(p->s.size == nunits)
        prevp->s.ptr = p->s.ptr;
      else {
        p->s.size -= nunits;
        p += p->s.size;
        p->s.size = nunits;
      }
      freep = prevp;
      return (void*)(p + 1);
    }
    if(p == freep)
      if((p = morecore(nunits)) == 0)
        return 0;
  }
}

不看函数,先看数据。

header

union header {
  struct {
    union header *ptr;	// 指向下一个内存块
    uint size;
  } s;
  Align x;
};

前面提到内存块的size和链表就是由header实现的,他是umalloc里的核心数据结构

freep

static Header base;
static Header *freep;

freep表示指向空闲链表任意节点的指针,base用于初始化freep

内存池模型

在看源码之前先看一下xv6动态内存分配时的内存池模型,这样更助于代码理解。
在这里插入图片描述
从图中可以看到:

  • 每一个申请的内存块都不是单独存在的,而是由header+数据载荷构成
  • 空闲的内存块会被加入空闲链表中管理(使用中的不会加入链表管理),并且空闲链表是一个环形链表
  • freep指向其中一个空闲内存块

下面的源码结合这张图理解效果最佳

malloc()

malloc()用于申请指定大小的内存块,并返回内存块首地址

void*
malloc(uint nbytes)
{
  Header *p, *prevp;
  uint nunits;
  // 计算申请nbytes空间需要多少Header结构(向上取整)
  nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;	
  // 初始化空闲链表
  if((prevp = freep) == 0){	
    base.s.ptr = freep = prevp = &base;	// 使用base进行初始化
    base.s.size = 0;
  }
  // 遍历空闲链表
  for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
  	// 内存池中是否有满足条件的内存块
    if(p->s.size >= nunits){
      if(p->s.size == nunits)
        prevp->s.ptr = p->s.ptr;
      else {
        p->s.size -= nunits;
        p += p->s.size;
        p->s.size = nunits;
      }
      freep = prevp;
      return (void*)(p + 1);
    }
    // 内存池没有则morecore申请新的内存块
    if(p == freep)
      if((p = morecore(nunits)) == 0)
        return 0;
  }
}

计算需要申请的内存大小

nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;

在umalloc中所有的内存块都是以header为单位进行分配的(也就是说我们的内存块占用空间是n header,而非n byte),目的是以header个单位对齐。

这里需要注意的是,在从byte转换为header时,两数相除需要向上取整才能容纳所有的数据。

向下取整:a/b eg.13 / 3 = 4
向上取整:(a-1)/b + 1 eg.(13 + 3 - 1) / 3 = 5

所以(nbytes + sizeof(Header) - 1)/sizeof(Header)计算出了数据载荷占用的header个数后,还需要+1加上一个header。

初始化空闲链表

 if((prevp = freep) == 0){	
    base.s.ptr = freep = prevp = &base;	// 使用base进行初始化
    base.s.size = 0;
  }

空闲链表只有在最开始会被初始化,判断条件就是freep为0的话就是还未初始化;紧接着会将base的地址赋值给prevp,freep(base是位于data段的一个静态变量,由编译器分配地址);同时base的地址还会赋值给base.s.ptr,环形链表就是在这里形成的。

遍历空闲链表

  for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
  	// 内存池中是否有满足条件的内存块
    if(p->s.size >= nunits){
      if(p->s.size == nunits)
        prevp->s.ptr = p->s.ptr;
      else {
        p->s.size -= nunits;
        p += p->s.size;
        p->s.size = nunits;
      }
      freep = prevp;
      return (void*)(p + 1);
    }
    // 内存池没有则morecore申请新的内存块
    if(p == freep)
      if((p = morecore(nunits)) == 0)
        return 0;
  }

遍历空闲链表中的空闲内存块,查看空闲链表(内存池)中是否有满足条件(空闲内存块大小>= 待申请内存块大小)的内存块:

  • 有则将此内存块拿去用,这里涉及链表节点以及大小的调整。
  • 没有则通过morecore申请新的内存块。因为他是从freep的后一个节点开始遍历的,遍历一圈结束时也就是p == freep的时候。

我们不会将以前的内存块彻底删除,而是将他留下,作为内存池的一部分重复使用。这样的好处是减少了申请新内存块的开销:

  • 空间上,不需要额外的空间;
  • 时间上:免去了物理地址到虚拟地址空间的映射,特别是对于lazy allocation的场合,频繁的申请新的地址空间特别耗时。

morecore()

morecore()用于申请新的内存块

static Header*
morecore(uint nu)
{
  char *p;
  Header *hp;

  if(nu < 4096)
    nu = 4096;						// 设置最小分配单位为4096个header
  p = sbrk(nu * sizeof(Header));	// 分配新的内存块
  if(p == (char*)-1)
    return 0;
  hp = (Header*)p;
  hp->s.size = nu;
  free((void*)(hp + 1));			// 将新内存加入空闲链表
  return freep;
}

morecore()会设置最小的分配单位为4096个header,可能是尽量分配大空间,因为小空间的内存块更容易产生内存碎片(猜测);然后通过sbrk()系统调用申请新的内存块,最后将新申请的这个内存块通过free()加入空闲链表,他会在malloc的下次循环被使用。

free()

free()用于释放内存块,同时还会合并空闲内存

void
free(void *ap)
{
  Header *bp, *p;
  // 获取内存块对应的header地址
  bp = (Header*)ap - 1;		
  // 遍历内存块的位置(位于哪两个空闲内存之间)
  for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)	
    if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
      break;
  // 检查并合并相邻的内存块
  if(bp + bp->s.size == p->s.ptr){
    bp->s.size += p->s.ptr->s.size;
    bp->s.ptr = p->s.ptr->s.ptr;
  } else
    bp->s.ptr = p->s.ptr;
  if(p + p->s.size == bp){
    p->s.size += bp->s.size;
    p->s.ptr = bp->s.ptr;
  } else
    p->s.ptr = bp;
  // 更新指向空闲内存块的指针
  freep = p;
}

free()首先遍历当前内存块的位置,它需要获得它相邻空闲链表节点的位置,这样才能进行之后的操作;然后它会检查当前内存块的大小是否满足合并的条件,分别和前后内存块比较,不能合并则重定向链表指向。

Linux中的malloc()实现

Linux中的动态内存分配策略有些许不一样:

  • malloc小于128k的内存,使用brk分配内存
  • malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0)

之所以使用brk来分配小内存,是因为brk是在堆空间这块连续区域上申请内存,频繁的brk申请大内存很容易导致内存的碎片化;而mmap不用担心这个问题,因为他是在堆和栈上的一片独立的区域分配的内存,不会有内存碎片的问题。

看起来mmap很有用,那为什么还需要brk来申请小内存呢?
这就是我们上面总结的知识了。brk并不是每次都需要申请新内存,而是可以从内存池中重用以前申请的内存,再次访问该内存不需要任何系统调用和缺页中断,开销极小,相比mmap每次都是重新申请要方便许多。

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

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

相关文章

FPGA实训报告DAY 1(Verilog HDL)

实习日志与总结 日期&#xff1a;2024 年 7 月 10 日 星期三 姓名&#xff1a;XXX 一、实习日志 上午 9:00 - 9:30 按时到达工位&#xff0c;参加部门早会&#xff0c;了解了今天的实习任务和目标&#xff0c;即初步学习 FPGA 简介和 Verilog 基础语法知识。 9:30 - 10:30…

数据结构(5.2_1)——二叉树的基本定义和术语

二叉树的基本概念 二叉树是n(n>0)个结点的有限集合: 或者为空二叉树&#xff0c;即n0&#xff1b;或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一颗二叉树。 特点:每个结点至多只有两颗字数&#xff1b;左子树不能颠倒(二叉树…

2024牛客暑期多校第一场

H 一开始以为考后缀和&#xff0c;耽误了一会。后面直接看样例猜结论&#xff0c;数字乘位置为对答案的贡献 #include<bits/stdc.h>using namespace std;#define int long long #define PII pair<int,int>const int M1000000007;void solve() {int n;cin>>…

git 代理错误拒绝连接

git 克隆项目拒绝连接 现象 Failed to connect to 127.0.0.1 port 15732: 拒绝连接 问题描述 代理错误解决方法 取消代理 git config --global --unset http.proxy

MyBatis源码中的设计模式1

1. 建造者模式的应用 建造者模式属于创建类模式&#xff0c;通过一步一步地创建一个复杂的对象&#xff0c;能够将部件与其组装过程分开。用户只需指定复杂对象的类型&#xff0c;就可以得到该对象&#xff0c;而不需要了解其内部的具体构造细节。《Effective Java》中也提到&…

springboot的全局异常处理

主要有两个异常注解&#xff0c;RestControllerAdvice和 ExceptionHandler(Exception.class) 案例 package com.lwy.exception;import com.lwy.pojo.Result; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotati…

类和对象的简述(c++篇)

开局之前&#xff0c;先来个小插曲&#xff0c;放松一下&#xff1a; 让我们的熊二来消灭所有bug 各位&#xff0c;在这祝我们&#xff1a; 放松过后&#xff0c;开始步入正轨吧。爱学习的铁子们&#xff1a; 目录&#xff1a; 一类的定义&#xff1a; 1.简述&#xff1a; 2…

飞睿智能UWB Tag蓝牙防丢器标签,宠物安全新升级,5cm精准定位测距不迷路

宠物早已成为许多家庭不可或缺的一员&#xff0c;它们用无条件的爱温暖着我们的心房&#xff0c;陪伴我们度过每一个平凡而温馨的日子。然而&#xff0c;随着宠物活动范围的扩大和外界环境的复杂多变&#xff0c;宠物走失的风险也随之增加。每一次出门遛弯&#xff0c;都像是心…

如何使用在线工具将手机相册中的图片转换为JPG格式

我们经常在手机相册中保存大量的图片&#xff0c;无论是家庭聚会的照片还是旅行的瞬间&#xff0c;每一幅图像都承载着珍贵的记忆。然而&#xff0c;有时候我们会遇到图片格式不兼容的问题&#xff0c;尤其是在需要将图片分享到特定平台或编辑时。 例如&#xff0c;某些社交平台…

PCIe EtherCAT实时运动控制卡PCIE464的IO与编码器读写应用

硬件介绍 PCIE464运动控制卡是正运动推出的一款EtherCAT总线脉冲型、PCIE接口式的运动控制卡&#xff0c;可选6-64轴运动控制&#xff0c;支持多路高速数字输入输出&#xff0c;可轻松实现多轴同步控制和高速数据传输。 PCIE464运动控制卡适合于多轴点位运动、插补运动、轨迹规…

Qt 多语言

记录Qt多语言的实现过程 目录 1.项目配置文件.pro配置 2.程序中的字符串用tr()封装 3.生成翻译文件 4.使用Qt语言家修改翻译文件 4.1使用Qt语言家打开 4.2 .更改文件配置 5. 生成qm文件 6.代码执行切换语言 6.1入口处 6.2 事件执行 0.效果 1.项目配置文件.pro配置 T…

观测云对接 Fluentd 采集业务日志最佳实践

概述 Fluentd 是一个开源数据收集器&#xff0c;专为简化日志管理和使日志数据更加易于访问、使用而设计。作为一个高度可扩展的工具&#xff0c;它能够统一数据收集和消费过程&#xff0c;使得构建实时分析的日志系统变得更加高效。 观测云目前已集成 Fluentd &#xff0c;可…

十、Java集合 ★ ✔(模块18-20)【泛型、通配符、List、Set、TreeSet、自然排序和比较器排序、Collections、可变参数、Map】

day05 泛型,数据结构,List,Set 今日目标 泛型使用 数据结构 List Set 1 泛型 1.1 泛型的介绍 ★ 泛型是一种类型参数&#xff0c;专门用来保存类型用的 最早接触泛型是在ArrayList&#xff0c;这个E就是所谓的泛型了。使用ArrayList时&#xff0c;只要给E指定某一个类型…

mybatisPlus和mybatis的版本冲突问题、若依换成MP、解决git无法推送、使用若依框架的swagger、以后再遇到团队项目应该怎么做。

20240716 一. mybatisPlus和mybatis的版本冲突问题1. 使用前的准备2. 我遇到了一个很严重的问题。3. 解决问题&#xff0c;好吧也没解决&#xff0c;发现问题&#xff01;&#xff01; 二、该死的git&#xff01;&#xff01;&#xff01;&#xff01;1. 解决无法在idea中使用g…

2024年公路水运工程施工企业安全生产管理人员证模拟考试题库及公路水运工程施工企业安全生产管理人员理论考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年公路水运工程施工企业安全生产管理人员证模拟考试题库及公路水运工程施工企业安全生产管理人员理论考试试题是由安全生产模拟考试一点通提供&#xff0c;公路水运工程施工企业安全生产管理人员证模拟考试题库是…

大数据平台之YARN

Hadoop YARN&#xff08;Yet Another Resource Negotiator&#xff09;是Hadoop 2.x引入的一个通用资源管理和作业调度框架&#xff0c;它将资源管理和作业调度/监控分离开来&#xff0c;从而提升了集群的资源利用率和可扩展性。YARN是Hadoop生态系统的核心组件之一&#xff0c…

Go 1.19.4 函数-Day 08

1. 函数概念和调用原理 1.1 基本介绍 函数是基本的代码块&#xff0c;用于执行一个任务。 Go 语言最少有个 main() 函数。 你可以通过函数来划分不同功能&#xff0c;逻辑上每个函数执行的是指定的任务。 函数声明告诉了编译器函数的名称&#xff0c;返回类型&#xff0c;和参…

持续集成06--Jenkins构建触发器

前言 在持续集成&#xff08;CI&#xff09;的实践中&#xff0c;构建触发器是自动化流程中不可或缺的一环。它决定了何时启动构建过程&#xff0c;从而确保代码变更能够及时地得到验证和反馈。Jenkins&#xff0c;作为业界领先的CI/CD工具&#xff0c;提供了多种构建触发器选项…

【C++编程】双端数组 deque 容器基本操作

&#x1f525; 特点&#xff1a;deque 头插、头删速度比 vector 快 deque 是一个双向队列&#xff08;double-ended queue&#xff09;&#xff0c;可以在队列的两端进行元素的插入和删除操作。 deque 涵盖了 queue&#xff08;队列&#xff09;、stack&#xff08;堆栈&#x…

一五六、Node+Vue 使用七牛上传图片,并配置个人域名

1. 七牛云ak/sk获取 点击注册&#x1f517;开通七牛开发者帐号如果已有账号&#xff0c;直接登录七牛开发者后台&#xff0c;点击这里&#x1f517;查看 Access Key 和 Secret Key 2. Node.js获取七牛token 安装qiniu npm install qiniu创建空间 Node获取token const qi…