Linux mmap系统调用视角看缺页中断

问题

1. mmap具体是怎么实现比read/write少一次内存copy的

2.mmap共享映射和私有映射在内核实现的时候到底有什么区别

3.mmap的文件映射和匿名映射在内核实现的时候到底有什么区别

4.父子进程的COW具体怎么实现的

概述

实际开发过程中经常使用或者看到mmap函数,具体细节可以man mmap查看相关细节。这个系统调用是个多面手,应用空间申请内存(比如glibc库申请大内存使用的是mmap),还是读写大文件,链接动态库,多进程间共享内存都可以看到mmap的身影,要想真正的理解这个系统一方面是从这几种使用场景的需求上理解mmap,更重要的必须基于内核源码,深入剖析其每个参数具体对应的内核实现。

 内存拷贝次数

以mmap映射文件场景来讲,mmap读写文件与read/write的相比少一次内存拷贝,如果想真正理解这句话最好亲自去看下read/write系统调用的实现流程。以read系统调用来讲其声明如下:

NAMEread - read from a file descriptor
SYNOPSIS#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);

传入一个用户态空间的虚拟地址指针buf,最终内核实现的时候要将文件内容copy到该buf中,我们的都知道内核为了加速读写文件速度会在内核中创建page cache内存(不考虑direct io),那么文件内容首先是读取到了内核的page cache内存中,page cache再拷贝到用户态地址buf当中而mmap的内核page直接跟用户态地址实现映射,那么只要缺页异常时候(文件缺页异常读取数据时mm/filemap.c::filemap_fault函数实现)将数据读取到对应的page中,自然用户态虚拟地址就可以读取到(因为用户态的虚拟地址通过页表直接映射了filemap_fault缺页中断生成的page)。

重点:mmap和read/write生成的page有什么区别?

我们看到mmap和read/write系统调用在内核态都会创建物理页page。mmap是缺页中断时候创建的,如果匿名映射(MAP_ANON)创建的匿名页面;如果文件映射创建的file-back page页面;这两种页面都通过用户态页表完成映射。而read/write系统调用创建的page则不同,这种page cache从某种角度来讲是"临时工",因为并没有用户态页表映射该page,write函数为例,内核系统调用实现的时候只把用户态buf copy给"临时工" page cache返回即可。那么临时工是怎么体现的?

read/write既然没有通过用户态页表,而内核把用户态buf拷贝给内核中的page cache又必须使用内核态虚拟地址,最终内核是通过kmap将page cache临时映射到内核虚拟地址:

//write系统调用会调用到该函数
ssize_t generic_perform_write(struct file *file,struct iov_iter *i, loff_t pos)
{...//iov_iter封装了用户态地址buf,该函数将用户态buf copy到内核page cache中copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);...
}size_t iov_iter_copy_from_user_atomic(struct page *page,                                                                                                                 struct iov_iter *i, unsigned long offset, size_t bytes)
{//通过kmap_atomic将内核page cache临时映射到内核态虚拟地址char *kaddr = kmap_atomic(page), *p = kaddr + offset;if (unlikely(!page_copy_sane(page, offset, bytes))) {kunmap_atomic(kaddr);return 0;}if (unlikely(i->type & ITER_PIPE)) {kunmap_atomic(kaddr);WARN_ON(1);return 0;}iterate_all_kinds(i, bytes, v,copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,v.bv_offset, v.bv_len),memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len))//因为是临时映射的,使用完该虚拟地址通过kunmap,因为这段虚拟地址还是有限的(32位应该是4M)kunmap_atomic(kaddr);return bytes;
}

通过上面场景我们同时也学习到kmap的典型使用场景,经常中文书籍中将kmap翻译成“永久映射”这完全是一种误导,恰恰相反,kmap使用场景是临时映射。

对于mmap这种文件读写情况不太想画图了,引用网络上一张图把:

mmap函数参数详解

NAMEmmap, munmap - map or unmap files or devices into memory
SYNOPSIS#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);int munmap(void *addr, size_t length);
  • prot : 设置内存映射区域vma的读写属性(vma中的vma_flags),最终也会影响页表项pte的读写属性。取值范围:
PROT_EXEC  Pages may be executed.
PROT_READ  Pages may be read.
PROT_WRITE Pages may be written.
PROT_NONE  Pages may not be accessed.
  • flags: 设置映射共享等属性。比如MAP_SHARED,MAP_PRIVATE,MAP_ANONYMOUS等。

PRIVATE:私有意味着修改内容触发写时复制,进程独自拥有不共享物理page。

MAP_SHARED:何谓共享,即缺页时不会触发写时复制,只要在原来的page内容上修改即可。分为文件共享和匿名共享:

