mmap, munmap, mremap, msync, remap_file_pages系统调用

mmap

系统调用号

9

函数签名

内核接口

asmlinkage long sys_mmap(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, off_t pgoff);

glibc封装

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

简介

mmap做了一个特别神奇的事:把硬盘上的文件与内存之间建立映射。首先,我们来看看最终效果。假设我们有一个二进制文件data.bin,其内容为(16进制):

11 45 14 19 19 81 00

我们通过mmap,将这个文件映射到了内存中从0x10000开始的区域。接下来,如果我们的程序从内存0x10002读取2字节的内容,将得到0x1914这个数字(小端序)。

也就是说,我们没有借助read系统调用,而是直接对某个内存区域进行读取,就能读取到硬盘上的文件的内容。

接着我们来看看其参数。

总得来说,mmap的行为是,在flags的控制下,将描述符为fd的文件中,从offset位置开始,length个字节映射到地址为addr的内存空间中,并设置其内存保护为prot

在一般情况下,地址和长度应遵守这样的限制:

  • 如果addrNULL,则内核自己选择适当的内存地址进行映射。如果其不为NULL,则内核以addr的值为参考,选择一个适当的内存地址进行映射(一般是之后最近的页边界)。
  • offset应为页大小的倍数。
  • length应大于0。如果被映射的大小不是页大小的整数倍,则剩下的页的部分会被0填充。

控制mmap行为的核心为flags。其可以包含以下标志位:

  • 核心标志位

    以下三个标志位必须且只能包含一个。

    • MAP_SHARED

      建立一个共享的映射。

      如果在该进程中,对该映射后的内存区域进行修改,那么在别的进程中,如果其使用了mmap,将同一个文件也进行了映射,那么可以同步看见该修改。同时被映射的文件也会被修改。

    • MAP_SHARED_VALIDATE

      行为和MAP_SHARED类似。但是会核验flags,如果其包含了未知的标志位,将报错EOPNOTSUPP

    • MAP_PRIVATE

      建立一个私有的写时复制(copy-on-write)的映射。

      对该映射的内存区域进行的修改不会同步到其他进程中,也不会修改硬盘里相应的文件。

  • 附加标志位

    除了三个必要的标志位之外,还有一些标志位也可以被包含。其主要包括

    • MAP_ANONYMOUS

      忽略fd,被映射的内存区域将被初始化为0。

      此时fd应为-1,offset应为0。

    • MAP_FIXED

      addr看作确切的地址,而非一个参考。

      内核将准确地将文件映射到从addr开始的内存区域。如果这个映射与之前已经存在的内存映射有重合,则重合的部分将被新的映射覆盖。

    • MAP_FIXED_NOREPLACE

      行为和MAP_FIXED类似,但不会覆盖已有的内存映射。如果与已有的内存映射有重合,那么将直接返回错误EEXIST

prot参数则是控制映射的内存区域的内存保护,其可能的值包括

  • PROC_EXEC

    页可执行

  • PROC_READ

    页可读

  • PROC_WRITE

    页可写

  • PROC_NONE

    页不可访问

使用mmap读取文件的好处在于:使用read读取文件时,会先将文件的内容从硬盘上复制到内核的内存空间中,然后再由内核将数据复制到用户的内存空间中。但使用mmap时,文件的内容是可以直接复制到用户的内存空间中的。

实现

mmap的实现位于Linux内核源码的arch/x86/kernel/sys_x86_64.c,其直接调用了位于mm/mmap.cksys_mmap_pgoff函数。在经过了复杂的函数链之后,我们可以发现,其最终是调用的位于include/linux/fs.hcall_mmap函数:

static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{
	return file->f_op->mmap(file, vma);
}

其中struct vm_area_struct这个结构体,表示一块虚拟内存,其包含一个类型为struct vm_operations_struct的字段vm_ops,表示对虚拟内存的操作,其定义在include/linux/mm.h:

/*
 * These are the virtual MM functions - opening of an area, closing and
 * unmapping it (needed to keep files on disk up-to-date etc), pointer
 * to the functions called when a no-page or a wp-page exception occurs.
 */
struct vm_operations_struct {
	void (*open)(struct vm_area_struct * area);
	void (*close)(struct vm_area_struct * area);
	int (*split)(struct vm_area_struct * area, unsigned long addr);
	int (*mremap)(struct vm_area_struct * area);
	vm_fault_t (*fault)(struct vm_fault *vmf);
	vm_fault_t (*huge_fault)(struct vm_fault *vmf,
			enum page_entry_size pe_size);
	void (*map_pages)(struct vm_fault *vmf,
			pgoff_t start_pgoff, pgoff_t end_pgoff);
	unsigned long (*pagesize)(struct vm_area_struct * area);

