进程虚拟内存

进程虚拟地址空间

Executable and Linkable Format(ELF)


  上图是可执行文件的内容结构图,由ELF header、program headers、各section、sections headers组成。
- ELF header:描述整个文件的基本属性,如文件版本号、目标机器型号、程序入口地址等。
- program headers:描述ELF文件该如何被操作系统映射到进程的虚拟地址空间,对于LOAD类型的Segment,每个Segment对应一个VMA。对于操作系统来说,并不关心各个section所包含的内容,它只关心跟装载相关的问题,最主要的是section的权限(可读,可写,可执行),所以对于相同类型的section,将会被合并成要给Segment进行映射,如init/text/rodata,这些都是可读可执行所以合并成一个Segment来描述。对于.o文件是没有program heades的。
- sections:代码经过编译之后,将会分类链接多个section,如init/text/data/bss。
- sections headers:用于ELF文件中各sections的。

ELF header

描述ELF header的结构体

typedef struct {
 unsigned char e_ident[EI_NIDENT]; /* 16 bytes */
 Elf64_Half e_type; /* File type */
 ....
 Elf64_Addr e_entry; /* Entry point virtual address */
 Elf64_Off e_phoff; /* Prog headers file offset */
 Elf64_Off e_shoff; /* Sec headers file offset */
 ....
 Elf64_Half e_phentsize; /* Prog headers entry size */
 Elf64_Half e_phnum; /* Prog headers entry count */
 Elf64_Half e_shentsize; /* Sec headers entry size */
 Elf64_Half e_shnum; /* Sec headers entry count */
 Elf64_Half e_shstrndx; /* Sec string table index */
} Elf64_Ehdr;

可以通过readelf -h 来获取ELF的header信息。

$ readelf -h wifi_daemon
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           AArch64
  Version:                           0x1
  Entry point address:               0x401c00
  Start of program headers:          64 (bytes into file)
  Start of section headers:          44568 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28

program headers

  描述Program header的结构体。

// Program header for ELF64.
typedef struct  {
  Elf64_Word p_type;    // Type of segment
  Elf64_Word p_flags;   // Segment flags
  Elf64_Off p_offset;   // File offset where segment is located, in bytes
  Elf64_Addr p_vaddr;   // Virtual address of beginning of segment
  Elf64_Addr p_paddr;   // Physical addr of beginning of segment (OS-specific)
  Elf64_Xword p_filesz; // Num. of bytes in file image of segment (may be zero)
  Elf64_Xword p_memsz;  // Num. of bytes in mem image of segment (may be zero)
  Elf64_Xword p_align;  // Segment alignment constraint
} Elf64_Phdr;

可以通过readelf -l []来获取ELF的pragram header信息。

$ readelf -l wifi_daemon

