Linux内核File cache机制(中篇)

在之前的公众号文章中,我们介绍过File cache的定义,其读流程及写流程,想了解上篇详情的可阅读:Linux内核File cache机制(上篇)

本篇则接上篇,主要介绍Linux mmap读文件流程涉及缺页中断。

一、mmap及缺页中断读文件流程分析

上面篇幅,“什么是Filecache”篇幅介绍了文件缓存框架,走读了read()流程中涉及文件缓存的操作流程。对于用户来说,读过程一般是同步读的过程(当然也有异步读的接口,其思想跟文件缓存异步预读机制类同,不再阐述),其执行完接口调用后所需数据就已经在内存中了,而操作的时候就通过write()等接口进行读写。

除了以上的接口,文件缓存的读取和使用是否还有其他方法?答案是肯定的,例如mmap()等接口可以实现将文件映射到用户地址空间,进程可以像访问普通内存一样对文件进行访问。mmap接口从内核的归属上是内存接口,Linux系统对于用户内存分配总是苛刻的,当用户分配内存时,内核优先只给虚拟内存(MAP_POPULATE等模式本文不作讨论),只有使用时才分配物理内存,通过mmap接口进行文件映射属于内核内存的管理框架内,自然也符合这个管理思想。

那么系统怎么及时地给用户分配物理内存呢?这里就引出缺页中断的概念:

  • 当用户访问虚拟地址时发现地址没有映射到具体的物理内存空间,则触发中断进入内核态,进行物理内存填充,本文只关注文件页面的缺页中断。

  • 对于文件缓存的缺页中断,另外一种触发的场景也非常常见:文件缓存因为内存紧张被系统回收了,因为回收的过程会进行虚拟内存解映射,此时用户再次访问到之前分配的虚拟地址时,同样也需要进入缺页中断流程进行数据的填充。关于文件缓存的回收,放到“Filecache 回收流程分析”章节,读者有兴趣可以进行查阅。

本篇幅通过分析mmap/munmap调用执行流程和文件缓存的缺页中断流程,了解该路径下文件缓存的读取过程。

1. mmap函数生命周期

mmap系统调用用于将用户空间的一端内存区域映射到内核空间,根据传递参数的不同,其可以实现共享内存、单独分配匿名内存以及映射文件页面等。本文主要阐述映射文件缓存,普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,而不需要调用read()/write()接口。

  • addr:是否指定映射的内存区域的起始地址,NULL表示由内核指定;

  • length:要映射的内存区域大小;

  • prot:内存保护权限的标志;

  • flags:执行映射对象的类型,例如是否共享、是否锁定映射内存、是否匿名映射(不与文件关联)等;

  • fd:文件描述符,内核通过该句柄来寻找具体的文件,如果要映射文件,必须正确地传递该参数。另外调用返回后,fd可以关闭,但是映射依然有效;

  • offset:表示映射对象(文件)的内容的位置;

对于具体的参数使用可以通过man mmap进行查阅。对于文件映射,其mmap的生命周期如下:

Linux内核File cache机制(中篇)_第1张图片

mmap系统调用最终调用到内核的ksys_mmap_pgoff接口,文件映射则通过文件描述符得到file结构体,然后最终调用到do_mmap函数,MAP_POPULATE本文不阐述。

do_mmap执行可以简单归纳如下:

  • get_unmmaped_area:获取虚拟地址;

  • mmap_region

  • vm_area_alloc:为虚拟地址申请vma抽象结构体;

  • 初始化vma结构体,包括其实虚拟地址、vm_flags(权限)、vm_file赋值文件结构体指针等和文件映射相关的成员;

  • call_mmap:调用文件系统的mmap函数集;

get_unmmaped_area用于在进程虚拟地址中找到一块合适的空闲地址,内核中是通过红黑树进行管理和查找,对于虚拟内存的管理框架,如果读者有兴趣,可以查阅文章《进程内存管理初探》。(虚拟内存框架是相对抽象的子系统,包括vma、vm_struct等抽象结构体以及操作函数集合、地址管理逻辑等,如果读者有兴趣,可以在评论区留言出专篇)

下面走读一下mmap_region函数,一探mmap的核心逻辑,函数定义于mm/mmap.c中:

Linux内核File cache机制(中篇)_第2张图片

函数的逻辑主干很清晰,就不在细述。最后调用call_mmap函数通过mapping->a_ops->mmap回调文件系统操作函数集,以ext4文件系统为例,定义在fs/ext4/file.c中:

Linux内核File cache机制(中篇)_第3张图片

至此mmap函数调用就结束了,可能读者会很疑惑:mmap构建了一个vma结构体后什么都没做?其实非MAP_POPULATE分支的mmap函数就是构建vma结构体,该结构抽象管理一段虚拟地址。如本章节篇头提及,Linux内核对用户内存分配是很苛刻的,从内核的角度:因为物理内存是公共资源且比较紧缺,用户申请分配物理内存可能不用,避免共用资源浪费,所以就只先给虚拟内存,因为虚拟内存是属于进程内部独立的,只影响进程内部,对系统的影响仅仅是增加了vma等管理结构体的内存占用开销。

2. 文件缓存的缺页中断处理流程

