【Linux】—内存映射(线性映射、非线性映射、逆映射)

内存映射

本节将描述内存映射是如何实现

相关数据结构

vm_area_struct

在这里插入图片描述

上图中展现的vm结构体中的属性,在内存映射时会使用到

  • 相关结构体描述
    • vm_mm:每个vm_area_struct会记录映射向自己的mm_struct,其中vm_mm则存放的对应的mm_stuct的结构体指针。
    • vm_start、vm_end:表示当前的vm的起始地址和结束地址。
    • shared:主要负责管理vm的映射情况。
      • vm_set:存放了一个parent指针,这个指针貌似是指向其优先级树节点的。其中优先级树负责管理页面的映射情况,在后面实现逆映射会用到。
      • vm_file:存放这块vm对应的file文件指针。
struct vm_area_struct {
  	//反向指针,指向该区域所属的mm_struct实例
  	struct mm_struct * vm_mm;	/* The address space we belong to. *///所属地址空间
  	unsigned long vm_start;		/* Our start address within vm_mm. *///vm_mm内的起始
  	unsigned long vm_end;		/* The first byte after our end address//在vm_mm内结束地址之后的第一个字节的地址
  					   within vm_mm. */
  	struct vm_area_struct *vm_next;//链接各进程的虚拟内存区域链表,按地址排序
  	pgprot_t vm_page_prot;		/* Access permissions of this VMA. *///虚拟内存区域的访问权限
  	//vm_flags会定义虚拟内存的一些管理行为
  	unsigned long vm_flags;		/* Flags, listed below. */
  	struct rb_node vm_rb;
  	union {
  		struct {
  			struct list_head list;
  			void *parent;	/* aligns with prio_tree_node parent *///prio_tree_node的父亲节点
  			struct vm_area_struct *head;
  		} vm_set;
  		struct raw_prio_tree_node prio_tree_node;//(内存管理对象)优先级树节点prio_tree_node
  	} shared;
  	struct list_head anon_vma_node;	/* Serialized by anon_vma->lock */
  	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */
  	struct vm_operations_struct * vm_ops;//处理该结构的函数指针
  	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
  					   units, *not* PAGE_CACHE_SIZE *///(vm_file的偏移量,偏移量是page_size)
  	struct file * vm_file;		/* File we map to (can be NULL). *///映射到的文件
  	void * vm_private_data;		/* was vm_pte (shared mem) *///vm_pte(共享内存)
  	unsigned long vm_truncate_count;/* truncate_count or restart_addr */
};
mm_struct

  mm_struct每个进程进入时,系统都会分配一个mm_struct来对该进程对应的内存部分进行管理。对mm_struct,我们可以分为以下几个部分来理解

  • 内存管理部分
    • mmap:存储该mm下的所有对应的vm区域下的内存列表
    • mm_rb:用来管理vm,基于红黑树构建,方便查找。
    • mmap_cache、mmap_base、task_size会在mm中的vm时会用到
  • 内存布局相关
    • total_vm、stack_vm、start_code、start_brk:这些在elf文件读取时,会进行填充,通过读取elf文件中的信息,从而确定不同区域代码段的范围
    struct mm_struct {
    	//mmap、mm_rb、mmap_cache会根据对象分别存储
    	struct vm_area_struct * mmap;		/* 虚拟内存区域列表 */
    	struct rb_root mm_rb;
    	struct vm_area_struct * mmap_cache;	/* 上一次find_vma的结果 */
    	unsigned long (*get_unmapped_area) (struct file *filp,
    				unsigned long addr, unsigned long len,
    				unsigned long pgoff, unsigned long flags);
    	void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
    	unsigned long mmap_base;		/* base of mmap area 虚拟空间中用于内存映射的起始位置,可以使用get_unmapped_area在mmap区域中为新映射找到适当位置*/
    	unsigned long task_size;		/* size of task vm space 进程的地址空间长度*/
    	unsigned long cached_hole_size; 	/* if non-zero, the largest hole below free_area_cache */
    	unsigned long free_area_cache;		/* first hole of size cached_hole_size or larger */
    	pgd_t * pgd;
    	atomic_t mm_users;			/* How many users with user space? */
    	atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1) */
    	int map_count;				/* number of VMAs */
    	struct rw_semaphore mmap_sem;
    	spinlock_t page_table_lock;		/* Protects page tables and some counters */
    
    	unsigned long total_vm, locked_vm, shared_vm, exec_vm;
    	unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
    	//start_code和end_code为虚拟空间中可执行代码占用的虚拟地址空间
    	//start_data,end_data为已初始化的数据区域
    	unsigned long start_code, end_code, start_data, end_data;
    	//start_brk表示堆区域当前的结束地址,brk表示堆区域当前的结束地址
    	//因为堆的生命周期不变,但是堆的长度会发生变化,则brk的值也会发生变化
    	unsigned long start_brk, brk, start_stack;
    	unsigned long arg_start, arg_end, env_start, env_end;
    };
    

   mm_struct对虚拟内存的管理如下图(直接从书中截取)
