内存管理基础学习笔记 – 3.3 进程地址空间 – mmap系统调用

  • Post author:
  • Post category:其他




1. 前言

本专题我们开始学习内存管理部分,本文为进程地址空间的学习笔记。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。

mmap/munmap接口是用户空间最常用的一个系统调用接口,主要用于:用户空间分配内存、读写大文件、链接动态库文件,多进程间共享内存。mmap最终底层调用的的内核函数是do_mmap,本文主要介绍do_mmap的执行过程。

kernel版本:5.10

平台:arm64



2. do_mmap函数说明

unsigned long do_mmap(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);



总体参数说明

  • file/pgoff

    如果新的线性区vma将把一个文件映射到内存,则使用文件描述符指针file和文件偏移量pgoff,通过file参数可以看出是否与文件关联,用以区分匿名映射和文件映射
  • addr:

    指定映射到进程地址空间的起始地址,一般设置为null, 让内核选择一个合适的地址
  • len:

    线性地址区间的长度
  • prot:

    指定这个线性区所包含的访问权限
  • flags:

    设置内存映射的属性,如共享映射、私有映射等



prot参数说明

指定这个线性区所包含的访问权限,它最终转换为页表项pte的权限标志

//include/uapi/asm-generic/mman-common.h
#define PROT_READ       0x1             /* page can be read */
#define PROT_WRITE      0x2             /* page can be written */
#define PROT_EXEC       0x4             /* page can be executed */
#define PROT_SEM        0x8             /* page may be used for atomic ops */
#define PROT_NONE       0x0             /* page can not be accessed */
#define PROT_GROWSDOWN  0x01000000      /* mprotect flag: extend change to start of growsdown vma */
#define PROT_GROWSUP    0x02000000      /* mprotect flag: extend change to end of growsup vma *



flags参数说明

设置内存映射的属性,如共享映射、私有映射等

//include/uapi/asm-generic/mman-common.h 
/* 0x01 - 0x03 are defined in linux/mman.h */
#define MAP_TYPE        0x0f            /* Mask for type of mapping (OSF/1 is _wrong_) */
#define MAP_FIXED       0x100           /* Interpret addr exactly */
#define MAP_ANONYMOUS   0x10            /* don't use a file */

/* not used by linux, but here to make sure we don't clash with OSF/1 defines */
#define _MAP_HASSEMAPHORE 0x0200
#define _MAP_INHERIT    0x0400
#define _MAP_UNALIGNED  0x0800

/* These are linux-specific */
#define MAP_GROWSDOWN   0x01000         /* stack-like segment */
#define MAP_DENYWRITE   0x02000         /* ETXTBSY */
#define MAP_EXECUTABLE  0x04000         /* mark it as an executable */
#define MAP_LOCKED      0x08000         /* lock the mapping */
#define MAP_NORESERVE   0x10000         /* don't check for reservations */
#define MAP_POPULATE    0x20000         /* populate (prefault) pagetables */
#define MAP_NONBLOCK    0x40000         /* do not block on IO */
#define MAP_STACK       0x80000         /* give out an address that is best suited for process/thread stacks */
#define MAP_HUGETLB     0x100000        /* create a huge page mapping */
#define MAP_FIXED_NOREPLACE     0x200000/* MAP_FIXED which doesn't unmap underlying mapping *
//include/uapi/linux/mman.h
#define MAP_SHARED      0x01            /* Share changes */
#define MAP_PRIVATE     0x02            /* Changes are private */
#define MAP_SHARED_VALIDATE 0x03        /* share + validate extension flags */



file参数说明

通过file参数可以看出是否与文件关联,用以区分匿名映射和文件映射

在Linux中映射可以分为两种:

  • 匿名映射:没有映射对应的相关文件,匿名映射的内存区域的内容会初始化为0;
  • 文件映射:映射和实际文件相关联,通常把文件内容映射到进程地址空间,这样应用就可以通过操作进程地址空间读写文件



3. 映射类型

