歡迎光臨
每天分享高質量文章

ARM64 Kernel Image Mapping的變化

引言

隨著linux的代碼更新,閱讀linux-4.15代碼,從中發現很多與眾不同的地方。之所以與眾不同,就是因為和我之前從網上博客或者書籍中看到的內容有所差異。當然了,並不是為了表明書上或者博客的觀點是錯誤的。而是因為linux代碼更新的太快,網上的博客和書籍跟不上linux的步伐而已。究竟是哪些發生了差異了?例如:kernel image映射區域從原來的linear mapping region(線性映射區域)搬移到VMALLOC區域。因此,我希望通過本篇文章揭曉這些差異。當然,我相信不久的將來這篇文章也將會成為一段歷史。

註:文章代碼分析基於linux-4.15,架構基於aarch64(ARM64)。涉及頁表代碼分析部分,假設頁表映射層級是4,即配置CONFIG_ARM64_PGTABLE_LEVELS=4。地址寬度是48,即配置CONFIG_ARM64_VA_BITS=48。

kernel啟動頁表在哪裡

在ARM64架構上,彙編代碼初始化階段會創建兩次地址映射。第一次是為了打開MMU操作的準備。因為再打開MMU之前,當前代碼運行在物理地址之上,而打開MMU之後代碼運行在虛擬地址之上。為了從物理地址(Physical Address,簡稱PA)轉換到虛擬地址(Virtual Address,簡稱VA)的平滑過渡,ARM推薦創建VA和PA相等的一段映射(例如:虛擬地址addr通過頁表查詢映射的物理地址也是addr)。這段映射在linux中稱為identity mapping。第二次是kernel image映射。而這段映射在linux-4.15代碼上映射區域是VMALLOC區域。

kernel啟動開始首先就會打開MMU,但是打開MMU之前,我們需要填充頁表。也就是告訴MMU虛擬地址和物理地址的對應關係。系統啟動初期使用section mapping,因此需要3個頁面儲存頁表項。但是我們有identity mapping和kernel image mapping,因此總需要6個頁面。那麼這6個頁面記憶體是在哪裡分配的呢?可以從vmlinux.lds.S中找到答案。

  1. BSS_SECTION(0, 0, 0)
  2.  
  3. . = ALIGN(PAGE_SIZE);
  4. idmap_pg_dir = .;
  5. . += IDMAP_DIR_SIZE;
  6. swapper_pg_dir = .;
  7. . += SWAPPER_DIR_SIZE;

從鏈接腳本中可以看到預留6個頁面儲存頁表項。緊跟在bss段後面。idmap_pg_dir是identity mapping使用的頁表。swapper_pg_dir是kernel image mapping初始階段使用的頁表。請註意,這裡的記憶體是一段連續記憶體。也就是說頁表(PGD/PUD/PMD)都是連在一起的,地址相差PAGE_SIZE(4k)。

如何填充頁表的頁表項

從鏈接腳本vmlinux.lds.S檔案中可以找到kernel代碼起始代碼段是”.head.text”段,因此kernel的代碼起始位置位於arch/arm64/kernel/head.S檔案_head標號。在head.S檔案中有三個宏定義和創建地址映射相關。分別是:create_table_entrycreate_pgd_entrycreate_block_map

