Linux学习总结—内存结构、启动和进程空间

来源:互联网 发布:高仿能在淘宝买吗 编辑:程序博客网 时间:2024/06/06 01:25
3Linux的内存结构和管理
物理内存区域
Linux 内核按照 3:1 的比率来划分虚拟内存:3 GB 的虚拟内存用于用户空间,1 GB 的内存用于内核空间。内核代码及其数据结构都必须位于这 1 GB 的地址空间中,但是对于此地址空间而言,更大的消费者是物理地址的虚拟映射。
为了迎合大量用户的需要,支持更多内存、提高性能,建立一种独立于架构的内存描述方法,Linux 内存模型将内存划分成分配给每个 CPU 的空间。每个空间都称为一个节点;每个节点都被划分成一些区域。区域(表示内存中的范围)可以进一步划分为以下类型:
  • ZONE_DMA0-16 MB):包含 ISA/PCI 设备需要的低端物理内存区域中的内存范围。
  • ZONE_NORMAL16-896 MB):由内核直接映射到高端范围的物理内存的内存范围。所有的内核操作都只能使用这个内存区域来进行,因此这是对性能至关重要的区域。
  • ZONE_HIGHMEM896 MB 以及更高的内存):系统中内核不能映像到的其他可用内存。
节点的概念在内核中是使用struct pglist_data结构来实现的。区域是使用struct zone_struct结构来描述的。物理页框是使用struct Page结构来表示的,所有这些Struct都保存在全局结构数组struct mem_map中,这个数组存储在NORMAL_ZONE的开头。节点、区域和页框之间的基本关系如下图所示。
7. 节点、区域和页框之间的关系
 
