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系统调用来讲其声明如下:

NAME
       read - 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函数参数详解

NAME
       mmap, 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.c

unsigned 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相关的flag
	vm_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_mmap
        call_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);
		else
			return 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机制(手撕源代码) - 知乎

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL】表的内连和外连

本期我们来谈谈表的连接 目录 一、内连接 二、外连接 2.1 左外连接 2.2 右外连接 一、内连接 内连接实际上就是利用where子句对两种表形成的笛卡儿积进行筛选&#xff0c;我们上期学习的复合查询都是内连接&#xff0c;也是在开发过程中使用的最多的连接查询 使用内连接时…

使用windows搭建WebDAV服务,并内网穿透公网访问【无公网IP】

文章目录 1. 安装IIS必要WebDav组件2. 客户端测试3. 使用cpolar内网穿透&#xff0c;将WebDav服务暴露在公网3.1 打开Web-UI管理界面3.2 创建隧道3.3 查看在线隧道列表3.4 浏览器访问测试 4. 安装Raidrive客户端4.1 连接WebDav服务器4.2 连接成功4.2 连接成功 1. Linux(centos8…

Qt 文件对话框使用 Deepin风格

当你在Deepin或UOS 上开发 Qt 程序时&#xff0c;如果涉及到文件对话框功能&#xff0c;那么就会遇到调用原生窗口的问题。 如果你使用的是官方的Qt版本&#xff0c;那么在Deepin或者UOS系统上&#xff0c;弹出的文件对话框会是如下这样&#xff1a; 而Deepin或UOS系统提供的默…

考研数据结构:第七章 查找

文章目录 一、查找的基本概念二、顺序查找和折半查找2.1顺序查找2.3折半查找2.3.1算法思想2.3.2代码实现2.3.3查找效率分析2.3.4折半查找判定树的构造2.3.5折半查找效率2.3.6小结 2.4分块查找 三、树形查找3.1二叉排序树3.1.1二叉排序树定义3.1.2查找操作3.1.3插入操作3.1.4二叉…

DVI端口静电防护,如何设计保护电路图?

Digital Visual Interface&#xff0c;简称DVI&#xff0c;中文名&#xff1a;数字视频接口&#xff0c;是一种视频接口标准&#xff0c;用来传输未经压缩的数字化视频&#xff0c;广泛应用于LCD、数字投影机等显示设备上。DVI端口的种类非常多&#xff0c;有DVI-A、DVI-D、DVI…

Rx.NET in Action 第二章学习笔记

2 Hello, Rx 本章节涵盖的内容: 不使用Rx的工作方式向项目中添加Rx创建你的第一个Rx应用程序 Rx 的目标是协调和统筹来自社交网络、传感器、用户界面事件等不同来源的基于事件的异步计算。例如&#xff0c;建筑物周围的监控摄像头和移动传感器会在有人靠近建筑物时触发&#xf…

threejs中gltf模型出现的问题(黑色,颜色不协调,太小)和解决方案

模型一片漆黑 如下图 可能原因&#xff0c;没有灯光&#xff0c;加下以下代码&#xff1a; // 4、加入灯光 const lightness new THREE.HemisphereLight(0xffffff, 0x444444); lightness.position.set(0, 20, 0); scene.add(lightness); const shadowLight new THREE.Direct…

文献综述|NLP领域后门攻击、检测与防御

前言&#xff1a;在信息安全中后门攻击&#xff08;Backdoor Attack&#xff09;是指绕过安全控制而获取对程序或系统访问权的方法。而随着深度学习以及各种神经网络模型的广泛应用&#xff0c;神经网络中存在的后门问题也引起了研究人员的广泛关注。神经网络后门攻击就是使网络…

robotframework+selenium 进行webui页面自动化测试

robotframework其实就是一个自动化的框架&#xff0c;想要进行什么样的自动化测试&#xff0c;就需要在这框架上添加相应的库文件&#xff0c;而用于webui页面自动化测试的就是selenium库. 关于robotframework框架的搭建我这里就不说了&#xff0c;今天就给大家根据一个登录的实…

淘宝API接口的实时数据和缓存数据区别

