mmap是一个用户空间很常用的系统调用,无论是分配内存、读写大文件、链接动态库文件,还是多进程间共享内存,都可以看到其身影。本文首先介绍了进程地址空间和mmap,然后分析了内核代码以了解其实现,最后通过一个简单的demo驱动示例,加深对mmap的理解。
进程地址空间及vma 作为前置知识,先来对进程地址空间做个简单介绍,以便更好地理解后面的内容。现代操作系统的内存管理离不开硬件的支持,如分段机制、分页机制。它们用于实现内存的隔离、保护以及高效使用。进程之间地址空间相互隔离,每个进程都有一套页表,实现线性地址到物理地址的转换。
虚拟内存映射 下面是32位系统(x86)的进程地址空间布局图
0~3G 部分是用户空间的地址,3G~4G 部分是内核地址空间。虚拟地址从低到高分别为代码段、数据段(已初始化的静态变量)、bss段(未初始化的静态变量)、heap堆、mmap映射区、栈、命令行参数、环境变量。
从0xc0000000开始就是内核地址空间了。内核地址空间又分为线性内存区和高端内存区。高端内存区是用于vmalloc机制、fixmap等的。在x86体系中,最低16MB物理内存是DMA内存区,用于执行DMA操作。
64位系统(x86_64)上,内存地址可用空间为0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,这是一个非常巨大的地址空间。而Linux实际上只用了低47位(128T),高17位作扩展。实际用到的地址空间为0x0000000000000000 ~ 0x00007FFFFFFFFFFF (用户空间)和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF (内核空间)。
在64位处理器中,由于有足够的内核空间可以线性映射物理内存,所以就不需要高端内存这个管理区了。更详细的信息可以参考内核文档 。
VMA 进程地址空间在Linux内核中使用struct vm_area_struct
来描述,简称VMA 。由于这些地址空间归属于各个用户进程,所以在用户进程的struct mm_struct
中也有相应的成员。进程可以通过内核的内存管理机制动态地添加或删除这些内存区域。
每个内存区域具有相关的权限,比如可读、可写、可执行。如果进程访问了不在有效范围内的内存区域、或非法访问了内存,那么处理器会报缺页异常,严重的会出现段错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 struct vm_area_struct { unsigned long vm_start; unsigned long vm_end; struct vm_area_struct *vm_next , *vm_prev ; struct rb_node vm_rb ; unsigned long rb_subtree_gap; struct mm_struct *vm_mm ; pgprot_t vm_page_prot; unsigned long vm_flags; struct { struct rb_node rb ; unsigned long rb_subtree_last; } shared; struct list_head anon_vma_chain ; struct anon_vma *anon_vma ; const struct vm_operations_struct *vm_ops ; unsigned long vm_pgoff; struct file * vm_file ; void * vm_private_data; atomic_long_t swap_readahead_info; #ifndef CONFIG_MMU struct vm_region *vm_region ; #endif #ifdef CONFIG_NUMA struct mempolicy *vm_policy ; #endif struct vm_userfaultfd_ctx vm_userfaultfd_ctx ; } __randomize_layout;
解释下几个主要的成员:
vm_start和vm_end:表示vma的起始和结束地址,相减就是vma的长度
vm_next和vm_prev:链表指针
vm_rb:红黑树节点
vm_mm:所属进程的内存描述符mm_struct数据结构
vm_page_prot:vma的访问权限
vm_flags:vma的标志
anon_vma_chain和anon_vma:用于管理RMAP反向映射
vm_ops:指向操作方法结构体
vm_pgoff:文件映射的偏移量。
vm_file:指向被映射的文件
mmap简介 1 2 3 void *mmap (void *addr, size_t length, int prot, int flags, int fd, off_t offset) ;int munmap (void *addr, size_t length) ;
addr:指定起始地址,为了可移植性一般设为NULL
length:表示映射到进程地址空间的大小
prot:读写属性,PROT_EXEC、PROT_READ、PROT_WRITE、PROT_NONE
flags:标志,如共享映射、私有映射
fd:文件描述符,匿名映射时设为-1。
offset:文件映射时,表示偏移量
flag标志
MAP_SHARED:创建一个共享的映射区域。多个进程可以这样映射同一个文件,修改后的内容会同步到磁盘文件中。
MAP_PRIVATE:创建写时复制的私有映射。多个进程可以私有映射同一个文件,修改之后不会同步到磁盘中。
MAP_ANONYMOUS:创建匿名映射,即没有关联到文件的映射
MAP_FIXED:使用参数addr创建映射,如果无法映射指定的地址就返回失败,addr要求按页对齐。如果指定的地址空间与已有的VMA重叠,会先销毁重叠的区域。
MAP_POPULATE:对于文件映射,会提前预读文件内容到映射区域,该特性只支持私有映射。
4类映射 根据prot和flags的不同组合,可以分为以下4种映射类型:
mmap内存映射原理
当用户空间调用mmap时,系统会寻找一段满足要求的连续虚拟地址,然后创建一个新的vma插入到mm系统的链表和红黑树中。
调用内核空间mmap,建立文件块/设备物理地址和进程虚拟地址vma的映射关系
如果是磁盘文件,没有特别设置标志的话这里只是建立映射不会实际分配内存。
如果是设备文件,直接通过remap_pfn_range函数建立设备物理地址到虚拟地址的映射。
(如果是磁盘文件映射)当进程对这片映射地址空间进行访问时,引发缺页异常,将数据从磁盘中拷贝到物理内存。后续用户空间就可以直接对这块内核空间的物理内存进行读写,省去了用户空间跟内核空间之间的拷贝过程。
内核代码分析 当我们在用户空间调用mmap时,首先通过系统调用进入内核空间,可以看到这里将offset转成了以页为单位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 SYSCALL_DEFINE6(mmap, unsigned long , addr, unsigned long , len, unsigned long , prot, unsigned long , flags, unsigned long , fd, unsigned long , off) { long error; error = -EINVAL; if (off & ~PAGE_MASK) goto out; error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT); out: return error; }
来看系统调用sys_mmap_pgoff
,如果是不是匿名映射,会通过fd获取file结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 SYSCALL_DEFINE6(mmap_pgoff, unsigned long , addr, unsigned long , len, unsigned long , prot, unsigned long , flags, unsigned long , fd, unsigned long , pgoff) { struct file *file = NULL ; unsigned long retval; if (!(flags & MAP_ANONYMOUS)) { file = fget(fd); } retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff); return retval; }
接着看vm_mmap_pgoff
函数,这里主要用信号量对进程地址空间做了一个保护,然后根据populate的值会prefault页表,如果是文件映射则会对文件进行预读。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 unsigned long vm_mmap_pgoff (struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long pgoff) { unsigned long ret; struct mm_struct *mm = current->mm; unsigned long populate; LIST_HEAD(uf); ret = security_mmap_file(file, prot, flag); if (!ret) { if (down_write_killable(&mm->mmap_sem)) return -EINTR; ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff, &populate, &uf); up_write(&mm->mmap_sem); userfaultfd_unmap_complete(mm, &uf); if (populate) mm_populate(ret, populate); } return ret; }
do_mmap_pgoff
只是简单调用do_mmap
1 2 3 4 5 6 7 8 9 static inline unsigned long do_mmap_pgoff (struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long pgoff, unsigned long *populate, struct list_head *uf) { return do_mmap(file, addr, len, prot, flags, 0 , pgoff, populate, uf); }
我们来看do_mmap
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 unsigned long do_mmap (struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, vm_flags_t vm_flags, unsigned long pgoff, unsigned long *populate, struct list_head *uf) { struct mm_struct *mm = current->mm; len = PAGE_ALIGN(len); addr = get_unmapped_area(file, addr, len, pgoff, flags); addr = mmap_region(file, addr, len, vm_flags, pgoff, uf); if (!IS_ERR_VALUE(addr) && ((vm_flags & VM_LOCKED) || (flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE)) *populate = len; return addr; }
这个函数主要将映射长度页对齐,对prot属性和flags标志进行了检查和处理,设置了vm_flags。get_unmapped_area
函数检查指定的地址或自动选择可用的虚拟地址。然后就调用mmap_region
,可以看到返回之后,根据调用接口时设置的flags对populate进行了设置。如果设置了MAP_LOCKED
,或者设置了MAP_POPULATE
但没有设置MAP_NONBLOCK
,就进行前面提到的prefault操作。
然后继续看mmap_region
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 unsigned long mmap_region (struct file *file, unsigned long addr, unsigned long len, vm_flags_t vm_flags, unsigned long pgoff, struct list_head *uf) { vma = vma_merge(mm, prev, addr, addr + len, vm_flags, NULL , file, pgoff, NULL , NULL_VM_UFFD_CTX); if (vma) goto out; vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL); vma->vm_mm = mm; vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_flags = vm_flags; vma->vm_page_prot = vm_get_page_prot(vm_flags); vma->vm_pgoff = pgoff; INIT_LIST_HEAD(&vma->anon_vma_chain); if (file) { vma->vm_file = get_file(file); error = call_mmap(file, vma); } else if (vm_flags & VM_SHARED) { error = shmem_zero_setup(vma); } return addr; }
该函数首先做了一些地址空间检查,接着vma_merge
检查是否可以和老的映射合并,然后就是分配vma并初始化。如果是文件映射,调用call_mmap
;如果是匿名共享映射,调用shmem_zero_setup
,它里面会进行/dev/zero
文件相关设置。
call_mmap
只是简单地调用文件句柄中的mmap操作函数。
1 2 3 4 5 static inline int call_mmap (struct file *file, struct vm_area_struct *vma) { return file->f_op->mmap(file, vma); }
如果是普通文件系统中的文件的话,我们以ext4为例,里面主要是设置了vma->vm_ops
为ext4_file_vm_ops
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static int ext4_file_mmap (struct file *file, struct vm_area_struct *vma) { vma->vm_ops = &ext4_file_vm_ops; return 0 ; } static const struct vm_operations_struct ext4_file_vm_ops = { .fault = ext4_filemap_fault, .map_pages = filemap_map_pages, .page_mkwrite = ext4_page_mkwrite, };
后续当访问这个vma地址空间时,就会调用相应的操作函数进行处理,比如页错误处理函数会调用ext4_filemap_fault
,里面又会调用filemap_fault
。
如果是设备文件的话,由相应的设备驱动实现mmap方法,在里面建立设备物理内存到vma地址空间的映射。接下来通过一个简单的驱动demo来演示。
简单总结一下
1 2 3 4 5 6 7 8 9 mmap // offset转成页为单位 +-- sys_mmap_pgoff // 通过fd获取file +-- vm_mmap_pgoff // 信号量保护,映射完成后populate +-- do_mmap_pgoff // 简单封装 +-- do_mmap // 映射长度页对齐,prot和flags检查,设置vm_flags,获取映射虚拟地址 +-- mmap_region // 地址空间检查,vma_merge,vma分配及初始化 |-- call_mmap // 文件映射,简单封装 | +-- file->f_op->mmap // 调用实际文件的mmap方法 |-- shmem_zero_setup // 匿名共享映射,/dev/zero
驱动demo 我们编写了一个简单的misc设备,在驱动加载的时候使用alloc_pages
分配设备的物理内存(4页),当然也可以使用kmalloc或vmalloc。然后实现了几个操作方法,其中最主要的就是mmap方法,为了方便测试我们还实现了read、write、llseek等方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/mm.h> #include <linux/gfp.h> #include <linux/miscdevice.h> #include <linux/uaccess.h> #define DEMO_NAME "demo_dev" #define PAGE_ORDER 2 #define MAX_SIZE (PAGE_SIZE << PAGE_ORDER) static struct device *mydemodrv_device ;static struct page *page = NULL ;static char *device_buffer = NULL ;static const struct file_operations demodrv_fops = { .owner = THIS_MODULE, .open = demodrv_open, .release = demodrv_release, .read = demodrv_read, .write = demodrv_write, .mmap = demodev_mmap, .llseek = demodev_llseek }; static struct miscdevice mydemodrv_misc_device = { .minor = MISC_DYNAMIC_MINOR, .name = DEMO_NAME, .fops = &demodrv_fops, }; static int __init demo_dev_init (void ) { int ret; ret = misc_register(&mydemodrv_misc_device); if (ret) { printk("failed to register misc device" ); return ret; } mydemodrv_device = mydemodrv_misc_device.this_device; printk("succeeded register misc device: %s\n" , DEMO_NAME); page = alloc_pages(GFP_KERNEL, PAGE_ORDER); if (!page) { printk("alloc_page failed\n" ); return -ENOMEM; } device_buffer = page_address(page); printk("device_buffer physical address: %lx, virtual address: %px\n" , page_to_pfn(page) << PAGE_SHIFT, device_buffer); return 0 ; } static void __exit demo_dev_exit (void ) { printk("removing device\n" ); __free_pages(page, PAGE_ORDER); misc_deregister(&mydemodrv_misc_device); } module_init(demo_dev_init); module_exit(demo_dev_exit); MODULE_AUTHOR("catbro666" ); MODULE_LICENSE("GPL v2" ); MODULE_DESCRIPTION("mmap test module" );
这里主要看一下mmap方法的实现,核心函数是remap_pfn_range
,它用于建立实际物理地址到vma虚拟地址的映射。我们来看下它的参数,第一个是要映射的用户空间vma,第二个是映射起始地址,第三个是内核内存的物理页帧号,第四个是映射区域的大小,第五个是对这个映射的页保护标志。
我们用到的大部分参数通过vma获取,如上一节所看到的,外层函数已经做好了vma初始化工作。因为我们是用alloc_pages
分配的内存,其物理地址是连续的,所以映射也比较简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 static int demodev_mmap (struct file *file, struct vm_area_struct *vma) { struct mm_struct *mm ; unsigned long size; unsigned long pfn_start; void *virt_start; int ret; mm = current->mm; pfn_start = page_to_pfn(page) + vma->vm_pgoff; virt_start = page_address(page) + (vma->vm_pgoff << PAGE_SHIFT); size = min(((1 << PAGE_ORDER) - vma->vm_pgoff) << PAGE_SHIFT, vma->vm_end - vma->vm_start); printk("phys_start: 0x%lx, offset: 0x%lx, vma_size: 0x%lx, map size:0x%lx\n" , pfn_start << PAGE_SHIFT, vma->vm_pgoff << PAGE_SHIFT, vma->vm_end - vma->vm_start, size); if (size <= 0 ) { printk("%s: offset 0x%lx too large, max size is 0x%lx\n" , __func__, vma->vm_pgoff << PAGE_SHIFT, MAX_SIZE); return -EINVAL; } ret = remap_pfn_range(vma, vma->vm_start, pfn_start, size, vma->vm_page_prot); if (ret) { printk("remap_pfn_range failed, vm_start: 0x%lx\n" , vma->vm_start); } else { printk("map kernel 0x%px to user 0x%lx, size: 0x%lx\n" , virt_start, vma->vm_start, size); }
再来看下read方法的实现,主要就是从设备内存中拷贝数据到用户空间的buf中,然后更新文件偏移。write方法也是类似,这里就不再展示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static ssize_t demodrv_read (struct file *file, char __user *buf, size_t count, loff_t *ppos) { int actual_readed; int max_read; int need_read; int ret; max_read = PAGE_SIZE - *ppos; need_read = max_read > count ? count : max_read; if (need_read == 0 ) dev_warn(mydemodrv_device, "no space for read" ); ret = copy_to_user(buf, device_buffer + *ppos, need_read); if (ret == need_read) return -EFAULT; actual_readed = need_read - ret; *ppos += actual_readed; printk("%s actual_readed=%d, pos=%lld\n" , __func__, actual_readed, *ppos); return actual_readed; }
测试程序 安装驱动 我们首先编译安装驱动,设备节点文件已经自动创建。查看内核日志可以看到已经成功创建了设备,并分配了内存。起始物理地址为0x5b1558000,内核虚拟地址为0xffff8d1ab1558000。
1 2 3 4 5 6 $ sudo insmod mydemodev.ko $ ll /dev|grep demo crw------- 1 root root 10, 58 12月 12 23:33 demo_dev $ dmesg | tail -n 2 [110047.799513] succeeded register misc device: demo_dev [110047.799517] device_buffer physical address: 5b1558000, virtual address: ffff8d1ab1558000
测试程序1 接下来我们写了几个测试程序来对这个驱动进行测试。首先来看第一个测试程序,我们打开驱动设备文件/dev/demo_dev
,然后mmap映射了1页的大小,这里前后分别sleep了5秒,是为了提供观察的时间。然后通过映射的用户空间虚拟地址进行读写测试,验证mmap是否正确映射了。首先通过虚拟地址写,随后用read读取进行比对检查。然后通过write写,随后用虚拟地址读取进行比对检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> #include <string.h> #include <assert.h> #define DEMO_DEV_NAME "/dev/demo_dev" int main () { char buf[64 ]; int fd; char *addr = NULL ; int ret; char *message = "Hello World\n" ; char *message2 = "I'm superman\n" ; fd = open(DEMO_DEV_NAME, O_RDWR); if (fd < 0 ) { printf ("open device %s failed\n" , DEMO_DEV_NAME); return -1 ; } sleep(5 ); addr = mmap(NULL , (size_t )getpagesize(), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0 ); sleep(5 ); ret = sprintf (addr, "%s" , message); assert(ret == strlen (message)); ret = read(fd, buf, 64 ); assert(ret == 64 ); assert(!memcmp (buf, message, strlen (message))); ret = write(fd, message2, strlen (message2)); assert(ret == strlen (message2)); assert(!memcmp (addr + 64 , message2, strlen (message2))); munmap(addr, (size_t )getpagesize()); close(fd); return 0 ; }
我们编译运行测试程序,结果如我们预期。从内核日志可以看到映射起始物理地址0x5b1558000,偏移为0,vma大小是1页,映射大小也是1页。将内核空间虚拟地址0xffff8d1ab1558000映射到了用户空间0x7f21c0f58000。
1 2 3 4 5 6 $ sudo ./test1 $ dmesg|tail -n 4 [110691.745381] phys_start: 0x5b1558000, offset: 0x0, vma_size: 0x1000, map size:0x1000 [110691.745388] map kernel 0xffff8d1ab1558000 to user 0x7f21c0f58000, size: 0x1000 [110696.745816] demodrv_read actual_readed=64, pos=64 [110696.745822] demodrv_write actual_written=13, pos=77
与此同时,我们使用pmap观察mmap前后的进程的地址空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 $ sudo pmap -x $(pgrep test1) [sudo] password for ssl: 30830: ./test1 Address Kbytes RSS Dirty Mode Mapping 0000557b19475000 4 4 0 r-x-- test1 0000557b19475000 0 0 0 r-x-- test1 0000557b19676000 4 4 4 r---- test1 0000557b19676000 0 0 0 r---- test1 0000557b19677000 4 4 4 rw--- test1 0000557b19677000 0 0 0 rw--- test1 00007f21c0941000 1948 888 0 r-x-- libc-2.27.so 00007f21c0941000 0 0 0 r-x-- libc-2.27.so 00007f21c0b28000 2048 0 0 ----- libc-2.27.so 00007f21c0b28000 0 0 0 ----- libc-2.27.so 00007f21c0d28000 16 16 16 r---- libc-2.27.so 00007f21c0d28000 0 0 0 r---- libc-2.27.so 00007f21c0d2c000 8 8 8 rw--- libc-2.27.so 00007f21c0d2c000 0 0 0 rw--- libc-2.27.so 00007f21c0d2e000 16 8 8 rw--- [ anon ] 00007f21c0d2e000 0 0 0 rw--- [ anon ] 00007f21c0d32000 156 156 0 r-x-- ld-2.27.so 00007f21c0d32000 0 0 0 r-x-- ld-2.27.so 00007f21c0f41000 8 8 8 rw--- [ anon ] 00007f21c0f41000 0 0 0 rw--- [ anon ] 00007f21c0f59000 4 4 4 r---- ld-2.27.so 00007f21c0f59000 0 0 0 r---- ld-2.27.so 00007f21c0f5a000 4 4 4 rw--- ld-2.27.so 00007f21c0f5a000 0 0 0 rw--- ld-2.27.so 00007f21c0f5b000 4 4 4 rw--- [ anon ] 00007f21c0f5b000 0 0 0 rw--- [ anon ] 00007ffdacdf1000 132 8 8 rw--- [ stack ] 00007ffdacdf1000 0 0 0 rw--- [ stack ] 00007ffdacf3c000 12 0 0 r---- [ anon ] 00007ffdacf3c000 0 0 0 r---- [ anon ] 00007ffdacf3f000 4 4 0 r-x-- [ anon ] 00007ffdacf3f000 0 0 0 r-x-- [ anon ] ffffffffff600000 4 0 0 --x-- [ anon ] ffffffffff600000 0 0 0 --x-- [ anon ] ---------------- ------- ------- ------- total kB 4376 1120 68 $ sudo pmap -x $(pgrep test1) 30830: ./test1 Address Kbytes RSS Dirty Mode Mapping 0000557b19475000 4 4 0 r-x-- test1 0000557b19475000 0 0 0 r-x-- test1 0000557b19676000 4 4 4 r---- test1 0000557b19676000 0 0 0 r---- test1 0000557b19677000 4 4 4 rw--- test1 0000557b19677000 0 0 0 rw--- test1 00007f21c0941000 1948 888 0 r-x-- libc-2.27.so 00007f21c0941000 0 0 0 r-x-- libc-2.27.so 00007f21c0b28000 2048 0 0 ----- libc-2.27.so 00007f21c0b28000 0 0 0 ----- libc-2.27.so 00007f21c0d28000 16 16 16 r---- libc-2.27.so 00007f21c0d28000 0 0 0 r---- libc-2.27.so 00007f21c0d2c000 8 8 8 rw--- libc-2.27.so 00007f21c0d2c000 0 0 0 rw--- libc-2.27.so 00007f21c0d2e000 16 8 8 rw--- [ anon ] 00007f21c0d2e000 0 0 0 rw--- [ anon ] 00007f21c0d32000 156 156 0 r-x-- ld-2.27.so 00007f21c0d32000 0 0 0 r-x-- ld-2.27.so 00007f21c0f41000 8 8 8 rw--- [ anon ] 00007f21c0f41000 0 0 0 rw--- [ anon ] 00007f21c0f58000 4 0 0 rw-s- demo_dev 00007f21c0f58000 0 0 0 rw-s- demo_dev 00007f21c0f59000 4 4 4 r---- ld-2.27.so 00007f21c0f59000 0 0 0 r---- ld-2.27.so 00007f21c0f5a000 4 4 4 rw--- ld-2.27.so 00007f21c0f5a000 0 0 0 rw--- ld-2.27.so 00007f21c0f5b000 4 4 4 rw--- [ anon ] 00007f21c0f5b000 0 0 0 rw--- [ anon ] 00007ffdacdf1000 132 8 8 rw--- [ stack ] 00007ffdacdf1000 0 0 0 rw--- [ stack ] 00007ffdacf3c000 12 0 0 r---- [ anon ] 00007ffdacf3c000 0 0 0 r---- [ anon ] 00007ffdacf3f000 4 4 0 r-x-- [ anon ] 00007ffdacf3f000 0 0 0 r-x-- [ anon ] ffffffffff600000 4 0 0 --x-- [ anon ] ffffffffff600000 0 0 0 --x-- [ anon ] ---------------- ------- ------- ------- total kB 4380 1120 68
可以看到mmap之后多了一个叫做demo_dev的段,其起始地址就是我们映射的用户空间地址0x7f21c0f58000。
1 2 00007f21c0f58000 4 0 0 rw-s- demo_dev 00007f21c0f58000 0 0 0 rw-s- demo_dev
测试程序2 测试程序2差别不大,打开同一个设备文件,mmap建立相同的映射,然后分别通过read和虚拟地址读取前一个程序写的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 int main () { char buf[64 ]; int fd; char *addr = NULL ; int ret; char *message = "Hello World\n" ; char *message2 = "I'm superman\n" ; fd = open(DEMO_DEV_NAME, O_RDWR); if (fd < 0 ) { printf ("open device %s failed\n" , DEMO_DEV_NAME); return -1 ; } addr = mmap(NULL , (size_t )getpagesize(), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0 ); ret = read(fd, buf, sizeof (buf)); assert(ret == sizeof (buf)); assert(!memcmp (buf, message, strlen (message))); assert(!memcmp (addr + sizeof (buf), message2, strlen (message2))); munmap(addr, (size_t )getpagesize()); close(fd); return 0 ; }
编译运行,测试结果如我们预期。同一个内核虚拟地址现在映射到了不同的用户空间虚拟地址。通过mmap我们实现了进程间通信。
1 2 3 4 5 $ sudo ./test2 $ dmesg|tail -n 3 [111333.818374] phys_start: 0x5b1558000, offset: 0x0, vma_size: 0x1000, map size:0x1000 [111333.818378] map kernel 0xffff8d1ab1558000 to user 0x7f015ee94000, size: 0x1000 [111333.818381] demodrv_read actual_readed=64, pos=64
测试程序3 这次我们来测试一些特殊情况,映射的大小改成了1个字节,根据前面的代码分析,映射是需要页对齐的,所以预期实际会映射一个页。在一页的范围内是可以正常读写的。然后尝试写到vma映射范围之外,预期会出现段错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 int main () { char buf[64 ]; int fd; char *addr = NULL ; off_t offset; int ret; char *message = "Hello World\n" ; char *message2 = "I'm superman\n" ; fd = open(DEMO_DEV_NAME, O_RDWR); if (fd < 0 ) { printf ("open device %s failed\n" , DEMO_DEV_NAME); return -1 ; } addr = mmap(NULL , 1 , PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0 ); ret =sprintf (addr, "%s" , message); assert(ret == strlen (message)); ret = read(fd, buf, sizeof (buf)); assert(ret == sizeof (buf)); assert(!memcmp (buf, message, strlen (message))); ret = sprintf (addr + getpagesize() - sizeof (buf), "%s" , message2); assert(ret == strlen (message2)); offset = lseek(fd, getpagesize() - sizeof (buf), SEEK_SET); assert(offset == getpagesize() - sizeof (buf)); ret = read(fd, buf, sizeof (buf)); assert(ret == sizeof (buf)); assert(!memcmp (buf, message2, strlen (message2))); printf ("expect segment error\n" ); ret = sprintf (addr + getpagesize(), "something" ); printf ("never reach here\n" ); munmap(addr, 1 ); close(fd); return 0 ; }
我们编译运行测试,结果如我们预期,实际映射了1页的大小,当尝试超出映射范围写时,出现了段错误(SIGSEGV)。
1 2 3 4 5 6 7 8 9 10 $ sudo ./test3 expect segment error Segmentation fault $ dmesg|tail -n 6 [111762.605089] phys_start: 0x5b1558000, offset: 0x0, vma_size: 0x1000, map size:0x1000 [111762.605093] map kernel 0xffff8d1ab1558000 to user 0x7f96b5d08000, size: 0x1000 [111762.605105] demodrv_read actual_readed=64, pos=64 [111762.605110] demodrv_read actual_readed=64, pos=4096 [111762.605165] test3[31001]: segfault at 7f96b5d09000 ip 0000560c0fd3ad25 sp 00007ffc5a515330 error 7 in test3[560c0fd3a000+2000] [111762.605170] Code: e8 80 fb ff ff 48 8d 3d 1a 02 00 00 e8 14 fb ff ff e8 cf fb ff ff 48 63 d0 48 8b 45 80 48 01 d0 48 bb 73 6f 6d 65 74 68 69 6e <48> 89 18 66 c7 40 08 67 00 c7 85 7c ff ff ff 09 00 00 00 48 8d 3d
测试程序4 这次我们又修改了mmap的参数,这次映射了2页的大小,偏移设置为3页。因为我们设备分配的物理内存大小是4页,所以映射的第2页已经超出了实际的设备物理内存。预期映射的第一页可以正常读写,第二页会出现bus错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int main () { char buf[64 ]; int fd; char *addr = NULL ; off_t offset; int ret; char *message = "Hello World\n" ; char *message2 = "I'm superman\n" ; fd = open(DEMO_DEV_NAME, O_RDWR); if (fd < 0 ) { printf ("open device %s failed\n" , DEMO_DEV_NAME); return -1 ; } addr = mmap(NULL , getpagesize() * 2 , PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, getpagesize() * 3 ); ret =sprintf (addr, "%s" , message); assert(ret == strlen (message)); offset = lseek(fd, getpagesize() * 3 , SEEK_SET); ret = read(fd, buf, sizeof (buf)); assert(ret == sizeof (buf)); assert(!memcmp (buf, message, strlen (message))); printf ("expect bus error\n" ); ret = sprintf (addr + getpagesize(), "something" ); printf ("never reach here\n" ); munmap(addr, getpagesize() * 2 ); close(fd); return 0 ; }
编译运行测试程序,结果如预期。虽然vma的大小为2页,但是实际只映射了1页的物理内存,当尝试写到第二页时出现了bus错误(SIGBUS)。
1 2 3 4 5 6 7 $ sudo ./test4 expect bus error Bus error $ dmesg|tail -n 3 [112105.841706] phys_start: 0x5b155b000, offset: 0x3000, vma_size: 0x2000, map size:0x1000 [112105.841710] map kernel 0xffff8d1ab155b000 to user 0x7fe662ec4000, size: 0x1000 [112105.841723] demodrv_read actual_readed=64, pos=12352
参考资料