文件共享其中任何已进程的改动其他进程可见(因为多个进程映射的是一样的物理page,自然互相可见),且内容的修改会同步磁盘文件

匿名共享:跟文件共享的区别没有映射磁盘文件,多进程之间还是映射一样的物理page,所以也可以互相可见,经常用来进程间通信。内核底层通过shmem实现。

MAP_PRIVATE: 何谓私有,修改页面触发写时复制,这样每个进程有自己独立的物理内存page,也就是私有。分为文件私有和匿名私有:

文件私有:一个进程修改文件会触发写时复制,其他进程不会看到映射内容的改变,修改内容也不会回写磁盘,最常见的场景是加载动态库。

匿名私有:不映射文件,修改内容触发写时复制。

mmap内核系统调用源码剖析之prot和flags

我们看下flags和prot两个参数具体是怎么影响vma和pte的:

mm/mmap.cunsigned long do_mmap(struct file *file, unsigned long addr,unsigned long len, unsigned long prot,unsigned long flags, unsigned long pgoff,unsigned long *populate, struct list_head *uf)
{.../* Do simple checking here so the lower-level routines won't have* to. we assume access permissions have been handled by the open* of the memory object, so we don't do any here.*///prot是mmap函数的prot参数,下面函数将prot转换成vm_flags相关的flagvm_flags = calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;if (file) {...switch (flags & MAP_TYPE) {case MAP_SHARED:/** Force use of MAP_SHARED_VALIDATE with non-legacy* flags. E.g. MAP_SYNC is dangerous to use with* MAP_SHARED as you don't know which consistency model* you will get. We silently ignore unsupported flags* with MAP_SHARED to preserve backward compatibility.*/flags &= LEGACY_MAP_MASK;fallthrough;case MAP_SHARED_VALIDATE:...vm_flags |= VM_SHARED | VM_MAYSHARE;...case MAP_PRIVATE:...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;}}...addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);...return addr;
}unsigned long mmap_region(struct file *file, unsigned long addr,unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,struct list_head *uf)
{...vma->vm_mm = mm;vma->vm_start = addr;vma->vm_end = addr + len;vma->vm_flags = vm_flags;//将vm_flags转换成vm_page_prot,看下面mk_pte函数知道,vm_page_prot最终会影响页表项//读写属性。vma->vm_page_prot = vm_get_page_prot(vm_flags);vma->vm_pgoff = pgoff;...if(file) {...//触发fs中的mmap,比如ext4_file_mmapcall_mmap(...);}...
}//内核缺页时候创建pte一般调用mk_pte函数,就是通过vma->vm_page_prot生成pte的相关flag
//比如匿名缺页中断:
static int do_anonymous_page(struct vm_fault *vmf)
{...//如果vm_flags没有设置entry = mk_pte(page, vma->vm_page_prot);...
}

 cal_vm_prot_bits转换prot的逻辑也简单,我们知道prot取值为PROT_READ/PROT_WRITE/PROT_EXEC,该函数把prot分别兑换成VM_READ/VM_WRITE/VM_EXEC。

总结:最终vm_flags融合了读写相关的属性(来自cal_vm_prot_bits转换的mmap函数的prot参数)和共享属性(源自mmap函数flag参数),最终vm_flags影响vma和pte的flag。

缺页中断流程层次图

 源码:


/** These routines also need to handle stuff like marking pages dirty* and/or accessed for architectures that don't do it in hardware (most* RISC architectures).  The early dirtying is also good on the i386.** There is also a hook called "update_mmu_cache()" that architectures* with external mmu caches can use to update those (ie the Sparc or* PowerPC hashed page tables that act as extended TLBs).** We enter with non-exclusive mmap_lock (to exclude vma changes, but allow* concurrent faults).** The mmap_lock may have been released depending on flags and our return value.* See filemap_fault() and __lock_page_or_retry().*/
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{pte_t entry;...if (!vmf->pte) {if (vma_is_anonymous(vmf->vma))return do_anonymous_page(vmf);elsereturn do_fault(vmf);}if (!pte_present(vmf->orig_pte))return do_swap_page(vmf);if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))return do_numa_page(vmf);vmf->ptl = pte_lockptr(vmf->vma->vm_mm, vmf->pmd);spin_lock(vmf->ptl);entry = vmf->orig_pte;if (unlikely(!pte_same(*vmf->pte, entry))) {update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);goto unlock;}if (vmf->flags & FAULT_FLAG_WRITE) {if (!pte_write(entry))return do_wp_page(vmf);entry = pte_mkdirty(entry);}entry = pte_mkyoung(entry);if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry,vmf->flags & FAULT_FLAG_WRITE)) {update_mmu_cache(vmf->vma, vmf->address, vmf->pte);} else {/* Skip spurious TLB flush for retried page fault */if (vmf->flags & FAULT_FLAG_TRIED)goto unlock;/** This is needed only for protection faults but the arch code* is not yet telling us if this is a protection fault or not.* This still avoids useless tlb flushes for .text page faults* with threads.*/if (vmf->flags & FAULT_FLAG_WRITE)flush_tlb_fix_spurious_fault(vmf->vma, vmf->address);}
unlock:pte_unmap_unlock(vmf->pte, vmf->ptl);return 0;
}