Elf file type is EXEC (Executable file)
Entry point 0x401c00
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R      8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001b 0x000000000000001b  R      1
      [Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x000000000000899c 0x000000000000899c  R E    10000
  LOAD           0x0000000000008b60 0x0000000000418b60 0x0000000000418b60
                 0x00000000000005b4 0x00000000000005e0  RW     10000
  DYNAMIC        0x0000000000008b68 0x0000000000418b68 0x0000000000418b68
                 0x0000000000000270 0x0000000000000270  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000020 0x0000000000000020  R      4
  GNU_EH_FRAME   0x0000000000008628 0x0000000000408628 0x0000000000408628
                 0x000000000000008c 0x000000000000008c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000008b60 0x0000000000418b60 0x0000000000418b60
                 0x00000000000004a0 0x00000000000004a0  R      1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
02     .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .init_array .fini_array .data.rel.ro .dynamic .got .data .bss
   04     .dynamic
   05     .note.ABI-tag
   06     .eh_frame_hdr
   07
   08     .init_array .fini_array .data.rel.ro .dynamic .got

  从上可以看出一共有9各segment,与前面elf header信息中的Number of program headers:9对应。这里我们重点关注02和03的segment即可,因为这两个segment的类型是LOAD类型,每个segment就对应一个VMA,与我们后面关于虚拟地址到物理地址的映射有着非常重要的联系。
操作系统只关心段的权限(可读、可写、可执行),所以对于相同类型权限段可以合并到一起当作一个段来映射,所以通常的分类有一下三种:
- 可读可执行:代码块为代表
- 可读可写:data块和BSS块为代表
- 只读:rodata块为代表

sections

  下面是描述sections headers的结构体。

// Section header.
struct Elf32_Shdr {
  Elf32_Word sh_name;      // Section name (index into string table)
  Elf32_Word sh_type;      // Section type (SHT_*)
  Elf32_Word sh_flags;     // Section flags (SHF_*)
  Elf32_Addr sh_addr;      // Address where section is to be loaded
  Elf32_Off sh_offset;     // File offset of section data, in bytes
  Elf32_Word sh_size;      // Size of section, in bytes
  Elf32_Word sh_link;      // Section type-specific header table index link
  Elf32_Word sh_info;      // Section type-specific extra information
  Elf32_Word sh_addralign; // Section address alignment
  Elf32_Word sh_entsize;   // Size of records contained within the section
};

  可以通过readelf -S []来读取ELF的section header的信息。

$ readelf -S wifi_daemon
There are 29 section headers, starting at offset 0xae18:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001b  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .hash             HASH             0000000000400278  00000278
       00000000000001a0  0000000000000004   A       5     0     8
  [ 4] .gnu.hash         GNU_HASH         0000000000400418  00000418
       0000000000000024  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           0000000000400440  00000440
       0000000000000618  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400a58  00000a58
       00000000000003b8  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400e10  00000e10
       0000000000000082  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400e98  00000e98
       0000000000000060  0000000000000000   A       6     3     8
  [ 9] .rela.dyn         RELA             0000000000400ef8  00000ef8
       0000000000000030  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400f28  00000f28
       00000000000005e8  0000000000000018  AI       5    22     8
  [11] .init             PROGBITS         0000000000401510  00001510
       0000000000000018  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000401530  00001530
       0000000000000410  0000000000000000  AX       0     0     16
  [13] .text             PROGBITS         0000000000401940  00001940
       00000000000038e4  0000000000000000  AX       0     0     64
  [14] .fini             PROGBITS         0000000000405224  00005224
       0000000000000014  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         0000000000405238  00005238
       00000000000033f0  0000000000000000   A       0     0     8
  [16] .eh_frame_hdr     PROGBITS         0000000000408628  00008628
       000000000000008c  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         00000000004086b8  000086b8
       00000000000002e4  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000418b60  00008b60
       0000000000000000  0000000000000008  WA       0     0     1
  [19] .fini_array       FINI_ARRAY       0000000000418b60  00008b60
       0000000000000000  0000000000000008  WA       0     0     1
  [20] .data.rel.ro      PROGBITS         0000000000418b60  00008b60
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .dynamic          DYNAMIC          0000000000418b68  00008b68
       0000000000000270  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000418dd8  00008dd8
       0000000000000228  0000000000000008  WA       0     0     8
  [23] .data             PROGBITS         0000000000419000  00009000
       0000000000000114  0000000000000000  WA       0     0     8
  [24] .bss              NOBITS           0000000000419118  00009114
       0000000000000028  0000000000000000  WA       0     0     8
  [25] .comment          PROGBITS         0000000000000000  00009114
       0000000000000033  0000000000000001  MS       0     0     1
  [26] .symtab           SYMTAB           0000000000000000  00009148
       00000000000011a0  0000000000000018          27   103     8
  [27] .strtab           STRTAB           0000000000000000  0000a2e8
       0000000000000a38  0000000000000000           0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  0000ad20
       00000000000000f4  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

  可以看出wifi_daemon中一共有29个section,与ELF header描述内容中的Number of section headers:29是匹配的。

进程虚拟地址空间布局


  系统对虚拟地址空间进行布局,在2.3章节中描述了内核空间的划分,同样针对用户空间也有进行了划分。可以分为4类:
- 栈:用于维护函数调用的上下文,栈在用户空间的最高地址处分配,地址是向下增长。
- 堆:用户程序动态分配内存的区域,当使用malloc分配内存时,虚拟地址空间将在这个范围,地址向上增长。
- MMAP:在栈和堆空间有一个MMAP区域,主要用于mmap系统调用的映射。包括文件映射(包含动态库、文件IO)和匿名映射。
- 可执行文件映像:存储可执行文件在内存的映像,装载器会将ELF内容读取或映射到这里。
  可执行文件映像可再进行分类,前面章节描述了,程序最终编译临界成各个sections,如text,data,bss等等,但对于系统来说关注的是加载的方式,如读写执行权限,因此可执行文件映像的分类是按照权限划分的。如上分为只读权限(对应init,text等section),读写权限(对应.data,bss等)。

虚拟地址空间描述


  Linux系统操作的是虚拟地址,访问实际内存需要将虚拟地址通过MMU查询页表,转化为物理地址。用户空间的虚拟地址可以按照如上图进行分为几个segment,每个segment都对应一个VMA。虚拟地址到物理地址的映射,是每个VMA到物理地址的映射。
  VMA作为进程地址空间一块连续区域,使用struct vm_area_struct结构体进行描述。该结构体中描述了连续区域的起始地址和地址。
  VMA之间通过双向链接连接在一起,在struct vm_area_struct中vm_next,vm_prev分别指向下一个VMA和上一个VMA,使用双向链表来组织VMA,便于进程虚拟地址对VMA的插入。
  VMA同时又被加入到一棵红黑树中,在struct vm_area_struct中的vm_rb描述红黑树的节点,根节点在struct mm_struct mm_rb来描述,既然VMA通过链表串一起了,为什么再使用红黑树来组织,主要是使用红黑树能够加快进程搜索VMA的速度。
  mm_struct数据结构中的pgd指向了该进程的页表页目录,每个进程都有自己一份独立的页表,当CPU第一次访问虚拟地址空间时,如果查询页表找不到对应的物理页,将会发生缺页异常,在缺页异常中,进行分配物理页面,当然如果页表没有创建,需要先申请物理页面创建页表,最后将物理页面填充到页表中,完成虚拟地址到物理地址的映射关系。
  上图中数据结构的层级关系struct task_struct->struct mm_struct->struct vm_area_struct。
  可以通过节点/proc/[pid]/maps来查看内存mappings,下面例子中每一行都表示一个VMA。可以man proc来查看各项参数意义。

address           perms offset  dev   inode       pathname
00400000-00452000 r-xp 00000000 08:02 173521      /usr/bin/dbus-daemon
00651000-00652000 r--p 00051000 08:02 173521      /usr/bin/dbus-daemon
00652000-00655000 rw-p 00052000 08:02 173521      /usr/bin/dbus-daemon
00e03000-00e24000 rw-p 00000000 00:00 0           [heap]
...
35b1800000-35b1820000 r-xp 00000000 08:02 135522  /usr/lib64/ld-2.15.so
35b1a1f000-35b1a20000 r--p 0001f000 08:02 135522  /usr/lib64/ld-2.15.so
35b1a20000-35b1a21000 rw-p 00020000 08:02 135522  /usr/lib64/ld-2.15.so
35b1a21000-35b1a22000 rw-p 00000000 00:00 0
35b1c00000-35b1dac000 r-xp 00000000 08:02 135870  /usr/lib64/libc-2.15.so
35b1dac000-35b1fac000 ---p 001ac000 08:02 135870  /usr/lib64/libc-2.15.so
35b1fac000-35b1fb0000 r--p 001ac000 08:02 135870  /usr/lib64/libc-2.15.so
35b1fb0000-35b1fb2000 rw-p 001b0000 08:02 135870  /usr/lib64/libc-2.15.so
..
7fffb2c0d000-7fffb2c2e000 rw-p 00000000 00:00 0   [stack]
  • address: VMA对应的起始地址,对应struct vm_area_struct中的vm_start,vm_end。
  • perms:VMA的权限,r=read,w=write,x=execute,s=shared,p=private。s和p二选一,主要是判断当前的地址空间是进程私有,还是共享。
  • offset: 文件映射,表示此段虚拟内存起始地址在文件中以页为单位的偏移,匿名映射为0。
  • dev:所映射文件所属的设备号,匿名映射为0。
  • inode:映射文所属节点节点号,匿名映射为0。
  • pathname:文件映射,对应的就是映射的文件名。匿名映射,是此段虚拟内存在进程的角色,如heap,stack等。
      是否可以查看进程虚拟内存都被谁占用了,对应的VMA的大小是否有增大趋势,可以用于判断内存泄露?

操作系统角度看可执行文件的装载运行


  从操作系统角度看,一个进程最关键的特征是它拥有独立的虚拟地址空间,这使得它跟别的进程有差别。下面来看一个程序被执行比较通用的情形,流程如下:
- 创建一个独立的虚拟地址空间。
- 建立执行程序虚拟空间与可执行文件的映射关系。
- 将CPU的指令寄存器设置成可执行文件的入口函数,启动运行。
- 运行过程中,通过缺页异常将指令、数据装载进内存。
(1)创建独立的虚拟地址空间
  系统访问使用的是虚拟地址,虚拟地址通过查询页表找到对应的物理内存,因此最开始是创建好虚拟地址空间,而创建虚拟地址空间实际上并不是创建空间直接建立好跟物理内存的连续,而是先创建映射函数所需要的相应数据结构,比如task_struct,mm_struct等。对应页表的创建,实际上只分配了一个页目录就可以了,不需要设置页的映射关系,等实际程序访问的时候通过缺页异常才进行设置。
(2)建立执行程序虚拟空间与可执行文件的映射关系
  上一个步骤建立了虚拟地址空间,这一步所做的建立虚拟空间与可执行文件的映射关系,因为程序执行时发生缺页异常,系统会从物理内存分配一块内存,然后将缺页从磁盘读取到内存中,再设置缺页的虚拟页和物理页的映射关系,这样程序就可以运行了。因此,当程序发生缺页异常时,需要知道程序当前的页在磁盘的那一个位置。
Linux系统将进程虚拟地址空间分配成多个段,这个段叫做虚拟内存区域(VMA,Virtual Memory Area)。如系统创建进程后,会设置一个.text段(原则上应该时多个sections的合并)的VMA。
(3)将CPU的指令寄存器设置可执行程序文件的入口函数
这一步就是将ELF文件头中保存的入口地址赋值为PC,然后启动运行。
(4)缺页异常
  在上述步骤执行后,实际可执行文件的指令和数据都还没有装入到内存中,操作系统只是通过该可执行文件头部信息建立起可执行文件和进程虚拟内存空间直接的映射关系,当程序启动运行时,执行相关的虚拟地址,当发现这虚拟地址对应的物理页面为空,将会发生缺页异常,缺页异常会分配一块内存,然后将可执行文件的指令和数据从磁盘加载到内存中,后续就可以直接访问内存进行读写访问执行了。

VMA的操作函数

VMA查找

VMA插入

VMA合并与拆分


malloc函数


  用户空间分配内存,不会每次申请都向linux内核做一个系统调用进行分配获取内存,而是在用户空间维持着一个缓冲池,这个缓冲池有自己的内存管理算法。当用户进行malloc时候,如果缓存池中有内存,就直接获取返回,如果缓存池中没有内存,就会下陷做系统调用到linux内核中获取内存。
  用户空间通过系统调用向内核获取内存时分为两种情况:当分配的内存小于M_MMAP_THRESHOLD阈值时会使用brk系统调用来扩展堆空间,当分配内存大于M_MMAP_THRESHOLD阈值时,会使用mmap进行映射分配。M_MMAP_THRESHOLD通常为128K,用户可以通过调用mallopt函数来修改该阈值。
  malloc通过brk方式申请的内存,free释放内存时,并不会归还给系统,而是缓存到malloc内存池中,待下次使用。
  malloc通过mmap申请到的内存,free释放内存时,会把内存归还给系统,内存得到真正释放。

与malloc相关的函数

malloc系统调用流程

待补充

mmap函数

基本概念


  mmap用于内存映射,将一段区域映射到自己的进程地址空间中。这段区域可以是文件页属性也可以是物理页属性,所以分为两种:
- 文件映射:将文件映射到进程空间,文件存放在存储设备上(文件内容会以page cache缓存到物理内存中)。
- 匿名映射:没有文件对应的区域,内容在物理内存上。
  mmap用于文件映射能提高读写效率,主要的差异点是常规的文件操作需要从磁盘到页缓存拷贝,然后内核空间到用户空间还有拷贝,有两次数据拷贝动作;而mmap操控文件,只需要从磁盘到用户主存一次拷贝,后续的读写直接对主存读写,相当于少了依次内核到用户空间的拷贝。
mmap针对进程是否可见,有分为两种:

- 私有映射:数据源拷贝一次副本,进程之间互不影响。
- 共享映射:共享的进程都能看到。
根据排列组合就有4中映射情况:
- 私有匿名映射:可以用于分配大的内存,如malloc堆空间。
- 共享匿名隐射:可用于父子进程间通信,在内存文件系统中创建/dev/zero设备。
- 私有文件映射:常用于动态库的加载,如代码段,数据段等。
- 共享文件映射:非父子之间的进程间通信,文件读写等。

实现原理

do_mmap函数调用流程待补充.....

  举例mmap文件内存映射的实现过程,可以分为三个阶段:
- 1.创建mmap虚拟地址空间VMA
  如果mmap没有指定虚拟地址空间区域,则搜索一段空闲的连续虚拟地址空间,并分配一个vm_area_struct实例添加到红黑树和链表中。
- 2.建立VMA与文件物理地址(在磁盘那个位置)的映射关系
  通过虚拟文件系统inode模块定位到文件在磁盘的位置,建立VMA与文件的联系。 call_mmap->file->f_op->mmap?
- 3.访问文件对应的虚拟地址,引发缺页异常,将文件内容加载到内存
  前面阶段,并没有将文件的数据拷贝到内存中,真正的文件读取是进程发起读写操作时。进程在读货写操作访问映射的虚拟地址空间,通过查询页表,发现这一段地址并不再物理页面上,引发缺页异常,于是先从磁盘加载数据到内存中。后续堆文件的读写,直接就对对应的物理内存读写,当改变了内容,系统会自动将脏页面写回到磁盘上,当前也可以强行同步(msync)

缺页异常

  Linux系统有一个重要的特性就是用户“欺骗性”,如通过malloc申请了内存,但是实际上并没有分配内存给你,等实际访问内存的时候才会分配给你。用户很多重要的初始化操作只是针对虚拟内存的,虚拟内存实际对应的物理内存空间并没有分配,等实际需要访问的时候才会进行分配,因此当进程访问虚拟地址空间,发现虚拟地址空间没有与物理内存建立映射关系,处理器就会自动触发缺页异常(缺页中断)。下面是触发缺页异常的一些场景情形:

  缺页异常会执行到对应的中断函数,会跟实际的处理器架构有些关系,在实际中缺页异常最终会调用到do_page_fault(arch/arm64/mm/fault.c)函数,接下来将会从这个函数进行重点分析。

do_page_fault

匿名页面


  发生匿名页缺页异常,一般是①malloc/mmap分配进程地址空间区域,没有对应的物理内存将会触发分配。② 用户栈不够时,进行栈区的扩大处理。
  匿名页面分配时,会判断页面是否可写,如果是只读权限,那么系统会分配一个zero page。Zero page是一个特殊的物理页(实际没有使用物理内存空间),里面值全部为0,zero page针对匿名页场景专门进行的优化,主要是节省内存和对性能的一定优化。当malloc或者mmap分配内存仅仅是进程地址空间中的虚拟内存,如果用程序需要读这个malloc分配的虚拟内存,那么系统会返回全0的数据,因此linux内核不必为这种情况单独分配物理内存,而是使用系统零页,当程序需要写入这个页面时就会触发一个缺页异常,于是缺页异常变成写时复制的缺页异常。malloc分配虚拟内存,有以下几种情况。
- malloc分配后,直接读内存,这时缺页异常,分配到的是zero page,PTE的属性是read only。
- malloc分配后,先读后写,先读的时候缺页异常分配的是系统零页,再写再触发缺页异常触发写时复制。
- malloc分配后,直接写内存,触发缺页异常,使用alloc_zerod_user_highpage_movable分配新页面。

文件页面


  文件页面异常分为读文件异常,写私有文件异常,写共享文件异常。总体的思路是没有分配物理页面,就进行分配物理页面,然后将内容从文件中拷贝到物理页面中,再建立好页表。
- 读文件异常:会尝试多映射数据周围的内容,因为周围数据再次被命中的概率比较高,这样减少缺页异常次数。
- 写私有文件:写私有文件会发生写时复制,会先分配一块物理页面cow_page,先将文件内容读取文件缓存页(page cache),然后再将其内容复制到cow_page中。
- 写共享文件:写共享文件不会发生写时复制,如果mkwrite函数不为空,将会通知进程页面变成可写。同时会将页设置为脏页。

swap页面换入


  当内存不足时,会把页面交换到磁盘swap分区中,当再次访问这块内存时会发生缺页异常,大致的流程就是搜索swap cache看页面是否在内存中,如果不在说明被交换出去了,那就需要从磁盘里面读出来,然后重新刷新页表,重新建立虚拟地址和物理页面的映射关系。

页面写时复制COW

  通常有以下两种情况会触发写时复制(Copy on write,CoW)。
(1)父进程创建一个子进程,为了避免复制物理页,子进程和父进程以只读方式共享的匿名页和文件页,当有一个进程需要写只读页时,将会触发页错误异常,进程会拷贝一份新的物理页进行写。
(2)进程创建了一个私有文件映射,当进行读访问时,缺页异常将文件内容读取到page cache中,并将以只读方式跟虚拟页建立映射关系。当进程再对改内容进行写时,缺页异常会触发写时复制,为page cache创建一个副本,新建一个虚拟页与复制的物理页建立联系。

vm_normal_page从页表项得到页帧号,如果返回值为NULL,表示这是一个特殊的页映射,这种特殊的页映射只有页帧号,没有对应实际的物理页,具体是什么用途,需要再研究?
如上图,写时复制一共有四种情形。
- 写时复制:wp_page_copy
- 可写且共享的特殊映射页面:wp_pfn_shared
- 可写且共享的普通页面:wp_page_shared
- KSM匿名页面(复用的页面):wp_page_reuse
下面重点看看wp_page_copy的流程:

  1.为什么会产生page fault?
Page fault是硬件提供的特性,由硬件触发,触发条件为CPU访问某线性地址时,如果没有找到对应可访问页表项,则由硬件直接触发page fault。
  2.发生缺页的地址是否可以位于内核态地址空间?
有可能,内核地址空间发生缺页异常仅可能在vmalloc区,线性映射区域对应的页表在内核初始化就已经建立好了,所以这部分内存对应的虚拟地址空间不可能产生page fault。

RMAP

  反向映射是物理页面page可以寻找到其对应的虚拟地址空间VMA。当进行页面回收的时候,就需要利用反向映射技术找到其对应的进程VMA,然后将VMA与当前页面断开映射关系,即可进行回收当前页面。
  一个物理页面是可以同时被多个进程的虚拟页面映射的,但是一个虚拟页面只能映射一个物理页面。不同虚拟页面映射到同一物理页面的场景主要有子进程复制了父进程的VMA以及KSM机制的存在。

关键数据结构

基本原理

映射到一个进程的反向映射

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    ......
    __anon_vma_prepare(vma)
    page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
    page_add_new_anon_rmap(page, vma, vmf->address, false);
    ......
}

  以匿名映射为例说明:
  在缺页异常分配物理页时,会调用两个函数做RAMP相关的处理:__anon_vma_prepare,以及page_add_new_anon_rmap。

  创建过程:
