mmap實現用戶空間和內核空間 信息交互

http://blog.csdn.net/arethe/article/details/6941112

用戶空間與內核空間的通信方法有很多,如ioctl,procfs,sysfs等。但是,這些方法僅能在用戶空間與內核空間之間交互簡單的數據。如果要實現大批量數據的傳遞,最好的方法就是共享內存。利用設備驅動模型中的mmap函數,可以很容易實現一個簡單的共享內存。本文通過具體實例,介紹一下這種共享內存的實現方法。

         系統調用mmap通常用來將文件映射到內存,以加快該文件的讀寫速度。當用mmap操作一個設備文件時,可以將設備上的存儲區域映射到進程的地址空間,從而以內存讀寫的方法直接控制設備。如果我們在內核模塊裏申請了一段內存區域,也可以利用此方法,將這個過程映射到用戶空間,以實現內核空間與用戶空間之間的共享內存。

         Linux中的每個進程都有一個獨立的地址空間,在內核中使用數據結構mm_struct表示。而一個進程的地址空間,由多個vm_area組成。每個vm_area都映射了一段具體的物理內存空間或IO空間。我們以圖形環境Xorg爲例,在/proc/1105(Xorg的PID)/maps文件中可以看到:
[code]

b6720000-b6721000 rw-p 0000b000 08:01 5349666    /lib/libnss_files-2.11.so

b6744000-b674d000 r-xp 00000000 08:01 3344582    /usr/lib/xorg/modules/input/evdev_drv.so

b674d000-b674e000 rw-p 00009000 08:01 3344582    /usr/lib/xorg/modules/input/evdev_drv.so

b674e000-b67c7000 rw-p 00000000 00:00 0

b67c7000-b67c8000 rw-s f8641000 00:0f 11265      /dev/nvidia0
[/code]

         這只是maps文件中的部分節選。其中的每個地址段都對應一個vma。將物理內存映射到用戶地址空間的過程,可以概括爲2部分。首先,申請一個新的vma。其次爲物理內存頁面創建頁表項,並將該頁表項關聯到vma上。

         在我們調用系統調用mmap時,內核中的sys_mmap函數首先根據用戶提供給mmap的參數(如起始地址、空間大小、行爲修飾符等)創建新的vma。然後調用相應文件的file_operations中的mmap函數。如果是設備文件,那麼file_operations中的mmap函數由設備驅動的編寫者實現。而在mmap中,我們僅僅需要完成頁表項的創建即可。

         下面我們通過一個實例,來詳細說明這種共享內存的實現方法。本文借鑑了《情景分析》中的方法,一段代碼一段代碼的分析。
[code]

  1#include <linux/module.h>

  2#include <linux/kernel.h>

  3#include <linux/fs.h>

  4#include <linux/cdev.h>

  5#include <linux/mm.h>

  6#include <linux/gfp.h>

  7

  8MODULE_LICENSE("GPL");

  9

 10int dev_major = 256;

 11int dev_minor = 0;

 12

 13char* shmem;

 14#define SHM_SIZE 1 //1 PAGE

 15struct page*shm_page;

 16

 17int symboler_open(struct inode*, struct file*);

 18

 19int symboler_release(struct inode*,struct file*);

 20

 21ssize_t symboler_read(struct file*,char *,size_t, loff_t *);

 22

 23ssize_t symboler_write(struct file*,const char*,size_t, loff_t *);

 24

 25int symboler_mmap (struct file*, struct vm_area_struct *);

 26

 27long symboler_ioctl(struct file*,unsigned int, unsigned long);

 28

 29struct file_operations symboler_fops ={

 30         owner:THIS_MODULE,

 31         open: symboler_open,

 32         release:symboler_release,

 33         read:  symboler_read,

 34         write:symboler_write,

 35         unlocked_ioctl:symboler_ioctl,

 36         mmap:  symboler_mmap,

 37};

38

 39struct symboler_dev{

 40         int     sym_var;

 41         struct   cdev    cdev;

 42};

 43

 44struct symboler_dev     *symboler_dev;
[/code]

         首先創建設備驅動均需要的file_operations數據結構。dev_major與dev_minor分別是設備文件的主設備號與次設備號。全局變量shmem是指向共享內存區域的指針,供內核程序在操作此共享內存時使用。SHM_SIZE表示共享內存區域的大小,以頁面數爲單位。指針shm_page指向共享內存中起始頁面的page結構。指針symboler_dev表示我們虛擬出的字符設備。
[code]

46 int symboler_open(struct inode*inode, struct file*filp)

 47{

 48         printk("%s()is called.\n", __func__);

 49

 50         return 0;

 51}

 52

 53int symboler_release(struct inode*inode, struct file*filp)

 54{

 55         printk("%s()is called.\n", __func__);

 56

 57         return 0;

 58}

 59

 60ssize_t symboler_read(struct file*filp, char *buf, size_t len, loff_t *off)

 61{

 62         printk("%s()is called.\n", __func__);

 63

 64         return 0;

 65}

 66

 67ssize_t symboler_write(struct file*filp, const char*buf, size_t len, loff_t *off)

 68{

 69         printk("%s()is called.\n", __func__);

 70

 71         return 0;

 72}
[/code]

         這些是file_operations中的打開、關閉與讀寫函數。由於本文僅僅展示一個簡單的示例,這些函數中沒有任何操作。
[code]

74 void symboler_vma_open(struct vm_area_struct *vma)

 75{

 76         printk("%s()is called.\n", __func__);

 77}

 78

 79void symboler_vma_close(struct vm_area_struct *vma)

 80{

 81         printk("%s()is called.\n", __func__);

 82}

 83

 84static struct vm_operations_struct symboler_remap_vm_ops = {

 85         .open =symboler_vma_open,

 86         .close =symboler_vma_close,

 87};
