良许Linux教程网 干货合集 Linux系统内核架构详解(二)

Linux系统内核架构详解(二)

Linux系统中内核是一个非常重要的一部分,那么Linux内核具体是什么样子呢?下面本篇文章和大家深入讲解一下Linux系统内核机构,有需要的朋友可以参考一下。

Linux系统内核架构详解(二)

5.页帧

1)页帧代表系统内存的最小单位,对内存中的每个页都会创建struct page的一个实例。

2)page的定义

    struct page{       unsigned long flags;                               /*原子标志,用于描述页的属性,有些情况下会异步更新*/       atomic_t _count;                                   /*使用计数,表示内核中引用该页的次数。若_count=0,page实例当前                                                           不可用,可以删除。若_count>0,该实例不会从内存中删除*/                                      union{                atomic_t _mapcount;                       /*内存管理子系统中映射的页表项计数,用于表示页是否已经被映射,还                                                            用于限制逆向映射搜索。在页表中有多少项指向该页。*/                                                          注:atomic_t是个32个比特位,允许以原子方式修改其值,即不受并发访                                                              问的影响。                                                               unsigned int inuse;                       /*用于slub分配器,对象的数目*/       };       union{            struct{                      unsigned long private;              /*由映射私有,不透明数据,内存管理会忽略该数据                                                            1)PagePrivate,通常用buffer_heads;                                                            2)PageSwapCache,则用于swp_entry_t;                                                            3)PG_buddy,则用于表示伙伴系统中的阶。*/                      struct address_space *mapping;      /*如果最低位为0,则指向inode address_space,或为NULL。                                                            如果页映射为匿名内存,最低位置位,即mapping=1,该指针指向                                                            anon_vma对象                                                            mapping指定了页帧所在的地址空间                                                            mapping不仅能够保存一个指针,还能包含一些额外的信息,用于判断                                                             页是否属于未关联到地址空间的某个匿名内存区*/                                                           注:对该指针的使用时可能的,因为address_space实例总是对齐到                                                            sizeof(long)。如果该指针指向address_space实例,则可以直接使                                                            用,如果使用了技巧将最低位设置为1,内核可以使用以下操作来恢复                                                             指针。                                                            anon_vma=(struct anon_vma*)(mapping-PAGE_MAPPING_ANON)                  };            ...            struct kmem_cache *slab;                      /*用于slub分配器,指向slab的指针*/            struct page *first_page;                      /*用于符合页的尾页,指向首页*/            };       union{            pgoff_t index;                                /*在映射内的偏移量。index是页帧在映射内部的偏移量*/            void *freelist;                                           };       struct list_head lru;                              /*换出页列表,lru是一个表头,用于在各种链表上维护该页,以便将页                                                             按照不同类别分组,最重要的类别是活动不活动页。*/#if defined(WANT_PAGE_VIRTUAL)       void *virtual;                                     /*内核虚拟地址*/用于高端内存区域中的页,既无法直接映射到内核内存                                                           中的页。virtual用于存储该页的虚拟地址。#endif                                                    /*WANT_PAGE_VIRTUAL*/                                      };

PG_locked常数定义了标志中用于指定页锁定与否的比特位置

 1)PageLocked查询比特位是否置位。

 2)SetPageLocked设置PG_locked位,不考虑先前的状态。

 3)TestSetPageLocked设置比特位,而且返回原值。

 4)ClearPageLocked清楚比特位,不考虑先前的状态。

 5)TestClearPageLocked清除比特位,返回原值。

PG_locked指定了页是否锁定,如果该比特位置位,内核的其他部分不允许访问该页,这防止了内存管理出现竞态条件。

PG_error:若该页的I/O操作期间发生错误。

PG_referenced和PG_active控制了系统使用该页的活跃程度

PG_uptodate表示页的数据已经从块设备读取,期间没有出错。

PG_dirty:脏位,即与磁盘上的数据相比,页的内容已经被改变。

PG_lru实现页面回收和切换,内核使用两个最近最少使用(least recently used,lru)链表来区别活动和不活动页,如果页在其中一个链表中,则设置该比特位。

PG_active:如果页在活动链表中,则设置该比特位。

PG_highmem:表示页在高端内存中,无法持久映射到内存中。

PG_private:如果page结构的private成员非空,设置该位。用于I/O的页,可使用该字段将页细分为多个缓冲区,但内核的其他部分也有各种不同的方法,将私有数据附加到页上。

PG_writeback:如果页的内容处于向块设备回写的过程中。

PG_slab:页时slab分配器的一部分。

PG_swapcache:页处于交换缓存。此时,private包含一个类型为swap_entry_t的项。

