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與作者討論。