六。内存管理机制--MMU

大家发现什么村务一定要告诉我,大家一起学习:

1、#define abc(n) do{xxx;yyy;zzz;}while(0)
    #define abc(n)  {xxx;yyy;zzz;}

    加上do while 和不加可能执行结果可能一样,但是内核一般都加
    是怕在执行函数的时候出现错误    
    int a ;
    if(a > 0)
        abc(100);//若用下面的宏定义,加上分号编译出错,是因为在后面加了分号,多加的分号就分割了if / else
    else
        abc(200);
 
    return 0;

################# 内存管理 #################
1、概念:
    MPU 内存保护单元< 有操作系统的一般不用,用 MMU >
        会写一个页表类似的东西,但不是页表,记录下面内存空间的只读、同步等属性
        每一段内存空间的权限:ro sync ex rw nx ...
        示意将内存分为:[0, 0x1000)[0x1000, 0x2000)[0x2000, 0x3000)[0x3000, ...)
    MMU 内存管理单元 MMU = MPU + (查找页表)
        arm9、arm11、armA系列内有 MMU;一般面向应用的电子带有。但是并不是说有操作系统就一定有 MMU    

    如果做工控,控制器一般有 MPU 或没有,eg:cortex-M 和 cortex-R 系列单片机用的就是 MPU;只有面向想飞电子的高端芯片才用 MMU,安装有操作系统的控制器,要实现多任务,多任务会用到虚拟地址,所以需要 MMU。但并使有操作系统就一定有 MMU

2、linux内核管理机制
    bootargs = "mem = 128M" 当初写这个原因就是告诉内核内存有多大,内核就可以计算出用分配多少页表
    内核用的是小页映射 page = 4K
    每个页对应一个结构体 struct page ---> 描述一页的物理内存
        virtual 如果分配过存虚拟地址,没有的就是NULL

    page 结构体在内存中以连续、有序数组存放

    struct zone    //标记的都是物理地址;标准的linux/unix  X86 上这样分配
        DMA    [0 - 16M] //分这16M是历史问题,以前的DMA只有24位,最大访问16M,所以留下这块给DMA用
        NORMAL[16M - 896M]
        HIGH [896M - ...]

    嵌入式    
        2.6.28    DMA[0x50000000, 0x58000000]        此内核只有这一个区
        3.4.24    NORMAL[0x50000000, 0x58000000]    此内核只有这一个区

    只要用到分配内存,不管三七二十一,先包含 这个头文件

3、MMU:<笔记>
                            MMU
 —————————————————
|                  | ADDR          | TLB     |     |
| ARM core |======        | 缓存     |    | 32位,物理地址
|                  |                     |———--|    |=============
|—————                      |             |    | ADDR
|    CP15                           | 查页表 |    |
|      |                                 |             |    |
|       ---> c2(TTBR)      |             |    |
|           |                           |             |    |
|           |                           |             |    |
 ———|—————————————
            |-----------------------------> 页表基地址
                    
    a>.从arm出来的地址总线上,肯定是物理地址;虚拟地址一定不会出现在地址总线上,
        用到虚拟地址的时候一定是 MMU 打开的情况,MMU拿到虚拟地址后取查页表,找到对应的物理地址,将其放到总线上;如果关闭 MMU,从ARM core中出来的地址就直接上总线了。前期在学裸板的时候,MMU 是关闭的,我们直接访问的就是物理地址。

    b>.MMU
        1.MMU 内有一段缓存,存放的是用过的页表的,
        2.页表的每一个条目(32位,armA8以前的都是,armA9、armA15等为了使用大内存扩宽了)都在一定范围内对应一个虚拟地址和物理地址的对应关系
            MMU 是通过物理地址访问页表的位置的(即找到基地址)
            CP15 可用于开关 MMU,其中的 C2 寄存器内存放着页表的基地之
            访问虚拟地址的时候, MMU 根据 C2里的基地址找到页表的位置,再根据基地址找到相应的条目,找到虚拟地址和物理地址的对应关系

    c>.arm1176 手册的 464 页的图就是一个页表的放大图,此图的上面是低地址,下面是高地址
        这些条目就最后两位的不同,有 4 中情况:

        <1>.00 --- 非法的;此类条目是不能用的,每个进程都会有 4G 的虚拟内存,但不是全部的都会被映射,没映射的内存就是这一类,主要访问这类内存就会出现段错误,
        <2>.01 --- 页映射;对应有二级页表;通过一级页表的31 ~ 20 位找到找到最后两位是 01 的条目,通过一级页表的 31 ~ 10 位找到二级页表的基地址,再根据 19 ~ 12 位作为二级页表的偏移,找到二级页表中对应的相应的条目,二级页表的条目根据最后两位也有 4 种情况:

            (1)00 --- 非法的。与一级页表的相同
            (2)01 --- 64K 大页映射。通过二级页表的31 ~ 16 位作为基地址,知道某个大页的基地址,然后 15 ~ 0 位做偏移找到相对应的条目,得到物理地址
            (3)1x --- 4K 小页映射。通过二级页表的31 ~ 12 位作为基地址,知道某个大页的基地址,然后 11 ~ 0 位做偏移找到相对应的条目,得到物理地址

        <3>.10 --- (bit(18) = 0)普通段映射;普通段映射,1M物理内存; 使用虚拟地址的前12位,做偏移。若找到段映射条目,把页表的31 ~ 20 位拿出,以段对齐,后20 位补 0,找到段,再将虚拟地址的后20位在段内做偏移,找到一个字,就是对应的物理地址;
        <4>.10 --- (bit(18) = 1)超级段映射,16M物理内存;超级段映射方法相同与上面相同

    d>.例子:根据映射关系举个例子,普通段映射eg:虚拟地址:0x12345678,物理地址:0x56000000 ~ 0x57000000
        通过 123 做偏移找到的条目中前 12 位放 560, 映射到物理地址前12位560,后面以 0 补齐,就找到段的起始位置;
        后20位做段内偏移,找到对应的字

    e>.例子:二级页表;例如虚拟地址是0x12345000 找到0x56001000;    
        一级页表从51000000开始
        二级页表从50000000开始

        则在二级页表的第0x45个位置 尾部写10,然后把0x56001放到前31到10位上,二级页表完成
        把一级页表的第123位,尾部写01,写上二级页表起始地址0x50000000的前20位写进去,一级页表完成

