前言
这次写一个内存分配器的直接原因是因为在做一个实验的过程中需要用到多进程共享内存技术。而无论通过内存映射文件还是共享内存段,我们的多个进程能够直接共享到的是都是一大片共享内存段。而平时在一个进程内使用的Malloc
,New
等内存共享关键字都无法分配这一大片共享内存段里面的内存,这意味着链表等设计动态内存分配的技术都用不了,这个问题已经遇到过好多次了,这次就来实现一个不怎么考虑效率的内存分配器。在这里致谢知乎大佬团队——马龙的荒野求生的文章,贴上传送门
技术背景
我们知道,在上古开荒时期,我们的计算机体系结构采用的都是直接面向物理内存的寻址方式,这样做的话会有诸多不好,其中一个方面就是多道进程同时在一个地址空间内运行,势必会造成大量的外部碎片。但是,自从有高人提出了保护模式之后,计算机的内存管理就进入了现代化阶段。
大家都知道,在保护模式下,我们的进程可以独享4G的空间(X86),这时,由于内存的线性表特性,一个重要的问题就体现了出来:如何有效的管理这4G空间?对于一个程序设计者来说,内存可以看作两种类型:
- 在编译期确定的内存:这种内存的特点是其大小是在程序运行之前就确定的,并由编译器确认这种内存的起始地址。由于大小确定,我们可以通过算法来在虚拟空间中合理安排他们,一种较为简单和常见的手段就是线性排列;
- 在运行期确定的内存:这种内存的特点是其大小是不可预期的,对于这种内存,编译器无法安排其起始地址。必须将计算代价放置到运行期。一般的解决方案是:编译器在编译时保留一大段连续的内存空间,不作他用,并将这一块内存空间的地址通过约定的接口告诉编程者,编程者自己实现算法来管理这一段内存空间。我们把这种管理算法叫做内存分配器,又叫做堆分配器。这块内存称为堆区。
当然,由于内存分配器的固有实现缺点,恶意使用者可以通过特殊的输入来实现任意代码执行。并且内存分配器在任何一个程序中都是必不可少的,堆溢出经常能够拿到高权限。
如何做一个内存分配器
从罐头工厂到堆分配器
我们可以把一个内存分配器想象成一个罐头仓库管理员,而堆区想象成罐头仓库。但是和普通的罐头工程不同,我们的仓库有两点特殊:
- 我们有大大小小的罐头,即规程不统一;
- 罐头管理员无法知道罐头中是否存储有食物;
我们现在需要做两件事情: - 在一个大小固定的仓库中尽可能多的存储罐头,并确保空罐头不占用空间;
- 当一个罐头入库的时候,尽可能快的让其找到自己的位置;
实现中遇到的问题
让我们来理一理实现一个内存分配器需要解决哪些问题。
首先能想到的是,如何在没有链表等结构的情况下跟踪记录下每个内存块的空闲状态,由于堆分配器不可能预知用户需要的连续内存块大小,这就说明我们的方法是需要适应未知大小的内存块的。
需要注意的是,我们这里使用的是内存块这个术语,表示我们的管理单位是内存的一块区域,而不是一个一个字节。所以我们在初始化时,需要将这块内存首先切割成一个个块。
第二个需要解决的问题是如何分配内存。假设我们现在有三个空闲的内存块A、B、C。我们需要通过某种算法来选择某个空闲快来分配内存
第三个需要解决的问题是,如何解决剩下的内存?现在我们通过某个算法,确认了要从Block A划走一块内存给用户,那么我们应当如何解决剩下的内存块?是和其他空闲内存块合并还是单独放在这里?
第四个需要解决的问题是,当用户返还Buffer时,我们如何处理用户返还的空间,如果处理不当,很容易造成堆内有足够大的内存,但是用户无法申请成功。下面就是一个例子:用户在某时刻已申请了堆区中的所有空间,并在某个时间,决定归还两个区块A和B,并在下一时刻申请可以空间C。这三个空间块的大小为:
B
<
A
<
C
<
A
+
B
B<A<C<A+B
B<A<C<A+B。现假设堆分配器仅仅将用户归还的空间的空闲位置1。则该堆的部分布局应该为:
从图中可以看到,即使堆区中有足够的连续空间,但是从堆管理视角来看,这个空间实际上离散的。
让我们总结一下需要解决的问题:
- 如何在不使用诸如链表等动态结构的情况下将整个堆区组织起来;
- 决定一种方式来决定从哪一个空闲块中来划分内存块;
- 决定空闲块中剩下的内存如何组织;
- 当用户归还内存时,如何处理这块内存;
接下来我们会一一解决这些问题,当这些问题解决完了,我们的内存分配器也就完成了。
实现方案
首先需要说明的是,任何一种管理方案都有着其优点和缺点。这里仅仅解析其中一种方案。
问题一:采用什么样的结构来组织堆区
由于我们现在无法使用任何动态的数据结构,我们的老前辈采用一种特殊的数据结构来组织堆区:堆区是一个起始地址固定,整个长度不定的由若干个长度不定的数据块组成的线性结构,每个数据块的开头由长度固定的数据字段来描述这个数据块的长度及其属性(对于本实验来说,我们只需要一个Bit来标记本内存块是否被使用)。其结构如下所示:
使用这样结构,堆管理器可以根据堆的起始地址来知道整个堆区的内存布局情况。而堆的起始地址一般是存储在一个固定的地方,在OS的PCB中有一个数据字段来描述,对于我们自己实现的堆管理器,可以用一个全局变量来是实现。用C语言来描述,我们的每个内存块可以被描述为:
struct Block_Header {
DWORD Buffer_Size;//空闲的块大小
DWORD Block_Info;
};
除此之外,我们的堆管理器还需要一些总体信息来维护分配,这部分信息我们称之为堆首,同样的,堆首的任何信息不应该含有动态大小。我这里的堆首数据结构为:
struct Block_Header {
DWORD Buffer_Size;//空闲的块大小
DWORD Block_Info;
};
因此整个堆区的信息如下所示:
问题二:决定一种方式来决定从哪一个空闲块中来划分内存块
部分人可能对这个问题有着一些疑问,用户每次请求分配的时候直接从最开始进行扫描,一旦找到第一个能够分配的块,直接分配不就可以了吗?实际上,这种分配方式在堆管理器的最早期进行使用,但是这种方式会有一些问题:在堆的头部会留下许多小的空闲块,这些空闲块如此之小,以至于不能被大部分的用户请求所使用;除此之外,这样的方式还会造成效率的低下。
实际上,内存分配器无论是在内核还是用户态编程使用的极其平凡,许多大佬发明了许多算法来合理规划空闲块的分配情况。这些算法大致可以分为两类:基于顺序搜索和基于索引搜索。这些算法在任何一本合格的操作系统书中都有介绍,这里我选取循环搜索的策略来作为空闲块搜索策略。
循环搜索的策略为:堆管理器将堆区当作一个循环队列,并使用一个变量来标识上次搜索到的位置,当下一次申请请求到来时,堆管理器从上一次的搜索位置开始搜索,并将本次搜索到的位置记录在变量中。这种方法的思想是让堆区的全部内存得到一致的对待。另外,实际上这种算法也用在了OS的许多地方。
问题三、问题四:决定空闲块中剩下的内存如何组织、当用户归还内存时,如何处理这块内存
实际上这两个问题的核心都是在于:我们如何处理及何时处理多个连续的内存块。
如何处理连续的空闲内存块
先来看一下什么时候会出现原本不连续的空闲内存块变成了连续的空闲内存块:
图中的情况是最基本的情况。我们需要注意的一点是相邻空闲块可位于归还上侧或下侧。
我们先看一下如何处理这些连续的空闲块。对于位于下侧的空闲块,我们可以直接利用堆首的信息进行合并。但是对于那些位于上侧的空闲块,情况就变得复杂了:单向链表的性质使得我们无法获取上一个块的信息。如果不追求完美,我们完全可以只合并下侧的内存块,这样的堆分配器对于大内存的分配成功率肯定远远不如两侧都都合并的堆分配器,但是胜在实现简单,我这里就选择了这种方式。那现在让看一下如果要解决这个问题,我们需要做什么呢?很简单,将单项链表修改为双向链表,我们需要对内存块进行一点点改造,改造后的内存块如下所示:
通过尾部的长度固定的信息,我们就可以追寻到上一个Block的属性和大小了。
何时对连续的空闲内存块进行合并
我们有两个时刻可以对连续的内存空闲块进行处理:
- 在用户归还内存块时,检查相邻内存块并进行合并;
- 将内存块的合并推延到用户申请的时候;
我们选择第二个时刻,原因是这样可以减少消耗。让我们来假象一下如果在第一个时刻进行合并会发送什么情况?来举一个最极端的例子
从图中我们可以看到,我们刚刚合并好的内控又被切成了三个地方,我们刚刚做的事情变成了无用功。为了避免这种问题,我们应当选择第一种时刻进行合并。
代码
我们已经解决了一个简单的内存分配器应该解决的问题,接下来就是进行编码了。给出我的代码,其中的Memory allocator.cpp
是一个测试代码
Malloc.h
#pragma once
#ifndef IN
#define IN
#endif // !IN
#ifndef OUT
#define OUT
#endif // !OUT
namespace _Malloc {
typedef unsigned int DWORD;
typedef unsigned short WORD;
typedef unsigned char BYTE;
constexpr int ERROR_OK = 0;
constexpr int Unexcepted_Error = 1;
constexpr int Unable_Malloc = 2;
constexpr DWORD Block_Size = 1 << 31;
constexpr unsigned char Block_Buzy = 1;
constexpr unsigned char Block_Free = 0;
struct Block_Header {
DWORD Buffer_Size;//空闲的块大小
DWORD Block_Info;
};
struct Heap_Header {
DWORD Size;//整个堆的大小
Block_Header* Pre_Free_Block;
};
int Init_Heap(
void* Begin_Add, void* End_Add);
int malloc(
IN DWORD Size, OUT void ** Result_P
);
int _Free(
void* Pointer
);
}
Malloc.cpp
#include "Malloc.h"
#include<string.h>
#include<assert.h>
#include<vector>
using namespace std;
vector<_Malloc::DWORD> Used_Block;
_Malloc::DWORD Heap_Begin_Address = 0;
constexpr unsigned int Heap_Header_Size = sizeof(_Malloc::Heap_Header);
constexpr unsigned int Block_Header_Size = sizeof(_Malloc::Block_Header);
inline _Malloc::Heap_Header* Get_Heap_Entry() {
return (_Malloc::Heap_Header*)Heap_Begin_Address;
}
int _Malloc::Init_Heap(void* Begin_Add, void* End_Add)
{
memset(Begin_Add, 0, (DWORD)End_Add - (DWORD)Begin_Add);
_Malloc::Heap_Header* Heap_Header = (_Malloc::Heap_Header*)Begin_Add;
Heap_Header->Size = (DWORD)End_Add - (DWORD)Begin_Add;
Heap_Header->Pre_Free_Block = (_Malloc::Block_Header*)((DWORD)Begin_Add+Heap_Header_Size);
Heap_Begin_Address = (DWORD)Begin_Add;
_Malloc::Block_Header* Block_Header_Pt;
for (Block_Header_Pt = (_Malloc::Block_Header*)((DWORD)Begin_Add+Heap_Header_Size); (DWORD)Block_Header_Pt+_Malloc::Block_Size < (DWORD)Heap_Header+Heap_Header->Size- Heap_Header_Size; Block_Header_Pt+=_Malloc::Block_Size)
{
Block_Header_Pt->Buffer_Size = _Malloc::Block_Size - Block_Header_Size;
Block_Header_Pt->Block_Info = _Malloc::Block_Free;
}
Block_Header_Pt->Buffer_Size = (Heap_Header->Size % _Malloc::Block_Size)-Block_Header_Size;
Block_Header_Pt->Block_Info = Block_Free;
return _Malloc::ERROR_OK;
}
inline _Malloc::Block_Header* Get_Next_LP_Block_Header(_Malloc::Block_Header* Current) {
_Malloc::Heap_Header* Heap_Header = (_Malloc::Heap_Header*)Heap_Begin_Address;
if (Current->Buffer_Size + (_Malloc::DWORD)Current+Block_Header_Size > Heap_Begin_Address + Heap_Header->Size)
return (_Malloc::Block_Header*)(Heap_Begin_Address + Heap_Header_Size);
else {
return (_Malloc::Block_Header*)((_Malloc::DWORD)Current + Current->Buffer_Size + Block_Header_Size);
}
}
inline void MergeBlock(_Malloc::Block_Header& Block) {
//fixd:将递归修改为迭代
_Malloc::Block_Header* LP_Next_Block_Header = Get_Next_LP_Block_Header(&Block);
while (
((_Malloc::DWORD)LP_Next_Block_Header != (_Malloc::DWORD)(Heap_Begin_Address + Heap_Header_Size)) //循环到了堆首
&& LP_Next_Block_Header->Block_Info != _Malloc::Block_Buzy
&& Block.Block_Info != _Malloc::Block_Buzy
&& Block.Buffer_Size + LP_Next_Block_Header->Buffer_Size <= _Malloc::Block_Size)
{
Block.Buffer_Size += LP_Next_Block_Header->Buffer_Size + Block_Header_Size;
memset(LP_Next_Block_Header, 0, Block_Header_Size);
LP_Next_Block_Header = Get_Next_LP_Block_Header(&Block);
}
return;
}
int _Malloc::malloc(IN DWORD Size, OUT void ** Result_P)
{
_Malloc::Heap_Header* Heap_Header = Get_Heap_Entry();
_Malloc::Block_Header* LP_Origin_Block_Header = (_Malloc::Block_Header*)Heap_Header->Pre_Free_Block;
_Malloc::Block_Header* LP_Current_Block_Header = LP_Origin_Block_Header;
do
{
MergeBlock(*LP_Current_Block_Header);
//bug fix:修复了当Buffer_Size<Block_Header_Size时引起的 溢出
if ((long long )LP_Current_Block_Header->Buffer_Size-(long long)(Block_Header_Size) > (long long)Size && LP_Current_Block_Header->Block_Info==Block_Free) {
_Malloc::Block_Header* LP_Next_BlockHeader = (_Malloc::Block_Header*)((_Malloc::DWORD)LP_Current_Block_Header + Block_Header_Size + Size);
LP_Next_BlockHeader->Block_Info = Block_Free;
LP_Next_BlockHeader->Buffer_Size = LP_Current_Block_Header->Buffer_Size - Size-Block_Header_Size;
LP_Current_Block_Header->Buffer_Size = Size;
LP_Current_Block_Header->Block_Info = Block_Buzy;
Heap_Header->Pre_Free_Block = LP_Next_BlockHeader;
*Result_P = (void*)((DWORD)LP_Current_Block_Header + Block_Header_Size);
memset(*Result_P, 0, Size);
std::vector<DWORD>::iterator it = find(Used_Block.begin(), Used_Block.end(), (DWORD)*Result_P);
Used_Block.push_back((DWORD)*Result_P);
return _Malloc::ERROR_OK;
}
LP_Current_Block_Header = Get_Next_LP_Block_Header(LP_Current_Block_Header);
} while (LP_Current_Block_Header!=LP_Origin_Block_Header);
return _Malloc::Unable_Malloc;
}
int _Malloc::_Free(void* Pointer)
{
_Malloc::Block_Header* LP_Block_Header = (_Malloc::Block_Header*)((DWORD)Pointer - Block_Header_Size);
LP_Block_Header->Block_Info = _Malloc::Block_Free;
Used_Block.erase(find(Used_Block.begin(), Used_Block.end(), (DWORD)Pointer));
return _Malloc::ERROR_OK;
}
Memory allocator.cpp
// Memory allocator.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include"Malloc.h"
#include<vector>
#include<assert.h>
#include<stdlib.h>
#pragma warning(disable:4996)
using namespace std;
using namespace _Malloc;
unsigned char* Total_Buffer;
inline unsigned int GetRandom() {
_asm {
RDRAND eax;
}
}
char Tmp[1024];
int main()
{
FILE* fp = fopen(".\\output.txt", "w");
vector<_Malloc::DWORD> Used_Block;
Total_Buffer = new unsigned char[1 << 20];
_Malloc::Init_Heap(Total_Buffer, (void*)((unsigned int)Total_Buffer + (1 << 20)));
for (size_t i = 0; i < 100000; i++)
{
unsigned char* Buffer = NULL;
if (GetRandom() % 2) {
DWORD BufferLength = GetRandom() % 500+0x10;
if (_Malloc::malloc(BufferLength, (void**)&Buffer) != _Malloc::ERROR_OK)
assert(0);
sprintf_s(Tmp, 1024, "Malloc Buffer 0X%x\n", Buffer);
fputs(Tmp, fp);
Used_Block.push_back((DWORD)Buffer);
Block_Header* tmp = (Block_Header*)(Buffer - sizeof(Block_Header));
*(DWORD*)Buffer = BufferLength;
for (size_t j = 4; j < BufferLength-0x10; j++)
{
Buffer[j] = 0xaa;
}
tmp = (Block_Header*)(tmp->Buffer_Size + (DWORD)tmp + sizeof(Block_Header));
Block_Header* Current_Block = (Block_Header*)((DWORD)Buffer - sizeof(Block_Header));
assert(Current_Block->Buffer_Size == BufferLength && Current_Block->Block_Info == Block_Buzy);
}
else
{
if (Used_Block.size()) {
DWORD Buffer = *Used_Block.rbegin();
sprintf_s(Tmp, 1024, "Free Buffer 0X%x\n", Buffer);
fputs(Tmp, fp);
Block_Header* tmp = (Block_Header*)(Buffer - sizeof(Block_Header));
if (tmp->Buffer_Size > 516) {
fclose(fp);
}
if(false)
_Malloc::malloc(50, (void**)&Buffer);
for (int i = 0; i < tmp->Buffer_Size; i++)
{
((unsigned char*)Buffer)[i] = 0xbb;
}
Used_Block.pop_back();
_Malloc::_Free((void*)Buffer);
}
}
}
Heap_Header* LP_Heap_Header = (Heap_Header*)Total_Buffer;
DWORD Total_BlockNumber = 0;
DWORD Counted_Size = 0;
Block_Header* LP_Current_BlockHeader = (Block_Header*)((DWORD)LP_Heap_Header + sizeof(Heap_Header));
while (true)
{
Total_BlockNumber++;
Counted_Size += LP_Current_BlockHeader->Buffer_Size + sizeof(Block_Header);
LP_Current_BlockHeader = (Block_Header*)((DWORD)LP_Current_BlockHeader + LP_Current_BlockHeader->Buffer_Size+sizeof(Block_Header));
if (Counted_Size != LP_Heap_Header->Size)
break;
assert((LP_Current_BlockHeader->Buffer_Size + sizeof(Block_Header) + (DWORD)LP_Current_BlockHeader) < LP_Heap_Header->Size + (DWORD)LP_Heap_Header);
}
fclose(fp);
}