根据文件关联性和映射区域是否共享等属性,mmap映射类型分为4类:

  • 私有匿名映射(fd==-1且flags=MAP_ANONYMOUS | MAP_PRIVATE)

    创建的mmap映射是私有匿名映射

    用途:在glibc中分配大块内存,需要分配的内存大于MMAP_THRESHOLD(128kb),glibc会使用mmap代替brk来分配内存

  • 共享匿名映射(fd==-1且flags=MAP_ANONYMOUS | MAP_SHARED)

    用途:让相关进程共享一块内存区域,通常用于父子进程之间通信

  • 私有文件映射(fd为文件句柄,flags=MAP_PRIVATE)

    用途:加载动态共享库

  • 共享文件映射(fd为文件句柄,flags=MAP_SHARED)

    如果prot参数指定了PROT_WRITE,那么打开文件时需要指定O_RDWR标志位。

    用途:

    (1)读写文件

    文件映射到进程地址空间,同时对映射的内容作了修改,内核的回写机制最终会将修改的内容同步到磁盘中。

    (2)进程间通信

    多个进程同时映射到一个相同的文件,就实现了多个进程的共享内存通信。



4. do_mmap

do_mmap(file, addr,len, prot, flags, pgoff, populate, uf)
    |--根据flags和prot作边界处理
    |--addr = get_unmapped_area(file, addr, len, pgoff, flags)
    |  //组合新线性区标志
    |--vm_flags = calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags)|
    |      		mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC
    |  //判断是否超过进程锁住不能换出的页数的阀值rlim_cur
    |--mlock_future_check(mm, vm_flags, len)
    |--根据flags和prot来修正vm_flags标志
    |--addr = mmap_region(file, addr, len, vm_flags, pgoff, uf)

do_mmap为addr,addr+len的区间查找或分配一个vma, 并对其进行初始化

  1. get_unmapped_area:在进程地址空间中寻找一个可以使用的线性地址区间,它返回一段没有映射过的空间的起始地址

  2. vm_flags:组合新线性区标志

  3. mlock_future_check:判断是否超过进程锁住不能换出的页数的阀值rlim_cur

  4. 根据flags和prot来修正vm_flags标志

  5. mmap_region:为addr,addr+len的区间分配一个vma, 并对其进行初始化



|- -根据flags和prot作边界处理

根据flags和prot作边界处理
    |  //应用假定PROT_READ隐含PROT_EXEC,一种情况除外:如果文件系统以noexec方式mount, 此假定不成立
    |--if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
    |      if (!(file && path_noexec(&file->f_path)))
    |          prot |= PROT_EXEC
    |--if (flags & MAP_FIXED_NOREPLACE) 
    |      flags |= MAP_FIXED
    |--if (!(flags & MAP_FIXED))
    |      addr = round_hint_to_min(addr)
    |--if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)//overflow处理
    |      return -EOVERFLOW
    |--if (mm->map_count > sysctl_max_map_count) //Too many mappings
    |      return -ENOMEM



|- -根据flags和prot来修正vm_flags标志

