Linux mmap原理与源码分析

1 概述

本文只是笔者为加深对mmap原理印象总结了总结了https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&mid=2247488750&idx=1&sn=247a4603299e203793fac8b6c5e61071&chksm=ce77d2a9f9005bbf3b024bc9f9192f2de63a70fd33db1113d9f9c0d8a1ced2099fbeb727a3d7&cur_album_id=2559805446807928833&scene=21#wechat_redirect知识,建议读者直接跳转到此文阅读。介绍了关于mmap的使用原理和mmap在内核中调用流程源码的大致流程分析,同时介绍了其他涉及的知识。

2 应用使用

应用层用户可以调用mmap来映射内核一段内存到应用层,使得应用层可以直接操作该内存,内存可以作为私有或者共享使用,同样也可以映射某个文件,通过操作该私有或者共享内存来写文件。以下是mmap应用层使用示例:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>

#define PAGE_SIZE 4096

// 映射类型宏定义
#define FILE_SHARED      1
#define FILE_PRIVATE     2  
#define ANON_SHARED      3
#define ANON_PRIVATE     4

// 测试函数声明
void test_file_shared_mapping(void);
void test_file_private_mapping(void);
void test_anon_shared_mapping(void);
void test_anon_private_mapping(void);

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("用法: %s <映射类型>\n", argv[0]);
        printf("映射类型:\n");
        printf("  %d - 文件共享映射\n", FILE_SHARED);
        printf("  %d - 文件私有映射\n", FILE_PRIVATE);
        printf("  %d - 匿名共享映射\n", ANON_SHARED);
        printf("  %d - 匿名私有映射\n", ANON_PRIVATE);
        return 1;
    }

    int map_type = atoi(argv[1]);
    
    switch (map_type) {
        case FILE_SHARED:
            test_file_shared_mapping();
            break;
        case FILE_PRIVATE:
            test_file_private_mapping();
            break;
        case ANON_SHARED:
            test_anon_shared_mapping();
            break;
        case ANON_PRIVATE:
            test_anon_private_mapping();
            break;
        default:
            printf("错误的映射类型\n");
            return 1;
    }
    
    return 0;
}

// 文件共享映射测试
void test_file_shared_mapping(void) {
    printf("=== 文件共享映射测试 ===\n");
    
    const char *filename = "test_shared_file.txt";
    int fd = open(filename, O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        perror("open");
        exit(1);
    }
    
    // 调整文件大小
    if (ftruncate(fd, PAGE_SIZE) == -1) {
        perror("ftruncate");
        close(fd);
        exit(1);
    }
    
    // 创建文件共享映射
    char *shared_mem = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, 
                          MAP_SHARED, fd, 0);
    if (shared_mem == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(1);
    }
    
    // 写入数据
    strcpy(shared_mem, "文件共享映射测试数据");
    printf("父进程写入: %s\n", shared_mem);
    
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        munmap(shared_mem, PAGE_SIZE);
        close(fd);
        exit(1);
    }
    
    if (pid == 0) {
        // 子进程
        printf("子进程读取: %s\n", shared_mem);
        
        // 子进程修改数据
        strcpy(shared_mem, "子进程修改后的数据");
        printf("子进程修改后: %s\n", shared_mem);
        exit(0);
    } else {
        // 父进程
        wait(NULL);  // 等待子进程结束
        printf("父进程最终读取: %s\n", shared_mem);
        
        // 清理
        munmap(shared_mem, PAGE_SIZE);
        close(fd);
        unlink(filename);  // 删除测试文件
    }
}

// 文件私有映射测试
void test_file_private_mapping(void) {
    printf("=== 文件私有映射测试 ===\n");
    
    const char *filename = "test_private_file.txt";
    int fd = open(filename, O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        perror("open");
        exit(1);
    }
    
    // 调整文件大小
    if (ftruncate(fd, PAGE_SIZE) == -1) {
        perror("ftruncate");
        close(fd);
        exit(1);
    }
    
    // 创建文件私有映射
    char *private_mem = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, 
                           MAP_PRIVATE, fd, 0);
    if (private_mem == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(1);
    }
    
    // 写入初始数据
    strcpy(private_mem, "文件私有映射初始数据");
    printf("映射初始数据: %s\n", private_mem);
    
    // 修改数据(写时复制)
    strcpy(private_mem, "修改后的私有数据");
    printf("修改后数据: %s\n", private_mem);
    
    // 清理
    munmap(private_mem, PAGE_SIZE);
    close(fd);
    unlink(filename);
}

// 匿名共享映射测试
void test_anon_shared_mapping(void) {
    printf("=== 匿名共享映射测试 ===\n");
    
    // 创建匿名共享映射
    char *anon_shared = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, 
                              MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (anon_shared == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }
    
    // 写入数据
    strcpy(anon_shared, "匿名共享映射数据");
    printf("父进程写入: %s\n", anon_shared);
    
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        munmap(anon_shared, PAGE_SIZE);
        exit(1);
    }
    
    if (pid == 0) {
        // 子进程
        printf("子进程读取: %s\n", anon_shared);
        
        // 子进程修改数据
        strcpy(anon_shared, "子进程修改的共享数据");
        printf("子进程修改后: %s\n", anon_shared);
        exit(0);
    } else {
        // 父进程
        wait(NULL);  // 等待子进程结束
        printf("父进程最终读取: %s\n", anon_shared);
        
        // 清理
        munmap(anon_shared, PAGE_SIZE);
    }
}

// 匿名私有映射测试
void test_anon_private_mapping(void) {
    printf("=== 匿名私有映射测试 ===\n");
    
    // 创建匿名私有映射
    char *anon_private = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, 
                               MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (anon_private == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }
    
    // 写入初始数据
    strcpy(anon_private, "匿名私有映射初始数据");
    printf("初始数据: %s\n", anon_private);
    
    // 修改数据(写时复制)
    strcpy(anon_private, "修改后的匿名私有数据");
    printf("修改后数据: %s\n", anon_private);
    
    // 清理
    munmap(anon_private, PAGE_SIZE);
}

3 使用原理

用户调用mmap后,陷入内核系统调用mmap中:

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


SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
		unsigned long, prot, unsigned long, flags,
		unsigned long, fd, unsigned long, pgoff)
{
	return ksys_mmap_pgoff(addr, len, prot, flags, fd, pgoff);
}

addr : 表示要映射的这段虚拟内存区域在进程虚拟内存空间中的起始地址(虚拟内存地址),如果这个虚拟地址已经被使用或者是一个无效的地址,那么内核则会自动选取一个合适的地址来划分虚拟内存区域,NULL表示由内核决定,需PAGE_SIZE(4K) 对齐

length:如果是匿名映射,length 参数决定了我们要映射的匿名物理内存有多大,如果是文件映射,length 参数决定了我们要映射的文件区域有多大,需 PAGE_SIZE(4K) 对齐

fd :映射的是磁盘上的一个文件,指定要映射文件的描述符(file descriptor),通过参数 pgoff指定文件映射区域在文件中偏移。

prot :指定其在进程虚拟内存空间中映射出的这段虚拟内存区域 VMA 的访问权限

cpp 复制代码
#define PROT_READ 0x1  表示该虚拟内存区域背后映射的物理内存是可读的
#define PROT_WRITE 0x2  表示该虚拟内存区域背后映射的物理内存是可写的
#define PROT_EXEC 0x4  表示该虚拟内存区域背后映射的物理内存所存储的内容是可以被执行的
#define PROT_NONE 0x0  表示这段虚拟内存区域是不能被访问的,既不可读写,也不可执行。

flags:指定映射方式

