(转载)内存分配器101——写一个简单的内存分配器

news2024/9/20 6:04:36

文章目录

  • 前提
  • 正文
    • Malloc()
    • free()
    • calloc()
    • realloc()

前提

之前学习过手写一个简单的内存分配器,原文是英文的,当初学习的时候便将英文翻译为中文的,方便阅读,当然和原文相比少了点味道。今天整理资料的时候看到了自己的翻译,所以整理下发出来吧,本地的就直接删了。

正文

相关代码在这里:github.com/arjun024/memalloc

原文链接: https://arjunsreedharan.org/post/148675821737/memory-allocators-101-write-a-simple-memory

这篇文章是关于使用C写一个简单的内存分配器。我们将要实现malloc() , calloc(), realloc()free()

这是一篇初级文章,所以我不会详细说明每一个细节。同时实现的内存分配器并不快速和高效,我们不会调整分配的内存使其和 a page boundary 对齐,但是它可以工作,就是这样。

在我们开始之前,你得熟悉一个程序的内存布局。一个进程运行在自己的虚拟空间内,虚拟空间通常包括5部分:

  • Text section: 存放二进制指令。

  • Data section: 存放非0初始化的静态数据和全局变量。

  • BSS (Block Started by Symbol): 存放0初始化的静态数据和全局变量,程序中没有初始化的静态数据会被初始为0并存放到这里。

  • Heap: 存放动态分配的数据。

  • Stack: 存放局部变量,函数参数,指针等。

在这里插入图片描述

如你所见,栈和堆以相反的方向增长。

有时,data, bss 和 heap 统称为 “data segment(数据段)”,末端由一个名为 program break 的指针表明,也可以称为 brk。所以,brk 指针是堆的末尾。

如果想要在堆上分配更多的内存,我们需要请求系统增加 brk,同样的,释放内存的话,需要请求系统减少 brk。

假设运行在Linux上,我们可以使用系统调用函数sbrk()来操作brk。

sbrk(0): 返回brk现在的地址。

sbrk(x): 将brk增加x个字节,以此分配内存。

sbrk(-x): 将brk减少x个字节,以此释放内存。

失败的话,sbrk()返回(void*) -1

说实在的,在2015年sbrk()不是我们最好的选择。今天更好的选择可能是mmap()sbrk()不是线程安全的,它只能以LIFO(后进先出)的方式增加或减少。

然而,当申请的内存不太大时,glibc仍然使用sbrk()来实现malloc,所以我们使用sbrk()来实现一个简单的内存分配器。

Malloc()

malloc(size)函数分配size字节的内存,并返回分配内存的指针。

如下所示:

void *malloc(size_t size)
{
    void *block;
    block = sbrk(size);
    if (block == (void*) -1)
    {
        return NULL;
    }
    return block;
}

在上面的代码中,在指定size下我们调用sbrk()函数。

成功后,在堆上分配size个字节的内存。很简单,不是吗?难办的是释放这个内存。

free(ptr)函数通过指针ptr来释放内存块,这个指针必需由malloc(),calloc(),或者realloc()函数返回。

但是要释放一块内存,首先要知道这块内存的大小。在现有的设计方案中,这是不可能的,因为没有对size进行保存。因此要找到一个方来保存分配的内存的size。

此外,操作系统提供的堆内存是连续的,所以我们只能释放堆末尾的内存,不能释放中间的内存。将堆想象为一条长面包,你可以在一端拉伸它,但你必需保持它是一个整体。

为了解决要释放的内存不在堆尾的问题,我们对free memory 和 release memory 做以区分。

从现在起,free memory 不是意味着要将这块内存释放,并还给OS。它仅仅意味着我们将这块内存标记为free,标记为free的内存可以在后面的malloc()函数调用时重新使用。因为不在堆尾的内存不可以被release,所以这是我们唯一的方法。

现在,为每一个分配的内存块存储两个东西:

  1. size
  2. Whether a block is free or not-free?

为了储存这个信息,为每一个新分配的内存块添加一个header。

struct header_t{
    size_t size;
    unsigned is_free;
};

想法很简单。当程序申请size个字节的内存时,计算total_size = header_size + size,并且调用sbrk(total_size)。我们使用sbrk()返回的内存空间来容纳header和实际的内存块。header是内部管理,对于程序调用者来说是完全不可见的。

现在,每一个内存块看起来像这样:

在这里插入图片描述