PG_reclaim:可用的内存数量变少时,内核试图周期性的回首页,即剔除不活动,未用的页。内核决定回收某个特定的页之后,设置该位。

PG_buddy:如果页空闲且包含在伙伴系统的列表中,设置该位,伙伴系统是页分配机制的核心。

PG_compound表示该页属于一个更大的复合页,符合页有多个毗邻的普通页组成,

PageXXX(page)会检查页是否设置了PG_XXX位,

SetPageXXX在某个比特位没有设置的情况下,设置该比特位,并返回原值。

ClearPageXXX无条件的清除某个特定的比特位。

TestClearPageXXX清除某个设置的比特位,并返回原值。

3.3页表

1)页表用于建立用户进程的虚拟地址空间和系统物理内存(内存,页帧)之间的关联。

2)页表用于向每个进程提供一致的虚拟地址空间。应用程序看到的地址空间是一个连续的内存区,该表也将虚拟内存页映射到物理内存,因而支持共享内存的实现,还可以在不额外增加物理内存的情况下,将页换出到块设备来增加有效的可用内存空间。

3)页表管理分为两个部分,第一部分依赖于体系结构,第二部分是体系结构无关的。

3.3.1数据结构

1)void *数据类型用于定义可能指向内存中任何字节位置的指针。

2)内存源代码假定void *和unsigned long类型所需的比特位相同,他们之间可以进行强制类型转换

  sizeof(void *) = = sizeof(unsigned long)

1.内存地址的分解

BITS_PER_LONG定义用于unsigned long变量的比特位数目,因而也适用于指向虚拟地址空间的通用指针。

img

注:1)PAGE_SHIFT:指针末端的几个比特位,用于指定所选页帧内部的位置,比特位的具体数目由PAGE_SHIFT指定。

   2)PMD_SHIFT:指定了页内偏移量和最后一级页表项所需比特位的总数,该值减去PAGE_SHIFT,可得最后一级页表项索引所需比特位的数目。该值表明了一个中间层页表项管理的部分地址空间的大小,即2^(PMD_SHIFT)字节。

   3)PUD_SHIFT由PMD_SHIFT加上中间层页表索引所需的比特位长度,而PGDIR_SHIFT则由PUD_SHIFT加上上层页表索引所需的比特位长度

   4)对全局页目录中的一项所能寻址的部分地址空间长度计算以2为底的对数,即为PGDIR_SHIFT.

   5)宏定义:PTRS_PER_PGD指定了全局页目录中项的数目

             PTRS_PER_PMD对应于中间页目录

             PTRS_PER_PUD对应于上层页目录中项的数目

             PTRS_PER_PTE则是页表中项的数目

PTRS_PER_XXX指定了给定目录项能够代表多少指针。

#define PAGE_SIZE (1UL

2.页表的格式

1)pgd_t用于全局页目录项

2)pud_t用于上层页目录项

3)pmd_t用于中间页目录项

4)pte_t用于直接页表项

注:pmd_offset需要全局页目录项(src _pgd)和一个内存地址作为参数。它从某个中间页目录项返回一项。

   src_pgd = pmd_offset(src_pgd, address)

   内核使用32或者64位类型来表示页表项,这意味着并非表项的所有比特位都存储了有用的数据,即下一级表的基地址,多余的比特位用于保存额外的信息。

3.特定于PTE的信息

1)最后一级页表中的项不仅包含了指向页的内存位置的指针,还在上述的多余比特位包含了与页有关的附加信息。

  _PAGE_PRESENT指定了虚拟内存页是否存在于内存中。

 _PAGE_ACCESSED:CPU每次访问页时,会定期检查该比特位。以确认页使用的活跃程度。在读或者写之后会设置该比特位。

 _PAGE_DIRTY:表示该页是否是脏的,即页的内容是否已经修改过。

PAGE_FILE:数值与PAGE_DIRTY相同,但用于不同的上下文中,即页不在内存的时候。显然,不存在的页不可能是脏的,因此可以重新解释该比特位,如果没有设置,则该项指向一个换出页的位置。

 _PAGE_USER,则允许用户空间代码访问该页,否则只有内核才可以访问。

PAGE_READ,PAGE_WRITE,_PAGE_EXECUTE:指定了普通的用户进程是否允许读取,写入,执行该页中的机器代码。

 _PAGE_BIT_NX:用于将页标记为不可执行的。

 pte_present检查页表指向的页是否存在于内存中

 pte_dirty检查与页表项相关的页是否是脏的,及其内容在上次内核检查之后是否已经被修改过。

 pte_write检查内核是否写入到页

 pte_file用于非线性映射,通过操作页表提供了文件内容的一种不同视图。在pte_present返回false时,才能调用pte_file