cpp 复制代码
#define MAP_FIXED   0x10  强制映射到[addr , addr + length] 这段虚拟内存地址
#define MAP_ANONYMOUS   0x20        匿名映射
#define MAP_SHARED  0x01        共享映射
#define MAP_PRIVATE 0x02        私有映射

3.1 私有匿名映射

MAP_PRIVATE | MAP_ANONYMOUS 表示私有匿名映射,我们常常利用这种映射方式来申请虚拟内存,需要特别强调一下 mmap 私有匿名映射申请到的只是虚拟内存,内核只是在进程虚拟内存空间中划分一段虚拟内存区域 VMA 出来,并将 VMA 该初始化的属性初始化好,mmap 系统调用就结束了(当进程开始访问这段虚拟内存区域时,发现这段虚拟内存区域背后没有任何物理内存与其关联,体现在内核中就是这段虚拟内存地址在页表中的 PTE 项是空的,会进入缺页异常为这段虚拟内存区域分配对应大小的物理内存页)。

3.2 私有文件映射

在调用 mmap 进行内存文件映射的时候可以通过指定参数 flags 为 MAP_PRIVATE,然后将参数 fd 指定为要映射文件的文件描述符(file descriptor)来实现对文件的私有映射。

调用 mmap 进行内存文件映射的时候,内核首先会在进程的虚拟内存空间中创建一个新的虚拟内存区域 VMA 用于映射文件,通过 vm_area_struct->vm_file 将映射文件的 struct flle 结构与虚拟内存映射关联起来。

cpp 复制代码
struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */

	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. */

	/* linked list of VM areas per task, sorted by address */
	struct vm_area_struct *vm_next, *vm_prev;

	struct rb_node vm_rb;

...

	struct mm_struct *vm_mm;	/* The address space we belong to. */

	...
	unsigned long vm_pgoff;	
	struct file * vm_file;		/* File we map to (can be NULL). */

...
} __randomize_layout;

根据 vm_file->f_inode 我们可以关联到映射文件的 struct inode,近而关联到映射文件在磁盘中的磁盘块 i_block即可找到映射的位置。一个文件包含多个磁盘块,当它们被读取到内存之后,一个文件也就对应了多个文件页,这些文件页在内存中统一被一个叫做 page cache 的结构所组织,每一个文件在内核中都会有一个唯一的 page cache 与之对应,用于缓存文件中的数据。

文件的 struct inode 结构中除了有磁盘块的信息之外,还有指向文件 page cache 的 i_mapping 指针。

cpp 复制代码
struct inode {
    struct address_space *i_mapping;
}

当多个进程调用 mmap 对磁盘上同一个文件进行私有文件映射的时候,内核只是在每个进程的虚拟内存空间中创建出一段虚拟内存区域 VMA 出来,此时内核只是为进程申请了用于映射的虚拟内存,并将虚拟内存与文件映射起来,mmap 系统调用就返回了,全程并没有物理内存的影子出现,文件的 page cache 也是空的,没有包含任何的文件页。当任意一个进程开始访问这段映射的虚拟内存时,由于虚拟内存没有映射物理内存会触发缺页异常。

内核会首先通过 vm_area_struct->vm_pgoff 在文件 page cache 中查找是否有缓存相应的文件页。如果文件页不在 page cache 中,内核则会在物理内存中分配一个内存页,然后将新分配的内存页加入到 page cache 中,并增加页引用计数。随后会通过 address_space_operations 重定义的 readpage 激活块设备驱动从磁盘中读取映射的文件内容,然后将读取到的内容填充新分配的内存页。

cpp 复制代码
static const struct address_space_operations ext4_aops = {
    .readpage       = ext4_readpage
}

内核会为映射的这段虚拟内存在页表中创建 PTE,然后将虚拟内存与 page cache 中的文件页通过 PTE 关联起来,缺页处理就结束了,但是由于我们指定的私有文件映射,所以 PTE 中文件页的权限是只读的。此时如果另外一个进程(进程2)只读形式访问该内存块,虽然同样进入异常,但前面的进程(进程1)已经分配好缓存页,此时该page cache中的物理缓存页会与当前进程分配好的虚拟地址映射起来。

当任意一个进程通过虚拟映射区对文件进行写入操作,由于采用的是私有文件映射的方式,各个进程页表中对应 PTE 却是只读的,会产生一个写保护类型的缺页中断,内核会重新申请一个内存页,然后将 page cache 中的内容拷贝到这个新的内存页中,进程页表中对应的 PTE 会重新关联到这个新的内存页上,此时 PTE 的权限变为可写,同理对另外一个进程也是再分配一个物理内存映射到虚拟内存,将page cache内容拷贝到当前页内存上。两个进程的写入此时是独立的不会写入磁盘。

进程 1 和进程 2 对各自虚拟内存区的修改只能反应到各自对应的物理内存页上,而且各自的修改在进程之间是互不可见的,最重要的一点是这些修改均不会回写到磁盘文件中。

利用 mmap 私有文件映射这个特点来加载二进制可执行文件的 .text , .data section 到进程虚拟内存空间中的代码段和数据段中。因为同一份代码,也就是同一份二进制可执行文件可以运行多个进程,而代码段对于多进程来说是只读的,没有必要为每个进程都保存一份,多进程之间共享这一份代码就可以了,正好私有文件映射的读共享特点可以满足我们的这个需求。

cpp 复制代码
static int load_elf_binary(struct linux_binprm *bprm)
{
   // 将二进制文件中的 .text .data section 私有映射到虚拟内存空间中代码段和数据段中
  error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
        elf_prot, elf_flags, total_size);
}

static int load_aout_binary(struct linux_binprm * bprm)
{
        ............ 省略 .............
        // 将 .text 采用私有文件映射的方式映射到进程虚拟内存空间的代码段
        error = vm_mmap(bprm->file, N_TXTADDR(ex), ex.a_text,
            PROT_READ | PROT_EXEC,
            MAP_FIXED | MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE,
            fd_offset);

        // 将 .data 采用私有文件映射的方式映射到进程虚拟内存空间的数据段
        error = vm_mmap(bprm->file, N_DATADDR(ex), ex.a_data,
                PROT_READ | PROT_WRITE | PROT_EXEC,
                MAP_FIXED | MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE,
                fd_offset + ex.a_text);

        ............ 省略 .............
}

3.3 共享文件映射

通过将 mmap 系统调用中的 flags 参数指定为 MAP_SHARED , 参数 fd 指定为要映射文件的文件描述符(file descriptor)来实现对文件的共享映射。

共享文件映射其实和私有文件映射前面的映射过程是一样的,唯一不同的点在于私有文件映射是读共享的,写的时候会发生写时复制(copy on write),并且多进程针对同一映射文件的修改不会回写到磁盘文件上。而共享文件映射因为是共享的,多个进程中的虚拟内存映射区最终会通过缺页中断的方式映射到文件的 page cache 中,后续多个进程对各自的这段虚拟内存区域的读写都会直接发生在 page cache 上。

3.4 共享匿名映射

通过将 mmap 系统调用中的 flags 参数指定为 MAP_SHARED | MAP_ANONYMOUS ,并将 fd 参数指定为 -1 来实现共享匿名映射,这种映射方式常用于父子进程之间共享内存,父子进程之间的通讯。

同样的在分配虚拟内存阶段不会为当前操作分配物理内存,只有到分配完虚拟内存进程访问时触发缺页异常才进行物理内存分配。因为此时是共享内存,在第二个之后的进程访问该内存时,需要找到由第一个进程触发缺页异常分配的物理内存来映射,共享匿名映射在内核中是通过一个叫做 tmpfs 的虚拟文件系统来实现的,这里借鉴了文件映射的方式来找到该物理内存。

