在Linux系統中,一般有32位(4GB)的地址空間,進程的4GB內存空間被分為兩個部分 — 用戶空間與內核空間,用戶空間地址一般分布為0 ~ 3GB(即PAGE_OFFSET,一般等於0xc0000000),剩下的3 ~ 4GB則為內核空間。進程與內核不各自使用4GB的空間,好處就是進程進入內核不需要切換頁表,降低了進出內核的消耗。內核空間從低地址到高地址又分為:物理內存映射區、vmalloc 虛擬內存分配區、高端內存映射區、保留區。每個進程的用戶空間都是完全獨立的,用戶進程各自有不同的頁表。而內核空間是由內核負責映射,它並不會跟著進程改變,是固定的。內核空間地址有自己的頁表,內核的虛擬地址空間獨立於其他用戶進程。
一、數據結構
用戶進程創建後可以訪問整個用戶空間的虛擬地址,這段空間是未分段的線性地址范圍,在內核中進程地址空間以及與之相關的所有信息都保存在 mm_struct 中,該結構出現在進程控制結構 task_struct 中。進程用到的每段連續有效地址范圍稱為內存區,一個內存區由 vm_area_struct 描述符表示,每個內存區描述符都描述它所表示的一段連續地址區間。不同的內存區有不同的保護方案和特點,比如程序代碼段的某些部分標記為只讀,而其他部分標記為可寫或可執行。
1、mm_struct
[cpp]
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache; /* last find_vma result */
...
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
int map_count; /* number of VMAs */
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* Protects page tables and some counters */
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
...
unsigned long total_vm, locked_vm, shared_vm, exec_vm;
unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
unsigned long start_code, end_code, start_data, end_data; // 進程在內存中代碼段、數據段的起始和結束地址
unsigned long start_brk, brk, start_stack; // 進程堆的起始和結束地址、棧的起始地址
unsigned long arg_start, arg_end, env_start, env_end; // 參數、環境段的起始和結束地址
...
};
其中 mmap 參數表示進程的所有內存區描述符組成鏈表的頭節點地址,mm_struct 通過 mmap訪問該鏈表,而 vm_area_struct 中的 vm_next 指針將各個內存區鏈接起來;mmap_cache 指向進程最後一次訪問的內存區描述符指針,用於提高訪問效率;mm_users 存放訪問該進程地址空間的進程數量;mm_count 是對 mm_struct 的使用統計,其值為0時說明沒有進程使用則將其回收;map_count 存在在進程地址空間中的內存區數量,即 vm_area_struct 描述符數量。
2、vm_area_struct
[cpp]
struct vm_area_struct {
struct mm_struct * vm_mm; /* The address space we belong to. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
struct rb_node vm_rb;
...
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
...
};
其中 vm_mm 指向該內存區所屬的進程地址空間;vm_start 和 vm_end 表示該虛擬內存區的起始和結束地址,考慮到性能問題,內存區的起始地址必須是頁面大小的整數倍;vm_next 指向該進程的下一個虛擬內存區;vm_ops 用於操作特定的虛擬內存區,包括打開、關閉、反映射內存區等操作。
二、編程接口
在用戶空間動態申請內存的函數為 malloc,釋放函數為 free。內核空間申請內存涉及的函數主要包括 kmalloc、__get_free_pages 和 vmalloc 等。kmalloc 和 __get_free_pages 申請的內存位於物理內存映射區域,而且在物理也是連續的,它們與真實的物理地址只有一個固定的偏移,因此存在較簡單的轉換關系。而 vmalloc 在虛擬內存空間給出一塊連續的內存區,實質上,這片連續的虛擬內存在物理內存中並不一定連續,其申請的虛擬內存和物理內存之間也沒有簡單的換算關系。
1、kmalloc
[cpp]
/**
* kmalloc - 分配一塊指定大小的內存
* @size: 內存區的大小
* @flags: 分配標志,可能值有 GFP_ATOMIC、GFP_KERNEL 等
*
* Note: 當標志為 GFP_KERNEL 的時候可能會引起睡眠
*/
void *kmalloc(size_t size, int flags);
使用 kmalloc 分配的內存應該用 kfree 釋放。
2、mmap
[cpp]
/**
* mmap - 將物理地址映射至用戶空間
* @addr: 指定文件應被映射到進程空間的起始地址,一般被指定為NULL,此時選擇起始地址的任務留給內核來完成
* @len: 映射到用戶空間的字節數
* @prot: 指定被映射空間的訪問權限,可取如下幾個值的或:
* PROT_READ(可讀),PROT_WRITE (可寫),PROT_EXEC (可執行),PROT_NONE(不可訪問)
* @flags: 由以下幾個常值指定:MAP_SHARED,MAP_PRIVATE,MAP_FIXED,MAP_ANON
* @fd: 映射到用戶空間的文件的描述符,一般由open()返回
* 同時,fd可以指定為-1,此時須指定flags參數中的MAP_ANON,表明進行的是匿名映射
* @offset: 被映射內存區在文件中的偏移值
*/
void* mmap(void * addr, size_t len, int prot, int flags, int fd, off_t offset);
該函數映射文件描述符 fd 指定文件的 [offset, offset + len] 物理內存區至調用進程的 [addr, addr + len] 的用戶空間虛擬內存區,通常用於內存的共享或者用戶空間程序控制硬件設備,函數的返回值為最後文件映射到用戶空間的地址,進程可直接操作該地址。當用戶調用 mmap 的時候,內核會根據 fd 找到相對應設備驅動或者文件系統的file_operations,比如在 camera 裡面就是 v4l2_file_operations,然後調用裡面定義的 mmap 操作,一個常見的處理流程如下:
[cpp]
static int xxx_mmap(struct file *file, struct vm_area_struct *vma)
{
int ret;
unsigned long size = vma->vm_end - vma->vm_start; // 計算將要映射的內存大小
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; // 計算映射內存區的偏移值
struct xxx_info *info = file->private_data;
mutex_lock(&info->lock);
if (size > info->gbuffers * info->gbufsize - offset) { // 如果要映射的區域過大則返回錯誤值
mutex_unlock(&info->lock);
return -EINVAL;
}
if (!info->mmap_start_base) { // 檢測物理地址是否有效
mutex_unlock(&info->lock);
return -EIO;
}
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);// 對映射區域增加 nocache 屬性
ret = remap_pfn_range(vma, vma->vm_start, // 調用 remap_pfn_range 函數創建頁表
(uint32_t)(info->mmap_start_base + offset) >> PAGE_SHIFT,
size, vma->vm_page_prot);
if(ret) {
mutex_unlock(&info->lock);
return -EAGAIN;
}
mutex_unlock(&info->lock);
return 0;
}
在驅動程序中,我們能使用 remap_pfn_range 映射內存中的保留頁和設備 I/O 內存,另外,kmalloc 申請的的內存若要被映射到用戶空間可以通過mem_map_reserve 設置為保留頁後進行,示例代碼如下:
[cpp]
buffer = kmalloc(size, GFP_KERNEL); // 申請buffer
for(page = virt_to_page(buffer); page < virt_to_page(buffer + size); page++)
mem_map_reserve(page); // 設置頁為保留頁
我們再看一下 remap_pfn_range 的說明:
[cpp]
/**
* remap_pfn_range - 映射物理地址到用戶空間
* @vma: 虛擬內存區域指針,由內核根據用戶請求自動填充,函數將物理地址區域映射至該虛擬內存區
* @addr: 虛擬內存區的起始地址,函數將為 addr ~ addr + size 的虛擬地址區域構造頁表
* @pfn: 被映射物理地址的頁幀號,即將物理地址右移PAGE_SHIFT(12位,一般PAGE_SIZE為4KB)
* @size: 被映射內存區域大小
* @prot: 頁表的保護屬性
*
* Note: 調用者需要持有 mm 信號量
*/
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot);
總結 mmap 的執行順序如下:
(1)在用戶進程創建一個虛擬內存區 - vma
(2)驅動程序調用 ramap_pfn_range 為被映射的物理內存區構造頁表
(3)將頁表分配給虛擬內存區 - vma
3、ioremap_nocache
[cpp]
/**
* ioremap_nocache - 將物理地址映射至內核空間
* @phys_addr: 物理地址起始值
* @size: 要映射的空間大小
*
* Note: 必須由 iounmap() 釋放
*/
void __iomem *ioremap_nocache (unsigned long phys_addr, unsigned long size)
{
return __ioremap(phys_addr | MEM_NON_CACHEABLE, size, 0);
}
在驅動中申請到一片連續的物理內存後,通常需要將物理地址映射到內核的虛擬地址空間,然後就可以在驅動代碼裡面直接使用虛擬地址訪問那段物理內存了,用法如下:
[cpp]
vbase = ioremap_nocache((unsigned long)phys_start_base, cam_total_buf_size);