img

3.3.2.页表项的创建和操作

img

3.4初始化内存管理

3.4.1建立数据结构

1)先决条件

  内存在mm/page_alloc.c中定义了一个pg_data_t实例(称作contig_page_data)管理所有的系统内存。

      #define NONE_DATA(nid) (&contig_page_data)

注:尽管该宏有一个形式参数用于选择NUMA节点,但在UMA系统中只有一个伪节点,因此总是返回到同样的数据。

2)系统启动

img

注:

img

3)节点和内存域初始化

  build_all_zonelists建立管理节点及其内存域所需的数据结构

     mm/page_alloc.cstatic int __build_all_zonelists(void *dummy)     /*由于UMA系统只有一个节点,build_zonelists只调用了一次,就对内存建立了内                                                    存域列表                                                    NUMA系统调用该函数的次数等同于节点的数目,每次调用对一个不同节点生成内                                                     存域数据*/{   int nid;   for_each_online_node(nid){                      /*遍历了系统中所有的活动节点*/            pg_data_t *pgdat = NODE_DATA(nid);     /*返回contig_page_data地址*/            build_zonelists(pgdat);                /*需要一个指向pgdat_t实例的指针作为参数,其中包含了节点内存配置的所有现                                                     存信息,而新建的数据结构也会放置在其中*/...           }           return 0;}

内存想要分配高端内存,先在当前节点的高端内存中找到一个大小适当的空闲段,若失败,则查看该节点的普通内存域,如果还失败,

则试图在该节点的DMA内存域执行分配。如果在3个本地内存域都无法找到空闲内存,则查看其他节点。

高端内存是最廉价的,因为内核没有任何部分依赖于该内存域分配的内存。

许多内核数据必须保存在普通内存域,而不能放置在高端内存域,所以只要高端内存没用尽,都不会从普通内存域分配内存。

最昂贵的是DMA内存域,因为它用于外设和系统之间的数据传输。

     typedef struct pglist_data{...        struct zonelist node_zonelists[MAX_ZONELISTS];             /*对每种可能的内存域类型,都配置了一个独立的数组项,数                                                                     组项包含了类型为zonelist的一个备用列表。                                                                    */...}pg_data_t;                                                        /*建立备用层次结构的任务委托给build_zonelists,该函数                                                                     为每个NUMA节点都创建了相应的数据结构,他需要指向相关                                                                     的pg_data_t实例的指针作为参数。*/#define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES *MAX_NR_ZONES)        /*该备用列表必须包含所有节点的所有域,因此由                                                                    MAX_NUMNODES *MAX_NZ_ZONES项组成,外加一个用于标记列                                                                    别结束的空指针*/struct zonelist{                                                   /*一个大的外部循环首先迭代所有的节点内存域,每个循环在                                                                    zonelist数组中找到第i个zonelist,对第i个内存域计算备                                                                     用列表*/...       struct zone *zones[MAX_ZONES_PER_ZONELIST + 1];//NULL分离}
      mm/page_alloc.cstatic void __init build_zonelists(pg_data_t *pgdat){    int node, local_node;    enum zone_type i, j;        local_node = pgdat->node_id;    for (i = 0; i node_zonelists + i;        /*node_zonelists的数组元素通过指针操作寻址,实际工作则委托给build_                                                       zonelist_node.在调用时,它首先生成本地节点内分配内存时的备用次序。*/         j = (pgdat,zonelist,0,i);...}
       mm/page_alloc.cstatic int __init build_zonelists_node(pg_data_t *pgdat,struct zonelist *zonelist,int nr_zones,                                       enum zone_type zone_type)                                                      /*1)nr_zone表示从备用列表中的哪个位置开始填充新项。                                                        2)备用列表的各项是借助于zone_type参数排序的,该参数指定了最先选择                                                           哪个内存域,该参数的初始值是外层循环的控制变量i,其值可能是                                                           ZONE_HIGHMEM ,ZONE_NORMAL,ZONE_DMA或者ZONE_DMA32.                                                        3)内核在build_zonelists中按分配代价从昂贵到低廉的次序,迭代了节                                                            点中所有的内存域。                                                        4)在build_zonelists_node中,则按照分配代价从低廉到昂贵的次序,迭                                                           代了分配代价不低于当前内存域的内存域。*/{       struct zone*zone;       do{              zone = pgdat->node_zones + zone_type;              if (populated_zone(zone)){             /*在build_zonelists_node的每一步中,都对所选的内存域调用populated_z                                                       one,确认zone->present_pages大于0,即确认内存域中确实有页存在。将指                                                       向zone实例的指针添加到zonelist->zones中的当前位置,后备列表的当前                                                        位置保存在nr_zone*/                  zonelist->zones[nr_zones++] = zone;              }              zone_type--;                           /*在每一步结束时,都将内存域类型减1,即设置为一个更昂贵的内存域类型*/         }while(zone_type >= 0);          return nr_zones;}
        在build_zonelists_node时,会执行以下赋值        zonelist->zones[0] = ZONE_HIGHMEM;        zonelist->zones[1] = ZONE_NORMAL;        zonelist->zones[2] = ZONE_DMA;

img

第一步之后,列表中的分配目标是高端内存,接下来是第二个节点的普通和DMA内存域。

     mm/page_alloc.cstatic void __init build_zonelist(pg_data_t *pgdat){...      for (node = local_node + 1;node zones[j] = NULL;  }}

注:1)第一个循环依次迭代大于当前节点编号的所有节点。

   2)有四个节点编号副本为0,1,2,3.此时只剩下节点3

   3)新的项通过build_zonelists_node被加到备用列表,此时j的作用就体现出来了。在本地节点的备用目标找到之后,该变量的值是3,该值用作新项的起始位置。

   4)如果节点3也由内存域组成,备用列表在第二个循环之后情况如图3-9的第二步所示。

   5)如果这些节点也有3个内存域,则循环完之后备用列表的情况如图3-9下半部分所示。

   6)列表的最后一项赋值为空指针,显示标记列表结束。

   7) 对总数N个节点中节点m来说,内核生成备用列表时,选择备用节点的顺序总是:m,m+1,m+2,……,N-1,0,1,…….m-1