根据flags和prot来修改vm_flags标志
    |-------//>>>>>>文件映射,将请求内存的种类(flags中指定)与打开文件时指定的标志进行比较
    |--if (file)
    |      struct inode *inode = file_inode(file)
    |      file_mmap_ok(file, inode, pgoff, len)
    |      switch (flags & MAP_TYPE)
    |      case MAP_SHARED: /*共享映射*/
    |          /*如果请求的是共享可写内存映射,检查文件是为写入而不是追加模式打开*/
    |          if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
    |              return -EACCES
    |          /*如果请求的是共享内存映射,检查文件上没有强制锁*/
    |          if (locks_verify_locked(inode))
    |              return -EAGAIN
    |              vm_flags |= VM_SHARED | VM_MAYSHARE;
    |          /*如果是不可写共享内存映射,VM_MAYWRITE,VM_SHARED清零*/
    |          if (!(file->f_mode & FMODE_WRITE))
    |              vm_flags &= ~(VM_MAYWRITE | VM_SHARED)
    |     case MAP_PRIVATE: /*私有映射*/
    |         /*任何映射种类,都要检查文件是为读操作而打开*/
    |         if (!(file->f_mode & FMODE_READ))
    |             return -EACCES;
    |------//>>>>>>> 匿名映射
    |--else
    |      switch (flags & MAP_TYPE) {
    |      case MAP_SHARED:
    |          if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
    |              return -EINVAL
    |          vm_flags |= VM_SHARED | VM_MAYSHARE;
    |      case MAP_PRIVATE:
    |          pgoff = addr >> PAGE_SHIFT;
    |  /*Set 'VM_NORESERVE' if we should not account for the memory use of this mapping*/
    |--if (flags & MAP_NORESERVE)
           vm_flags |= VM_NORESERVE



|- -mmap_region

mmap_region(file, addr, len,  vm_flags,  pgoff, uf)
  |  //Check against address space limit
  |--may_expand_vm(mm, vm_flags, len >> PAGE_SHIFT)
  |--munmap_vma_range(mm, addr, len, &prev, &rb_link, &rb_parent, uf)
  |--accountable_mapping(file, vm_flags)
  |  //是否可以扩展旧的vma,如果不能则分配新的
  |--vma_merge(mm, prev, addr,addr+len,vm_flags,...)
  |  //分配一个新的vma
  |--vma = vm_area_alloc(mm) 
  |  //初始化vma
  |--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) 
  |      if (vm_flags & VM_DENYWRITE)//映射的文件不允许写入
  |          deny_write_access(file)//排除常规的文件操作
  |      if (vm_flags & VM_SHARED)
  |          mapping_map_writable(file->f_mapping)//共享文件映射允许其他进程可见, 标记文件为可写
  |      //对映射文件执行mmap方法,大多数文件系统为generic_file_mmap,对于f2fs为f2fs_file_mmap
  |      call_mmap(file, vma)
  |			|--file->f_op->mmap(file, vma);
  |      addr = vma->vm_start
  |      vm_flags = vma->vm_flags
  |-------//>>>>>>共享匿名映射
  |  else if (vm_flags & VM_SHARED)
  |      shmem_zero_setup(vma) //指定映射文件是/dev/zero
  |------//>>>>>>私有匿名映射
  |  else 
  |      vma_set_anonymous(vma) //设置vma->vm_ops为null,vma->vm_ops往往是匿名映射和文件映射的区分标志?
  |--vma_link(mm, vma, prev, rb_link, rb_parent)//将申请的新vma加入mm中的vma链表
  \--vma_set_page_prot(vma);//Update vma->vm_page_prot to reflect vma->vm_flags
  

mmap_region为addr,addr+len的区间分配一个vma, 并对其进行初始化

  1. may_expand_vm:检查调用进程是否在扩展新的地址空间后,是否会超过进程空间限制值

  2. munmap_vma_range:unmap覆盖addr开始len大小的所有vma的页面

  3. accountable_mapping:Private writable mapping: check memory availability?

  4. vma_merge:查询以addr为起始地址,大小为len的区间是否可以和已有的vma进行合并,如果可以合并,返回合并后的vma

  5. vm_area_alloc: 如果vma_merge不能合并,将调用此函数分配新的vma

  6. call_mmap:底层调用了file->f_op->mmap回调,对映射文件执行mmap方法,大多数文件系统为generic_file_mmap,对于f2fs为f2fs_file_mmap,f2fs_file_mmap会设置vma->vm_ops = &f2fs_file_vm_ops,其中的f2fs_file_vm_ops->fault就是_do_fault文件映射缺页中断时执行的,用于将文件内容读到page

  7. vma_link:将申请的新vma加入mm中的vma链表

  8. vma_set_page_prot: 根据vma->vm_flags更新vma->vm_page_prot



5. 小结

https://blog.csdn.net/lggbxf/article/details/94012088

通过分析mmap的源码我们发现在调用mmap()的时候仅仅申请一个vm_area_struct来建立文件与虚拟内存的映射,并没有建立虚拟内存与物理内存的映射。Linux并不在调用mmap()时就为进程分配物理内存空间,直到下次真正访问地址空间时发现数据不存在于物理内存空间时,触发Page Fault即缺页中断,Linux才会将缺失的Page换入内存空间。



参考文档

  1. 奔跑吧,Linux内核
  2. https://blog.csdn.net/lggbxf/article/details/94012088



版权声明:本文为jasonactions原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。