Linux内存管理(7)- page fault

  • 了解linux page fault.

1.概述

  A page fault (sometimes called #PF, PF or hard fault)[a] is a type of exception raised by computer hardware when a running program accesses a memory page that is not currently mapped by the memory management unit (MMU) into the virtual address space of a process. Logically, the page may be accessible to the process, but requires a mapping to be added to the process page tables, and may additionally require the actual page contents to be loaded from a backing store such as a disk. The processor’s MMU detects the page fault, while the exception handling software that handles page faults is generally a part of the operating system kernel. When handling a page fault, the operating system tries to make the required page accessible at the location in physical memory or terminates the program in cases of an illegal memory access.

2.出现Page Fault的情况

  在Linux内核中内存是有限的,而为了最大限度的利用内存内核采取了分页的机制: 进程被分配虚拟地址空间,虚拟地址空间映射真实的物理内存,进程的数据页并不是全部加载至物理内存的地址空间之中,只有当用户访问的数据页不在物理内存之中,通过请页机制进行加载数据页至物理内存。

  在mmap的内核实现中,可以发现mmap仅仅建立了进程的虚拟地址空间与物理内存的映射,当访问的数据页不在物理内存之中时,触发缺页中断,这里就用到了请页机制. 在硬件层面,当CPU访问的数据页不在物理内存中,CPU就会触发缺页中断,通知内核进行处理。
在这里插入图片描述
3.错误处理

3.1.结构体定义

struct fsr_info {
 	int (*fn)(unsigned long addr, unsigned int fsr, struct pt_regs *regs);
 	int sig;
 	int code;
 	const char *name;
};

3.2.定义错误处理函数

1>.静态定义:参考fsr-3level.c

fsr-3level.c

static struct fsr_info fsr_info[] = {
 { do_bad,  SIGBUS,  0,  "unknown 0"   },
 { do_bad,  SIGBUS,  0,  "unknown 1"   },
 { do_bad,  SIGBUS,  0,  "unknown 2"   },
 { do_bad,  SIGBUS,  0,  "unknown 3"   },
 { do_bad,  SIGBUS,  0,  "reserved translation fault" },
 { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 1 translation fault" },
 { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 2 translation fault" },
 ...
};

2>.动态定义:函数hook_fault_code支持动态设置,如下:

void __init
hook_fault_code(int nr, int (*fn)(unsigned long, unsigned int, struct pt_regs *),
  int sig, int code, const char *name)
{
 if (nr < 0 || nr >= ARRAY_SIZE(fsr_info))
  BUG();

 fsr_info[nr].fn   = fn;
 fsr_info[nr].sig  = sig;
 fsr_info[nr].code = code;
 fsr_info[nr].name = name;
}

4.Arm64处理

  Page Fault异常处理,依赖于体系结构,介绍Arm64的处理,如下所示:

arch/arm64/kernel/entry.S:
在这里插入图片描述

  Arm64在取指令或者访问数据时,需要把虚拟地址转换成物理地址,这个过程需要进行几种检查,在不满足的情况下都能造成异常:

  • 地址的合法性,比如以39有效位地址为例,内核地址的高25位为全1,用户进程地址的高25位为全0;
  • 地址的权限检查,这里边的权限位都位于页表条目中;

  从上图中可以看到,最后都会调到do_mem_abort函数:

arch/arm64/mm/fault.c:
asmlinkage void __exception do_mem_abort(unsigned long addr, unsigned int esr,
                     struct pt_regs *regs)
{
    const struct fault_info *inf = esr_to_fault_info(esr);
    struct siginfo info;

    if (!inf->fn(addr, esr, regs))
        return;

    pr_alert("Unhandled fault: %s (0x%08x) at 0x%016lxn",
         inf->name, esr, addr);

    mem_abort_decode(esr);

    info.si_signo = inf->sig;
    info.si_errno = 0;
    info.si_code  = inf->code;
    info.si_addr  = (void __user *)addr;
    arm64_notify_die("", regs, &info, esr);
}

  根据传进来的esr获取fault_info信息,从而调用函数。struct fault_info用于错误状态下对应的处理方法,而内核中也定义了全局结构fault_info,存放了所有的情况。主要的错误状态和处理函数对应如下:

static const struct fault_info fault_info[] = {
    { do_bad,       SIGBUS,  0,     "ttbr address size fault"   },
    { do_bad,       SIGBUS,  0,     "level 1 address size fault"    },
    { do_bad,       SIGBUS,  0,     "level 2 address size fault"    },
    { do_bad,       SIGBUS,  0,     "level 3 address size fault"    },
    { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "level 0 translation fault" },
    { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "level 1 translation fault" },
    { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "level 2 translation fault" },
    { do_translation_fault, SIGSEGV, SEGV_MAPERR,   "level 3 translation fault" },
    { do_bad,       SIGBUS,  0,     "unknown 8"         },
    { do_page_fault,    SIGSEGV, SEGV_ACCERR,   "level 1 access flag fault" },
    { do_page_fault,    SIGSEGV, SEGV_ACCERR,   "level 2 access flag fault" },
    { do_page_fault,    SIGSEGV, SEGV_ACCERR,   "level 3 access flag fault" },
    { do_bad,       SIGBUS,  0,     "unknown 12"            },
    { do_page_fault,    SIGSEGV, SEGV_ACCERR,   "level 1 permission fault"  },
    { do_page_fault,    SIGSEGV, SEGV_ACCERR,   "level 2 permission fault"  },
    { do_page_fault,    SIGSEGV, SEGV_ACCERR,   "level 3 permission fault"  },
     ...
};

从代码中可以看出:

  • 出现0/1/2/3级页表转换错误时,会调用do_translation_fault,实际中do_translation_fault最终也会调用到do_page_fault;
  • 出现1/2/3级页表访问权限的时候,会调用do_page_fault
    其他的错误则调用do_bad,其中未列出来的部分还包括do_sea等操作函数;

3.1.do_translation_fault

arch/arm64/mm/fault.c:
605 static int __kprobes do_translation_fault(unsigned long addr,
606                       unsigned int esr,
607                       struct pt_regs *regs)
608 {   
609     if (is_ttbr0_addr(addr))
610         return do_page_fault(addr, esr, regs);
611 
612     do_bad_area(addr, esr, regs);                                                                        
613     return 0;
614 }    

在这里插入图片描述

3.2.do_page_fault
在这里插入图片描述
  如上所示,do_page_fault函数为页错误异常处理的核心函数,与体系结构相关,上图中的handle_mm_fault函数为通用函数,也就是不管哪种处理器结构,最终都会调用到该函数。

3.3.handle_mm_fault

  用于处理用户空间的页错误异常:

  • 进程在用户模式下访问用户虚拟地址,触发页错误异常;
  • 进程在内核模式下访问用户虚拟地址,触发页错误异常;

  从do_page_fault函数的流程图中也能看出来,当触发异常的虚拟地址属于某个vma,并且拥有触发页错误异常的权限时,会调用到handle_mm_fault函数,而handle_mm_fault函数的主要逻辑是通过__handle_mm_fault来实现的。
在这里插入图片描述

3.4. do_fault

  do_fault函数用于处理文件页异常,包括以下三种情况:

  • 读文件页错误;
  • 写私有文件页错误;
  • 写共享文件页错误;
    在这里插入图片描述

3.5.do_anonymous_page

  匿名页的缺页异常处理调用本函数,在以下情况下会触发:

  • malloc/mmap分配了进程地址空间区域,但是没有进行映射处理,在首次访问时触发;
  • 用户栈不够的情况下,进行栈区的扩大处理;
    在这里插入图片描述

3.6 do_swap_page

  如果访问Swap页面出错(页面不在内存中),则从Swap cache或Swap文件中读取该页面。do_swap_page调用的很多函数都是空函数,大体的流程如下图:
在这里插入图片描述

3.7 do_wp_page

  do_wp_page函数用于处理写时复制(copy on write),会在以下两种情况处理:

  • 创建子进程时,父子进程会以只读方式共享私有的匿名页和文件页,当试图写的时候,触发页错误异常,从而复制物理页,并创建映射;
  • 进程创建私有文件映射,读访问后触发异常,将文件页读入到page cache中,并以只读模式创建映射,之后发生写访问后,触发COW;

在这里插入图片描述

关键的复制工作是由wp_page_copy完成的:
在这里插入图片描述

3.9. arm64_notify_die

void arm64_notify_die(const char *str, struct pt_regs *regs,
              struct siginfo *info, unsigned long err, unsigned long trap)
{
       if (user_mode(regs)) {
              // 。。。
              force_sig_info(info->si_signo, info, current);
       } else {
              die(str, regs, err);
       }
 }

  该函数首先使用user_mode,判断abort时是属于用户模式还是内核模式,判断方法是看cpsr寄存器中的模式位。按照arm的定义,模式位为0代表用户模式。

  • 如果是用户模式,那么强制发送一个信号给导致abort的任务(注意这里的任务可能是一个线程)。具体哪个信号被发送由struct fsr_info结构体中定义的值决定,一般来说,是一个能使进程停止的信号,比如SIGSEGV等等(SIGSEGV之类的信号即使被发给一个线程,也会停止整个进程,具体可看get_signal_to_deliver函数)。
  • 如果是内核模式,那么调用die函数,这是kernel处理OOPS的标准函数。
发布了161 篇原创文章 · 获赞 15 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_41028621/article/details/104345008