UNIX再学习 -- 死磕内存管理

malloc/free简化实现;malloc 和 sbrk 关系;虚拟内存机制。

一个内存管理 C 语言部分讲,UNIX部分讲,Linux部分还讲,死磕到底!!

一、mallc/free简化实现

上篇文章已经讲解了动态内存分配/释放函数,参看:UNIX再学习 – 内存管理
下面来讲一下,它的自定义函数实现,其中有三个部分:

1、内存控制块

内存控制块用于管理每次分配的内存块,记录该内存块的字节大小、忙闲状态,以及相关内存控制块的首地址。

代码如下所示

typedef struct mem_control_block
{
    size_t size;  //本块大小
    bool free;  //空闲状态
    struct mem_control_block *next;  //后块指针
}MCB;
MCB *g_top = NULL;  //栈顶指针

讲解

上述代码中: 
定义了一个结构体 mem_control_block,别名为 MCB。 
定义结构体成员 size,用于保存申请分配的内存块的字节数。 
定义结构体成员 free,用于保存该内存是否被分配给进程。 
定义结构体成员 next,用于保存下一个链表节点的地址。 
定义链表栈的栈顶指针 g_top。

2、分配内存

遍历内存控制块链表,若有大小足够的空闲块,则重用该块,否则分配新的足量内存并将其控制块压入链表栈。

代码如下所示:

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),即跳过内存控制块所占的空间,返回可以使用的空间地址。

3、释放内存

先将被释放内存块标记为空闲,然后遍历内存控制块链表,将靠近栈顶的连续空闲块及其内存控制块一并释放。

代码如下所示

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);
        }

将靠近栈顶的连续空闲块及其内存控制块一并释放。

4、完整代码

#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>

//内存控制块
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

扩展部分

参看:Linux实验心得——内存管理
参看:malloc的实现原理学习(2)

二、malloc 和 sbrk 关系

1、首先我想讲一个函数 malloc_usable_size

参看:MALLOC_USABLE_SIZE 讲解

#include <malloc.h>
size_t malloc_usable_size (void *ptr);
1

这个函数返回调用 malloc 后实际分配的可用内存的大小,如果ptr 为NULL,则为 0

举个例子

#include <stdio.h>
#include <stdlib.h>

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

通过这个示例说明,使用malloc申请内存空间,正常情况下系统会返回至少 12B 的空间。

2、分析 malloc 和 sbrk

通过上面 malloc 函数的简化实现,可以看到,当内存控制块链表中找不到大小足够的空闲块进行分配时,分配新的足量内存并将其控制块压入链表栈。其中,以下代码:

  mcb = sbrk (sizeof (MCB) + size);
        if (mcb == (void*)-1)
            return NULL;

使用sbrk函数分配新的足量内存。
sbrk 实现虚拟内存到内存的映射,是系统调用,是Unix/Linux系统提供的接口(只能在Unix/Linux系统下才能用的)。而malloc是标准c函数在,所以在Unix/Linux和windows下都能用。 
在Unix/Linux下,malloc 底层实现就是通过系统调用sbrk实现的;在windows下malloc则是通过调用windows系统提供的接口实现。

描述:

这部分,在 UNIX环境高级编程第 7 章,也有讲到。 
虽然 sbrk 可以扩充或缩小进程的存储空间,但是大多数 malloc 和 free 的实现都不减小进程的存储空间。释放的空间可共以后再分配,但将它们保持在 malloc 池中而不返回给内核。 
大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息 – 分配块的长度、指向下一个分配块的指针等。这就意味着,如果超过一个已分配区的尾端或者在已分配区起始位置之前进行写操作,则会改写另一块管理记录信息。这种类型的错误是灾难性的,但是因为这种错误不会很快就暴露出来,所以也就很难发现。

举个例子说明:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>  

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

示例总结

参看:自己动手写malloc

参看:UNIX再学习 – 内存管理

