精华内容
下载资源
问答
  • Linux的内存管理分为虚拟内存管理和物理内存管理,本文主要介绍虚拟内存管理的原理和实现。在介绍虚拟内存管理前,首先介绍一下x86 CPU内存寻址的具体过程。 x86 内存寻址 Intel x86 CPU把内存地址分为3种:逻辑...

    目录

    x86 内存寻址

    x86 分页机制

    虚拟内存地址管理

    虚拟地址与物理地址映射 - do_page_fault()

    推荐阅读


     

    Linux的内存管理分为 虚拟内存管理 和 物理内存管理,本文主要介绍 虚拟内存管理 的原理和实现。在介绍 虚拟内存管理 前,首先介绍一下 x86 CPU 内存寻址的具体过程。

     

    x86 内存寻址


    Intel x86 CPU 把内存地址分为3种:逻辑地址线性地址 和 物理地址

    • 逻辑地址: 由 段寄存器:偏移量 组成(段寄存器 为16位,偏移量 为32位),偏移量 是应用程序能够直接操作的地址,比如在C语言中使用 & 操作符取得的变量地址就是 逻辑地址
    • 线性地址:也称为 虚拟地址,是通过 CPU 的分段单元把 段寄存器:偏移量 转换成一个32位的无符号整数,范围从 0x00000000 ~ 0xFFFFFFFFF。分段机制的原理是,段寄存器指向一个段描述符,段描述符里面包含了段的基地址(开始地址),然后通过基地址加上偏移量就是线性地址。
    • 物理地址:内存中的每个字节都由一个32位的整数编号表示,而这个整数编号就是内存的 物理地址。比如在安装了4GB内存条的计算机中,能够寻址的物理地址范围为 0x00000000 ~ 0xFFFFFFFFF。在开启了分页机制的情况下,线性地址要经过分页单元转换才能得到 物理地址

    下图展示了 逻辑地址线性地址 和 物理地址 三者的关系:

    x86 分页机制


    前面介绍过,应用程序中的逻辑地址需要通过分段机制和分页机制转换后才能得到真正的物理地址。由于Linux把代码段和数据段的基地址都设置为0,所以逻辑地址中的偏移量就等价于线性地址。所以这里就不介绍分段机制了,有兴趣可以查阅相关的文章或者书籍。

    由于Linux主要使用分页机制,所以下面重点介绍一下分页机制的原理。

    由于CPU只能对 物理地址 进行寻址,所以 线性地址 需要映射到 物理地址 才能使用,而映射是按 页(Page) 作为单位进行的,一个页的大小为4KB,所以32位的线性地址可以划分为 2^32 / 2^12 = 2^20 (1048576) 个页。

    映射是通过 页表 作为媒介的。页表是一个类型为整型的数组,数组的每个元素保存了线性地址页对应的物理地址页的起始地址。由于32位的线性地址可以划分为 2^20 个页,而每个线性地址页需要一个整型来映射到物理地址页,所以页表的大小为 4 * 2^20 (4MB)。由于并不是所有的线性地址都会映射到物理地址,所以为了节省空间,引入了一个二级管理模式的机器来组织分页单元。如下图:

    从上图可以看出,线性地址被划分为3部分:面目录索引(10位)页表索引(10位) 和 偏移量(12位)。而 cr3寄存器 保存了 页目录 的物理地址,这样就可以通过 cr3寄存器 来找到 页目录页目录项 指向页表地址,页表项 指向映射的物理内存页地址,而 偏移量 指定了在物理内存页的偏移量。

     

    虚拟内存地址管理


    应用程序使用 malloc() 函数向Linux内核申请内存时,Linux内核会返回可用的虚拟内存地址给应用程序。我们可以通过以下程序来验证:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        void *ptr;
    
        ptr = malloc(1024);
    
        printf("%p\n", ptr);
    
        return 0;
    }

    运行程序后输出:

    # 0x7fffeefe6260

    内核返回的是虚拟内存地址,但虚拟内存地址映射到物理内存地址不会在申请内存时进行,只有在应用程序读写申请的内存时才会进行映射。

    每个进程都可以使用4GB的虚拟内存地址,所以Linux内核需要为每个进程管理这4GB的虚拟内存地址。例如记录哪些虚拟内存地址是空闲的可以分配的,哪些虚拟内存地址已经被占用了。而Linux内核使用 vm_area_struct 结构进行管理,定义如下:

    struct vm_area_struct {
        struct mm_struct * vm_mm;   /* VM area parameters */
        unsigned long vm_start;
        unsigned long vm_end;
    
        /* linked list of VM areas per task, sorted by address */
        struct vm_area_struct *vm_next;
    
        pgprot_t vm_page_prot;
        unsigned long vm_flags;
    
        /* AVL tree of VM areas per task, sorted by address */
        short vm_avl_height;
        struct vm_area_struct * vm_avl_left;
        struct vm_area_struct * vm_avl_right;
        
        struct vm_area_struct *vm_next_share;
        struct vm_area_struct **vm_pprev_share;
    
        struct vm_operations_struct * vm_ops;
        unsigned long vm_pgoff;
        struct file * vm_file;
        ...
    };

    vm_area_struct 结构各个字段作用:

    • vm_mm:指向进程内存空间管理对象。
    • vm_start:内存区的开始地址。
    • vm_end:内存区的结束地址。
    • vm_next:用于连接进程的所有内存区。
    • vm_page_prot:指定内存区的访问权限。
    • vm_flags:内存区的一些标志。
    • vm_file:指向映射的文件对象。
    • vm_ops:内存区的一些操作函数。

    vm_area_struct 结构通过以下方式对虚拟内存地址进行管理,如下图:

    从上图可以看出,通过 vm_area_struct 结构体可以把虚拟内存地址划分为多个用途不相同的内存区,比如可以划分为数据区、代码区、堆区和栈区等等。每个进程描述符(内核用于管理进程的结构)都有一个类型为 mm_struct 结构的字段,这个结构的 mmap 字段保存了已经被使用的虚拟内存地址。

    当应用程序通过 malloc() 函数向内核申请内存时,会触发系统调用 sys_brk(),(当然,根据申请的内存大小,也可能是sys_mmap)sys_brk() 实现如下:

    asmlinkage unsigned long sys_brk(unsigned long brk)
    {
        unsigned long rlim, retval;
        unsigned long newbrk, oldbrk;
        struct mm_struct *mm = current->mm;
    
        down(&mm->mmap_sem);
    
        if (brk < mm->end_code)
            goto out;
        newbrk = PAGE_ALIGN(brk);
        oldbrk = PAGE_ALIGN(mm->brk);
        if (oldbrk == newbrk)
            goto set_brk;
    
        if (brk <= mm->brk) { // 缩小堆空间
            if (!do_munmap(mm, newbrk, oldbrk-newbrk))
                goto set_brk;
            goto out;
        }
    
        // 检测是否超过限制
        rlim = current->rlim[RLIMIT_DATA].rlim_cur;
        if (rlim < RLIM_INFINITY && brk - mm->start_data > rlim)
            goto out;
    
        // 如果已经存在, 那么直接返回
        if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
            goto out;
    
        // 是否有足够的内存页?
        if (!vm_enough_memory((newbrk-oldbrk) >> PAGE_SHIFT))
            goto out;
    
        // 所有判断都成功, 现在调用do_brk()进行扩展堆空间
        if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
            goto out;
    set_brk:
        mm->brk = brk;
    out:
        retval = mm->brk;
        up(&mm->mmap_sem);
        return retval;
    }

    sys_brk() 系统调用的 brk 参数指定了堆区的新指针,sys_brk() 首先会进行一些检测,然后调用 do_brk() 函数进行虚拟内存地址的申请,do_brk() 函数实现如下:

    unsigned long do_brk(unsigned long addr, unsigned long len)
    {
        struct mm_struct * mm = current->mm;
        struct vm_area_struct * vma;
        unsigned long flags, retval;
        
        ...
    
        // 如果新申请的内存空间与之前的内存空间相连并且特性一样, 那么就合并内存空间
        if (addr) {
            struct vm_area_struct * vma = find_vma(mm, addr-1);
            if (vma && vma->vm_end == addr &&
                !vma->vm_file &&
                vma->vm_flags == flags)
            {
                vma->vm_end = addr + len;
                goto out;
            }
        }
    
        // 新申请一个 vm_area_struct 结构
        vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
        if (!vma)
            return -ENOMEM;
    
        vma->vm_mm = mm;
        vma->vm_start = addr;
        vma->vm_end = addr + len;
        vma->vm_flags = flags;
        vma->vm_page_prot = protection_map[flags & 0x0f];
        vma->vm_ops = NULL;
        vma->vm_pgoff = 0;
        vma->vm_file = NULL;
        vma->vm_private_data = NULL;
    
        insert_vm_struct(mm, vma); // 添加到虚拟内存管理器中
    
    out:
        ...
        return addr;
    }
    

    do_brk() 函数主要通过调用 kmem_cache_alloc() 申请一个 vm_area_struct 结构,然后对这个结构的各个字段进行初始。最后通过调用 insert_vm_struct() 函数把这个结构添加到进程的虚拟内存地址链表中。

    为了加速查找虚拟内存区,Linux内核还为 vm_area_struct 结构构建了一个 AVL树(新版本为红黑树),有兴趣的可以查阅源码或相关资料。

    虚拟地址与物理地址映射 - do_page_fault()


    前面说过,虚拟地址必须要与物理地址进行映射才能使用,如果访问了没有被映射的虚拟地址,CPU会触发内存访问异常,并且调用异常处理例程对没被映射的虚拟地址进行映射操作。Linux的内存访问异常处理例程是 do_page_fault(),代码如下:

    asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
    {
        ...
    
        // 获取发生错误的虚拟地址
        __asm__("movl %%cr2,%0":"=r" (address));
    
        ...
    
        down(&mm->mmap_sem);
    
        // 查找第一个结束地址比address大的vma
        vma = find_vma(mm, address);
        if (!vma)
            goto bad_area;
    
        ...
    
        // 这里是进行物理内存映射的地方
        switch (handle_mm_fault(mm, vma, address, write)) {
        case 1:
            tsk->min_flt++;
            break;
        case 2:
            tsk->maj_flt++;
            break;
        case 0:
            goto do_sigbus;
        default:
            goto out_of_memory;
        }
    
        ...
    
    bad_area:
        up(&mm->mmap_sem);
    
    bad_area_nosemaphore:
        // 用户空间触发的虚拟内存地址越界访问, 发送SIGSEGV信息(段错误)
        if (error_code & 4) {
            tsk->thread.cr2 = address;
            tsk->thread.error_code = error_code;
            tsk->thread.trap_no = 14;
            info.si_signo = SIGSEGV;
            info.si_errno = 0;
            info.si_addr = (void *)address;
            force_sig_info(SIGSEGV, &info, tsk);
            return;
        }
    }

    当异常发生时,CPU会把触发异常的虚拟内存地址保存到 cr2寄存器中,do_page_fault() 函数首先通过读取 cr2寄存器 获取到触发异常的虚拟内存地址,然后调用 find_vma() 函数获取虚拟内存地址对应的 vm_area_struct 结构,如果找不到说明这个虚拟内存地址是不合法的(没有进行申请),所以内核会发送 SIGSEGV 信号(传说中的段错误)给进程。如果虚拟地址是合法的,那么就调用 handle_mm_fault() 函数对虚拟地址进行映射。


     

    推荐阅读


    Linux虚拟内存管理 | 虚拟地址与物理地址映射、段错误SIGSEGV

    linux内核之slob、slab、slub

    Linux内核:kmalloc()和SLOB、SLAB、SLUB内存分配器

    Linux内存管理:内存分配:slab分配器

    Linux内存管理:内存描述之内存节点node

    Linux内存管理:内存描述之内存区域zone

    Linux内存管理:内存描述之内存页面page

    Linux内存管理:内存描述之高端内存

    Linux内存管理:分页机制

    内存管理:Linux Memory Management:MMU、段、分页、PAE、Cache、TLB

    Linux内存管理:ARM64体系结构与编程之cache(1)

    Linux内存管理:ARM64体系结构与编程之cache(2)

    ARM SMMU原理与IOMMU技术(“VT-d” DMA、I/O虚拟化、内存虚拟化)

    内核引导参数IOMMU与INTEL_IOMMU有何不同?

    提升KVM异构虚拟机启动效率:透传(pass-through)、DMA映射(VFIO、PCI、IOMMU)、virtio-balloon、异步DMA映射、预处理

    展开全文
  • linux虚拟内存物理内存 虚拟地址空间Linux整体架构图Linux虚拟内存系统内存管理分页式内存管理分段式内存管理段页式内存管理 虚拟地址空间 参考: ... 地址空间:非负整数地址的有序集合,如{0,1,2,...}\{0,1,2,...\...

    虚拟地址空间

    参考:
    https://sylvanassun.github.io/2017/10/29/2017-10-29-virtual_memory/

    • 地址空间:非负整数地址的有序集合,如{0,1,2,...}\{0,1,2,...\}

    • 线性地址空间:如果地址空间中的整数是连续的,则称为线性地址空间

    • 虚拟地址空间:在一个带虚拟内存的系统中,如果CPU用n位2进制数表示虚拟地址,则该连续的虚拟地址形成的范围(0,1,..2n1)(0,1,..2^n-1)称为“虚拟地址空间” {0,1,2,...,2n1}\{0,1,2,...,2^n-1\}

    例如linux系统中用32位来表示虚拟地址,则虚拟地址空间为{0,1,2,...,2321}\{0,1,2,...,2^{32}-1\},大小为4GB.

    • 虚拟内存:一种对物理内存的抽象概念,可以理解为一个连续的字节组成的数组,每个字节都用一个虚拟地址表示,一个系统所有的虚拟地址组成了虚拟地址空间。

    虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)

    • 物理地址空间:真实物理内存单元的地址,是硬件电路通过地址总线去寻址的空间,物理上是高低电位,对应0/1表示。

    ------------------------------------------------------参考《深入理解计算机系统》

    Linux整体架构图

    在这里插入图片描述
    linux内核控制并且管理硬件资源,包括进程的调度和管理、内存管理、文件系统管理、设备驱动管理、网络管理等等。并且提供应用程序统一的系统调用接口

    Linux虚拟内存

    • Linux中每个进程都有自己独立的4G虚拟内存空间,各个进程的内存空间具有类似的结构。这4GB的虚拟地址空间划分成两个部分:内核空间和用户空间
      在这里插入图片描述在这里插入图片描述

    • Linux中每个用户进程都有自身的虚拟地址范围(用户空间),从0到TASK_SIZE。用户空间之上的区域(从TASK_SIZE到2322^{32})保留给内核专用(内核空间),用户进程不能访问。TASK_SIZE是一个特定于计算机体系结构的常数,把地址空间按给定比例划分为两部分。linux的用户空间为3GB(用户进程自己使用),内核空间为1GB(被所有进程共享)

    • 一个新进程建立的时候,内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体,task_struct中有一个struct mm_struct指针,mm_struct结构体抽象了进程自己的虚拟地址空间。

    参考:https://www.cnblogs.com/Rofael/archive/2013/04/13/3019153.html在这里插入图片描述
    关于进程控制块的具体内容参考:https://www.cnblogs.com/Rofael/archive/2013/04/13/3019153.html

    • 在每个mm_struct又都有一个pgd_t 指针pgd指向页表,然后通过页表实现从虚拟地址到物理地址的转换。

    • 每个mm_struct中还有一个vm_area_struct指针mmap,vm_area_struct结构体描述的是一段连续的、具有相同访问属性的虚存空间。vm_area_struct结构所描述的虚存空间以vm_start、vm_end成员表示,它们分别保存了该虚存空间的首地址和末地址后第一个字节的地址,以字节为单位,所以虚存空间范围可以用[vm_start, vm_end)表示。

    参考:https://blog.csdn.net/ywf861029/article/details/6114794

    通常,进程所使用到的虚存空间不连续,且各部分虚存空间的访问属性也可能不同。所以一个进程的虚存空间需要多个vm_area_struct结构来描述

    在vm_area_struct结构的数目较少的时候,各个vm_area_struct按照升序排序,以单链表的形式组织数据(通过vm_next指针指向下一个vm_area_struct结构)。

    但是当vm_area_struct结构的数据较多的时候,仍然采用链表组织的化,势必会影响到它的搜索速度。针对这个问题,vm_area_struct还添加了vm_avl_hight(树高)、vm_avl_left(左子节点)、vm_avl_right(右子节点)三个成员来实现红黑树,以提高vm_area_struct的搜索速度。

    进程建立vm_area_struct结构后,只是说明进程可以访问这个虚存空间,但有可能还没有分配相应的物理页面并建立好页面映射。在这种情况下,若是进程执行中有指令需要访问该虚存空间中的内存,便会产生一次缺页异常。这时候,就需要通过vm_area_struct结构里面的vm_ops->nopage所指向的函数来将产生缺页异常的地址对应的文件数据读取出来。

    总结: linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域(这个区域只是整个虚拟内存空间中的一小块),由于linux整个虚拟内存空间(3GB)中的虚拟内存区域功能(text段,Data段,BBS段,Heap段,MMAP段,Stack段)都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问,如下图所示:
    在这里插入图片描述
    关于linux虚拟内存空间分段的具体内容请看https://blog.csdn.net/love_gaohz/article/details/41310597
    在这里插入图片描述

    段名 存储内容 分配方式 生长方向 读写特点 运行态
    代码段text 程序指令、字符串常量、虚函数表 静态分配 由低到高 只读 用户态
    数据段data 初始化的全局变量和静态变量 静态分配 由低到高 可读可写 用户态
    BSS段bbs 未初始化的全局变量和静态变量 静态分配 由低到高 可读可写 用户态
    堆heap 动态申请的数据 动态分配 由低到高 可读可写 用户态
    映射段 动态链接库、共享文件、匿名映射对象 动态分配 由低到高 可读可写 用户态
    栈stack 局部变量、函数参数与返回值、函数返回地址、调用者环境信息 静态+动态分配 由高到低 可读可写 用户态

    下面以 C++ 为例,看一下常见变量所属的内存段
    来自https://blog.csdn.net/K346K346/article/details/45592329

    #include <string.h>
    
    int a = 0;                 					// a在数据段,0为文字常量,在代码段
    char *p1;                  				// BSS段,系统默认初始化为NULL
    void main()
    {
        int b;                 					//栈
        char *p2 = "123456";  			//字符串"123456"在代码段,p2在栈上
        static int c =0;      				//c在数据段
        const int d=0; 						//栈
        static const int d;					//数据段
        p1 = (char*)malloc(10);		//分配的10字节在堆
        strcpy(p1,"123456"); 			//"123456"放在代码段,编译器可能会将它与p2所指向的"123456"优化成一个地方
    }
    
    

    内存管理

    下面补充一点关于内存管理的基础知识,并不针对linux内核,linux内核的内存管理请参考如下两篇文章:
    1.Linux内存管理(上)
    2.Linux内存管理(下)

    分页式内存管理

    • 将物理内存空间分为一个个相等的分区(比如:每个分区4KB),每个分区就是一个页框(也叫内存块、物理块、页帧),每个页框有一个编号,即页框号(或内存块号、物理块号、页帧号),页框号从0开始。
    • 将用户进程的虚拟地址空间也分为与页框大小相等的一个个区域,称为页面,每个页面也有一个编号,即页号,页号也是从0开始

    这里有三个页的概念
    虚拟页(VP, Virtual Page),虚拟空间中的页;
    物理页(PP, Physical Page),物理内存中的页;
    磁盘页(DP, Disk Page),磁盘中的页。

    各个进程页面可以离散地分配到物理内存的页框中,为了记录每个页面与每个页框的对应关系,引入了页表

    对于每个进程的虚拟内存页对应到物理内存页地址时,存在如下三种情况-
    未分配:未分配的虚拟内存块上不会有任何数据,所以不占用任何物理内存空间(内存和磁盘)
    缓存的:当前已缓存在内存中的已分配的页
    未缓存的:未缓存在物理内存中的已分配页,它在保存磁盘中
    在这里插入图片描述
    在这里插入图片描述
    多级页表:
    在这里插入图片描述

    • 对于分页式内存管理:进程虚拟地址怎样映射到物理内存地址呢?

    简单地将就是进程虚拟地址先转化成进程的【页号:页内偏移】;然后查询页表去找到对应在物理内存中的【页框号】,页内偏移地址是一样的,
    在这里插入图片描述

    • 关于虚拟地址到物理地址的转换

      虚拟内存系统利用某种方法来判定一个虚拟页是否缓存在DRAM(内存)中,如果是,还必须确定这个虚拟页存放在哪个物理页中。这些功能是由软硬件联合提供的,包括操作系统软件、MMU中的地址翻译硬件和一个存放在物理内存中叫做页表的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换未物理地址时,都会读取页表,操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。

      下图展示了MMU如何利用页表来实现这种映射。CPU中一个控制寄存器,页表基址寄存器(Page Table Base Register,PTBR)指向当前页表。N位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(Virtual Page Offset,VPO)和一个(n-p)位的虚拟页号(Virtual Page Number,VPN)。MMU利用VPN来选择适当的PTE.
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      ------------------------------------------------------参考《深入理解计算机系统》

    在这里插入图片描述

    分段式内存管理

    • 按照程序自身的逻辑关系将进程的虚拟地址空间划分成若干个段,每个段都有一个段名,每段从0开始编址,地址格式【段名:偏移】

    • 物理内存的分配以进程的段为单位分配,每个段在物理内存中占据连续空间,但各段之间可以不相邻
      在这里插入图片描述
      同理,进程被分为多段,各段被离散地装入内存,为了保证程序能正常远行,就必须记录进程的各段与物理内存的各段之间的对应关系,即段表
      在这里插入图片描述
      在这里插入图片描述

    • 进程虚拟地址到物理内存地址的转换
      在这里插入图片描述

    段页式内存管理

    分页式管理主要目的是实现离散分配,提高内存利用率,分页仅仅是系统管理上的需要,完全是系统行为,对用户不可见。页的大小固定且由系统决定。

    分段式管理:主要目的是更好地满足用户需求,一个段通常包含一组属于一个逻辑模块的的信息,分段对用户是可见的,用户编程时需要显示地给出段名。段的长度不固定,决定于用户编写的程序。
    分段有利于段的共享:在分段系统中,段的共享是通过段表中相应表项指向被共享段的同一个物理副本来实现的。当一个作业正在从共享段中读取数据时,必须防止另一个作业修改此共享段中的数据。不能修改的代码称为纯代码或可重入代码,这样的代码和不能修改的数据是可以共享的,可修改的代码或数据不能共享。(这也说明了为啥共享库的的代码只会在内存中保存一次,然后所有的程序都可以共享这一段代码)

    段页式存储管理方式:将分段和分页结合,即先将用户进程分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。
    在这里插入图片描述

    段表中存储的是段号、状态、页表大小、页表起始。每个段表项的长度相等,一般段号是隐藏的。
    每个页表由页号、页面存放的页框号组成。每个页表项的长度相等,一般页号是隐藏的。

    在这里插入图片描述

    • 段页式内存管理的地址转换
      在这里插入图片描述

    Linux内核态和用户态

    • Linux将虚拟地址空间划分成两个部分:内核空间和用户空间
      在这里插入图片描述

    • 在linux中CPU进程有两种状态:核心态和用户态,也即两种特权级别,核心态的进程可以访问地址高于TASK_SIZE的内存区域,用户态禁止访问内核空间,这样可以防止进程无意间修改内核的数据。

    当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。

    什么需要区分内核空间与用户空间

    在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。

    所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。
    Linux 系统进程被划分为用户态内核态两种特权级别

    在这里插入图片描述
    在内核态下,进程运行在内核地址空间中, 此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。

    在用户态下,进程运行在用户地址空间中,被执行的代码只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址

    所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。

    如何从用户空间进入内核空间

    其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。

    比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个 “系统调用” 告诉内核:“我要读取磁盘上的某某文件”。其实就是通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据。

    再比如,库接口malloc申请动态内存,malloc的实现内部最终还是会调用brk()或者mmap()系统调用来分配内存。也是先切换到内核态,分配成功后在切换会用户态。

    • 那怎么从用户态进入到内核态
      从用户态到内核态切换可以通过三种方式:
    1. 系统调用,其实系统调用本身就是中断,其是软件中断,跟硬中断不同。

    2. 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。

    3. 外设中断:当外设完成用户的请求时,会向CPU发送中断信号。

    在这里插入图片描述

    linux中使用内存映射(mmap)操作文件

    参考:https://www.cnblogs.com/huxiao-tee/p/4660352.html

    • 普通文件操作:

    1,打开或创建文件,得到文件描述符,
    2,将内存中的数据以一定的格式和顺序写入文件,或者将文件中的数据以一定的格式和顺序读入到内存;
    3,关闭文件描述符

    将文件从磁盘读取到内核空间,在从内核空间读取到用户空间,一共发生了两次数据拷贝

    • linux中使用内存映射mmap操作文件:
    1,首先打开文件,使用的函数原型如下:
     int open(  //返回值:大于等于0代表操作成功,返回打开的文件描述符号,=-1
    const char *pathname,   //要打开的文件名
    int flags, //打开的方式,打开方式包括:O_RDONLY 只读方式 O_WRONLY 只写,O_RDWR读写,O_CREAT创建,O_EXCL文件如果存在,使用此标记,会返回错误
    mode_t mode); //指定创建文件的权限,只对创建文件有效,对于打开无效;
    
    2,获取文件大小
    int fstat(int fd,//文件描述符号
    struct stat*buf);//返回文件属性结构体
    返回值:成功返回0;失败返回-1
    
    3,把文件映射成虚拟内存
    void *mmap(void *addr,  //从进程的那个地址开始映射,如果为NULL,由系统指定;
    size_t length, //映射的地址空间的大小
    int prot, //内存的保护模式
    int flags,//映射模式 有匿名,私有,保护等标记 具体查询man手册;
    int fd,  //如果为文件映射,则此处为文件的描述符号
    off_t offset);//如果为文件映射,则此处代表定位到文件的那个位置,然后开始向后映射。
    返回值:映射成功,返回首地址;
    
    4,通过对内存的读写来实现对文件的读写
    通常使用:memset 和memcpy来实现操作;
    
    5,卸载映射
    int munmap(void *addr,  //要卸载的内存的地址
    size_t length);//内存的大小
    
    6,关闭文件
    int close(int fd);  //要关闭的文件描述符号  ,成功返回0,错误返回-1,错误参照errorno;
    
    

    文件磁盘地址映射到虚拟内存区域,这一步没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,此时通过已经建立好的映射关系,使用一次数据拷贝,就将文件从磁盘中传入内存的用户空间中,供进程使用
    在这里插入图片描述

    • mmap内存映射原理

    mmap内存映射的实现过程,总的来说可以分为三个阶段:
    (一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
    1、进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
    2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
    3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
    4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
    (二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
    5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
    6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
    7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
    8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
    (三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
    注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。
    9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
    10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
    11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
    12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
    注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

    • mmap优点总结
    1. 对文件的读取操作只需一次拷贝,用内存读写取代I/O读写,提高了文件读取效率
    2. 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉(因为不同的虚拟地址可以映射到同一个物理地址)。
    3. 提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
      同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。
    4. 可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。
    • mmap操作文件代码示例
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/mman.h>
    #include <sys/stat.h>
    typedef struct{
      char name[20];
      short age;
      float score;
      char sex;
    }student;
    
    int main()
    {
      student *p,*pend;
      //打开文件描述符号
      int fd;
      /*打开文件*/
        fd=open("user.dat",O_RDWR);
        if(fd==-1){//文件不存在
            fd=open("user.dat",O_RDWR|O_CREAT,0666);
            if(fd==-1){
                printf("打开或创建文件失败:%m\n");
                exit(-1);
            }
        }
      //打开文件ok,可以进行下一步操作
      printf("open ok!\n");
    
      //获取文件的大小,映射一块和文件大小一样的内存空间,如果文件比较大,可以分多次,一边处理一边映射;
      struct stat st; //定义文件信息结构体
      /*取得文件大小*/
      int r=fstat(fd,&st);
      if(r==-1){
          printf("获取文件大小失败:%m\n");
          close(fd);
          exit(-1);
      }
    
      int len=st.st_size;
      /*把文件映射成虚拟内存地址*/
      p=static_cast<student*>(mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0));
      if(p==NULL || p==(void*)-1){
          printf("映射失败:%m\n");
          close(fd);
          exit(-1);
      }
      /*定位到文件开始*/
      pend=p;
      /*通过内存读取记录*/
      int i=0;
      while(i<(len/sizeof(student)))
      {
        printf("第%d个条\n",i);
        printf("name=%s\n",p[i].name);
        printf("age=%d\n",p[i].age);
        printf("score=%f\n",p[i].score);
        printf("sex=%c\n",p[i].sex);
        i++;
      }
    
      /*卸载映射*/
      munmap(p,len);
      /*关闭文件*/
      close(fd);
    }
    
    展开全文
  • 操作系统为了屏蔽I/O底层的差异,创建了VFS(虚拟文件系统),为了屏蔽I/O层内存之间的差异,产生了虚拟内存。为了屏蔽cpu内存之间的差异,创建了进程。每个程序运行起来都会拥有一个自己的虚拟地址空间,32位cpu...

             操作系统为了屏蔽I/O底层的差异,创建了VFS(虚拟文件系统),为了屏蔽I/O层与内存之间的差异,产生了虚拟内存。为了屏蔽cpu与内存之间的差异,创建了进程。每个程序运行起来都会拥有一个自己的虚拟地址空间,32位cpu的操作系统,它是一个4GB的内存地址块,其地址线也为32位,所以虚拟地址空间为2^32 -1= 4G。

            在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,根据需要将其映射到物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。

           Linux虚拟内存空间布局如下:

    在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;堆由程序员自己管理,即显式地申请和释放空间。  BSS段、数据段和代码段是可执行程序编译时的分段,运行时还需要栈和堆。

    总结:

    1、 每个进程都有自己独立的4G内存空间,各个进程的内存空间具有类似的结构;

    2、每个进程的4G内存空间只是虚拟内存空间,每次访问内存空间的某个地址,都需要把地址翻译为实际物理内存地址;

    3、所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上;

    4、可以认为虚拟空间都被映射到了磁盘空间中,(事实上也是按需要映射到磁盘空间上,通过mmap),并且由页表记录映射位置。

    展开全文
  • 用于内存芯片级的单元寻址,处理器和CPU连接的地址总线相对应。 ——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节...

    一、概念

    物理地址(physical address)

    用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
    ——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。

    虚拟内存(virtual memory)

    这是对整个内存(不要与机器上插那条对上号)的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;
    之所以是这样,是因为现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。这个“转换”,是所有问题讨论的关键。
    有了这样的抽像,一个程序,就可以使用比真实物理地址大得多的地址空间。(拆东墙,补西墙,银行也是这样子做的),甚至多个进程可以使用相同的地址。不奇怪,因为转换后的物理地址并非相同的。
    ——可以把连接后的程序反编译看一下,发现连接器已经为程序分配了一个地址,例如,要调用某个函数A,代码不是call A,而是call 0x0811111111 ,也就是说,函数A的地址已经被定下来了。没有这样的“转换”,没有虚拟地址的概念,这样做是根本行不通的。
    打住了,这个问题再说下去,就收不住了。

    逻辑地址(logical address)

    Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说的连接器为A分配的0x08111111这个地址就是逻辑地址。
    ——不过不好意思,这样说,好像又违背了Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些”

    线性地址(linear address)或也叫虚拟地址(virtual address)

    跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。


    CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。

    这样做两次转换,的确是非常麻烦而且没有必要的,因为直接可以把线性地址抽像给进程。之所以这样冗余,Intel完全是为了兼容而已。

    二、CPU段式内存管理,逻辑地址如何转换为线性地址

    一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:
    在这里插入图片描述
    索引号,或者直接理解成数组下标——那它总要对应一个数组吧,它又是什么东东的索引呢?这个东东就是“段描述符(segment descriptor)”,呵呵,段描述符具体地址描述了一个段(对于“段”这个字眼的理解,我是把它想像成,拿了一把刀,把虚拟内存,砍成若干的截——段)。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,我刚才对段的抽像不太准确,因为看看描述符里面究竟有什么东东——也就是它究竟是如何描述的,就理解段究竟有什么东东了,每一个段描述符由8个字节组成,如下图:
    在这里插入图片描述
    这些东东很复杂,虽然可以利用一个数据结构来定义它,不过,我这里只关心一样,就是Base字段,它描述了一个段的开始位置的线性地址。

    Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。

    GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

    好多概念,像绕口令一样。这张图看起来要直观些:
    在这里插入图片描述
    首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
    1)、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
    2)、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
    3)、把Base + offset,就是要转换的线性地址了。

    还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。OK,来看看Linux怎么做的。

    三、Linux的段式管理

    Intel要求两次转换,这样虽说是兼容了,但是却是很冗余,呵呵,没办法,硬件要求这样做了,软件就只能照办,怎么着也得形式主义一样。
    另一方面,其它某些硬件平台,没有二次转换的概念,Linux也需要提供一个高层抽像,来提供一个统一的界面。所以,Linux的段式管理,事实上只是“哄骗”了一下硬件而已。

    按照Intel的本意,全局的用GDT,每个进程自己的用LDT——不过Linux则对所有的进程都使用了相同的段来对指令和数据寻址。即用户数据段,用户代码段,对应的,内核中的是内核数据段和内核代码段。这样做没有什么奇怪的,本来就是走形式嘛,像我们写年终总结一样。

    include/asm-i386/segment.h
    
    #define GDT_ENTRY_DEFAULT_USER_CS        14
    #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
    #define GDT_ENTRY_DEFAULT_USER_DS        15
    #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)
    #define GDT_ENTRY_KERNEL_BASE        12
    #define GDT_ENTRY_KERNEL_CS                (GDT_ENTRY_KERNEL_BASE + 0)
    #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)
    #define GDT_ENTRY_KERNEL_DS                (GDT_ENTRY_KERNEL_BASE + 1)
    #define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8)
    
    
    把其中的宏替换成数值,则为:
    
    #define __USER_CS 115        [00000000 1110  0  11]
    #define __USER_DS 123        [00000000 1111  0  11]
    #define __KERNEL_CS 96      [00000000 1100  0  00]
    #define __KERNEL_DS 104    [00000000 1101  0  00]
    
    
    
    方括号后是这四个段选择符的16位二制表示,它们的索引号和T1字段值也可以算出来了
    
    __USER_CS              index= 14   T1=0
    __USER_DS               index= 15   T1=0
    __KERNEL_CS           index=  12  T1=0
    __KERNEL_DS           index= 13   T1=0
    
    
    
    T1均为0,则表示都使用了GDT,再来看初始化GDT的内容中相应的12-15项(arch/i386/head.S):
    
    .quad 0x00cf9a000000ffff        /* 0x60 kernel 4GB code at 0x00000000 */
    .quad 0x00cf92000000ffff        /* 0x68 kernel 4GB data at 0x00000000 */
    .quad 0x00cffa000000ffff        /* 0x73 user 4GB code at 0x00000000 */
    .quad 0x00cff2000000ffff        /* 0x7b user 4GB data at 0x00000000 */
    

    按照前面段描述符表中的描述,可以把它们展开,发现其16-31位全为0,即四个段的基地址全为0。

    这样,给定一个段内偏移地址,按照前面转换公式,0 + 段内偏移,转换为线性地址,可以得出重要的结论,“在Linux下,逻辑地址与线性地址总是一致(是一致,不是有些人说的相同)的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的。!!!”

    忽略了太多的细节,例如段的权限检查。呵呵。

    Linux中,绝大部份进程并不例用LDT,除非使用Wine ,仿真Windows程序的时候。

    四.CPU的页式内存管理

    CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。

    另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。

    这里注意到,这个total_page数组有2^20个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。文字描述太累,看图直观一些:
    在这里插入图片描述
    如上图,
    1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。万里长征就从此长始了。
    2、每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
    3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
    依据以下步骤进行转换:
    1)、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
    2)、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
    3)、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
    4)、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的葫芦;

    这个转换过程,应该说还是非常简单地。全部由硬件完成,虽然多了一道手续,但是节约了大量的内存,还是值得的。那么再简单地验证一下:
    1、这样的二级模式是否仍能够表示4G的地址;
    页目录共有:2^10项,也就是说有这么多个页表
    每个目表对应了:2^10页;
    每个页中可寻址:2^12个字节。
    还是2^32 = 4GB

    2、这样的二级模式是否真的节约了空间;
    也就是算一下页目录项和页表项共占空间 (2^10 * 4 + 2 ^10 *4) = 8KB。哎,……怎么说呢!!!
    红色错误,标注一下,后文贴中有此讨论。。。。。。
    按<深入理解计算机系统>中的解释,二级模式空间的节约是从两个方面实现的:
    A、如果一级页表中的一个页表条目为空,那么那所指的二级页表就根本不会存在。这表现出一种巨大的潜在节约,因为对于一个典型的程序,4GB虚拟地址空间的大部份都会是未分配的;
    B、只有一级页表才需要总是在主存中。虚拟存储器系统可以在需要时创建,并页面调入或调出二级页表,这就减少了主存的压力。只有最经常使用的二级页表才需要缓存在主存中。——不过Linux并没有完全享受这种福利,它的页表目录和与已分配页面相关的页表都是常驻内存的。

    值得一提的是,虽然页目录和页表中的项,都是4个字节,32位,但是它们都只用高20位,低12位屏蔽为0——把页表的低12屏蔽为0,是很好理解的,因为这样,它刚好和一个页面大小对应起来,大家都成整数增加。计算起来就方便多了。但是,为什么同时也要把页目录低12位屏蔽掉呢?因为按同样的道理,只要屏蔽其低10位就可以了,不过我想,因为12>10,这样,可以让页目录和页表使用相同的数据结构,方便。

    本贴只介绍一般性转换的原理,扩展分页、页的保护机制、PAE模式的分页这些麻烦点的东东就不啰嗦了……可以参考其它专业书籍。

    五.Linux的页式内存管理

    原理上来讲,Linux只需要为每个进程分配好所需数据结构,放到内存中,然后在调度进程的时候,切换寄存器cr3,剩下的就交给硬件来完成了(呵呵,事实上要复杂得多,不过偶只分析最基本的流程)。

    前面说了i386的二级页管理架构,不过有些CPU,还有三级,甚至四级架构,Linux为了在更高层次提供抽像,为每个CPU提供统一的界面。提供了一个四层页管理架构,来兼容这些二级、三级、四级管理架构的CPU。这四级分别为:

    页全局目录PGD(对应刚才的页目录)
    页上级目录PUD(新引进的)
    页中间目录PMD(也就新引进的)
    页表PT(对应刚才的页表)。

    整个转换依据硬件转换原理,只是多了二次数组的索引罢了,如下图:
    在这里插入图片描述
    那么,对于使用二级管理架构32位的硬件,现在又是四级转换了,它们怎么能够协调地工作起来呢?嗯,来看这种情况下,怎么来划分线性地址吧!
    从硬件的角度,32位地址被分成了三部份——也就是说,不管理软件怎么做,最终落实到硬件,也只认识这三位老大。
    从软件的角度,由于多引入了两部份,,也就是说,共有五部份。——要让二层架构的硬件认识五部份也很容易,在地址划分的时候,将页上级目录和页中间目录的长度设置为0就可以了。
    这样,操作系统见到的是五部份,硬件还是按它死板的三部份划分,也不会出错,也就是说大家共建了和谐计算机系统。

    这样,虽说是多此一举,但是考虑到64位地址,使用四层转换架构的CPU,我们就不再把中间两个设为0了,这样,软件与硬件再次和谐——抽像就是强大呀!!!

    例如,一个逻辑地址已经被转换成了线性地址,0x08147258,换成二制进,也就是:
    0000100000 0101000111 001001011000
    内核对这个地址进行划分
    PGD = 0000100000
    PUD = 0
    PMD = 0
    PT = 0101000111
    offset = 001001011000

    现在来理解Linux针对硬件的花招,因为硬件根本看不到所谓PUD,PMD,所以,本质上要求PGD索引,直接就对应了PT的地址。而不是再到PUD和PMD中去查数组(虽然它们两个在线性地址中,长度为0,2^0 =1,也就是说,它们都是有一个数组元素的数组),那么,内核如何合理安排地址呢?
    从软件的角度上来讲,因为它的项只有一个,32位,刚好可以存放与PGD中长度一样的地址指针。那么所谓先到PUD,到到PMD中做映射转换,就变成了保持原值不变,一一转手就可以了。这样,就实现了“逻辑上指向一个PUD,再指向一个PDM,但在物理上是直接指向相应的PT的这个抽像,因为硬件根本不知道有PUD、PMD这个东西”。

    然后交给硬件,硬件对这个地址进行划分,看到的是:
    页目录 = 0000100000
    PT = 0101000111
    offset = 001001011000
    嗯,先根据0000100000(32),在页目录数组中索引,找到其元素中的地址,取其高20位,找到页表的地址,页表的地址是由内核动态分配的,接着,再加一个offset,就是最终的物理地址了。

    展开全文
  • 》,我们知道了CPU是如何访问内存的,本篇文章我们来讲下虚拟地址空间和物理地址空间的映射。通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间。注意这里是32位内核地址空间划分,64位内核地址空间划分...
  • 项目中经常需要把内存数据dump出来看看是否与自己设想的一样,dump... ARM小机端的内存起始地址并不是0,而是0x40000000也就是说虚拟地址与物理内存起始地址两者的差为:0x80000000那如果要将内核的虚拟地址转换为物理
  • 转载自:Linux 虚拟内存物理内存的理解 虚拟内存: 第一层理解 1.、每个进程都有自己独立的4G内存空间,各个进程的内存空间具有类似的结构; 2、一个新进程建立的时候,将会建立起自己的内存空间,此进程的数据,...
  • 当进程需要内存时,从内核获得的仅仅是虚拟的内存区域,而不是实际的物理地址,进程并没有获得物理内存,获得的仅仅是对一个新的线性地址区间的使用权。实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会...
  • 》,我们知道了CPU是如何访问内存的,本篇文章我们来讲下虚拟地址空间和物理地址空间的映射。通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间。注意这里是32位内核地址空间划分,64位内核地址空间划分...
  • 将会建立起自己的内存空间,此进程的数据,代码等从磁盘拷贝到自己的进程空间,哪些数据在哪里,都由进程控制表中的task_struct记录,task_struct中记录中一条链表,记录中内存空间的分配情况,哪些地址有数据,哪些...
  •  用于内存芯片级的单元寻址,处理器和CPU连接的地址总线相对应。  ——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一...
  • 虚拟内存: 第一层理解 每个进程都有自己独立的4G内存空间,各个进程的内存空间具有类似的结构 一个新进程建立的时候,将会建立起自己的内存空间,此进程的数据,代码等从磁盘拷贝到自己的进程空间,哪些数据在哪里...
  • Linux进程虚拟内存物理内存

    千次阅读 2011-02-28 22:38:00
    Linux进程虚拟内存物理内存 收藏 先介绍几个基本概念: SIZE: 进程使用的地址空间, 如果进程映射了100M的内存, 进程的地址空间将报告为100M内存. 事实上, 这个大小不是一个程序实际使用的内存数....
  • 转换后才能访问到真正的物理内存,地址转换的过程分为两块:分段和分页. 分段机制简单地来说就是讲进程的代码,数据,栈分在不同的虚拟地址段上,从而避免 进程间的互相影响,分段之前的地址我们称之为逻辑地址,由两部分...
  • Linux 虚拟地址物理地址转换

    千次阅读 2019-03-08 13:58:14
    Table of Contents 概念 地址转换过程 概念 虚拟地址和物理地址的概念 CPU通过地址来访问内存中的单元,地址虚拟地址和物理地址之分,如果CPU没有MMU...直接被内存芯片(以下称为物理内存,以便与虚拟内...
  • Linux虚拟内存

    2020-09-11 09:52:45
    3、虚拟地址与物理内存进行映射才能使用,否则就会产生段错误。 4、虚拟地址与物理内存的映射由操作系统动态维护。 5、让用户使用虚拟地址一方面为了安全,另一方面操作系统可以让应用程序使用比实际物理内存更的...
  • 转载自:https://blog.csdn.net/lvyibin890/article/details/82217193操作系统有虚拟内存与物理内存的概念。在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于...
  • 1 虚拟空间用户空间2 内存区间 系统物理地址的组织 1 用户空间页面目录映射关系2用户空间的映射3内核空间虚拟地址的映射 相关数据结构关系图 源码版本 2.4.0 1. 虚拟空间 0-3G 用户空间 0x...
  • linux虚拟内存

    2017-09-18 17:36:56
    Linux虚拟内存管理有几个关键概念: 每个进程有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址 虚拟地址可通过每个进程上页表与物理地址进行映射,获得真正物理地址 如果虚拟地址...
  • 虚拟内存与物理内存 为什么要有虚拟内存??? 1.每个进程有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址(每个进程都假设自己看到的是完整的从0开始的内存) 2.程序可以使用一系列虚拟地址来访问大于...
  • 概念: 虚拟地址和物理地址的概念 CPU通过地址来访问内存中的单元,地址有虚拟地址和物理地址之分,如果CPU没有...直接被内存芯片(以下称为物理内存,以便与虚拟内存区分)接收,这称为物理地址(Physical Address...
  • 在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2^32也就是4G。并且这是固定的,如果没有虚拟内存,且每次...
  • 转自:http://blog.csdn.net/ordeder/article/details/41630945 版权声明:本文为博主... 目录(?)[-] 虚拟空间 ...进程虚拟地址的组织 1 虚拟空间用户空间 2 内存区间 ...系统物理地址的组织 1 ...
  • Linux虚拟内存管理

    千次阅读 2017-05-22 21:21:15
    虚拟地址可通过每个进程上页表与物理地址进行映射,获得真正的物理地址 如果虚拟地址所对应的物理地址不在物理内存中,则产生缺页中断,真正分配物理地址,同时更新进程的页表;如果此时物理内存已经耗尽,则根据...
  • 除了进程私有的虚拟地址空间外,内核虚拟内存地址包含内核的代码和数据,所有进程的这部分区域被映射到同一个物理页面。 另外,每一个进程相关的进程信息,例如进程的页表,mm内存映射结构等也存放在内核地址空间...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 820
精华内容 328
关键字:

linux虚拟内存地址与物理内存地址

linux 订阅