create_table_entry實現如下。

  1. /*
  2. * Macro to create a table entry to the next page.
  3. *
  4. * tbl: 頁表基地址
  5. * virt: 需要創建地址映射的虛擬地址
  6. * shift: #imm page table shift
  7. * ptrs: #imm pointers per table page
  8. *
  9. * Preserves: virt
  10. * Corrupts: tmp1, tmp2
  11. * Returns: tbl -> next level table page address
  12. */
  13. .macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
  14. lsr \tmp1, \virt, #\shift
  15. and \tmp1, \tmp1, #\ptrs – 1    // table index
  16. add \tmp2, \tbl, #PAGE_SIZE
  17. orr \tmp2, \tmp2, #PMD_TYPE_TABLE // address of next table and entry type
  18. str \tmp2, [\tbl, \tmp1, lsl #3]
  19. add \tbl, \tbl, #PAGE_SIZE    // next level table page
  20. .endm

這裡是彙編中的宏定義。彙編中宏定義是以.macro開頭,以.endm結尾。宏定義中以\x來取用宏定義中的引數x。該宏定義的作用是創建一個level的頁表項(PGD/PUD/PMD)。具體是哪個level是由virt、shift和ptrs引數決定。我總是喜歡幫你翻譯成C語言的形式。C語言如果不懂的話,我也沒辦法了。既然彙編你不熟悉,沒關係,下麵幫你轉換成C語言的宏定義。

  1. #define PAGE_SIZE            (1 << 12)
  2. #define PMD_TYPE_TABLE       (3 << 0)
  3.  
  4. #define create_table_entry(tbl, virt, shift, ptrs, tmp1, tmp2) do { \
  5. tmp1 = virt >> shift;                     /* 1 */           \
  6. tmp1 &= ptrs 1;                         /* 1 */           \
  7. tmp2 = tbl + PAGE_SIZE;                   /* 2 */           \
  8. tmp2 |= PMD_TYPE_TABLE;                   /* 3 */           \
  9. *((long *)(tbl + (tmp1 << 3))) = tmp2;    /* 4 */           \
  10. tbl += PAGE_SIZE;                         /* 5 */           \
  11. } while (0)

  1. 根據virt和ptrs引數計算該虛擬地址virt的頁表項在頁表中的index。例如計算virt地址在PGD也表中的indedx,可以傳遞shift = PGDIR_SHIFT,ptrs = PTRS_PER_PGD,tbl傳遞PGD頁表基地址。所以,宏定義是一個創建中間level的頁表項。
  2. 既然要填充當前level的頁表項就需要告知下一個level頁表的基地址,這裡就是計算下一個頁表的基地址。還記得上面說的idmap_pg_dir和swapper_pg_dir嗎?頁表(PGD/PUD/PMD)都是連在一起的,地址相差PAGE_SIZE。
  3. 告知MMU這是一個中間level頁表並且是有效的。
  4. 頁表項的真正填充操作,tmp1 << 3是因為ARM64的地址占用8bytes。
  5. 更新tbl,也就只指向下一個level頁表的地址,可以方便再一次呼叫create_table_entry填充下一個level頁表項而不用自己更新tbl。

create_pgd_entry的實現如下。

  1. /*
  2. * Macro to populate the PGD (and possibily PUD) for the corresponding
  3. * block entry in the next level (tbl) for the given virtual address.
  4. *
  5. * Preserves: tbl, next, virt
  6. * Corrupts: tmp1, tmp2
  7. */
  8. .macro create_pgd_entry, tbl, virt, tmp1, tmp2
  9. create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2
  10. create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
  11. .endm

create_pgd_entry可以用來填充PGD、PUD、PMD等中間level頁表對應頁表項。雖然名字是創建PGD的描述符,但是實際上是一級一級的創建頁表項,最終只留下最後一級頁表沒有填充頁表項。老規矩轉換成C語言分析。

  1. #define SWAPPER_TABLE_SHIFT PUD_SHIFT
  2.  
  3. #define create_pgd_entry(tbl, virt, tmp1, tmp2) do {                                          \
  4. create_table_entry(tbl, virt, PGDIR_SHIFT, PTRS_PER_PGD, tmp1, tmp2);         /* 1 */ \
  5. create_table_entry(tbl, virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, tmp1, tmp2); /* 2 */ \
  6. } while (0)

  1. 這裡的tbl引數相當於PGD頁表地址,呼叫create_table_entry創建PGD頁表中virt地址對應的頁表項。
  2. 填充下一個level的頁表項。這裡是PUD頁表。由於使用了ARM64初期使用section mapping,因此PUD頁表就是最後一個中間level的頁表,所以只剩下PMD頁表的頁表項沒有填充,virt地址對應的PMD頁表項最終會填充block descriptor。假設這裡使用4級頁表,那麼下麵還會創建PMD頁表的頁表項,也就是只留下PTE頁表。所以,宏定義是創建所有中間level的頁表項,只留下最後一級頁表。