调用栈(以匿名页缺页中断为例):

#0  0xffffffff813988ff in do_anonymous_page (vmf=<optimized out>) at mm/memory.c:4409
#1  handle_pte_fault (vmf=<optimized out>) at mm/memory.c:4367
#2  __handle_mm_fault (flags=<optimized out>, address=<optimized out>, vma=<optimized out>) at mm/memory.c:4504
#3  handle_mm_fault (vma=<optimized out>, address=12040240, flags=<optimized out>, regs=<optimized out>) at mm/memory.c:4602
#4  0xffffffff8114b2a4 in do_user_addr_fault (regs=0xffff8880045fff58, hw_error_code=6, address=12040240) at arch/x86/mm/fault.c:1372
#5  0xffffffff824e4c09 in handle_page_fault (address=<optimized out>, error_code=<optimized out>, regs=<optimized out>) at arch/x86/mm/fault.c:1429
#6  exc_page_fault (regs=0xffff8880045fff58, error_code=6) at arch/x86/mm/fault.c:1482
#7  0xffffffff82600ace in asm_exc_page_fault () at ./arch/x86/include/asm/idtentry.h:538

我们知道缺页中断本质上也是一个缺页异常,cpu处理这种异常会分发到特定的异常处理函数,比如这里调用到exc_page_fault,最终就会调用进入do_anonymous_page匿名缺页中断。

匿名缺页中断

pte不存在的情况下,如果vma_is_anonymous返回true判定是匿名页:

static inline bool vma_is_anonymous(struct vm_area_struct *vma)                                                                                                          
{return !vma->vm_ops;
}

即设置了vma->vm_ops就不是匿名页,vm_ops又是哪里设置的呢? 向上看mmap_region中有call_mmap调用,最终会调用到ext4中具体的mmap函数设置:

 OK,经过上面判定最终确认是匿名缺页(比如mmap函数使用的时候指定了file 映射设置vm_ops,自然不会进入匿名缺页逻辑)。具体匿名缺页函数可以参照如下文章:

Linux 匿名页的生命周期_nginux的博客-CSDN博客

文件缺页中断

如果上面逻辑没有进入匿名缺页,自然进入文件缺页的处理流程,即do_fault函数,文件缺页根据mmap中的flag又多为多种逻辑:

 FAULT_FLAG_WRITE根据cpu状态而设置的一个flag,没有置位说明是只读异常,那么调用do_read_fault;否则意味是写异常,又要细分是否为写时复制,如果vm_flags没有设置VM_SHARED意味PRIVATE,调用do_cow_fault触发写时复制,否则为do_share_fault共享。

根据前面分析vm_flags就来自于mmap函数的flag,对这里的写异常,如果设置了MAP_SHARED,那么这里就进入了do_shared_fault,否则是do_cow_fault。

根据mmap使用MAP_SHARED的文件映射,写时缺页会把内容也会写回磁盘,所以我们推测do_shared_fault内部会进行文件回写逻辑:

 fault_dirty_shared_page来实现这部分逻辑。

do_wp_page

留个坑后面补上

父子进程写时复制的场景

学过操作系统都知道,linux为了性能考虑,fork系统调用子进程并不会完全复制父进程的物理page,两者共享物理内存,同时也比较省内存。只有等任何一方写数据的时候才会触发COW。我们思考如下问题:

如果父进程执行如下流程:

1. addr = mmap(PROT_READ|PROT_WRITE, MAP_PRIVATE)先创建了一个虚拟地址的映射

2. 向addr 写入了数据.

3.fork一个子进程.

4.子进程向addr写入数据触发COW。

理论推导第四步应该会走do_wp_page逻辑,但是要走入整个逻辑要满足如下条件:

 就是说entry页表项必须是不可写的,我们知道我们mmap的时候明明是设置了PROT_READ | PROT_WRITE明明是可读写,那么第一次缺页创建pte的时候pte也是可读写的,那么到底是哪里将pte修改成只读的?答案:fork系统调用发现是private的私有映射,就会将相应的pte修改成只读。

 

参考文章:

Linux内核虚拟内存管理之匿名映射缺页异常分析_vm_get_page_prot_零声教育的博客-CSDN博客