4 源码分析

上一节讲到应用调用mmap时,内核陷入系统调用是linux-5.15.158\mm\mmap.c下的:mmap_pgoff:

cpp 复制代码
SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
		unsigned long, prot, unsigned long, flags,
		unsigned long, fd, unsigned long, pgoff)
{
	return ksys_mmap_pgoff(addr, len, prot, flags, fd, pgoff);
}

其中,ksys_mmap_pgoff主要是对大页进行预处理,之后再调用vm_mmap_pgoff开始内存映射:

cpp 复制代码
/*
ksys_mmap_pgoff 函数主要是针对 mmap 大页映射的情况进行预处理:
	在使用 mmap 进行匿名映射的时候,必须在 flags 参数中指定 MAP_ANONYMOUS 标志,否则映射流程将会终止,并返回 EBADF 错误。
	mmap 在对文件进行大页映射的时候,映射文件必须是 hugetlbfs 中的文件,flags 参数无需设置 MAP_HUGETLB, mmap 不能对普通文件进行大页映射,这种映射方式必须提前手动挂载 hugetlbfs 文件系统到指定路径下。映射长度需要与大页尺寸进行对齐。
	MAP_HUGETLB 需要和 MAP_ANONYMOUS 配合一起使用,MAP_HUGETLB 只能支持匿名映射的方式来使用 HugePage,当 mmap 设置 MAP_HUGETLB 标志进行匿名大页映射的时候,在这里需要为进程在大页池(hstate)中预留好本次映射所需要的大页个数,注意此时只是预留,还并未分配给进程,大页池中被预留好的大页不能被其他进程使用。当进程发生缺页的时候,内核会直接从大页池中把这些提前预留好的内存映射到进程的虚拟内存空间中。
*/
unsigned long ksys_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;

	 // 预处理文件映射
	if (!(flags & MAP_ANONYMOUS)) {
		// 根据 fd 获取映射文件的 struct file 结构
		audit_mmap_fd(fd, flags);

		file = fget(fd);
			return -EBADF;
		// 映射文件是否是 hugetlbfs 中的文件,hugetlbfs 中的文件默认由大页支持
		if (is_file_hugepages(file)) {
			// mmap 进行文件大页映射,len 需要和大页尺寸对齐
			len = ALIGN(len, huge_page_size(hstate_file(file)));
			// 这里可以看出如果想要使用 mmap 对文件进行大页映射,那么映射的文件必须是 hugetlbfs 中的
        // mmap 文件大页映射并不需要指定 MAP_HUGETLB,并且 mmap 不能对普通文件进行大页映射
		} else if (unlikely(flags & MAP_HUGETLB)) {
			retval = -EINVAL;
			goto out_fput;
		}
	} else if (flags & MAP_HUGETLB) {
		// 从这里我们可以看出 MAP_HUGETLB 只能支持 MAP_ANONYMOUS 匿名映射的方式使用 HugePage
		struct ucounts *ucounts = NULL;
		struct hstate *hs;// 内核中的大页池(预先创建)
		// 选取指定大页尺寸的大页池(内核中存在不同尺寸的大页池)
		hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
		if (!hs)
			return -EINVAL;
    // 映射长度 len 必须与大页尺寸对齐
		len = ALIGN(len, huge_page_size(hs));
		/*
		 * VM_NORESERVE is used because the reservations will be
		 * taken when vm_ops->mmap() is called
		 * A dummy user value is used because we are not locking
		 * memory so no accounting is necessary
		 */
		 // 在 hugetlbfs 中创建 anon_hugepage 文件,并预留大页内存(禁止其他进程申请)
		file = hugetlb_file_setup(HUGETLB_ANON_FILE, len,
				VM_NORESERVE,
				&ucounts, HUGETLB_ANONHUGE_INODE,
				(flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
		if (IS_ERR(file))
			return PTR_ERR(file);
	}
	// 开始内存映射
	retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
out_fput:
	if (file)
		fput(file); // file 引用计数减 1
	return retval;
}

4.1 是否立即分配物理内存

当前flags设置了MAP_POPULATE 或者 MAP_LOCKED 标志位时,物理内存的分配会在分配为虚拟内存后,直接进行物理内存的分配与映射,不会等到缺页异常再进行:

cpp 复制代码
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;// 获取进程虚拟内存空间
	// 是否需要为映射的 VMA,提前分配物理内存页,避免后续的缺页
    // 取决于 flag 是否设置了 MAP_POPULATE 或者 MAP_LOCKED,这里的 populate 表示需要分配物理内存的大小
	unsigned long populate;
	LIST_HEAD(uf);

	ret = security_mmap_file(file, prot, flag);
	if (!ret) {
		// 对进程虚拟内存空间加写锁保护,防止多线程并发修改
		if (mmap_write_lock_killable(mm))
			return -EINTR;
		 // 开始 mmap 内存映射,在进程虚拟内存空间中分配一段 vma,并建立相关映射关系
        // ret 为映射虚拟内存区域的起始地址
		ret = do_mmap(file, addr, len, prot, flag, pgoff, &populate,
			      &uf);
		mmap_write_unlock(mm);// 释放写锁
		userfaultfd_unmap_complete(mm, &uf);
		if (populate)
		 // 提前分配物理内存页面,后续访问不会缺页
            // 为 [ret , ret + populate] 这段虚拟内存立即分配物理内存
			mm_populate(ret, populate);
	}
	return ret;
}

首先会通过 do_mmap_pgoff 函数在进程虚拟内存空间中分配出一段未映射的虚拟内存区域,返回值 ret 表示映射的这段虚拟内存区域的起始地址。紧接着就会调用 mm_populate,内核会在 mmap刚刚映射出来的这段虚拟内存区域上,依次扫描这段vma中的每一个虚拟页,并对每一个虚拟页触发缺页异常,从而为其立即分配物理内存。

mm_populate函数的作用主要是在进程虚拟内存空间中,找出 [ret,ret+populate] 这段虚拟地址范围内的所有vma,并为每一个vma填充物理内存。

cpp 复制代码
int __mm_populate(unsigned long start, unsigned long len, int ignore_errors)
{
	struct mm_struct *mm = current->mm;
	unsigned long end, nstart, nend;
	struct vm_area_struct *vma = NULL;
	int locked = 0;
	long ret = 0;

	end = start + len;
	// 依次遍历进程地址空间中 [start , end] 这段虚拟内存范围的所有 vma
	for (nstart = start; nstart < end; nstart = nend) {
		/*
		 * We want to fault in pages for [nstart; end) address range.
		 * Find first corresponding VMA.
		 */
		if (!locked) {
			locked = 1;
			mmap_read_lock(mm);
			vma = find_vma(mm, nstart);
		} else if (nstart >= vma->vm_end)
			vma = vma->vm_next;
		if (!vma || vma->vm_start >= end)
			break;
		/*
		 * Set [nstart; nend) to intersection of desired address
		 * range with the first VMA. Also, skip undesirable VMA types.
		 */
		nend = min(end, vma->vm_end);
		if (vma->vm_flags & (VM_IO | VM_PFNMAP))
			continue;
		if (nstart < vma->vm_start)
			nstart = vma->vm_start;
		/*
		 * Now fault in a range of pages. populate_vma_page_range()
		 * double checks the vma flags, so that it won't mlock pages
		 * if the vma was already munlocked.
		 */
		 // 为这段地址范围内的所有 vma 分配物理内存
		ret = populate_vma_page_range(vma, nstart, nend, &locked);
		if (ret < 0) {
			if (ignore_errors) {
				ret = 0;
				continue;	/* continue at next VMA */
			}
			break;
		}
		nend = nstart + ret * PAGE_SIZE;// 继续为下一个 vma (如果有的话)分配物理内存
		ret = 0;
	}
	if (locked)
		mmap_read_unlock(mm);
	return ret;	/* 0 or negative error code */
}

