mmap是一个很常用的系统调用,无论是分配内存、读写大文件、链接动态库文件,还是多进程间共享内存,都可以看到其身影。本文首先介绍了进程地址空间和mmap,然后分析了内核代码以了解其实现,最后通过一个简单的demo驱动示例,加深对mmap的理解。

本博客已迁移至CatBro's Blog,那是我自己搭建的个人博客,欢迎关注。

本文链接

进程地址空间及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中也有相应的成员。进程可以通过内核的内存管理机制动态地添加或删除这些内存区域。

每个内存区域具有相关的权限,比如可读、可写、可执行。如果进程访问了不在有效范围内的内存区域、或非法访问了内存,那么处理器会报缺页异常,严重的会出现段错误。

// include/linux/mm_types.h

/*
* This struct defines a memory VMM memory area. There is one of these
* per VM-area/task. A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
*/
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */ unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */ /* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev; struct rb_node vm_rb; /*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap; /* Second cache line starts here. */ struct mm_struct *vm_mm; /* The address space we belong to. */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */ /*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree.
*/
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared; /*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */ /* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops; /* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */ atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#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简介

// include<sys/mman.h>
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种映射类型:

  • 私有匿名:通常用于内存分配(大块)
  • 私有文件:通常用于加载动态库
  • 共享匿名:通常用于进程间共享内存,默认打开/dev/zero这个特殊的设备文件
  • 共享文件:通常用于内存映射I/O,进程间通信

mmap内存映射原理

  1. 当用户空间调用mmap时,系统会寻找一段满足要求的连续虚拟地址,然后创建一个新的vma插入到mm系统的链表和红黑树中。
  2. 调用内核空间mmap,建立文件块/设备物理地址和进程虚拟地址vma的映射关系
    1. 如果是磁盘文件,没有特别设置标志的话这里只是建立映射不会实际分配内存。
    2. 如果是设备文件,直接通过remap_pfn_range函数建立设备物理地址到虚拟地址的映射。
  3. (如果是磁盘文件映射)当进程对这片映射地址空间进行访问时,引发缺页异常,将数据从磁盘中拷贝到物理内存。后续用户空间就可以直接对这块内核空间的物理内存进行读写,省去了用户空间跟内核空间之间的拷贝过程。

内核代码分析

当我们在用户空间调用mmap时,首先通过系统调用进入内核空间,可以看到这里将offset转成了以页为单位。

// arch/x86/kernel/sys_x86_64.c
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结构体。

// mm/mmap.c
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页表,如果是文件映射则会对文件进行预读。

// mm/util.c
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

// include/linux/mm.h
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实现:

// mm/mmap.c
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

// mm/mmap.c
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); // 调用文件的mmap
//...
} 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操作函数。

// include/linux/fs.h
static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{
return file->f_op->mmap(file, vma);
}

如果是普通文件系统中的文件的话,我们以ext4为例,里面主要是设置了vma->vm_opsext4_file_vm_ops

// fs/ext4/file.c
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来演示。

简单总结一下

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等方法。

{% note default %}

ps: 驱动及测试程序代码已上传github,catbro666/mmap-driver-demo

{% endnote %}

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/gfp.h> // alloc_page
#include <linux/miscdevice.h> // miscdevice misc_xxx
#include <linux/uaccess.h> // copy_from/to_user #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分配的内存,其物理地址是连续的,所以映射也比较简单。

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;
} // 外层vm_mmap_pgoff已经用信号量保护了
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方法也是类似,这里就不再展示。

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。

$ 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写,随后用虚拟地址读取进行比对检查。

// test1.c
#include <stdio.h> // printf
#include <fcntl.h> // open
#include <unistd.h> // read, close, getpagesize
#include <sys/mman.h> // mmap
#include <string.h> // memcmp, strlen
#include <assert.h> // assert #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); /* 测试映射正确 */
/* 写到mmap映射的虚拟地址中,通过read读取设备文件 */
ret = sprintf(addr, "%s", message);
assert(ret == strlen(message)); ret = read(fd, buf, 64);
assert(ret == 64);
assert(!memcmp(buf, message, strlen(message))); /* 通过write写入设备文件,修改体现在mmap映射的虚拟地址 */
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。

$ 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前后的进程的地址空间

{% fold 点击展开进程地址空间 %}

$ 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

{% endfold %}

可以看到mmap之后多了一个叫做demo_dev的段,其起始地址就是我们映射的用户空间地址0x7f21c0f58000。

00007f21c0f58000       4       0       0 rw-s- demo_dev
00007f21c0f58000 0 0 0 rw-s- demo_dev

测试程序2

测试程序2差别不大,打开同一个设备文件,mmap建立相同的映射,然后分别通过read和虚拟地址读取前一个程序写的内容。

// test.2
int main()
{
char buf[64];
int fd;
char *addr = NULL;
int ret;
char *message = "Hello World\n";
char *message2 = "I'm superman\n"; /* 另一进程打开同一设备文件,然后用mmap映射 */
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); /* 通过read读取设备文件 */
ret = read(fd, buf, sizeof(buf)); assert(ret == sizeof(buf));
assert(!memcmp(buf, message, strlen(message))); /* 通过mmap映射的虚拟地址读取 */
assert(!memcmp(addr + sizeof(buf), message2, strlen(message2))); munmap(addr, (size_t)getpagesize());
close(fd);
return 0;
}

编译运行,测试结果如我们预期。同一个内核虚拟地址现在映射到了不同的用户空间虚拟地址。通过mmap我们实现了进程间通信。

