精华内容
下载资源
问答
  • Linux虚拟内存缺页中断

    千次阅读 多人点赞 2020-08-02 21:46:06
    Linux虚拟内存地址空间 为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享...

    Linux虚拟内存地址空间

    为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存

    虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。

    请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换

    虚拟内存的好处:
    1. 扩大地址空间
    2. 内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
    3. 公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
    4. 当进程通信时,可采用虚存共享的方式实现
    5. 当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
    6. 虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
    7. 在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片
    虚拟内存的代价:
    1. 虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
    2. 虚拟地址到物理地址的转换,增加了指令的执行时间。
    3. 页面的换入换出需要磁盘I/O,这是很耗时的
    4. 如果一页中只有一部分数据,会浪费内存

    操作系统中的缺页中断

    malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。
    缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。

    缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:

    1、保护CPU现场

    2、分析中断原因

    3、转入缺页中断处理程序进行处理

    4、恢复CPU现场,继续执行

    但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:

    1、在指令执行期间产生和处理缺页中断信号

    2、一条指令在执行期间,可能产生多次缺页中断

    3、缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。

    展开全文
  • 作者简介韩传华,就职于南京大鱼半导体有限公司,主要从事linux相关系统软件开发工作,负责Soc芯片BringUp及系统软件开发,乐于分享喜欢学习,喜欢专研Linux内核源代码。前面讲到...

    作者简介

    韩传华,就职于南京大鱼半导体有限公司,主要从事linux相关系统软件开发工作,负责Soc芯片BringUp及系统软件开发,乐于分享喜欢学习,喜欢专研Linux内核源代码。

    前面讲到过写时复制缺页异常(COW),一般用于父子进程之间共享页,而我们会常见一种缺页异常是匿名映射缺页异常,今天我们就来讨论下这种缺页异常,让大家彻底理解它。注:本文使用linux-5.0内核源代码。文章分为以下几节内容:

    1.匿名映射缺页异常的触发情况 2.0页是什么?为什么使用0页?

    3.源代码分析 

        3.1 触发条件    

        3.2 第一次读匿名页

        3.3 第一次写匿名页  

        3.4 读之后写匿名页

    4.应用层实验

    5.总结

    在讲解匿名映射缺页异常之前我们先要了解以下什么是匿名页?与匿名页相对应的是文件页,文件页我们应该很好理解,就是映射文件的页,如:通过mmap映射文件到虚拟内存然后读文件数据,进程的代码数据段等,这些页有后备缓存也就是块设备上的文件,而匿名页就是没有关联到文件的页,如:进程的堆、栈等。还有一点需要注意:下面讨论的都是私有的匿名页的情况,共享匿名页在内核演变为文件映射缺页异常(伪文件系统),后面有机会我们会讲解,感兴趣的小伙伴可以看一看mmap的代码实现对共享匿名页的处理。

    一,匿名映射缺页异常的触发情况

    前面我们讲解了什么是匿名页,那么思考一下什么情况下会触发匿名映射缺页异常呢?这种异常对于我们来说非常常见:

    1.当我们应用程序使用malloc来申请一块内存(堆分配),在没有使用这块内存之前,仅仅是分配了虚拟内存,并没有分配物理内存,第一次去访问的时候才会通过触发缺页异常来分配物理页建立和虚拟页的映射关系。

    2.当我们应用程序使用mmap来创建匿名的内存映射的时候,页同样只是分配了虚拟内存,并没有分配物理内存,第一次去访问的时候才会通过触发缺页异常来分配物理页建立和虚拟页的映射关系。

    3.当函数的局部变量比较大,或者是函数调用的层次比较深,导致了当前的栈不够用了,这个时候需要扩大栈。当然了上面的这几种场景对应应用程序来说是透明的,内核为用户程序做了大量的处理工作,下面几节会看到如何处理。

    二,0页是什么?为什么使用0页?

    这里为什么会说到0页呢?什么是0页呢?是地址为0的页吗?答案是:系统初始化过程中分配了一页的内存,这段内存全部被填充0。下面我们来看下0页如何分配的:在arch/arm64/mm/mmu.c中:

        61 /*
        62  * Empty_zero_page is a special page that is used for zero-initialized data
        63  * and COW.
        64  */
        65 unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)] __page_aligned_bss;
        66 EXPORT_SYMBOL(empty_zero_page);
    

    可以看到定义了一个全局变量,大小为一页,页对齐到bss段,所有这段数据内核初始化的时候会被清零,所有称之为0页。

    那么为什么使用0页呢?一个是它的数据都是被0填充,读的时候数据都是0,二是节约内存,匿名页面第一次读的时候数据都是0都会映射到这页中从而节约内存(共享0页),那么如果有进程要去写这个这个页会怎样呢?答案是发生COW重新分配页来写

    三,源代码分析

    3.1 触发条件

    当第一节中的触发情况发生的时候,处理器就会发生缺页异常,从处理器架构相关部分过渡到处理器无关部分,最终到达handle_pte_fault函数:

      3742 static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
      3743 {
      3744         pte_t entry;
      ...
      3782         if (!vmf->pte) {
      3783                 if (vma_is_anonymous(vmf->vma))
      3784                         return do_anonymous_page(vmf);
      3785                 else
      3786                         return do_fault(vmf);
      3787         }
    

    3782和3783行是匿名映射缺页异常的触发条件:

    1.发生缺页的地址所在页表项不存在。

    2.是匿名页发生的,即是vma->vm_ops为空。

    当满足这两个条件的时候就会调用do_anonymous_page函数来处理匿名映射缺页异常。

      2871 /*
      2872  * We enter with non-exclusive mmap_sem (to exclude vma changes,
      2873  * but allow concurrent faults), and pte mapped but not yet locked.
      2874  * We return with mmap_sem still held, but pte unmapped and unlocked.
      2875  */
      2876 static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
      2877 {
      2878         struct vm_area_struct *vma = vmf->vma;
      2879         struct mem_cgroup *memcg;
      2880         struct page *page;
      2881         vm_fault_t ret = 0;
      2882         pte_t entry;
      2883 
      2884         /* File mapping without ->vm_ops ? */
      2885         if (vma->vm_flags & VM_SHARED)
      2886                 return VM_FAULT_SIGBUS;
      2887 
      2888         /*
      2889         ¦* Use pte_alloc() instead of pte_alloc_map().  We can't run
      2890         ¦* pte_offset_map() on pmds where a huge pmd might be created
      2891         ¦* from a different thread.
      2892         ¦*
      2893         ¦* pte_alloc_map() is safe to use under down_write(mmap_sem) or when
      2894         ¦* parallel threads are excluded by other means.
      2895         ¦*
      2896         ¦* Here we only have down_read(mmap_sem).
      2897         ¦*/
      2898         if (pte_alloc(vma->vm_mm, vmf->pmd))
      2899                 return VM_FAULT_OOM;
      2904 
      ...
    

    2885行判断:发生缺页的vma是否为私有映射,这个函数处理的是私有的匿名映射

    2898行 如何页表不存在则分配页表(有可能缺页地址的页表项所在的直接页表不存在)。

    3.2 第一次读匿名页情况

      ...
      2905         /* Use the zero-page for reads */
      2906         if (!(vmf->flags & FAULT_FLAG_WRITE) &&
      2907                         !mm_forbids_zeropage(vma->vm_mm)) {
      2908                 entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
      2909                                                 vma->vm_page_prot));
      2910                 vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
      2911                                 vmf->address, &vmf->ptl);
      2912                 if (!pte_none(*vmf->pte))
      2913                         goto unlock;
      2914                 ret = check_stable_address_space(vma->vm_mm);
      2915                 if (ret)
      2916                         goto unlock;
      2917                 /* Deliver the page fault to userland, check inside PT lock */
      2918                 if (userfaultfd_missing(vma)) {
      2919                         pte_unmap_unlock(vmf->pte, vmf->ptl);
      2920                         return handle_userfault(vmf, VM_UFFD_MISSING);
      2921                 }
      2922                 goto setpte;
      2923         }
      ...
      2968 setpte:
      2969         set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
      
    

    2906到2923行是处理的是私有匿名页读的情况:这里就会用到我们上面将的0页了。

    2906和 2907行判断是否是由于读操作导致的缺页而且没有禁止0页。

    2908-2909行是核心部分:设置页表项的值映射到0页

    我们主要研究这个语句:pfn_pte用来将页帧号和页表属性拼接为页表项值:

    arch/arm64/include/asm/pgtable.h:
    77 #define pfn_pte(pfn,prot)       \
    78         __pte(__phys_to_pte_val((phys_addr_t)(pfn) << PAGE_SHIFT) | pgprot_val(prot))
    

    是将pfn左移PAGE_SHIFT位(一般为12bit),或上pgprot_val(prot)

    先看my_zero_pfn:

    include/asm-generic/pgtable.h:
       875 static inline unsigned long my_zero_pfn(unsigned long addr)
       876 {
       877         extern unsigned long zero_pfn;
       878         return zero_pfn;
       879 }
    

    || \/

    mm/memory.c:
       126 unsigned long zero_pfn __read_mostly;
       127 EXPORT_SYMBOL(zero_pfn);
       128 
       129 unsigned long highest_memmap_pfn __read_mostly;
       130 
       131 /*
       132  * CONFIG_MMU architectures set up ZERO_PAGE in their paging_init()
       133  */
       134 static int __init init_zero_pfn(void)
       135 {
       136         zero_pfn = page_to_pfn(ZERO_PAGE(0));
       137         return 0;
       138 }
       139 core_initcall(init_zero_pfn);
    

    || \/

    arch/arm64/include/asm/pgtable.h:
       54 /*
       55  * ZERO_PAGE is a global shared page that is always zero: used
       56  * for zero-mapped memory areas etc..
       57  */
       58 extern unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)];
       59 #define ZERO_PAGE(vaddr)        phys_to_page(__pa_symbol(empty_zero_page))
    

    最终我们看到使用的就是内核初始化设置的empty_zero_page这个0页得到页帧号。再看看pfn_pte的第二个参数vma->vm_pageprot,这是vma的访问权限,在做内存映射mmap的时候会被设置。

    那么我们想知道的时候是什么时候0页被设置为了只读属性的(也就是页表项何时被设置为只读)

    我们带着这个问题去在内核代码中寻找答案。其实代码看到这里一般看不到头绪,但是我们要知道何时vma的vm_page_prot成员被设置的,如何被设置的,有可能就能找到答案。

    我们到mm/mmap.c中去寻找答案:我们以do_brk_flags函数为例,这是设置堆的函数我们关注到3040行设置了vm_page_prot:

    3040         vma->vm_page_prot = vm_get_page_prot(flags);  
    

    || \/

       110 pgprot_t vm_get_page_prot(unsigned long vm_flags)
       111 {
       112         pgprot_t ret = __pgprot(pgprot_val(protection_map[vm_flags &
       113                                 (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]) |
       114                         pgprot_val(arch_vm_get_page_prot(vm_flags)));
       115 
       116         return arch_filter_pgprot(ret);
       117 }
       118 EXPORT_SYMBOL(vm_get_page_prot);
    

    vm_get_page_prot函数会根据传递来的vmflags是否为VMREAD|VMWRITE|VMEXEC|VMSHARED来转换为保护位组合,继续往下看 || \/

        78 /* description of effects of mapping type and prot in current implementation.
        79  * this is due to the limited x86 page protection hardware.  The expected
        80  * behavior is in parens:
        81  *
        82  * map_type     prot
        83  *              PROT_NONE       PROT_READ       PROT_WRITE      PROT_EXEC
        84  * MAP_SHARED   r: (no) no      r: (yes) yes    r: (no) yes     r: (no) yes
        85  *              w: (no) no      w: (no) no      w: (yes) yes    w: (no) no
        86  *              x: (no) no      x: (no) yes     x: (no) yes     x: (yes) yes
        87  *
        88  * MAP_PRIVATE  r: (no) no      r: (yes) yes    r: (no) yes     r: (no) yes
        89  *              w: (no) no      w: (no) no      w: (copy) copy  w: (no) no
        90  *              x: (no) no      x: (no) yes     x: (no) yes     x: (yes) yes
        91  *
        92  * On arm64, PROT_EXEC has the following behaviour for both MAP_SHARED and
        93  * MAP_PRIVATE:
        94  *                                                              r: (no) no
        95  *                                                              w: (no) no
        96  *                                                              x: (yes) yes
        97  */
        98 pgprot_t protection_map[16] __ro_after_init = {
        99         __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111,
       100         __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111
       101 };
    

    protection_map数组定义了从P000到S111一共16种组合,P表示私有(Private),S表示共享(Share),后面三个数字依次为可读、可写、可执行,如:_S010表示共享、不可读、可写、不可执行。

    || \/

    arch/arm64/include/asm/pgtable-prot.h:
       93 #define PAGE_NONE               __pgprot(((_PAGE_DEFAULT) & ~PTE_VALID) | PTE_PROT_NONE | PTE_RDONLY | PTE_NG | PTE_PXN | PTE_UXN)
       94 #define PAGE_SHARED             __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_UXN | PTE_WRITE)
       95 #define PAGE_SHARED_EXEC        __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_WRITE)
       96 #define PAGE_READONLY           __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN | PTE_UXN)
       97 #define PAGE_READONLY_EXEC      __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN)
       98 #define PAGE_EXECONLY           __pgprot(_PAGE_DEFAULT | PTE_RDONLY | PTE_NG | PTE_PXN)
       99 
      100 #define __P000  PAGE_NONE
      101 #define __P001  PAGE_READONLY
      102 #define __P010  PAGE_READONLY
      103 #define __P011  PAGE_READONLY
      104 #define __P100  PAGE_EXECONLY
      105 #define __P101  PAGE_READONLY_EXEC
      106 #define __P110  PAGE_READONLY_EXEC
      107 #define __P111  PAGE_READONLY_EXEC
      108 
      109 #define __S000  PAGE_NONE
      110 #define __S001  PAGE_READONLY
      111 #define __S010  PAGE_SHARED
      112 #define __S011  PAGE_SHARED
      113 #define __S100  PAGE_EXECONLY
      114 #define __S101  PAGE_READONLY_EXEC
      115 #define __S110  PAGE_SHARED_EXEC
      116 #define __S111  PAGE_SHARED_EXEC
    

    可以发现对于私有的映射只有只读(PTE_RDONLY)没有可写属性(PTE_WRITE)105-107行 ,虽然之前设置的时候是设置了可写(VM_WRITE)!而对应共享映射则会有可写属性。

    而这个被设置的保护位组合最终会在缺页异常中被设置到页表中:上面说到的do_anonymous_page函数:

    2908                 entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
    2909                                                 vma->vm_page_prot));
    

    对于私有匿名映射的页,假设设置的vmflags为VMREAD|VMWRITE则对应的保护位组合为:P110即为PAGE_READONLY_EXEC=pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PT_ENG | PTE_PXN)不会设置为可写。

    所以就将其页表设置为了只读!!!

    2922行 跳转到setpte去将设置好的页表项值填写到页表项中。

    当匿名页读之后再次去写时候会由于页表属性为只读导致COW缺页异常,详将COW相关文章,再此不在赘述。下面用图说话:

    3.3 第一次写匿名页的情况

    接着do_anonymous_page函数继续往下分析:

     2876 static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
      2877 {
      ...
      2924 
      2925         /* Allocate our own private page. */
      2926         if (unlikely(anon_vma_prepare(vma)))
      2927                 goto oom;
      2928         page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
      2929         if (!page)
      2930                 goto oom;
      2931 
      2932         if (mem_cgroup_try_charge_delay(page, vma->vm_mm, GFP_KERNEL, &memcg,
      2933                                         false))
      2934                 goto oom_free_page;
      2935 
      2936         /*
      2937         ¦* The memory barrier inside __SetPageUptodate makes sure that
      2938         ¦* preceeding stores to the page contents become visible before
      2939         ¦* the set_pte_at() write.
      2940         ¦*/
      2941         __SetPageUptodate(page);
      2942 
      2943         entry = mk_pte(page, vma->vm_page_prot);
      2944         if (vma->vm_flags & VM_WRITE)
      2945                 entry = pte_mkwrite(pte_mkdirty(entry));
     2946 
      2947         vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
      2948                         &vmf->ptl);
      2949         if (!pte_none(*vmf->pte))
      2950                 goto release;
      2951 
      2952         ret = check_stable_address_space(vma->vm_mm);
      2953         if (ret)
      2954                 goto release;
      2955 
      2956         /* Deliver the page fault to userland, check inside PT lock */
      2957         if (userfaultfd_missing(vma)) {
      2958                 pte_unmap_unlock(vmf->pte, vmf->ptl);
      2959                 mem_cgroup_cancel_charge(page, memcg, false);
      2960                 put_page(page);
      2961                 return handle_userfault(vmf, VM_UFFD_MISSING);
      2962         }
      2963 
      2964         inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
      2965         page_add_new_anon_rmap(page, vma, vmf->address, false);
      2966         mem_cgroup_commit_charge(page, memcg, false, false);
      2967         lru_cache_add_active_or_unevictable(page, vma);
      2968 setpte:
      2969         set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
      2970 
      2971         /* No need to invalidate - it was non-present before */
      2972         update_mmu_cache(vma, vmf->address, vmf->pte);
      2973 unlock:
      2974         pte_unmap_unlock(vmf->pte, vmf->ptl);
      2975         return ret;
      2976 release:
      2977         mem_cgroup_cancel_charge(page, memcg, false);
      2978         put_page(page);
      2979         goto unlock;
      2980 oom_free_page:
      2981         put_page(page);
      2982 oom:
      2983         return VM_FAULT_OOM;
      2984 }
    

    当判断不是读操作导致的缺页的时候,则是写操作造成,处理写私有的匿名页情况,请记住这依然是第一次访问这个匿名页只不过是写访问而已

    2928 行会分配一个高端 可迁移的 被0填充的物理页。2941 设置页中数据有效 2943 使用页帧号和vma的访问权限设置页表项值(注意:这个时候页表项属性依然为只读)。

    2944-2945行 如果vma可写,则设置页表项值为脏且*可写*(这个时候才设置为可写)。

    2964行 匿名页计数统计 2965行 添加到匿名页的反向映射中 2967行 添加到lru链表 2969 将设置好的页表项值填充到页表项中。

    下面用图说话:

    3.4 读之后写匿名页

    读之后写匿名页,其实已经很简单了,那就是发生COW写时复制缺页。下面依然看图说话:

    四,应用层实验

    实验1:主要体验下内核的按需分配页策略!实验代码:mmap映射10 * 4096 * 4096/1M=160M内存空间,映射和写页前后获得内存使用情况:

        1 #include <stdio.h>
        2 #include <stdlib.h>
        3 #include <sys/mman.h>
        4 #include <unistd.h>
        5 
        6 
        7 #define MAP_LEN (10 * 4096 * 4096)
        8 
        9 int main(int argc, char **argv)
       10 {
       11         char *p;
       12         int i;
       13 
       14 
       15         puts("before mmap ->please exec: free -m\n");
       16         sleep(10);
       17         p = (char *)mmap(0, MAP_LEN, PROT_READ |PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
       18 
       19         puts("after mmap ->please exec: free -m\n");
       20         puts("before write....\n");
       21         sleep(10);
       22 
       23         for(i=0;i <4096 *10; i++)
       24                 p[4096 * i] = 0x55;
       25 
       26 
       27         puts("after write ->please exec: free -m\n");
       28 
       29         pause();
       30 
       31         return 0;
       32 }                                      
    

    执行结果:

    出现“before mmap ->please exec: free -m”打印后执行:

    $ free -m
                  总计         已用        空闲      共享    缓冲/缓存    可用
    内存:15921        6561         462         796        8897        8214
    交换:16290         702       15588
    

    出现“after mmap ->please exec: free -m”打印后执行:

    $ free -m
                  总计         已用        空闲      共享    缓冲/缓存    可用
    内存:15921        6565         483         771        8872        8236
    交换:16290         702       15588
    

    出现“after write ->please exec: free -m”后执行:

    $:~/study/user_test/page-fault$ free -m
                  总计         已用        空闲      共享    缓冲/缓存    可用
    内存:15921        6727         322         770        8871        8076
    交换:16290         702       15588
    

    我们只关注已用内存,可以发现映射前后基本上已用内存没有变化(考虑到其他内存申请情况存在,也会有内存变化)是6561M和6565M,说明mmap的时候并没有分配物理内存,写之后发现内存使用为6727M, 6727-6565=162M与我们mmap的大小基本一致,说明了匿名页实际写的时候才会分配等量的物理内存。

    实验2:主要体验下匿名页读之后写内存页申请情况 实验代码:mmap映射10 * 4096 * 4096/1M=160M内存空间,映射、读然后写页前后获得内存使用情况:

        1 #include <stdio.h>
        2 #include <stdlib.h>
        3 #include <sys/mman.h>
        4 #include <unistd.h>
        5 
        6 
        7 #define MAP_LEN (10 * 4096 * 4096)
        8 
        9 int main(int argc, char **argv)
       10 {
       11         char *p;
       12         int i;
       13 
       14 
       15         puts("before mmap...pls show free:.\n");
       16         sleep(10);
    ✗  17         p = (char *)mmap(0, MAP_LEN, PROT_READ |PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
       18 
       19         puts("after mmap....\n");
       20 
       21         puts("before read...pls show free:.\n");
       22         sleep(10);
       23 
       24         puts("start read....\n");
       25 
       26 
       27         for(i=0;i <4096 *10; i++)
       28                 printf("%d ", p[4096 * i]);
       29         printf("\n");
       30 
       31         puts("after read....pls show free:\n");
       32 
       33         sleep(10);
       34 
       35         puts("start write....\n");
       36 
       37         for(i=0;i <4096 *10; i++)
       38                 p[4096 * i] = 0x55;
       39 
       40 
       41         puts("after write...pls show free:.\n");
       42 
       43         pause();
       44 
       45         return 0;
       46 }
    

    执行结果:出现"before mmap ->please exec: free -m" 后执行:

    $ free -m
                  总计         已用        空闲      共享    缓冲/缓存    可用
    内存:15921        6590         631         780        8700        8164
    交换:16290         702       15588
    

    出现"before read ->please exec: free -m"后执行:

    $ free -m
                  总计         已用        空闲      共享    缓冲/缓存    可用
    内存:15921        6586         644         770        8690        8178
    交换:16290         702       15588
    

    出现"after read ->please exec: free -m"后执行:

    $ free -m
                  总计         已用        空闲      共享    缓冲/缓存    可用
    内存:15921        6587         624         789        8709        8158
    交换:16290         702       15588
    

    出现"after write ->please exec: free -m"后执行:

    $ free -m
                  总计         已用        空闲      共享    缓冲/缓存    可用
    内存:15921        6749         462         789        8709        7996
    交换:16290         702       15588
    

    可以发现:读之后和之前基本上内存使用没有变化(实际上映射到了0页,这是内核初始化时候分配好的),知道写之后6749-6587=162M符合预期,而且打印可以发现数据全为0。

    分析:实际上,mmap的时候只是申请了一块vma,读的时候发生一次缺页异常,映射到0页,所有内存没有分配,当再次写这个页面的时候,发生了COW分配新页(cow中分配新页的时候会判断原来的页是否为0页,如果为0页就直接分配页然后用0填充)。

    五,总结

    匿名映射缺页异常是我们遇到的一种很常用的一种异常,对于匿名映射,映射完成之后,只是获得了一块虚拟内存,并没有分配物理内存,当第一次访问的时候:如果是读访问,会将虚拟页映射到0页,以减少不必要的内存分配;如果是写访问,则会分配新的物理页,并用0填充,然后映射到虚拟页上去。而如果是先读访问一页然后写访问这一页,则会发生两次缺页异常:第一次是匿名页缺页异常的读的处理,第二次是写时复制缺页异常处理。

    (END)

    Linux阅码场原创精华文章汇总

    更多精彩,尽在"Linux阅码场",扫描下方二维码关注

    分享、在看与点赞,我能拥有一个吗

    展开全文
  • Linux虚拟内存系统 Linux将进程的地址空间,将虚拟内存区域组成一些区域(段)的集合。一个区域就是已分配的虚拟内存连续片(chunk),这些段通过某种方式相互链接。 一个进程的地址空间由下而上由多个不同类型的...

    Linux虚拟内存系统

    Linux将进程的地址空间,将虚拟内存区域组成一些区域(段)的集合。一个区域就是已分配的虚拟内存连续片(chunk),这些段通过某种方式相互链接。

    一个进程的地址空间由下而上由多个不同类型的连续区域(段)组合而成:代码段、初始化数据段、未初始化数据段、运行时堆区域、共享库的映射区域、运行时栈区域、内核代码和数据段(多个进程通过虚拟内存方式共享)、内核为每个进程维护的元数据(页表、task_struct、mm结构)

    它们在虚拟地址空间中内部连续,中间则存在空隙,内核通过链表(早期)来记录这些段的位置信息,访问权限等

    task_struct中保存有内核运行进程的基本信息,PID、用户进程栈指针、程序计数器PC等

    Linux采取链表组织多个段区域

    在这里插入图片描述

    task_struct结构中mm_struct结构用于维护进程的虚拟内存空间,其核心是mmap结构,即一个维护每个虚拟内存段的链表,每一个vm_area_struct结构记录了一个虚拟内存段的基本信息。

    vm_strat 指向这个区域的起始位置
    vm_end 指向这个区域的结束位置
    vm_prot 区域的访问权限,如代码段只读
    vm_flag 一些标记位,进程是否私有或共享
    vm_next 指向链表中的下一个区域结构

    Linux缺页异常处理

    MMU内存管理单元在引用一个虚拟地址时,会执行一个前置判断,其基本过程如下:

    ①虚拟地址A位置是否合法,即地址A是否在某个合法的区域内,其具体的做法是遍历每个区域结构,判断地址A是否在某个区域的vm_start和vm_end之间。如果不再某个区域内,触发一个段访问错误

    虚拟地址空间可能包含很多这样的区域,链表遍历查找会很耗时,Linux在链表基础之上实现了一个查找树,并借此提高查找速度。

    ②试图访问的内存是否有合法的权限,进程是否有读写该区域的权限。例如运行在用户模式下的进程无法对代码段执行写操作,否则触发一个保护异常。

    ③若一切顺利,内核确认对地址A的引用存在且合法,那么执行页面置换逻辑,将相应的虚拟页置换进主内存中,缺页中断完成后,MMU即可对该虚拟内存正常的引用。

    在这里插入图片描述

    展开全文
  • 理解 Linux虚拟内存

    2020-10-07 15:05:39
    理解 Linux虚拟内存 文章目录理解 Linux虚拟内存前言一、虚拟内存二、分页和页表三、内存寻址和分配四、功能4.1进程内存管理4.2数据共享4.3 SWAP 前言 毋庸置疑,虚拟内存绝对是操作系统中最重要的概念之一...

    理解 Linux 的虚拟内存


    前言

    毋庸置疑,虚拟内存绝对是操作系统中最重要的概念之一。我想主要是由于内存的重要”战略地位”。CPU太快,但容量小且功能单一,其他 I/O 硬件支持各种花式功能,可是相对于 CPU,它们又太慢。于是它们之间就需要一种润滑剂来作为缓冲,这就是内存大显身手的地方。

    而在现代操作系统中,多任务已是标配。多任务并行,大大提升了 CPU 利用率,但却引出了多个进程对内存操作的冲突问题,虚拟内存概念的提出就是为了解决这个问题。


    一、虚拟内存

    在这里插入图片描述

    上图是虚拟内存最简单也是最直观的解释。

    操作系统有一块物理内存(中间的部分),有两个进程(实际会更多)P1 和 P2,操作系统偷偷地分别告诉 P1 和 P2,我的整个内存都是你的,随便用,管够。可事实上呢,操作系统只是给它们画了个大饼,这些内存说是都给了 P1 和 P2,实际上只给了它们一个序号而已。只有当 P1 和 P2 真正开始使用这些内存时,系统才开始使用辗转挪移,拼凑出各个块给进程用,P2 以为自己在用 A 内存,实际上已经被系统悄悄重定向到真正的 B 去了,甚至,当 P1 和 P2 共用了 C 内存,他们也不知道。

    操作系统的这种欺骗进程的手段,就是虚拟内存。对 P1 和 P2 等进程来说,它们都以为自己占用了整个内存,而自己使用的物理内存的哪段地址,它们并不知道也无需关心。

    二、分页和页表

    虚拟内存是操作系统里的概念,对操作系统来说,虚拟内存就是一张张的对照表,P1 获取 A 内存里的数据时应该去物理内存的 A 地址找,而找 B 内存里的数据应该去物理内存的 C 地址。

    我们知道系统里的基本单位都是 Byte 字节,如果将每一个虚拟内存的 Byte 都对应到物理内存的地址,每个条目最少需要 8字节(32位虚拟地址->32位物理地址),在 4G 内存的情况下,就需要 32GB 的空间来存放对照表,那么这张表就大得真正的物理地址也放不下了,于是操作系统引入了 页(Page)的概念。

    在系统启动时,操作系统将整个物理内存以 4K 为单位,划分为各个页。之后进行内存分配时,都以页为单位,那么虚拟内存页对应物理内存页的映射表就大大减小了,4G 内存,只需要 8M 的映射表即可,一些进程没有使用到的虚拟内存,也并不需要保存映射关系,而且Linux 还为大内存设计了多级页表,可以进一页减少了内存消耗。操作系统虚拟内存到物理内存的映射表,就被称为页表。


    三、内存寻址和分配

    我们知道通过虚拟内存机制,每个进程都以为自己占用了全部内存,进程访问内存时,操作系统都会把进程提供的虚拟内存地址转换为物理地址,再去对应的物理地址上获取数据。CPU 中有一种硬件,内存管理单元 MMU(Memory Management Unit)专门用来将翻译虚拟内存地址。CPU 还为页表寻址设置了缓存策略,由于程序的局部性,其缓存命中率能达到 98%。

    以上情况是页表内存在虚拟地址到物理地址的映射,而如果进程访问的物理地址还没有被分配,系统则会产生一个缺页中断,在中断处理时,系统切到内核态为进程虚拟地址分配物理地址。

    四、功能

    虚拟内存不仅通过内存地址转换解决了多个进程访问内存冲突的问题,还带来更多的益处。

    4.1进程内存管理

    它有助于进程进行内存管理,主要体现在:

    内存完整性:由于虚拟内存对进程的”欺骗”,每个进程都认为自己获取的内存是一块连续的地址。我们在编写应用程序时,就不用考虑大块地址的分配,总是认为系统有足够的大块内存即可。
    安全:由于进程访问内存时,都要通过页表来寻址,操作系统在页表的各个项目上添加各种访问权限标识位,就可以实现内存的权限控制。

    4.2数据共享

    通过虚拟内存更容易实现内存和数据的共享。

    在进程加载系统库时,总是先分配一块内存,将磁盘中的库文件加载到这块内存中,在直接使用物理内存时,由于物理内存地址唯一,即使系统发现同一个库在系统内加载了两次,但每个进程指定的加载内存不一样,系统也无能为力。

    而在使用虚拟内存时,系统只需要将进程的虚拟内存地址指向库文件所在的物理内存地址即可。如上文图中所示,进程 P1 和 P2 的 B 地址都指向了物理地址 C。

    而通过使用虚拟内存使用共享内存也很简单,系统只需要将各个进程的虚拟内存地址指向系统分配的共享内存地址即可。

    4.3 SWAP

    虚拟内存可以让帮进程”扩充”内存。

    我们前文提到了虚拟内存通过缺页中断为进程分配物理内存,内存总是有限的,如果所有的物理内存都被占用了怎么办呢?

    Linux 提出 SWAP 的概念,Linux 中可以使用 SWAP 分区,在分配物理内存,但可用内存不足时,将暂时不用的内存数据先放到磁盘上,让有需要的进程先使用,等进程再需要使用这些数据时,再将这些数据加载到内存中,通过这种”交换”技术,Linux 可以让进程使用更多的内存。

    参考:https://www.cnblogs.com/zhenbianshu/p/10300769.html

    展开全文
  • Linux缺页中断处理

    千次阅读 2010-11-10 19:58:00
    Linux缺页中断处理1.请求调页中断:进程线性地址空间里的页面不必常驻内存,例如进程的分配请求被理解满足,空间仅仅保留vm_area_struct的空间,页面可能被交换到后援存储器,或者写一个只读页面(COW)。Linux...
  • Linux缺页异常处理

    千次阅读 2018-11-01 14:32:47
    1.如果访问的虚拟地址在进程空间没有对应的VMA(mmap和malloc可以分配vma),则缺页处理失败,程序出现段错误. 2.Linux把没有映射到文件的映射叫做匿名映射(malloc和mmap的匿名映射)。 3.remap_pfn_range把内核...
  • Linux 进程虚拟内存

    2021-03-27 16:59:34
    本文整理了 Linux 内核中进程虚拟内存的相关知识。 进程虚拟内存 虚拟地址空间 各个进程的虚拟地址空间起始于 0,一直到 TASK_SIZE - 1,其上是内核地址空间。在 IA-32 系统上地址空间的范围是 4GB,按照默认比例 3...
  • 文章目录虚拟内存空间空户空间内核空间用户空间内存分配malloc内核空间内存分配kmallocvmalloc 虚拟内存空间 空户空间 内核空间 用户空间内存分配 malloc 内核空间内存分配 kmalloc vmalloc
  • Linux系统虚拟内存空间一般布局示意图 1.1 线性空间 线性地址空间:是指Linux系统中从0x00000000到0xFFFFFFFF整个4GB虚拟存储空间。线性空间又分为用户空间和内核空间。 1.1.1 用户空间(进程地址空间) 用户...
  • Linux 虚拟内存

    2021-01-23 20:19:56
    操作系统中的 CPU 和主内存(Main memory)都是稀缺资源,所有运行在当前操作系统的进程会共享系统中的 CPU 和内存资源,操作系统会使用 CPU 调度器分配 CPU 时间1并引入虚拟内存系统以管理物理内存,本文会分析操作...
  • Linux V0.11 缺页中断响应代码: .globl _page_fault _page_fault: xchgl %eax,(%esp) pushl %ecx pushl %edx push %ds push %es push %fs movl $0x10,%edx mov %dx,%ds mov %dx,%es
  • Linux——缺页中断与内存置换算法,你想知道的都在这里!缺页中断页面置换算法缺页次数一、先进先出(FIFO)二.最近最久未使用(LRU)三、最佳置换算法(OPT) 缺页中断 缺页中断就是要访问的页不在主存,需要操作...
  • 这篇文章是我对虚拟内存、可执行文件的编译以及内存分布的总结,这篇文章要搞清楚以下几个问题。 1、为什么要虚拟内存; 2、虚拟地址的内存分布; 3、虚拟地址与物理地址的映射; 4、可执行文件的编译过程; 5、可...
  • 缺页中断发生在系统对虚拟地址转换成物理地址的过程中。如果对应的页目录或者页表项没有对应有效的物理内存,则会发生缺页中断。 系统在初始化的时候注册了缺页中断的处理函数。中断号是14。 // 缺页和写保护异常...
  • linux虚拟内存

    2017-09-18 17:36:56
    Linux虚拟内存管理有几个关键概念: 每个进程有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址 虚拟地址可通过每个进程上页表与物理地址进行映射,获得真正物理地址 如果虚拟地址...
  • 为什么 Linux 需要虚拟内存2020-04-08 为什么这么设计 系统设计 虚拟内存 操作系统 Linux 磁盘为什么这么设计(Why’s THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会...
  • Linux虚拟内存

    2019-07-14 01:32:04
    一、虚拟内存定义 虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间)。 二、其他概念 逻辑地址是指CPU所生产的的地址 物理地址支持实际内存的地址 ...
  • 尽管每个进程独立拥有3GB的可访问地址空间,但是这些资源都是内核开出的空头支票,也就是说进程手握着和自己相关的一个个虚拟内存区域(vma),但是这些虚拟内存区域并不会在创建的时候就和物理页框挂钩,由于程序
  • 浅谈Linux虚拟内存技术

    千次阅读 2019-02-23 17:03:16
    文章目录虚拟内存虚拟内存的概念实例分析Linux虚拟内存技术虚拟内存的页、物理内存的页框及页表请页与交换快表页的共享页的保护多级页表Linux的页表结构 内存是程序得以运行的重要物质基础。如何在有限的内存空间...
  • 进程虚拟内存虚拟地址空间各个进程的虚拟地址空间起始于 0,一直到 TASK_SIZE - 1,其上是内核地址空间。在 IA-32 系统上地址空间的范围是 4GB,按照默认比例 3:1 来划分的话,内核分配 1GB,各个用户进程可用的...
  • Linux虚拟内存管理

    千次阅读 多人点赞 2017-05-22 21:21:15
    Linux虚拟内存管理有几个关键概念: 每个进程有独立的虚拟地址空间,进程访问的虚拟地址空间并不是真正的物理地址 虚拟地址可通过每个进程上页表与物理地址进行映射,获得真正的物理地址 如果虚拟地址所对应...
  • Linux内存缺页错误处理

    千次阅读 2014-03-03 11:56:30
    linux内存访问是通过页表访问的形式访问的,当虚拟内存找不到对应物理内存,则抛出缺页错误。 缺页错误函数在mm/fault.c中的do_page_fault定义, do_page_fault(struct pt_regs *regs, unsigned long error_code) { ...
  • Linux中用户进程线性地址能寻址的范围是0 - 3G,那么是不是需要提前先把这3G虚拟内存的页表都建立好呢?一般情况下,物理内存是远远小于3G的,加上同时有很多进程都在运行,根本无法给每个进程提前建立3G的线性地址...
  • Linux虚拟内存和物理内存的关系

    千次阅读 2015-01-29 16:43:03
    首先,让我们看下虚拟内存: 第一层理解 1. 每个进程都有自己独立的4G内存空间,各个进程的内存空间具有类似的结构 2. 一个新进程建立的时候,将会建立起自己的内存空间,此进程的数据,代码等从磁盘拷贝到...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,473
精华内容 4,589
关键字:

linux缺页虚拟内存

linux 订阅