- 了解mmap机制。参考此处
1.概述
mmap 即地址的映射, 是一种内存映射文件的方法,将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
Linux通过内存映像机制来提供用户程序对内存直接访问的能力。内存映像的意思是把内核中特定部分的内存空间映射到用户级程序的内存空间去。也就是说,用户空间和内核空间共享一块相同的内存。这样做的直观效果显而易见:内核在这块地址内存储变更的任何数据,用户可以立即发现和使用,根本无须数据拷贝。举个例子理解一下,使用mmap方式获取磁盘上的文件信息,只需要将磁盘上的数据拷贝至那块共享内存中去,用户进程可以直接获取到信息,而相对于传统的write/read IO系统调用, 必须先把数据从磁盘拷贝至到内核缓冲区中(页缓冲),然后再把数据拷贝至用户进程中。两者相比,mmap会少一次拷贝数据,这样带来的性能提升是巨大的。
使用内存访问来取代read()和write()系统调用能够简化一些应用程序的逻辑。在一些情况下,它能够比使用传统的I/O系统调用执行文件I/O这种做法提供更好的性能。原因是:
- 正常的read()或write()需要两次传输:一次是在文件和内核高速缓冲区之间,另一次是在高速缓冲区和用户空间缓冲区之间。使用mmap()就不需要第二次传输了。对于输入来讲,一旦内核将相应的文件块映射进内存之后,用户进程就能够使用这些数据了;对于输出来讲,用户进程仅仅需要修改内核中的内容,然后可以依靠内核内存管理器来自动更新底层的文件。
- 除了节省内核空间和用户空间之间的一次传输之外,mmap()还能够通过减少所需使用的内存来提升性能。当使用read()或write()时,数据将被保存在两个缓冲区中:一个位于用户空间,另个一位于内核空间。当使用mmap()时,内核空间和用户空间会共享同一个缓冲区。此外,如果多个进程正在同一个文件上执行I/O,那么它们通过使用mmap()就能够共享同一个内核缓冲区,从而又能够节省内存的消耗。
2.函数
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset)
- 描述:mmap()系统调用把fd文件描述符对象从offset位置开始(字节为单位),映射到以addr为起始地址的页面。
2.1.参数解释
- start:映射区的开始地址,一般为空,选址工作交给内核
- length:映射区的长度
- prot:期望的内存保护标志, 有以下取值
- PROT_NONE: 页面不允许访问
- PROT_READ: 页面可读
- PROT_WRITE: 页面可写
- PROT_EXEC: 页面可执行
通常可写意味着可读,可读意味着可执行,因此不应该完全依赖于flag决定读写模式
- flags:自定映射对象的类型,映射选项以及对被映射对象的修改是否只对进程可见(copy on write)。
- MAP_ANONYOUS: 匿名映射,不指定文件名
- MAP_ANON: 和上一个是同义词
- MAP_FILE: 从普通文件映射而来(默认选型)
- MAP_FIXED: 要求addr必须是页面大小的整数倍,如果不是,调用失败。不推荐使用
- MAP_PRIVATE: 对被映射对象的修改尽进程自己可见(Copy on Write)
- MAP_SHARED: 修改对其他进程都可见
- fd:要被映射的文件描述符(可以是普通文件,管道,连接等),一般由open返回, -1表示匿名映射。
- offset:被映射对象内容的起始地址
2.2.返回值
一旦调用成功,mmap返回被映射区域的指针,进程可以对这个指针进行读写。根据flags指定的模式,这些读写有可能被其他进程觉察到,由此实现基于共享内存的进程通信。
2.3.mmap用于共享内存的两种方式
- 匿名映射:把fd置为-1,指涉特殊文件,flags置为MAP_ANON,适用于具有亲缘关系的进程间通信
- 基于文件的映射:fd大于0,指涉普通文件,适用于任何进程间的通信。
2.3.1.匿名映射
匿名映射是一种没用对应文件的一种映射,是使用特殊文件提供的匿名内存映射:一个匿名映射没有对应的文件,这种映射的分页会被初始化为0。可以把它看成是一个内容总是被初始化为0的虚拟文件映射,比如在具有血缘关系的进程之间,如父子进程之间, 当一个进程调用mmap().之后又调用了fork(), 之后子进程会继承(拷贝)父进程映射后的空间,同时也继承了mmap()的返回地址,通过修改数据共享内存里的数据, 父子进程够可以感知到数据的变化,这样一来,父子进程就可以通过这块共享内存来实现进程间通信。
/* 例如一些网络套接字进行共享*/
ptr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);
pid = fork();
switch (pid)
{
case pid < 0:
printf ("err\n");
case pid = 0:
/* 使用互斥的方式访问共享内存 */
lock(ptr)
修改数据;
unlock(ptr);
case pid > 0:
/* 使用互斥的方式访问共享内存 */
lock(ptr)
修改数据;
unlock(ptr);
}
2.3.2.基于文件的映射:
适用于任何进程之间, 此时,需要打开或创建一个文件,然后再调用mmap(), 典型调用代码如下:
fd = open (name, flag, mode);
if(fd<0)
{
printf("error!\n");
}
/* 这块内存可读可写可执行 */
ptr = mmap(NULL, len , PROT_READ|PROT_WRITE|PROT_EXEC, MAP_SHARED , fd , 0);
这样用户进程就可以像读取内存一样读取文件了,效率非常高。
2.4.msync(void* addr, size_t len, int flags)
- 一般来说,进程对地址空间中共享内容的修改不会立即写入磁盘,而是等待munmap的调用后才执行写磁盘操作,而msync允许我们手工指定同步行为的发生。
2.5.int munmap(void* addr, size_t len)
- 解除进程空间某个地址(就是mmap返回的那个地址)和内核某个文件(就是mmap中的那个fd)之间的映射关系。
3.mmap系统调用
mmap属于系统调用,用户控件间接通过swi指令触发软中断,进入内核态(各种环境的切换),进入内核态之后,便可以调用内核函数进行处理。 调用流程如下:
- mmap->sys_mmap2-> sys_mmap_pgoff ->vm_mmap_pgoff->do_mmap_pgoff
3.1.sys_mmap2
include/uapi/asm-generic/unistd.h:
#define __NR_mmap __NR3264_mmap
#define __NR_mmap2 __NR3264_mmap
645 #define __NR3264_mmap 222
646 __SC_3264(__NR3264_mmap, sys_mmap2, sys_mmap)
sys_mmap2:
arch/arm/kernel/entry-common.S:
410 sys_mmap2:
411 str r5, [sp, #4]
412 b sys_mmap_pgoff
413 ENDPROC(sys_mmap2)
3.2.sys_mmap_pgoff:
kernel/mm/mmap.c:
1509 SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
1510 unsigned long, prot, unsigned long, flags,
1511 unsigned long, fd, unsigned long, pgoff)
1512 {
1513 struct file *file = NULL;
1514 unsigned long retval;
1515
//对非匿名文件映射的检查,必须能根据文件句柄找到struct file
1516 if (!(flags & MAP_ANONYMOUS)) {
1517 audit_mmap_fd(fd, flags);
1518 file = fget(fd);
1519 if (!file)
1520 return -EBADF;
1521 if (is_file_hugepages(file))
1522 len = ALIGN(len, huge_page_size(hstate_file(file))); //根据file->f_op来判断是否是hugepage,然后进行hugepage页面对齐。
1523 retval = -EINVAL;
1524 if (unlikely(flags & MAP_HUGETLB && !is_file_hugepages(file)))
1525 goto out_fput;
1526 } else if (flags & MAP_HUGETLB) {
1527 struct user_struct *user = NULL;
1528 struct hstate *hs;
1530 hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
1531 if (!hs)
1532 return -EINVAL;
1533
1534 len = ALIGN(len, huge_page_size(hs));
1535 /*
1536 * VM_NORESERVE is used because the reservations will be
1537 * taken when vm_ops->mmap() is called
1538 * A dummy user value is used because we are not locking
1539 * memory so no accounting is necessary
1540 */
1541 file = hugetlb_file_setup(HUGETLB_ANON_FILE, len,
1542 VM_NORESERVE,
1543 &user, HUGETLB_ANONHUGE_INODE,
1544 (flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
1545 if (IS_ERR(file))
1546 return PTR_ERR(file);
1547 }
1548
1549 flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);
1551 retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
1552 out_fput:
1553 if (file)
1554 fput(file);
1555 return retval;
1556 }
- 一些常规的错误检查工作;
- 通过file = fget(fd);得到对应的struct file对象指针;
- 调用vm_mmap_pgoff(file, addr, len, prot, flags, pgoff)函数完成后续的映射工作。
3.3.调用 vm_mmap_pgoff:
320 unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
321 unsigned long len, unsigned long prot,
322 unsigned long flag, unsigned long pgoff)
323 {
324 unsigned long ret;
325 struct mm_struct *mm = current->mm;
326 unsigned long populate;
327 LIST_HEAD(uf);
328
329 ret = security_mmap_file(file, prot, flag);
330 if (!ret) {
331 if (down_write_killable(&mm->mmap_sem))
332 return -EINTR;
333 ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff,
334 &populate, &uf);
335 up_write(&mm->mmap_sem);
336 userfaultfd_unmap_complete(mm, &uf);
337 if (populate)
338 mm_populate(ret, populate);
339 }
340 return ret;
341 }
3.4.do_mmap_pgoff
2172 static inline unsigned long
2173 do_mmap_pgoff(struct file *file, unsigned long addr,
2174 unsigned long len, unsigned long prot, unsigned long flags,
2175 unsigned long pgoff, unsigned long *populate,
2176 struct list_head *uf)
2177 {
2178 return do_mmap(file, addr, len, prot, flags, 0, pgoff, populate, uf);
2179 }
3.5.do_mmap
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 mm_struct *mm = current->mm;
*populate = 0;
if (!len)
return -EINVAL;
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && path_noexec(&file->f_path)))
prot |= PROT_EXEC;
if (!(flags & MAP_FIXED)) //对于非MAP_FIXED,addr不能小于mmap_min_addr大小,如果小于则使用mmap_min_addr页对齐后的地址。
addr = round_hint_to_min(addr);
/* Careful about overflows.. */
len = PAGE_ALIGN(len);
if (!len) //这里不是判断len是否为0,而是检查len是否溢出。
return -ENOMEM;
/* offset overflow? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff) //检查offset是否溢出
return -EOVERFLOW;
/* Too many mappings? */
if (mm->map_count > sysctl_max_map_count) //进程中mmap个数限制,超出返回ENOMEM错误。
return -ENOMEM;
addr = get_unmapped_area(file, addr, len, pgoff, flags); //在创建新的ma区域之前首先寻找一块足够大小的空闲区域,本函数就是用于查找未映射的区域,返回值addr就是这段空间的首地址。
if (offset_in_page(addr))
return addr;
vm_flags |= calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; //根据prot/flags以及mm->flags来得到vm_flags。
if (flags & MAP_LOCKED)
if (!can_do_mlock())
return -EPERM;
if (mlock_future_check(mm, vm_flags, len))
return -EAGAIN;
if (file) { //文件映射情况处理,主要更新vm_flags。
struct inode *inode = file_inode(file);
if (!file_mmap_ok(file, inode, pgoff, len))
return -EOVERFLOW;
switch (flags & MAP_TYPE) {
case MAP_SHARED: ///共享文件映射
if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
return -EACCES;
if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
return -EACCES;
if (locks_verify_locked(file))
return -EAGAIN;
vm_flags |= VM_SHARED | VM_MAYSHARE;
if (!(file->f_mode & FMODE_WRITE))
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
case MAP_PRIVATE: //私有文件映射
if (!(file->f_mode & FMODE_READ))
return -EACCES;
if (path_noexec(&file->f_path)) {
if (vm_flags & VM_EXEC)
return -EPERM;
vm_flags &= ~VM_MAYEXEC;
}
if (!file->f_op->mmap)
return -ENODEV;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
break;
default:
return -EINVAL;
}
} else { //匿名映射情况处理
switch (flags & MAP_TYPE) {
case MAP_SHARED: //共享匿名映射
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
pgoff = 0;
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE: //私有匿名映射
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}
if (flags & MAP_NORESERVE) {
/* We honor MAP_NORESERVE if allowed to overcommit */
if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
vm_flags |= VM_NORESERVE;
/* hugetlb applies strict overcommit unless MAP_NORESERVE */
if (file && is_file_hugepages(file))
vm_flags |= VM_NORESERVE;
}
addr = mmap_region(file, addr, len, vm_flags, pgoff); //实际创建vma
if (!IS_ERR_VALUE(addr) &&
((vm_flags & VM_LOCKED) ||
(flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
*populate = len;
return addr;
}
3.5.1.get_unmapped_area()
2104 unsigned long
2105 get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
2106 unsigned long pgoff, unsigned long flags)
2107 {
2108 unsigned long (*get_area)(struct file *, unsigned long,
2109 unsigned long, unsigned long, unsigned long);
2110
2111 unsigned long error = arch_mmap_check(addr, len, flags);
2112 if (error)
2113 return error;
2114
2115 /* Careful about overflows.. */
2116 if (len > TASK_SIZE)
2117 return -ENOMEM;
2118 #匿名映射,指向当前进程的unmap 空间的分配函数
2119 get_area = current->mm->get_unmapped_area;
#file 不为空的话,则unmap 空间的分配函数执行file中指定的函数
2120 if (file) {
2121 if (file->f_op->get_unmapped_area)
2122 get_area = file->f_op->get_unmapped_area;
2123 } else if (flags & MAP_SHARED) {
#如果file为空,说明可能申请的是匿名空间,这里检查如果是共享内存的话,则分配函数执行共享内存的分配函数
2129 pgoff = 0;
2130 get_area = shmem_get_unmapped_area;
2131 }
2132
/*
* 当不是用于文件内存映射或是匿名内存映射,
* 调用current->mm->get_unmapped_area.
* 即调用arch_get_unmapped_area或arch_get_unmapped_area_topdown
*/
2133 addr = get_area(file, addr, len, pgoff, flags);
2134 if (IS_ERR_VALUE(addr))
2135 return addr;
2136
2137 if (addr > TASK_SIZE - len)
2138 return -ENOMEM;
2139 if (offset_in_page(addr))
2140 return -EINVAL;
2141
2142 error = security_mmap_addr(addr);
2143 return error ? error : addr;
2144 }
get_unmapped_area是mmap实现的一个关键点,它实现mm-> get_unmapped_area与file->f_op->get_unmapped_area的选择。
- 如果是匿名映射,使用mm-> get_unmapped_area,如下所示:
/* 每个进程的mm_struct里面都有一个get_unmapped_area函数
比如arch/arm/mm/mmap.c里面,arch_pick_mmap_layout 函数对
get_unmapped_area赋值。
void arch_pick_mmap_layout(struct mm_struct *mm)
{
...
if (mmap_is_legacy()) {
mm->mmap_base = mm->mmap_legacy_base;
mm->get_unmapped_area = arch_get_unmapped_area;
mm->unmap_area = arch_unmap_area;
} else {
mm->get_unmapped_area = arch_get_unmapped_area_topdown;
mm->unmap_area = arch_unmap_area_topdown;
}
选择布局的工作由arch_pick_mmap_layout完成。
arch_get_unmapped_area()完成从低地址向高地址创建新的映射;
arch_get_unmapped_area_topdown()完成从高地址向低地址创建新的映射。
- 如果驱动程序的file_operations定义了get_unmapped_area,则会使用驱动程序定义的方法file->f_op->get_unmapped_area,但是通常都不会有驱动程序定义这个方法,所以一般都是使用current->mm->get_unmapped_area 这个方法。目的是在mmap区域找到一块没有被映射过的vm_area_struct对象。
1>.以arch_get_unmapped_area为例:
当addr非空,表示指定了一个特定的优先选用地址,内核会检查该区域是否与现存区域重叠,由find_vma()完成查找功能。当addr为空或是指定的优先地址不满足分配条件时,内核必须遍历进程中可用的区域,设法找到一个大小适当的空闲区域,有vm_unmapped_area()做实际的工作。
unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,
unsigned long len, unsigned long pgoff, unsigned long flags)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;
struct vm_unmapped_area_info info;
if (len > TASK_SIZE - mmap_min_addr)
return -ENOMEM;
/* MAP_FIXED : 表示映射将在固定地址创建 */
if (flags & MAP_FIXED)
return addr;
if (addr) {
addr = PAGE_ALIGN(addr);
/*
* find_vma() 寻找第一个满足 addr < vm_area_struct->vm_end 的vma区
* vma = NULL 在vma红黑树的右子树,addr 是所存在的所有线性区线性地址最大
* vma != NULL 一定是tmp == NULL (tmp在find_vma指向当前结点)跳出循环的
*/
vma = find_vma(mm, addr);
/*
* 以下分别判断:
* 1: 请求分配的长度是否小于进程虚拟地址空间大小
* 2: 新分配的虚拟地址空间的起始地址是否在mmap_min_addr(允许分配虚拟地址空间的最低地址)之上
* 3: vma是否空
* 4: vma非空,新分配的虚拟地址空间,是否与相邻的vma重合
*/
if (TASK_SIZE - len >= addr && addr >= mmap_min_addr &&
(!vma || addr + len <= vma->vm_start))
return addr;
}
info.flags = 0;
info.length = len;
info.low_limit = mm->mmap_base;
info.high_limit = TASK_SIZE;
info.align_mask = 0;
return vm_unmapped_area(&info);
}
//当addr为空或者指定的优选地址不满足分配条件时,内核必须遍历进程中可用的区域,设法找到一个大小适当的空闲区域,vm_unmapped_area()完成实际的工作。
static inline unsigned long vm_unmapped_area(struct vm_unmapped_area_info *info)
{
/* arch_get_unmapped_area是低地址到高地址创建映射 所以这时默认调用unmapped_area */
if (info->flags & VM_UNMAPPED_AREA_TOPDOWN)
return unmapped_area_topdown(info); 从高地址到低地址穿点映射。
else
return unmapped_area(info); 从低地址到高地址创建映射。
}
3.5.2.mmap_region
mmap_region是mmap实现的另一个关键点,它实现对设备文件或普通文件的file->f_op->mmap(file, vma)函数的调用mmap_region。
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)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
...
vma = vm_area_alloc(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;
if (file) {
...
vma->vm_file = get_file(file);
error = call_mmap(file, vma);
...
} else if (vm_flags & VM_SHARED) {
...
} else {
vma_set_anonymous(vma);
}
vma_link(mm, vma, prev, rb_link, rb_parent);
...
return addr;
...
}
该方法先调用vm_area_alloc,分配一个类型为struct vm_area_struct的实例,并赋值给vma,然后设置vma的起始地址、结束地址等信息。这个vma里包含的内容,就是上面pmap命令输出的内存段。之后,如果我们是想mmap一个file,则调用call_mmap:
static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{
return file->f_op->mmap(file, vma);
}
该方法又调用了file->f_op->mmap指针指向的方法,以ext4文件系统为例,该方法为ext4_file_mmap:
// fs/ext4/file.c
static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{
...
if (IS_DAX(file_inode(file))) {
...
} else {
vma->vm_ops = &ext4_file_vm_ops;
}
return 0;
}
最后 mmap_region方法返回该内存段的起始地址给用户。mmap系统调用只是为当前进程分配并初始化了一个vma实例,用来标识该进程拥有这段以vma表示的内存空间,并没有实际分配物理内存。
4.mmap案例测试
mmap_test.c:
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/time.h>
#include<fcntl.h>
#include<sys/mman.h>
#define MAX 1024*128
int main()
{
int i=0;
int count=0, fd=0;
struct timeval tv1, tv2;
char *array = (char *)malloc(MAX);
/*read*/
gettimeofday( &tv1, NULL );
fd = open( "./mmap_test", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
if(fd<0)
printf("Open file failed\n");
if(MAX != read( fd, (char*)array, MAX ))
{
printf("Reading data failed...\n");
return -1;
}
memset(array, 'a', MAX);
lseek(fd,0,SEEK_SET);
if(MAX != write(fd, (void *)array, MAX))
{
printf( "Writing data failed...\n" );
return -1;
}
close( fd );
gettimeofday( &tv2, NULL );
free( array );
printf( "Time of read/write: %ldus\n", (tv2.tv_usec - tv1.tv_usec));
/*mmap*/
gettimeofday( &tv1, NULL );
fd = open( "./mmap_test2", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
array = mmap( NULL, MAX, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
memset(array, 'b', MAX);
munmap( array, MAX );
msync( array, MAX, MS_SYNC );
close( fd );
gettimeofday( &tv2, NULL );
printf( "Time of mmap/munmap/msync: %ldus\n", (tv2.tv_usec - tv1.tv_usec));
return 0;
}
首先创建两个128KB的空文件,然后执行./mmap_test
dd bs=1024 count=128 if=/dev/zero of=./mmap_test
dd bs=1024 count=128 if=/dev/zero of=./mmap_test2
两个文件内容分别变成了’A’和’B’:
Time of read/write: 134us
Time of mmap/munmap/msync: 91us
refer to
- https://www.jianshu.com/p/755338d11865
- https://www.jianshu.com/p/71c9b73d788e
- https://www.cnblogs.com/holyxp/p/10016582.html