4、
               虚拟内存                                             物理内存
    4G ————————                        ————————————
        间接/动态 映射区                                            HIGH
        —————————
        直接/静态 映射区                           ———————————— 896M
  3G ———————— 0xc00000000                   NORMAL

                                                               ———————————— 16M
                                                                                DMA
     0 ————————
                                                                ———————————— 0M

    内核一启动,只会映射直接区,直接映射区是映射到DMA 和 NORMAL 内存处,即低896M内存了;这种映射叫做平坦映射
    内核刚启动的时候 HIGH 是不能访问的
    直接映射区会根据你的物理内存的大小变化,最大是 896M;剩下的都是间接映射区
    HIGH内存一般通过 ioremap 映射到间接映射区;因为间接映射区地址有限,映射完一定要通过 iounmap 释放

5、buddy  内存管理子系统:
    buddy上挂载了55条链表,分成11组(0 ~ 10),每组5条,每条链表上挂载的是5种不同的内存,5种类型的页:
        MIGRATE_UNMOVABLE     0 -- 不可移动的页;启动规定好的,系统工作相关的
        MIGRATE_RECLAIMABLE   1 -- 启动规定好的;系统工作相关的
        MIGRATE_MOVABLE       2 -- 可移动的页;映射文件的都是可移动的,当内核想得到连续的内存,需要移动文件所占的物理内存,内核自动更改页表位置和映射关系,对程序不会产生影响
        MIGRATE_PCPTYPES      3 -- 没用
        MIGRATE_RESERVE       3 -- 没用
        MIGRATE_ISOLATE       4 -- 没用
        MIGRATE_TYPES         5 -- 没用

    buddy子系统是基于MMU的,是管理内存子系统最底层的一层,每一组所挂的链表的节点大小等于 2 的组号次幂 乘以 4K(页的大小),即 2^(组号) * 4K;
    由上可知, buddy 分配连续的最大物理空间是 4M,可以通过修改结构体 struct zone 中的 MAX_ARDER 的值增加组个数
    
    buddy子系统挂载的永远都是空闲的内存;申请内存时,按照大小是层0组开始一层层的向上的,不够向上层借;释放内层时一样,若是连续内存,大小达到上层要求,就会挂载到上层,一层层的往上

    优点:能最大限度的保证有连续的内存
    缺点:分配方法粗糙,操作范围最小为 4K,会造成内存浪费

    buddy子系统常用函数:
    alloc_pages()    申请连续的页,物理地址连续;
        释放用 __free_pages()
    alloc_page()    申请一页;
        释放用 __free_page()
    __get_free_pages()    与 alloc_pages() 函数没什么区别;
        释放用 free_pages()
    __get_free_page()    与 alloc_page() 函数没什么区别;
        释放用 free_page()
    注:不建议在 buddy 中拿内存