[/code]

         這段代碼實現了vma的操作方法集合。vma的所有操作都定義在數據結構vm_operations_struct中。在這裏,我們也無需添加任何操作。
[code]

89int symboler_mmap (struct file*filp, struct vm_area_struct *vma)

 90{

 91         printk("%s()is called.\n", __func__);

 92         if(remap_pfn_range(vma, vma->vm_start, page_to_pfn(shm_page),vma->vm_end -vma->vm_start, vma->vm_page_prot))

 93                 return -EAGAIN;

 94

 95         vma->vm_ops =&symboler_remap_vm_ops;

 96         symboler_vma_open(vma);

 97

 98         return 0;

 99}
[/code]

         這就是最關鍵的mmap操作-- symboler_mmap。當用戶空間使用系統調用mmap操作我們的設備文件時,最終會執行到symboler_mmap。函數remap_pfn_range用來爲一段物理地址(RAM中的地址)建立新的頁表。它的原型如下:

int remap_pfn_range(struct vm_area_struct *vma,unsigned longaddr, unsigned longpfn, unsigned longsize, pgprot_tprot);

         該函數將爲處於virt_addr與virt_addr+size之間的虛擬內存區域建立頁表。參數@vma表示虛擬區域,@pfn所代表的頁將被映射到該區域內。參數@virt_addr表示重新映射時起始的用戶虛擬地址。參數@pfn爲與物理內存對應的頁幀號。參數@size以字節爲單位,表示被映射區域的大小。參數@prot爲“保護(protection)”屬性。在執行symboler_mmap時,代表虛擬地址區域的vma結構已由sys_mmap創建並初始化完畢,並且作爲參數供symboler_mmap使用。

         在這段代碼中,我們使用函數page_to_pfn(shm_page)將表示物理頁面的page結構轉換爲其對應的頁幀號。這個函數的實現很簡單。在內核中,所有物理頁面的page結構均存放在數組vmemmap中。因此使用參數shm_page減去vmemmap即可得到shm_page對應的頁幀號。
[code]

111 intsymboler_init(void)

112 {

113         intret,err;

114

115         dev_tdevno =MKDEV(dev_major,dev_minor);

116

125         ret= register_chrdev_region(devno,1, "symboler");

126

127         if(ret < 0)

128         {

129                 printk("symboler register failure.\n");

130                 return ret;

131         }

132         else

133                 printk("symboler register successfully.\n");

134

135

136         symboler_dev = kmalloc(sizeof(struct symboler_dev), GFP_KERNEL);

137

138         if(!symboler_dev)

139         {

140                 ret = -ENOMEM;

141                 printk("create device failed.\n");

142         }

143         else

144         {

145                 symboler_dev->sym_var =0;

146                 cdev_init(&symboler_dev->cdev, &symboler_fops);

147                 symboler_dev->cdev.owner= THIS_MODULE;

148                 err = cdev_add(&symboler_dev->cdev,devno, 1);

149

150                 if(err <0)

151                         printk("Add device failure\n");

152         }

153

154         shm_page = alloc_pages(__GFP_WAIT, SHM_SIZE);

155         shmem= page_address(shm_page);

156         strcpy(shmem, "hello,mmap\n");

157

158         return ret;

159 }
[/code]

         宏MKDEV將主設備號與次設備號組合成一個32位整數。函數register_chrdev_region將我們的字符設備註冊到系統中。第145行到第148行初始化了我們的字符設備。

         第154行用alloc_pages申請到了我們需要的頁面,並返回該區域第一個頁面的page結構。page_address()函數將page結構轉換成內核中可以直接訪問的線性地址。對低端內存而言,將物理地址加上3G(32位並且沒有使能PAE的處理器)即可得到線性地址。而將頁幀號左移12位,便可以得到對應的物理地址。因此,page_address的實現也非常簡單。有興趣的讀者可以到源碼樹中查看該函數的實現方法。Shmem此時便指向了我們剛申請的內存區域的起始地址。可以對其進行直接讀寫操作。我們在例子中,將字符串“hello,mmap”寫到了共享區域中。

         內核代碼的主要部分介紹完了。主要思想是建立一個模擬的字符設備,在它的驅動程序中申請一塊物理內存區域,並利用mmap將這段物理內存區域映射到進程的地址空間中。利用page_address將其轉換爲內核空間中可以使用的線性地址。當然,我還需要在/dev下建立一個設備文件,執行如下命令即可:

mknod  /dev/shm c  256  0

         下面我們再看看用戶空間中,應該如何獲得共享內存區域的地址。
[code]

  1#include <stdio.h>

  2#include <fcntl.h>

  3#include <unistd.h>

  4#include <sys/types.h>

  5#include <sys/stat.h>

  6#include <sys/mman.h>

  7

  8int main(void)

  9{

 11         intfd;

 12         char*mem_start;

 13

 14         fd= open("/dev/shm",O_RDWR);

 15

 19         if((mem_start =mmap(NULL,4096, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0)) == MAP_FAILED)

 20         {

 21                 printf("mmap failed.\n");

 22                 exit(0);

 23         }

 24

 25         printf("mem:%s\n", mem_start);

 28

 29         return 0;

 30}   
[/code]

         運行程序後,便可以輸出我們在內核中寫入共享內存的字符串“hello,mmap”。到這裏,一個簡單的共享內存模型就介紹完了。如果本文的敘述中有錯誤,歡迎大家通過Email與作者討論。

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