在經過create_pgd_entry宏的呼叫後,就填充好了從PGD開始的所有中間level的頁表的頁表項的填充操作。現在是不是只剩下PTE頁表的頁表項沒有填充呢?所以最後一個create_block_map就是完成這個操作的。

  1. /*
  2. * Macro to populate block entries in the page table for the start..end
  3. * virtual range (inclusive).
  4. *
  5. * Preserves: tbl, flags
  6. * Corrupts: phys, start, end, pstate
  7. */
  8. .macro create_block_map, tbl, flags, phys, start, end
  9. lsr \phys, \phys, #SWAPPER_BLOCK_SHIFT
  10. lsr \start, \start, #SWAPPER_BLOCK_SHIFT
  11. and \start, \start, #PTRS_PER_PTE – 1               // table index
  12. orr \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT  // table entry
  13. lsr \end, \end, #SWAPPER_BLOCK_SHIFT
  14. and \end, \end, #PTRS_PER_PTE – 1                   // table end index
  15. 9999: str \phys, [\tbl, \start, lsl #3]               // store the entry
  16. add \start, \start, #1                              // next entry
  17. add \phys, \phys, #SWAPPER_BLOCK_SIZE               // next block
  18. cmp \start, \end
  19. b.ls 9999b
  20. .endm

create_block_map宏的作用是創建虛擬地址(從start到end)區域映射到到phys物理地址。傳入5個引數,分別如下意思。

  • tbl:頁表基地址
  • flags:將要填充頁表項的flags
  • phys:創建映射的物理地址
  • start:創建映射的虛擬地址起始地址
  • end:創建映射的虛擬地址結束地址

我們還是依然翻譯成C語言分析。

  1. #define SWAPPER_BLOCK_SHIFT PMD_SHIFT
  2. #define SWAPPER_BLOCK_SIZE (1 << PMD_SHIFT)
  3.  
  4. #define create_block_map(tbl, flags, phys, start, end) do {  \
  5. phys >>= SWAPPER_BLOCK_SHIFT;                /* 1 */ \
  6. start >>= SWAPPER_BLOCK_SHIFT;               /* 2 */ \
  7. start &= PTRS_PER_PTE 1;                   /* 2 */ \
  8. phys = flags | (phys << SWAPPER_BLOCK_SHIFT);/* 3 */ \
  9. end >>= SWAPPER_BLOCK_SHIFT;                 /* 4 */ \
  10. end &= PTRS_PER_PTE 1;                     /* 4 */ \
  11.                                                             \
  12. while (start != end) {                       /* 5 */ \
  13. *((long *)(tbl + (start << 3))) = phys;  /* 6 */ \
  14. start++;                                 /* 7 */ \
  15. phys += SWAPPER_BLOCK_SIZE;              /* 8 */ \
  16. }                                                    \
  17. } while (0)

  1. 針對phys的低SWAPPER_BLOCK_SHIFT位進行清零,和第三步驟的phys << SWAPPER_BLOCK_SHIFT收尾呼應。相當於對齊(這裡的情況是2M對齊)。
  2. 計算起始地址start的頁目錄項的index。
  3. 構造描述符。
  4. 計算結束地址end的頁目錄項的index。
  5. 迴圈填充start到end的頁目錄項。
  6. 根據頁表基地址tbl和當前的start變數填充對應的頁表項。start << 3是因為ARM64地址占用8 bytes。
  7. 更新下一個頁表項。
  8. 更新下一個block的物理地址。

如何使用上述三個接口創建映射關係呢?其實很簡單,首先我們需要先呼叫create_pgd_entry宏填充PGD以及所有中間level的頁表項。最後的PMD頁表的填充可以呼叫create_block_map宏來完成操作。

如何創建頁表

在彙編代碼階段的head.S檔案中,負責創建映射關係的函式是create_page_tables。create_page_tables函式負責identity mapping和kernel image mapping。前文提到identity mapping主要是打開MMU的過度階段,因此對於identity mapping不需要映射整個kernel,只需要映射操作MMU代碼相關的部分。如何區分這部分代碼呢?當然是利用linux中常用手段自定義代碼段。自定義的代碼段的名稱是”.idmap.text”。除此之外,肯定還需要在鏈接腳本中宣告兩個標量,用來標記代碼段的開始和結束。可以從vmlinux.lds.S中找到答案。

  1. #define IDMAP_TEXT                             \
  2. . = ALIGN(SZ_4K);                          \
  3. VMLINUX_SYMBOL(__idmap_text_start) = .;    \
  4. *(.idmap.text)                             \
  5. VMLINUX_SYMBOL(__idmap_text_end) = .;