(1)分配页面的时候,为每个VMA创建一个AVC,然后再创建一个AV。
(2)AVC->vma指向VMA,AVC->AV指向AV。
(3)将AVC添加到VMA->anon_vma_chain链表中。
(4)将AVC添加到AV->rb_root红黑树中。
(5)将page->mapping指向av。
  反向映射过程:
(1)通过page->mapping找到av。
(2)在av->rb_root红黑树中从根节点进行遍历avc。
(3)从avc中avc->vma找到vma。

void page_add_new_anon_rmap(struct page *page,
    struct vm_area_struct *vma, unsigned long address, bool compound)
{
    int nr = compound ? thp_nr_pages(page) : 1;

    VM_BUG_ON_VMA(address < vma->vm_start || address >= vma->vm_end, vma);
__SetPageSwapBacked(page);
设置page的标志位位PG_swapbacked,表示页面可以交换到磁盘。
    if (compound) {
        VM_BUG_ON_PAGE(!PageTransHuge(page), page);
        /* increment count (starts at -1) */
        atomic_set(compound_mapcount_ptr(page), 0);
        if (hpage_pincount_available(page))
            atomic_set(compound_pincount_ptr(page), 0);

        __mod_lruvec_page_state(page, NR_ANON_THPS, nr);
    } else {
        /* Anon THP always mapped first with PMD */
        VM_BUG_ON_PAGE(PageTransCompound(page), page);
        /* increment count (starts at -1) */
        atomic_set(&page->_mapcount, 0);
    }
    __mod_lruvec_page_state(page, NR_ANON_MAPPED, nr);
__page_set_anon_rmap(page, vma, address, 1);
设置页面位匿名映射
}