populate_vma_page_range 函数则是在 __mm_populate 的处理基础上,为指定地址范围 [start , end] 内的每一个虚拟内存页,通过 __get_user_pages 函数为其分配物理内存。

cpp 复制代码
long populate_vma_page_range(struct vm_area_struct *vma,
		unsigned long start, unsigned long end, int *locked)
{
	struct mm_struct *mm = vma->vm_mm;
	unsigned long nr_pages = (end - start) / PAGE_SIZE;// 计算 vma 中包含的虚拟内存页个数,后续会按照 nr_pages 分配物理内存
	int gup_flags;

	VM_BUG_ON(!PAGE_ALIGNED(start));
	VM_BUG_ON(!PAGE_ALIGNED(end));
	VM_BUG_ON_VMA(start < vma->vm_start, vma);
	VM_BUG_ON_VMA(end   > vma->vm_end, vma);
	mmap_assert_locked(mm);

	gup_flags = FOLL_TOUCH | FOLL_POPULATE | FOLL_MLOCK;
	if (vma->vm_flags & VM_LOCKONFAULT)
		gup_flags &= ~FOLL_POPULATE;
	/*
	 * We want to touch writable mappings with a write fault in order
	 * to break COW, except for shared mappings because these don't COW
	 * and we would not want to dirty them for nothing.
	 */
	if ((vma->vm_flags & (VM_WRITE | VM_SHARED)) == VM_WRITE)
		gup_flags |= FOLL_WRITE;

	/*
	 * We want mlock to succeed for regions that have any permissions
	 * other than PROT_NONE.
	 */
	if (vma_is_accessible(vma))
		gup_flags |= FOLL_FORCE;

	/*
	 * We made sure addr is within a VMA, so the following will
	 * not result in a stack expansion that recurses back here.
	 */
	 // 循环遍历 vma 中的每一个虚拟内存页,依次为其分配物理内存页
	return __get_user_pages(mm, start, nr_pages, gup_flags,
				NULL, NULL, locked);
}

__get_user_pages 会循环遍历 vma 中的每一个虚拟内存页,首先会通过 follow_page_mask 在进程页表中查找该虚拟内存页背后是否有物理内存页与之映射,如果没有则调用 faultin_page,其底层会调用到 handle_mm_fault 进入缺页处理流程,内核在这里会为其分配物理内存页,并在进程页表中建立好映射关系。

cpp 复制代码
static long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
        unsigned long start, unsigned long nr_pages,
        unsigned int gup_flags, struct page **pages,
        struct vm_area_struct **vmas, int *nonblocking)
{
    long ret = 0, i = 0;
    struct vm_area_struct *vma = NULL;
    struct follow_page_context ctx = { NULL };

    if (!nr_pages)
        return 0;

    start = untagged_addr(start);
    // 循环遍历 vma 中的每一个虚拟内存页
    do {
        struct page *page;
        unsigned int foll_flags = gup_flags;
        unsigned int page_increm;
        // 在进程页表中检查该虚拟内存页背后是否有物理内存页映射
        page = follow_page_mask(vma, start, foll_flags, &ctx);
        if (!page) {
            // 如果虚拟内存页在页表中并没有物理内存页映射,那么这里调用 faultin_page
            // 底层会调用到 handle_mm_fault 进入缺页处理流程,分配物理内存,在页表中建立好映射关系
            ret = faultin_page(tsk, vma, start, &foll_flags,
                    nonblocking);

    } while (nr_pages);

    return i ? i : ret;
}

4.2 虚拟内存映射整体流程

do_mmap 是 mmap 系统调用的核心函数,内核会在这里完成内存映射的整个流程,其中最为核心的是如下两个方面的内容:

get_unmapped_area 函数用于在进程地址空间中寻找出一段长度为 len,并且还未映射的虚拟内存区域 vma 出来。返回值 addr 表示这段虚拟内存区域的起始地址。

mmap_region 函数是整个内存映射的核心,它首先会为这段选取出来的映射虚拟内存区域分配 vma 结构,并根据映射信息进行初始化,以及建立 vma 与相关映射文件的关系,最后将这段 vma 插入到进程的虚拟内存空间中。

内核会对我们申请的虚拟内存容量进行审计(account),结合当前物理内存容量以及 swap 交换区的大小来综合判断是否允许本次虚拟内存的申请。内核定义了如下三个 overcommit 策略:

  • OVERCOMMIT_GUESS 是内核的默认 overcommit 策略。在这种模式下,特别激进的,过量的虚拟内存申请将会被拒绝,内核会对虚拟内存能够过量申请多少做出一定的限制,这种策略既不激进也不保守,比较中庸。

  • OVERCOMMIT_ALWAYS 是最为激进的 overcommit 策略,无论进程申请多大的虚拟内存,只要不超过整个进程虚拟内存空间的大小,内核总会痛快的答应。但是这种策略下,虚拟内存的申请虽然容易了,但是当进程遇到缺页,内核为其分配物理内存的时候,会非常容易造成 OOM 。