详细讲解Linux内核写时复制技术COW机制(手撕源代码) - 知乎

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Visual Studio 2022的MFC框架——应用程序向导

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天我们来重新审视一下Visual Studio 2022开发工具下的MFC框架知识。 MFC(Microsoft Foundation Class&#xff0c;微软基础类库&#xff09;是微软为了简化程序员的开发工作所开发的一套C类的集合&#xf…

OPENCV C++(八)HOG的实现

hog适合做行人的识别和车辆识别 对一定区域的形状描述方法 可以表示较大的形状 把图像分成一个一个小的区域的直方图 用cell做单位做直方图 计算各个像素的梯度强度和方向 用3*3的像素组成一个cell 3*3的cell组成一个block来归一化 提高亮度不变性 常用SVM分类器一起使用…

数字图像处理 --- 相机的内参与外参(CV学习笔记)

Pinhole Camera Model&#xff08;针孔相机模型&#xff09; 针孔相机是一种没有镜头、只有一个小光圈的简单相机。 光线穿过光圈并在相机的另一侧呈现倒立的图像。为了建模方便&#xff0c;我们可以把物理成像平面(image plane)上的图像移到实际场景(3D object)和焦点(focal p…

代码分析Java中的BIO与NIO

开发环境 OS&#xff1a;Win10&#xff08;需要开启telnet服务&#xff0c;或使用第三方远程工具&#xff09; Java版本&#xff1a;8 BIO 概念 BIO(Block IO)&#xff0c;即同步阻塞IO&#xff0c;特点为当客户端发起请求后&#xff0c;在服务端未处理完该请求之前&#xff…

UE中低延时播放RTSP监控视频解决方案

第1章 方案简介 1.1 行业痛点 在各种智慧城市、智慧社区、智慧水利、智慧矿山等数字孪生项目中&#xff0c;经常使用通UE来开发三维可视化场景。在这些场景中通常都需要把现场的各种监控视频在UE的可视化场景中接入&#xff0c;主要包含海康威视、大华、宇视、华为等众多监控…

网络编程——数据包的组装和拆解

数据包的组装和拆解 一、数据包在各个层之间的传输 二、各个层的封包格式 1、链路层封包格式 -------------------------------------------------------------------------------------------------------------------------------------- | 目标MAC地址&#xff08;6字节&a…

WebView2对比CefSharp的超强优势

第一次使用了CefSharp组件&#xff0c;集成开发结束后&#xff0c;测试及使用过程中遇到了一些无法处理的bug及严重的性能问题。然后又测试对比了其他多种组件&#xff0c;具体情况可以阅读我的博客​ ​《.NET桌面程序集成Web网页开发的十种解决方案》​​。最终选用了微软新出…

Ubuntu 22.04安装和使用ROS1可行吗

可行。 测试结果 ROS1可以一直使用下去的&#xff0c;这一点不用担心。Ubuntu会一直维护的。 简要介绍 Debian发行版^_^ AI&#xff1a;在Ubuntu 22.04上安装ROS1是可行的&#xff0c;但需要注意ROS1对Ubuntu的支持只到20.04。因此&#xff0c;如果要在22.04上安装ROS1&am…

中间件多版本冲突的4种解决方案和我们的选择

背景 在小小的公司里面&#xff0c;挖呀挖呀挖。最近又挖到坑里去了。一个稳定运行多年的应用&#xff0c;需要在里面支持多个版本的中间件客户端&#xff1b;而多个版本的客户端在一个应用里运行时会有同名类冲突的矛盾。在经过询问chatGPT&#xff0c;百度&#xff0c;googl…

linux下.run安装脚本制作

1、安装文件(install.sh) PS: .run安装包内部执行脚本文件 2、资源文件(test.zip) PS: 待安装程序源文件 3、制作.run脚本(install.run) cat install.sh test.zip > install.run chmod ax install.run

【软件工程】3 ATM系统的设计

目录 3 ATM系统的设计 3.1体系结构设计 3.2 设计模式选择 3.3 补充、完善类图 3.4 数据库设计 3.4.1 类与表的映射关系 3.4.2 数据库设计规范 3.4.3 数据库表 3.5 界面设计 3.5.1 界面结构设计 3.5.2 界面设计 3.5.2.1 功能界面设计 3.5.2.2 交互界面 总博客&…

基于Python++PyQt5马尔科夫模型的智能AI即兴作曲—深度学习算法应用(含全部工程源码+测试数据)

目录 前言总体设计系统整体结构图系统流程图 运行环境Python 环境PC环境配置 模块实现1. 钢琴伴奏制作1&#xff09;和弦的实现2&#xff09;和弦级数转为当前调式音阶3&#xff09;根据预置节奏生成伴奏 2. 乐句生成1&#xff09;添加音符2&#xff09;旋律生成3&#xff09;节…