Linux file-backed page fault

想要了解更多Linux 内存相关知识敬请关注:Linux内存笔记公众号,期待与大家的更多交流与学习。
原创文章,转载请注明出处。

一、背景

​ Linux操作系统分为两层: users pace、Kernel space,用户空间通过MMU使用虚拟地址空间访问底层物理地址;用户空间访问底层一般的映射方式也是两种:anonymous mapping、file-backed mapping,不同的映射方式产生不同的内存类型也就是anon page, file page:

Active(anon):        352 kB
Inactive(anon):        0 kB
Active(file):       2444 kB
Inactive(file):    55520 kB

在这里插入图片描述

MAP_FILE
				Mapped from a regular file.  (This is the default mapping type, and need not be specified.)

File-backed mapping

​ 通过将文件内容映射到进程的地址空间来分配内存的方法,将完整或则是部分文件映射到内存当中,最终通过访问内存的方式访问文件。目前系统通过使用mmap系统调用完成file-backed mapping完成文件映射。

file-backed mapping与anonymous mapping最大的差异就是fd文件描述符; 匿名映射是没有fd的,但是文件映射存在fd,系统通过file-backed mapping产生的fd 并将内存与文件关联起来,最终通过fd 操作内存完成文件的读写访问。

file-backed mapping 为什么没有zero page: anonymous mapping产生匿名页,匿名页在没有做写入操作时数据初始化为0,所以如果只读的情况下新分配的内存被填充的肯定也是0,这种场景下就可以使用zero page替代;在只读的情况下使用zero page提高内存申请效率且降低内存浪费。file-backed mapping 目的是通过映射的方式实访问文件(访问内存的方式访问文件),只读一个文件(文件是存在的,即便文件内容全部为0这也是文件的内容),所以file mapping 映射文件到内存当中就必须提供物理内存与文件建立一定的映射关系,这样就不可能像匿名映射那样使用zero page,所以file-backed mapping不存在zero page。

二、源码分析

static int handle_pte_fault(struct vm_fault *vmf)
{
		...
    /*
    PTE为空:则vma_is_anonymous判断映射类型
    如果使用匿名映射时,将通过调用do_anonymous_page()函数来分配和映射新页面。此方法称为惰性页面分配
    如果使用文件映射时,请调用do_fault()函数从file读取页面
    */
    if (!vmf->pte) {
        if (vma_is_anonymous(vmf->vma))
          return do_anonymous_page(vmf);
        else
          return do_fault(vmf);
    }
    ...
}

当PTE 不存在且且vma->vm_ops非空的场景下被判断为文件映射,do_fault完成file-backed mapping的page fault核心流程。

static int do_fault(struct vm_fault *vmf)
{
 		...
    if (!vma->vm_ops->fault) {
      	...
    } else if (!(vmf->flags & FAULT_FLAG_WRITE))
     	 ret = do_read_fault(vmf);//没有写请求,则调用do_read_fault()函数以从映射文件中读取页面,造成读文件错误
    else if (!(vma->vm_flags & VM_SHARED))
      	ret = do_cow_fault(vmf);//没有共享方式,则do_cow_fault()函数以复制并写入新页面,相当于写私有文件
    else
      	ret = do_shared_fault(vmf);//如果是共享文件映射调用do_shared_fault()函数以设置写操作,相当于写共享文件
  	...
}

1、Read-only: 执行do_read_fault完成只读模式的文件映射;

2、Private Write: 执行do_cow_fault完成私有写操作文件映射;

3、Share Write: 执行do_shared_fault完成共享写操作文件映射
在这里插入图片描述

do_read_fault

static int do_read_fault(struct vm_fault *vmf)
{
  	...
    /*
     * Let's call ->map_pages() first and use ->fault() as fallback
     * if page by the offset is not ready to be mapped (cold cache or
     * something).
     */
    if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
        ret = do_fault_around(vmf);
        if (ret)
          	return ret;
    }
  	...
}

do_fault_around() tries to map few pages around the fault address. The hope is that the pages will be needed soon and this will lower the number of faults to handle。当map_pages存在回调函数并且fault_around_bytes >> PAGE_SHIFT大于1时,代码跳转到do_fault_around,的是在预先加载周围的页面时减少页错误异常的次数,同时起到一定的文件映射保护作用,单次的最大映射空间被限制在64K,有一定的保护作用。

