[golang]Writing a Memory Allocator

 

转载https://zhuanlan.zhihu.com/p/51056407

本篇文章讲述了如何基于C实现一个简单的内存分配器。

这是一篇入门级别的文章,不会涉及过深的内存分配算法及其相关实现。我们的目的是实现一个可以正常工作的内存分配器,但是它的性能和内存的利用率都不是最优的。我们将会实现 malloc(),calloc(),realoc(),free() 四个函数。

翻译自: Memory Allocators 101 - Write a simple memory allocator

1. 什么是内存管理器

  • 进程是如何向内核申请内存的?
  • 已申请的内存是由谁来管理的,内核?库函数?还是应用程序?
  • heap上到底存在什么?
  • 堆内存可以扩展吗?

除了我们即将讨论的内存分配器,还有如下几种的存在:

  • dlmalloc – General purpose allocator
  • ptmalloc2 – glibc
  • jemalloc – FreeBSD and Firefox
  • tcmalloc – Google
  • libumem – Solaris

每个内存分配器都说自己可以快速分配内存、可扩展而且高效。但并不是所有的分配器都适用于我们的应用程序。像那些对内存异常渴求的应用程序来说,它的性能很大程度上依赖于内存分配器的性能。

2. 进程地址空间由什么构成

在实现内存分配器之前,需要先了解一下Linux的进程地址空间:

每个Linux都有一个地址空间,逻辑上由以下五部分组成:

  • text:包含了形成程序可执行代码的机器指令。它是由编译器和汇编器把C、C++或者其他程序的源码转化为机器码产生的。通常,代码段是只读的
  • data:包含了所有(已初始化的)的程序变量、字符串、数字和其他数据的存储
  • bss:包含了所有未初始化的静态数据,这些数据将被初始化为0
  • heap:包含了进程动态申请的内存
  • stack:包含了局部变量,函数参数,指针拷贝

正如我们图中所看到的,heap是向上增长的,stack是向下增长的。在有些时候,我们把data, bss 和 heap 统称为数据段。heap的顶部,被一个brk(break) 指针标示,在heap申请内存的时候,需要请求操作系统增加brk。同样,在heap上释放内存的时候需要请求操作系统减小brk。

在Linux /Unix操作系统中,有一个sbrk的系统调用可以让我们实现修改brk指针

  • 调用sbrk(0)可以返回当前brk指针的地址
  • 调用sbrk(x),brk指针增加x字节,内存被申请
  • 调用sbrk(-x),brk指针减小x字节,内存被释放

3.malloc()

malloc(size)函数申请 size字节的内存并且返回已申请内存的地址。

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

上述代码调用了sbrk() ,成功的话,新申请的内存将位于heap的顶部。

问题的关键在于:该如何释放这部分内存?

free(ptr) 函数可以释放ptr指针所指向的内存,该ptr指针由malloc(),calloc(),realloc() 等调用成功返回。

但是想要释放一块内存的的话,我们需要知道这块内存的地址(指针) 以及这块内存的大小(size)。在当前的情况下,我们必须要把已申请内存块的地址以及大小保存下来。

在解决这个问题之前,需要先明确 freeing memory 和 releasing memory的区别:

  • Freeing a block of memory does not necessarily mean we release memory back to OS. It just means that we keep the block marked as free. This block marked as free may be reused on a later malloc() call. Since memory not at the end of heap can't be released.

目前为止,对于每块申请的内存,我们需要知道:

  • 该内存块的size
  • 该内存块是否是free的

要实现上述要求,需要给每一块申请的内存添加一个header_t的数据结构:

struct header_t {
	size_t size;
	unsigned is_free;
};

另外,我们还无法保证我们使用malloc() 申请的内存是连续的。假设进程调用了其他的sbrk() , 或者在我们申请的内存块之间有一段mmap() 映射的内存。我们需要一种方式来遍历我们所申请的内存,对于每块使用malloc()申请的内存,我们把它放在一个链表中。header_t 的数据结构和内存块的链表示意图如下所示:

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

保存链表的head和tail来对链表进行追踪:

struct header_t *head, *tail;

使用一个简单的线程锁来保证我们的内存分配器是线程安全的。

pthread_mutex_t global_malloc_lock;

基于以上讨论,新的malloc()函数实现如下:

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

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

我们分析以下上述代码:

如果所申请内存的size为0,返回NULL
Acquire Lock,遍历内存块组成的链表,查找是否有满足size并且标记为free的内存块。
如果找到了,将该内存块标记为non-free,Release Lock,返回该内存快的地址。
如果没有找到符合条件的内存块,那么我们从heap的顶部重新申请新的内存块,内存的大小:  total_size = sizeof(struct header_t) + size,然后调用 sbrk(total_size)修改brk指针。

4.free()

free() 首先要确定当前要释放的内存块是否位于heap的顶部(链表的尾部)。如果是的话, release it to the OS. 如果不是的话,标记为 free,便于以后重用。
void free(void *block)
{
	struct header_t *header, *tmp;
	void *programbreak;

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

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

5. 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;
}
 

6. realloc()

realloc() 将当前申请的内存块的大小修改为指定的size。
 void *realloc(void *block, size_t size)
{
	struct header_t *header;
	void *ret;
	if (!block || !size)
		return malloc(size);
	header = (struct header_t*)block - 1;
	if (header->size >= size)
		return block;
	ret = malloc(size);
	if (ret) {
		
		memcpy(ret, block, header->size);
		free(block);
	}
	return ret;
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值