0

0

Linux黑科技|mmap实现详解

心靈之曲

心靈之曲

发布时间:2025-01-03 08:32:48

|

822人浏览过

|

来源于良许Linux教程网

转载

故事的开始是这样的,某天在脉脉上看到有人发了下面的帖子:

Linux黑科技|mmap实现详解

mmap 原理

在之前的文章中,我们也介绍过 mmap 的原理,比如这篇:《原来 mmap 这么简单》。当然这篇文章只是简单介绍了 mmap 的原理,但是 mmap 的实现远不止那么简单,这是因为 mmap 涉及多个子系统,如:内存管理、文件系统、中断处理等。

好消息是,这几个子系统我们都有对应的文章介绍过:

  • 内存管理:《Linux虚拟内存空间管理》
  • 文件系统:《 什么是页缓存》
  • 中断处理:《Linux中断处理》

在阅读本文前,最好复习一下上面的文章。

虽然在《原来 mmap 这么简单》一文中,我们简单介绍过 mmap 的原理。但为了方便分析源码,下面还是简单回顾一下 mmap 的原理吧。

mmap 的全称是 memory map,中文意思是 内存映射。其用途是将文件映射到内存中,然后可以通过对映射区的内存进行读写操作,其效果等同于对文件进行读写操作。

下面我们通过一幅图来对 mmap 的原理进行阐述:

Linux黑科技|mmap实现详解

从上图可以看出,mmap 的原理就是将虚拟内存空间映射到文件的页缓存,在《什么是页缓存》一文中可知,对文件进行读写时需要经过页缓存进行中转的。所以当虚拟内存地址映射到文件的页缓存后,就可以直接通过读写映射区内存来对文件进行读写操作。

mmap 实现

在分析 mmap 的实现前,最好先了解其使用方式,mmap 的使用可以参考《原来 mmap 这么简单》这篇文章。

1. 文件映射

当我们使用 mmap() 系统调用对文件进行映射时,将会触发调用 do_mmap_pgoff() 内核函数来完成工作,我们来看看 do_mmap_pgoff() 函数的实现(经过精简后):

unsigned long
do_mmap_pgoff(struct file *file, unsigned long addr, 
              unsigned long len, unsigned long prot, 
              unsigned long flags, unsigned long pgoff)
{
    ...
    // 1. 获取一个未被使用的虚拟内存区
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (addr & ~PAGE_MASK)
        return addr;

    ...
    // 2. 调用 mmap_region() 函数继续进行映射操作
    return mmap_region(file, addr, len, flags, vm_flags, pgoff, accountable);
}

经过精简后的 do_mmap_pgoff() 函数主要完成 2 个工作:

  • 首先,调用 get_unmapped_area() 函数来获取进程没被使用的虚拟内存区,并且返回此内存区的首地址。
  • 然后,调用 mmap_region() 函数继续进行映射操作。

 

在 32 位的操作系统中,每个进程都有 4GB 的虚拟内存空间,应用程序在使用内存前,需要先向操作系统发起申请内存的操作。操作系统会从进程的虚拟内存空间中查找未被使用的内存地址,并且返回给应用程序。

操作系统会记录进程正在使用中的虚拟内存地址,如果内存地址没被登记,说明此内存地址是空闲的(未被使用)。

我们继续来看看 mmap_region() 函数的实现,代码如下(经过精简后):

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)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev;
    int correct_wcount = 0;
    int error;
    ...

    // 1. 申请一个虚拟内存区管理结构(vma)
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    ...

    // 2. 设置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 & (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)];
    vma->vm_pgoff = pgoff;

    if (file) {
        ...
        vma->vm_file = file;

        /* 3. 此处是内存映射的关键点,调用文件对象的 mmap() 回调函数来设置vma结构的 fault() 回调函数。
         *    vma对象的 fault() 回调函数的作用是:
         *        - 当访问的虚拟内存没有映射到物理内存时,
         *        - 将会调用 fault() 回调函数对虚拟内存地址映射到物理内存地址。
         */
        error = file->f_op->mmap(file, vma);
        ...
    }
    ...

    // 4. 把 vma 结构连接到进程虚拟内存区的链表和红黑树中。
    vma_link(mm, vma, prev, rb_link, rb_parent);
    ...

    return addr;
}