static int do_fault_around(struct vm_fault *vmf)
{
 		unsigned long address = vmf->address, nr_pages, mask;
    pgoff_t start_pgoff = vmf->pgoff;
    pgoff_t end_pgoff;
    int off, ret = 0;
		
    /*
    使用fault_around_bytes常规情况下是65536bit也就是64K,经过fault_around_bytes >> PAGE_SHIFT偏移可以得出
    nr_pages = 16也就是16个pages, 所以nr_pages是根据fault_around_bytes的变化而改变的。
    /sys/kernel/debug # cat fault_around_bytes 
		65536
		static unsigned long fault_around_bytes __read_mostly = rounddown_pow_of_two(65536); //系统默认值
    */
    nr_pages = READ_ONCE(fault_around_bytes) >> PAGE_SHIFT;
   	/*
   	PAGE_MASK = 0xfffff000 获取mask,根据nr_pages = 16数据可以获取出mask=0xFFFF_0000,
   	MMU pgd->pud->pmd->pte->offset: offset [11,0] 12位的offset(12位 页内偏移PAGE_SHIFT), pte [21,12] 9位 描述pte,
   	pte代表多少个page,offset则代表page内部偏移。如果address地址以page为单位对齐则肯定要过滤掉address的[11,0](页内偏移),
    pte[21,11] 要根据对齐的page数进行修改,假设fault_around_bytes = 64k 则16个pages(2^4),所以12~15被置0,这样mask以 		16page对齐就是吗mask = 0xFFFF_0000, 如果fault_around_bytes = 16k 则4个pages(2^2),2位即可描述4个page, 
    mask = 0xFFFF_C000
    */
    mask = ~(nr_pages * PAGE_SIZE - 1) & PAGE_MASK;
  
    /* 使用mask做address & mask对齐后,并与vmf->vma->vm_start比较取最大的值重新赋值给vmf->address */
    vmf->address = max(address & mask, vmf->vma->vm_start);
		/* 计算出对齐后的地址与原有地址的page的差值 */
    off = ((address - vmf->address) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
  	
  	/* 根据address对齐的地址计算出的off偏差,计算出实际的start_pgoff */
    start_pgoff -= off;

    /*
     *  end_pgoff is either end of page table or end of vma
     *  or fault_around_pages() from start_pgoff, depending what is nearest.
     */
    /*
    (1) = (PTRS_PER_PTE - 1)代表一个虚拟地址可以使用的最大的PTE个数(也就是512个物理pages)512 pages
    (2) = (vmf->address >> PAGE SHIE) & (PTRS_PER_PTE - 1)) 代表vmf已经使用了多少个PTE,也就是多少个物理页面pages,
    (3) = start_pgoff 代表了PTE的起始地址, 从该PTE向上增长。
    对于 end_pgoff = (1) - (2) + (3)可能更好理解,(1) - (2)可以得出该虚拟地址空间还剩下多少个可用的PTE也就是剩下的物理地址空			间个数
    在这个基础上加上(3)就可以得到最大的映射结束地址也就是end_pgoff。这里将end_pgoff修改为max_pgoff可能更为贴切,更方便理解。
    */
		end_pgoff = start_pgoff -
      ((vmf->address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1)) +
      PTRS_PER_PTE - 1;
    /*
    取最小值用于vmf->vma->vm_ops->map_pages(vmf, start_pgoff, end_pgoff)映射当中,通过这里分析也可以知道
    end_pgoff - start_pgoff的最大差值是15,也就是0~15这一共16个pages页面也就是64k,
    从文件的映射当中也就限制了单次映射的最大地址空间就是64K。
    */
  	end_pgoff = min3(end_pgoff, vma_pages(vmf->vma) + vmf->vma->vm_pgoff - 1,
        	start_pgoff + nr_pages - 1);
		...
    /* vmf文件与start_pgoff,end_pgoff建立映射关系,回调函数的源函数filemap_map_pages */
    vmf->vma->vm_ops->map_pages(vmf, start_pgoff, end_pgoff);
		...
}

为了方便演示假设fault_around_bytes=16k, nr_pages = 4, start_pgoff = 0以上条件作为前提,vma->vm_start = 0x0000_0000, vma->vm_end = 0x001_1000 将整个过程后数据计算出来,并使用示意图完成演示:mask = 0xFFFF_C000, off = 0, max_pgoff = 511, end_pgoff = min3(511, 17 ,3), 所以最终计算出来的end_pgoff = 3, 这样在后续map_pages(vmd, 0, 3)这样就是page 0 ~ page3 与fault_around_bytes = 16k能够对应上。示意图如下:
在这里插入图片描述

回头看这个do_fault_around 如果有这种机制的情况下一次最多可以映射 fault_around_bytes/PAGE_SIZE个pages,这样提高了fault的效率,如果没有这个机制一次只能映射一个page这样效率是低下的。重新回到do_read_fault函数:

	/* 当文件fault 不支持do_fault_around机制时就要do_fault完成文件的read fault */
	ret = __do_fault(vmf);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
			return ret;
  ...
/*
 * The mmap_sem must have been held on entry, and may have been
 * released depending on flags and vma->vm_ops->fault() return value.
 * See filemap_fault() and __lock_page_retry().
 */
static int __do_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    int ret;
    ...
    /*
    执行文件映射错误处理函数,相当于filemap_fault函数在filemap.c文件当中
    从文件读取的页面信息并传输到输出参数页面
    vma->vm_ops->fault回调函数与文件系统有关,比如ext4 就是ext4_filemap_fault
    代码的核心逻辑就是filemap_fault
    */
    ret = vma->vm_ops->fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY |
            VM_FAULT_DONE_COW)))
      return ret;
    ...
}