static void __page_set_anon_rmap(struct page *page,
    struct vm_area_struct *vma, unsigned long address, int exclusive)
{
    struct anon_vma *anon_vma = vma->anon_vma;
anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;
WRITE_ONCE(page->mapping, (struct address_space *) anon_vma);
Page中的mapping指向anon_vma
    page->index = linear_page_index(vma, address);
}

映射到多个进程的反向映射

未发生写时复制

  父进程创建子进程时,如果没有还没有进行写操作只有读操作,为了节省内存,父子是共享物理页面的,只有当父子进程需要修改页面内容是,才会发生写时复制一份。

- 遍历父进程的VMA,子进程进行复制一份VMA。
- 每复制一份VMA,就创建一个AVC(下图的AVC_c),用于建立父子之间的桥梁联系。AVC_c(AVC_c->anon_vma)指向父AV并添加到父AV(AV->rb_root)红黑树中,同时AVC_c(AVC_c->vma)又指向子进程的VMA。这样在遍历父AV的红黑树,就能通过AVC_c找到子进程的VMA。
- 子进程创建属于自己的AVC/AV,并建立VMA,AVC,AV的联系。
- 子进程复制了父进程的pte,所以子进程的vma对应虚拟页面也父进程的虚拟页面同时指向物理页面page(二对一的情况)。

  物理页面page寻找虚拟页面过程:
