1. 程式人生 > >[轉載]Linux驅動mmap內存映射

[轉載]Linux驅動mmap內存映射

創建 動態分配 this 內存 mem 程序 進行 並且 dff

原文地址:https://www.cnblogs.com/wanghuaijun/p/7624564.html

mmap在linux哪裏?

技術分享圖片

什麽是mmap?

上圖說了,mmap是操作這些設備的一種方法,所謂操作設備,比如IO端口(點亮一個LED)、LCD控制器、磁盤控制器,實際上就是往設備的物理地址讀寫數據。

但是,由於應用程序不能直接操作設備硬件地址,所以操作系統提供了這樣的一種機制——內存映射,把設備地址映射到進程虛擬地址,mmap就是實現內存映射的接口。

操作設備還有很多方法,如ioctl、ioremap

mmap的好處是,mmap把設備內存映射到虛擬內存,則用戶操作虛擬內存相當於直接操作設備了,省去了用戶空間到內核空間的復制過程,相對IO操作來說,增加了數據的吞吐量。

什麽是內存映射?

既然mmap是實現內存映射的接口,那麽內存映射是什麽呢?看下圖

技術分享圖片

每個進程都有獨立的進程地址空間,通過頁表和MMU,可將虛擬地址轉換為物理地址,每個進程都有獨立的頁表數據,這可解釋為什麽兩個不同進程相同的虛擬地址,卻對應不同的物理地址。

什麽是虛擬地址空間?

每個進程都有4G的虛擬地址空間,其中3G用戶空間,1G內核空間(linux),每個進程共享內核空間,獨立的用戶空間,下圖形象地表達了這點

技術分享圖片

驅動程序運行在內核空間,所以驅動程序是面向所有進程的。

用戶空間切換到內核空間有兩種方法:

(1)系統調用,即軟中斷

(2)硬件中斷

虛擬地址空間裏面是什麽?

了解了什麽是虛擬地址空間,那麽虛擬地址空間裏面裝的是什麽?看下圖

技術分享圖片

虛擬空間裝的大概是上面那些數據了,內存映射大概就是把設備地址映射到上圖的紅色段了,暫且稱其為“內存映射段”,至於映射到哪個地址,是由操作系統分配的,操作系統會把進程空間劃分為三個部分:

(1)未分配的,即進程還未使用的地址

(2)緩存的,緩存在ram中的頁

(3)未緩存的,沒有緩存在ram中

操作系統會在未分配的地址空間分配一段虛擬地址,用來和設備地址建立映射,至於怎麽建立映射,後面再揭曉。

現在大概明白了“內存映射”是什麽了,那麽內核是怎麽管理這些地址空間的呢?任何復雜的理論最終也是通過各種數據結構體現出來的,而這裏這個數據結構就是進程描述符。從內核看,進程是分配系統資源(CPU、內存)的載體,為了管理進程,內核必須對每個進程所做的事情進行清楚的描述,這就是進程描述符,內核用task_struct結構體來表示進程,並且維護一個該結構體鏈表來管理所有進程。該結構體包含一些進程狀態、調度信息等上千個成員,我們這裏主要關註進程描述符裏面的內存描述符(struct mm_struct mm)

內存描述符

具體的結構,請參考下圖

技術分享圖片

現在已經知道了內存映射是把設備地址映射到進程空間地址(註意:並不是所有內存映射都是映射到進程地址空間的,ioremap是映射到內核虛擬空間的,mmap是映射到進程虛擬地址的),實質上是分配了一個vm_area_struct結構體加入到進程的地址空間,也就是說,把設備地址映射到這個結構體,映射過程就是驅動程序要做的事了。

內存映射的實現

以字符設備驅動為例,一般對字符設備的操作都如下框圖

技術分享圖片

而內存映射的主要任務就是實現內核空間中的mmap()函數,先來了解一下字符設備驅動程序的框架

技術分享圖片

以下是mmap_driver.c的源代碼

//所有的模塊代碼都包含下面兩個頭文件
#include <linux/module.h>
#include <linux/init.h>
 
#include <linux/types.h> //定義dev_t類型
#include <linux/cdev.h> //定義struct cdev結構體及相關操作
#include <linux/slab.h> //定義kmalloc接口
#include <asm/io.h>//定義virt_to_phys接口
#include <linux/mm.h>//remap_pfn_range
#include <linux/fs.h>
 
#define MAJOR_NUM 990
#define MM_SIZE 4096
 