filemap_fault是一个复杂的函数,从page cache 当中读取文件的动作(预读), 预读原理在后续会做详细介绍。

int filemap_fault(struct vm_fault *vmf)
{
    ...
    /*
     * Do we have something in the page cache already?
     */
    /* 查找缺页是否存在于 Page Cache当中, mapping 为该文件的 adress_space, offset 为该页的偏移量. */
    page = find_get_page(mapping, offset);
    if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) {
        /*
         * We found the page, so try async readahead before
         * waiting for the lock.
         */
      	/* 如果文件已经存在到内存的缓存当中也就是page cache当中,直接异步预读 */
        do_async_mmap_readahead(vmf->vma, ra, file, page, offset);
    } else if (!page) {
        /* No page in the page cache at all */
      	/* 如果文件不存在到内存的缓存当中,则进行同步预读 */
        do_sync_mmap_readahead(vmf->vma, ra, file, offset);
  			...
    retry_find:
      	/* 同步预读后如果文件还没有在内存缓存当中查找到,那么直接goto no_cached_page */
        page = find_get_page(mapping, offset);
        if (!page)
          	goto no_cached_page;
    }
    ...
no_cached_page:
      /*
       * We're only likely to ever get here if MADV_RANDOM is in
       * effect.
       */
  	/* 如果缓存当中始终没有找到对应的page, 则直接从buddy当中申请一个新的page 建立文件与物理页面的映射
     * 这里有一个点需要注意通过workingset确认这个page要放到inactive/active list当中
     * 在内存充足的场景通常移动到inactive list,当低内存时根据系统内存状况分布,主要原因就是workingset决定
     * 后续会详细介绍workingset基本原理。
    */
    error = page_cache_read(file, offset, vmf->gfp_mask);
    ...
}

__do_fault主要功能两点:1、从page cache 当中预读,与缓存在内存当中的文件建立映射关系; 2、当page cache没有缓存该文件是要直接申请物理内存然后使用物理内存与文件建立映射关系。
在这里插入图片描述

file-backed fault 的只读anonymous fault只读模式存在巨大差异:

1、anonymous read fault使用zero page 无论read 空间多大都只占用一个page 的大小

if a process instantiates a new (non-huge) page by trying to read from it, the kernel still will not allocate a new memory page. Instead, it maps a special page, called simply the “zero page,” into the process’s address space instead. Thus, all unwritten anonymous pages, across all processes in the system, are, in fact, sharing one special page. Needless to say, the zero page is always mapped read-only; it would not do to have some process changing the value of zero for everybody else. Whenever a process attempts to write to the zero page, it will generate a write-protection fault; the kernel will then (finally) get around to allocating a real page of memory and substitute it into the process’s address space at the right spot.

2、file-backed read fault则是首先尝试从page cache缓存当中预读,如果page cache当中没有该文件则直接allocated page 申请物理内存并与文件建立映射关系;所以这样就会出现两种情况:1、如果文件已经被缓存则做read操作时inactive file不会增加;2、如果文件第一次访问,则page cache当中没有缓存则做read 时 inactive file 就会增加used 内存也会增加。

do_cow_fault

static int do_cow_fault(struct vm_fault *vmf)
{
		...
    /* 
    分配一个页面 order = 0, 所以只分配一个物理页面, 如果分配失败则说明页面不足,返回OOM错误 
    copy no write, 这类异常发生在fork创建子进程过程,申请新的page供后续拷贝文件使用
    */
    vmf->cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
    if (!vmf->cow_page)
      	return VM_FAULT_OOM;
		...
    /* 读取文件 */
    ret = __do_fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
      	goto uncharge_out;
		...
    
    /* 
    vmf->page 将文件读取到内存当中,通过接口将文件的内容拷贝到cow_page新的page当中,完成写实拷贝
   	并为新页面设置PG_uptodate标志 */
		copy_user_highpage(vmf->cow_page, vmf->page, vmf->address, vma);
		__SetPageUptodate(vmf->cow_page);
  	...
}

