Linux ------- 内存映射(1)

一、内存映射的原理    

内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<---->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。

内存映射分为2种:

1.文件映射:将一个普通文件的全部或者一部分内容映射到进程的虚拟内存中。映射后,进程就可以直接在对应的内存区域操作文件内容!普遍文件映射到用户空间的内存区域的示意图

2.匿名映射:匿名映射没有对应的文件或者对应的文件是虚拟文件(如:/dev/zero),映射后会把内存分页全部初始化为0.

当多个进程映射了同一个内存区域时,他们会共享物理内存的相同分页。通过fork()创建的子进程也会继承父进程的映射副本!!!

如果多个进程都会同一个内存区域操作时,会根据映射的特性,会有不同的行为。映射特征可分为私有映射和共享映射:

1.私有映射:映射的内容对其进程不可见。对于文件映射来说,某一个进程在映射内存中改变文件的内容不会反映的底层文件中。内核会使用copy-on-write(写时复制)技术来解决这个问题:只要有一个进程修改了分页中的内容,内核会为该进程重新创建一个新的分页,并将需要修改的内容复制到新分页中。

2.共享映射:某一个进程对共享内存的内存区域操作都会对其他进程可见!!!对于文件映射,操作的内容回反映到底层文件中。

注意:进程指向exec()调用后,先前的内存映射会丢失,而fork()创建的子进程会继承父进程的,映射的特征(私有和共享)也会被继承。

异常信号:

1.当映射内存的属性设置只读时,如果进行写操作会产生SIGSEGV信号。

2.当映射内存的字节数大于被映射文件的大小,且大于该文件当前的内存分页大小时。如果访问的区域超过了该文件分页大小,会产生SIGBUS信号。

有点绕口,举个简单的例子:假设内核维护的内存分页是4k,4096字节),一个普通文件a.txt的大小是10字节。如果创建一个映射内存为4079字节,并映射该文件。此时,因为a.txt的大小用一个分页就可以完全映射,10字节远小于一个分页的4096字节,所以内核只会给它一个分页。内存地址时从0开始,0-9区间对应a.txt文件的数据,我们也可以访问10-4096的区间。但如果访问4096区间时,已经超过一个分页的大小了,此时会产生SIGBUS信号!!!

二、函数接口

mmap函数是unix/linux下的系统调用,详细内容可参考《Unix Netword programming》卷二12.2节。

mmap系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

       mmap系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。mmap并不分配空间, 只是将文件映射到调用进程的地址空间里(但是会占掉你的 virutal memory), 然后你就可以用memcpy等操作写文件, 而不用write()了.写完后,内存中的内容并不会立即更新到文件中,而是有一段时间的延迟,你可以调用msync()来显式同步一下, 这样你所写的内容就能立即保存到文件里了.这点应该和驱动相关。 不过通过mmap来写文件这种方式没办法增加文件的长度, 因为要映射的长度在调用mmap()的时候就决定了.如果想取消内存映射,可以调用munmap()来取消内存映射

1.创建映射

#include <sys/mman.h>

void *mmap(void *addr, size_t length,int prot,int flags,int fd,off_t offset);

addr:映射后要存放的虚拟内存地址。如果是NULL,内核会自动帮你选择。

length:映射内存的字节数。

prot:权限保护:PORT_NONE(无法访问),PORT_READ(可读),PORT_WRITE(可写),

length:映射内存的字节数。

prot:权限保护:PROT_NONE(无法访问),PORT_READ(可读),PORT_WRITE(可写),

PORT_EXEC(可执行).

flags:映射特征:MAP_PRIVATE(私有),MAP_SHARED(共享),MAP_ANONYMOUS.还有一些其他的可查询man手册。

fd:要映射的文件描述符。

offset:文件的偏移量,如果为0,且length为文件长度,代表映射整个文件。

2.解除映射

#include <sys/mman.h>

int munmap(void *addr,size_t length);

addr:要解除内存的起始地址。如果addr不在刚刚映射区域的开始位置,解除一部分后内存区域可能会分成两半!!!

length:要解除的字节数。

3.同步映射区

#include <sys/mman.h>

int msync(void *addr, size_t length, int flags);

addr:要同步的内存起始地址。

length:要同步的字节长度。

