Fork me on GitHub
0%

fork与mmap的底层原理

引言

上一篇主要对页中断的机制进行简单的了解并对源码进行相关的阅读,而过程中依赖于页中断的场景是比较多的。本篇主要针对fork、execve、mmap三个系统调用的原理进行分析。对于它们所完成的功能背后都是由页中断在背后进行默默支撑的。

fork

函数原型

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
  • 功能

    以当前进程作为父进程,将所有资源进行拷贝创建一个子进程。

  • 返回值

    • <0,fork函数调用失败
    • =0,父进程返回值
    • >0,子进程返回值,实际上是子进程的PID

    当fork系统调用成功时,它会返回两个值:0(父进程),新的子进程的PID(>0

工作流程

  1. 复制一份进程控制块(Process Control Block,PCB)。

    这个过程我理解类似于复制粘贴,此时父子进程拥有相同的虚拟资源(各自一份)和硬件资源(同一个)。PCB结构体内所存储的所有内容对两个进程全部公开且共享。

  2. 复制页表并将PTE添加写保护

    把页表中所有正常状态的数据段、堆和栈空间的虚拟内存页,设置为不可写,然后把已经映射的物理页面的引用计数加 1。

  3. 两个进程加入操作系统调度队列(fork作用结束)

  4. 如果父子进程都没有写操作,则正常调度直至两个进程结束,由于复制PCB两个进程的IP寄存器指向fork调用所以有两个返回值

  5. 父子进程如果有一个发生写操作,由于页表项设置成不可写,所以一定发生写保护中断

    • do_page_fault

    • handle_pte_fault

      判断中断发生类型。PTE页表项PRESENT置位说明已经映射了物理页面,同时flag参数是写请求但是判断PTE并没有写权限,说明是一次写保护中断

    • do_wp_page

      判断发生中断的虚拟地址所对应的物理地址的引用计数,如果大于 1,就说明现在存在多个进程共享这一块物理页面,那么它就需要为发生中断的进程再分配一个物理页面,把老的页面内容拷贝进这个新的物理页,最后把发生中断的虚拟地址映射到新的物理页。这就完成了一次写时复制 (Copy On Write, COW)

mmap

函数原型

1
2
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 参数

    • addr:用户进程中要映射的用户空间的起始地址,通常为NULL(由内核来指定)

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

    • prot:期望的内存保护标志

      1
      2
      3
      4
      PROT_EXEC 		// 页内容可以被执行
      PROT_READ // 页内容可以被读取
      PROT_WRITE // 页可以被写入
      PROT_NONE // 页不可访问
    • flags:指定映射对象的类型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      MAP_FIXED(***)
      //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。该参数是用于固定mmap第一个参数addr的
      MAP_SHARED(***)
      //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。相当于创建了一个共享映射区,一个进程对该文件的修改,其他进程也可以观察到,这就实现了数据的通讯
      MAP_PRIVATE(***)
      //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
      MAP_DENYWRITE
      //这个标志被忽略
      MAP_EXECUTABLE
      //同上
      MAP_NORESERVE
      //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
      MAP_LOCKED
      //锁定映射区的页面,从而防止页面被交换出内存。
      MAP_GROWSDOWN
      //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
      MAP_ANONYMOUS(***)
      //匿名映射,映射区不与任何文件关联(需要注意此时,fd参数需要置空)
      MAP_ANON
      //MAP_ANONYMOUS的别称,不再被使用。
      MAP_FILE
      //兼容标志,被忽略。
      MAP_32BIT
      //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台 上得到支持。
      MAP_POPULATE
      //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
      MAP_NONBLOCK
      //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
    • fd:文件描述符(由open函数返回)

    • offset:设置在内核空间中已经分配好的的内存区域中的偏移,例如文件的偏移量,大小为PAGE_SIZE的整数倍

  • 返回值

    mmap()返回被映射区的指针,该指针就是需要映射的内核空间在用户空间的虚拟地址

函数功能

私有匿名映射

  • 应用场景:分配堆空间(本质上来说,这部分申请的空间是从文件映射区映射得到的,但是功能上与堆空间类似,暂且可以称之为堆)
  • 参数
    • flag:MAP_PRIVATE、MAP_ANONYMOUS
    • fd:-1
  • 原理
    1. 在文件映射区分配用户所需要的内存大小
    2. 创建VMA结构,不进行实际物理内存的分配,返回
    3. 写内存时,调用do_anonymous_page 进行物理内存的分配
    4. 建立虚拟内存到物理内存的映射关系

私有文件映射

  • 应用场景:加载动态链接库

  • 参数

    • flag:MAP_PRIVATE
    • fd:非空,该参数填充使用open系统调用打开的文件描述符
  • 原理:

    1. 在文件映射区分配用户所需要的内存大小
    2. 创建VMA结构,不进行实际物理内存的分配,返回
    3. 第一次访问,一定是缺页中断由于是文件映射,则调用do_fault函数进一步判断
    4. 如果是读操作,调用do_read_fault
    5. 如果是写操作,并且是私有映射调用do_cow_fault
    6. 私有文件映射的代码段(只读页)是在多进程间共享的,而数据段(可写页)是私有的在每一个进程中独立,而这个副本仍然是写时复制
  • 其它

    在linux文件系统上,磁盘上的每一个文件都有一个inode结构与之对应,且这个inode结构在整个系统上是唯一的,也就是说多个进程看到的inode结构是一样的。

    该inode包含有关文件或目录的元数据信息,如文件类型、文件大小、权限、所有者、时间戳和数据块指针。

    在 inode 结构中,有一个哈希表,以文件的页号为 key,以物理内存页为 value。当进程 A 打开了文件 f,然后读取了它的第 4 页,这时,内核就会把 4 和这个物理页,放入这个哈希表中。当进程 B 再打开文件 f,要读取它的第 4 页时,因为 f 的第 4 页的内容已经被加载到物理页中了,所以就不用再加载一次了。只需要将 B 的虚拟地址与这个物理页建立映射就可以了

共享文件映射

  • 应用场景:多进程通信

  • 参数

    • flag:MAP_SHARED
    • fd:非空,该参数填充使用open系统调用打开的文件描述
  • 原理

    共享文件映射的原理与私有文件的映射原理几乎一致,但是共享文件的需求就是要求每一个进程对文件的修改对其他文件都可见,所以对于可写页面也不会创建副本而是共享。

    而当发生缺页中断的时候,最终调用的函数为do_share_fault(文件映射、共享映射)

共享匿名映射

  • 应用场景:父子间的进程通讯

  • 参数

    • flag:MAP_ANONYMOUS、MAP_SHARED
    • fd:-1
  • 代码示例

    1
    2
    3
    // 父进程创建一块内存区域,子进程也可以看到这块区域
    addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANOUNYMOUS, -1, 0);
    child_pid = fork();
  • 原理

    当fork子进程的时候,会将vma结构一起复制一份。父子进程都可以看到这块区域,对于这块内存是共享的可读写,所以后面的操作与共享文件映射是一致的。而当发生写操作的时候,写保护中断发生作用,调用do_wp_fault,这个流程就可以参考fork的操作。

功能总结

image

参考资料

<<编程高手必学的内存知识 – 10>> 海纳