Linux内核 mmap内存映射的实现原理

news2025/6/20 2:37:13

在Linux内核以及Linux系统编程的时候,经常会碰到mmap内存映射,mmap函数是实现高性能编程的一个关键点。本文详细介绍一下mmap实现原理。

虚拟地址映射物理地址

虚拟地址映射物理地址采用的是页表机制,64位CPU采用的是4级页表。 64位CPU虚拟地址长度为64位,但实际只用48位就已满足虚拟地址映射物理内存的要求,如下图:

在这里插入图片描述
用户空间和内核空间共256T,2的48次方刚好为256T,所以48位地址空间能映射所有的虚拟地址。

48位虚拟地址由五部分组成:

  • pgd表偏移,四级表,9位
  • pud表偏移,三级表,9位
  • pmd表偏移,二级表,9位
  • ptl表偏移,一级表,9位
  • 物理页偏移,12位

pgd,pud,pmd,ptl表实现原理都相同,我们以pgd来讲解。

一张pgd表对应一个物理页,一个物理页的大小为4KB,一个pgd_t表项为8个字节,一张pgd表能存储4*1024/8=512个表项。

2的9次方等于512,所以采用9位的表偏移就能索引整张表的表项。

在这里插入图片描述
虚拟地址映射物理地址需要依次索引pgd,pud,pmd,ptl表,具体过程如下:

  • 查询pgd表:查询pgd表,首先需要找到pgd表物理页首地址,pgd表物理页首地址由task_struct->mm_struct->pgd成员保存,每个进程的task_struct->mm_struct->pgd成员数值不同,所以不同的进程即使使用相同的虚拟地址也不会访问相同到物理地址。通过pgd表首地址+虚拟地址pgd表偏移索引到pgd_t表项完成pgd表查询。

  • 查询pud表:pgd_t表项存储的是pud表物理页首地址。通过pud表首地址+虚拟地址pud表偏移索引到pud_t表项完成pud表查询。

  • 查询pmd表:pud_t表项存储的是pmd表物理页首地址。通过pmd表首地址+虚拟地址pmd表偏移索引到pmd_t表项完成pmd表查询。

  • 查询ptl表:pmd_t表项存储的是ptl表物理页首地址。通过ptl表首地址+虚拟地址ptl表偏移索引到pte表项完成ptl表查询。

  • 步骤5:映射物理地址:pte表项存储的是物理页首地址,pte+虚拟地址物理页偏移就能定位到物理地址。

定位到物理地址后,虚拟地址映射物理地址的过程就已完成。

在这里插入图片描述

mmap实现原理

mmap函数是一种内存映射文件的方法,它可以将一个文件或设备映射到进程的地址空间中,使得进程可以像访问内存一样访问文件或设备。

mmap可以分为:文件映射和匿名映射。

mmap函数主要工作就是创建VMA。

VMA简介

VMA(Virtual Memory Area,虚拟内存区域)是Linux内核中用于管理进程虚拟内存的数据结构。每个进程都有一个VMA链表,用于描述进程的虚拟地址空间的不同区域。

VMA包含了一段连续的虚拟地址空间,它定义了该区域的起始地址、结束地址以及一些属性信息。VMA可以表示进程的代码段、数据段、堆、栈等不同的内存区域。

VMA对应Linux内核struct vm_area_struct对象。

struct vm_area_struct {  
    /* The first cache line has the info for VMA tree walking. */  
    unsigned long vm_start;         /* Our start address within vm_mm. */  
    unsigned long vm_end;           /* The first byte after our end address 
                                           within vm_mm. */  
    /* linked list of VM areas per task, sorted by address */  
    struct vm_area_struct *vm_next, *vm_prev;  
    struct rb_node vm_rb;  
    unsigned long rb_subtree_gap;  
	  
    struct mm_struct *vm_mm;        /* The address space we belong to. */  
    pgprot_t vm_page_prot;          /* Access permissions of this VMA. */  
    unsigned long vm_flags;         /* Flags, see mm.h. */  
    struct {  
        struct rb_node rb;  
        unsigned long rb_subtree_last;  
    } shared;  
  
    struct list_head anon_vma_chain; /* Serialized by mmap_sem & 
                                          * page_table_lock */  
    struct anon_vma *anon_vma;      /* Serialized by page_table_lock */  
  