img

3.4.2特定于体系结构的设置

1)内核在内存中的布局

1)配置选项PHYSICAL_START用于确定内核在内存中的位置,会受到配置选项PHYSICAL_ALIGN设置的物理对齐方式的影响。

2)内核可以连编为可重定位二进制程序,在这种情况下完全忽略编译时给定的物理起始地址。

img

注:1)前4KB是第一个页帧,通常留给BIOS使用。

   2)使用0x100000作为起始地址,这对应于第二兆字节的开始处。

   3)内核占据的内存分为几个段,其边界保存在变量中。

      1)text和etext是代码段的起始和结束地址,包含了编译后的内核代码。

      2)数据段位于etext和edata之间,保存了大部分内核变量。

      3)初始化数据在内核启动过程结束后不再需要,保存在最后一段,从edata到end.

      4)在内核初始化完成后,其中的大部分数据都可以从内存删除,给应用程序留出更多空间。这一段内存区划分为更小的子区间,以控制哪些可以删除,哪些不能删除。

      5)只有起始地址_text总是相同的。

      6)每次编译内核时,都生成一个文件System.map并保存在源代码目录下,除了所有其他(全局)变量,内核定义的函数和例程的地址,该文件还包含以上常量。

    $cat System.map    查看_text,_etext,_edata$cat /proc/iomem   查看物理内存划分的各个段的信息

2.初始化步骤

img

注:1)该图只包括与内存管理相关的函数调用。

   2)start_kernel内部调用setup_arch

   3)调用machine_specific_memory_setup,创建一个列表,包括系统占据的内存区和空闲内存区

   4)内核提供了一个特定于机器的函数,定义在include/asm-x86/mach-type/setup.c.type中,type可以是default,voyager或者visws。

   5)在系统启动时,找到的内存区由内核函数print_memory_map显示。

   6)内核接下来用parse_cmdline_early分析命令行,主要关注类似mem=XXX[KkmM], high mem=XXX[KkmM],

memmap=XXX[KkmM], @ XXX[KkmM]之类的参数。

   7)highmem允许修改检测到的高端内存域长度值。

   8)下一个主要步骤在setup_memory中进行。一个用于不连续内存系统(在arch/x86/mm/disconting_32.c),一个是连续内存系统( 在arch/x86/kernel/setup_32.c )

      1)确定(每个节点)可用的物理内存页的数目

      2)初始化bootmem分配器

      3)接下来分配各种内存区

   9)paging_init初始化内核页表并启用内核分页。

       (1)pagetable_init该函数确保了直接映射到内核地址空间的物理内存被初始化。

  11)低端内存中的所有页帧都直接映射到PAGE_OFFSET之上的虚拟内存区。这使得内核无需处理页表,即可寻址相当一部分可用内存。

  12)调用zone_sizes_init会初始化系统中所有节点的pgdat_t实例。

       (1)使用add_active_range,对可用的物理内存建立一个相对简单的列表。

       (2)体系结构无关的函数free_area_init_nodes接下来使用该信息建立完备的内核数据结构。

