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

目录

摘要:

理论:

实例:


摘要:

        用户空间与内核空间的通信方式很多,如ioctl、procfs、sysfs等。但是,这些方法仅能在用户空间和内核空间之间交互简单的数据。如果要实现大批量的数据的传递,最好的方式就是共享内存。利用设备模型中的mmap函数,可以很容易实现一个简单的共享内存,本文通过具体的实例,介绍一下这种共享内存的实现方法。

理论:

        系统调用mmap通常用将文件映射到内存,以加快文件的读写速度。当用mmap操作一个设备文件时,可以将设备上的存储区域映射到进程的地址空间,从而以内存读写的方法直接控制设备。如果我们在内核模块申请了一段内存区域,也可以利用次方法,将这个区域映射到用户空间,以实现用户空间和内核空间的共享内存。

        Linux中的每个进程都有一个独立的地址空间,在内核中使用数据结构mm_struct表示,而一个进程的地址空间,由多个vm_area_struct组成,每一vm_area_struct区域都映射了一段具体的物理内存空间或IO空间。

[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,其次是分配物理页,并创建页表。

        当我们调用系统调用mmap时,内核中色sys_mmap函数首先根据用户提供给的mmap的参数(如起始地址大小、空间大小、行为修饰符)创建新的VMA并插入链表或者树。然后调用响应文件的file_operatios中的mmap函数,如果是设备文件,则file_operations中的mmap函数由设备驱动的编写者实现,而在mmap中,我们仅仅需要完成页表项的创建即可。

参考:linux库函数mmap()原理_skybabybzh的博客-CSDN博客_mmap

实例:

        通过一个实例实现共享内存的方法:

#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#include<linux/mm.h>
#include<linux/gfp.h>
#include<linux/slab.h>

int dev_major=256;//主设备号
int dev_minor=0;//次设备号

char *shmem;//指向共享区域的指针,内核操作时使用
#define SHM_SIZE 1 //共享页表的大小
struct page *shm_page;

int symboler_open(struct inode*,struct file*);
int symboler_release(struct inode*,struct file*);
ssize_t symboler_read(struct file*,char*,size_t,loff_t*);
ssize_t symboler_write(struct file*,const char*,size_t, loff_t *);
int symboler_mmap(struct file*,struct vm_area_struct *);
long symboler_ioctl(struct file*,unsigned int,unsigned long);
struct file_operations symboler_fops={
    .owner=THIS_MODULE,
    .open=symboler_open,
    .release=symboler_release,
    .read=symboler_read,
    .write=symboler_write,
    .unlocked_ioctl=symboler_ioctl,
    .mmap=symboler_mmap,
};

struct symboler_cdev{
    int sym_var;
    struct cdev cdev;
};
struct symboler_cdev *symboler_cdev_var;

//仅做测试,所以没有写具体内容
int symboler_open(struct inode* inode,struct file* filp){
    printk("%s()is called.\n", __func__);
    return 0;
}
int symboler_release(struct inode* inode,struct file* filp){
    printk("%s()is called.\n", __func__);
    return 0;
}
ssize_t symboler_read(struct file* filp,char* buf,size_t len,loff_t* off){
    printk("%s()is called.\n", __func__);
    return 0;
}
ssize_t symboler_write(struct file* filp,const char* buf,size_t len,loff_t* off){
    printk("%s()is called.\n", __func__);
    return 0;
}
long symboler_ioctl(struct file* filp, unsigned int cmd ,unsigned long argp)
{
    printk("%s()is called.\n", __func__);
    return 0;
}
// 这段代码实现了vma的操作方法集合。vma的所有操作都定义在数据结构vm_operations_struct中。在这
//里,我们也无需添加任何操作
void symboler_vma_open(struct vm_area_struct *vma)
{
    printk("%s()is called.\n", __func__);
}
void symboler_vma_close(struct vm_area_struct *vma)
{
    printk("%s()is called.\n", __func__);
}
static struct vm_operations_struct symboler_remap_vm_ops={
    .open =symboler_vma_open,
    .close =symboler_vma_close,
};

/*这就是最关键的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使用*/
int symboler_mmap(struct file *filp,struct vm_area_struct *vma){
    printk("%s() is called.\n",__func__);
    if(remap_pfn_range(vma,vma->vm_start,page_to_pfn(shm_page),vma->vm_end-vma->vm_start,vma->vm_page_prot))
        return -EAGAIN;
    vma->vm_ops=&symboler_remap_vm_ops;
    symboler_vma_open(vma);
    return 0;
}

int symboler_init(void){
    int ret, err;
    dev_t devno = MKDEV(dev_major,dev_minor);
    ret = register_chrdev_region(devno,1,"symboler");
    if(ret < 0){
        printk("symboler register failure.\n");
        return ret;
    }else{
        printk("symboler register successfully.\n");
    }
    symboler_cdev_var = kmalloc(sizeof(struct symboler_cdev), GFP_KERNEL);
    if(!symboler_cdev_var){
        ret = -ENOMEM;
        printk("create device failed.\n");
    }else{
        symboler_cdev_var->sym_var =0;
        cdev_init(&symboler_cdev_var->cdev, &symboler_fops);
        symboler_cdev_var->cdev.owner= THIS_MODULE;
        err = cdev_add(&symboler_cdev_var->cdev,devno, 1);
        if(err <0)
            printk("Add device failure\n");
    }
    shm_page = alloc_pages(GFP_NOWAIT, SHM_SIZE);
    shmem= page_address(shm_page);
    strcpy(shmem, "hello,mmap\n");
    return ret;
}

static void __exit symboler_exit(void)
{
    dev_t devno;
    cdev_del(&symboler_cdev_var->cdev);
    devno = MKDEV(dev_major,dev_minor);
    unregister_chrdev_region(devno, 1);
    kfree(symboler_cdev_var);
    free_pages(shmem,SHM_SIZE);
}
MODULE_AUTHOR("wang youkang");
module_init(symboler_init);
module_exit(symboler_exit);

         宏MKDEV将主设备号与次设备号组合成一个32位整数。函数register_chrdev_region将我们的字符设备注册到系统中。

        用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

        下面我们再看看用户空间中,应该如何获得共享内存区域的地址。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
int main(void){
    int fd;
    char* mem_start;
    fd = open("/dev/shm",O_RDWR);
    if((mem_start =mmap(NULL,4096, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0)) == MAP_FAILED)               
    {
         printf("mmap failed.\n");
         exit(0);
    }
    printf("mem:%s\n", mem_start);
    return 0;
}

运行程序后,便可以输出我们在内核中写入共享内存的字符串“hello,mmap”。到这里,一个简单的共享内存模型就介绍完了。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值