6、slab 子系统
    基于 buddy 子系统的一层子系统

    slab 从buddy子系统中申请的内存分成两类,通用内存、专用内存:
    通用内存:
        可以访问到字节

        申请内存函数:
            < 一下函数带 z 只是会把申请的内存刷成 0 >
            kmalloc() / kzalloc():所申请的内存虚拟地址和物理地址都是连续的,一般最大 4M
                释放用 kfree()
            vmalloc() / vzalloc():申请大内存,虚拟地址一定连续,物理地址可能不连续
                释放用 vfree()
                vmalloc / vzalloc 需要自己的头文件
                申请优先从高端内存(HIGH)分配内存,高端内存没有时再取低端内存
    专用内存:
        可用于进程、网络、...
        用于进程:会提前创建好进程结构体 task_struct ,用就拿,不用送回
        用于网络:会提前创建号缓存 sk_buff,用就拿,不用送回

        创建高速缓存:kmem_cache_create()
            创建一次会有 n 个,大于 n 时,内核会自动再分配 n 个(不同的内核 n 的大小不定)
        拿从高速缓存:kmem_cache_alloc()
        放回高速缓存:kmem_cache_free()
        销毁高速缓存:kmem_cache_destory()

        优先把高速缓存放到硬件cache中
        每一个高速缓存对应于一个结构体 struct kmem_cache,自己创建页需要先创建这么一个结构体,可以通过 kmem_cache 的个数来判断高速缓存的个数
            kmem_cache_creat("name", size,align,flags, ctor)
            name:你的高速缓存的名字
            size:每个元素的大小
            align:0;无用是就填 0
            flags:对其方式;因为高速缓存会被缓存到硬件cache中,所以以硬件cache行的方式对其最快
            ctor:函数指针;会产生 n 个结构体,这个函数会被调用n次,这个函数可自己写,可以用于初始化结构体内某些有规律的成员

7.dma 一致性内存
    注意与 SCU(一致性硬件)的区分
    一致性硬件:
        SCU可以保证一级cash的一致性
        cache分为数据cache和指令cache,
        ddr把东西放到cache,arm再从cache拿
        声卡放音乐的时候,cpu需要把数据写道cache,然后cache数据拿到ddr,dma再从ddr中取数据放到pcmdata,再声卡播放

    要求实时性很强,arm 中的数据一到 cache 中,就立刻需要同步到 DDR(内存)中,DMA会不断从内存中读取数据,这就叫一致性内存

    分配一致性内存:dma_alloc_coherent() / dam_zalloc_coherent()  < z 清零 >
        包含在头文件
    释放一致性内存:dma_free_coherent()

    DMA 使用的一般都是物理地址,%99.9的可能是这样的;只有很少很少的一部分,会使用虚拟地址,DMA内部就会带有一个硬件,能读取页表

    v = dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)
        参数1:NULL先不需关心
        参数2:申请大小
        参数3:引用回一个物理地址,这个物理地址可以在配置dma通道的时候用
        参数4:GFP_KERNEL

    这种方式是放在动态内存区(间接映射区)这里就相当于是映射了一个设备dma,还有ioremap也是

8、修改页表:
    外设的地址也叫设备内存,一般不会映射到直接映射区

    通过函数 ioremap 将物理地址映射到间接映射区,用完就要用 iounmap 函数释放

    包含头文件
    ioremap(0x7f008000, 0x1000)
    分配地址要以页对齐,因为底层实现一次是一个页(4K)

9、综述:

    映射 修改页表
——————————————————————————————————————————————
    dma 一致性内存
——————————————————————————————————————————————
    slab
    1.物理连续 2.物理不连续 3.高速缓存
——————————————————————————————————————————————
    buddy
——————————————————————————————————————————————
    MMU
——————————————————————————————————————————————

    buddy子系统是内核分配内存的基础,slab从buddy申请内存,然后把内存告诉通用缓存或者专用缓存(slab就是联系buddy和cache的桥梁),在这里能调用kmalloc和zmalloc和创建高速缓存,如果cache需要和内存同步,就要创建一致性内存dma_alloc系列函数,如果在知道物理地址但是不知道虚拟地址的情况下,就需要修改页表来让物理地址映射到间接映射区,这些所有的申请空间函数和都映射到动态映射区,其中dma和ioremap相当于添加了一个设备,即把外部的dma和led等等设备的物理地址映射到动态内存区(间接映射区)

10、函数解析:
    kmalloc/kzalloc(20, GPL_KERNEL);
        GPL_KERNEL:允许函数睡眠
        GPL_ATOMIC:不允许函数睡眠,被打断就返回出错

    vmalloc(20)
        默认传的就是 GPL_KERNEL,在不允许睡眠的情况下不能使用这个函数

11、不同层次申请、释放内存方法:
    代码参考 /code/04mm/*

从buffy子系统里拿

方法1:
    创建页表
    alloc_pages可以申请若干个连续的页
    __free_pages()
    
    alloc_page可以申请一个页
    __free_page()

方法2:
    创建页表
    __get_free_pages() 申请若干页
    free_pages()

    __get_free_page()申请一个页
    free_page()

从slab里拿
方法3:
    创建高速缓存:
    kmem_cache_create()创建高速缓存
    kmem_cache_alloc()申请高速缓存空间(拿东西)
    kmem_cache_free()释放缓存空间(放回去)
    kmem_cache_destroy()销毁高速缓存

为dma申请    
方法4:
    创建一致性内存
    
    dma_alloc_coherent()分配
    dma_zalloc_coherent()//把内存里所有东西都清0
    dma_free_coherent()

为映射创建
方法5:
    例如知道物理地址 0x72000000 然后用ioremap()做映射(映射到间接映射区)用iounmap()释放
    因为在代码里只能访问虚拟地址,所以需要把物理地址映射到间接映射区,然后为了不混淆,用的时候映射,不用的时候释放


你可能感兴趣的:(内核笔记,内存管理,linux内核,链表,嵌入式,内核)