	/* notification that a previously read-only page is about to become
	 * writable, if an error is returned it will cause a SIGBUS */
	vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);

	/* same as page_mkwrite when using VM_PFNMAP|VM_MIXEDMAP */
	vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf);

	/* called by access_process_vm when get_user_pages() fails, typically
	 * for use by special VMAs that can switch between memory and hardware
	 */
	int (*access)(struct vm_area_struct *vma, unsigned long addr,
		      void *buf, int len, int write);

	/* Called by the /proc/PID/maps code to ask the vma whether it
	 * has a special name.  Returning non-NULL will also cause this
	 * vma to be dumped unconditionally. */
	const char *(*name)(struct vm_area_struct *vma);

#ifdef CONFIG_NUMA
	/*
	 * set_policy() op must add a reference to any non-NULL @new mempolicy
	 * to hold the policy upon return.  Caller should pass NULL @new to
	 * remove a policy and fall back to surrounding context--i.e. do not
	 * install a MPOL_DEFAULT policy, nor the task or system default
	 * mempolicy.
	 */
	int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);

	/*
	 * get_policy() op must add reference [mpol_get()] to any policy at
	 * (vma,addr) marked as MPOL_SHARED.  The shared policy infrastructure
	 * in mm/mempolicy.c will do this automatically.
	 * get_policy() must NOT add a ref if the policy at (vma,addr) is not
	 * marked as MPOL_SHARED. vma policies are protected by the mmap_sem.
	 * If no [shared/vma] mempolicy exists at the addr, get_policy() op
	 * must return NULL--i.e., do not "fallback" to task or system default
	 * policy.
	 */
	struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
					unsigned long addr);
#endif
	/*
	 * Called by vm_normal_page() for special PTEs to find the
	 * page for @addr.  This is useful if the default behavior
	 * (using pte_page()) would not find the correct page.
	 */
	struct page *(*find_special_page)(struct vm_area_struct *vma,
					  unsigned long addr);
};

和我们的mmap有关的字段是

vm_fault_t (*fault)(struct vm_fault *vmf);

这个函数指针将在我们出现页错误的时候调用。

我们上面看到,mmap最终落实到了各个文件类型自己定义的mmap操作中。我们常见的EXT4文件系统中,这个操作为函数ext4_file_mmap:

static const struct vm_operations_struct ext4_file_vm_ops = {
	.fault		= ext4_filemap_fault,
	.map_pages	= filemap_map_pages,
	.page_mkwrite   = ext4_page_mkwrite,
};

static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{
	struct inode *inode = file->f_mapping->host;
	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
	struct dax_device *dax_dev = sbi->s_daxdev;

	if (unlikely(ext4_forced_shutdown(sbi)))
		return -EIO;

	/*
	 * We don't support synchronous mappings for non-DAX files and
	 * for DAX files if underneath dax_device is not synchronous.
	 */
	if (!daxdev_mapping_supported(vma, dax_dev))
		return -EOPNOTSUPP;

	file_accessed(file);
	if (IS_DAX(file_inode(file))) {
		vma->vm_ops = &ext4_dax_vm_ops;
		vma->vm_flags |= VM_HUGEPAGE;
	} else {
		vma->vm_ops = &ext4_file_vm_ops;
	}
	return 0;
}

可以看到,在通常情况下,是使用ext4_filemap_fault作为我们之前讲的vm_operations_struct中的fault字段。这个函数最终会被落实到mm/filemap.cfilemap_fault函数。在这个函数中,如果这个文件在内核的页缓存中,则直接去找那个页即可。如果没有,则调用pagecache_get_page,最终使用__add_to_page_cache_locked创建相应的页。

munmap

系统调用号

11

函数签名

内核接口

asmlinkage long sys_munmap(unsigned long addr, size_t len);

glibc封装

#include <sys/mman.h>
int munmap(void *addr, size_t length);

简介

在使用mmap将文件映射到内存空间之后,即使我们使用close关闭被映射的文件的描述符,该映射依然存在。如果需要取消相应的映射,我们可以使用munmap

munmap接受两个参数,表示需要取消映射的内存范围,其中,addr需要是页大小的整数倍。对于从addr开始,长度为length的内存区域,只要某个通过mmap建立的内存映射与该区域有交集,那么相应的内存映射就将被取消。

实现

munmap的实现位于Linux内核源码的mm/mmap.c文件中,其核心代码为__do_munmap函数:

mremap

系统调用号

25

函数签名

内核接口

asmlinkage long sys_mremap(unsigned long addr, unsigned long old_len, unsigned long new_len, unsigned long flags, unsigned long new_addr);

glibc封装

#define _GNU_SOURCE
#include <sys/mman.h>
void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ... /* void *new_address */);

简介