flag:MS_SYNC(执行同步文件写入),此操作内核会把内容直接写入到磁盘。MS_ASYNC(执行异步文件写入),此操作内核会先把内容写到内核的缓存区,某个适合的时候再写到磁盘。

三、mmap在linux哪里?

mmap是操作这些设备的一种方法,所谓操作设备,比如IO端口(点亮一个LED)、LCD控制器、磁盘控制器,实际上就是往设备的物理地址读写数据。

但是,由于应用程序不能直接操作设备硬件地址,所以操作系统提供了这样的一种机制——内存映射,把设备地址映射到进程虚拟地址,mmap就是实现内存映射的接口。

操作设备还有很多方法,如ioctl、ioremap

mmap的好处是,mmap把设备内存映射到虚拟内存,则用户操作虚拟内存相当于直接操作设备了,省去了用户空间到内核空间的复制过程,相对IO操作来说,增加了数据的吞吐量。

四、虚拟地址空间

      个进程都有4G的虚拟地址空间,其中3G用户空间,1G内核空间(linux),每个进程共享内核空间,独立的用户空间,下图形象地表达了这点

驱动程序运行在内核空间,所以驱动程序是面向所有进程的。

用户空间切换到内核空间有两种方法:

(1)系统调用,即软中断

(2)硬件中断

虚拟空间装的大概是上面那些数据了,内存映射大概就是把设备地址映射到上图的红色段了,暂且称其为“内存映射段”,至于映射到哪个地址,是由操作系统分配的,操作系统会把进程空间划分为三个部分:

(1)未分配的,即进程还未使用的地址

(2)缓存的,缓存在ram中的页

(3)未缓存的,没有缓存在ram中

操作系统会在未分配的地址空间分配一段虚拟地址,用来和设备地址建立映射,至于怎么建立映射,后面再揭晓。

现在大概明白了“内存映射”是什么了,那么内核是怎么管理这些地址空间的呢?任何复杂的理论最终也是通过各种数据结构体现出来的,而这里这个数据结构就是进程描述符。从内核看,进程是分配系统资源(CPU、内存)的载体,为了管理进程,内核必须对每个进程所做的事情进行清楚的描述,这就是进程描述符,内核用task_struct结构体来表示进程,并且维护一个该结构体链表来管理所有进程。该结构体包含一些进程状态、调度信息等上千个成员,我们这里主要关注进程描述符里面的内存描述符(struct mm_struct mm)

五、内存描述符

现在已经知道了内存映射是把设备地址映射到进程空间地址(注意:并不是所有内存映射都是映射到进程地址空间的,ioremap是映射到内核虚拟空间的,mmap是映射到进程虚拟地址的),实质上是分配了一个vm_area_struct结构体加入到进程的地址空间,也就是说,把设备地址映射到这个结构体,映射过程就是驱动程序要做的事了。

六、内存映射的实现

以字符设备驱动为例,一般对字符设备的操作都如下框图

而内存映射的主要任务就是实现内核空间中的mmap()函数,先来了解一下字符设备驱动程序的框架,见于博客---

以下是mmap_driver.c的源代码

[cpp] view plain copy
//所有的模块代码都包含下面两个头文件  
#include <linux/module.h>  
#include <linux/init.h>  
  
#include <linux/types.h> //定义dev_t类型  
#include <linux/cdev.h> //定义struct cdev结构体及相关操作  
#include <linux/slab.h> //定义kmalloc接口  
#include <asm/io.h>//定义virt_to_phys接口  
#include <linux/mm.h>//remap_pfn_range  
#include <linux/fs.h>  
  
#define MAJOR_NUM 990  
#define MM_SIZE 4096  
  
static char driver_name[] = "mmap_driver1";//驱动模块名字  
static int dev_major = MAJOR_NUM;  
static int dev_minor = 0;  
char *buf = NULL;  
struct cdev *cdev = NULL;  
  
static int device_open(struct inode *inode, struct file *file)  
{  
    printk(KERN_ALERT"device open\n");  
    buf = (char *)kmalloc(MM_SIZE, GFP_KERNEL);//内核申请内存只能按页申请,申请该内存以便后面把它当作虚拟设备  
    return 0;  
}  
  
static int device_close(struct inode *indoe, struct file *file)  
{  
    printk("device close\n");  
    if(buf)  
    {  
        kfree(buf);  
    }  
    return 0;  
}  
  
