Linux 内核中 mmap 的实现(基于 2.6.12)
概述
基于 2.6.12 内核, 说明 mmap 系统调用的核心数据结构、系统调用路径及关键实现. 主要文件: mm/mmap.c、mm/msync.c、mm/filemap.c、include/linux/mm.h、include/linux/mman.h.
核心数据结构
mm_struct (进程地址空间描述符)
// include/linux/mm_types.h
struct mm_struct {
struct vm_area_struct *mmap; // VMA 链表头
struct rb_root mm_rb; // VMA 红黑树根节点
struct vm_area_struct *mmap_cache; // 最近使用的 VMA 缓存
unsigned long free_area_cache; // 空闲区域搜索的起始地址
unsigned long mmap_base; // mmap 区域的基地址
unsigned long total_vm; // 总虚拟内存页数
unsigned long locked_vm; // 锁定的内存页数
struct rw_semaphore mmap_sem; // 保护地址空间的读写信号量
// ... 其他字段
};
vm_area_struct (虚拟内存区域)
// include/linux/mm_types.h
struct vm_area_struct {
struct mm_struct *vm_mm; // 所属的地址空间
unsigned long vm_start; // 虚拟地址区间起始地址
unsigned long vm_end; // 虚拟地址区间结束地址(不包含)
struct vm_area_struct *vm_next; // 链表中的下一个 VMA
struct rb_node vm_rb; // 红黑树节点
unsigned long vm_flags; // 权限与属性标志
pgprot_t vm_page_prot; // 页保护属性
pgoff_t vm_pgoff; // 文件页偏移(文件映射时使用)
struct file *vm_file; // 关联的文件对象(文件映射时使用)
void *vm_private_data; // 私有数据
struct vm_operations_struct *vm_ops; // VMA 操作函数指针
// ... 其他字段
};
file (文件对象)
// include/linux/fs.h
struct file {
struct dentry *f_dentry; // 目录项
struct vfsmount *f_vfsmnt; // 文件系统挂载点
struct file_operations *f_op; // 文件操作函数指针
struct address_space *f_mapping; // 地址空间(用于页缓存)
fmode_t f_mode; // 文件打开模式
// ... 其他字段
};
系统调用路径(x86_64 类推)
sys_mmap/sys_mmap2(架构层入口) →do_mmap_pgoff(核心实现)sys_munmap→do_munmap(解除映射)sys_msync→msync_interval(同步映射区域到文件)sys_mprotect→do_mprotect(修改保护属性)
核心流程
创建映射(mmap)
流程概述:
- 参数校验与页对齐: 检查长度、偏移、权限标志
- 选择/验证地址: 非 MAP_FIXED 由
get_unmapped_area选择合适空洞, MAP_FIXED 必须满足对齐且不与现有 VMA 冲突 - 权限检查与文件映射: 共享映射需要写权限匹配, 文件映射调用
file->f_op->mmap建立关联 - 建立 VMA: 分配
vm_area_struct, 设置vm_flags、vm_pgoff、vm_file, 插入mm_struct的 VMA 红黑树和链表 - 返回映射起始地址
sys_mmap2 系统调用入口
// arch/x86_64/kernel/sys_x86_64.c (简化示意)
asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff)
{
struct file *file = NULL;
unsigned long error;
// 如果不是匿名映射, 获取文件指针
if (!(flags & MAP_ANONYMOUS)) {
file = fget(fd);
if (!file)
return -EBADF;
}
// 获取写锁保护地址空间操作
down_write(¤t->mm->mmap_sem);
// do_mmap_pgoff 完成核心逻辑
error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);
up_write(¤t->mm->mmap_sem);
if (file)
fput(file);
return error;
}
do_mmap_pgoff 核心实现
// mm/mmap.c
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 vm_area_struct *vma, *prev;
struct inode *inode;
unsigned int vm_flags;
int correct_wcount = 0;
int error;
struct rb_node **rb_link, *rb_parent;
int accountable = 1;
unsigned long charged = 0, reqprot = prot;
// 1. 文件映射的初步检查
if (file) {
// 检查文件是否支持 mmap 操作
if (!file->f_op || !file->f_op->mmap)
return -ENODEV;
// 检查执行权限与文件系统挂载标志
if ((prot & PROT_EXEC) &&
(file->f_vfsmnt->mnt_flags & MNT_NOEXEC))
return -EPERM;
}
// 2. 处理 READ_IMPLIES_EXEC 个性标志
// 某些架构/程序期望 PROT_READ 隐含 PROT_EXEC
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && (file->f_vfsmnt->mnt_flags & MNT_NOEXEC)))
prot |= PROT_EXEC;
// 3. 参数校验: 长度不能为 0
if (!len)
return -EINVAL;
// 4. 长度页对齐并检查溢出
len = PAGE_ALIGN(len);
if (!len || len > TASK_SIZE)
return -ENOMEM;
// 5. 检查文件偏移溢出
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EOVERFLOW;
// 6. 检查 VMA 数量限制
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;
// 7. 获取或选择映射地址
// 非 MAP_FIXED 时由 get_unmapped_area 选择合适地址
addr = get_unmapped_area(file, addr, len, pgoff, flags);
if (addr & ~PAGE_MASK)
return addr; // 返回错误码
// 8. 计算 VMA 标志位
// 将用户空间的 prot 和 flags 转换为内核的 vm_flags
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
// 9. 处理 MAP_LOCKED 标志(锁定内存)
if (flags & MAP_LOCKED) {
if (!can_do_mlock())
return -EPERM;
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;
}
inode = file ? file->f_dentry->d_inode : NULL;
// 10. 根据映射类型设置 VMA 标志
if (file) {
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))
return -EACCES;
// 检查是否有强制锁
if (locks_verify_locked(inode))
return -EAGAIN;
vm_flags |= VM_SHARED | VM_MAYSHARE;
// 如果文件不可写, 清除写权限标志
if (!(file->f_mode & FMODE_WRITE))
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
break;
case MAP_PRIVATE:
// 私有映射: 至少需要读权限
if (!(file->f_mode & FMODE_READ))
return -EACCES;
break;
default:
return -EINVAL;
}
} else {
// 匿名映射
switch (flags & MAP_TYPE) {
case MAP_SHARED:
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
// 设置 pgoff 为虚拟地址对应的页号
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}
// 11. LSM (Linux Security Module) 安全检查
error = security_file_mmap(file, reqprot, prot, flags);
if (error)
return error;
// 12. 清除可能冲突的旧映射
error = -ENOMEM;
munmap_back:
vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
if (vma && vma->vm_start < addr + len) {
// 如果新映射与现有 VMA 冲突, 先解除旧映射
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
// 13. 检查地址空间限制
if (!may_expand_vm(mm, len >> PAGE_SHIFT))
return -ENOMEM;
// 14. 内存记账(accounting)
if (accountable && (!(flags & MAP_NORESERVE) ||
sysctl_overcommit_memory == OVERCOMMIT_NEVER)) {
if (vm_flags & VM_SHARED) {
// 共享映射需要记账
vm_flags |= VM_ACCOUNT;
} else if (vm_flags & VM_WRITE) {
// 私有可写映射需要预留内存
charged = len >> PAGE_SHIFT;
if (security_vm_enough_memory(charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}
}
// 15. 尝试合并相邻的匿名私有映射
if (!file && !(vm_flags & VM_SHARED) &&
vma_merge(mm, prev, addr, addr + len, vm_flags,
NULL, NULL, pgoff, NULL))
goto out;
// 16. 分配 VMA 结构体
vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
memset(vma, 0, sizeof(*vma));
// 17. 初始化 VMA 基本字段
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = protection_map[vm_flags & 0x0f];
vma->vm_pgoff = pgoff;
// 18. 处理文件映射或匿名共享映射
if (file) {
// 处理 MAP_DENYWRITE 标志(禁止其他进程写入文件)
if (vm_flags & VM_DENYWRITE) {
error = deny_write_access(file);
if (error)
goto free_vma;
correct_wcount = 1;
}
vma->vm_file = file;
get_file(file); // 增加文件引用计数
// 调用文件系统的 mmap 方法(通常是 generic_file_mmap)
error = file->f_op->mmap(file, vma);
if (error)
goto unmap_and_free_vma;
} else if (vm_flags & VM_SHARED) {
// 匿名共享映射: 使用 shmem (共享内存文件系统)
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}
// 19. 清理共享映射的 VM_ACCOUNT 标志(由 shmem 负责记账)
if ((vm_flags & (VM_SHARED|VM_ACCOUNT)) == (VM_SHARED|VM_ACCOUNT))
vma->vm_flags &= ~VM_ACCOUNT;
// 20. 保存可能被文件系统 mmap 方法修改的地址和偏移
addr = vma->vm_start;
pgoff = vma->vm_pgoff;
vm_flags = vma->vm_flags;
// 21. 尝试与相邻 VMA 合并, 或插入新的 VMA
if (!file || !vma_merge(mm, prev, addr, vma->vm_end,
vma->vm_flags, NULL, file, pgoff, vma_policy(vma))) {
// 不能合并, 插入新的 VMA
file = vma->vm_file;
vma_link(mm, vma, prev, rb_link, rb_parent);
if (correct_wcount)
atomic_inc(&inode->i_writecount);
} else {
// 可以合并, 释放刚分配的 VMA
if (file) {
if (correct_wcount)
atomic_inc(&inode->i_writecount);
fput(file);
}
mpol_free(vma_policy(vma));
kmem_cache_free(vm_area_cachep, vma);
}
out:
// 22. 更新统计信息
mm->total_vm += len >> PAGE_SHIFT;
__vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
mm->locked_vm += len >> PAGE_SHIFT;
// MAP_LOCKED 时立即分配物理页
make_pages_present(addr, addr + len);
}
// 23. 处理 MAP_POPULATE 标志(预分配页)
if (flags & MAP_POPULATE) {
up_write(&mm->mmap_sem);
sys_remap_file_pages(addr, len, 0, pgoff, flags & MAP_NONBLOCK);
down_write(&mm->mmap_sem);
}
return addr;
unmap_and_free_vma:
// 错误处理: 文件系统 mmap 失败
if (correct_wcount)
atomic_inc(&inode->i_writecount);
vma->vm_file = NULL;
fput(file);
unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
charged = 0;
free_vma:
kmem_cache_free(vm_area_cachep, vma);
unacct_error:
if (charged)
vm_unacct_memory(charged);
return error;
}
get_unmapped_area 地址选择
// mm/mmap.c
unsigned long get_unmapped_area(struct file *file, unsigned long addr,
unsigned long len, unsigned long pgoff,
unsigned long flags)
{
unsigned long ret;
// 非 MAP_FIXED 时选择地址
if (!(flags & MAP_FIXED)) {
unsigned long (*get_area)(struct file *, unsigned long,
unsigned long, unsigned long, unsigned long);
// 优先使用文件系统提供的地址选择函数
get_area = current->mm->get_unmapped_area;
if (file && file->f_op && file->f_op->get_unmapped_area)
get_area = file->f_op->get_unmapped_area;
addr = get_area(file, addr, len, pgoff, flags);
if (IS_ERR_VALUE(addr))
return addr;
}
// 检查地址范围是否有效
if (addr > TASK_SIZE - len)
return -ENOMEM;
// 检查地址是否页对齐
if (addr & ~PAGE_MASK)
return -EINVAL;
// 处理大页映射的特殊检查
if (file && is_file_hugepages(file)) {
ret = prepare_hugepage_range(addr, len);
} else {
ret = is_hugepage_only_range(current->mm, addr, len);
}
if (ret)
return -EINVAL;
return addr;
}
// 默认的地址选择算法(自底向上)
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;
unsigned long start_addr;
if (len > TASK_SIZE)
return -ENOMEM;
// 如果指定了建议地址, 先检查是否可用
if (addr) {
addr = PAGE_ALIGN(addr);
vma = find_vma(mm, addr);
if (TASK_SIZE - len >= addr &&
(!vma || addr + len <= vma->vm_start))
return addr;
}
// 从上次搜索的缓存地址开始
start_addr = addr = mm->free_area_cache;
full_search:
// 遍历 VMA 链表查找空闲区域
for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {
// 检查是否超出地址空间
if (TASK_SIZE - len < addr) {
// 从头开始重新搜索
if (start_addr != TASK_UNMAPPED_BASE) {
start_addr = addr = TASK_UNMAPPED_BASE;
goto full_search;
}
return -ENOMEM;
}
// 找到合适的空洞
if (!vma || addr + len <= vma->vm_start) {
// 更新缓存地址
mm->free_area_cache = addr + len;
return addr;
}
addr = vma->vm_end;
}
}
generic_file_mmap 文件映射设置
// mm/filemap.c
int generic_file_mmap(struct file *file, struct vm_area_struct *vma)
{
struct address_space *mapping = file->f_mapping;
// 检查地址空间是否支持 readpage 操作(用于缺页处理)
if (!mapping->a_ops->readpage)
return -ENOEXEC;
// 更新文件的访问时间
file_accessed(file);
// 设置 VMA 的操作函数指针(包含 fault、populate 等)
vma->vm_ops = &generic_file_vm_ops;
return 0;
}
vma_link 插入 VMA
// mm/mmap.c
static void vma_link(struct mm_struct *mm, struct vm_area_struct *vma,
struct vm_area_struct *prev, struct rb_node **rb_link,
struct rb_node *rb_parent)
{
// 插入到 VMA 链表
__vma_link_list(mm, vma, prev);
// 插入到 VMA 红黑树
__vma_link_rb(mm, vma, rb_link, rb_parent);
// 如果文件映射, 还需要插入到文件的地址空间树
if (vma->vm_file) {
struct address_space *mapping = vma->vm_file->f_mapping;
spin_lock(&mapping->i_mmap_lock);
__vma_link_file(vma);
spin_unlock(&mapping->i_mmap_lock);
}
// 更新 VMA 计数
mm->map_count++;
}
解除映射(munmap)
流程概述:
- 对齐地址与长度: 地址必须页对齐, 长度页对齐
- 查找覆盖的 VMA: 找到与解除区域重叠的所有 VMA
- 拆分 VMA: 如果解除区域只覆盖 VMA 的一部分, 需要拆分
- 移除 VMA: 从链表、红黑树、文件地址空间树中移除
- 释放页表和资源: 调用
unmap_region释放页表项和物理页
sys_munmap 系统调用入口
// mm/mmap.c
asmlinkage long sys_munmap(unsigned long addr, size_t len)
{
int ret;
struct mm_struct *mm = current->mm;
profile_munmap(addr);
// 获取写锁保护地址空间操作
down_write(&mm->mmap_sem);
ret = do_munmap(mm, addr, len);
up_write(&mm->mmap_sem);
return ret;
}
do_munmap 核心实现
// mm/mmap.c
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
{
unsigned long end;
struct vm_area_struct *vma, *prev, *last;
// 1. 参数校验: 地址必须页对齐, 范围必须有效
if ((start & ~PAGE_MASK) || start > TASK_SIZE || len > TASK_SIZE - start)
return -EINVAL;
// 2. 长度页对齐
if ((len = PAGE_ALIGN(len)) == 0)
return -EINVAL;
// 3. 查找第一个重叠的 VMA
vma = find_vma_prev(mm, start, &prev);
if (!vma)
return 0; // 没有重叠的 VMA, 直接返回成功
// 此时 start < vma->vm_end
// 4. 检查是否真的重叠
end = start + len;
if (vma->vm_start >= end)
return 0; // 不重叠
// 5. 如果解除区域不是从 VMA 起始处开始, 需要拆分 VMA
if (start > vma->vm_start) {
int error = split_vma(mm, vma, start, 0);
if (error)
return error;
prev = vma; // 拆分后 prev 指向前半部分
}
// 6. 检查是否需要拆分最后一个 VMA
last = find_vma(mm, end);
if (last && end > last->vm_start) {
int error = split_vma(mm, last, end, 1);
if (error)
return error;
}
// 重新定位到要解除的第一个 VMA
vma = prev ? prev->vm_next : mm->mmap;
// 7. 从链表和红黑树中分离要解除的 VMA
detach_vmas_to_be_unmapped(mm, vma, prev, end);
// 8. 解除页表映射并释放物理页
unmap_region(mm, vma, prev, start, end);
// 9. 释放 VMA 结构体和相关资源
unmap_vma_list(mm, vma);
return 0;
}
同步映射区域(msync)
流程概述:
- 参数校验: 地址页对齐, 标志位检查
- 查找覆盖的 VMA: 遍历所有与同步区域重叠的 VMA
- 同步脏页: 对于共享映射, 将脏页写回文件
- 文件系统同步: MS_SYNC 时调用文件系统的 fsync 方法
sys_msync 系统调用入口
// mm/msync.c
asmlinkage long sys_msync(unsigned long start, size_t len, int flags)
{
unsigned long end;
struct vm_area_struct *vma;
int unmapped_error, error = -EINVAL;
// 1. 设置同步写标志(用于 I/O 调度)
if (flags & MS_SYNC)
current->flags |= PF_SYNCWRITE;
down_read(¤t->mm->mmap_sem);
// 2. 参数校验: 标志位检查
if (flags & ~(MS_ASYNC | MS_INVALIDATE | MS_SYNC))
goto out;
// 地址必须页对齐
if (start & ~PAGE_MASK)
goto out;
// MS_ASYNC 和 MS_SYNC 不能同时设置
if ((flags & MS_ASYNC) && (flags & MS_SYNC))
goto out;
// 3. 长度页对齐并检查溢出
error = -ENOMEM;
len = (len + ~PAGE_MASK) & PAGE_MASK;
end = start + len;
if (end < start)
goto out;
error = 0;
if (end == start)
goto out;
// 4. 查找第一个覆盖的 VMA
vma = find_vma(current->mm, start);
unmapped_error = 0;
// 5. 遍历所有覆盖的 VMA 进行同步
for (;;) {
error = -ENOMEM;
if (!vma)
goto out;
// 处理未映射的区域
if (start < vma->vm_start) {
unmapped_error = -ENOMEM;
start = vma->vm_start;
}
// 同步当前 VMA 覆盖的部分
if (end <= vma->vm_end) {
if (start < end) {
error = msync_interval(vma, start, end, flags);
if (error)
goto out;
}
error = unmapped_error;
goto out;
}
// 同步到当前 VMA 的结束
error = msync_interval(vma, start, vma->vm_end, flags);
if (error)
goto out;
start = vma->vm_end;
vma = vma->vm_next;
}
out:
up_read(¤t->mm->mmap_sem);
current->flags &= ~PF_SYNCWRITE;
return error;
}
msync_interval 同步区间
// mm/msync.c
static int msync_interval(struct vm_area_struct *vma,
unsigned long addr, unsigned long end, int flags)
{
int ret = 0;
struct file *file = vma->vm_file;
// 1. MS_INVALIDATE 与 VM_LOCKED 冲突
if ((flags & MS_INVALIDATE) && (vma->vm_flags & VM_LOCKED))
return -EBUSY;
// 2. 只处理共享文件映射
if (file && (vma->vm_flags & VM_SHARED)) {
// 同步页表中的脏页到页缓存
filemap_sync(vma, addr, end);
// 3. MS_SYNC 时同步到磁盘
if (flags & MS_SYNC) {
struct address_space *mapping = file->f_mapping;
int err;
// 将页缓存中的脏页写回磁盘
ret = filemap_fdatawrite(mapping);
// 调用文件系统的 fsync 方法
if (file->f_op && file->f_op->fsync) {
err = file->f_op->fsync(file, file->f_dentry, 1);
if (err && !ret)
ret = err;
}
// 等待写操作完成
err = filemap_fdatawait(mapping);
if (!ret)
ret = err;
}
}
return ret;
}
保护属性变更(mprotect)
流程概述:
- 对齐区间: 地址和长度页对齐
- 遍历相关 VMA: 找到所有需要修改的 VMA
- 校验新权限: 检查新权限是否合法(如文件不可写时不能设置写权限)
- 拆分 VMA: 如果只修改部分 VMA, 需要拆分
- 更新 vm_flags: 修改 VMA 标志位和页保护属性
- 刷新 TLB: 使页表缓存失效
关键限制(2.6.12 默认)
vm.max_map_count: 每个进程最大 VMA 数量(/proc/sys/vm/max_map_count)RLIMIT_AS: 进程虚拟地址空间限制RLIMIT_MEMLOCK: 进程可锁定内存限制TASK_SIZE: 用户空间地址空间大小(架构相关)
等待与唤醒
- mmap 本身不涉及等待队列, 但缺页时会在
handle_mm_fault中可能因 I/O 阻塞 - MAP_LOCKED/SHM_LOCK 等锁定内存场景, 会受内存限额和锁定限制影响
- msync 的 MS_SYNC 模式会等待 I/O 完成
文件与路径
mm/mmap.c: mmap/munmap 系统调用实现、VMA 管理mm/msync.c: msync 系统调用实现mm/mprotect.c: mprotect 系统调用实现mm/filemap.c: 文件映射相关操作(generic_file_mmap等)include/linux/mm.h: 内存管理相关结构体和函数声明include/linux/mman.h: mmap 相关常量定义
小结
- mmap 的核心在于
do_mmap_pgoff: 地址选择、VMA 构建、文件/匿名映射、插入 VMA 结构 - munmap 通过
do_munmap实现: 查找重叠 VMA、拆分、移除、释放页表 - msync 通过
msync_interval实现: 同步共享映射的脏页到文件 - 2.6.12 不支持后续特性(如 memfd, userfaultfd, MAP_POPULATE 扩展行为等), 描述保持 2.6.12 视角

577

被折叠的 条评论
为什么被折叠?