AMD64计算机内存有关的初始化次序

img

注:1)基本的内容设置并不需要任何特定于计算机类型的处理,总是可以调用setup_memory_region完成。

   2)调用add_active创建可用内存的一个简单列表

   3)内核接下来调用init_memory_mapping将可用的物理内存直接映射到虚拟地址空间中从PAGE_OFFSET开始的内核部分。

   4)contig_initmem_init负责激活bootmem分配器。

   5)最后一个函数paging_init,它并不初始化分页机制,只是处理一些稀疏内存系统的设置例程。

        该函数调用free_area_init_nodes,负责初始化内核管理物理页帧的数据结构。这是一个体系结构无关的函数,依赖于前面add_active_range提供的信息。

3.分页机制的初始化

1)paging_init负责建立只能用于内核的页表,用户空间无法访问。

2)内核总是将总4GB可用虚拟空间按3:1的比例划分,低端3GB用于用户态应用程序,而高端的1GB则用于内核。

3)在用户应用程序的执行切换到核心态时,内核必须装载在一个可靠的环境中,因此有必要将地址空间的一部分分配给内核专用。

4)物理内存页则映射到内核地址空间的起始处,以便内核直接使用。而无需复杂的页表操作。

  虽然用于用户层过程的虚拟地址部分随进程切换而改变,但是内核部分总是相同的。

img

  (1)地址空间的划分

      按3:1的比例划分地址空间,内核地址空间自身又分为各个段。

img

注:该图给出了用来管理虚拟地址空间的第4个G字节的页表项的结构,它表明了虚拟地址空间的各个区域的用途,这与物理内存的分配无关。

     简单的说:kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续,malloc不保证任何东西(这点是自己猜测的,不一定正确)kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大。内存只有在要被DMA访问的时候才需要物理上连续vmalloc比kmalloc要慢 详细的解释:      对于提供了MMU(存储管理器,辅助操作系统进行内存管理,提供虚实地址转换等硬件支持)的处理器而言,Linux提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。      进程的4GB内存空间被人为的分为两个部分--用户空间与内核空间。用户空间地址分布从0到3GB(PAGE_OFFSET,在0x86中它等于0xC0000000),3GB到4GB为内核空间。      内核空间中,从3G到vmalloc_start这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map等等),比如我们使用 的 VMware虚拟系统内存是160M,那么3G~3G+160M这片内存就应该映射物理内存。在物理内存映射区之后,就是vmalloc区域。对于 160M的系统而言,vmalloc_start位置应在3G+160M附近(在物理内存映射区与vmalloc_start期间还存在一个8M的gap 来防止跃界),vmalloc_end的位置接近4G(最后位置系统会保留一片128k大小的区域用于专用页面映射)      kmalloc和get_free_page申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址:   #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)   extern inline unsigned long virt_to_phys(volatile void * address)   {        return __pa(address);   }上面转换过程是将虚拟地址减去3G(PAGE_OFFSET=0XC000000)。与之对应的函数为phys_to_virt(),将内核物理地址转化为虚拟地址:   #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))   extern inline void * phys_to_virt(unsigned long address)   {        return __va(address);   }virt_to_phys()和phys_to_virt()都定义在include/asm-i386/io.h中。而vmalloc申请的内存则位于vmalloc_start~vmalloc_end之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续。我们用下面的程序来演示kmalloc、get_free_page和vmalloc的区别:#include #include #include MODULE_LICENSE("GPL");unsigned char *pagemem;unsigned char *kmallocmem;unsigned char *vmallocmem;int __init mem_module_init(void){//最好每次内存申请都检查申请是否成功//下面这段仅仅作为演示的代码没有检查pagemem = (unsigned char*)get_free_page(0);printk("pagemem addr=%x", pagemem);kmallocmem = (unsigned char*)kmalloc(100, 0);printk("kmallocmem addr=%x", kmallocmem);vmallocmem = (unsigned char*)vmalloc(1000000);printk("vmallocmem addr=%x", vmallocmem);return 0;}void __exit mem_module_exit(void){free_page(pagemem);kfree(kmallocmem);vfree(vmallocmem);}module_init(mem_module_init);module_exit(mem_module_exit);我们的系统上有160MB的内存空间,运行一次上述程序,发现pagemem的地址在0xc7997000(约3G+121M)、kmallocmem 地址在0xc9bc1380(约3G+155M)、vmallocmem的地址在0xcabeb000(约3G+171M)处,符合前文所述的内存布局。