在这里插入图片描述


address_space

adress_space是一个中间结构,用来管理一个文件区域与其相关的所有虚拟内存区域。以方便实现逆映射。(这里的文件区域,应该是指的实际的物理页面)

  • 在address_space中有很多字段,这里主要关注其优先级树,优先级树会管理与文件相关的所有vm
    struct address_space {
      ...
      struct prio_tree_root	i_mmap;
      ...
    }
    

   这里为其虚址管理过程
在这里插入图片描述

下面我们将进一步了解文件如何实现映射到虚拟内存上。


【内存映射】线性映射——标识符检查

创建映射前,会先对传入的标识符进行验证,然后再进行获取地址等处理。

/*
file:要映射的文件。如果映射的是匿名内存,则该参数为 NULL。
addr:希望映射的起始虚拟地址。如果 addr 为 0,内核会自动分配一个虚拟地址。
len:希望映射的区域长度(字节)。必须是页大小(通常为 4KB)的整数倍数。
prot:映射区域的访问权限。可以是以下值的按位或:
	PROT_READ:允许读取
	PROT_WRITE:允许写入
	PROT_EXEC:允许执行
	PROT_NONE:禁止访问
flags:控制映射的行为。可以是以下标志的按位或
	MAP_SHARED:映射区域与其他进程共享
	MAP_PRIVATE:创建一个私有映射,对映射的修改不会影响到文件
	MAP_FIXED:强制使用指定的虚拟地址
	MAP_ANONYMOUS:创建一个匿名映射,不与任何文件关联
pgoff:文件中的偏移量,以页为单位。如果映射的是匿名内存,则忽略该参数。
*/
unsigned long do_mmap_pgoff(struct file * file, unsigned long addr,unsigned long len, 
                            unsigned long prot,unsigned long flags, unsigned long pgoff)
  • 检测映射区域的访问权限(port)和映射行为(flags)的合法性
    • 如果映射区域的访问权限(port)是可读且文件是可执行的,则port添加可执行权限
      if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
          if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
    	    prot |= PROT_EXEC;
    
    • 检测映射行为(flag),是否需要强制使用给定地址,如果不是就对地址做一下修正以下。
     if (!(flags & MAP_FIXED))
      	addr = round_hint_to_min(addr);
    
  • 获取地址并确定vm_flags标记
    • 获取满足长度要求的虚拟地址:从地址 addr 开始,尝试向上寻找一个大小为 len 的未映射连续内存区域,使得该区域中没有任何已经映射的内存页,并且满足一些额外的限制条件。(这里就是获取到需要映射内存地址了)
      addr = get_unmapped_area(file, addr, len, pgoff, flags);
      if (addr & ~PAGE_MASK)
      	return addr;
    
    • 根据访问权限和映射行为,确定vm_flag
      • calc_vm_prot_bits(prot):将 Linux 虚拟内存系统中的保护位(protection bits)用于权限控制的抽象描述符 prot 转换为实际的二进制数值。
      • calc_vm_prot_bits(flags):将虚拟内存区域的属性标志(flag bits)用于区分不同类型的内存区域的抽象描述符 flags 转换为实际的二进制数值。
      • mm->def_flags:mm->def_flags表示进程默认的vm_flags标志
      • VM_MAYREAD、VM_MAYWRITE和VM_MAYEXEC:表示该虚拟内存区域可以读、写和执行等操作。
    • 即将portflag等相关标志位,赋值到vm_flag
    vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) 
              |mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
    
    • 如果file存在,则进一步对标志进行合法检测
      • 如果file是共享映射
        • 则需要进一步检测访问权限和file的文件权限是否都可写
        • 如果对应的inode是可写入的,但file是不可写入的,则修改vm_flag设置为不可写入的形式
        if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
             return -EACCES;
        if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
            return -EACCES;
         if (locks_verify_locked(inode))
            return -EAGAIN;
         vm_flags |= VM_SHARED | VM_MAYSHARE;
         if (!(file->f_mode & FMODE_WRITE))//file是不可写入的
             vm_flags &= ~(VM_MAYWRITE | VM_SHARED);//如果file是不可写入的,那么就要将vm_flag设置为不可写,不可共享
        
      • 如果file是私有映射
        • file不可读,错误
        • file不可执行,但是vm_flags可执行,错误。否则将vm_flag修改为不可执行
        if (!(file->f_mode & FMODE_READ))//不可读
          	return -EACCES;
        if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {//不可执行
          	if (vm_flags & VM_EXEC)//但vm_flag可执行
          		return -EPERM;
          	vm_flags &= ~VM_MAYEXEC;//vm_flags未设置,则纠正
          }
        if (is_file_hugepages(file))//如果是hugepages,则accountable置0(默认为1)
          	accountable = 0;
          //file需要有文件描述符&被映射
        if (!file->f_op || !file->f_op->mmap)
          	return -ENODEV;
          break;
        
      上面这个过程就是检测file的文件模式和vm_flag之间能不能够保持一致,检测合法性、一致性后,则进入执行分配操作。进入
      mmap_region(file, addr, len, flags, vm_flags, pgoff,accountable);
      

