Linux內核mmap機制

1. 問:如何將物理地址映射到用戶空間的虛擬地址上?


2.linux內核mmap機制

2.1.回顧LED驅動數據流的操作過程

通過分析LED驅動,得出以下結論:
如果利用read,write,ioctl三個系統調用函數實現對LED硬件進行操作,這三個系統調用函數操作數據最終要經過兩次數據拷貝,
分別是用戶空間到內核空間,內核空間到硬件或者硬件到內核,內核到用戶;

如果操作訪問的數據量比較小,對系統性能的影響幾乎可以忽略不計,如果數據量比較大,這種影響不能忽略,例如顯卡,攝像頭,聲卡等;

明確:數據的最終走向要不是用戶到硬件,或者硬件到用戶;

2.2.linux對於這類設備,在數據訪問的時候,爲了提供性能,
利用mmap機制將硬件設備的物理地址直接映射到用戶空間的虛擬地址上,
將來用戶在用戶空間訪問這個虛擬地址就是在訪問對應的物理地址,不再經過內核空間,
將兩次數據拷貝轉換成一次數據拷貝!

3.mmap實現機制

3.1本質目的:就是將設備物理地址(物理內存)映射到用戶空間的虛擬地址(虛擬內存)上,
將來用戶在用戶空間訪問映射的虛擬地址就是在訪問物理地址;不再經過內核空間,將原先的兩次數據拷貝(read,write,ioctl)轉換成一 次

3.2.回憶linux的mmap應用編程:
void *addr;
int fd = open("a.txt", O_RDWR);
addr = mmap(0, 文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 文件偏移量);
memcpy(addr, "hello,world", 12);

參數說明:
0:讓內核幫你在MMAP內存映射區找一塊空閒的內存區域用來映射文件;
addr:內核將找到的空閒的虛擬內存區域的首地址告訴給用戶,那麼即可通過這個地址來訪問硬件

說明:“文件”:太抽象,“文件”最終對應的就是一個硬件設備(內存,閃存,硬盤),對應的一個硬件設備的物理地址;
利用mmap將文件映射到用戶3G虛擬內存的MMAP內存映射區,存映射區的虛擬地址上,
將來用戶訪問這個虛擬地址就是在訪問硬件(文件);不再經過內核空間

3.3.mmap系統調用過程:
1.當應用程序調用mmap,首先調用到C庫的mmap函數
2.C庫的mmap函數將會做以下事情:
2.1.保存mmap系統調用號到R7寄存器
2.2.調用svc觸發軟中斷異常
3.跳轉到內核準備的異常向量表軟中斷的處理入口
3.1.從R7寄存器取出mmap的系統調用號
3.2.在以系統調用號爲索引,在系統調用表中找到對應的內核函數sys_mmap(由內核實現)
4.內核的sys_mmap將會做以下事情:
4.1.內核在用戶3G虛擬內存的MMAP內存映射區找一塊空閒的內存區域,將來映射某個硬件設備;
4.2.一旦找到,內核用struct vm_area_struct創建一個對象來描述這塊空閒的內存區域;
struct vm_area_struct {
unsigned long vm_start; //空閒內存區域的首地址
unsigned long vm_end;//結束地址
pgprot_t vm_page_prot; //訪問權限
unsigned long vm_pgoff; //偏移量
...
};

4.3.最後內核調用底層驅動的mmap函數,
並且內核將描述這塊空閒內存區域的信息傳遞給底層驅動的mmap(對象的首地址傳遞給底層驅動的mmap),
就等價於內核將空閒內存區域的所有屬性告知給底層驅動的mmap

此時此刻,物理地址和用戶虛擬地址做好映射了嗎?內核沒有做映射,直接去調用底層驅動的mmap了

5.底層驅動的mmap函數
struct file_operations {
int (*mmap)(struct file *file, struct vm_area_struct *vma)
}

