• 操作系统内存换入-请求调页---14



    引言

    虚拟内存是实现分段和分页的关键所在,而分段和分页是操作系统管理内存的两个核心机制。

    而换入和换出又是实现虚拟内存的关键。


    段、页同时存在

    在这里插入图片描述
    对于用户而言,程序分段后可以随意的在虚拟内存上划分空间进行放入,并且访问也只需要给出访问虚拟内存的虚拟地址即可。

    而虚拟地址映射到物理地址这个过程对用户是透明的。


    用户眼里的内存!

    在这里插入图片描述
    想要让用户可以随意操作这4G大小的虚拟内存,就必须做好虚拟地址到物理地址的映射关系。

    但是物理内存往往可能没有虚拟内存大,那么此时怎么才能让多出来的虚拟内存映射到物理内存上呢?


    用换入、换出实现“大内存”

    当虚拟内存有4G大小,而物理内存只有1G大小的情况下,如何才能让1G大小的物理内存给用户带来4G虚拟内存的使用体验呢?

    在这里插入图片描述
    利用换入和换出思想就可以实现上面的需求:

    • 假设用户启动了程序1,那么首先需要为程序1在虚拟空间中开辟段空间存放,然后创建对应的页表,然后将程序1从磁盘读入到物理内存。

    在这里插入图片描述

    • 假设用户此时又启动了程序2,那么还是重复前面两个步骤

    在这里插入图片描述

    • 最后要将程序2读入物理内存时,发现进程1已经占满了物理内存,那么此时我们就需要将进程1相关数据换出到磁盘保存,然后再将进程2相关数据读入到物理内存

    在这里插入图片描述
    通过物理内存的换出和换入,就可以实现1G的物理内存向用户模拟提供4G虚拟大内存的服务。

    在这里插入图片描述
    这一节我们先来探讨一下换入是怎么完成的.


    请求调页

    • 当用户发出的load addr命令,对应的addr虚拟地址经过解析查询页表后,发现对应的虚拟页号还没有和实际的物理页号进行映射,说明当前页数据还存在于磁盘上,没有读入到内存中来,这也被称为缺页异常,这个过程之前讲过,由MMU完成
    • 当MMU发现了缺页异常后,会发出缺页中断,对应的intr寄存器中缺页位中断信号被设置为1,表示发生了缺页中断请求
    • 因为每执行完一条指令,都会去看是否有对应的中断产生,因此执行完当前load addr指令后,会发现产生了缺页中断
    • 那么随即会去执行缺页处理的中断程序,对应的页错误处理程序,会通过相关算法和数据结构,去磁盘中调度对应缺失的页,并且会在内存中找到一个空闲页,来存放从磁盘读取出来的缺失页数据
    • 然后建立页表关于缺失页相关的映射,然后中断处理程序结束。

    按理来说,当调用load addr指令发生缺页中断时,pc自动加一,那么中断程序结束后,回来执行的应该是load addr下一条指令才对,但是这里load addr虽然执行完了,但是因为mmu接收到的虚拟地址解析后发生了缺页异常,因此这里需要进行特殊处理:

    • 当MMU发生了缺页异常后,会抛出对应的缺页中断,然后保持pc值不变,这样中断处理程序结束后,回来执行的依旧是load addr指令,对于用户而言,仿佛什么都没有发生,只是当前指令执行的耗时会多一些

    在这里插入图片描述
    详细过程可以看下图:
    在这里插入图片描述


    • 问题:采用请求调页而不是请求调段,是因为?( )
      在这里插入图片描述

    一个实际系统的请求调页

    这个故事从哪里开始?

    • 请求调页,当然从缺页中断开始

    查询对应的手册可以发现,缺页中断对应的中断号为14。

    在操作系统初始化过程中的trap_init中断初始化函数中,可以看到14号中断的初始化过程:
    在这里插入图片描述

    和GDT一样,中断描述符表在系统最多只能有一个,中断描述符表内可以存放256个描述符,分别对应256个中断


    处理中断page fault

    • page_falut中断程序执行肯定要进入内核,首先就是把用户栈相关寄存器状态压栈,也被称为保护现场
    //在linux/mm/page.s中
    .globl _page_fault
    //错误码被压 到了栈中
    xchgl %eax,(%esp)
    pushl %ecx
    pushl %edx
    push %ds
    push %es
    push %fs
    movl $0x10, %edx
    mov %dx, %ds
    mov %dx, %es
    mov %dx, %fs
    //页错误线性地址---访问发生页错误的虚拟地址
    movl %cr2, %edx
    //压入参数
    pushl %edx
    pushl %eax
    //测试标志P
    testl $1, %eax
    jne 1f
    //调用对应的c函数
    call _do_no_page
    jmp 2f
    1: call _do_wp_page //保护
    2: add $8, %esp
    pop %fs
    pop %es
    pop %ds
    pop %edx
    pop %ecx
    pop %eax
    iret
    
    • 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
    • 29
    • 30
    • 31
    • 32
    • 33

    上面这段汇编代码中,比较重要的是下面几句:

    ...
    //错误码被压 到了栈中
    xchgl %eax,(%esp)
    ...
    //页错误线性地址---访问发生页错误的虚拟地址
    movl %cr2, %edx
    //压入参数
    pushl %edx
    pushl %eax
    ...
    //调用对应的c函数
    call _do_no_page
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • cr2寄存器用来存放发生页错误时,对应的虚拟地址
    • 而这里将cr2中保存的虚拟地址赋值给了edx寄存器
    • 然后将eax和edx分别压入栈中,即分别将错误码和产生错误的虚拟地址压入栈中
    • 调用对应的c函数来处理缺页异常

    这里最后压入栈中的eax和edx是作为_do_no_page方法的参数值的。


    do_no_page

    执行缺页处理,页异常中断处理过程中调用的函数。在page.s程序中被调用。函数参数error_code和address是进程在访问页面时由CPU因缺页产生异常而自动生成。error_code指出出错类型,address是产生缺页的线性地址。

    该函数首先查看所缺页是否在交换设备中,若是则交换进来。否则尝试与已加载的相同文件进行页面共享,或者只是由于进程动态申请内存页面只需映射一页物理内存页即可。若共享操作不成功,那么只能从相应文件中读入所缺的数据页面到指定线性地址处

    //在linux/mm/memory.c中
    //错误码和对应产生错误的虚拟地址
    void do_no_page(unsigned long error_code,unsigned long address){
    
    //虚拟地址最后12位页内偏移清零,拿到的是虚拟页号
    address&=0xfffff000; //页面地址
    
    // address是线性页面地址,current->atart_code是进程在线性地址空间的起始地址,
    // 两个相减所以tmp是事发地点的逻辑地址(即进程逻辑空间偏移量)
    tmp=address–current->start_code; //页面对应的偏移
    
    // 缺页中断有多种情况,这里是第一种。
    // current->executable == 0 表明当前进程没有可执行文件。tmp>=current_end_data
    // 表示缺页的逻辑地址大于进程的代码段和数据段之和。这两种情况都对应着第一种缺页
    // 异常来历:即当前缺页是由于进程压栈(为堆或栈中数据寻找新的页面)造成的,因此
    // 直接调用get_empty_page为进程申请一页新物理内存即可。
    if(!current->executable||tmp>=current->end_data){
    get_empty_page(address); return; }
    
    // 若走到这里,则说明缺页异常不是由于进程压栈造成,那肯定就是执行exevce来得
    // 进程的executable时导致缺页异常的。
    // 于是乎先尝试share_page。即先看当前进程的executable是否被其他进程同样引用,若
    // 能在其他进程中找到并且与其共享这页物理内存是最好了(因为这样省了读,而且避免了
    // 同样地内存被多次加载到内存占地方又浪费时间)
    if (share_page(tmp))
    return;
    
    // 要是没能share成功,退而求其次吧。只能为该进程注册一页新的物理内存,并且读取
    // 相应内容到这页物理内存中了。
    
    //申请一个空闲页
    page=get_free_page();
    
    //从磁盘将数据读入该空闲页
    //current->executable就是当前正在执行的程序的可执行文件
    //current->executable->i_dev当前正在执行的程序可执行文件对应的设备
    bread_page(page, current->executable->i_dev, nr);
    //建议虚拟页号和当前空闲页的映射关系
    put_page(page, address);
    
    • 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    void get_empty_page(unsigned long address){ 
    unsigned long tmp = get_free_page();
    put_page(tmp, address);
    }
    
    • 1
    • 2
    • 3
    • 4

    put_page

    put_page用来完成物理页面与一个线性地址页面的挂接,从而将一个线性地址空间内的页面落实到物理地址空间内,

    page为物理地址,address为线性地址

    //在linux/mm/memory.c中
    unsigned long put_page(unsigned long page, //物理地址
    unsigned long address){
    
    unsigned long tmp, *page_table;
    
    // 要映射的线性地址只能在主内存区,你不能给内存起始1M范围内映射线性地址
    // 因为LOW_MEM内是给内核用的,且不属于分页内存范围。
    if (page < LOW_MEM || page >= HIGH_MEMORY)
    printk("Trying to put page %p at %p\n",page,address);
    
    // (page-LOW_MEM)>>12得到page这个物理地址对应的页号
    // mem_map[(page-LOW_MEM)>>12] !=1是检查这个page地址对应的物理内存页面
    // 是不是已经注册过的页面(用get_free_page函数申请,申请时会再mem_map中
    // 注册这个页面,也就是把这个页面对应的mem_map项加1。所以此处==1就代表
    // 这个物理页面是刚申请好备用的,只有这种物理页面才能被映射到一个线性地
    // 址。所以这里!=1就要警告)
    if (mem_map[(page-LOW_MEM)>>12] != 1)
    printk("mem_map disagrees with %p at %p\n",page,address);
    
    
    //((address>>20)&ffc)---拿到的是虚拟地址中的页目录项,该页面项可以看做是一个指针
    //这个指针用来得到对应页表的基地址
    //再配合address的次10位
    //就能寻址到address对应的内存页面基地址了。
    //最后12bit用来在页面内寻址。
    page_table=(unsigned long *)((address>>20)&ffc); 
    
    // 检验address对应的页目录表项内容,即页表地址是不是有效地,若页表有效,则
    // 直接把页表地址给page_table;若页表无效,则调用get_free_page函数在mem_map
    // 数组内申请一个空闲页面,并将这个空闲页面设置为用户级、只读、存在,再将
    // 新申请并处理好的页面地址赋值给page_table
    if((*page_table)&1)
    page_table=(unsigned long*)(0xfffff000&*page_table);
    else{
    if (!(tmp=get_free_page()))
    return 0;
    *page_table = tmp|7;
    page_table = (unsigned long *) tmp;
    }
    
    // 上面已经得到了一个有效地page_table了,现在就要将page这个物理页面和address
    // 这个线性地址处的页面挂接起来了。挂接的方法就是在page_table中,将address对应
    // 的页表项中的内容(即address这个地址所在的页面基地址)设置为page这个物理地址
    // |7没什么好说的,这里值得关注的是page_table竟然还可以当做数组来用···刚开始
    // 着实吃了一惊,不过仔细想想page_table本来就是个指针,而1024个页表项也和数组
    // 结构一样的,用数组的方式来访问也是可行的(当然我还是觉得用指针+偏移量更好理解)
    // 再次感叹C语言的灵活啊
    // 再说明下(address>>12)&0x3ff吧,这个操作实际是取出address次10位表示页面在页表内的偏移值。这样用这个偏移值
    // 结合page_table这个页表首地址,就能找到address所在的页面基地址了。
    
    //简而言之: 取出虚拟address次10位,作为虚拟页号在页表中的下标索引,然后将当前页表该索引处的值设置为物理页面的地址
    page_table[(address>>12)&0x3ff] = page|7;
    return page; 
    }
    
    • 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    联系上一节

    上一节中,我们讲到copy_page_tables函数只是为子进程申请了一块空闲物理页用来作为子进程的起始页表:

    if (!(to_page_table = (unsigned long *) get_free_page()))      
                return -1;  /* Out of memory, see freeing */  //取空闲物理页为to_page_table赋值       
    *to_dir = ((unsigned long) to_page_table) | 7;    //将页表存进相应页目录项,//7表示可读写
    
    • 1
    • 2
    • 3

    操作系统段页结合的实际内存管理–13

    此时并没有把子进程申请的页表对应的物理页面和用户进程的线性地址空间进行映射。某一个物理页面和进程的线性地址空间映射是指,将这个页面的物理地址存入该进程线性地址空间所对应的页表的某个页表项当中,这样进程才有权限对该页面进行访问。

    具体映射函数就是上面分析的put_page(page, address),用来完成物理页面与一个线性地址页面的映射,从而将一个线性地址空间内的页面落实到物理地址空间内,其中page就是某个物理页面的物理地址,address是要映射的进程的线性地址。

    在复习一遍put_page函数的流程:

    • 首先会根据该线性地址前10位得到页目录项
    • 其次页目录项取*号得到的就是页表的物理地址,这个页表也就是这个进程拥有的
    • 然后根据线性地址接下来的10位,得到相对于这个页表的位置索引
    • 最后把这个地址存入位置索引对应的页表项。

    之所以说,并没有把页表对应的页面和用户进程的线性地址空间进行映射,是因为并没有把页表对应的物理地址放入这个页表的某个页表项当中。

    如果要映射,相当于把页表自己的物理地址放入页表自己的某个页表项当中,只有这样,才能说将页表映射到了进程的线性地址空间,也只有这样,用户进程才有权限访问该页表本身。

    总之,用户进程只能访问其线性地址空间所对应的页表(前10位找到页目录项,页目录项中的内容就是该线性地址所对应的页表的物理地址),中的页表项所指向的物理内存页面。

    而不能访问内核管理进程所使用的页表本身,也就是用户进程读写不了页表的页表项,但可以读写页表项中指向的物理页面。

    而内核将所有物理内存16MB和自己的16MB线性地址空间全部映射了,也就是将16MB物理内存所有页面的物理地址全部存入16MB线性地址空间所对应的4个内核页表(4M对应一个页表)的页表项当中了,当然也包括内核为进程分配的页表对应的页面的物理地址,因此内核有权限读写管理所有进程的页表,即进程的页表位于内核的线性地址空间。


    参考

    Linux内存管理之copy_page_tables源码理解

  • 相关阅读:
    复杂问题问答
    【服务器 | 宝塔】宝塔面板卸载重装教程:清理删除宝塔面板并重新开始
    【JAVA毕业设计源码】基于微信小程序的批发零售业管理系统
    早餐与风景
    Day2:写前端项目(html+css+js)
    本地快速让某个目录变成服务器访问
    11.LoadRunner录制的方式
    SpringMVC篇
    工作积累——JPA事务中数据更新后查询结果为旧数据的问题
    【JAVASE系列】08_抽象类与接口
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/125984408