在这里插入图片描述

      /*
      mmap() 是一种内存映射文件的系统调用,它将一个文件或其它对象映射到进程的虚拟地址空间,
      使得对这块区域的读写操作可以直接在内存中完成。这个过程可以让操作系统将磁盘上的文件映射
      到进程的地址空间,使得进程可以像读写内存一样来读写文件
      */
      unsigned long do_mmap_pgoff(struct file * file, unsigned long addr,
      			unsigned long len, unsigned long prot,
      			unsigned long flags, unsigned long pgoff){
      	//获取本进程的内存地址
      	struct mm_struct * mm = current->mm;
      	struct inode *inode;//获取节点
      	unsigned int vm_flags;
      	int error;
      	int accountable = 1;
      	unsigned long reqprot = prot;
    	//prot表示映射区域的访问权限
    	//如果映射区域是可读的,且文件存在&不禁止在该文件上执行程序
    	//则对映射区域的权限设置为可执行
    	if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
    		if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
    			prot |= PROT_EXEC;

    	if (!len)
    		return -EINVAL;
    	//没有强制使用地址,则要对addr进行修正
    	if (!(flags & MAP_FIXED))
    	//round_hint_to_min:用于将给定的地址 addr 向下舍入到最接近的内存页大小的整数倍
    	//为了保证所分配的内存块是以整数倍的内存页大小对齐的
    		addr = round_hint_to_min(addr);

    	//它会对应用程序传递给 mmap 系统调用的三个参数进行一些检查,确保它们符合系统的限制和要求。
    	error = arch_mmap_check(addr, len, flags);
    	if (error)
    		return error;
    	//长度对齐
    	len = PAGE_ALIGN(len);
    	if (!len || len > TASK_SIZE)
    		return -ENOMEM;
    	/* offset overflow? */
    	if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
                   return -EOVERFLOW;
    	/* Too many mappings? */
    	//sysctl_max_map_count,用于限制单个进程能够创建的内存映射区域的数量
    	if (mm->map_count > sysctl_max_map_count)
    		return -ENOMEM;
    	//用于在给定进程的地址空间中寻找一段未被映射的内存区域
    	/*
    	它会从地址 addr 开始,尝试向上寻找一个大小为 len 的连续内存区域,
    	使得该区域中没有任何已经映射的内存页,并且满足一些额外的限制条件。
    	(所以这里就是获取到需要映射内存地址了)
    	file 表示当前正在进行内存映射操作的文件对象
    	addr 表示希望在哪个地址开始寻找未被映射的内存区域
    	len 表示希望找到的内存区域的长度
    	pgoff 表示在文件中的偏移量
    	flags 表示内存区域的访问权限和其他属性等信息
    	*/
    	addr = get_unmapped_area(file, addr, len, pgoff, flags);
    	if (addr & ~PAGE_MASK)
    		return addr;
    	//1.根据prot的标志计算出相应的vm_flags标志位
    	//2.根据flags计算出对应的vm_flags标志位
    	//3.mm->def_flags表示进程默认的vm_flags标志
    	//4.VM_MAYREAD、VM_MAYWRITE和VM_MAYEXEC是一些宏定义,表示该虚拟内存区域是否可以读、写和执行等操作。
    	//根据这些要求合并成一个vm_flag的标志符
    	vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
    			mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

    	if (flags & MAP_LOCKED) {
    		if (!can_do_mlock())
    			return -EPERM;
    		vm_flags |= VM_LOCKED;
    	}
    	/* mlock MCL_FUTURE? */
    	if (vm_flags & VM_LOCKED) {
    		unsigned long locked, lock_limit;
    		locked = len >> PAGE_SHIFT;
    		locked += mm->locked_vm;
    		lock_limit = current->signal->rlim[RLIMIT_MEMLOCK].rlim_cur;
    		lock_limit >>= PAGE_SHIFT;
    		if (locked > lock_limit && !capable(CAP_IPC_LOCK))
    			return -EAGAIN;
    	}
    	//如果file存在,则innode是目录项中获取得inode节点,否则就为null
    	inode = file ? file->f_path.dentry->d_inode : NULL;
    	//文件存在的话,再做一些检测
    	if (file) {
    		//files中指定了访问模式
    		//根据flag的映射类型进行判断
    		switch (flags & MAP_TYPE) {
    		case MAP_SHARED://共享映射时判断file模式是否合适
    			if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
    				return -EACCES;
    			/*
    			 * Make sure we don't allow writing to an append-only
    			 * file..
    			 */
    			//innode是可追加的&file是可写的,但映射的flag是共享的,这是不允许的
    			//拒绝为可追加内容的file,提供共享内存
    			if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
    				return -EACCES;

    			/*
    			 * Make sure there are no mandatory locks on the file.
    			 */
    			//有锁得节点也不行
    			if (locks_verify_locked(inode))
    				return -EAGAIN;
    			//经过上述处理,则将vm_flags设置共享的形式
    			//意思是如果file是不可写,flag为共享的状态下,映射的文件为独有的
    			//但是innode是追加状态时,就不允许设置共享内存
    			vm_flags |= VM_SHARED | VM_MAYSHARE;
    			if (!(file->f_mode & FMODE_WRITE))//file是不可写入的
    				vm_flags &= ~(VM_MAYWRITE | VM_SHARED);//如果file是不可写入的,那么就要将vm_flag设置为不可写,不可共享
    			/* fall through */
    		case MAP_PRIVATE://私有映射
    		//文件不可读,错误
    		//文件不可执行,但是vm_flags可执行->错误
    			if (!(file->f_mode & FMODE_READ))//不可读
    				return -EACCES;
    			if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {//不可执行
    				if (vm_flags & VM_EXEC)//但vm_flag可执行
    					return -EPERM;
    				vm_flags &= ~VM_MAYEXEC;//vm_flags未设置,则纠正
    			}
    			if (is_file_hugepages(file))//如果是hugepages,则accountable置0(默认为1)
    				accountable = 0;
    			//file需要有文件描述符&被映射
    			if (!file->f_op || !file->f_op->mmap)
    				return -ENODEV;
    			break;

    		default:
    			return -EINVAL;
    		}
    	} else {
    		switch (flags & MAP_TYPE) {
    		case MAP_SHARED:
    			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;
    		}
    	}
    	error = security_file_mmap(file, reqprot, prot, flags, addr, 0);
    	if (error)
    		return error;
    	//将获取的地址、长度和vm的访问要求进行传入,开始实现映射
    	return mmap_region(file, addr, len, flags, vm_flags, pgoff,
    			   accountable);
    }