注:1)地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中,由于内核地址空间从偏移量0xc0000000开始,及经常提到的3GB,每个虚拟地址x都对应于物理地址x-0xc0000000,因此这是一个简单的线性平移。

   2)直接映射区域从0xc0000000到high_memory地址。

   3)若物理内存超过896MB,则内核无法直接映射全部的物理内存,该值可能比此前提到的最大限制1GB还小,因为内核必须保留地址空间的最后128MB用于其他目的。

   4)将这128MB+直接映射的896MB内存,则得到内核虚拟地址空间的总数为1024MB=1GB。

   5)内核使用两个经常使用的缩写normal和highmem,来区分是否可以直接映射的页帧。

   6)内核移植的每个体系结构必须提供两个宏,用于一致映射的内核虚拟内存部分,进行物理和虚拟地址之间的转换。

     __pa(vaddr)返回与虚拟地址vaddr相关的物理地址

     __va(vaddr)则计算出对于物理地址paddr的虚拟地址

– – – – – – – – 两个函数都用void指针和unsigned long操作。只能用于其中的一直映射部分,不适用于处理虚拟地址空间的任意地址。

      页帧映射到从PAGE_OFFSET开始的虚拟地址空间。include/asm-x86/page_32.h#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)               /* __pa(vaddr)返回与虚拟地址vaddr相关的物理地址*/#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))     /*__va(vaddr)则计算出对于物理地址paddr的虚拟地址*/

内核最后的128MB

(1)虚拟内存中连续,但物理内存中不连续的内存区,可以在vmalloc区域分配。该机制通常用于用户过程,内核自身会试图尽力避免非连续的物理地址。

(2) 持久映射用于将高端内存域中的非持久页映射到内核中。

(3) 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择。

__VMALLOC_RESREVE设置了vmalloc区域的长度

MAXMEM则表示内核可以直接寻址的物理内存的最大可能数量。

       直接映射的边界由high_memory指定arch/x86/kernel/setup_32.cstatic unsigned long __init setup_memory(void){...#ifdef CONFIG_HIGHMEM       high_memory = (void *) __va(highstart_pfn * PAGE_SIZE-1)+1;#else       high_memory = (void *) __va(max_low_pfn *PAGE_SIZE-1)+1;     /*max_low_pfn指定了物理内存数量小于896MB的系统上的                                                                     内存页的数目。该值的上界受限于896MB可容纳的最大页数                                                                    (具体的计算在find_max_low_pfn给出)。如果启用高端                                                                     内存的支持,则high-memory表示两个内存区之间的边界,                                                                     总是896MB*/#endif...}

若VMALLOC_OFFSET取最小值,那么在直接映射的所有内存页和用于非连续分配的区域之间,会出现一个缺口。

       include/asm-x86/pgtable_32.h#define VMALLOC_OFFSET (8*1024*1024)

这个缺口可用作针对任何内核故障的保护措施,如果访问越界地址,则访问失败并产生一个异常。

       VMALLOC_START和VMALLOC_END定义了vmalloc区域的开始和结束,该区域用于物理上不连续的内核映射。#define VMALLOC_START (((unsigned long) high_memory + 2*VMALLOC_OFFSET-1) & ~(VMALLOC_OFFSET-1))#ifdef CONFIG_HIGHMEM#define VMALLOC_END (PKMAP_BASE-2*PAGE_SIZE)#else#define VMALLOC_END (FIXADDR_START-2*PAGE_SIZE)#endif

注:1)vmalloc区域的起始地址。取决于在直接映射物理内存时,使用了多少虚拟地址空间内存。

   2)两个区域之间至少为VMALLOC_OFFSET的一个缺口,而且vmalloc区域从可被VMALLOC_OFFSET整除的地方开始。

   3)vmalloc区域在何处结束取决于是否启用了高端内存支持。如果没有启用,那么就不需要持久映射区域,因为整个物理内存都可以直接映射。

  4)根据不同的配置,该区域结束于持久内核映射或者固定映射区域的起始处。

img

注:PKMAP_BASE定义了其起始地址

   LAST_PKMAP定义了容纳该映射所需的页数。

  最后一个内存段由固定映射占据,这些地址指向物理内存中的随机位置,固定映射区域延伸到虚拟地址空间的顶端。

img

imgimg

***备选划分方式

img

***划分虚拟地址空间

img