我们不能确保分配的内存块是连续的,想象调用程序有一个外部的sbrk(),或者在我们的内存块中有一段mmap()的内存。我们还需要一种方法来遍历我们的内存块,因此可以追踪到分配的内存,将其放入list中,header 看起来像这样:

struct header_t {
	size_t size;
	unsigned is_free;
	struct header_t *next;
};

内存块的链式表达像这样:

在这里插入图片描述

现在让我们使用一个union和一个16字节的stub变量来包装整个header,这会导致header末尾的地址16字节对齐。回想一下,一个union的大小是它其中最大的数据成员的大小。header 的结尾是内存块的开始,所以内存块的首地址和16字节对齐。

union header {
    struct {
        size_t size;
        unsigned is_free;
        union header *next;
    } s;
    ALIGN stub;
};

对于链表,我们有一个头指针和尾指针。

header_t *head, *tail;

为了防止多个线程同时访问内存,在这里我们使用一个锁。

有一个全局的锁,每次对内存进行操作之前,必需获取锁;操作完成后,必需释放锁。

pthread_mutex_t global_malloc_lock;

malloc()函数现在被修改为:

void *malloc(size_t size)
{
	size_t total_size;
	void *block;
	header_t *header;
	if (!size)
		return NULL;
	pthread_mutex_lock(&global_malloc_lock);
	header = get_free_block(size);
	if (header) {
		header->s.is_free = 0;
		pthread_mutex_unlock(&global_malloc_lock);
		return (void*)(header + 1);
	}
	total_size = sizeof(header_t) + size;
	block = sbrk(total_size);
	if (block == (void*) -1) {
		pthread_mutex_unlock(&global_malloc_lock);
		return NULL;
	}
	header = block;
	header->s.size = size;
	header->s.is_free = 0;
	header->s.next = NULL;
	if (!head)
		head = header;
	if (tail)
		tail->s.next = header;
	tail = header;
	pthread_mutex_unlock(&global_malloc_lock);
	return (void*)(header + 1);
}

header_t *get_free_block(size_t size)
{
	header_t *curr = head;
	while(curr) {
		if (curr->s.is_free && curr->s.size >= size)
			return curr;
		curr = curr->s.next;
	}
	return NULL;
}

解释下这段代码:

检查size是否为0,如果是,返回NULL

对于合法的size,先获取锁,然后调用get_free_block()函数——它会遍历链表,查找是否已经有被标记为free的内存块可以容纳指定的size。

如果一个比较大的free块被找到,将其标记为not-free,释放全局锁,并且返回块的指针。在这种情况下,header指针指向内存块的header部分。记住,我们要向外部隐藏header部分。当(header+1)时,它指向header末尾的字节,也是实际内存块的首字节。它被强制转为(void*)并返回。

如果没有足够大的free块,我们会通过调用sbrk()函数来扩展堆。堆会被扩展合适的大小以满足size和header的大小。因此,首先计算total_size = sizeof(header_t) + size;然后调用sbrk(total_size)

在从操作系统获得的内存里,我们首先要构造header。在C语言中,无须将void*强转为其它类型,它始终被安全地提升。这就是为什么没有显式地:header = (header_t*)block

我们更新next,head,tail指针。如前面所说的,对于调用者我们要隐藏header,因此返回(void*)(header+1)。同时释放全局锁。

free()

现在我们来看看free()函数怎么做。free()函数首先要判断要释放的内存是否位于堆的尾部。如果是,将其释放并还给操作系统;否则,我们将其标记为free,希望以后可以重新使用。

void free(void *block)
{
	header_t *header, *tmp;
	void *programbreak;

	if (!block)
		return;
	pthread_mutex_lock(&global_malloc_lock);
	header = (header_t*)block - 1;

	programbreak = sbrk(0);
	if ((char*)block + header->s.size == programbreak) {
		if (head == tail) {
			head = tail = NULL;
		} else {
			tmp = head;
			while (tmp) {
				if(tmp->s.next == tail) {
					tmp->s.next = NULL;
					tail = tmp;
				}
				tmp = tmp->s.next;
			}
		}
		sbrk(0 - sizeof(header_t) - header->s.size);
		pthread_mutex_unlock(&global_malloc_lock);
		return;
	}
	header->s.is_free = 1;
	pthread_mutex_unlock(&global_malloc_lock);
}