從鏈接腳本中可以看出idmap_text_start和idmap_text_end分別是.idmap.text段的起始和結束地址。在創建identity mapping的時候會使用。另外我們同樣從鏈接腳本中得到_text和_end兩個變數,分別是kernel代碼鏈接的開始和結束地址。編譯器的鏈接地址實際上就是最後代碼期望運行的地址。在KASLR關閉的情況下就是kernel image需要映射的虛擬地址。當我們編譯kernel後,可以根據符號表System.map檔案查看哪些函式被放在”.idmap.text”段。當然你也可以看代碼,但是我覺得沒有這種方法簡單。

  1. ffff200008fbc000 T __idmap_text_start
  2. ffff200008fbc000 T kimage_vaddr
  3. ffff200008fbc008 T el2_setup
  4. ffff200008fbc054 t set_hcr
  5. ffff200008fbc118 t install_el2_stub
  6. ffff200008fbc16c t set_cpu_boot_mode_flag
  7. ffff200008fbc190 T secondary_holding_pen
  8. ffff200008fbc1b4 t pen
  9. ffff200008fbc1c8 T secondary_entry
  10. ffff200008fbc1d4 t secondary_startup
  11. ffff200008fbc1e4 t __secondary_switched
  12. ffff200008fbc218 T __enable_mmu
  13. ffff200008fbc26c t __no_granule_support
  14. ffff200008fbc290 t __primary_switch
  15. ffff200008fbc2b0 T cpu_resume
  16. ffff200008fbc2d0 T cpu_do_resume
  17. ffff200008fbc340 T idmap_cpu_replace_ttbr1
  18. ffff200008fbc370 T __cpu_setup
  19. ffff200008fbc3f0 t crval
  20. ffff200008fbc408 T __idmap_text_end

create_page_tables的彙編代碼比較簡單,就不轉換成C語言講解了。create_page_tables實現如下。

  1. __create_page_tables:
  2. mov x7, SWAPPER_MM_MMUFLAGS
  3. /*
  4. * Create the identity mapping.
  5. */
  6. adrp x0, idmap_pg_dir                                             /* 1 */
  7. adrp x3, __idmap_text_start          // __pa(__idmap_text_start)  /* 2 */
  8. create_pgd_entry x0, x3, x5, x6                                      /* 3 */
  9. mov x5, x3                              // __pa(__idmap_text_start)  /* 4 */
  10. adr_l x6, __idmap_text_end            // __pa(__idmap_text_end)
  11. create_block_map x0, x7, x3, x5, x6                                  /* 5 */
  12.  
  13. /*
  14. * Map the kernel image.
  15. */
  16. adrp x0, swapper_pg_dir                                           /* 6 */
  17. mov_q x5, KIMAGE_VADDR + TEXT_OFFSET  // compile time __va(_text)
  18. add x5, x5, x23                         // add KASLR displacement    /* 7 */
  19. create_pgd_entry x0, x5, x3, x6                                      /* 8 */
  20. adrp x6, _end                        // runtime __pa(_end)
  21. adrp x3, _text                       // runtime __pa(_text)
  22. sub x6, x6, x3                          // _end – _text
  23. add x6, x6, x5                          // runtime __va(_end)
  24. create_block_map x0, x7, x3, x5, x6                                  /* 9 */

  1. x0暫存器PGD頁表基地址,這裡是idmap_pg_dir,是為了創建identity mapping。
  2. adrp指令可以獲取__idmap_text_start符號的實際運行物理地址。
  3. 填充PGD及中間level頁表的頁表項。
  4. 因為我們為了創建虛擬地址和物理地址相等的映射,因此這裡的x5和x3值相等。
  5. 呼叫create_block_map創建identity mapping,註意這裡傳遞的引數物理地址(x3)和虛擬地址(x5)相等。
  6. 創建kernel image mapping,PGD頁表基地址是swapper_pg_dir。
  7. KASLR預設關閉的情況下,x23的值為0。
  8. 填充PGD及中間level頁表的頁表項。
  9. 填充PMD頁表項。因為採用的是section mapping,所以一個頁表項對應2M大小。註意彙編中的註釋,va()代表得到的事虛擬地址,pa()得到的是物理地址。

