1. 程式人生 > >六。記憶體管理機制--MMU

六。記憶體管理機制--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]    此核心只有這一個區

    只要用到分配記憶體,不管三七二十一,先包含 <linux/slab.h> 這個標頭檔案

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 需要自己的標頭檔案 <linux/vmalloc.h>
                申請優先從高階記憶體(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 清零 >
        包含在標頭檔案<linux/dma-mapping.h>
    釋放一致性記憶體: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 函式釋放

    包含標頭檔案 <asm/io.h>
    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、不同層次申請、釋放記憶體方法:
    程式碼參考 <path>/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()釋放
    因為在程式碼裡只能訪問虛擬地址,所以需要把實體地址對映到間接對映區,然後為了不混淆,用的時候對映,不用的時候釋放