现在的位置: 首页 > 综合 > 正文

利用mmap实现用户空间与内核空间的共享内存通信

2013年08月11日 ⁄ 综合 ⁄ 共 6905字 ⁄ 字号 评论关闭

利用mmap实现用户空间与内核空间的共享内存通信

秦白衣

Arethe Qin

arethe.kernel@gmail.com

 

         用户空间与内核空间的通信方法有很多,如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与作者讨论。

抱歉!评论已关闭.