static int device_mmap(struct file *file, struct vm_area_struct *vma)  
{  
    vma->vm_flags |= VM_IO;//表示对设备IO空间的映射  
    vma->vm_flags |= VM_RESERVED;//标志该内存区不能被换出,在设备驱动中虚拟页和物理页的关系应该是长期的,应该保留起来,不能随便被别的虚拟页换出  
    if(remap_pfn_range(vma,//虚拟内存区域,即设备地址将要映射到这里  
                       vma->vm_start,//虚拟空间的起始地址  
                       virt_to_phys(buf)>>PAGE_SHIFT,//与物理内存对应的页帧号,物理地址右移12位  
                       vma->vm_end - vma->vm_start,//映射区域大小,一般是页大小的整数倍  
                       vma->vm_page_prot))//保护属性,  
    {  
        return -EAGAIN;  
    }  
    return 0;  
}  
  
static struct file_operations device_fops =  
{  
    .owner = THIS_MODULE,  
    .open  = device_open,  
    .release = device_close,  
    .mmap = device_mmap,  
};  
  
static int __init char_device_init( void )  
{  
    int result;  
    dev_t dev;//高12位表示主设备号,低20位表示次设备号  
    printk(KERN_ALERT"module init2323\n");  
    printk("dev=%d", dev);  
    dev = MKDEV(dev_major, dev_minor);  
    cdev = cdev_alloc();//为字符设备cdev分配空间  
    printk(KERN_ALERT"module init\n");  
    if(dev_major)  
    {  
        result = register_chrdev_region(dev, 1, driver_name);//静态分配设备号  
        printk("result = %d\n", result);  
    }  
    else  
    {  
        result = alloc_chrdev_region(&dev, 0, 1, driver_name);//动态分配设备号  
        dev_major = MAJOR(dev);  
    }  
      
    if(result < 0)  
    {  
        printk(KERN_WARNING"Cant't get major %d\n", dev_major);  
        return result;  
    }  
      
      
    cdev_init(cdev, &device_fops);//初始化字符设备cdev  
    cdev->ops = &device_fops;  
    cdev->owner = THIS_MODULE;  
      
    result = cdev_add(cdev, dev, 1);//向内核注册字符设备  
    printk("dffd = %d\n", result);  
    return 0;  
}  
  
static void __exit char_device_exit( void )  
{  
    printk(KERN_ALERT"module exit\n");  
    cdev_del(cdev);  
    unregister_chrdev_region(MKDEV(dev_major, dev_minor), 1);  
}  
  
module_init(char_device_init);//模块加载  
module_exit(char_device_exit);//模块退出  
  
MODULE_LICENSE("GPL");  
MODULE_AUTHOR("ChenShengfa");  

下面是测试代码test_mmap.c下面是makefile文件 下面是makefile文件

[cpp] view plain copy

#include <stdio.h>  
#include <fcntl.h>  
#include <sys/mman.h>  
#include <stdlib.h>  
#include <string.h>  
  
int main( void )  
{  
    int fd;  
    char *buffer;  
    char *mapBuf;  
    fd = open("/dev/mmap_driver", O_RDWR);//打开设备文件,内核就能获取设备文件的索引节点,填充inode结构  
    if(fd<0)  
    {  
        printf("open device is error,fd = %d\n",fd);  
        return -1;  
    }  
    /*测试一:查看内存映射段*/  
    printf("before mmap\n");  
    sleep(15);//睡眠15秒,查看映射前的内存图cat /proc/pid/maps  
    buffer = (char *)malloc(1024);  
    memset(buffer, 0, 1024);  
    mapBuf = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//内存映射,会调用驱动的mmap函数  
    printf("after mmap\n");  
    sleep(15);//睡眠15秒,在命令行查看映射后的内存图,如果多出了映射段,说明映射成功  
      
    /*测试二:往映射段读写数据,看是否成功*/  
    strcpy(mapBuf, "Driver Test");//向映射段写数据  
    memset(buffer, 0, 1024);  
    strcpy(buffer, mapBuf);//从映射段读取数据  
    printf("buf = %s\n", buffer);//如果读取出来的数据和写入的数据一致,说明映射段的确成功了  
      
      
    munmap(mapBuf, 1024);//去除映射  
    free(buffer);  
    close(fd);//关闭文件,最终调用驱动的close  
    return 0;  
}  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值