    /* Function pointers to deal with this struct. */  
    const struct vm_operations_struct *vm_ops;  
  
    /* Information about our backing store: */  
    unsigned long vm_pgoff;         /* Offset (within vm_file) in PAGE_SIZE 
                                           units, *not* PAGE_CACHE_SIZE */  
    struct file * vm_file;          /* File we map to (can be NULL). */  
    void * vm_private_data;         /* was vm_pte (shared mem) */  
    ...  
};  

struct vm_area_struct结构体主要成员如下:

  • vm_start:虚拟内存区域起始地址。

  • vm_end:虚拟内存区域结束地址,vm_end减去vm_start为映射区域长度。

  • vm_page_prot:虚拟内存访问权限,PROT_READ:可读,PROT_WRITE:可写,PROT_EXEC:可执行

  • vm_page_flags:内存映射标志,MAP_SHARED:共享映射,MAP_PRIVATE:私有映射

  • vm_ops:文件映射操作集合,匿名映射为NULL。

  • vm_pgoff:文件映射文件偏移量,匿名映射无效。

  • vm_file:映射文件,匿名映射为NULL。

注意:VMA用于指导虚拟内存映射物理内存,没有VMA指导无法完成虚拟地址和物理地址映射。

其中需要重点关注的是vm_ops变量,它指向的是一组函数指针,定义如下:

struct vm_operations_struct {  
    void (*open)(struct vm_area_struct * area);  
    void (*close)(struct vm_area_struct * area);  
    int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);  
    void (*map_pages)(struct vm_area_struct *vma, struct vm_fault *vmf);  
    int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);  
    int (*pfn_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);  
    int (*access)(struct vm_area_struct *vma, unsigned long addr,  
                  void *buf, int len, int write);  
    const char *(*name)(struct vm_area_struct *vma);  
    struct page *(*find_special_page)(struct vm_area_struct *vma,  
                                          unsigned long addr);  
};  

当进程在申请的内存的时候,linux内核其实只分配一块虚拟内存地址,并没有分配实际的物理内存,相当于操作系统只给进程这一块地址的使用权。只有当程序真正使用这块内存时,会产生一个缺页异常,这时内核去真正为进程分配物理页,并建立对应的页表,从而将虚拟内存和物理内存建立一个映射关系,这样可以做到充分利用到物理内存。

mmap系统调用

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

参数如下:

  • start:映射空间的起始地址,一般设置为 NULL;
  • length:映射空间的长度;
  • prot:内存保护标志,包括PROT_EXEC(可执行)、PROT_READ(可读)、PROT_WRITE(可写)、PROT_NONE(不可访问) ;
  • flags:映射类型,通常用来标记共享内存(MAP_SHARED)、匿名映射(MAP_ANONYMOUS)等。
  • fd:真正要映射的文件描述符;
  • offset:映射文件的偏移量。

一个简单的demo如下:

int main(int argc, char **argv)  
{  
    char *filename = "/tmp/foo.data";  
    struct stat stat;  
    int fd = open(filename, O_RDWR, 0);  
    fstat(fd, &stat);  
    void *bufp = mmap(NULL, stat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);  
    memcpy(bufp, "Linuxdd", 7); 
    munmap(bufp, stat.st_size); 
    close(fd);
    return 0;  
}  

从demo中可以看出,mmap是将一个文件直接映射到进程的地址空间,进程可以像操作内存一样去读写磁盘上的文件内容,而不需要再调用read/write等系统调用。

源码分析

基于3.10.0-514

SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
    unsigned long, prot, unsigned long, flags,
    unsigned long, fd, unsigned long, off)
{
  long error;
  error = -EINVAL;
  if (off & ~PAGE_MASK) //判断off是不是按页対齐的
    goto out;


  error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
out:
  return error;
}

内部直接调用的是sys_mmap_pgoff函数:

SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
    unsigned long, prot, unsigned long, flags,
    unsigned long, fd, unsigned long, pgoff)
{
  struct file *file = NULL;
  unsigned long retval = -EBADF;


  if (!(flags & MAP_ANONYMOUS)) {//有名文件映射
    audit_mmap_fd(fd, flags);
    if (unlikely(flags & MAP_HUGETLB))
      return -EINVAL;
    file = fget(fd);//根据fd得到对应file结构
    if (!file)
      goto out;
    if (is_file_hugepages(file))//如果是hugetlbfs文件系统文件,将文件大小对齐到页面大小
      len = ALIGN(len, huge_page_size(hstate_file(file)));
  } else if (flags & MAP_HUGETLB) {
    struct user_struct *user = NULL;
    struct hstate *hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) &
               SHM_HUGE_MASK);


    if (!hs)
      return -EINVAL;


    len = ALIGN(len, huge_page_size(hs));
    /*
     * VM_NORESERVE is used because the reservations will be
     * taken when vm_ops->mmap() is called
     * A dummy user value is used because we are not locking
     * memory so no accounting is necessary
     */
    file = hugetlb_file_setup(HUGETLB_ANON_FILE, len,
        VM_NORESERVE,
        &user, HUGETLB_ANONHUGE_INODE,
        (flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
    if (IS_ERR(file))
      return PTR_ERR(file);
  }


  flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);


  retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
  if (file)
    fput(file);
out:
  return retval;
}

sys_mmap_pgoff只是做了一些准备,其通过调用位于mm/util.c的vm_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,  
                unsigned long *populate)  
{  
    // 申请一个vm_area_struct结构体  
    struct vm_area_struct *vma;  
      
    // ...  
  
    // 为vma分配内存  
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);  
    if (!vma)  
        goto error_getting_vma;  
  
    // ... 初始化相关  
  
    // 如果是文件映射,给文件添加一个引用计数  
    if (file) {  
        region->vm_file = get_file(file);  
        vma->vm_file = get_file(file);  
    }  
  
    down_write(&nommu_region_sem);  
  
    // ...  
  
    // 真正去做文件映射  
    if (file && vma->vm_flags & VM_SHARED)  
        ret = do_mmap_shared_file(vma);  
    else  
        ret = do_mmap_private(vma, region, len, capabilities);  
  
    // ...  
    // 将vma插入到链表中  
    add_vma_to_mm(current->mm, vma);  
    // ...  
}  

在做文件映射时,如果不是共享的文件,则调用的是do_mmap_private函数,此函数流程如下:

static int do_mmap_private(struct vm_area_struct *vma,  
               struct vm_region *region,  
               unsigned long len,  
               unsigned long capabilities)  
{  
    // ...  
    if (capabilities & NOMMU_MAP_DIRECT) {  
        // 调用文件映射的方法  
        ret = vma->vm_file->f_op->mmap(vma->vm_file, vma);  
        // ...  
    }  
  
    // ...  
}  

此处f_op->mmap指向的是generic_file_mmap:

int generic_file_mmap(struct file * file, struct vm_area_struct * vma)  
{  
    struct address_space *mapping = file->f_mapping;  
  
    if (!mapping->a_ops->readpage)  
        return -ENOEXEC;  
    file_accessed(file);  
    vma->vm_ops = &generic_file_vm_ops;  
    return 0;  
}  

内部就是给前面提到的vm_ops函数指针的集合赋值,generic_file_vm_ops指向的是针对文件操作的一系列函数:

const struct vm_operations_struct generic_file_vm_ops = {  
    .fault      = filemap_fault,  
    .map_pages  = filemap_map_pages,  
    .page_mkwrite   = filemap_page_mkwrite,  
}; 

其中包括缺页处理,映射页,置为可写三个操作;其中缺页异常的处理逻辑如下:

int filemap_fault(struct vm_area_struct *vma, struct vm_fault *vmf)  
{  
    // ...  
      
    // 先判断当前页有没有被cache  
    page = find_get_page(mapping, offset);  
    if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) {  
        // 预读机制,从cache中拿到数据  
       do_async_mmap_readahead(vma, ra, file, page, offset);  
    } else if (!page) {  
        // 未cache到,直接同步读取  
        do_sync_mmap_readahead(vma, ra, file, offset);  
        count_vm_event(PGMAJFAULT);  
        mem_cgroup_count_vm_event(vma->vm_mm, PGMAJFAULT);  
        ret = VM_FAULT_MAJOR;  
retry_find:  
        page = find_get_page(mapping, offset);  
        if (!page)  
            goto no_cached_page;  
    }  
  
    // ...  
  
    // 找到对应页将其赋值给vmf,并返回  
    vmf->page = page;  
    return ret | VM_FAULT_LOCKED;  
    // ...  
}  

