一个内存管理 C 语言部分讲,UNIX部分讲,Linux部分还讲,死磕到底!!
typedef struct mem_control_block
{
size_t size; //本块大小
bool free; //空闲状态
struct mem_control_block *next; //后块指针
}MCB;
MCB *g_top = NULL; //栈顶指针
void* my_malloc (size_t size)
{
MCB* mcb;
for (mcb = g_top; mcb; mcb = mcb->next)
if (mcb->free && mcb->size >= size)
break;
if (! mcb)
{
mcb = sbrk (sizeof (MCB) + size);
if (mcb == (void*)-1)
return NULL;
mcb->size = size;
mcb->next = g_top;
g_top = mcb;
}
mcb->free = false;
return mcb + 1;
}
void* my_malloc (size_t size)
函数的形参size代表要申请的存储空间的字节数。 MCB* mcb;
for (mcb = g_top; mcb; mcb = mcb->next)
if (mcb->free && mcb->size >= size)
break;
遍历内存控制块链表,寻找大小足够的空闲块进行分配。其中,g_top是内存控制块链表第一个结点的指针,而以下代码:
if (mcb->free && mcb->size >= size)
break;
当mcb->free为真时,代表该内存控制块没有被进程使用。当mcb->size >= size为真时,代表该内存控制块内的空间字节数大于等于所申请的空间字节数。当两个条件同时为真时,表示该内存控制块内的空间可以被分配给进程使用。 if (! mcb)
{
mcb = sbrk (sizeof (MCB) + size);
if (mcb == (void*)-1)
return NULL;
mcb->size = size;
mcb->next = g_top;
g_top = mcb;
}
当内存控制块链表中找不到大小足够的空闲块进行分配时,分配新的足量内存并将其控制块压入链表栈。其中,以下代码:
mcb = sbrk (sizeof (MCB) + size);
if (mcb == (void*)-1)
return NULL;
使用sbrk函数分配新的足量内存。 mcb->free = false;
将mcb指向的内存控制块声明为已被分配。return mcb + 1;
mcb + 1等效于mcb + 1 * sizeof(MCB),即跳过内存控制块所占的空间,返回可以使用的空间地址。void my_free (void* ptr)
{
if (ptr)
{
MCB* mcb = (MCB*)ptr - 1;
mcb->free = true;
for (mcb = g_top; mcb->next; mcb = mcb->next)
if (! mcb->free)
break;
if (mcb->free)
{
g_top = mcb->next;
brk (mcb);
}
else if (mcb != g_top)
{
g_top = mcb;
brk ((void*)mcb + sizeof (MCB) + mcb->size);
}
}
}
void my_free (void* ptr)
函数形参为要释放的存储空间地址。上述代码中,以下代码:
if (ptr)
如果该地址为空,则什么都不做,直接退出。 MCB* mcb = (MCB*)ptr - 1;
(MCB*)ptr - 1等效于(MCB*)ptr – 1 * sizeof(MCB),即指向内存控制块的第一个字节的地址。 mcb->free = true;
将该内存控制块标记为空闲,即进程不再使用它了。for (mcb = g_top; mcb->next; mcb = mcb->next)
if (! mcb->free)
break;
遍历内存控制块链表。其中,g_top是内存控制块链表第一个结点的指针,而以下代码:
if (! mcb->free)
break;
在内存控制块链表中找到进程正在使用的第一个内存控制块结点。 if (mcb->free)
{
g_top = mcb->next;
brk (mcb);
}
当内存控制块链表中只有一个结点时,删除该节点。 else if (mcb != g_top)
{
g_top = mcb;
brk ((void*)mcb + sizeof (MCB) + mcb->size);
}
将靠近栈顶的连续空闲块及其内存控制块一并释放。#include
#include
#include
//内存控制块
typedef struct mem_control_block
{
size_t size; // 本块大小
bool free; // 空闲标志
struct mem_control_block* next; // 后块指针
} MCB;
//单向链表栈
MCB* g_top = NULL;//栈顶指针
//malloc 函数的实现
void* my_malloc (size_t size)
{
MCB* mcb;
for (mcb = g_top; mcb; mcb = mcb->next)
if (mcb->free && mcb->size >= size)
break;
if (! mcb)
{
mcb = sbrk (sizeof (MCB) + size);
if (mcb == (void*)-1)
return NULL;
mcb->size = size;
mcb->next = g_top;
g_top = mcb;
}
mcb->free = false;
return mcb + 1;
}
//free 函数的实现
void my_free (void* ptr)
{
if (ptr)
{
MCB* mcb = (MCB*)ptr - 1;
mcb->free = true;
for (mcb = g_top; mcb->next; mcb = mcb->next)
if (! mcb->free)
break;
if (mcb->free)
{
g_top = mcb->next;
brk (mcb);
}
else if (mcb != g_top)
{
g_top = mcb;
brk ((void*)mcb + sizeof (MCB) + mcb->size);
}
}
}
int main()
{
int *p = my_malloc(sizeof(int));
*p = 10;
printf("%d\n", *p);
my_free(p);
return 0;
}
输出结果:
10
参看:MALLOC_USABLE_SIZE 讲解
#include
size_t malloc_usable_size (void *ptr);
1
这个函数返回调用 malloc 后实际分配的可用内存的大小,如果ptr 为NULL,则为 0
#include
#include
int alloc_memory (char *p, int size)
{
p = (char*)malloc (size);
if (NULL == p)
perror ("malloc"), exit (1);
printf ("%d\n", malloc_usable_size (p));
}
int main (void)
{
char *p = NULL;
alloc_memory (p, 0);
alloc_memory (p, 10);
alloc_memory (p, 20);
return 0;
}
mcb = sbrk (sizeof (MCB) + size);
if (mcb == (void*)-1)
return NULL;
使用sbrk函数分配新的足量内存。#include
#include
#include
int main (void)
{
void *cur = sbrk (0);
printf ("cur 1 = %p\n", cur);
void *ptr = malloc (100);
void *ptr1 = malloc (100);
cur = sbrk (0);
printf ("cur 2 = %p\n", cur);
printf ("ptr = %p\n", ptr);
printf ("ptr1 = %p\n", ptr1);
free (ptr);
free (ptr1);
cur = sbrk (0);
printf ("cur 3 = %p\n", cur);
printf ("ptr = %p\n", ptr);
printf ("ptr1 = %p\n", ptr1);
return 0;
}
输出结果:
cur 1 = 0x8f17000
cur 2 = 0x8f38000
ptr = 0x8f17008
ptr1 = 0x8f17070
cur 3 = 0x8f38000
ptr = 0x8f17008
ptr1 = 0x8f17070
参看:UNIX再学习 – 内存管理
可以看出,malloc 所申请的空间的起始地址,第一次,比一开始的堆末尾地址向后移动了 8 个字节。第二次,后移了112个字节。这 8 个字节应该就是,用来记录管理信息的额外空间 – 分配块的长度、指向下一个分配块的指针等。而我们采用的是 8 字节对齐,申请 100 个字节,经过对齐,应为 104 个字节,再加上 8 个额外空间,即 112 个字节。大家都知道,进程需要使用的代码和数据都放在内存中,比放在外存中要快很多。问题是内存空间太小了,不能满足进程的需求,而且现在都是多进程,情况更加糟糕。所以提出了虚拟内存,使得每个进程用于3G的独立用户内存空间和共享的1G内核内存空间。(每个进程都有自己的页表,才使得3G用户空间的独立)这样进程运行的速度必然很快了。而且虚拟内存机制还解决了内存碎片和内存不连续的问题。为什么可以在有限的物理内存上达到这样的效果呢?
首先呢,提一个概念,交换空间(swap space),这个大家应该不陌生,在重装系统的时候,会让你选择磁盘分区,就比如说一个硬盘分几个部分去管理。其中就会分一部分磁盘空间用作交换,叫做swap space。其实就是一段临时存储空间,内存不够用的时候就用它了,虽然它也在磁盘中,但省去了很多的查找时间啊。当发生进程切换的时候,内存与交换空间就要发生数据交换一满足需求。所以啊,进程的切换消耗是很大的,这也说明了为什么自旋锁比信号量效率高的原因。
那么我们的程序里申请的内存的时候,linux内核其实只分配一个虚拟内存( 线性地址),并没有分配实际的物理内存。只有当程序真正使用这块内存时,才会分配物理内存。这就叫做延迟分配和请页机制。释放内存时,先释放线性区对应的物理内存,然后释放线性区;"请页机制"将物理内存的分配延后了,这样是充分利用了程序的局部性原来,节约内存空间,提高系统吞吐;就是说一个函数可能只在物理内存中呆了一会,用完了就被清除出去了,虽然在虚拟地址空间还在。(不过虚拟地址空间不是事实上的存储,所以只能说这个函数占据了一段虚拟地址空间,当你访问这段地址时,就会产生缺页处理,从交换区把对应的代码搬到物理内存上来)
左边是物理地址分配,与实际的CPU相关。4KB的这些都是一些控制器所占有,比如lcdc sd卡,他们的寄存器地址就是这样定死的。但是呢,我们要访问这些寄存器的时候,还是不能直接用,要使用内存管理的规则,使用虚拟地址去访问它,所以在驱动等内核程序中需要使用虚拟地址访问寄存器。如果有人直接使用物理地址访问寄存器,那么唯一的解释就是没有开mmu。不过这样你的进程就没有4G内存可以用了。
物理地址分布:
这是偷的别人的图啦,物理地址有896M直接映射到虚拟地址的内存空间,这是一一对应的映射,只有起始地址不一样,偏移是一样的。这个大小大多是固定的,哪怕你的内存超过一个G,太小了就另外说了。注意:用户区的代码也是放在这段物理地址里面的,就是说物理地址可以进行二次映射。但不管怎么样,这段物理地址都是受内核管理。当你内存很大的时候,超过896M时,剩余的那些内存怎么办呢?这多出来的叫做高端内存,如果你使用vmalloc申请空间,就会在高端内存中分配,如果你使用kmalloc申请空间,就会在小于896的内存中分配。所以还是很讲究的啊!!如果你的程序需要使用高端内存,就要调用内核API来分配,所以高端内存并不是想用就能用的哦。不过通过系统把一些应用常住在高端内存到是个好注意。不过前提是你的内存灰常大啊。
为什么要这样做呢?先看看这里面放些什么?
虚拟地址分布:
关于0-3G用户空间内存的分布:
谈到段式分布,就要说说逻辑地址,线性地址与物理地址的关系:
linux通过段机制把逻辑地址转换为虚拟地址(就是线性地址),再通过页机制把虚拟地址转换为物理地址。所谓分段就是基址不同,偏移一样,比如说32位,一般程序里面都不会使用这么多的位,可以把前12位用作基址,后20位用作偏移,这样在特定段就可以只使用偏移寻址了。寻址很方便,不过linux页基址做的更好。
最后呢再说几个点:
1 线性地址空间:指linux系统中的虚拟地址空间。
2 cpu寻址是属于物理地址。所以在使用cpu寻址前要把地址转换好。
3 物理内存中的高端内存是DDR减去896M后多出来的那一段。虚拟地址里面的高端内存是指用于映射高端内存的虚拟地址空间。不过高端内存被映射到用户空间,那就是另外一回事了吧。
4 内核空间是可以访问用户空间的,级别高就是好啊。不过不是通过虚拟地址直接访问的。
我有种预感,我在 CSND 写博客有点写不下去了,编辑器太烂了。初尝 Markdown 编辑器,字体超小,看着都伤眼睛。HTML 编译器,插入代码,各种字体冲突。美化文章布局上,就用去了很多时间。
CSND 博客故障处理,占用工作时间,也就算了。这个一直在用的编辑器这的让人头痛。