首先得到我们想要释放的块的header,我们需要得到一个指针。该指针位于块的后面,距离等于header的大小,所以haeder = (header_t*)block - 1

sbrk(0)返回brk的地址。为了检查要释放的block是否位于堆的末尾,首先找到现在block 的末尾,可以这样计算:(char*)block + header->s.size,然后和brk进行比较。

如果的确在末尾,我们减少堆的大小,并将空间还给操作系统。我们首先重置head和tail指针,然后计算要释放的内存量。总的大小是sizeof(header_t)+header->s.size,我们可以使用sbrk(-x)来释放。

如果要释放的内存不是链表的末尾,我们就简单将headeris_free字段设置为1。

calloc()

calloc(num,nsize)函数分配num个内存,每个大小为nsize字节,并初始化为0。

void *calloc(size_t num, size_t nsize)
{
	size_t size;
	void *block;
	if (!num || !nsize)
		return NULL;
	size = num * nsize;
	/* check mul overflow */
	if (nsize != size / num)
		return NULL;
	block = malloc(size);
	if (!block)
		return NULL;
	memset(block, 0, size);
	return block;
}

realloc()

realloc()改变指定内存块的大小。

void *realloc(void *block, size_t size)
{
	header_t *header;
	void *ret;
	if (!block || !size)
		return malloc(size);
	header = (header_t*)block - 1;
	if (header->s.size >= size)
		return block;
	ret = malloc(size);
	if (ret) {
		
		memcpy(ret, block, header->s.size);
		free(block);
	}
	return ret;
}

首先找到block的header,并察看header的size是否满足要求。如果满足,则什么都不用做。如果不满足,我们使用malloc()重新分配,然后使用memcpy()复制内容。之前的block会被释放。

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

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

相关文章

为什么越来越多的助贷中介公司做债务重组?

大家有没有注意到个现象?现在越来越多的助贷中介公司和专门做债务重组的公司一起“合作”了。有的是接了单转手给重组公司,有的则是亲自下场,用自有资金做起了重组的事情。为什么会这样呢?好端端的贷款中介不做,偏要蹚…

RabbitMQ练习(Remote procedure call (RPC))

1、RabbitMQ教程 《RabbitMQ Tutorials》https://www.rabbitmq.com/tutorials 2、环境准备 参考:《RabbitMQ练习(Hello World)》。 确保RabbitMQ、Sender、Receiver容器正常安装和启动。 rootk0test1:~# docker run -it --rm --name rab…

前端内存泄露案例与解决方案

什么是内存泄漏? 内存泄露(Memory Leaks):是指应用程序已经不再需要的内存,由于某种原因未返回给操作系统或者空闲内存池(Pool of Free Memory)。 内存泄露可能带来的问题:变慢、卡…

SAP LE学习笔记07 - MM与WM跨模块收货到仓库的流程中 如何实现 先上架再入库

上一章讲了LE中收货的一些特殊情况: 1,MM模块收货时,特别移动指标来标识的物料直接产生TO 2,MM中直接收货到仓库的固定Storage Bin(棚番)上 SAP LE学习笔记06 - MM与WM跨模块收货到仓库的流程中 带特别移动指标的物料也可以直接…

spring security 会话管理

一、简介 当浏览器调用登录接口登录成功后,服务端会和浏览器之间建立一个会话(Session)浏览器在每次发送请求时都会携带一个 Sessionld,服务端则根据这个 Sessionld 来判断用户身份当浏览器关闭后,服务端的 Session 并不会自动销毁&#xff0…

结构型设计模式-适配器(adapter)模式-python实现

设计模式汇总:查看 通俗示例 想象一下,你刚从国外带回一台最新的笔记本电脑,但是你发现它的电源插头是德标插头,而家里的电源插座是中式插座,这时怎么办呢?你需要一个电源适配器来将德标插头转换成中式插座…

“萌宠经济”全球化浪潮:宠物品牌如何利用TikTok达人破局出海

在全球“萌宠经济”不断升温的背景下,宠物品牌出海成为了重要的战略。随着市场的增长和消费者对宠物产品的需求增加,品牌需要寻找有效的方式进入新的海外市场。在这种情况下,TikTok平台的崛起和宠物达人的影响力成为了宠物品牌破局出海的关键…

数据结构与算法(快速基础C++版)