mmap_region() 函数主要完成以下 4 件事情:

  • 申请一个 vm_area_struct 结构(vma),内核使用 vma 来管理进程的虚拟内存地址,关于 vma 的详细介绍可以参考:《Linux虚拟内存空间管理》。
  • 设置 vma 结构各个字段的值。
  • 通过调用文件对象的 mmap() 回调函数来设置vma结构的 fault() 回调函数,一般文件对象的 mmap() 回调函数为:generic_file_mmap()
  • 把新创建的 vma 结构连接到进程的虚拟内存区链表和红黑树中。

内核使用 vm_area_struct 结构来管理进程的虚拟内存地址。当进程需要使用内存时,首先要向操作系统进行申请,操作系统会使用 vm_area_struct 结构来记录被分配出去的内存区的大小、起始地址和权限等。

我们来看看 vm_area_struct 结构的定义:

struct vm_area_struct {
    struct mm_struct *vm_mm;
    unsigned long vm_start;              // 内存区的开始地址
    unsigned long vm_end;                // 内存区的结束地址
    struct vm_area_struct *vm_next;      // 把进程所有已分配的内存区链接起来
    pgprot_t vm_page_prot;               // 内存区的权限
    ...
    struct rb_node vm_rb;                // 为了加快查找内存区而建立的红黑树
    ...
    struct vm_operations_struct *vm_ops; // 内存区的操作回调函数集

    unsigned long vm_pgoff;
    struct file *vm_file;                // 如果映射到文件,将指向映射的文件对象
    ...
};

struct vm_operations_struct {
    // 当虚拟内存区没有映射到物理内存地址时,将会触发缺页异常,
    // 而在缺页异常处理函数中,将会调用此回调函数来对虚拟内存映射到物理内存。
    int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
    ...
};

当把文件映射到虚拟内存空间时,需要把 vma 结构的 vm_file 字段设置为要映射的文件对象,然后调用文件对象的 mmap() 回调函数来设置 vma 结构的 fault() 回调函数。

扣子编程
扣子编程

扣子推出的AI编程开发工具

下载

 

vma 结构的 fault() 回调函数的作用是:当虚拟内存区没有映射到物理内存地址时,将会触发缺页异常。而在缺页异常处理中,将会调用此回调函数来对虚拟内存映射到物理内存。

我们来看看 generic_file_mmap() 函数是怎么设置 vma 结构的 fault() 回调函数的:

struct vm_operations_struct generic_file_vm_ops = {
    .fault = filemap_fault, // 将 fault() 回调函数设置为:filemap_fault()
};

int generic_file_mmap(struct file *file, struct vm_area_struct *vma)
{
    ...
    vma->vm_ops = &generic_file_vm_ops;
    ...
    return 0;
}

至此,文件映射的过程已经分析完毕。我们来看看其调用链:

sys_mmap()
└→ do_mmap_pgoff()
   └→ mmap_region()
      └→ generic_file_mmap()

2. 缺页异常

前面介绍了 mmap() 系统调用的处理过程,可以发现 mmap() 只是将 vmavm_file 字段设置为被映射的文件对象,并且将 vmafault() 回调函数设置为 filemap_fault()。也就是说,mmap() 系统调用并没有对虚拟内存进行任何的映射操作。

我们在《漫画解说 “内存映射”》一文中介绍过,虚拟内存必须映射到物理内存才能使用。如果访问没有映射到物理内存的虚拟内存地址,CPU 将会触发缺页异常。也就是说,虚拟内存并不能直接映射到磁盘中的文件。

那么 mmap() 是怎么将文件映射到虚拟内存中呢?我们在《 什么是页缓存》一文中介绍过,读写文件时并不是直接对磁盘上的文件进行操作的,而是通过 页缓存 作为中转的,而页缓存就是物理内存中的内存页。所以,mmap() 可以通过将文件的页缓存映射到虚拟内存空间来实现对文件的映射。

但我们在 mmap() 系统调用的实现中,也没看到将文件页缓存映射到虚拟内存空间。那么映射过程是在什么时候发生的呢?

 

答案就是:缺页异常

由于 mmap() 系统调用并没有直接将文件的页缓存映射到虚拟内存中,所以当访问到没有映射的虚拟内存地址时,将会触发 缺页异常。当 CPU 触发缺页异常时,将会调用 do_page_fault() 函数来修复触发异常的虚拟内存地址。

我们主要来看看 do_page_fault() 函数对文件映射的实现部分,其调用链如下:

do_page_fault()
└→ handle_mm_fault()
   └→ handle_pte_fault()
      └→ do_linear_fault()
         └→ __do_fault()