用户经过mmap接口后,得到一段虚拟内存,其没有物理载体,即没有承载任何用户需要的数据,那么当用户访问该虚拟内存后,怎么获取数据?答案就是Linux 的缺页中断机制,简单概括:

  • 用户访问虚拟地址,MMU发现虚拟地址没有映射到物理内存;

  • 触发中断进行缺页处理流程;

  • 根据vma结构体成员判断是哪种缺页的类型,进行物理页面分配以及数据填充;

Linux根据内存用途分成多种页面,缺页中断也根据页面类型会走不同的分支。本文只讨论文件缺页中断,其他类型可根据相同思路走读代码。以arm处理器,其处理流程如下:

Linux内核File cache机制(中篇)_第4张图片

其核心处理阶段包括:

  • 平台的缺页中断;

  • 内存核心框架,衔接到文件系统的fault回调函数;

  • 文件缓存管理框架进行文件页面预读;

  • 通用块设备层、IO调度层和设备驱动层进行数据读取;

本文走读前三部分,最后一部分放在IO和文件系统篇幅,后续更新。

(1)缺页中断

缺页中断跟正常的中断处理流程相似,硬件触发经过汇编代码后跳转到do_DataAbort函数处理,定义在arch/arm/mm/fault.c,arm平台有两个寄存器用于记录中断的信息:

  • FSR:失效状态寄存器

  • FAR:失效地址寄存器

Linux内核File cache机制(中篇)_第5张图片

内核使用fsr_info结构体抽象异常处理操作集合:

Linux内核File cache机制(中篇)_第6张图片

以二级页表架构为例子,定义于arch/arm/mm/fsr-2level.h中:

Linux内核File cache机制(中篇)_第7张图片

通过do_page_fault()调用到__do_page_fault函数:通过当前进程的mm_struct和异常的地址得到vma结构体,然后调用handle_mm_fault函数。

Linux内核File cache机制(中篇)_第8张图片

handle_mm_fault函数定义在mm/memory.c中,此时已经进入内存管理框架内,该函数主要分配页表,然后通过handle_pte_fault函数,针对不同的页面类型,进行分支处理:

Linux内核File cache机制(中篇)_第9张图片

处理流程也很清晰,根据不同的情况走不同的分支,do_fault函数用于处理文件页面的缺页中断,当然文件页面的缺页中断也有不同的原因:

Linux内核File cache机制(中篇)_第10张图片

本文走读do_read_fault流程,其他分支读者可以自行分析,其逻辑大同小异。

Linux内核File cache机制(中篇)_第11张图片

__do_fault函数通过 vma->vm_ops->fault()回调到具体文件系统的操作函数集合,前文已知vm_ops函数操作集是在具体的文件系统赋值的,以ext4文件系统为例,ext4_filemap_fault通过filemap_fault函数进行文件页面读取。

(2)filemap_fault

filemap_fault是文件缺页中断的核心处理函数,定义于mm/memory.c中,其逻辑作用:

  • 判断页面是否在文件缓存中,如存在则判断是否需要异步预读;

  • 如不存在则进行同步预读;

其主要的逻辑还是集中在预读窗口的构建。

Linux内核File cache机制(中篇)_第12张图片

函数主干简化后如上,逻辑很清晰就不再细述。关注一下此时的同步预读和异步预读策略跟read分析的预读流程,是否存在不同。

  • do_async_mmap_readahead函数

首先是异步预读do_async_mmap_readahead函数:

Linux内核File cache机制(中篇)_第13张图片

@1中PG_Readahead标志,表示该页面上次是提前异步读取的第一个页面,在“read发起读文件流程分析:4.1章节”的__do_page_cache_readahead中有分析。当前如果访问到这个文件表示,上次提前预读的页面被命中,那么可能是顺序读,此时需继续进行异步预读,page_cache_async_readahead已经在“read发起读文件流程分析”章节分析,这里不再累述。

  • do_sync_mmap_readahead函数

Linux内核File cache机制(中篇)_第14张图片

ra_submit通过__do_page_cache_readahead函数进行预读,该函数在“read发起读文件流程分析”章节都已经分析。

3. munmap函数生命周期

munmap系统调用逻辑非常简单:

  • 内核根据虚拟地址找到vma结构体;

  • unmap_region进行解映射,并释放页表

  • remove_vma_list将vma从用户进程的mm_struct中剥离

Linux内核File cache机制(中篇)_第15张图片

这里需要注意的是,调用munmap接口后,是否文件缓存就会一定马上被回收?答案是否定的:

  • 首先文件页面可能被其他进程映射使用;

  • 其次Linux系统对待文件缓存还是比较“博爱”的,除非用户主动调用drop_cache等接口将文件缓存从内存刷出,不然内核会尽量将文件缓存保存在内存中,避免下次用户使用时需要再次从磁盘中读取。既然是“尽量”,那就得有限度,该限度就是内核空闲内存是否低于LOW_WATERMARK水位线?如果低于该值,就会进行内存回收,其中就包括文件缓存的回收。

二、File cache回收流程分析

Linux File cache的回收涉及的知识点很多,包括内存管理LRU机制、workingset机制、内存回收shrink机制和脏页管理机制等,此部分内容放置在《Linux内核File cache机制(下篇)》中,后续发布,有兴趣的可以关注。

Linux内核File cache机制(中篇)_第16张图片

长按关注

内核工匠微信

Linux 内核黑科技 | 技术文章 | 精选教程

你可能感兴趣的:(内核,python,java,linux,操作系统)