mmap接口:
功能:底層驅動的mmap永遠只做一件事:將物理地址映射到用戶虛擬地址上;
參數:
file:文件指針;
vma:此指針指向內核創建的描述內核找的空閒內存區域的對象,底層驅動可以通過此指針獲取空閒內區域的屬性(起始地址,結束地址...)
目的:
通過芯片手冊和原理圖能夠獲取設備的物理地址
通過vma指針能夠獲取用戶虛擬內存區域的信息

問:如何將物理地址最終映射到用戶虛擬地址上呢?
答:
明確:此事由底層驅動的mmap來做!

通過調用remap_pfn_range函數來進行;

int remap_pfn_range(struct vm_area_struct *vma,
unsigned long addr,
unsigned long pfn,
unsigned long size,
pgprot_t prot)
函數功能:
1.將已知的物理地址映射到已知的用戶虛擬地址上;
參數:
vma:指向內核創建的描述空閒內存區域的對象
addr:空閒內存區域的首地址;vm_start
pfn:將物理地址右移12位
size:空閒虛擬內存的大小,vm_end - vm_start
prot:訪問權限,bm_prot
切記:映射時指針的虛擬地址和物理地址必須是頁面大小的整數倍;
0xe0200060:配置寄存器物理地址
0xe0200064:數據寄存器物理地址
通過閱讀芯片手冊,發現GPIO所有寄存器的基地址:0xE0200000,並且寄存器存儲空間都是連續的;
其他寄存器的地址只需基地址加一個對應的偏移量即可;所以用mmap做地址映射時,物理地址可以採用0xe0200000,對應的虛擬地址將來也要加上對應的偏移量

物理地址 用戶虛擬地址
0xe0200000 A (vm_start)
0xe0200060 A + 0x60
0xe0200064 A + 0x64

#include <linux/mm.h>

切記:對於GPIO操作的設備,利用mmap訪問操作的時候,記得要把cache功能屏蔽掉!

案例

利用mmap實現開關燈 

#include <linux/init.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>

//vma:指向空閒虛擬內存區域
static int led_mmap(struct file *file,
                        struct vm_area_struct *vma)
{
    //對於GPIO操作的設備,禁止cache功能
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    
    //只做一件事:將物理地址映射到用戶虛擬地址上
    remap_pfn_range(vma, vma->vm_start,
                    0xe0200000 >> 12,
                    vma->vm_end - vma->vm_start,
                        vma->vm_page_prot);
    return 0;
}

//定義初始化硬件操作方法
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .mmap = led_mmap
};
//定義初始化混雜設備對象
static struct miscdevice led_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "myled",
    .fops = &led_fops
};
static int led_init(void)
{
    //註冊混雜設備對象
    misc_register(&led_misc);
    return 0;
}

static void led_exit(void)
{
    //卸載混雜設備對象
    misc_deregister(&led_misc);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");


#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char *argv[])
{
    int fd;
    unsigned char *gpio_vir_base; //0xe0200000對應的用戶虛擬地址
    unsigned long *gpiocon, *gpiodata;//配置和數據的用戶虛擬地址
    
    if (argc != 2) {
        printf("Usage:\n%s <on|off>\n", argv[0]);
        return -1;
    }
    
    fd = open("/dev/myled", O_RDWR);
    if (fd < 0)
        return -1;
    
    //將0xe0200000物理地址,物理內存大小爲0x1000映射到用戶虛擬內存上
    gpio_vir_base = mmap(0, 0x1000, PROT_READ|PROT_WRITE,
                            MAP_SHARED, fd, 0);
    
    //獲取0xe0200060,0xe0200064的用戶虛擬地址
    gpiocon = (unsigned long *)(gpio_vir_base + 0x60);
    gpiodata = (unsigned long *)(gpio_vir_base + 0x64);
   
    //配置GPIO爲輸出口
    *gpiocon &= ~((0xf << 12) | (0xf << 16));
    *gpiocon |= ((1 << 12) | (1 << 16));
    
    if (!strcmp(argv[1], "on")) 
        *gpiodata |= ((1 << 3) | (1 << 4));
    else
        *gpiodata &= ~((1 << 3) | (1 << 4));

    //解除地址映射
    munmap(gpio_vir_base, 0x1000);
    close(fd);
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章