总结mmap文件映射过程:

  • 用户在进程中触发mmap操作
  • 内核对参数做基本的校验,并针对映射长度做一些内存对齐
  • 分配vm_area_struct结构,并对其进行初始化; 调用文件系统的mmap映射,将缺页异常等函数指针赋于vm_ops
  • 将新建的vm_area_struct结构插入到mm链表中; 当进程访问这片内存时,引发缺页异常,从而调用filemap_fault
  • 缺页异常查找cache中有无请求的页,如果没有,内核发起请求将数据从磁盘装入内存

与read/write的区别

在这里插入图片描述

用户进程发起read操作,内核会做一些基本的page cache判断,从磁盘中读取数据到kernel buffer中;,然后内核将buffer的数据再拷贝至用户态的user buffer,唤醒用户进程继续执行。

在这里插入图片描述

内核直接将内存暴露给用户态,用户态对内存的修改也直接反映到内核态,少了一次的内核态至用户态的内存拷贝,速度上会有一定的提升。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1955194.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法板子:堆排序——找出数组中前m个最小元素,包括构造小根堆、查询堆中最小值

基础知识: 堆是一棵完全二叉树,除了最后一层外每一层都是满的,并且最后一层如果有右节点必有左节点堆的节点从1开始编号; 如果一个节点的编号是i,那么该节点的左孩子是2i,右孩子是2i1小根堆中,父节点的值小…

医院影像平台源码,C/S体系结构的C#语言PACS系统全套商业源代码

医学学影像临床信息系统具有图像采集、显示、存储、传输和管理等功能,支持DICOM影像设备和非DICOM影像设备,可以识别CT、MR、CR/DR、X光、DSA、B超、NM、SC等设备的图像类型,可对数字影像进行无损压缩和有损压缩处理。C/S体系结构的多媒体数据…

B端系统UI个性化设计:感受定制之美

B端系统UI个性化设计:感受定制之美 引言 艾斯视觉作为ui设计和前端开发从业者,其观点始终认为:在当今竞争激烈的商业环境中,B端(Business-to-Business)系统的设计不再仅仅是功能性的堆砌,而是…

Hakuin:一款自动化SQL盲注(BSQLI)安全检测工具

关于Hakuin Hakuin是一款功能强大的SQL盲注漏洞安全检测工具,该工具专门针对BSQLi设计,可以帮助广大研究人员优化BSQLi测试用例,并以自动化的形式完成针对目标Web应用程序的漏洞扫描与检测任务。 该工具允许用户以轻松高效的形式对目标Web应…

Python | TypeError: ‘function’ object is not subscriptable

Python | TypeError: ‘function’ object is not subscriptable 在Python编程中,遇到“TypeError: ‘function’ object is not subscriptable”这一错误通常意味着你尝试像访问列表、元组、字典或字符串等可订阅(subscriptable)对象那样去…

短视频矩阵源码技术分享

在当今数字媒体时代,短视频已成为吸引观众和传递信息的重要手段。对于开发者而言,掌握短视频矩阵源码技术不仅是提升自身技能的需要,更是把握行业发展趋势的必然选择。本文将深入探讨短视频矩阵源码的关键技术要点及其实现方法,帮…

文心智能体爆肝网文创作

前言 随着人工智能技术的飞速发展,智能体技术正逐渐渗透到我们生活的方方面面。本文将从博主开发的“爆肝网文小说”智能体入手,探讨智能体技术的概念、开发过程以及其在现代生活中的应用和潜力。 1、零代码开发智能体 1.1、什么是文心智能体 文心智…

使用拉链法实现哈希

开散列 开散列又叫链地址法,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合成为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头节点存储在哈希表中。 ​ 通过结…

webpack5