通过一场流程就完成了文件的copy on write操作,这个过程核心操作就是以上三个步骤,更加细节的__do_fault后续详细分析。do_cow_fault发生在父进程fork子进程过程当中,进入fault 后直接分配新的物理内存并完成父子进程数据的拷贝。

do_shared_fault

static int do_shared_fault(struct vm_fault *vmf)
{
    ...
    /* page cache中读取错误地址的页面。文件缓存当中没有找到对应的文件直接结束运行 */
    ret = __do_fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
      	return ret;

    /*
     * Check if the backing address space wants to know that the page is
     * about to become writable
     */
    /* vma操作处理程序中设置了page_mkwrite挂钩函数,则将调用do_page_mkwrite()函数来设置写入。
    如果失败,则释放fault_page并退出功能 */
    if (vma->vm_ops->page_mkwrite) {
      	unlock_page(vmf->page);
      	tmp = do_page_mkwrite(vmf);
      	if (unlikely(!tmp ||
          (tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
        	put_page(vmf->page);
        	return tmp;
      	}
    }
    ...
    fault_dirty_shared_page(vma, vmf->page);
    ...
}

总结以上三个流程核心流程是都是__do_fault 文件页的page fault的核心思想就是从page cache缓存当中读取文件,这样提高文件的page fault效率,这其中的预读,预fault都发挥重大作用。以ext4为例紧凑的示意图如下:
在这里插入图片描述

三、思考与DEMO

jinsheng@jinsheng:~/Workspace/code/linux/linux-4.14/linux-4.14.177/kmodules$ dd if=/dev/zero of=hello.txt bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes (105 MB, 100 MiB) copied, 0.101575 s, 1.0 GB/s
jinsheng@jinsheng:~/Workspace/code/linux/linux-4.14/linux-4.14.177/kmodules$ ls -lh hello.txt 
-rw-rw-r-- 1 jinsheng jinsheng 100M 1221 23:59 hello.txt
jinsheng@jinsheng:~/Workspace/code/linux/linux-4.14/linux-4.14.177/kmodules$ 

创建了一个文件内存全部为0的test文件,文件大小100M, 以test文件作为demo 实验进行demo设计。

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{
    char *addr = NULL;
    int i = 0;
    int fd = 0;
    struct stat sb;
    char t;

    fd = open("hello.txt", O_RDWR);
    if (fd == -1){
        printf("open hello.txt failure\n");
        return -1;
    }

    if (fstat(fd, &sb) == -1) {          /* To obtain file size */
        printf("get file size failure\n");
        close(fd);
        return -1;
    }

		printf("file size = %ld\n", sb.st_size);

    addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (addr == NULL){
        printf("mmap hello.txt failure\n");
        close(fd);
        return -1;
    }

    printf("mmap file sucess\n");
    system("cat /proc/meminfo | grep file");
    system("free -m");
    printf("\n");

    getchar();

    printf("read !!!!!!\n");
    for (i = 0; i < sb.st_size; i++){
        t = addr[i];
    }
    printf("\n\n");

    printf("after read !!!!!!\n");
    system("cat /proc/meminfo | grep file");
    system("free -m");
    printf("\n");

    munmap(addr, sb.st_size);
    close(fd);

    printf("munmap file & close fd !!!!!!\n");
    system("cat /proc/meminfo | grep file");
    system("free -m");

    return 0;
}

这是第一次read 此时系统当中并没有缓存hello.txt文件,所以在mmap 后read 就会看到inactive file 从458636K 增长到560300k 文件页增长约100M; 如果读取一次后重新在执行程序就会发现inactive file 不再增长,原因就是第一次读区已经将文件加载到page cache缓存当中,所以下一次read 操作直接从page cache当中加载文件。

jinsheng@jinsheng:~/Workspace/code/linux/linux-4.14/linux-4.14.177/kmodules$ ./x64_read 
file size = 104857600
mmap file sucess
Active(file):     139980 kB
Inactive(file):   458636 kB
              total        used        free      shared  buff/cache   available
Mem:           3876         632        2591           7         652        3006
Swap:          2047           0        2047

read !!!!!!

after read !!!!!!
Active(file):     140868 kB
Inactive(file):   560300 kB
              total        used        free      shared  buff/cache   available
Mem:           3876         607        2515           7         752        3031
Swap:          2047           0        2047

munmap file & close fd !!!!!!
Active(file):     140868 kB
Inactive(file):   560300 kB
              total        used        free      shared  buff/cache   available
Mem:           3876         607        2515           7         752        3031
Swap:          2047           0        2047
  • 16
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值