【内存映射】线性映射——内存分配

上面进行了标识符检查后,下面就根据上面的检测结果,进行内存分配

unsigned long mmap_region(struct file *file, unsigned long addr,
			  unsigned long len, unsigned long flags,
			  unsigned int vm_flags, unsigned long pgoff,
			  int accountable)
file:要映射的文件对象
addr:用户空间的起始地址
len:映射的内存长度
flag:映射标志,指定了映射的类型和操作方式
vm_flags:内存区域的属性标志,在linux中控制着内存区的访问权限和缓存策略等属性
pfoff:文件中的偏移位置,指定了文件从哪个位置开始映射
accoutable:是否跟踪内存使用情况,1-表示需要,0-表示不需要
  • 虚拟地址空间的分配
    • 遍历mm下的红黑树,查看一下给定地址addr下,是否有满足的vma
      • 如果存在,且看看长度是否合法,如果不行就重新分配
      vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
      //获取到地址,但是起始地址不满足要求(这里应该是检测一下,vm中有无合适的,有的话才继续向下遍历)
      if (vma && vma->vm_start < addr + len) {//检查检查地址范围 [addr, addr+len) 是否和某个 VMA 重叠
      	//不满足,,解除映射,然后返回
      	if (do_munmap(mm, addr, len))
      		return -ENOMEM;
      	goto munmap_back;//重新分配
      }  
      
    • 从内核内存分配器中分配内存空间—vma
    • 对vma相关属性进行配置
      vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
      if (!vma) {
      	error = -ENOMEM;
      	goto unacct_error;
      }
      //对vma进行相关管理数据的填充
      vma->vm_mm = mm;//vm对应的mm
      vma->vm_start = addr;
      vma->vm_end = addr + len;
      vma->vm_flags = vm_flags;//vm相关的权限标志
      vma->vm_page_prot = vm_get_page_prot(vm_flags);
      vma->vm_pgoff = pgoff;  
      
  • 如果需要映射的是file,需要进行挂载
    • vm_flags如果是可扩展的,则释放并退出
      if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
        goto free_vma;
      
    • 如果vm_flags是不可写入,但file能够写入,则退出
      if (vm_flags & VM_DENYWRITE) {
      		error = deny_write_access(file);
      		if (error)
      			goto free_vma;
      		correct_wcount = 1;
      	}
      
    • 将file文件挂载在该vm下,并在file结构中进行链接
      vma->vm_file = file;
      get_file(file);
      error = file->f_op->mmap(file, vma);
      
  • 对相关数量进行统计
    • 根据长度,即本进程mm下的vm数量
    • vm_state_count,应该是对各种类型页面(如匿名/文件页面、脏/干净页面等)的数量和大小进行记录
      mm->total_vm += len >> PAGE_SHIFT;
      vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
      
  • 返回地址
      return addr;
    