对于4 GB 的内存可以通过使用kmap()ZONE_HIGHMEM映射到ZONE_NORMAL来进行访问。
物理内存区域的管理是通过一个区域分配器(zone allocator实现的。它负责将内存划分为很多区域;它可以将每个区域作为一个分配单元使用。每个特定的分配请求都利用了一组区域,内核可以从这些位置按照从高到低的顺序来进行分配。
例如:
  • 对于某个用户页面的请求可以首先从普通区域中来满足(ZONE_NORMAL);
  • 如果失败,就从ZONE_HIGHMEM开始尝试;
  • 如果这也失败了,就从ZONE_DMA开始尝试。
这种分配的区域列表依次包括ZONE_NORMALZONE_HIGHMEMZONE_DMA区域。另一方面,对于 DMA 页的请求可能只能从 DMA 区域中得到满足,因此这种请求的区域列表就只包含 DMA 区域。
 
Linux物理内存的管理
在物理页面管理上实现了基于区的伙伴系统(zone based buddy system)。对不同区的内存使用单独的伙伴系统(buddy system)管理,而且独立地监控空闲页。相应接口alloc_pages(gfp_mask, order)_ _get_free_pages(gfp_mask, order)等。
单单分配页面的分配器肯定是不能满足要求的。内核中大量使用各种数据结构,大小从几个字节到几十上百k不等,都取整到2的幂次个页面那是完全不现实的。Linux提供了CacheSlab分配算法,提供大小为2,4,8,16,...,131056字节的内存对象管理。对象的要素有大小、结构、构造和析构函数。每个Cache有若干Slab组成,Slab2的整数次幂为单位向分区申请页面。每个Slab被划分成若干个size大小的对象,每个对象之间可能需要Cache对齐。为了防止对象频繁的分配释放,Slab并不物理上释放已经分配不使用的对象,当下次再申请对象时就不需要经过初始化直接把对象分配使用。CacheSlab都满时,Slab像分区申请一个Slab大小需要的页面,并进行Slab和对象的初始化划分。
8 Slab的对象结构
 
Cachekmem_cache_t结构表示,主要包括array_cache[]每个CPU的本地对象Cacheslabs_fullslabs_partialslabs_free三个Slab双向链表,gfporder每个Slab所需页面的次幂,colourcolour_off表示Slab Cache对齐参数及一些状态参数。
Slab描述符用slab结构表示,list指向Slab所在链表,colouroff表示对象在Slab中的偏移,s_mem表示第一个对象的地址,inuse表示当前使用的对象数,free表示第一个空闲的对象索引。每个Slab描述符后面放着一个kmem_bufctl_t数组,用来描述Slab中的空闲对象。
Slab的管理结构既可以放在每个Slab页面上也可以集中放在其他位置,这取解决对象大小等因素。Slab结构如下图所示:
9 Slab对象结构
管理区的初始化
管理区的初始化在函数start_kernel()-> setup_arch()->zone_sizes_init()—>…—> free_area_init_node()中进行。该函数在setup_memory()建立引导内存分配器和paging_init()建立内核页表后调用。传递参数有管理区结点标志符nid,初始化的pg_data_t,管理区大小zones_size,第一个管理区的起始物理地址node_start_pfn等。函数原型:
void __meminit free_area_init_node(int nid, struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long node_start_pfn, unsigned long *zholes_size)
alloc_node_mem_map()用于为节点分配mem_map数组,free_area_init_core()用于向每个zone_t填充相关信息,标记所有页保留,标记所有内存队列为空,清空内存位图;并初始化区的mem_map
mem_map的初始化:在NUMA系统中全局mem_map被处理成一个起始于PAGE_OFFSET的虚拟数组,全局mem_map从未被明确的申明国,取而代之被处理成起始于PAGE_OFFSET的虚拟数组。局部映射地址存储在pg_data_t>node_mem_map中,也存在于虚拟mem_map中。对于节点中的每个管理区,虚拟mem_map中表示管理区的地址存储在zone>zone_mem_map中。余下的节点都把mem_map作为真实的数组,因为只有有效的管理区会被节点所使用。
 
4Linux的内存初始化
引导内存分配器
由于硬件配置的多样性,在编译时静态初始化所有的内核存储结构是不现实的。物理页面分配器是如何分配内存完成自身的初始化的呢?Linux是通过引导内存分配器boot memory allocator来完成的,该机制基于大部分分配器的原理,用位图代替空闲链表结构表示存储空间,位图中某位置1表示该页面已被分配,否则表示未被占有。该机制通过记录上一次分配页面帧号及结束时的偏移量实现小于一页的内存分配。该分配器也是基于NUMA上的节点分配的。
初始化引导内存分配器
每种体系结构都提供了setup_arch()函数,用于获取初始化引导内存分配器时所必须的参数信息。setup_arch()中调用setup_memory()初始化内存。setup_memory()中首先找出低端内存的PFN起点和终点和高端内存PFN的起点和终点,然后调用setup_bootmem_allocator()初始化引导内存分配器。
setup_bootmem_allocator()处理流程如下:
1)        调用init_bootmem()->init_bootmem_core()用contig_page_data及低端内存起点和终点初始化对应的bootmem_data_t结构,并插入pgdata_list节点列表中。
2)        调用register_bootmem_low_pages()函数通过检测e820映射图,并在每一个可用页面上调用free_bootmem()函数,将其位设为1。
3)        依次调用reserve_bootmem()分别为bootmem保存实际位图所需的页面预留空间、预留BIOS使用的第一个物理页面0、为EBDA区预留4K区、为AMD 768MPX芯片预留1个页面。
4)        如果配置了CONFIG_SMP则为trampoline跳转预留4K空间;
5)        如果加入了睡眠机制,则调用acpi_reserve_bootmem()为之保留内存;
6)        调用find_smp_config()读取SMP配置信息并为之保留内存;
7)        如果配置了CONFIG_BLK_DEV_INITRD或者CONFIG_KEXEC则为它们保留内存;
内存分配和释放
引导内存的分配接口有:alloc_bootmem_node(pgdat, x)alloc_bootmem_pages_node(pgdat, x)alloc_bootmem_low_pages_node(pgdat, x),最终它们都调用一个核心函数__alloc_bootmem_core (pg_data_t *pgdat, unsigned long size, unsigned long align, unsigned long goal)。该函数主要处理流程:
1)        函数开始处保证所有参数正确;
2)        以goal参数为基础计算开始扫描的起始地址;
3)        检查本次分配是否可以使用上次分配的页面以节省内存;
4)        在位图中标记已分配为1,并将页中内容清0;
    内存释放函数只有一个free_bootmem_node(),它调用free_bootmem_core().该函数比较简单,对于受释放影响的每个完整页面的相应位设为0,如果原来就是0则调用BUG()提示重复释放错误。对于释放函数只有完整的页面才可释放,不能部分释放一个页面。