电商API接口实时数据是指通过API接口获取到的与电商相关的实时数据。这些数据可以包括商品库存、订单状态、销售额、用户活跃度等信息。 通过电商API接口&#xff0c;可以实时获取到电商平台上的各种数据&#xff0c;这些数据可以帮助企业或开发者做出及时的决策和分析。例如&…

web会话跟踪以及JWT响应拦截机制

目录 JWT 会话跟踪 token 响应拦截器 http是无状态的&#xff0c;登录成功后&#xff0c;客户端就与服务器断开连接&#xff0c;之后再向后端发送请求时&#xff0c;后端需要知道前端是哪个用户在进行操作。 JWT Json web token (JWT), 是为了在网络应用环境间传递声明而…

Vue3 大屏数字滚动效果

父组件&#xff1a; <template> <div class"homePage"> <NumRoll v-for"(v, i) in numberList" :key"i" :number"v"></NumRoll> </div> </template> <script setup> import { onMounted, r…

安装elasticsearch

一、docker安装elasticsearch 1、下载镜像 docker pull elasticsearch:6.5.4 2、启动容器 docker run -p 9200:9200 -p 9300:9300 --name elasticsearch \ -e "discovery.typesingle-node" \ -e "cluster.nameelasticsearch" \ -e "ES_JAVA_OPTS-Xm…

答疑:Arduino IDE配置其他开发板下载速度慢

基于案例&#xff1a;Linux环境Arduino IDE中配置ATOM S3 通常&#xff0c;网络问题较多&#xff0c;可以使用一些技巧。 https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json 没有配置&#xff0c;不支持M5Stack&#xff08;ESP32&…

【设计模式】前端控制器模式

前端控制器模式&#xff08;Front Controller Pattern&#xff09;是用来提供一个集中的请求处理机制&#xff0c;所有的请求都将由一个单一的处理程序处理。该处理程序可以做认证/授权/记录日志&#xff0c;或者跟踪请求&#xff0c;然后把请求传给相应的处理程序。以下是这种…

机器学习笔记之优化算法(十二)梯度下降法:凸函数VS强凸函数

机器学习笔记之优化算法——梯度下降法&#xff1a;凸函数VS强凸函数 引言凸函数&#xff1a;凸函数的定义与判定条件凸函数的一阶条件凸函数的梯度单调性凸函数的二阶条件 强凸函数强凸函数的定义强凸函数的判定条件强凸函数的一阶条件强凸函数的梯度单调性强突函数的二阶条件…

小白带你学习linux的MongoDB(三十四)

目录 一、概述 1、相关概念 2、特性 二、应用场景 三、安装 四、目录结构 五、默认数据库 1、admin&#xff1a; 2、local: 3、config: 六、数据库操作 1、库操作 2、文档操作 七、MongoDB数据库备份 1、备份命令 2、回数据库删除…

Android侧滑栏(一)可缩放可一起移动的侧滑栏

在实际的各类App开发中&#xff0c;经常会需要做一个左侧的侧滑栏&#xff0c;类似于QQ这种。 今天这篇文章总结下自己在开发中遇到的这类可以跟随移动且可以缩放的侧滑栏。 一、实现原理 使用 HorizontalScrollView 实现一个水平方向的可滑动的View&#xff0c;左布局为侧滑…

纷享销客稳居2022 H2 SFA SaaS 本土CRM厂商市场份额 TOP 1

近期&#xff0c;国际知名研究机构IDC公布了2022年下半年《中国客户关系管理(CRM)SaaS市场跟踪研究报告》&#xff0c;报告全面解析了中国CRM SaaS以及细分市场SFA SaaS的市场现状&#xff0c;并对全球各大厂商在中国SFA市场的份额占比进行了排名。连接型CRM开创者纷享销客在SF…

MyBatis-Plugin源码全面分析

三、MyBatis-Plugin 1. 基本开发方式 需求&#xff1a;在MyBatis执行之前打印一行醒目的日志&#xff0c;携带参数 实现Interceptor接口&#xff1a; Intercepts(Signature(type Executor.class,method "query",args {MappedStatement.class,Object.class, RowB…