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与作者讨论。