Linux黑科技|mmap实现详解(详解.科技.Linux.mmap...........)

wufei123 2025-01-05 阅读:72 评论:0
故事的开始是这样的,某天在脉脉上看到有人发了下面的帖子: mmap 原理 在之前的文章中,我们也介绍过 mmap 的原理,比如这篇:《原来 mmap 这么简单》。当然这篇文章只是简单介绍了 mmap 的原理,但是 mmap 的实现远不止那么...

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

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() 函数的实现(经过精简后):

JavaScript
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() 函数的实现,代码如下(经过精简后):

JavaScript
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 结构的定义:

JavaScript
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() 回调函数。

 

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

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

JavaScript
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;
}

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

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

2. 缺页异常

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

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

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

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

 

答案就是:缺页异常。

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

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

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

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

JavaScript
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() 是通过将虚拟内存地址映射到文件的页缓存来实现的。当对映射后的虚拟内存进行读写操作时,其效果等价于直接对文件的页缓存进行读写操作。对文件的页缓存进行读写操作,也等价于对文件进行读写操作。

以上就是Linux黑科技|mmap实现详解的详细内容,更多请关注知识资源分享宝库其它相关文章!

版权声明

本站内容来源于互联网搬运,
仅限用于小范围内传播学习,请在下载后24小时内删除,
如果有侵权内容、不妥之处,请第一时间联系我们删除。敬请谅解!
E-mail:dpw1001@163.com

分享:

扫一扫在手机阅读、分享本文

发表评论
热门文章
  • BioWare埃德蒙顿工作室面临关闭危机,龙腾世纪制作总监辞职引关注(龙腾.总监.辞职.危机.面临.....)

    BioWare埃德蒙顿工作室面临关闭危机,龙腾世纪制作总监辞职引关注(龙腾.总监.辞职.危机.面临.....)
    知名变性人制作总监corrine busche离职bioware,引发业界震荡!外媒“smash jt”独家报道称,《龙腾世纪:影幢守护者》制作总监corrine busche已离开bioware,此举不仅引发了关于个人职业发展方向的讨论,更因其可能预示着bioware埃德蒙顿工作室即将关闭而备受关注。本文将深入分析busche离职的原因及其对bioware及游戏行业的影响。 Busche的告别信:挑战与感激并存 据“Smash JT”获得的内部邮件显示,Busche离职原...
  • 闪耀暖暖靡城永恒怎么样-闪耀暖暖靡城永恒套装介绍(闪耀.暖暖.套装.介绍.....)

    闪耀暖暖靡城永恒怎么样-闪耀暖暖靡城永恒套装介绍(闪耀.暖暖.套装.介绍.....)
    闪耀暖暖钻石竞技场第十七赛季“华梦泡影”即将开启!全新闪耀性感套装【靡城永恒】震撼来袭!想知道如何获得这套精美套装吗?快来看看吧! 【靡城永恒】套装设计理念抢先看: 设计灵感源于夜色中的孤星,象征着淡然、漠视一切的灰色瞳眸。设计师希望通过这套服装,展现出在虚幻与真实交织的夜幕下,一种独特的魅力。 服装细节考究,从面料的光泽、鞋跟声响到裙摆的弧度,都力求完美还原设计初衷。 【靡城永恒】套装设计亮点: 闪耀的绸缎与金丝交织,轻盈的羽毛增添华贵感。 这套服装仿佛是从无尽的黑...
  • boss直聘怎么取消面试预约 boss直聘上面试爽约了会怎么样(面试.爽约.预约.取消.boss.....)

    boss直聘怎么取消面试预约 boss直聘上面试爽约了会怎么样(面试.爽约.预约.取消.boss.....)
    求职宝典:boss直聘面试技巧及取消预约方法 各位求职者注意啦!在Boss直聘上,随意取消面试预约会留下爽约记录,影响后续求职!本文将指导您如何避免爽约,以及如何取消已预约的面试。 如何取消Boss直聘面试预约? 打开Boss直聘APP,进入“我的”页面。 点击“待面试”,查看面试日程。 选择需要取消的面试,点击“取消面试”按钮即可。 Boss直聘面试爽约的后果? 爽约行为会在HR端留下记录,影响您的求职成功率。其他HR也能看到您的不良记录,所以务必重视面试预约。...
  • 蛋仔派对2025最新皮肤兑换码汇总 最新皮肤兑换码一览(兑换.皮肤.最新.派对.汇总.....)

    蛋仔派对2025最新皮肤兑换码汇总 最新皮肤兑换码一览(兑换.皮肤.最新.派对.汇总.....)
    蛋仔派对2025最新皮肤兑换码大放送!游戏内新增多款皮肤兑换码,包含最新、福利和通用三种类型,助你轻松获取精美奖励! 赶紧来看看如何兑换吧! 兑换码列表: 最新兑换码: ccewndj4k4k、cdkqdfm4fh、peetnmp4ef、cdxymk8f67 福利兑换码: cca863ywtfa、eggy2310am、eggy2311gz、eggyeggy9wz 通用兑换码: pec74dkcty、jsrqkrrjmh、cd3wt7wrph、ccepn7d8cjf...
  • python怎么调用其他文件函数

    python怎么调用其他文件函数
    在 python 中调用其他文件中的函数,有两种方式:1. 使用 import 语句导入模块,然后调用 [模块名].[函数名]();2. 使用 from ... import 语句从模块导入特定函数,然后调用 [函数名]()。 如何在 Python 中调用其他文件中的函数 在 Python 中,您可以通过以下两种方式调用其他文件中的函数: 1. 使用 import 语句 优点:简单且易于使用。 缺点:会将整个模块导入到当前作用域中,可能会导致命名空间混乱。 步骤:...