【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:表示该虚拟内存区域可以读、写和执行等操作。
- 即将port和flag等相关标志位,赋值到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;
mmap_region(file, addr, len, flags, vm_flags, pgoff,accountable); - 如果file是共享映射

/*
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;
- 遍历mm下的红黑树,查看一下给定地址addr下,是否有满足的vma
- 如果需要映射的是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);
- vm_flags如果是可扩展的,则释放并退出
- 对相关数量进行统计
- 根据长度,即本进程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));//通过页表建立物理地址和虚拟地址之间的关联关系 - 取消vma和pte之间的映射关系&&将addr和pte重新建立映射关系
- 通过缺页方式,从底层将数据换回
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类型进行检查
- 如果是匿名页,则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),它们很可能还要再次被访问啊,不能直接回收,这些内存自然不能直接释放 |



浙公网安备 33010602011771号