static char driver_name[] = "mmap_driver1";//驅動模塊名字
static int dev_major = MAJOR_NUM;
static int dev_minor = 0;
char *buf = NULL;
struct cdev *cdev = NULL;
 
static int device_open(struct inode *inode, struct file *file)
{
    printk(KERN_ALERT"device open\n");
    buf = (char *)kmalloc(MM_SIZE, GFP_KERNEL);//內核申請內存只能按頁申請,申請該內存以便後面把它當作虛擬設備
    return 0;
}
 
static int device_close(struct inode *indoe, struct file *file)
{
    printk("device close\n");
    if(buf)
    {
        kfree(buf);
    }
    return 0;
}
 
static int device_mmap(struct file *file, struct vm_area_struct *vma)
{
    vma->vm_flags |= VM_IO;//表示對設備IO空間的映射
    vma->vm_flags |= VM_RESERVED;//標誌該內存區不能被換出,在設備驅動中虛擬頁和物理頁的關系應該是長期的,應該保留起來,不能隨便被別的虛擬頁換出
    if(remap_pfn_range(vma,//虛擬內存區域,即設備地址將要映射到這裏
                       vma->vm_start,//虛擬空間的起始地址
                       virt_to_phys(buf)>>PAGE_SHIFT,//與物理內存對應的頁幀號,物理地址右移12位
                       vma->vm_end - vma->vm_start,//映射區域大小,一般是頁大小的整數倍
                       vma->vm_page_prot))//保護屬性,
    {
        return -EAGAIN;
    }
    return 0;
}
 
static struct file_operations device_fops =
{
    .owner = THIS_MODULE,
    .open  = device_open,
    .release = device_close,
    .mmap = device_mmap,
};
 
static int __init char_device_init( void )
{
    int result;
    dev_t dev;//高12位表示主設備號,低20位表示次設備號
    printk(KERN_ALERT"module init2323\n");
    printk("dev=%d", dev);
    dev = MKDEV(dev_major, dev_minor);
    cdev = cdev_alloc();//為字符設備cdev分配空間
    printk(KERN_ALERT"module init\n");
    if(dev_major)
    {
        result = register_chrdev_region(dev, 1, driver_name);//靜態分配設備號
        printk("result = %d\n", result);
    }
    else
    {
        result = alloc_chrdev_region(&dev, 0, 1, driver_name);//動態分配設備號
        dev_major = MAJOR(dev);
    }
    
    if(result < 0)
    {
        printk(KERN_WARNING"Cant‘t get major %d\n", dev_major);
        return result;
    }
    
    
    cdev_init(cdev, &device_fops);//初始化字符設備cdev
    cdev->ops = &device_fops;
    cdev->owner = THIS_MODULE;
    
    result = cdev_add(cdev, dev, 1);//向內核註冊字符設備
    printk("dffd = %d\n", result);
    return 0;
}
 
static void __exit char_device_exit( void )
{
    printk(KERN_ALERT"module exit\n");
    cdev_del(cdev);
    unregister_chrdev_region(MKDEV(dev_major, dev_minor), 1);
}
 
module_init(char_device_init);//模塊加載
module_exit(char_device_exit);//模塊退出
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ChenShengfa");

下面是測試代碼test_mmap.c

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
 
int main( void )
{
    int fd;
    char *buffer;
    char *mapBuf;
    fd = open("/dev/mmap_driver", O_RDWR);//打開設備文件,內核就能獲取設備文件的索引節點,填充inode結構
    if(fd<0)
    {
        printf("open device is error,fd = %d\n",fd);
        return -1;
    }
    /*測試一:查看內存映射段*/
    printf("before mmap\n");
    sleep(15);//睡眠15秒,查看映射前的內存圖cat /proc/pid/maps
    buffer = (char *)malloc(1024);
    memset(buffer, 0, 1024);
    mapBuf = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//內存映射,會調用驅動的mmap函數
    printf("after mmap\n");
    sleep(15);//睡眠15秒,在命令行查看映射後的內存圖,如果多出了映射段,說明映射成功
    
    /*測試二:往映射段讀寫數據,看是否成功*/
    strcpy(mapBuf, "Driver Test");//向映射段寫數據
    memset(buffer, 0, 1024);
    strcpy(buffer, mapBuf);//從映射段讀取數據
    printf("buf = %s\n", buffer);//如果讀取出來的數據和寫入的數據一致,說明映射段的確成功了
    
    
    munmap(mapBuf, 1024);//去除映射
    free(buffer);
    close(fd);//關閉文件,最終調用驅動的close
    return 0;
}