在这里插入图片描述

  • 对于内存映射的过程可以总结为以下
    在这里插入图片描述

但是存在一个疑问,在将vm和file之间进行挂载的过程中,没有看到vm和address_space之间的关联,也没有看到其与rb_tree之间的挂载情况


【内存映射】非线性映射

线性映射下,会创建一个连续的vma与物理页面直接关联。如果存在大文件的情况,那么需要将文件的不同部分以不同顺序映射到虚拟内存的连续区域中,通常必须使用几个映射,这样代价很昂贵。为了加快效率,使用非线性映射的方式,使用页表机制实现物理页面和虚拟页面之间的映射关系建立

asmlinkage long sys_remap_file_pages(unsigned long start, unsigned long size,
	unsigned long prot, unsigned long pgoff, unsigned long flags)
 * sys_remap_file_pages - 对vma进行重映射
 * @start: 需要重映射的起始地址
 * @size: 重映射的地址大小
 * @prot: 新的访问权限
  • 获取vma
    • 通过start,从mm中获得对应的vma
    vma = find_vma(mm, start);//找到start对应的vma
    
  • (对标志位进行检查)对vma_flags不是非线性的
    • 设置非线性映射标志VM_NONLINEAR
    • 然后将该vma从address_space的线性结构转向非线性结构
    if (!(vma->vm_flags & VM_NONLINEAR)) {
        vma->vm_flags |= VM_NONLINEAR; //确定vm_flags为非线性的
        vma_prio_tree_remove(vma, &mapping->i_mmap);//将vma从mapping->i_mmap中移除
        vma_nonlinear_insert(vma, &mapping->i_mmap_nonlinear);//将这块vma加入到非线性映射区
    }
    
  • 重新建立映射
    • 取消vma和pte之间的映射关系&&将addr和pte重新建立映射关系
      • 这里我的理解是,此时addr对应的地址还是vma,vma中存储的就是file。后面就通过触发缺页机制,来对addr进行寻址,应该最终会找到vma上,然后通过vma来将底层设备数据进行调入
    populate_range(mm, vma, start, size, pgoff);
    ---->install_file_pte(mm, vma, addr, pgoff, vma->vm_page_prot)
         ------>zap_pte(mm, vma, addr, pte);//删除vma相关的页表项
         ------>set_pte_at(mm, addr, pte, pgoff_to_pte(pgoff));//通过页表建立物理地址和虚拟地址之间的关联关系
    
  • 通过缺页方式,从底层将数据换回
    make_pages_present(start, start+size);
    
  • 小结一下
    要重新映射start地址,需要定位与该地址相关的vma。在线性映射下,vma是用于管理虚拟内存的数据结构。为了将vma从线性管理转换为非线性管理,需要获取vma相应的address_space。然后取消vma与pte之间的关联,并基于start地址建立一个新的映射关系。这样做不会影响vma的存在,它仍然存储着file信息。最终,系统通过触发缺页机制对addr进行寻址并找到vma,然后通过vma将底层设备数据调入内存。
    在这里插入图片描述

逆映射实现过程

内存映射,让一个file文件(物理内存)与一个vm进行挂载。在实际的运行中,一个物理内存,可能被多个进程使用,与多个vm之间进行关联。当要进行页面回收时,就需要解除物理内存与其相关的vm之间的关系。

逆向映射的一个目的就是实现回收共享页框。通过逆映射的发展历史,可以大致对逆向映射有所了解。这里我们对Linux2.6.24中逆映射的实现进行了解。

在这里插入图片描述

上面这张图就是阐述了一个file文件与vm之间的关系,听过逆映射的发展历史可以发现逆映射的实现,是借助了address_space来实现的。

数据类型介绍