  • OVERCOMMIT_NEVER 是最为严格的一种控制虚拟内存 overcommit 的策略,在这种模式下,内核会严格的规定虚拟内存的申请用量。

cpp 复制代码
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)
{
	struct mm_struct *mm = current->mm;
	vm_flags_t vm_flags;
	int pkey = 0;

	*populate = 0;

	if (!len)
		return -EINVAL;

	/*
	 * Does the application expect PROT_READ to imply PROT_EXEC?
	 *
	 * (the exception is when the underlying filesystem is noexec
	 *  mounted, in which case we dont add PROT_EXEC.)
	 */
	if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
		if (!(file && path_noexec(&file->f_path)))
			prot |= PROT_EXEC;

	/* force arch specific MAP_FIXED handling in get_unmapped_area */
	if (flags & MAP_FIXED_NOREPLACE)
		flags |= MAP_FIXED;

	if (!(flags & MAP_FIXED))
		addr = round_hint_to_min(addr);

	/* Careful about overflows.. */
	len = PAGE_ALIGN(len);
	if (!len)
		return -ENOMEM;

	/* offset overflow? */
	if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
		return -EOVERFLOW;
	// 一个进程虚拟内存空间内所能包含的虚拟内存区域 vma 是有数量限制的
    // sysctl_max_map_count 规定了进程虚拟内存空间所能包含 VMA 的最大个数
    // 可以通过 /proc/sys/vm/max_map_count 内核参数调整 sysctl_max_map_count
    // mmap 需要再进程虚拟内存空间中创建映射的 VMA,这里需要检查 VMA 的个数是否超过最大限制
	/* Too many mappings? */
	if (mm->map_count > sysctl_max_map_count)
		return -ENOMEM;

	/* Obtain the address to map to. we verify (or select) it and ensure
	 * that it represents a valid section of the address space.
	 */
	 // 在进程虚拟内存空间中寻找一块未映射的虚拟内存范围
    // 这段虚拟内存范围后续将会用于 mmap 内存映射
	addr = get_unmapped_area(file, addr, len, pgoff, flags);
	if (IS_ERR_VALUE(addr))
		return addr;

	if (flags & MAP_FIXED_NOREPLACE) {
		if (find_vma_intersection(mm, addr, addr + len))
			return -EEXIST;
	}

	if (prot == PROT_EXEC) {
		pkey = execute_only_pkey(mm);
		if (pkey < 0)
			pkey = 0;
	}

	/* Do simple checking here so the lower-level routines won't have
	 * to. we assume access permissions have been handled by the open
	 * of the memory object, so we don't do any here.
	 */
	 // 通过 calc_vm_prot_bits 和 calc_vm_flag_bits 将 mmap 参数 prot , flag 中   
    // 设置的访问权限以及映射方式等枚举值转换为统一的 vm_flags,后续一起映射进 VMA 的相应属性中,相应前缀转换为 VM_  
	vm_flags = calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |
			mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
	// 设置了 MAP_LOCKED,表示用户期望 mmap 背后映射的物理内存锁定在内存中,不允许 swap
	if (flags & MAP_LOCKED)
		if (!can_do_mlock())// 这里需要检查是否可以将本次映射的物理内存锁定
			return -EPERM;
	// 进一步检查锁定的内存页数是否超过了内核限制
	if (mlock_future_check(mm, vm_flags, len))
		return -EAGAIN;

	if (file) {
		struct inode *inode = file_inode(file);
		unsigned long flags_mask;

		if (!file_mmap_ok(file, inode, pgoff, len))
			return -EOVERFLOW;

		flags_mask = LEGACY_MAP_MASK | file->f_op->mmap_supported_flags;

		switch (flags & MAP_TYPE) {
		case MAP_SHARED:
			/*
			 * Force use of MAP_SHARED_VALIDATE with non-legacy
			 * flags. E.g. MAP_SYNC is dangerous to use with
			 * MAP_SHARED as you don't know which consistency model
			 * you will get. We silently ignore unsupported flags
			 * with MAP_SHARED to preserve backward compatibility.
			 */
			flags &= LEGACY_MAP_MASK;
			fallthrough;
		case MAP_SHARED_VALIDATE:
			if (flags & ~flags_mask)
				return -EOPNOTSUPP;
			if (prot & PROT_WRITE) {
				if (!(file->f_mode & FMODE_WRITE))
					return -EACCES;
				if (IS_SWAPFILE(file->f_mapping->host))
					return -ETXTBSY;
			}

			/*
			 * Make sure we don't allow writing to an append-only
			 * file..
			 */
			if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
				return -EACCES;

			vm_flags |= VM_SHARED | VM_MAYSHARE;
			if (!(file->f_mode & FMODE_WRITE))
				vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
			fallthrough;
		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;
			/*
			 * Ignore pgoff.
			 */
			pgoff = 0;
			vm_flags |= VM_SHARED | VM_MAYSHARE;
			break;
		case MAP_PRIVATE:
			/*
			 * Set pgoff according to addr for anon_vma.
			 */
			pgoff = addr >> PAGE_SHIFT;
			break;
		default:
			return -EINVAL;
		}
	}

	/*
	 * Set 'VM_NORESERVE' if we should not account for the
	 * memory use of this mapping.
	 */
	     // 通常内核会为 mmap 申请虚拟内存的时候会综合考虑 ram 以及 swap space 的总体大小。
    // 当映射的虚拟内存过大,而没有足够的 swap space 的时候, mmap 就会失败。
    // 设置 MAP_NORESERVE,内核将不会考虑上面的限制因素
    // 这样当通过 mmap 申请大量的虚拟内存,并且当前系统没有足够的 swap space 的时候,mmap 系统调用依然能够成功
	if (flags & MAP_NORESERVE) {
		/* We honor MAP_NORESERVE if allowed to overcommit */
		// 设置 MAP_NORESERVE 的目的是为了应用可以申请过量的虚拟内存
        // 如果内核本身是禁止 overcommit 的,那么设置 MAP_NORESERVE 是无意义的
        // 如果内核允许过量申请虚拟内存时(overcommit 为 0 或者 1)
        // 无论映射多大的虚拟内存,mmap 将会始终成功,但缺页的时候会容易导致 oom
		if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
			vm_flags |= VM_NORESERVE; // 设置 VM_NORESERVE 表示无论申请多大的虚拟内存,内核总会答应
		// 大页内存是提前预留出来的,并且本身就不会被 swap
        // 所以不需要像普通内存页那样考虑 swap space 的限制因素
		/* hugetlb applies strict overcommit unless MAP_NORESERVE */
		if (file && is_file_hugepages(file))
			vm_flags |= VM_NORESERVE;
	}
	// 这里就是 mmap 内存映射的核心
	addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);

	  // 当 mmap 设置了 MAP_POPULATE 或者 MAP_LOCKED 标志
    // 那么在映射完之后,需要立马为这块虚拟内存分配物理内存页,后续访问就不会发生缺页了
	if (!IS_ERR_VALUE(addr) &&
	    ((vm_flags & VM_LOCKED) ||
	     (flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
		*populate = len;  // 设置需要分配的物理内存大小
	return addr;
}

当我们期望对 mmap 背后映射的物理内存进行锁定的时候,内核首先需要调用 can_do_mlock 函数,对能够锁定的物理内存资源配额进行判断,如果配额不足则不能对本次映射的物理内存进行锁定,mmap 返回 EPERM 错误,流程结束。

cpp 复制代码
bool can_do_mlock(void)
{
    // 内核会限制能够被锁定的内存资源大小,单位为bytes
    // 这里获取 RLIMIT_MEMLOCK 能够锁定的内存资源,如果为 0 ,则不能够锁定内存了。
    // 我们可以通过修改 /etc/security/limits.conf 文件中的 memlock 相关配置项
    // 来调整能够被锁定的内存资源配额,设置为 unlimited 表示不对锁定内存进行限制
    if (rlimit(RLIMIT_MEMLOCK) != 0)
        return true;
    // 检查内核是否允许 mlock ,mlockall 等内存锁定操作
    if (capable(CAP_IPC_LOCK))
        return true;
    return false;
}

当通过 can_do_mlock 的检验之后,内核还需要近一步通过 mlock_future_check 函数来检查本次映射需要锁定的物理内存页数加上进程已经锁定的物理内存页数总体上是否超过了内存资源锁定限额 rlimit(RLIMIT_MEMLOCK)。如果已经超过限额,本次 mmap 流程就会停止。

cpp 复制代码
static inline int mlock_future_check(struct mm_struct *mm,
                     unsigned long flags,
                     unsigned long len)
{
    unsigned long locked, lock_limit;

    if (flags & VM_LOCKED) {
        // 需要锁定的内存页数
        locked = len >> PAGE_SHIFT;
        // 更新进程内存空间中已经锁定的内存页数
        locked += mm->locked_vm;
        // 获取内核还能允许锁定的内存页数
        lock_limit = rlimit(RLIMIT_MEMLOCK);        
        lock_limit >>= PAGE_SHIFT;
        // 如果超出允许锁定的内存限额,那么就返回错误
        if (locked > lock_limit && !capable(CAP_IPC_LOCK))
            return -EAGAIN;
    }
    return 0;
}

4.3 虚拟内存分配流程

mmap 系统调用分配虚拟内存的本质其实就是在进程的虚拟内存空间中的文件映射与匿名映射区,找出一段未被映射过的空闲虚拟内存区域 vma,这个 vma 就是我们申请到的虚拟内存。

4.3.1 关于文件映射与匿名映射区的布局

文件映射与匿名映射区的布局在Linux下有两种布局:经典与新式布局

经典布局文件映射与匿名映射区的布局的起始地址mm_struct->mmap_base是task_size/3(32bit系统下,3GB/3即1GB处)即处从低地址到高地址向上增长:

新式布局则从task_size开始高地址往低地址分配:

这些布局的初始化,在进程加载流程调用的load_elf_binary完成。

4.3.2 虚拟内存的分配

get_unmapped_area 主要的目的就是在具体的映射区布局下,根据布局特点,真正负责划分虚拟内存区域的函数:

cpp 复制代码
unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
		unsigned long pgoff, unsigned long flags)
{
	unsigned long (*get_area)(struct file *, unsigned long,
				  unsigned long, unsigned long, unsigned long);
	// 在进程虚拟空间中寻找还未被映射的 VMA 这段核心逻辑是被内核实现在特定于体系结构的函数中
    // 该函数指针用于指向真正的 get_unmapped_area 函数
    // 在经典布局下,真正的实现函数为 arch_get_unmapped_area
	unsigned long error = arch_mmap_check(addr, len, flags);
	if (error)
		return error;

	/* Careful about overflows.. */
	// 映射的虚拟内存区域长度不能超过进程的地址空间
	if (len > TASK_SIZE)
		return -ENOMEM;
	// 如果是匿名映射,则采用 mm_struct 中保存的特定于体系结构的 arch_get_unmapped_area 函数
	get_area = current->mm->get_unmapped_area;
	if (file) {
		// 如果是文件映射话,则需要使用 file->f_op 中的 get_unmapped_area,来为文件映射申请虚拟内存
        // file->f_op 保存的是特定于文件系统中文件的相关操作
		if (file->f_op->get_unmapped_area)
			get_area = file->f_op->get_unmapped_area;
	} else if (flags & MAP_SHARED) {
		/*
		 * mmap_region() will call shmem_zero_setup() to create a file,
		 * so use shmem's get_unmapped_area in case it can be huge.
		 * do_mmap() will clear pgoff, so match alignment.
		 */
		 // 共享匿名映射是通过在 tmpfs 中创建的匿名文件实现的
        // 所以这里也有其专有的 get_unmapped_area 函数
		pgoff = 0;
		get_area = shmem_get_unmapped_area;
	}
	// 在进程虚拟内存空间中,根据指定的 addr,len 查找合适的VMA
	addr = get_area(file, addr, len, pgoff, flags);
	if (IS_ERR_VALUE(addr))
		return addr;
	// VMA 区域不能超过进程地址空间
	if (addr > TASK_SIZE - len)
		return -ENOMEM;
	// addr 需要与 page size 对齐
	if (offset_in_page(addr))
		return -EINVAL;

	error = security_mmap_addr(addr);
	return error ? error : addr;
}

最终都会调用到mm->get_unmapped_area指针函数中:

cpp 复制代码
const struct file_operations ext4_file_operations = {
        .mmap           = ext4_file_mmap
        .get_unmapped_area = thp_get_unmapped_area,
};


unsigned long __thp_get_unmapped_area(struct file *filp, unsigned long len,
                loff_t off, unsigned long flags, unsigned long size)
{
        ........... 省略 ........

        addr = current->mm->get_unmapped_area(filp, 0, len_pad,
                                              off >> PAGE_SHIFT, flags);
        return addr;
}

unsigned long shmem_get_unmapped_area(struct file *file,
                      unsigned long uaddr, unsigned long len,
                      unsigned long pgoff, unsigned long flags)
{
    unsigned long (*get_area)(struct file *,
        unsigned long, unsigned long, unsigned long, uns

         ........... 省略 ........

    get_area = current->mm->get_unmapped_area;
    
    return addr;
}

在经典布局下,mm->get_unmapped_area 指向的是linux-5.15.158\mm\mmap.c下的 arch_get_unmapped_area 函数:

cpp 复制代码
// 内核标准实现 
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, *prev;
    struct vm_unmapped_area_info info;
    // 进程虚拟内存空间的末尾 TASK_SIZE
    const unsigned long mmap_end = arch_get_mmap_end(addr);
    // 映射区域长度是否超过进程虚拟内存空间
    if (len > mmap_end - mmap_min_addr)
        return -ENOMEM;
    // 如果我们指定了 MAP_FIXED 表示必须要从我们指定的 addr 开始映射 len 长度的区域
    // 如果这块区域已经存在映射关系,那么后续内核会把旧的映射关系覆盖掉
    if (flags & MAP_FIXED)
        return addr;

    // 没有指定 MAP_FIXED,但是我们指定了 addr
    // 我们希望内核从我们指定的 addr 地址开始映射,内核这里会检查我们指定的这块虚拟内存范围是否有效
    if (addr) {
        // addr 先保证与 page size 对齐
        addr = PAGE_ALIGN(addr);
        // 内核这里需要确认一下我们指定的 [addr, addr+len] 这段虚拟内存区域是否存在已有的映射关系
        // [addr, addr+len] 地址范围内已经存在映射关系,则不能按照我们指定的 addr 作为映射起始地址
        // 在进程地址空间中查找第一个符合 addr < vma->vm_end  条件的 VMA
        // 如果不存在这样一个 vma(!vma), 则表示 [addr, addr+len] 这段范围的虚拟内存是可以使用的,内核将会从我们指定的 addr 开始映射
        // 如果存在这样一个 vma ,则表示  [addr, addr+len] 这段范围的虚拟内存区域目前已经存在映射关系了,不能采用 addr 作为映射起始地址
        // 这里还有一种情况是 addr 落在 prev 和 vma 之间的一块未映射区域
        // 如果这块未映射区域的长度满足 len 大小,那么这段未映射区域可以被本次使用,内核也会从我们指定的 addr 开始映射
        vma = find_vma_prev(mm, addr, &prev);
        if (mmap_end - len >= addr && addr >= mmap_min_addr &&
            (!vma || addr + len <= vm_start_gap(vma)) &&
            (!prev || addr >= vm_end_gap(prev)))
            return addr;
    }

    // 如果我们明确指定 addr 但是指定的虚拟内存范围是一段无效的区域或者已经存在映射关系
    // 那么内核会自动在地址空间中寻找一段合适的虚拟内存范围出来
    // 这段虚拟内存范围的起始地址就不是我们指定的 addr 了
    info.flags = 0;
    // VMA 区域长度
    info.length = len;
    // 这里定义从哪里开始查找 VMA, 这里我们会从文件映射与匿名映射区开始查找
    info.low_limit = mm->mmap_base;
    // 查找结束位置为进程地址空间的末尾 TASK_SIZE
    info.high_limit = mmap_end;
    info.align_mask = 0;
    return vm_unmapped_area(&info);
}

4.3.3 寻找合适内存区域

arch_get_unmapped_area调用了find_vma_prev, 作用就是根据指定的映射起始地址 addr,在进程地址空间中查找出符合 addr<vma->vm_end 条件的第一个 vma 出来,然后在进程地址空间中的 vma 链表 mmap 中,找出它的前驱节点 pprev :

cpp 复制代码
struct mm_struct {
    struct vm_area_struct *mmap;  /* list of VMAs */
}
cpp 复制代码
struct vm_area_struct *
find_vma_prev(struct mm_struct *mm, unsigned long addr,
            struct vm_area_struct **pprev)
{
    struct vm_area_struct *vma;
    // 在进程地址空间 mm 中查找第一个符合 addr < vma->vm_end 的 VMA
    vma = find_vma(mm, addr);

    if (vma) {
        // 恰好包含 addr 的 VMA 的前一个虚拟内存区域 
        *pprev = vma->vm_prev;
    } else {
        // 如果当前进程地址空间中,addr 不属于任何一个 VMA 
        // 那么这里的 pprev 指向进程地址空间中最后一个 VMA
        struct rb_node *rb_node = rb_last(&mm->mm_rb);

        *pprev = rb_node ? rb_entry(rb_node, struct vm_area_struct, vm_rb) : NULL;
    }
    // 返回查找到的 vma,不存在则返回 null(内核后续会创建 VMA)
    return vma;
}

如果不存在这样一个 vma(addr < vma->vm_end),那么内核直接从我们指定的 addr 地址处开始映射就好了,这时 pprev 指向进程地址空间中最后一个 vma。如果存在这样一个 vma,那么内核就会判断,该 vma 与其前驱节点 pprev 之间的地址间隙 gap 是否能容纳下一段 len 长度的映射区间,如果可以,那么内核就映射在这个地址间隙 gap 中。如果不可以,内核就需要在 vm_unmapped_area 函数中重新到整个进程地址空间中查找出一个 len 长度的空闲映射区域,这种情况下映射区的起始地址就不是我们指定的 addr 了, find_vma 会根据我们指定的 addr 在这颗红黑树中查找第一个符合 addr<vma->vm_end条件的 vma 。

arch_get_unmapped_area还调用了vm_unmapped_area ,用于寻找未映射的虚拟内存区域。

cpp 复制代码
static inline unsigned long
vm_unmapped_area(struct vm_unmapped_area_info *info)
{
    // 按照进程虚拟内存空间中文件映射与匿名映射区的地址增长方向
    // 分为两个函数,来在进程地址空间中查找未映射的 VMA
    if (info->flags & VM_UNMAPPED_AREA_TOPDOWN)
        // 当文件映射与匿名映射区的地址增长方向是从上到下逆向增长时(新式布局)
        // 采用 topdown 后缀的函数查找
        return unmapped_area_topdown(info);
    else
        // 地址增长方向为从下倒上正向增长(经典布局),采用该函数查找
        return unmapped_area(info);
}

其中unmaped_area的核心实现:

cpp 复制代码
unsigned long unmapped_area(struct vm_unmapped_area_info *info)
{
    /*
     * We implement the search by looking for an rbtree node that
     * immediately follows a suitable gap. That is,
     * - gap_start = vma->vm_prev->vm_end <= info->high_limit - length;
     * - gap_end   = vma->vm_start        >= info->low_limit  + length;
     * - gap_end - gap_start >= length
     */

    struct mm_struct *mm = current->mm;
    // 寻找未映射区域的参考 vma (该区域以存在映射关系)
    struct vm_area_struct *vma;
    // 未映射区域产生在 vma->vm_prev 与 vma 这两个虚拟内存区域中的间隙 gap 中
    // length 表示本次映射区域的长度
    // low_limit ,high_limit 表示在进程地址空间中哪段地址范围内查找,一个地址下限(mm->mmap_base),另一个标识地址上限(TASK_SIZE)
    // gap_start, gap_end 表示 vma->vm_prev 与 vma 之间的 gap 范围,unmapped_area 将会在这里产生
    unsigned long length, low_limit, high_limit, gap_start, gap_end;

    // gap_start 需要满足的条件:gap_start =  vma->vm_prev->vm_end <= info->high_limit - length
    // 否则 unmapped_area 将会超出 high_limit 的限制
    high_limit = info->high_limit - length;

    // gap_end 需要满足的条件:gap_end = vma->vm_start >= info->low_limit + length
    // 否则 unmapped_area 将会超出 low_limit 的限制
    low_limit = info->low_limit + length;

    // 首先将 vma 红黑树的根节点作为 gap 的参考 vma
    if (RB_EMPTY_ROOT(&mm->mm_rb))
        // 'empty' nodes are nodes that are known not to be inserted in an rbtree
        goto check_highest;
    // 获取红黑树根节点的 vma
    vma = rb_entry(mm->mm_rb.rb_node, struct vm_area_struct, vm_rb);

    // rb_subtree_gap 为当前 vma 及其左右子树中所有 vma 与其对应 vm_prev 之间最大的虚拟内存地址 gap
    // 最大的 gap 如果都不能满足映射长度 length 则跳转到 check_highest 处理
    if (vma->rb_subtree_gap < length)
        // 从进程地址空间最后一个 vma->vm_end 地址处开始映射
        goto check_highest;

    while (true) {
        // 获取当前 vma 的 vm_start 起始虚拟内存地址作为 gap_end
        gap_end = vm_start_gap(vma);
        // gap_end 需要满足:gap_end >= low_limit,否则 unmapped_area 将会超出 low_limit 的限制
        // 如果存在左子树,则需要继续到左子树中去查找,因为我们需要按照地址从低到高的优先级来查看合适的未映射区域
        if (gap_end >= low_limit && vma->vm_rb.rb_left) {
            struct vm_area_struct *left =
                rb_entry(vma->vm_rb.rb_left,
                     struct vm_area_struct, vm_rb);
            // 如果左子树中存在合适的 gap,则继续左子树的查找
            // 否则查找结束,gap 为当前 vma 与其 vm_prev 之间的间隙    
            if (left->rb_subtree_gap >= length) {
                vma = left;
                continue;
            }
        }
        // 获取当前 vma->vm_prev 的 vm_end 作为 gap_start
        gap_start = vma->vm_prev ? vm_end_gap(vma->vm_prev) : 0;
check_current:
        // gap_start 需要满足:gap_start <= high_limit,否则 unmapped_area 将会超出 high_limit 的限制
        if (gap_start > high_limit)
            return -ENOMEM;

        if (gap_end >= low_limit &&
            gap_end > gap_start && gap_end - gap_start >= length)
            // 找到了合适的 unmapped_area 跳转到 found 处理
            goto found;

       // 当前 vma 与其左子树中的所有 vma 均不存在一个合理的 gap
       // 那么从 vma 的右子树中继续查找
        if (vma->vm_rb.rb_right) {
            struct vm_area_struct *right =
                rb_entry(vma->vm_rb.rb_right,
                     struct vm_area_struct, vm_rb);
            if (right->rb_subtree_gap >= length) {
                vma = right;
                continue;
            }
        }

        // 如果在当前 vma 以及它的左右子树中均无法找到一个合适的 gap
        // 那么这里会从当前 vma 节点向上回溯整颗红黑树,在它的父节点中尝试查找是否有合适的 gap
        // 因为这时候有可能会有新的 vma 插入到红黑树中,可能会产生新的 gap
        while (true) {
            struct rb_node *prev = &vma->vm_rb;
            if (!rb_parent(prev))
                goto check_highest;
            vma = rb_entry(rb_parent(prev),
                       struct vm_area_struct, vm_rb);
            if (prev == vma->vm_rb.rb_left) {
                gap_start = vm_end_gap(vma->vm_prev);
                gap_end = vm_start_gap(vma);
                goto check_current;
            }
        }
    }

check_highest:
    // 流程走到这里表示在当前进程虚拟内存空间的所有 VMA 中都无法找到一个合适的 gap 来作为 unmapped_area
    // 那么就从进程地址空间中最后一个 vma->vm_end 开始映射
    // mm->highest_vm_end 表示当前进程虚拟内存空间中,地址最高的一个 VMA 的结束地址位置
    gap_start = mm->highest_vm_end;
    gap_end = ULONG_MAX;  /* Only for VM_BUG_ON below */
    // 这里最后需要检查剩余虚拟内存空间是否满足映射长度
    if (gap_start > high_limit)
        // ENOMEM 表示当前进程虚拟内存空间中虚拟内存不足
        return -ENOMEM;

found:
    // 流程走到这里表示我们已经找到了一个合适的 gap 来作为 unmapped_area 
    // 直接返回 gap_start (需要与 4K 对齐)作为映射的起始地址
    /* We found a suitable gap. Clip it with the original low_limit. */
    if (gap_start < info->low_limit)
        gap_start = info->low_limit;

    /* Adjust gap address to the desired alignment */
    gap_start += (info->align_offset - gap_start) & info->align_mask;

    VM_BUG_ON(gap_start + info->length > info->high_limit);
    VM_BUG_ON(gap_start + info->length > gap_end);
    return gap_start;
}

4.4 映射核心流程mmap_region

get_unmapped_area 函数为在进程地址空间中挑选出一段地址范围为 [addr , addr + len] 的虚拟内存区域供 mmap 进行映射,但现在只是确定了[addr , addr + len]这段虚拟内存区域是可以映射的,这段区域只是被内核先划分出来了,但是还未分配出去,在mmap_region函数中,需要为这段虚拟内存区域分配vma结构,并根据映射方式对vma进行初始化,这样这段虚拟内存才算真正的被分配给了进程。

cpp 复制代码
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;
    int error;
    struct rb_node **rb_link, *rb_parent;
    unsigned long charged = 0;

    // 检查本次映射是否超过了进程虚拟内存空间中的虚拟内存容量的限制,超过则返回 false
    if (!may_expand_vm(mm, vm_flags, len >> PAGE_SHIFT)) {
        unsigned long nr_pages;

        // 如果 mmap 指定了 MAP_FIXED,表示内核必须要按照用户指定的映射区来进行映射
        // 这种情况下就会导致,我们指定的映射区[addr, addr + len] 有一部分可能与现有映射重叠
        // 内核将会覆盖掉这段已有的映射,重新按照用户指定的映射关系进行映射
        // 所以这里需要计算进程地址空间中与指定映射区[addr, addr + len]重叠的虚拟内存页数 nr_pages
        nr_pages = count_vma_pages_range(mm, addr, addr + len);
        // 由于这里的 nr_pages 表示重叠的虚拟内存部分,将会被覆盖,所以这部分被覆盖的虚拟内存不需要额外申请
        // 这里通过 len >> PAGE_SHIFT 减去这段可以被覆盖的 nr_pages 在重新检查是否超过虚拟内存相关区域的限额
        if (!may_expand_vm(mm, vm_flags,
                    (len >> PAGE_SHIFT) - nr_pages))
            return -ENOMEM;
    }

   // 如果当前进程地址空间中存在于指定映射区域 [addr, addr + len] 重叠的部分
   // 则调用  do_munmap 将这段重叠的映射部分解除掉,后续会重新映射这部分
    while (find_vma_links(mm, addr, addr + len, &prev, &rb_link,
                  &rb_parent)) {
        if (do_munmap(mm, addr, len, uf))
            return -ENOMEM;
    }
   
    /*
     * 判断将来是否会为这段虚拟内存 vma ,申请新的物理内存,比如 私有,可写(private writable)的映射方式,内核将来会通过 cow 重新为其分配新的物理内存。
     * 私有,只读(private readonly)的映射方式,内核则会共享原来映射的物理内存,而不会申请新的物理内存。
     * 如果将来需要申请新的物理内存则会根据当前系统的 overcommit 策略以及当前物理内存的使用情况来  
     * 综合判断是否允许本次虚拟内存的申请。如果虚拟内存不足,则返回 ENOMEM,这样的话可以防止缺页的时候发生 OOM
     */
    if (accountable_mapping(file, vm_flags)) {
        charged = len >> PAGE_SHIFT;
        // 根据内核 overcommit 策略以及当前物理内存的使用情况综合判断,是否能够通过本次虚拟内存的申请
        // 虚拟内存的申请一旦这里通过之后,后续发生缺页,内核将会有足够的物理内存为其分配,不会发生 OOM
        if (security_vm_enough_memory_mm(mm, charged))
            return -ENOMEM;
        // 凡是设置了 VM_ACCOUNT 的 VMA,表示这段虚拟内存均已经过 vm_enough_memory 的检测
        // 当虚拟内存发生缺页的时候,内核会有足够的物理内存分配,而不会导致 OOM 
        // 其虚拟内存的用量都会被统计在 /proc/meminfo 的 Committed_AS  字段中    
        vm_flags |= VM_ACCOUNT;
    }

    // 为了精细化的控制内存的开销,内核这里首先需要尝试看能不能和地址空间中已有的 vma 进行合并
    // 尝试将当前 vma 合并到已有的 vma 中
    vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
            NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
    if (vma)
        // 如果可以合并,则虚拟内存分配过程结束
        goto out;

    // 如果不可以合并,则只能从 slab 中取出一个新的 vma 结构来
    vma = vm_area_alloc(mm);
    if (!vma) {
        error = -ENOMEM;
        goto unacct_error;
    }
    // 根据我们要映射的虚拟内存区域属性初始化 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) {
        // 将文件与虚拟内存映射起来
        vma->vm_file = get_file(file);
        // 这一步中将虚拟内存区域 vma 的操作函数 vm_ops 映射成文件的操作函数(和具体文件系统有关)
        // ext4 文件系统中的操作函数为 ext4_file_vm_ops
        // 从这一刻开始,读写内存就和读写文件是一样的了
        error = call_mmap(file, vma);
        if (error)
            goto unmap_and_free_vma;

        addr = vma->vm_start;
        vm_flags = vma->vm_flags;
    } else if (vm_flags & VM_SHARED) {
        // 这里处理共享匿名映射
        // 前面提到共享匿名映射依赖于 tmpfs 文件系统中的匿名文件
        // 父子进程通过这个匿名文件进行通讯
        // 该函数用于在 tmpfs 中创建匿名文件,并映射进当前共享匿名映射区 vma 中
        error = shmem_zero_setup(vma);
        if (error)
            goto free_vma;
    } else {
        // 这里处理私有匿名映射
        // 将  vma->vm_ops 设置为 null,只有文件映射才需要 vm_ops 这样才能将内存与文件映射起来
        vma_set_anonymous(vma);
    }
    // 将当前 vma 按照地址的增长方向插入到进程虚拟内存空间的 mm_struct->mmap 链表以及mm_struct->mm_rb 红黑树中
    // 并建立文件与 vma 的反向映射
    vma_link(mm, vma, prev, rb_link, rb_parent);

    file = vma->vm_file;