可以看出,malloc 所申请的空间的起始地址,第一次,比一开始的堆末尾地址向后移动了 8 个字节。第二次,后移了112个字节。这 8 个字节应该就是,用来记录管理信息的额外空间 – 分配块的长度、指向下一个分配块的指针等。而我们采用的是 8 字节对齐,申请 100 个字节,经过对齐,应为 104 个字节,再加上 8 个额外空间,即 112 个字节。
而堆尾地址变为 0x8f38000,增加了 0x21000,十六进制 0x21000 转换成 十进制为 135168 = 33 * 4096 上面讲到当前系统内存页的大小为 4Kb。malloc 申请内存,系统会一次映射 33 个内存页。

三、虚拟内存机制

参看:linux内存管理

1、为什么需要使用虚拟内存

大家都知道,进程需要使用的代码和数据都放在内存中,比放在外存中要快很多。问题是内存空间太小了,不能满足进程的需求,而且现在都是多进程,情况更加糟糕。所以提出了虚拟内存,使得每个进程用于3G的独立用户内存空间和共享的1G内核内存空间。(每个进程都有自己的页表,才使得3G用户空间的独立)这样进程运行的速度必然很快了。而且虚拟内存机制还解决了内存碎片和内存不连续的问题。为什么可以在有限的物理内存上达到这样的效果呢?

2、虚拟内存的实现机制

首先呢,提一个概念,交换空间(swap space),这个大家应该不陌生,在重装系统的时候,会让你选择磁盘分区,就比如说一个硬盘分几个部分去管理。其中就会分一部分磁盘空间用作交换,叫做swap space。其实就是一段临时存储空间,内存不够用的时候就用它了,虽然它也在磁盘中,但省去了很多的查找时间啊。当发生进程切换的时候,内存与交换空间就要发生数据交换一满足需求。所以啊,进程的切换消耗是很大的,这也说明了为什么自旋锁比信号量效率高的原因。

那么我们的程序里申请的内存的时候,linux内核其实只分配一个虚拟内存( 线性地址),并没有分配实际的物理内存。只有当程序真正使用这块内存时,才会分配物理内存。这就叫做延迟分配和请页机制。释放内存时,先释放线性区对应的物理内存,然后释放线性区;"请页机制"将物理内存的分配延后了,这样是充分利用了程序的局部性原来,节约内存空间,提高系统吞吐;就是说一个函数可能只在物理内存中呆了一会,用完了就被清除出去了,虽然在虚拟地址空间还在。(不过虚拟地址空间不是事实上的存储,所以只能说这个函数占据了一段虚拟地址空间,当你访问这段地址时,就会产生缺页处理,从交换区把对应的代码搬到物理内存上来)

3、物理内存与虚拟内存的布局

左边是物理地址分配,与实际的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 博客故障处理,占用工作时间,也就算了。这个一直在用的编辑器这的让人头痛。

时间: 2024-10-22 03:29:50

UNIX再学习 -- 死磕内存管理的相关文章

UNIX再学习 -- 文件I/O

在 UNIX/Linux 系统中,一切皆文件,这句话想必都有听过.对于文件的操作几乎适用于所有的设备,这也就看出了文件操作的重要性了.在C语言再学习部分有讲过标准I/O文件操作,参看:C语言再学习 -- 文件 下面我们来讲解下系统文件I/O的. 一.文件描述符 1.文件描述符简介 首先从文件描述符开始讲起.因为,对于内核而言,所有打开的文件都是通过文件描述符引用的.那么文件描述符到底是什么? 文件描述符(file descriptor)通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文

linux kernel学习笔记-5内存管理(转)