释放引导内存分配器
系统启动后,引导内存分配器就不在需要,内核提供mem_init()负责释放引导内存分配器并把其余的页面传人到普通的页面分配器中。
mem_init()的流程如下:
1)        调用ppro_with_ram_bug ()检查奔腾Pro版本中是否存在一个bug,该bug阻止高端内存的某些页被使用;
2)        调用free_all_bootmem()释放引导内存分配器,并把低端地址页面转移到伙伴分配器中管理;
3)        遍历所有内存计算保留内存的页面数;
4)        调用set_highmem_pages_init()并逐页初始化高端内存;
5)        计算用于初始化的代码和数据的代码段、数据段和内存大小并打印内存信息;
6)        打印内核虚拟内存布局并进行检测;
7)        如果配置了CONFIG_X86_PAE当CPU不支持,则使系统瘫痪;
8)        测试WP位是否可用;
9)        如果配置了CONFIG_SMP,调用zap_low_mappings()填充swapper_pg_dir的PGD用户空间部分的表项,将这些页面都映射到0,这是因为在后面SMP辅助处理器启动时它需要为进入保护模式进行地址映射;
free_all_bootmem()-> free_all_bootmem_core()执行以下操作:
1)        对于该节点上分配器可以识别的所有未分配的页面:
l          将它们结构页面上的PG_reserved标志清0;
l          将计数器置为1;
l          调用__free_pages()以使伙伴系统分配器能建立free空闲列表;
2)        释放位图使用的所有页面,并将之交给伙伴分配器。
这样,伙伴系统就控制了所有的低端内存页面。
对于高端内存,由set_highmem_pages_init()进行初始化,该函数对highstart_pfn和highend_pfn之间的页面分别调用add_one_highpage_init()该函数将PG_reserved标志清0,初始化计数器为1,调用__free_page()将自己释放到伙伴分配器中。
初始化页表
前面已经分析过在启动时startup_32函数为系统8 MB 的物理内存设置页表。在setup_arch()调用setup_memory()初始化引导内存分配器后,需要完成对其余所有物理地址的映射。这里是通过paging_init()完成的。处理流程为:
1)        调用pagetable_init()初始化对应于ZONE_DMA、ZONE_NORMAL的所有物理内存必须要的页表;它对从FIXADDR_START开始的高端内存调用permanent_kmaps_init()进行初始化;
2)        将初始化后的swapper_pg_dir页表载入CR3寄存器中,以供换页单元使用;
3)        如果配置了CONFIG_X86_PAE且CPU支持,则设置CR4寄存器中的相应位;
4)        调用kmap_init()初始化带有PAGE_KERNEL标志位的每个PTE;
 
5Linux的进程地址空间
Linux管理系统中的进程用一个task_struct数据结构来表示,Linux地址空间如下所示,对用户空间来说,它只能访问0~3G空间的范围。
10 内核地址空间
Linux进程地址空间有mm_struct管理,task_struct中的mm成员表示。mm_struct数据结构包含了已加载可执行映象的信息和指向进程页表的指针,它还包含了一个指向vm_area_struct链表的指针,每个指针代表进程内的一个虚拟内存区域。vm_area_struct表示的内存区域是一个页面对齐的、并且相互之间不会重叠,它可能是一个malloc使用的进程堆或者是一个内存映射文件,也可以是mmap()分配的匿名内存区域。如果该区域是一个文件的映像,则vm_file字段被设置,通过vm_file可以找到该区域代表的地址空间内容。
图11 进程地址空间的数据结构
 
mm_struct结构的初始化和释放
mm_init()初始化一个mm结构,allocate_mm()用于从slab分配器分配一个mm_struct结构。系统中第一个mm_struct通过init_mm()初始化,后继的子mm_struct都通过copy_mm()进行复制得到。第一个mm_struct结构在编译时静态配置。do_munmap()负载删除一个VMA区域,释放相关页面,并修复区域。当进程退出时,必须删除与mm_struct相关的所有VMA,由函数exit_mmap()负责操作。该函数首先刷新CPU高速缓存,依次删除每一个VMA并释放相关页面,然后刷新TLB和删除页表项。 
原创粉丝点击