經過以上初始化,頁表就算是初始化完成。kernel映射區域從先行映射區域遷移到VMALLOC區域在哪裡體現呢?答案就是KIMAGE_VADDR宏定義。KIMAGE_VADDR是kernel的虛擬地址。其定義在arch/arm64/mm/memory.h檔案。

  1. #define VA_BITS         (CONFIG_ARM64_VA_BITS)
  2. #define VA_START        (UL(0xffffffffffffffff) (UL(1) << VA_BITS) + 1)
  3. #define PAGE_OFFSET     (UL(0xffffffffffffffff) (UL(1) << (VA_BITS 1)) + 1)
  4. #define KIMAGE_VADDR    (MODULES_END)
  5. #define VMALLOC_START   (MODULES_END)
  6. #define VMALLOC_END     (PAGE_OFFSET PUD_SIZE VMEMMAP_SIZE SZ_64K)
  7. #define MODULES_END     (MODULES_VADDR + MODULES_VSIZE)
  8. #define MODULES_VADDR   (VA_START + KASAN_SHADOW_SIZE)
  9. #define MODULES_VSIZE   (SZ_128M)
  10. #define VMEMMAP_START   (PAGE_OFFSET VMEMMAP_SIZE)
  11. #define PCI_IO_END      (VMEMMAP_START SZ_2M)
  12. #define PCI_IO_START    (PCI_IO_END PCI_IO_SIZE)
  13. #define FIXADDR_TOP     (PCI_IO_START SZ_2M)
  14. #define TASK_SIZE_64    (UL(1) << VA_BITS)

上面的宏定義顯得不夠直觀,畫張圖表示現階段kernel的地址空間分佈情況。可以看出KIMAGE_VADDR正好處在VMALLOC區域,因此kernnel的運行地址位於VMALLOC區域。

virt_to_phys和phys_to_virt怎麼辦

通過上面的介紹,你應該有所瞭解kernel image和linear mapping region不在一個區域。virt_to_phys宏的作用是將內核虛擬地址轉換成物理地址(針對線性映射區域)。在kernel image還在線性映射區域的時候,virt_to_phys宏可以將kernel代碼中的一個地址轉換成物理地址,因為線性映射區域,物理地址和虛擬地址只有一個偏移。因此兩者很容易轉換。那麼現在kernel image和線性映射區域分開了,virt_to_phys宏又該如何實現呢?virt_to_phys宏實現如下。

  1. #define PHYS_OFFSET              ({ VM_BUG_ON(memstart_addr & 1); memstart_addr; })
  2.  
  3. #define __is_lm_address(addr)    (!!((addr) & BIT(VA_BITS 1)))
  4. #define __lm_to_phys(addr)       (((addr) & ~PAGE_OFFSET) + PHYS_OFFSET)
  5. #define __kimg_to_phys(addr)     ((addr) kimage_voffset)
  6.  
  7. #define __virt_to_phys_nodebug(x) ({              \
  8. phys_addr_t __x = (phys_addr_t)(x);           \
  9. __is_lm_address(__x) ? __lm_to_phys(__x) :    \
  10.       __kimg_to_phys(__x);           \
  11. #define __virt_to_phys(x) __virt_to_phys_nodebug(x)
  12.  
  13. static inline phys_addr_t virt_to_phys(const volatile void *x)
  14. {
  15. return __virt_to_phys((unsigned long)(x));
  16. }

 