下面是makefile文件

ifneq ($(KERNELRELEASE),)

obj-m := mmap_driver.o

else
KDIR := /lib/modules/3.2.0-52-generic/build

all:
    make -C $(KDIR) M=$(PWD) modules
clean:
    rm -f *.ko *.o *.mod.o *.mod.c *~ *.symvers *.order

endif

下面命令演示一下驅動程序的編譯、安裝、測試過程(註:其他用戶在mknod之後還需要chmod改變權限)

# make    //編譯驅動

# insmod mmap_driver.ko    //安裝驅動

# mknod /dev/mmap_driver c 999 0    //創建設備文件

# gcc test_mmap.c -o test.o    //編譯應用程序

# ./test.o    //運行應用程序來測試驅動程序


拓展:

關於這個過程,涉及一些術語

(1)設備文件:linux中對硬件虛擬成設備文件,對普通文件的各種操作均適用於設備文件

(2)索引節點:linux使用索引節點來記錄文件信息(如文件長度、創建修改時間),它存儲在磁盤中,讀入內存後就是一個inode結構體,文件系統維護了一個索引節點的數組,每個元素都和文件或者目錄一一對應。

(3)主設備號:如上面的999,表示設備的類型,比如該設備是lcd還是usb等

(4)次設備號:如上面的0,表示該類設備上的不同設備

(5)文件(普通文件或設備文件)的三個結構

①文件操作:struct file_operations

②文件對象:struct file

③文件索引節點:struct inode

關於驅動程序中內存映射的實現,先了解一下open和close的流程

(1)設備驅動open流程

技術分享圖片

①應用程序調用open("/dev/mmap_driver", O_RDWR);

②Open就會通過VFS找到該設備的索引節點(inode),mknod的時候會根據設備號把驅動程序的file_operations結構填充到索引節點中(關於mknod /dev/mmap_driver c 999 0,這條指令創建了設備文件,在安裝驅動(insmod)的時候,會運行驅動程序的初始化程序(module_init),在初始化程序中,會註冊它的主設備號到系統中(cdev_add),如果mknod時的主設備號999在系統中不存在,即和註冊的主設備號不同,則上面的指令會執行失敗,就創建不了設備文件)

③然後根據設備文件的索引節點中的file_operations中的open指針,就調用驅動的open方法了。

④生成一個文件對象files_struct結構,系統維護一個files_struct的鏈表,表示系統中所有打開的文件

⑤返回文件描述符fd,把fd加入到進程的文件描述符表中

(2)設備驅動close流程

應用程序調用close(fd),最終可調用驅動的close,為什麽根據一個簡單的int型fd就可以找到驅動的close函數?這就和上面說的三個結構(struct file_operations、struct file、struct inode)息息相關了,假如fd = 3

技術分享圖片

(3)設備驅動mmap流程

由open和close得知,同理,應用程序調用mmap最終也會調用到驅動程序中mmap方法

①應用程序test.mmap.c中mmap函數

void* mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr:映射後虛擬地址的起始地址,通常為NULL,內核自動分配

length:映射區的大小

prot:頁面訪問權限(PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE)

flags:參考網絡資料

fd:文件描述符

offset:文件映射開始偏移量

②驅動程序的mmap_driver.c中mmap函數

上面說了,mmap的主要工作是把設備地址映射到進程虛擬地址,也即是一個vm_area_struct的結構體,這裏說的映射,是一個很懸的東西,那它在程序中的表現是什麽呢?——頁表,沒錯,就是頁表,映射就是要建立頁表。進程地址空間就可以通過頁表(軟件)和MMU(硬件)映射到設備地址上了

技術分享圖片

技術分享圖片

virt_to_phys(buf),buf是在open時申請的地址,這裏使用virt_to_phys把buf轉換成物理地址,是模擬了一個硬件設備,即把虛擬設備映射到虛擬地址,在實際中可以直接使用物理地址。

總結

①從以上看到,內核各個模塊錯綜復雜、相互交叉

②單純一個小小驅動模塊,就涉及了進程管理(進程地址空間)、內存管理(頁表與頁幀映射)、虛擬文件系統(structfile、structinode)

③並不是所有設備驅動都可以使用mmap來映射,比如像串口和其他面向流的設備,並且必須按照頁大小進行映射。

[轉載]Linux驅動mmap內存映射