要了解逆映射的实现,就需要了解page和vma的相关管理结构。

  • page中关于逆映射的结构
    struct page {
      	union {
      	atomic_t _mapcount;	/*统计映射数目*/
      	...
      };
      	union {
          struct {
      		...
      		/*指向address_space空间(在__page_set_anon_rmap中会看到部分使用)*/
      		struct address_space *mapping;	
      		...
          };
      }
    
  • address_space中的结构
    struct address_space {
      ...
         struct prio_tree_root	i_mmap;		/* tree of private and shared mappings *///线性
         struct list_head	i_mmap_nonlinear;/*list VM_NONLINEAR mappings *///非线性
      ...
      }
    
  • vma中的结构
    struct vm_area_struct {
      ...
      union {
      	struct {
      		/*
      		shared,则是和i_mmap_nonlinear相关,我暂时理解,就是映射到了文件同一个部分
      		,则用这个链表来进行存储
      		*/
      		struct list_head list;
      		void *parent;//用来和优先级搜索树节点对齐
      		struct vm_area_struct *head;//指向链表list的表头
      	} vm_set;
      	//这里链接到优先级树上
      	struct raw_prio_tree_node prio_tree_node;//优先级搜索树中的一个节点
      	} shared;
      //匿名页表的双向链表的相关字段(匿名页存放在双链表中)
      struct list_head anon_vma_node;	//该vma在链表中的位置
      struct anon_vma *anon_vma;	//指向anon_vam表,就是保存的一个双向链表
      ...
      }
    
    • 匿名页存放在一个双向列表anon_vma中,file页则存放在i_mmap和i_mmap_nonlinear中。
    struct anon_vma {
      spinlock_t lock;	/* Serialize access to vma list */
      struct list_head head;	/* List of private "related" vmas */
      };
    
    • 关于vm_set的附注——优先级搜索树节点的设置
      • Linux中使用 (radix,size,heap) 来表示优先级搜索树中的节点。其中,radix 表示内存区域的起始位置,heap 表示内存区域的结束位置,size 与内存区域的大小成正比。在优先级搜索树中,父节点的 heap 值一定不会小于子节点的 heap 值。在树中进行查找时,根据节点的 radix 值进行。程序可以根据 size 值区分那些具有相同 radix 值的节点。
初步对page类型进行检查
  • 如果是匿名页,则try_to_unmap_anon
  • 如果是file文件,则try_to_unmap_file
int try_to_unmap(struct page *page, int migration)1{
	int ret;

	BUG_ON(!PageLocked(page));

	if (PageAnon(page))
		ret = try_to_unmap_anon(page, migration);
	else
		ret = try_to_unmap_file(page, migration);

	if (!page_mapped(page))
		ret = SWAP_SUCCESS;
	return ret;
}
  • 页面分类
名称作用
文件页内存回收,也就是系统释放掉可以回收的内存,比如缓存和缓冲区,就属于可回收内存。它们在内存管理中,通常被叫做文件页(File-backed Page)。大部分文件页,都可以直接回收,以后有需要时,再从磁盘重新读取
文件映射页除了缓存和缓冲区,通过内存映射获取的文件映射页,也是一种常见的文件页。它也可以被释放掉,下次再访问的时候,从文件重新读取。
匿名页应用程序动态分配的堆内存,也就是在内存管理中说到的匿名页(Anonymous Page),它们很可能还要再次被访问啊,不能直接回收,这些内存自然不能直接释放
建立逆向映射

要通过逆向映射来对页面进行管理,就要知道如何建立逆向映射,对进行逆向映射的建立,主要分为两种页面匿名页和file页

  • 匿名页
    static void __page_set_anon_rmap(struct page *page,
      struct vm_area_struct *vma, unsigned long address){
      struct anon_vma *anon_vma = vma->anon_vma;
    
      BUG_ON(!anon_vma);
      anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;
      page->mapping = (struct address_space *) anon_vma;
    
      page->index = linear_page_index(vma, address);
    
      /*
       * nr_mapped state can be updated without turning off
       * interrupts because it is not modified via interrupt.
       */
      __inc_zone_page_state(page, NR_ANON_PAGES);
      }
    
    • __page_set_anon_rmap中将新的vma中的匿名页表保存在page->mapping中。
    • 在anon_vma表头上加上一个PAGE_MAPPING_ANON。用于区分匿名页和普通映射。添加了PAGE_MAPPING_ANON,则表示进行置位为1,则为匿名页。否则为空,即为0,为普通映射。
  • file页
    void page_add_file_rmap(struct page *page){
    	if (atomic_inc_and_test(&page->_mapcount))
    		__inc_zone_page_state(page, NR_FILE_MAPPED);
    }
    
    如果page的类型是file,如果有新的vma进行挂载,则将page->_mapcount增加即可
当page是file文件类型
static int try_to_unmap_file(struct page *page, int migration)

在这里插入图片描述

  • 解除线性映射
    • 通过page的address_space,并对 i_mmap(优先级树) 进行遍历,然后尝试解除i_mmap上的每一个页面
    struct address_space *mapping = page->mapping;
     vma_prio_tree_foreach(vma, &iter, &mapping->i_mmap, pgoff, pgoff) {
      	ret = try_to_unmap_one(page, vma, migration);
      	if (ret == SWAP_FAIL || !page_mapped(page))
      		goto out;
      }
    
  • 解除非线性映射页面的方法

    先对解除非线性映射页面的方法做个基础了解,才能更好了解代码的意思

    • 非线性映射回顾
      非线性映射通过将物理内存空间分割成多个小的非重叠区域,并将它们映射到不同的虚拟地址空间中,其使用的基础就是通过页表机制,来实现对物理页面的多个部分的映射。
    • 非线性映射的逆映射
      • 对非线性映射页面进行解除映射时,page映射的vma根据其大小进行“聚类”,然后分批次的解除映射
  • 解除非线性映射
    • 通过list_for的循环,来计算非线性映射中的最大长度
      • 其中list_for_each_entry指从该page中的非线性映射链表i_mmap_nonlinear的头节点开始遍历,并且以每次获取的vma的list作为下一个节点进行遍历
      • 通过list_for的循环,来计算非线性映射中的最大长度
    //通过遍历非线性链表,找到非线性映射链表中的最大映射长度
    list_for_each_entry(vma, &mapping->i_mmap_nonlinear,
      					shared.vm_set.list) {
      if ((vma->vm_flags & VM_LOCKED) && !migration)
      		continue;
      cursor = (unsigned long) vma->vm_private_data;
      if (cursor > max_nl_cursor)
      	max_nl_cursor = cursor;
      cursor = vma->vm_end - vma->vm_start;
      if (cursor > max_nl_size)
      	max_nl_size = cursor;
    }
    
    • 统计page被映射的数量
      • 如果没有映射,则退出
    mapcount = page_mapcount(page);
    if (!mapcount)
      	goto out;
    
    • **批处理解除映射
      • 确定每次批处理的大小范围
      max_nl_size = (max_nl_size + CLUSTER_SIZE - 1) & CLUSTER_MASK;//非线性映射是统一对一片内存进行解除映射,那么通过CLUSTER_MASK的形式来实现聚类
      if (max_nl_cursor == 0)
      	max_nl_cursor = CLUSTER_SIZE;
      
      • 对vma进行遍历
        • vma->vm_private_data为vma的私有数据信息,其中存储了vma所在的页表项信息(关于vm_private_data的处理,并不是很清楚)
        • cursor所指范围在当前vma的范围,就尝试解除映射
        • 知道当前max_nl_cursor范围内的所有页面都解除才进行下一个循环
        • 直到将最大的映射长度的vma解除映射
    do {
      //这里实际上是两重循环,这里通过+CLUSTER_SIZE,来将少于max_nl_size
      //的每一个“页面类”进行进行集中处理,知道把最大类也处理为止
      //这里+CLUSTER_SIZE应该是
      list_for_each_entry(vma, &mapping->i_mmap_nonlinear,shared.vm_set.list) {
      	if ((vma->vm_flags & VM_LOCKED) && !migration)
      		continue;
      	cursor = (unsigned long) vma->vm_private_data;
      	while ( cursor < max_nl_cursor && cursor < vma->vm_end - vma->vm_start) {
      			try_to_unmap_cluster(cursor, &mapcount, vma);
      			cursor += CLUSTER_SIZE;
      			vma->vm_private_data = (void *) cursor;
      			//无映射页面->退出
      			if ((int)mapcount <= 0)
      				goto out;
      		}
      		vma->vm_private_data = (void *) max_nl_cursor;
      	}
      	cond_resched_lock(&mapping->i_mmap_lock);
      	max_nl_cursor += CLUSTER_SIZE;
      } while (max_nl_cursor <= max_nl_size);
    
    • 映射解除后,将vm_private_data置空
      list_for_each_entry(vma, &mapping->i_mmap_nonlinear, shared.vm_set.list)
      vma->vm_private_data = NULL;
      

当page为匿名页面
try_to_unmap_anon(struct page *page, int migration)
  • 对于匿名页,直接对anon_vma遍历,并依次使用try_to_unmap_one解除映射即可
    anon_vma = page_lock_anon_vma(page);
    if (!anon_vma)
    	return ret;
    list_for_each_entry(vma, &anon_vma->head, anon_vma_node) {
    	ret = try_to_unmap_one(page, vma, migration);
    	if (ret == SWAP_FAIL || !page_mapped(page)) 
      	break;
      	}
    page_unlock_anon_vma(anon_vma);
    
try_to_unmap_one

在对file页的线性映射和匿名页解除映射都使用了该接口

static int try_to_unmap_one(struct page *page, struct vm_area_struct *vma,int migration)
//page:待解除映射页面
//vma:待与vma解除映射的vma
//migration:页面的迁移类型
  • 得到vma对应的mm
    struct mm_struct *mm = vma->vm_mm;
    
  • 得到page的虚拟地址
    • 通过page和vma找到page在mm中的位置
    address = vma_address(page, vma);
    
  • 获取page对应的页表项
    • page_check_address会检测页目录、页表项等,判断该地址是否存在,如果存在就返回一个页表项
    pte = page_check_address(page, mm, address, &ptl);
    
  • 做一致性检查&&清除pte数据
    //刷新 CPU 缓存中指定页面的缓存,保证数据一致性
    flush_cache_page(vma, address, page_to_pfn(page));
    //清除pte上对应的页面项,并通知所有cpu
    pteval = ptep_clear_flush(vma, address, pte);
    
  • 如果page是匿名页
    • 对于匿名页的处理,涉及了交换缓存的知识,不是很清楚这里做的相关操作
    if (PageAnon(page)) {
      	swp_entry_t entry = { .val = page_private(page) };//swp_entry_t表示交换分区里的一个槽位,val字段表示交换槽位的编号和一些标志位信息
      	//如果page在交换缓存中
      	if (PageSwapCache(page)) {
      		/*
      		 * Store the swap location in the pte.
      		 * See handle_pte_fault() ...
      		 */
      		swap_duplicate(entry);//创建一个槽位副本
      		if (list_empty(&mm->mmlist)) {
      			spin_lock(&mmlist_lock);
      			if (list_empty(&mm->mmlist))
      				list_add(&mm->mmlist, &init_mm.mmlist);
      			spin_unlock(&mmlist_lock);
      		}
      		//用于减少给定进程(mm)的匿名(anonymous)页面计数器(anon_rss)
      		dec_mm_counter(mm, anon_rss);
      		...
      }
    
  • 如果page是正常页面
    • 如果是file页面,就在mm中的file页面计数-1
      dec_mm_counter(mm, file_rss);
    
  • 解除映射
    page_remove_rmap(page, vma);
    page_cache_release(page);
    
  • 小结
    • try_to_unmap_one得到待解除映射的page和vma,进而获得相应的mm和pte。在解除映射时,需要区分一下匿名页和文件页的区别。匿名页可能涉及缓存上的操作,最后直接解除映射关系
      在这里插入图片描述
try_to_unmap_cluster
static void try_to_unmap_cluster(unsigned long cursor,unsigned int *mapcount, struct vm_area_struct *vma)
//cursor:解除映射的地址
//mapcount:映射计数器
//vma:待解除映射的vma
  • 获取进程描述符
    struct mm_struct *mm = vma->vm_mm;
    
  • 获取起始虚拟地址address和结束虚拟地址end
    • cursor对应的偏移为起始地址address,end要么为vm->end,要么就是address + CLUSTER_SIZE;
    address = (vma->vm_start + cursor) & CLUSTER_MASK;
    end = address + CLUSTER_SIZE;
    if (address < vma->vm_start)
    	address = vma->vm_start;
    if (end > vma->vm_end)
    	end = vma->vm_end;
    
  • 根据虚拟地址address的页表项pte
    pgd = pgd_offset(mm, address);
    if (!pgd_present(*pgd))
    	return;
    pud = pud_offset(pgd, address);
    if (!pud_present(*pud))
    	return;
    pmd = pmd_offset(pud, address);
    if (!pmd_present(*pmd))
    	return;
    pte = pte_offset_map_lock(mm, pmd, address, &ptl);
    
  • 循环遍历address和end对应的地址
    for (; address < end; pte++, address += PAGE_SIZE)
    
    • 获取address对应的page页面
      • 根据相应的pte,来找到address对应的page页
      page = vm_normal_page(vma, address, *pte);
      
    • 对应面做基本的检查和设置
      //确保数据一致性,并进行数据清楚
      flush_cache_page(vma, address, pte_pfn(*pte));
      pteval = ptep_clear_flush(vma, address, pte);
      //设置脏页
      if (pte_dirty(pteval))
      	set_page_dirty(page);
      
    • 解除page和vma之间的映射关系
      page_remove_rmap(page, vma);
      page_cache_release(page);
      //计数-1
      dec_mm_counter(mm, file_rss);
      (*mapcount)--;
      

在这里插入图片描述

  • 小结
    和try_to_unmap_one的思路相似,不同在于try_to_unmap_cluster并不针对某一个页面进行处理,而是获取一个地址范围,根据获取的虚拟地址获取其对应的page,并将vma与其的关联全部清除。在每一次的循环中,通过pte找到page,然后解除映射

疑问:为什么要这么处理,这样处理的好处是什么?

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值