從__virt_to_phys_nodebug宏可以看出其中的奧秘。通過addr地址的(VA_BITS – 1)位是否為1判斷addr是位於kernel image區域還是線性映射區域(因為線性映射區域大小正好是kernel虛擬地址空間大小的一半)。針對線性映射區域,虛擬地址和物理地址的偏差是memstart_addr。針對kernel image區域,虛擬地址和物理地址的偏差是kimage_voffset。kimage_voffset和memstart_addr是如何計算的呢?先看看kimage_voffset的計算。

  1. #define KERNEL_START    _text
  2. #define __PHYS_OFFSET (KERNEL_START TEXT_OFFSET)
  3. ENTRY(kimage_vaddr)
  4. .quad _text TEXT_OFFSET
  5. /*
  6. * The following fragment of code is executed with the MMU enabled.
  7. *
  8. *   x0 = __PHYS_OFFSET
  9. */
  10. __primary_switched:
  11. ldr_l x4, kimage_vaddr        // Save the offset between      /* 2 */
  12. sub x4, x4, x0                  // the kernel virtual and       /* 3 */
  13. str_l x4, kimage_voffset, x5  // physical mappings            /* 4 */
  14.  
  15. b start_kernel
  16.  
  17. __primary_switch:
  18. bl __enable_mmu
  19. ldr x8, =__primary_switched
  20. adrp x0, __PHYS_OFFSET                                       /* 1 */
  21. br x8

 

  1. x0是_primary_switch函式中設置。x0暫存器通過adrp指令可以獲取運行時的地址。也就是實際運行的物理地址。你是不是好奇此時不是已經打開MMU了嘛!為什麼adrp得到的運行地址就是物理地址呢?請往上翻看看_primary_switch函式是不是位於”.idmap.text”段,那麼該段是identity mapping。因此獲取的運行地址雖然是虛擬地址,但是它和實際運行的物理地址相等。
  2. x4暫存器儲存的是kernel image的運行的虛擬地址。你是不是又好奇這個地方為什麼獲取的運行地址和物理地址不相等呢?其實是因為__primary_switched函式映射在kernel image mapping區域。
  3. 計算虛擬地址和物理地址的偏移。
  4. 將偏移寫入kimage_voffset全域性變數。

memstart_addr是kernel選取的物理基地址,memstart_addr在arm64_memblock_init函式中設置。arm64_memblock_init函式實現如下(截取部分代碼)。

  1. void __init arm64_memblock_init(void)
  2. {
  3. const s64 linear_region_size = -(s64)PAGE_OFFSET;
  4.  
  5. /*
  6. * Ensure that the linear region takes up exactly half of the kernel
  7. * virtual address space. This way, we can distinguish a linear address
  8. * from a kernel/module/vmalloc address by testing a single bit.
  9. */
  10. BUILD_BUG_ON(linear_region_size != BIT(VA_BITS 1));           /* 1 */
  11.  
  12. /*
  13. * Select a suitable value for the base of physical memory.
  14. */
  15. memstart_addr = round_down(memblock_start_of_DRAM(),            /* 2 */
  16.   ARM64_MEMSTART_ALIGN);
  17.  
  18. memblock_remove(max_t(u64, memstart_addr + linear_region_size,  /* 3 */
  19. __pa_symbol(_end)), ULLONG_MAX);
  20. if (memstart_addr + linear_region_size < memblock_end_of_DRAM()) {
  21. /* ensure that memstart_addr remains sufficiently aligned */
  22. memstart_addr = round_up(memblock_end_of_DRAM() linear_region_size,
  23. ARM64_MEMSTART_ALIGN);                         /* 4 */
  24. memblock_remove(0, memstart_addr);                          /* 5 */
  25. }
  26. }

 

  1. 從註釋以及代碼皆可以看出,PAGE_OFFSET是線性區域的開始虛擬地址。線性區域大小是整個kernel虛擬地址空間的一半。
  2. 選取一個合適的物理基地址,根據RAM的起始地址按照1G對齊。
  3. memstart_addr是選取的物理基地址。kernel虛擬地址空間一半大小作為線性映射區域。因此最大支持的記憶體範圍是memstart_addr + linear_region_size。所以告訴memblock,超過這個區域的範圍都是非法的。
  4. 如果memstart_addr + linear_region_size的值小於RAM的結束地址,說明[memstart_addr, memstart_addr + linear_region_size]地址空間範圍的區域無法改寫整個RAM地址範圍。這時候就需要從RAM結束地址減去linear_region_size的值作為memstart_addr。什麼時候會出現這種情況呢?當物理記憶體足夠大時,if陳述句就可能滿足條件了。
  5. 既然4滿足,自然這裡[0, memstart_addr]的地址空間需要從memblock中remove。

memstart_addr的值定下來之後,虛擬地址和物理地址以memstart_addr為偏差創建線性映射區域。在map_mem函式中完成。phys_to_virt宏的實現就不用介紹了,就是virt_to_phys的反操作。

 

    閱讀原文

    赞(0)

    分享創造快樂