注:1)pagetable_init首先初始化系统的页表,以swapper_pg_dir为基础(该变量此前用于保存临时数据) 。

     ***对超大页的支持,这些特别标记的页,其长度为4MB,而不是普通的4KB。该选项用于不会换出的内核页。增加页的大小,意味着需要的页表项变少,这对地址转换后备缓冲器(TLB)的影响是正面的,可以减少其中来自内核的缓存项 。

     ***如有可能,内核页会设置另一个属性(_PAGE_GLOBAL),这也是PAGE_KERNEL和PAGE_KERNEL_EXEC变量中__PAGE_GLOBAL比特位已经置位的原因。这些变量指定内核身份分配页帧时的标志集,因此这些设置会自动的应用到内核页。

     在上下文切换期间,设置了_PAGE_GLOBAL位的项,对应的TLB缓存项不从TLB刷出。内核总是出现在虚拟地址空间中同样的位置。

     ***借助于kernel_physical_mapping_init,将物理内存页(或前896MB)映射到虚拟地址空间中从PAGE_OFFSET开始的位置。内核接下来扫描各个页目录项的所有相关项,将指针设置为正确的值。

     ***接下来建立固定映射项和持久内核映射对应的内存区。

     在用pagetable_init完成页表中初始化之后,则将CR3寄存器设置为指向全局页目录(swapper_pg_dir)的指针,此时必须激活新的页。

     ***由于TLB缓存项仍然包括启动时分配的一些内存地址数据,此时也必须刷出。flush_all_tlb可完成所需的工作。与上下文切换期间相反,设置了PAGE_GLOBAL位的页也要刷出。

     kmap_init初始化全局变量kmap_pte.

     ***在从高端内存域将页映射到内核地址空间时,会使用该变量存入相应内存区的页表项。

     ***用于高端内存内核映射的第一个固定映射内存区的地址保存在全局变量kmem_vstart中。

