本文主要是介绍内存管理源码分析-mmap函数在内核的运行机制以及源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
mmap函数的介绍
mmap
函数的主要作用是可以将一个文件或者设备的内容映射到内存当中,用户就可以通过一些内存操作方式(如memcpy
、memset
)对文件或者设备进行直接的操作。这种操作可以减少一些IO的开销,如通过传统的读写文件的方式,可能会频繁的触发系统调用导致IO效率的降低。需要注意的是mmap
函数的内存分配方式是页对齐的,即使用户只需要2字节的数据,mmap
函数也会分配一个页的内存空间给用户。用户如果使用mmap函数对文件进行映射,用户在这段内存修改的数据不会马上回写到文件中,只用调用msync
或者munmap
函数后,修改后的数据才会同步到文件当中。
mmap
函数原型是
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
函数参数含义如下:
start:用户指定的映射区起始地址,如果设置为0时表示由系统决定映射区的起始地址。
length:指定映射的长度,单位是字节,最终分配的空间是页对齐的
prot:prot代表protection,内存保护标志,指定这段内存空间的保护特性,有下列的保护方式
- PROT_EXEC: 映射空间的数据可以执行
- PROT_READ: 映射空间的数据可以读取
- PROT_WRITE: 映射空间的数据可以写入
- PROT_NONE: 映射空间的数据不可以访问
flags: 指定映射对象的类型,常用有如下类型
- MAP_SHARED: 以共享内存的方式打开这段内存,即其他进程可以同时访问这段mmap出来的空间
- MAP_PRIVATE: 于MAP_SHARED相反,这个进程自己独有的内存空间
- MAP_LOCKED: 可以让系统不对mmap映射出来的内存进行换出
- MAP_ANONYMOUS: 匿名映射,即不通过文件进行映射,直接返回一段无指定文件关联的内存
- MAP_POPULATE: 提前为映射出来的内存建立好页表,可以减少用户访问过程中出发啊page-fault的次数,只能用于匿名映射
fd: 文件描述符,如果是基于文件的映射,这个fd应该是open函数的返回值,如果是匿名映射,则这个值需要设置为-1
offset: 开始映射的位置,例如文件映射,表示从文件第offset字节的位置开始,映射length字节长的数据
mmap函数的内核源码分析(基于Linux 4.x版本)
mmap
函数有多种用法,包括私有文件映射,共享文件映射,私有匿名映射,共享匿名映射(只能用于具有父子关系的进程之间)。
sys_mmap函数
mmap
函数与其他函数一样,都是通过系统调用进入内核,mmap
函数对应的内核系统调用是sys_mmap
函数:
unsigned long sys_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) // 判断offset是否是页对齐,如果不是页对齐就返回错误goto out;// 注意off >> PAGE_SHIFT表达式,用于计算offset位于第几个页error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
out:return error;
}
从函数可以知道,sys_mmap
函数主要处理了offset的页对齐问题。
sys_mmap_pgoff函数
sys_mmap_pgoff
函数源码如下(省略部分不核心的代码),需要注意最后一个参数pgoff
表示的是从第几个页开始映射:
unsigned long sys_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 = -EBADF;if (!(flags & MAP_ANONYMOUS)) { // 如果不是匿名映射,则表示是基于文件的映射file = fget(fd); // 文件映射先根据fd获取都文件对应的file结构retval = -EINVAL;}flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
out_fput:if (file) fput(file); //释放对file的引用
out:return retval;
}
从sys_mmap_pgoff
函数的代码可以知道,这个函数主要处理了文件映射和匿名映射的预处理,在进入vm_mmap_pgoff
函数之前,如果file不等于NULL,则表示为文件映射,如果是NULL,则表示是匿名映射。
mm_struct和vm_area_struct结构的介绍
在分析vm_mmap_pgoff
函数以及接下来的其他函数之前,需要简单介绍两个重要的数据结构,mm_struct
以及vm_area_struct
。简单来说,linux每一个进程都对应了一段虚拟地址空间,进程的所有程序的执行,数据的更新都是在这段虚拟地址空间上进行操作,然后通过页表机制,将虚拟地址空间转换为物理内存的地址空间,然后进行具体的数据执行和保存。Linux的进程是通过task_struct
结构进行表示,那么每个进程对应的虚拟地址空间通过mm_struct
进行表示,因此我们可以看到task_struct
结构内包含mm_struct
结构的成员变量。mm_struct
结构管理一个list,这个list保存很多的内存块(因为linux是按需分配,因此一个mm_struct
结构包含多个内存块),这些内存块通过vm_area_struct
结构进行表示,它记录了具体的虚拟地址的起始以及结束范围,一般被称为vma
。
vm_mmap_pgoff函数
接下来正式分析vm_mmap_pgoff
函数
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; // 对应MAP_POPULATE参数,表示是否提前建立好页表ret = security_mmap_file(file, prot, flag); // 这个函数与安全相关,这里不作分析,默认返回trueif (!ret) {down_write(&mm->mmap_sem);ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff,&populate); // 这个函数处理完毕之后,返回populate变量的值up_write(&mm->mmap_sem);if (populate)mm_populate(ret, populate); // 根据populate参数的值,是否进行建立页表等操作}return ret;
}
vm_mmap_pgoff
函数处理了一下安全性问题就进入了下一阶段的函数,然后根据返回结构处理页表建立的问题。
do_mmap_pgoff函数
do_mmap_pgoff
函数是mmap
函数的核心处理流程,因此篇幅比较长,裁剪了一部分变量合法性检查的代码:
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 mm_struct *mm = current->mm;vm_flags_t vm_flags;*populate = 0; // 初始化为不进行预先页表建立len = PAGE_ALIGN(len); // len原来是字节的长度,这里转换为页长度,即将申请映射长度进行页对齐addr = get_unmapped_area(file, addr, len, pgoff, flags); // 获取一段当前进程未被使用的虚拟地址空间,并返回其起始地址if (addr & ~PAGE_MASK) // 如果地址不是是页对齐,addr值可能是错误值(大概类似-EINVAL这种?),然后将错误结果返回给用户了return addr;vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; // 检查flag的参数设置,将mmap的flag转换为vm_area_struct的flagif (flags & MAP_LOCKED) // 如果设置了MAP_LOCKED参数if (!can_do_mlock()) // 检查一下内存可用空间等信息,看看是否可以进行mlockreturn -EPERM;if (mlock_future_check(mm, vm_flags, len)) // 继续检查是否可以进行mlockreturn -EAGAIN;if (file) { // 如果file不为NULL,则表示是基于文件的映射,如果是NULL则是匿名映射struct inode *inode = file_inode(file); // 根据file获取inode结构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)) // 判断是否为APPEND-ONLY文件,mmap不允许写入这种类型文件return -EACCES;if (locks_verify_locked(file))return -EAGAIN;// 更新一系列的使用于vm_area_struct的flagvm_flags |= VM_SHARED | VM_MAYSHARE;if (!(file->f_mode & FMODE_WRITE))vm_flags &= ~(VM_MAYWRITE | VM_SHARED);case MAP_PRIVATE: // 私有映射,也是设置flagif (!(file->f_mode & FMODE_READ)) // 如果文件本身不允许读,那么就直接返回return -EACCES;if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {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 { // 匿名映射,也是设置flagswitch (flags & MAP_TYPE) {case MAP_SHARED:if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))return -EINVAL;pgoff = 0; // 共享匿名映射忽略pgoffvm_flags |= VM_SHARED | VM_MAYSHARE;break;case MAP_PRIVATE:pgoff = addr >> PAGE_SHIFT; // 匿名私有映射使用分配出来的addr作为pgoffbreak;default:return -EINVAL;}}addr = mmap_region(file, addr, len, vm_flags, pgoff);if (!IS_ERR_VALUE(addr) &&((vm_flags & VM_LOCKED) ||(flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))*populate = len; // 根据返回的结果,以及MAP_POPULATE的参数,决定了进行页表建立的长度return addr;
}
这个函数首先处理的映射程度的页对齐,然后通过get_unmapped_area
函数获取了一段未使用的内存,然后根据映射类型([文件,匿名] x [私有,共享])进行了对应的flag参数设置,接下来通过mmap_region
函数完成映射过程,最后再判断一下是否要进行提前页表建立(populate=prefetch)。
mmap_region函数
mmap_region
函数完成最后的映射过程,即将用户需要映射的虚拟地址范围建立起来,然后再将其加入当前进程的mm_struct
结构中。
unsigned long mmap_region(struct file *file, unsigned long addr,unsigned long len, vm_flags_t vm_flags, unsigned long pgoff)
{struct mm_struct *mm = current->mm; // 获取当前进程的mm_struct结构struct vm_area_struct *vma, *prev;int error;struct rb_node **rb_link, *rb_parent;unsigned long charged = 0;if (!may_expand_vm(mm, len >> PAGE_SHIFT)) { // 检查需要的申请的长度是否超过的限制unsigned long nr_pages;if (!(vm_flags & MAP_FIXED))return -ENOMEM;nr_pages = count_vma_pages_range(mm, addr, addr + len);if (!may_expand_vm(mm, (len >> PAGE_SHIFT) - nr_pages))return -ENOMEM;}/* Clear old maps */error = -ENOMEM;
munmap_back:// 用find_vma_links函数寻找当前进程的虚拟地址空间所管理的内存块(vma)是否与目前预备分配的内存块的地址有相交的关系,如果有先将其unmapif (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) {if (do_munmap(mm, addr, len))return -ENOMEM;goto munmap_back;}if (accountable_mapping(file, vm_flags)) {charged = len >> PAGE_SHIFT;if (security_vm_enough_memory_mm(mm, charged))return -ENOMEM;vm_flags |= VM_ACCOUNT;}// 当前申请的虚拟地址空间是否可以当前进程的虚拟地址空间进行合并,如果可以合并,直接修改当前进程的vma的vm_start和vm_end的值vma = vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, file, pgoff, NULL);if (vma)goto out;// 如果无法合并,根据用户申请的地址空间范围,分配一个新的vma结构vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);if (!vma) {error = -ENOMEM;goto unacct_error;}// 初始化vma的信息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) { // 如果是文件映射if (vm_flags & VM_DENYWRITE) { // 文件不允许写入error = deny_write_access(file);if (error)goto free_vma;}if (vm_flags & VM_SHARED) { // 内存是共享的,能被其他进程访问error = mapping_map_writable(file->f_mapping); // 增加mapping共享vma的统计数目,如果非共享的情况下,mapping的共享vma统计数目是-1if (error)goto allow_write_and_free_vma;}vma->vm_file = get_file(file); // 增加file的引用计数,然后赋给vma->vm_fileerror = file->f_op->mmap(file, vma); // 调用文件系统的mmap函数作处理,文件系统会根据设计设定各自的mmap函数,同时还是设定fault函数去处理page fault的情况if (error)goto unmap_and_free_vma;// 执行文件系统自身的mmap函数之后,重新赋值addr = vma->vm_start;vm_flags = vma->vm_flags;} else if (vm_flags & VM_SHARED) { // 如果是匿名共享映射error = shmem_zero_setup(vma); // 将映射的文件指向/dev/zero设备文件,基于这个设备文件创建共享内存映射,并利用system V的一套共享内存机制if (error)goto free_vma;}vma_link(mm, vma, prev, rb_link, rb_parent); // 将新的vma结构加入到当前进程管理的虚拟地址空间的结构(mm_struct)的list当中if (file) {if (vm_flags & VM_SHARED)mapping_unmap_writable(file->f_mapping);if (vm_flags & VM_DENYWRITE)allow_write_access(file);}file = vma->vm_file;
out:perf_event_mmap(vma);vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT); // 更新当前的虚拟地址空间的使用统计信息if (vm_flags & VM_LOCKED) {if (!((vm_flags & VM_SPECIAL) || is_vm_hugetlb_page(vma) ||vma == get_gate_vma(current->mm)))mm->locked_vm += (len >> PAGE_SHIFT);elsevma->vm_flags &= ~VM_LOCKED;}if (file)uprobe_mmap(vma);vma->vm_flags |= VM_SOFTDIRTY;vma_set_page_prot(vma);return addr;
}
根据源码我们可以知道,该函数根据用户需要分配的地址空间的信息,或扩展当前的进程的虚拟地址空间范围,或创建一个新的vma结构加入到进程的mm_struct
当中,这样当前进程就有了可以直接访问的mmap分配的内存区域。这样就完成了整个mmap的映射过程,但实质上只是分配了vma结构去进程的虚拟地址空间当中,访问的时候会触发page-fault缺页异常,才会给这些刚刚分配的虚拟地址空间的vma结构建立虚拟地址和物理地址的映射关系。
文件私有映射和文件共享映射
基于mmap_region
函数的分析可以发现: 基于mmap
函数的私有文件映射和共享文件映射都需要建立各自的vma结构加入到当前进程的地址虚拟地址空间。对于共享文件映射(共享内存)而言,不同进程之间都是建立各自的vma结构加入到mm_struct
,这些各自的vma结构可能还没有建立虚拟地址到物理地址的页表映射,因此可能需要触发多次page-fault缺页异常,才能建立好各自进程的页表映射。
如进程A通过mmap
函数创建了一个基于文件的共享内存区域,进程A将对应范围的vma结构加入进程A的mm_struct
结构中,在后续访问中,通过page-fault缺页异常,逐渐构建了页表映射。此时进程B也通过mmap
函数访问这段共享内存区域,此时仍然需要通过mmap_region
函数创建vma结构,加入到进程Bmm_struct
结构中。虽然进程A为这段共享内存创建好了页表,但是对于进程B的页表仍然是没有创建好,仍然需要page-fault缺页异常构建其页表映射,即使进程A和进程B的页表映射都会指向同一段内存区域。因此,基于文件映射而言,会触发比较多的page-fault缺页异常,影响到IO的性能。
匿名私有映射和匿名共享映射
匿名私有映射
匿名私有映射的作用是返回一段匿名的内存空间给用户,但是也许会有疑问,这和一般通过malloc
函数分配的内存空间有什么差异呢。在分析差异之前,需要了解基于glibc
的malloc
函数是如何实现的。
malloc
函数基于三种方式给用户分配内存(需要注意的是,分配的都是虚拟地址空间,即vma,实际的物理内存还是需要page-fault缺页异常才能真实分配):
- 字节级别的内存分配: malloc内部有一个许多小的内存块(chunk)组成的chunk list,这个chunk list包含了小的内存块,当用户内存块的时候会首先在chunk list找是否有满足需求的内存块,如果有就直接返回,这样的设计是为了避免频繁的系统调用。
- 基于brk的内存分配: 当chunk list无法满足用户的需求时,就要通过brk系统调用,brk系统调用主要是将进程虚拟地址空间的data段的尾指针(end_data))根据内存请求尺寸扩展一下,同时更改或创建对应的vma,同时内核和进程都记录更新之后data段尾指针的地址,后续访问中再通过page-fault缺页异常获得这段空间的实际物理内存。
- 基于mmap的内存分配: 当请求内存的空间大于128KB(默认值,可调)时,会调用mmap匿名映射进行内存分配,它的分配位置时位于进程虚拟地址空间的堆和栈之间的一块独立的空间,同时也会创建一个新的vma结构,被进程记录。
brk
是通过扩展data尾指针(end_data)的方式进行虚拟地址空间的分配,那么带来一个现象,如果用户连续通过malloc
函数分配50KB,以及80KB的内存,那么就会将进程的end_data指针扩位于130KB处,如果这个时候释放掉50KB的空间,那么这段空间不会实际释放,因为如果将指针地址减去30KB,那么第二次分配的80KB的数据就是寻址错误,因此基于brk的内存必须等指针末尾的内存free
了之后,才能free
前面的内存。
mmap
是在堆和栈之间的找一块独立的空间进行分配,那么这段独立的空间就可以自由地真正地释放掉内存。
因此brk
和mmap
的主要差别在于其分配的方式,以及是否真正释放内存。
那么为什么内存不全用mmap
的方式进行分配呢? 这是因为mmap的开销相对比较大,而且mmap
释放的空间都是真释放的,因此重新mmap
一段内存的时候会触发大量的page-fault缺页异常。而brk
分配的空间,由于不是真正释放,因此可以将那段假释放的空间重新投入使用,这样不会触发大量page-fault缺页异常。
因此,通过上面的分析以及讨论,我们可以知道通过mmap
函数进行匿名私有分配的内存的特性以及用途。
匿名共享映射
匿名共享映射主要作用是建立一块没有backing-file的共享内存,即没有对应一个实际的文件系统上的普通文件的共享内存(实际上匿名共享映射对应了一个特殊的文件)。它相对基于文件的共享映射有更高的性能,因为文件所在的硬盘相对于内存是非常慢的设备,因此触发page-fault时,他需要花很多时间访问硬盘才能将该文件对应的页的数据读入到内存,然后返回给用户。需要注意的是基于mmap的共享内存只能适用于具有父子关系的进程之间。
匿名共享映射的实现基于POSIX共享内存机制,我们针对这部分进行源码分析,我们从mmap_region
函数的调用的shmem_zero_setup
函数开始:
shmem_zero_setup函数
shmem_zero_setup
函数获得了一个特殊的文件,然后基于这个特殊的文件进行共享内存映射,并获取了一套共享内存专用的操作函数集合。
int shmem_zero_setup(struct vm_area_struct *vma)
{struct file *file;loff_t size = vma->vm_end - vma->vm_start; //计算需要分配的空间file = shmem_file_setup("dev/zero", size, vma->vm_flags); // 打开特殊设备文件/dev/zero,作为backed-fileif (IS_ERR(file))return PTR_ERR(file);if (vma->vm_file)fput(vma->vm_file);vma->vm_file = file; // 将vma->file更新为匿名共享内存对应的特殊的设备文件vma->vm_ops = &shmem_vm_ops; // 使用匿名共享内存的一套操作函数集合return 0;
}static const struct vm_operations_struct shmem_vm_ops = {.fault = shmem_fault,.map_pages = filemap_map_pages,
};
shmem_fault
函数的主要作用是当触发page-fault缺页异常时,会调用这个共享内存专用的处理函数,进行一些处理。
shmem_file_setup函数
POSXI共享内存的内部,维护了一个tmpfs文件系统,所有的用于共享内存映射的特殊文件都是保存在这个文件系统中`。
struct file *shmem_file_setup(const char *name, loff_t size, unsigned long flags)
{return __shmem_file_setup(name, size, flags, 0); // 简单地调用__shmem_file_setup函数
}static struct file *__shmem_file_setup(const char *name, loff_t size,unsigned long flags, unsigned int i_flags)
{struct file *res;struct inode *inode;struct path path;struct super_block *sb;struct qstr this;if (IS_ERR(shm_mnt)) // 全局变量,表示TMPFS的挂载信息return ERR_CAST(shm_mnt);if (size < 0 || size > MAX_LFS_FILESIZE) // 大于最大可分配的内存空间return ERR_PTR(-EINVAL);if (shmem_acct_size(flags, size)) // 是否够空间进行分配return ERR_PTR(-ENOMEM);// 下面的所有操作都是为了在tmpfs建立一个特殊映射文件res = ERR_PTR(-ENOMEM);this.name = name; // 初始化名字this.len = strlen(name);this.hash = 0;sb = shm_mnt->mnt_sb;path.mnt = mntget(shm_mnt);path.dentry = d_alloc_pseudo(sb, &this); // 创建dentryif (!path.dentry)goto put_memory;d_set_d_op(path.dentry, &anon_ops); // 设置flagres = ERR_PTR(-ENOSPC);inode = shmem_get_inode(sb, NULL, S_IFREG | S_IRWXUGO, 0, flags); // 从tmpfs获取一个特殊映射文件对应的inode结构if (!inode)goto put_memory;inode->i_flags |= i_flags;d_instantiate(path.dentry, inode); // 将dentry和inode链接在一起,建立联系inode->i_size = size; // 文件尺寸为需要映射的空间的尺寸clear_nlink(inode);res = ERR_PTR(ramfs_nommu_expand_for_mapping(inode, size));if (IS_ERR(res))goto put_path;res = alloc_file(&path, FMODE_WRITE | FMODE_READ,&shmem_file_operations); // 有了inode和dentry之后,就可以创建file结构,并给与特殊映射文件专用的处理函数集合if (IS_ERR(res))goto put_path;return res; // 返回这个特殊映射文件的file结构put_memory:shmem_unacct_size(flags, size);
put_path:path_put(&path);return res;
}
综上所述,匿名共享映射其实本质上只是在tmpfs上建立一个特殊的映射文件(这个特殊的文件在用户态不可见),然后给这个映射文件赋予映射所需要的函数集合,如shmem_vm_ops
函数集合,那么在触发缺页异常的时候,就会根据这个函数集合处理缺页异常,返回给用户实际的物理页信息。
同时也可以发现,为什么匿名共享内存只能用于具有父子关系的进程之间,这是因为不同的进程之间,由于是匿名映射,无法找到一个共同的特殊文件进行映射(由于该映射特殊文件在用户态不可见)。
这篇关于内存管理源码分析-mmap函数在内核的运行机制以及源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!