webpack5主要是内部效率的优化对比webpack4,没有太多使用上的改动 基本配置 拆分配置和merge module.exports merge(commonConfig, { /** options **/})启动本地服务 在dev中添加配置 devServer: {static: {directory: distPath,},port: 8089,hot: true,compres…

uniapp开发小程序onShow中使用uni.$on监听未消亡Bug

大家好,我是学点!在一个键盘打碟鼠标卡点即吵闹又寂静无声的办公大厅,我听到来自领导的呼唤,进入领导办公室后领导说调用第三方接口与我们系统业务不匹配,第三方接口调用量严重超标,调用接口费用一直往上涨…

uniapp小程序中富文本内容渲染图片不展示的问题

文章目录 1.从后端请求的数据中图片是这样的2.前端我是用Uview中的u-parse组件3.这样修改去掉富文本中的所有反斜杠4.完美解决 1.从后端请求的数据中图片是这样的 <p><img src\\\"https://zhangsanfengcode.cn:8084/images/2024-06-28a257befe.jpg\\\" alt…

2.5 C#视觉程序开发实例2----图片内存管理 Csharp实现

2.5 C#视觉程序开发实例2----图片内存管理 Csharp实现 1 目标效果视频 mat-buffer 2 VisionManager类&#xff0c;专门用来管理Opencv相关的内存和 工具参数 2.1 定义一个mat_buffers数组 // Mat buffer 用于保存Mat 图片内存 //Mat[0]:register //Mat[1]:cur img //Mat[2-6…

JSP内置对象及作用域

Request 存东西ResponseSession 存东西Application [ SerlvetContext ] 存东西config [ SerlvetConfig ]out/targetpage 不用了解exception <% page contentType"text/html;charsetUTF-8" language"java" %> <html> <head><title>…

从0到1搭建一个组件库

最近我开启了一个新项目&#xff0c;基于echarts进行二次封装&#xff0c;希望能为Vue3项目量身打造一套高效、易用的图表组件库&#xff0c;取名为 v-echarts。 目前雏形已经搭建完成&#xff0c;先把整个搭建过程做一个记录。后续再持续迭代、完善该图表组件库。 v-echarts 文…

微软现在允许开发人员对 Phi-3-mini 和 Phi-3-medium 模型进行微调

今年 4 月&#xff0c;微软首次发布了Phi-3 系列 SLM&#xff08;小型语言模型&#xff09;&#xff0c;该产品具有低成本、低延迟的特性。Phi-3-mini 是一种 3.8B 语言模型&#xff0c;有 4K 和 128K 两种上下文长度。Phi-3-medium 是 14B 语言模型&#xff0c;也有两种上下文…

解决git每次push代码到github都需要输入用户名以及密码

产生原因&#xff1a; 出现以上情况的主要原因在于采用的是 https 方式提交代码&#xff0c; 如果采用的是 ssh 方式只需要在版本库中添加用户的 sha 的key就可以实现提交时无需输入用户名和密码。 解决方法 在终端中使用以下命令删除原先已经建立的http的链接方式&#xff0c…

leetcode-79. 单词搜索

题目描述 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成&#xff0c;其中“相邻”单元格是那些水平相…

基于JSP、java、Tomcat三者的项目实战--校园交易网(3)主页

前文功能的实现 技术支持&#xff1a;JAVA、JSP 服务器&#xff1a;TOMCAT 7.0.86 编程软件&#xff1a;IntelliJ IDEA 2021.1.3 x64 前文三篇登录和注册功能的实现 基于JSP、java、Tomcat、mysql三层交互的项目实战--校园交易网&#xff08;1&#xff09;-项目搭建&#xf…

go 协程池的实现

使用场景 这次需求是做一个临时的数据采集功能&#xff0c;为了将积压的数据快速的消耗完&#xff0c;但是单一的脚本消耗的太慢&#xff0c;于是乎就手写了一个简单的协程池&#xff1a; 为了能加快数据的收集速度为了稳定协程的数量&#xff0c;让脚本变得稳定 设计图如下…

【JUC】线程局部变量ThreadLocal

文章目录 ThreadLocal简介面试题是什么&#xff1f;能干吗&#xff1f;API介绍initialValue方法&#xff08;不推荐&#xff09;withInitial方法&#xff08;推荐&#xff09;remove ThreadLocal入门案例原始需求需求变更线程池 总结 ThreadLocal源码分析Thread、ThreadLocal、…