http://blog.sina.com.cn/s/blog_65373f1401019dtz.htmllinux kernel学习笔记-5 内存管理1. 相关的数据结构 相比用户空间而言,在内核中分配内存往往受到更多的限制,比如内核中很多情况下不能睡眠,此外处理内存分配失败也不像用户空间那么容易.内核使用了页和区两种数据结构来管理内存: 1.1 页 内核把物理页作为内存管理的基本单位.尽管CPU的最小可寻址单位通常为字(甚至字节),但是MMU(内存管理单元,管理内存并把虚拟地址转换为物理地址的

学习 Sql Server 内存管理之术语理解

在学习sql server 内存管理时,看到一些term:memory node,memory clerk,memory object,非常迷惑,在此将自己的理解记录下来,以便后续学习 1,NUMA架构和memory node 内存节点基于硬件 NUMA 创建,memory node是Numa node内的内存块,属于server物理内存的一部分.Memory Node的作用是使得内存的分配由Windows移交到SQL Server OS层面执行. 查看memory node select *

Linux System Programming 学习笔记(九) 内存管理

1. 进程地址空间 Linux中,进程并不是直接操作物理内存地址,而是每个进程关联一个虚拟地址空间 内存页是memory management unit (MMU) 可以管理的最小地址单元 机器的体系结构决定了内存页大小,32位系统通常是 4KB, 64位系统通常是 8KB 内存页分为 valid or invalid: A valid page is associated with an actual page of data,例如RAM或者磁盘上的文件 An invalid page is

cocos2d-x学习之自动内存管理

一.自动内存管理 1)概述 C++语言默认是没有提供自动内存管理的.使用者需要自己分配,自己释放.在cocos2d-x里提供了一个自动内存管理的方案.主要是通过CCObject来提供的,用户只要继承了CCObject,就可以通过调用autorelease()来告诉系统进行自动内存管理. 一般用法就是:    CCLayer* pLayer = CreateLayer(s_nActionIdx);    pLayer->autorelease(); 2)自动内存管理的实现 自动内存管理的实现原理大

计算机操作系统学习笔记_8_内存管理 --虚拟内存管理

td p { margin-bottom: 0cm; }h2.western { font-family: "Liberation Sans",sans-serif; font-size: 16pt; }h2.cjk { font-family: "微软雅黑"; font-size: 16pt; }h2.ctl { font-family: "AR PL UMing CN"; font-size: 16pt; }h1 { margin-botto

【IOS学习基础】内存管理

1.内存几大区域 1> 栈区:局部变量(基本数据类型.指针变量). 2> 堆区:程序运行的过程中动态分配的存储空间(创建的对象). 3> BSS段:没有初始化的全局变量和静态变量. 4> 数据区:已经初始化的全局变量和静态变量.(字符串常量) 5> 代码段:程序编译后的代码的内容. 2.引用计数器 1> 引用计数器:每个继承自NSObject的对象都有一个引用计数器,用来表示当前对象有几个拥有者. 2> 引用计数器的作用:用来判断对象是否应该回收. 3> 引

OC学习总结4 内存管理

内存管理 基本数据类型(int,float,double,char,struct,enum)的数据内存不需要内存管理 1 对象的内存管理 诞生 alloc或new或copy 生存 接收消息和执行操作 交友 借助方法的组合和参数,调用方法 销毁 内存被释放 delloc:相当于被释放内存的临终遗言,每个内存被销毁之前都会调用dealloc方法 2 引用计数 1 Cocoa采用引用计数也叫作保留计数 2 alloc new copy 用这三个方法创建对象时,对象的引用计数器为1 3 retain 给

ios学习笔记之内存管理

一,内存管理类型定义      1,基本类型  任何C的类型,eg:      int,short,char,long,long long,struct,enum,union等属于基本类型或结构体      内存管理对于C语言的基本类型无效      2,OC类型(非基本类型)      任何继承于NSObject类的对象都属于OC类型      也就是除了C之外的其他类型 二,OC对象结构      所有OC对象都有着一个计数器,保留着当前对象被引用的数量,如果计数器为0,那么就真正的释放这个