***冷热缓存的初始化

  per-CPU(或冷热)缓存,我们来处理相关数据结构的初始化,以及用于控制缓存填充行为的“水印”的计算。

  zone_pcp_init负责初始化该缓存。

  该函数由free_area_init_nodes调用。

       mm/page_alloc.cstatic __devinit void zone_pcp_init(struct zone *zone)      /*负责初始化该缓存*/{       int cpu;       unsigned long batch = zone_batchsize(zone);          /*算出批量大小(用于计算最小和最大填充水平的基础)后,代码将遍                                                              历所有的CPU,同时调用setup_pageset*/       for (cpu = 0;cpupresent_pages)                printk(KERN_DEBUG "%s zone:%lu pages,LIFO batch:%lu\n",zone->name,zone->present_pages,batch)
        mm/page_alloc.cstatic int __devinit zone_batchsize(struct zone *zone){       int batch;       batch = zone->present_pages / 1024;       if (batch * PAGE_SIZE > 512 * 1024)           batch = (512 * 1024) / PAGE_SIZE;       batch /= 4;       if (batch 

注:内存域中的内存数量超出512MB时,批量大小并不增长。

       mm/page_alloc.cinline void setup_pageset(struct_cpu_pageset *p,unsigned long batch){       struct per_cpu_pages *pcp;       memset(p,0,sizeof(*p));       pcp = &p->pcp[0];                /*热页。对热页来说,下限为0,上限为6*batch,缓存中页的平均数量大约是4*batch                                          batch*4相当于内存域中页数的千分之一(这也是zone_batchsize试图将批量大小优化到总                                           页数0.25%的原因。)*/       pcp->count = 0;       pcp->high = 6*batch;       pcp->batch = max(1UL,1*batch);   /*无符号长整形1*/       INIT_LIST_HEAD(&pcp->list);       pcp = &p->pcp[1];                /*冷页。冷页列表的水印稍低一点。因为冷页并不放置到缓存中,只用于一些不太关注性能的操                                          作,其上限值是batch值的两倍。*/       pcp->sount = 0;       pcp->high = 2*batch;       pcp->batch = max(1UL,batch/2);    /*决定了再重新填充列表时,有多少页会立即使用。一般会向列表中添加连续的多页,而不是                                           单页。*/       INIT_LIST_HEAD(&pcp->list);}                                         /*在zone_pcp_init结束后,会输出各个内存域的页数以及计算的批量的大小,从启动日志可                                            以看到。*/

***注册活动内存区

各个体系结构只需要注册所有活动内存的一个简单表,通用代码则据此生成主数据结构。

任何一个体系结构,如果打算利用内核提供的一般性框架,则需要设置配置选项ARCH_POPULATES_NODE_MAP,在注册所有活动内存区之后,其余的工作由通用的内核代码完成。

活动内存区就是不包含空洞的内存区,必须使用add_active_range在全局变量early_node_map中注册内存区。

       mm/page_alloc.cstatic struct node_active_region __meminitdata early_node_map[MAX_ACTIVE_REGIONS]; /*不同内存区的最大数目由                                                                                                                       MAX_ACTIVE_REGION给出。该值可以由                                                                                     特定于体系结构的代码使用                                                                                      CONFIG_MAX_ACTIVE_REGIONS设置。/static int __meminitate nr_nodemap_entries;                                        /*当前注册的内存区数目记载在                                                                                      nr_nodemap_entries中。*/
        struct node_active_region{       unsigned long start_pfn;     /*start_pfn标记了连续内存区中的第一个页帧*/       unsigned long end_pfn;       /*end_pfn标记了连续内存区中的最后一个页帧*/       int nid;                     /*nid是该内存区所属节点的NUMA ID。UMA设置为0*/}
         活动内存区是使用add_active_range注册的。mm/page_alloc.cvoid __init add_active_range(unsigned int nid,unsigned long start_pfn,unsigned long end_pfn)       注:在注册两个毗邻的内存区时,add_active_region会确保将他们合并为一个。此外该函数不提供其他的额外的功能特性。

***在IA-32上注册内存区

除了调用add_active_range之外,zone_size_init函数以页帧为单位,存储了不同内存区的边界。

  1. arch/x86/kernel/setup_32.c

  2. void __init zone_sizes_init(void)

  3. {

  4. unsigned long max_zone_pfns[MAX_NR_ZONES];

  5. memset (max_zone_pfns, 0,sizeof(max_zone_pfns));

  6. max_zone_pfns[ZONE_DMA] = virt_to_phys((char *)MAX_DMA_ADDRESS >> PAGE_SHIFT;  /*DMA操作的最高内存地址。该常数声

  7. 明为PAGE_OFFSET+0X1000000

  8. 物理内存页映射到从PAGE_OFFSET开始的虚拟地址空间                                                                         ,而物理内存的前16MB适合于DMA操作,16进制表示就                                                                          是前0x1000000字节。用virt_to_phys转换,可以获得                                                                         物理内存地址,而右移PAGE_SHIFT位则相当于除以页的                                                                         大小。计算到最后得到适用于DMA的页数。*/

  9. max_zone_pfns[ZONE_NORMAL] = max_low_pfn;                        /*全局变量,指定了低端内存中最高的页号。*/

  10. #ifdef CONFIG_HIGHMEM

  11. max_zone_pfns[ZONE_HIGHMEM] = highend_pfn;/全局变量,指定了低端内存中最高的页号。/

  12. add_active_range(0, 0,highend_pfn);

  13. #else

  14. add_active_range(0, 0,max_low_pfn);

  15. #endif

  16. free_area_init_nodes(max_zone_pfns);                               /*合并early_mem_map和max_zone_pfns中的信息。                                                                           分别选择各个内存域中的活动内存页区,并构建体                                                                             系结构无关的数据结构*/

  17. }

    1. max_zone_pfns值得设置由paging_init处理

    2. arch/x86/mm/init_64.c

    3. void __init paging_init(void)

    4. {

    5. unsigned long max_zone_pfns[MAX_NR_ZONES];

    6. memset(max_zone_pfns,0,sizeof(max_zone_pfns));

    7. max_zone_pfns[ZONE_DMA] = MAX_DMA_PFN;

    8. max_zone_pfns[ZONE_DMA32] = MAX_DMA32_PFN;

    9. max_zone_pfns[ZONE_NORMAL] = end_pfn;

    10. ...

    11. free_area_init_nodes(max_zone_pfns);

    12. }

    13. 
      
                     16位和32位DMA内存域的页帧边界保存在处理符号中,分别对应于16MB和4GB转换为页帧的值:include/asm-x86/dms_64.h/* 16MB ISA DMA内存域 */##define MAX_DMA_PFN((16*1024*1024) >> PAGE_SHIFT)/* 4GB PCI/AGP硬件总线主控器内存域 */#define MAX_DMA_PFN ((4UL*1024*1024*1024) >> PAGE_SHIFT)end_pfn检测到的最大页帧号。由于AMD64并不需要高端内存域,max_zone_pfns中对应的项是NULL

***在AMD64上注册内存区

在AMD64上注册内存区的工作分为两个函数,活动内存区的注册如下:

      arch/x86/kernel/e820_64.ce820_register_active_regions(int nid,unsigned long start_pfn,unsigned long end_pfn){     unsigned long ei_startpfn;     unsigned long ei_endpfn;     int i;     for(i = 0;i

以上就是良许教程网为各位朋友分享的Linux系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你!

137e00002230ad9f26e78-265x300

本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部