数据结构与算法(快速基础C版) 1. 基本概念第1章 绪论1.1 数据结构的研究内容1.2 基本概念和术语1.2.1 数据、数据元素、数据项和数据对象1.2.2 数据结构1.2.3 数据类型和抽象数据类型1.2.4 概念小结 1.3 算法和算法分析1.4 总结 2. 基本的数据结构第2章 …

【PyTorch常用库函数】一文教你快速上手torch.abs()函数:获取张量的绝对值

🎬 鸽芷咕:个人主页 🔥 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想,就是为了理想的生活! 引言 在深度学习领域,PyTorch是一个非常受欢迎的框架,它提供了丰富的库函数来支持各种复杂的计算任务。…

利用Leaflet.js创建交互式地图:多种形状单个区域绘制

引言 在地图应用开发中,用户经常需要对特定区域进行标识和规划。本文将深入探讨如何利用Vue.js的响应式特性与Leaflet.js的地图功能,打造一个支持多边形、矩形、圆形等多种形状绘制的交互式地图编辑器。 功能亮点 自由绘制多边形:用户可以自…

mysql基础语法——个人笔记

0 前言 以前学习且实践过mysql,但后来用得少,随着岁月更替,对其印象渐浅,所以每次需要用时,都会去再看一眼语法规范,然后才能放心动手操作 然而,在信息爆炸的时代,查语法规范时&am…

BUUCTF PWN wp--jarvisoj_level0

第一步 checksec ,该题为64位。 分析一下二进制保护机制: Arch: amd64-64-little 这个字段表示二进制程序的架构是 64 位的小端序的 x86-64 架构。小端序意味着低位字节存储在内存中的低地址上,高位字节存储在高地址上。RELRO: No RELRO …

迁移学习之领域自适应(domain adaptation)

比如有一堆有标注的训练数据,这些数 据来自源领域,用这些数据训练出一个模型,这个模型可以用在不一样的领域。在训练的时 候,我们必须要对测试数据所在的目标领域有一些了解。 随着了解的程度不同,领域自适应的方法也不…

(C++ STL)vector类的简单模拟实现与源码展示

vector类的简单模拟实现 一、前言二、vector 的成员变量三、vector 部分函数实现size、capacityreserveresizeinsert 与注意事项erase构造、析构、赋值拷贝 四、vector 源代码 以下代码环境为 VS2022 C。 一、前言 vector类 本质上就是数据结构中的顺序表。(可参考&#xff1…

【最新华为OD机试E卷】boos的收入(100分)-多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-E/D卷的三语言AC题解 💻 ACM金牌🏅️团队| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,…

4.负载均衡

文章目录 1.多级部署2.实现请求计数器3.负载均衡3.1服务端负载均衡3.2客户端负载均衡3.3自定义负载均衡3.4负载均衡策略3.5 LoadBalance 原理 4.部署实现 大家好,我是晓星航。今天为大家带来的是 负载均衡 相关的讲解!😀 1.多级部署 复制一…

C语言 | Leetcode C语言题解之第378题有序矩阵中第K小的元素

题目&#xff1a; 题解&#xff1a; bool check(int **matrix, int mid, int k, int n) {int i n - 1;int j 0;int num 0;while (i > 0 && j < n) {if (matrix[i][j] < mid) {num i 1;j;} else {i--;}}return num > k; }int kthSmallest(int **matri…

面试题小总结

一、为什么要使用Redis&#xff1f; 因为它是内存数据库&#xff0c;运行速度快因为它的工作线程是单线程&#xff0c;具有串行化&#xff0c;原子性具有IO模型&#xff0c;天生支撑高并发是kv模型&#xff0c;v具有多个数据结构具有本地方法&#xff0c;可以计算数据移动是二…

Mac用户必备:轻松添加Git SSH密钥全攻略

最近新买了一台MacBook笔记本&#xff0c;然后安装了git&#xff0c;准备下载代码&#xff0c;正好遇到配置GitHub的ssh密钥&#xff0c;记录一下整个操作流程。 操作步骤 在Mac上添加Git SSH密钥的步骤如下&#xff1a; 检查是否已有SSH密钥&#xff1a; 打开终端&#xff0…

Nginx: https解决安全问题

https原理 1 &#xff09;http协议存在的问题 数据使用明文传输&#xff0c;可能被黑客窃取 (需要信息加密)报文的完整性无法验证&#xff0c;可能被黑客篡改 (需要完整性校验)无法验证通信双方的身份&#xff0c;可能被黑客伪装 (需要身份认证) 2 ) https 原理 所谓 https,…