文章目錄
- 1 kmap
- 2 映射內核內存到用戶空間
- 使用remap_pfn_range
- 使用io_remap_pfn_range
- mmap文件操作
- 建立VMA和實際物理地址的映射
- mmap 之前分配 + 一次性映射
- mmap 之前分配 + Page Fault
- Page Fault 中分配 + 映射
內核內存有時需要重新映射,無論是從內核到用戶空間還是從內核空間到內核。常見的情況是將內核內存重新映射到用戶空間,但還有其他一些情況,例如需要訪問高內存的情況。
1 kmap
kmap()用于將指定的頁面映射到內核地址空間。
Linux內核將其896MB地址空間永久映射到物理內存較低的896MB(低端內存)。在4GB系統上,內核僅剩下128MB用來映射剩余3.2GB物理內存(高端內存)。由于低端內存采用永久一對一映射,因此內核可以直接尋址。而對于高端內存(高于896MB的內存),內核必須將所請求的高端內存區域映射到其地址空間,前面提到的128MB就是專門為此保留。用于執行此操作的函數是kmap()。kmap()用于將指定的頁面映射到內核地址空間。
void *kmap(struct page *page);
當分配到高端內存頁時,它不能直接尋址,必須調用調用kmap()函數將高端內存映射到內存地址空間。該映射將持續到kunmap()位置:
void *kunmap(struct page *page);
所謂暫時,指的是映射應該在不需要的時候立即撤銷。請記住,128MB不足以映射3.2GB。最好的編程習慣是在不需要時取消高端內存映射。這就是必須在每次訪問高端內存頁面時輸入kmap()-kunmap序列的原因了,
該函數適用于高端內存和低端內存,也就是說,如果頁面結構駐留在低端內存中,那么返回的是頁面的虛擬地址(因為低端內存頁面已經有永久映射)。如果頁面屬于高端內存,則在內核頁表中創建永久映射,并返回地址。
2 映射內核內存到用戶空間
映射物理地址是其中一個有用的功能,特別是在嵌入式系統中。有時可能想要與用戶空間共享部分內核內存。如前所述,CPU在用戶空間時以非特權模式運行。要讓進程訪問內核內存區域,需要將該區域映射到進程地址空間。
使用remap_pfn_range
remap_pfn_range()將物理內存(通過內核邏輯地址)映射到用戶空間進程。它對于實現mmap()特別有用。
在文件上調用mmap()系統調用后,CPU切換到特權模式,運行相應的file_operations.mmap()內核函數,它反過來調用remap_pfn_range()。這將產生映射區域的PTE,將其賦給進程,當然還有不同的保護標志,進程的VMA列表更新為新的VMA項,這將使用PTE訪問相同的內存。
這樣,內核不是通過復制來浪費內存,而只是復制PTE,但是內核和用戶空間PTE具有不同的屬性。remap_pfn_range()原型如下:
int remap_pfn_range ( struct vm_area_struct * vma,unsigned long virt_addr,unsigned long pfn,unsigned long size,pgprot_t prot);
- vma: 這個我們不用擔心,因為在調用file_operations.mmap函數時,mmap調用do_mmap()會創建一個新的VMA并初始化,此vma就是創建的新的VMA,加入進程的虛擬地址空間里,這個已經確定了。
- virt_addr:VMA開始位置的用戶虛擬地址(vma->vm_start),這將導致映射的虛擬地址范圍位于virt_addr~virt_addr+size
- pfn:所映射內核內存區域的頁面幀碼,它對應于通過PAGE_SHIFT位右移得到的物理地址。產生pfn時應應該考慮vma偏移量。由于vma結構的vm_pgoff字段在頁碼中包含偏移值,因此需要以字節形式精確提取偏移量:
offset= vma->vm_pgoff<<PAGE_SHIFT
.最后,pnf=virt_to_phys(buffer+offset)>>PAGE_SHIFT
. - size:需要建立映射的VMA的大小,以字節為單位
- prot:代表新VMA所要求的保護。驅動程序可以修改默認值,但應該使用OR運算符將vma->vm_page_port中的值作基礎,因為它的某些位已經由用戶空間設置,其中一些標志如下:
- VM_IO:指定設備內存映射I/O
- VM_DONTCOPY:告訴內核不要在分叉上復制該vma
- VM_DONTEXPAND:防止vma通過mremap擴展
- VM_DONTDUMP:禁止在核心轉儲內包含vma
使用io_remap_pfn_range
前面討論的remap_pfn_range()不適用于將I/O內存映射到用戶空間。在這種情況下,相應的函數是io_remap_pfn_range(),它們的參數相同,唯一改變的是PFN的來源,其原型如下:
int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_addr,unsigned long phys_addr, unsigned long size, pgprot_t prot);
當試圖將I/O內存映射到用戶空間時,不需要使用ioremap(),ioremap()用于內核映射(將I/O內存映射到內核地址空間)。
只需要將真實的物理I/O地址(通過PAGE_SHIFT向下移位生成PFN)直接傳遞給io_remap_pfn_range()。即使有一些體系將io_remap_pfn_range()定義為remap_pfn_range(),但在其他體系結構中并非如此,考慮到移植能力,只有在PFN參數指向RAM的情況下,才使用remap_pfn_range(),在pys_addr指向I/O聶村的情況下,才使用io_remap_pfn_range()。
mmap文件操作
內核mmap函數是struct file_operations結構的一部分,當用戶執行系統調用mmap(2),把物理內存映射到用戶虛擬地址時才執行它。出于安全考慮,用戶空間進程不能直接訪問設備內存,因此,用于空間進程使用mmap()系統調用將該設備映射到調用進程的虛擬地址空間。在映射之后,用戶空間進程可以通過返回的地址直接寫入設備內存。
#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);int munmap(void *addr, size_t length);
用戶空間的mmap()會通過系統調用調用內核的do_mmap()函數。
do_mmap()函數會:
- 首先創建一個新的VMA并初始化,然后加入進程的虛擬地址空間里
- 調用底層的mmap函數建立VMA和實際物理地址的映射(建立頁表)
什么是底層的mmap函數呢?在不同的設備是不一樣的。比如說我們映射的是一個普通文件,底層文件系統已經幫我們實現了mmap,我們可以直接使用,但是如果我們新寫了一個驅動,我們想為驅動提供mmap的接口,那么就需要我們實現mmap的接口,設備驅動的mmap實現主要是將這個物理設備的可操作區域映射到一個進程的虛擬地址空間,這樣用戶空間就可以直接采用指針的方式訪問設備的可操作區域。在驅動中的mmap實現主要完成一件事,就是建立設備的可操作區域到進程虛擬空間地址的映射過程。
- addr:映射開始的用戶空間虛擬地址,如果指定NULL,則自動確定正確的地址
- length:指定映射長度
- prot:指定VMA的權限
- flags:決定映射類型(私有還是共享)
- fd:設備文件描述符
- offset:指定映射區的偏移量(在物理內存里面)
建立VMA和實際物理地址的映射
在 linux 驅動中建立映射關系的方法主要有如下兩種:
- 一次性映射 —— 在 mmap 回調函數中,一次性建立好整塊內存的映射關系,通常以
remap_pfn_range()
為代表 。 - Page Fault —— mmap 先不建立映射關系,等上層觸發缺頁異常時,在 fault 中斷處理函數中建立映射關系,缺哪塊補哪塊,通常以 vm_insert_page() 為代表。
而內存分配的時機也會影響驅動程序的設計,大致分為如下三種:
- 在 mmap 系統調用之前分配
- 在 mmap 系統調用過程中分配
- 在 fault 中斷處理函數中分配
因此不同的分配時機 + 不同的映射機制,就會得到不同的 mmap 的實現策略。
下面就以示例代碼的形式為大家展示幾種典型的 mmap 驅動實現方式。
mmap 之前分配 + 一次性映射
描述:
- 驅動初始化時先分配好 3 個 PAGE。
- 上層執行 mmap 系統調用時,在底層 mmap 回調函數中通過 remap_pfn_range() 一次性建立好所有的映射關系,并將映射后的起始虛擬地址返回給應用程序。
- 應用程序使用返回的虛擬地址進行內存讀寫操作。
驅動代碼:
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/slab.h>static void *kaddr;static int my_mmap(struct file *file, struct vm_area_struct *vma)
{return remap_pfn_range(vma, vma->vm_start,(virt_to_phys(kaddr) >> PAGE_SHIFT) + vma->vm_pgoff,vma->vm_end - vma->vm_start, vma->vm_page_prot);
}static struct file_operations my_fops = {.owner = THIS_MODULE,.mmap = my_mmap,
};static struct miscdevice mdev = {.minor = MISC_DYNAMIC_MINOR,.name = "my_dev",.fops = &my_fops,
};static int __init my_init(void)
{kaddr = kzalloc(PAGE_SIZE * 3, GFP_KERNEL);return misc_register(&mdev);
}
module_init(my_init);
mmap 之前分配 + Page Fault
描述:
- 驅動初始化時預先分配好 3 個 PAGE。
- 上層執行 mmap 系統調用,底層驅動在 mmap 回調函數中不建立映射關系,而是將本地實現的 vm_ops 掛接到進程的 vma->vm_ops 指針上,然后函數返回。
- 上層獲取到一個未經映射的進程地址空間,并對其進行內存讀寫操作,導致觸發缺頁異常。缺頁異常最終會調用前面掛接的 vm_ops->fault() 回調接口,在該接回調中通過 vm_insert_page() 建立物理內存與用戶地址空間的映射關系。
- 異常返回后,應用程序就可以繼續之前被中斷的讀寫操作了。
注意:這種情況每次 Page Fault 中斷只能映射一個 Page
驅動代碼:
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/slab.h>static void *kaddr;static int my_fault(struct vm_fault *vmf)
{struct vm_area_struct *vma = vmf->vma;int offset, ret;offset = vmf->pgoff * PAGE_SIZE;ret = vm_insert_page(vma, vmf->address, virt_to_page(kaddr + offset));if (ret)return VM_FAULT_SIGBUS;return VM_FAULT_NOPAGE;
}static const struct vm_operations_struct vm_ops = {.fault = my_fault,
};static int my_mmap(struct file *file, struct vm_area_struct *vma)
{vma->vm_flags |= VM_MIXEDMAP;vma->vm_ops = &vm_ops;return 0;
}static struct file_operations my_fops = {.owner = THIS_MODULE,.mmap = my_mmap,
};static struct miscdevice mdev = {.minor = MISC_DYNAMIC_MINOR,.name = "my_dev",.fops = &my_fops,
};static int __init my_init(void)
{kaddr = kzalloc(PAGE_SIZE * 3, GFP_KERNEL);return misc_register(&mdev);
}
module_init(my_init);
Page Fault 中分配 + 映射
映射的過程和示例二完全一樣,只是內存分配的時機是在 page fault 中斷處理函數中進行的。
這里為了簡化代碼,總共只分配一個 page,多個 page 可通過 vmf->pgoff 來進行區分。
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/slab.h>static struct page *page;static int my_fault(struct vm_fault *vmf)
{struct vm_area_struct *vma = vmf->vma;int ret;if (!page)page = alloc_page(GFP_KERNEL);ret = vm_insert_page(vma, vmf->address, page);if (ret)return VM_FAULT_SIGBUS;return VM_FAULT_NOPAGE;
}static const struct vm_operations_struct vm_ops = {.fault = my_fault,
};static int my_mmap(struct file *file, struct vm_area_struct *vma)
{vma->vm_flags |= VM_MIXEDMAP;vma->vm_ops = &vm_ops;return 0;
}static struct file_operations my_fops = {.owner = THIS_MODULE,.mmap = my_mmap,
};static struct miscdevice mdev = {.minor = MISC_DYNAMIC_MINOR,.name = "my_dev",.fops = &my_fops,
};static int __init my_init(void)
{return misc_register(&mdev);
}
module_init(my_init);