- 通过page->mapping找到父进程的AV,如上图AV0。
- 遍历AV0的红黑树,这里找到两个AVC节点,分别是父AVC0节点以及父子进程桥梁节点AVC_c。
- 通过父AVC0找到父进程的VMA0(AVC0->vma指向父VMA0)。
- 通过桥梁AVC_c找到子进程的VMA1(AVC_c-vma指向子VMA1)
  经过上述过程,物理页面page就可以找到与其对应的两个虚拟页面了。

发生写时复制


  当父子进程的某一方出现写页面操作时,将会触发写时复制复制一份自己的物理页面,如上图,VMA1的pte指向复制的新物理页,同时物理页page->mapping指向AV1,这样copy page就能够通过AV1遍历其红黑树找到AVC1,进而通过AVC1找到VMA1。
  疑问:上图中AVC_c依旧在AV0的红黑树中,所以父进程的物理页page依旧通过AV0找到子进程的VMA1,但是子进程实际的pte已经指向copy page了,跟父进程的page没有关系了。网上看到的说法是,为了解决这个问题,即使page找到了对应的VMA,会检查vma的页表是否确实映射到了此页,进而解决这个问题,需要进一步研究代码求证。

反向映射的应用


  反向映射主要的应用场景如下:
- Kswapd回收页面是,需要断开映射到物理页面的PTE。
- 页面迁移时,需要断开所有映射到页面的PTE。