所以我们直接来看看 __do_fault() 函数的实现:

static int
__do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
           unsigned long address, pmd_t *pmd, pgoff_t pgoff,
           unsigned int flags, pte_t orig_pte)
{
    ...
    vmf.virtual_address = address & PAGE_MASK; // 要映射的虚拟内存地址
    vmf.pgoff = pgoff;                         // 映射到文件的偏移量
    vmf.flags = flags;                         // 标志位
    vmf.page = NULL;                           // 映射到虚拟内存中的物理内存页

    // 1. 如果虚拟内存管理区提供了 falut() 回调函数,那么将调用此函数来获取要映射的物理内存页,
    //    我们在 mmap() 系统调用的实现中看到,已经将其设置为 filemap_fault() 函数了。
    if (likely(vma->vm_ops->fault)) {
        ret = vma->vm_ops->fault(vma, &vmf);
        ...
    }
    ...

    if (likely(pte_same(*page_table, orig_pte))) {
        ...
        // 2. 通过物理内存页生成一个页表项值(可以参考内存映射一文)
        entry = mk_pte(page, vma->vm_page_prot);
        if (flags & FAULT_FLAG_WRITE)
            entry = maybe_mkwrite(pte_mkdirty(entry), vma);

        // 3. 将虚拟内存地址映射到物理内存(也就是将进程的页表项设置为刚生成的页表项的值)
        set_pte_at(mm, address, page_table, entry);
        ...
    }
    ...

    return ret;
}

__do_fault() 函数对处理文件映射部分主要分为 3 个步骤:

  • 调用虚拟内存管理区结构(vma)的 fault() 回调函数(也就是 filemap_fault() 函数)来获取到文件的页缓存。
  • 通过页缓存的物理内存页来生成一个页表项值,可以参考《漫画解说 “内存映射”》一文。
  • 将虚拟内存地址映射到页缓存的物理内存页(也就是将进程的页表项设置为上面生成的页表项的值)。

对于 filemap_fault() 函数是怎样读取文件页缓存的,本文不作解释,有兴趣的可以自行阅读源码。

最后,我们以一幅图来描述一下虚拟内存是如何与文件进行映射的:

从上图可以看出,mmap() 是通过将虚拟内存地址映射到文件的页缓存来实现的。当对映射后的虚拟内存进行读写操作时,其效果等价于直接对文件的页缓存进行读写操作。对文件的页缓存进行读写操作,也等价于对文件进行读写操作。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
golang map内存释放
golang map内存释放

本专题整合了golang map内存相关教程,阅读专题下面的文章了解更多相关内容。

75

2025.09.05

golang map相关教程
golang map相关教程

本专题整合了golang map相关教程,阅读专题下面的文章了解更多详细内容。

36

2025.11.16

golang map原理
golang map原理

本专题整合了golang map相关内容,阅读专题下面的文章了解更多详细内容。

60

2025.11.17

java判断map相关教程
java判断map相关教程

本专题整合了java判断map相关教程,阅读专题下面的文章了解更多详细内容。

41

2025.11.27

磁盘配额是什么
磁盘配额是什么

磁盘配额是计算机中指定磁盘的储存限制,就是管理员可以为用户所能使用的磁盘空间进行配额限制,每一用户只能使用最大配额范围内的磁盘空间。php中文网为大家提供各种磁盘配额相关的内容,教程,供大家免费下载安装。

1394

2023.06.21

如何安装LINUX
如何安装LINUX

本站专题提供如何安装LINUX的相关教程文章,还有相关的下载、课程,大家可以免费体验。

706

2023.06.29

linux find
linux find

find是linux命令,它将档案系统内符合 expression 的档案列出来。可以指要档案的名称、类别、时间、大小、权限等不同资讯的组合,只有完全相符的才会被列出来。find根据下列规则判断 path 和 expression,在命令列上第一个 - ( ) , ! 之前的部分为 path,之后的是 expression。还有指DOS 命令 find,Excel 函数 find等。本站专题提供linux find相关教程文章,还有相关

295

2023.06.30

linux修改文件名
linux修改文件名

本专题为大家提供linux修改文件名相关的文章,这些文章可以帮助用户快速轻松地完成文件名的修改工作,大家可以免费体验。

779

2023.07.05

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

158

2026.01.28

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 7.9万人学习

Git 教程
Git 教程

共21课时 | 3.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号