1 名词解释:
(1)页框:物理内存的描述,必须牢牢记住,页框就是物理内存
(2)页描述符:描述每一个页框的状态信息,所有的也描述符都保存在mem_map[ ]数组中,每个描述符32个字节
(3)节点:系统物理内存被划分为多个节点,每个节点内cpu访问页面的时间是相同的,对应的数据结构:节点描述符
(4)管理区:每个节点又分为多个管理区 对应的数据结构: 管理区描述符
2 页表管理
重点介绍内核页表的管理,主要分为两个阶段:启动阶段映射8M的页表和剩余页表的映射阶段
(1)启动阶段8M页表的映射过程
(2)剩余页表的映射过程
几个比较重要的地址转换:
虚拟地址转换成物理地址: virt_to_phsy(address){ __pa(address) }
虚拟地址转换页描述符的地址: virt_to_page( kaddr ) { return mem_map + __pa(kaddr) >>12 }
3 用户进程的地址空间
从内核看来,整个4G的地址空间是这样的。
进程可用的地址空间是被一个叫mm_struct(进程地址空间描述符)结构体来管理的,同一个进程内的多个线程是共享这个数据结构的。
同时,对于用户进程来说,每一个进程有一个独一无二的mm_struct,但是内核线程确不是必须的。下面是操作mm_struct的一些函数。
当然如果在进程创建的时候指定子进程共享父进程的虚拟地址空间的话,比如:
if ( clone_flags & CLONE_VM )
{
atomic_inc(&old_mm->mm_user)
mm = &oldmm;
goto good_mm;
}
还有一点许哟阿注意的就是系统中的第一个mm_struct是需要静态初始化的,以后的所有的mm_strcut都是通过拷贝生成的。
mmap函数,内存映射函数
该函数的主要功能是在进程地址空间中创建一个线性区。有两种类型的内存映射:共享型和私有型。二者的主要区别可以理解成是否对其他进程可见。共享型每次对线性区的读写都会修改
磁盘文件,一个进程修改共享型的线性区,其他映射这一线性区的所有进程都是可见的。与内存映射相关的数据结构:
(1)与所映射的文件相关的索引节点对象
(2)所映射文件的address_space对象
(3)不同进程对同一文件进行不同映射所使用的文件对象
(4)对文件进行每一不同映射所使用的vm_area_struct
(5)对文件进行映射的线性区所分配的每个页框对应的描述符
从图上能看出一个文件对应一个inode,对应一个address_space,对应多个struct file, 对应多个vm_area_file ,对应多个page(页框),当然也对应多个page(页描述符)、
mm_struct 内存描述符中的两棵树: 当前进程内所有线性区的一个链表和所有线性区的红黑树
mmap和mm_rb都可以访问线性区。事实上,它们都指向了同一个vm_area_struct结构,只是链接的方式不同
mmap指向的线性区链表用来遍历整个进程的地址空间
红黑树mm_rb用来定位一个给定的线性地址落在进程地址空间中的哪一个线性区中
另外,mmap_cache用来缓存最近用过的线性区
address_space中的两棵树:基数和优先级搜索数。
address_space的page_tree指向了组织构成这个文件的所有的页描述符的基树
address_space的i_mmap指向了组织构成这个文件的所有的线性区描述符的基树
要注意一个问题:
(1)共享内存映射的页通常保存在也高速缓存中,私有内存映射的页只要还没有修改,也保存在页高速缓存中。当进程试图修改一个私有映射的页时,内核 就把该页框进行复制,并在进程页表中用复制的页替换原来的页,这就是写时复制的基础。复制后的页框就不会放在页告诉缓存中了,原因是它不再是表示磁盘上那 个文件的有效数据。
(2)线性区的开始和结束地址都是4K对齐的
进程获得新线性区的一些典型情况:
刚刚创建的新进程
使用exec系统调用装载一个新的程序运行
将一个文件(或部分)映射到进程地址空间中
当用户堆栈不够用的时候,扩展堆栈对应的线性区
创建内存映射:
要想创建一个映射,就要调用mmap, mmap()最终会调用do_mmap()
static inline unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag,unsigned long offset)
file:要映射的文件描述符,知道映射哪个文件才行
offset:文件内的偏移量,指定要映射文件的一部分,当然也可以是全部
len: 要映射文件的那一部分的长度
flag:一组标志,显示的指定映射的那部分是MAP_SHARED或MAP_PRIVATE
prot: 一组权限,指定对线性区访问的一种或多种访问权限
addr: 一个可选的线性地址,表示从这个地址之后的某个位置创建线性区
基本的过程是:
(1)先为要映射的文件申请一段线性区,调用内存描述符的get_unmapped_area()
(2)做一些权限和标志位检查
(3)将文件对象的地址struct file地址赋值给线性区描述符vm_area_struct.vm_file
(4)调用mmap方法,这个方法最后调用generic_file_mmap()
其他线性区处理函数
(1)find_vma() : 查找一个线性地址所属的线性区或后继线性区
(2)find_vmm_interrection(): 查找一个与给定区间重叠的线性区
(3)get_unmapped_area() : 查找一个空闲的线性区
(4)insert_vm_struct () : 向进程的内存描述符中插入一个线性区
缺页异常处理程序
(1)背景知识
内核中的函数以直接了当的方式获得动态内存,内核是操作系统中优先级最高的成分,内核信任自己,采用面级内存分配和小内存分配以及非连续线性区得到内存
用户态进程分配内存时,请求被认为是不紧迫的,用户进程不可信任,因此,当用户态进程请求动态内存时,并没有立即获得实际的物理页框,而仅仅获得对一个
新的线性地址区间的使用权这个线性地址区间会成为进程地址空间的一部分,称作线性区(memory areas)。 这样,当用户进程真正向这些线性区写的时候,就会
产生缺页异常,在缺页异常处理程序中获得真正的物理内存。
(2)缺页异常处理程序需要区分引起缺页的两种情况:编程错引起的缺页和属于进程的地址空间尚未分配到物理页框
简单流程图:
详细流程图:
linux为什么要分为三个区:ZONE_DMA ZONE_NORMAL ZONE_HIGHMEM?
(1)isa总线的历史遗留问题,只能访问内存的前16M的空间
(2)大容量的RAM使得线性地址空间太小,并不是所有的物理空间都能映射到唯一的线性地址空间
如何确定某个页框属于哪个节点或管理区?
是由每个页框描述符中的flag的高位索引的,比如page_zone()函数就是接收页描述符的地址作为参数,返回页描述符中flag的高位,并到zone_table[ ]数组中确定相应的管理区描述符的地址
slab算法是用来满足对以页框为单位的请求而设置的,简单介绍以下slab算法的原理
对于以页为单位的请求发送到管理区分配器,然后管理区分配器搜索它所管辖的管理区,找一个满足请求的分配区,然后再由这个管理区中的伙伴系统去处理,为了加快这个
过程,每个分区中还提供了一个每cpu页框高速缓存,来处理单个页框的请求。
这个过程中有四个请求页框的函数和宏:
(1)alloc_pages, alloc_page返回分配的第一个页框的页描述符的地址
(2)__get_free_pages , __get_free_page 返回分配的第一个页框的线性地址
其实二者是相同,因为有专门用来处理线性地址到页描述符地址转换的函数 virt_to_page() 实现从线性地址到页描述符地址的转换
本文参考源码为linux-2.6.31。主要分析linux内存管理的页框管理和buddy算法。
看下面的node和zone组织示意图,在内核中node是数据结构pg_data_t来表示的,
下面来看一下内核源码中是怎么定义的。
在这里主要介绍以下zone_dma32, zone_movable两个新出现的zone类型。
ZONE_DMA32:在32bit的系统上,这个区域的大小是0M ,在64bit的系统上这个区域的大小是0~4G
ZONE_MOVABLE: 这是一个伪管理区,主要用来减少内存碎片的。
如果在系统中有多个节点,比如numa结构,将会有一个bitmap来记录每个节点的状态。
像N_POSSIBLE, N_ONLINE,N_CPU这些属性,是针对支持内存热插拔的系统来说的。对于node来说,linux内核实现了很多操作:
node_set_state(),node_clear_state(), for_each_node_state(),for_each_noline_node()等,但是在flat memory内存模型上,上面
讲的bitmap没有被使用,而且几个函数被实现为空函数。
有关zone的数据成员的意思就不一一介绍了,主要说一下,如何计算watermark的?
每个管理区保留的内存的数量存放在min_free_kbytes变量中,它的初值在内核初始化的时候通过函数init_per_zone_wmark_min()来设定
并可以通过setup_per_zone_wmark()来修改watermark[]数组的值。
2 下面再来看一下: 每CPU页框高速缓存
内核经常请求和释放单个页框,因此就在内存管理区定义了一个“每CPU”页框高速缓存,包含一些预先被分配的页框。在linux-2.6.24之前的内核中都是
每个高速缓存包含两个per_cpu_pages描述符,一个表示热高速缓存,另一个表示冷高速缓存。如下图所示:
但是在linux-2.6.31内核中好像并不是这种情况,具体如下图:
而是,把per_cpu_pages pcp这个数据结构的 list 变成了list[MIGRATE_PCPTYPE], 其中MIGRATE_PCPTYPE = 3.这里是不是多了一种类型的高速缓存,还没有弄清楚。
3 体系结构相关的页标志(主要列出一些比较重要的页标志)
PG_locked:指定当前页是否被上锁,只能由一个进程在修改这个页
PG_error:在涉及这个页的I/O操作上出错时,将会设置这个位
PG_referenced and PG_active control how actively a page is used by the system
PG_uptodate:数据没有错误的从硬盘读进这个页框
PG_dirty is set when the contents of the page have changed as compared to the data on hard disk.
PG_lru helps implement page reclaim and swapping
PG_highmem indicates that a page is in high memory because it cannot be mapped permanently into kernel memory
PG_slab is set for pages that are part of the slab allocator
PG_buddy is set if the page is free and contained on the lists of the buddy system
PG_compound denotes that the page is part of a larger compound page consisting of multiple adjacent regular pages.
针对这些标志位系统实现了一系列的宏来对他们进行操作:
(1) PageXXX(page) checks if a page has the PG_XXX bit set. For
instance, PageDirty checks for the PG_dirty bit, while PageActive
checks for PG_active, and so on.
(2)To set a bit if it is not set and return the previous value, SetPageXXX is provided.
(3)ClearPageXXX unconditionally deletes a specific bit.
(4)TestClearPageXXX clears a bit if it is set, but also returns the previously active value.
4 页表
在linux内核中,void * 和 unsigned long 有相同的长度,他们之间可以没有信息丢失的相互转换。
pte 相关的标志位
内核中用pte_t , pme_t, pud_t, pgd_t等unsinged long数据类型来表示不同级别的页表的入口地址,但实际上并不是所有的位都被用来表示下一级的地址。
拿pte_t来说,这个数据结构不但指示了某页在内存中的地址,而且还表示了很多其他而外的信息,pre_t中主要有以下标志位。
__PAGE_PRESENT pte_t指向的页是否在内存中
__PAGE_ACCESSED: 这个标志位是由CPU来自动设置的,每到read或write了这个页后。
__PAGE_DIRTY: 当页的内容被修改过后,该位就被CPU自动设置
__PAGE_FILE: 和__PAGE_DIRTY占相同的位置,但是用在不同的上下文中。当当前当问的页不在内存中时,显然不可能是脏的,因此这一位就可以用来表示是否
非线性的映射了文件
_PAGE_USER : 用户进程能够访问这个页
_PAGE_READ, _PAGE_WRITE, and _PAGE_EXECUTE specify whether normal user processes are allowed to read the page, write to the page,
or execute the machine code in the page. 注意一点,这里指的是user processes.
不同的体系结构都提供了两个接口在获取或修改这些位:__pgprot 和pte_modify。此外,不同的体系结构还跟据自己对硬件的支持情况,提供了一些其他的接口来操作这些标志位
❑ pte_present checks if the page to which the page table entry
points is present in memory. This function can, for instance, be used
to detect if a page has been swapped out.
❑ pte_dirty checks if the
page associated with the page table entry is dirty, that is, its
contents have been modified since the kernel checked last time. Note
that this function
may only be called if pte_present has ensured that the page is available.
❑ pte_write checks if the kernel may write to the page.
5 内存初始化
1 首先初始化的数据结构应该是节点 , pg_data_t,在UMA结构上,因为只有一个节点,就初始化了一个pg_data_t的一个实例,contig_page_data.下面是系统启动时,与内存初始化相关的函数
(1)setup_arch :体系结构相关的初始化函数
(2)setup_per_cpu_areas : 在smp系统上,初始化每cpu变量,这些变量的初始值存储在内核二进制文件的单独的section中,这个函数主要功能就是初始化
每一个cpu的每cpu变量。
(3)build_all_zonelists: 设置node 和 zone 数据结构
(4)mem_init:体系结构相关的函数,它来禁止 bootmem allocator,并完成向真实的内存分配器的转换。
(5)kmem_cache_init
(6)setup_per_cpu_pageset
重点分析一下 build_all_zonelists. 调用顺序是: build_all_zonelists –> __build_all_zonelists –> build_zonelists。
对于UMA结构来说这个函数就会调用build_zonelists()一次,但是对于NUMA来说将会对每个active的node调用一次build_zonelists().
在UMA结构上,NODE_DATA这个宏返回contig_page_data的地址。build_zonelists()这个函数将会建立一种node和zone的优先级顺序,这种顺序
当在请求的zone里没有空闲内存时非常有用。
这里理解为什么分配内存时按照 ZONE_HIGH -> ZONE_NORMAL -> ZONE_DMA的顺序?
这是因为,ZONE_HIGH的内存是最"cheap"的,因为内核中没有哪一个模块是依赖这一区域的内存,即使ZONE_HIGH的内存被用光了,对内核也没有什么影响。
1 linux内核启动的时候内存布局图
第一个4KB是保留给BIOS使用的,接下来的640K理论上是可用的,但是确不能作为加载内核的空间,因为这一部分后面紧跟着是ROM的固定映射,比如显示卡等
_text 到_etext是内核的代码段部分,即整个编译好的内核代码,内核的数据如全局变量之类的都放在_etext到_edata之间的内存中,_edata到_end之间的部分是
内核初始化期间需要的一些全局变量,这些全局变量的特点是启动完成后就不再使用了,因此可以被删除,释放出内存空间。上面讲的_text _etext _edata _end
到底是什么值,不同的系统将会不同,但是可以从System.map这个文件中查看它们的值是多少
/proc/iomem也提供了内存的布局信息
00000000-00001fff : System RAM
00002000-00005fff : reserved
00006000-0009fbff : System RAM
0009fc00-0009ffff : RAM buffer
000a0000-000bffff : Video RAM area
000c0000-000c7fff : Video ROM
000cd800-000cffff : Adapter ROM
000f0000-000fffff : reserved
000f0000-000fffff : System ROM
00100000-7d4ff7ff : System RAM
00100000-005adec8 : Kernel code
005adec9-007e2b27 : Kernel data
00877000-00904ad7 : Kernel bss
2 下面是setup_arch的与内存相关的函数调用过程
setup_arch
machine_specific_memory_setup // 由BIOS的e820例程来提供一个BIOS检测到的内存布局
parse_cmdline_early // 分析用户输入的命令行参数,有没有指定内存的布局,如果指定则修改BIOS检测到的内存布局
setup_memory //实现功能:(1)每个节点被检测的页框的数量 (2)bootmem allocator的初始化 (3)划分好保留的内存区
paging_init &
nbsp; // 初始化内核页表,开启分页机制
pagetable_init
zone_sizes_init // 初始化所有节点的实例pg_data_t
add_active_range
free_area_init_nodes
在 setup_memory中,init_memory_mapping 直接映射可用的物理内存到PAGE_OFFSET开始的空间中, contig_initmem_init负责激活bootmem allocator。
page_init仅仅是设置了内核页表,可以理解为页表的第一次初始化。
下面是内核空间的划分
Persistent mappings :主要就是用来将highmen中non-persistent pages 映射到内核空间
Fixmaps : 临时内核映射
static 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;
#endif
…
}
这个函数可以指定high_mem的值,但是只能指定< 896M的一个值。
#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
VMALLOC_START和VMALLOC_END的定义主要依赖以上几个参数,
3 paging_init函数的调用过程