$ 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映射范围之外,预期会出现段错误。

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;
}
/* 映射1个字节 */
addr = mmap(NULL, 1, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_LOCKED, fd, 0); /* 写到mmap映射的虚拟地址中,通过read读取设备文件 */
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)。

$ 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错误。

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;
}
/* 映射2页,offset 3页 */
addr = mmap(NULL, getpagesize() * 2, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_LOCKED, fd, getpagesize() * 3); /* 写到mmap映射的虚拟地址中,通过read读取设备文件 */
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)。

$ 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

参考资料

深入理解mmap--内核代码分析及驱动demo示例的更多相关文章

  1. 从linux内核代码分析操作系统启动过程

    朱宇轲 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 在本次的实验中, ...

  2. Linux启动过程的内核代码分析

    参考上文: http://www.cnblogs.com/long123king/p/3543872.html http://www.cnblogs.com/long123king/p/3545688 ...

  3. UCOSII内核代码分析

    1 UCOSII定义的关键数据结构 OS_EXT  INT8U             OSIntNesting; OSIntNesting用于判断当前系统是否正处于中断处理例程中. OS_EXT   ...

  4. [11]Windows内核情景分析---设备驱动

    设备驱动 设备栈:从上层到下层的顺序依次是:过滤设备.类设备.过滤设备.小端口设备[过.类.过滤.小端口] 驱动栈:因设备堆栈原因而建立起来的一种堆栈 老式驱动:指不提供AddDevice的驱动,又叫 ...

  5. inux2.6.xx内核代码分析( 72节)

    http://blog.csdn.net/ustc_dylan/article/category/469214

  6. (转):从内核代码聊聊pipe的实现

    来源: http://luodw.cc/2016/07/09/pipeof/ 用linux也有两年多了,从命令,系统调用,到内核原理一路学过来,我发现我是深深喜欢上这个系统:使用起来就是一个字&quo ...

  7. Linux内核中的GPIO系统之(3):pin controller driver代码分析

    一.前言 对于一个嵌入式软件工程师,我们的软件模块经常和硬件打交道,pin control subsystem也不例外,被它驱动的硬件叫做pin controller(一般ARM soc的datash ...

  8. Linux内核启动代码分析二之开发板相关驱动程序加载分析

    Linux内核启动代码分析二之开发板相关驱动程序加载分析 1 从linux开始启动的函数start_kernel开始分析,该函数位于linux-2.6.22/init/main.c  start_ke ...

  9. [置顶] 自娱自乐7之Linux UDC驱动2(自编udc驱动,现完成枚举过程,从驱动代码分析枚举过程)

    花了半个月,才搞定驱动中的枚举部分,现在说linux的枚举,windows可能有差别. 代码我会贴在后面,现在只是实现枚举,你可能对代码不感兴趣,我就不分析代码了,你可以看看 在<自娱自乐1&g ...

随机推荐

  1. Dubbo 支持分布式事务吗?

    目前暂时不支持,可与通过 tcc-transaction 框架实现 介绍:tcc-transaction 是开源的 TCC 补偿性分布式事务框架 Git 地址:https://github.com/c ...

  2. 部署新项目自动对数据库进行migrate和让用户收到创建用户/超级用户信息

    当项目中的models有数据表的时候,普通做法是用docke exec -it hello_web_1 bash,进入容器进行migrate,但是我们想要容器一启动就自动创建数据表,可以修改docke ...

  3. 说说do...while和while的区别

    一.do-while语句 do-while语句的语法: do{ statement }while(expression); 看下面示例: var i=10: do{ i+=2: }while(i< ...

  4. openldap 资料

    LDAP概念和原理介绍 相信对于许多的朋友来说,可能听说过LDAP,但是实际中对LDAP的了解和具体的原理可能还比较模糊,今天就从"什么是LDAP"."LDAP的主要产品 ...

  5. C语言之标识符(知识点3)

    条件:用户表示符仅由大小写英文字母,数字和下划线组成,且第一个字符不能是数字 注意: 不能和关键字或函数库相同名字 但关键字的大写就可以用了,因为关键字都是小写的,而C语言区分大小写 案例

  6. canvas动画—圆形扩散、运动轨迹

    介绍 在ECharts中看到过这种圆形扩散效果,类似css3,刚好项目中想把它用上,but我又不想引入整个echart.js文件,更重要的是想弄明白它的原理,所以自己动手.在这篇文章中我们就来分析实现 ...

  7. 利用AudioContext来实现网易云音乐的鲸鱼音效

    一直觉得网易云音乐的用户体验是很不错的,很早就注意到了里面的鲸鱼音效,如下图,就是一个环形的跟着音乐节拍跳动的特效. gif动图可能效果不太理想,可以直接在手机上体验 身为前端凭着本能的好奇心和探索心 ...

  8. node的两种随起随用静态服务器搭建

      一. anywhere Anywhere是一个随启随用的静态服务器,它可以随时随地将你的当前目录变成一个静态文件服务器的根目录. 1.确定电脑上安装了node.js 2.在当前所在项目文件夹下输入 ...

  9. uni-app 解析后台接口返回的HTML

    正常使用rich-text是可以解决问题的,但是在支付宝小程序中不显示,在文档中看到" 支付宝小程序 nodes 属性只支持使用 Array 类型.如果需要支持 HTML String,则需 ...

  10. 《头号玩家》AI电影调研报告(二)

    四. 涉及前沿技术及与现实的交互 1.VR技术 在影片中,斯皮尔伯格用他认为未来的VR虚拟技术为我们创造了众多精彩的画面,令人佩服其对科技的预见性.其中好多的装备特别引人注目,部分也在现实中存在:VR ...