out:
    // 更新地址空间 mm_struct 中的相关统计变量
    vm_stat_account(mm, vm_flags, len >> PAGE_SHIFT);
    return addr;
}

5 附录

https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&mid=2247488879&idx=1&sn=4cbbabc648e1a29466c3309371a27a4f&chksm=ce77d328f9005a3e7a0bb0b0ff7ad88b5a3f10c8ad850fb5b503d51001b63eeb2c909ba32a78&cur_album_id=2559805446807928833&scene=21#wechat_redirect

https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&mid=2247488750&idx=1&sn=247a4603299e203793fac8b6c5e61071&chksm=ce77d2a9f9005bbf3b024bc9f9192f2de63a70fd33db1113d9f9c0d8a1ced2099fbeb727a3d7&cur_album_id=2559805446807928833&scene=21#wechat_redirect

相关推荐
DeeplyMind1 小时前
TTM ttm_tt技术分析系列1:导读
linux·驱动开发·gpu·amd·gart
视觉装置在笑7131 小时前
awk 基础知识和进阶用法
linux·运维·服务器·正则表达式
Starry_hello world2 小时前
Linux 动静态库
linux
爱吃番茄鼠骗2 小时前
Linux操作系统———线程同步
linux·学习
majingming1232 小时前
野火鲁班猫修改IP
linux·运维·服务器
ayaya_mana2 小时前
Debian 12 上部署 OpenMediaVault 详细配置步骤
linux·运维·debian·nas·存储服务器·omv
xu_yule2 小时前
网络和Linux网络-8(传输层)TCP协议(流量控制+滑动窗口+拥塞控制+紧急指针+listen第二个参数)
linux·网络·tcp/ip
MyFreeIT2 小时前
ubuntu manual
linux·运维·ubuntu
٩( 'ω' )و2602 小时前
linux -- 进程间通信01
linux