目录
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, 并对其进行初始化
-
get_unmapped_area:在进程地址空间中寻找一个可以使用的线性地址区间,它返回一段没有映射过的空间的起始地址
-
vm_flags:组合新线性区标志
-
mlock_future_check:判断是否超过进程锁住不能换出的页数的阀值rlim_cur
-
根据flags和prot来修正vm_flags标志
-
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, 并对其进行初始化
-
may_expand_vm:检查调用进程是否在扩展新的地址空间后,是否会超过进程空间限制值
-
munmap_vma_range:unmap覆盖addr开始len大小的所有vma的页面
-
accountable_mapping:Private writable mapping: check memory availability?
-
vma_merge:查询以addr为起始地址,大小为len的区间是否可以和已有的vma进行合并,如果可以合并,返回合并后的vma
-
vm_area_alloc: 如果vma_merge不能合并,将调用此函数分配新的vma
-
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
-
vma_link:将申请的新vma加入mm中的vma链表
-
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换入内存空间。
参考文档
- 奔跑吧,Linux内核
- https://blog.csdn.net/lggbxf/article/details/94012088