内存 |
目 录
1. 内存
1. 内存管理子系统导读from aka
2. 用户态
3. 内核页目录的初始化
4. 内核线程页目录的借用
5. 用户进程内核页目录的建立
6. 内核页目录的同步
7. mlock代码分析
8. memory.c
1. copy_page
2. clear_page_tables
3. oom
4. free_page_tables
5. new_page_tables
6. copy_one_pte
7. copy_pte_range
8. copy_pmd_range
9. copy_page_range
10. free_pte
11. forget_pte
12. zap_pte_range
13. zap_pmd_range
14. zap_page_range
15. zeromap_pte_range等
16. remap_pte_range等
17. put_dirty_page
18. handle_mm_fault
9. mmap.c
10. 伙伴(buddy)算法
11. 页目录处理的宏
12. MM作者的文章
内存
内存管理系统是操作系统中最为重要的部分,因为系统的物理内存总是少于系统所需要的内存数量。虚拟内存就是为了克服这个矛盾而采用的策略。系统的虚拟内存通过在各个进程之间共享内存而使系统看起来有多于实际内存的内存容量。
虚拟内存可以提供以下的功能:
*广阔的地址空间。
系统的虚拟内存可以比系统的实际内存大很多倍。
*进程的保护。
系统中的每一个进程都有自己的虚拟地址空间。这些虚拟地址空间是完全分开的,这样一个进程的运行不会影响其他进程。并且,硬件上的虚拟内存机制是被保护的,内存不能被写入,这样可以防止迷失的应用程序覆盖代码的数据。
*内存映射。
内存映射用来把文件映射到进程的地址空间。在内存映射中,文件的内容直接连接到进程的虚拟地址空间。
*公平的物理内存分配。
内存管理系统允许系统中每一个运行的进程都可以公平地得到系统的物理内存。
*共享虚拟内存。
虽然虚拟内存允许进程拥有自己单独的虚拟地址空间,但有时可能会希望进程共享内存。
linux仅仅使用四个段
两个代表 (code 和 data/stack)是内核空间从[0xC000 0000] (3 GB)到[0xFFFF FFFF] (4 GB)
两个代表 (code 和 data/stack)是用户空间从[0] (0 GB) 到 [0xBFFF FFFF] (3 GB)
__
4 GB--->| | |
| Kernel | | 内核空间 (Code + Data/Stack)
| | __|
3 GB--->|----------------| __
| | |
| | |
2 GB--->| | |
| Tasks | | 用户空间 (Code + Data/Stack)
| | |
1 GB--->| | |
| | |
|________________| __|
0x00000000
内核/用户 线性地址
linux可以使用3层页表映射,例如在高级的I64服务器上,但是i386体系结构下只有2层有实际意义:
------------------------------------------------------------------
线性地址
------------------------------------------------------------------
/___/ /___/ /_____/
PD 偏移 PF 偏移 Frame 偏移
[10 bits] [10 bits] [12 bits]
| | |
| | ----------- |
| | | Value |----------|---------
| | | | |---------| /|/ | |
| | | | | | | | |
| | | | | | | Frame 偏移 |
| | | | | | /|/ |
| | | | |---------|<------ |
| | | | | | | |
| | | | | | | x 4096 |
| | | PF 偏移 |_________|------- |
| | | /|/ | | |
PD 偏移 |_________|----- | | | _________|
/|/ | | | | | | |
| | | | /|/ | | /|/
_____ | | | ------>|_________| 物理地址
| | /|/ | | x 4096 | |
| CR3 |-------->| | | |
|_____| | ....... | | ....... |
| | | |
页目录表 页表
Linux i386 分页
注意内核(仅仅内核)线性空间就等于内核物理空间,所以如下:
________________ _____
| 其他内核数据 |___ | | |
|----------------| | |__| |
| 内核 |/ |____| 实际的其他 |
3 GB --->|----------------| / | 内核数据 |
| |/ / | |
| __|_/_/____|__ Real |
| Tasks | / / | Tasks |
| __|___/_/__|__ Space |
| | / / | |
| | / /|----------------|
| | / | 实际内核空间 |
|________________| /|________________|
逻辑地址 物理地址
[内存实时分配]
|copy_mm
|allocate_mm = kmem_cache_alloc
|__kmem_cache_alloc
|kmem_cache_alloc_one
|alloc_new_slab
|kmem_cache_grow
|kmem_getpages
|__get_free_pages
|alloc_pages
|alloc_pages_pgdat
|__alloc_pages
|rmqueue
|reclaim_pages
·copy_mm [kernel/fork.c]
·allocate_mm [kernel/fork.c]
·kmem_cache_alloc [mm/slab.c]
·__kmem_cache_alloc
·kmem_cache_alloc_one
·alloc_new_slab
·kmem_cache_grow
·kmem_getpages
·__get_free_pages [mm/page_alloc.c]
·alloc_pages [mm/numa.c]
·alloc_pages_pgdat
·__alloc_pages [mm/page_alloc.c]
·rm_queue
·reclaim_pages [mm/vmscan.c]
[内存交换线程kswapd]
|kswapd
|// initialization routines
|for (;;) { // Main loop
|do_try_to_free_pages
|recalculate_vm_stats
|refill_inactive_scan
|run_task_queue
|interruptible_sleep_on_timeout // we sleep for a new swap request
|}
·kswapd [mm/vmscan.c]
·do_try_to_free_pages
·recalculate_vm_stats [mm/swap.c]
·refill_inactive_scan [mm/vmswap.c]
·run_task_queue [kernel/softirq.c]
·interruptible_sleep_on_timeout [kernel/sched.c]
[内存交换机制:出现内存不足的Exception]
| Page Fault Exception
| cause by all these conditions:
| a-) User page
| b-) Read or write access
| c-) Page not present
|
|
-----------> |do_page_fault
|handle_mm_fault
|pte_alloc
|pte_alloc_one
|__get_free_page = __get_free_pages
|alloc_pages
|alloc_pages_pgdat
|__alloc_pages
|wakeup_kswapd // We wake up kernel thread kswapd
·do_page_fault [arch/i386/mm/fault.c]
·handle_mm_fault [mm/memory.c]
·pte_alloc
·pte_alloc_one [include/asm/pgalloc.h]
·__get_free_page [include/linux/mm.h]
·__get_free_pages [mm/page_alloc.c]
·alloc_pages [mm/numa.c]
·alloc_pages_pgdat
·__alloc_pages
·wakeup_kswapd [mm/vmscan.c]
[目录]
内存管理子系统导读from aka
我的目标是‘导读’,提供linux内存管理子系统的整体概念,同时给出进一步深入研究某个部分时的辅助信息(包括代码组织,文件和主要函数的意义和一些参考文档)。之所以采取这种方式,是因为我本人在阅读代码的过程中,深感“读懂一段代码容易,把握整体思想却极不容易”。而且,在我写一些内核代码时,也觉得很多情况下,不一定非得很具体地理解所有内核代码,往往了解它的接口和整体工作原理就够了。当然,我个人的能力有限,时间也很不够,很多东西也是近期迫于讲座压力临时学的:),内容难免偏颇甚至错误,欢迎大家指正。
存储层次结构和x86存储管理硬件(MMU)
这里假定大家对虚拟存储,段页机制有一定的了解。主要强调一些很重要的或者容易误解的概念。
存储层次
高速缓存(cache) --〉 主存(main memory) ---〉 磁盘(disk)
理解存储层次结构的根源:CPU速度和存储器速度的差距。
层次结构可行的原因:局部性原理。
LINUX的任务:
减小footprint,提高cache命中率,充分利用局部性。
实现虚拟存储以满足进程的需求,有效地管理内存分配,力求最合理地利用有限的资源。
参考文档:
《too little,too small》by Rik Van Riel, Nov. 27,2000.
以及所有的体系结构教材:)
MMU的作用
辅助操作系统进行内存管理,提供虚实地址转换等硬件支持。
x86的地址
逻辑地址: 出现在机器指令中,用来制定操作数的地址。段:偏移
线性地址:逻辑地址经过分段单元处理后得到线性地址,这是一个32位的无符号整数,可用于定位 4G 个存储单元。
物理地址:线性地址经过页表查找后得出物理地址,这个地址将被送到地址总线上指示所要访问的物理内存单元。
LINUX: 尽量避免使用段功能以提高可移植性。如通过使用基址为0的段,使逻辑地址==线性地址。
x86的段
保护模式下的段:选择子+描述符。不仅仅是一个基地址的原因是为了提供更多的信息:保护、长度限制、类型等。描述符存放在一张表中(GDT或LDT),选择子可以认为是表的索引。段寄存器中存放的是选择子,在段寄存器装入的同时,描述符中的数据被装入一个不可见的寄存器以便cpu快速访问。(图)P40
专用寄存器:GDTR(包含全局描述附表的首地址),LDTR(当前进程的段描述附表首地址),TSR(指向当前进程的任务状态段)
LINUX使用的段:
__KERNEL_CS: 内核代码段。范围 0 -4G 。可读、执行。DPL=0。
__KERNEL_DS:内核代码段。范围 0 -4G 。可读、写。DPL=0。
__USER_CS:内核代码段。范围 0 -4G 。可读、执行。DPL=3。
__USER_DS:内核代码段。范围 0 -4G 。可读、写。DPL=3。
TSS(任务状态段):存储进程的硬件上下文,进程切换时使用。(因为x86硬件对TSS有一定支持,所有有这个特殊的段和相应的专用寄存器。)
default_ldt:理论上每个进程都可以同时使用很多段,这些段可以存储在自己的ldt段中,但实际linux极少利用x86的这些功能,多数情况下所有进程共享这个段,它只包含一个空描述符。
还有一些特殊的段用在电源管理等代码中。
(在2.2以前,每个进程的ldt和TSS段都存在GDT中,而GDT最多只能有8192项,因此整个系统的进程总数被限制在4090左右。2。4里不再把它们存在GDT中,从而取消了这个限制。)
__USER_CS和__USER_DS段都是被所有在用户态下的进程共享的。注意不要把这个共享和进程空间的共享混淆:虽然大家使用同一个段,但通过使用不同的页表由分页机制保证了进程空间仍然是独立的。
x86的分页机制
x86硬件支持两级页表,奔腾pro以上的型号还支持Physical address Extension Mode和三级页表。所谓的硬件支持包括一些特殊寄存器(cr0-cr4)、以及CPU能够识别页表项中的一些标志位并根据访问情况做出反应等等。如读写Present位为0的页或者写Read/Write位为0的页将引起CPU发出page fault异常,访问完页面后自动设置accessed位等。
linux采用的是一个体系结构无关的三级页表模型(如图),使用一系列的宏来掩盖各种平台的细节。例如,通过把PMD看作只有一项的表并存储在pgd表项中(通常pgd表项中存放的应该是pmd表的首地址),页表的中间目录(pmd)被巧妙地‘折叠’到页表的全局目录(pgd),从而适应了二级页表硬件。
TLB
TLB全称是Translation Look-aside Buffer,用来加速页表查找。这里关键的一点是:如果操作系统更改了页表内容,它必须相应的刷新TLB以使CPU不误用过时的表项。
Cache
Cache 基本上是对程序员透明的,但是不同的使用方法可以导致大不相同的性能。linux有许多关键的地方对代码做了精心优化,其中很多就是为了减少对cache不必要的污染。如把只有出错情况下用到的代码放到.fixup section,把频繁同时使用的数据集中到一个cache行(如struct task_struct),减少一些函数的footprint,在slab分配器里头的slab coloring等。
另外,我们也必须知道什么时候cache要无效:新map/remap一页到某个地址、页面换出、页保护改变、进程切换等,也即当cache对应的那个地址的内容或含义有所变化时。当然,很多情况下不需要无效整个cache,只需要无效某个地址或地址范围即可。实际上,
intel在这方面做得非常好用,cache的一致性完全由硬件维护。
关于x86处理器更多信息,请参照其手册:Volume 3: Architecture and Programming Manual
8. Linux 相关实现
这一部分的代码和体系结构紧密相关,因此大多位于arch子目录下,而且大量以宏定义和inline函数形式存在于头文件中。以i386平台为例,主要的文件包括:
page.h
页大小、页掩码定义。PAGE_SIZE,PAGE_SHIFT和PAGE_MASK。
对页的操作,如清除页内容clear_page、拷贝页copy_page、页对齐page_align
还有内核虚地址的起始点:著名的PAGE_OFFSET:)和相关的内核中虚实地址转换的宏__pa和__va.。
virt_to_page从一个内核虚地址得到该页的描述结构struct page *.我们知道,所有物理内存都由一个memmap数组来描述。这个宏就是计算给定地址的物理页在这个数组中的位置。另外这个文件也定义了一个简单的宏检查一个页是不是合法:VALID_PAGE(page)。如果page离memmap数组的开始太远以至于超过了最大物理页面应有的距离则是不合法的。
比较奇怪的是页表项的定义也放在这里。pgd_t,pmd_t,pte_t和存取它们值的宏xxx_val
pgtable.h pgtable-2level.h pgtable-3level.h
顾名思义,这些文件就是处理页表的,它们提供了一系列的宏来操作页表。pgtable-2level.h和pgtable-2level.h则分别对应x86二级、三级页表的需求。首先当然是表示每级页表有多少项的定义不同了。而且在PAE模式下,地址超过32位,页表项pte_t用64位来表示(pmd_t,pgd_t不需要变),一些对整个页表项的操作也就不同。共有如下几类:
·[pte/pmd/pgd]_ERROR 出措时要打印项的取值,64位和32位当然不一样。
·set_[pte/pmd/pgd] 设置表项值
·pte_same 比较 pte_page 从pte得出所在的memmap位置
·pte_none 是否为空。
·__mk_pte 构造pte
pgtable.h的宏太多,不再一一解释。实际上也比较直观,通常从名字就可以看出宏的意义来了。pte_xxx宏的参数是pte_t,而ptep_xxx的参数是pte_t *。2.4 kernel在代码的clean up方面还是作了一些努力,不少地方含糊的名字变明确了,有些函数的可读性页变好了。
pgtable.h里除了页表操作的宏外,还有cache和tlb刷新操作,这也比较合理,因为他们常常是在页表操作时使用。这里的tlb操作是以__开始的,也就是说,内部使用的,真正对外接口在pgalloc.h中(这样分开可能是因为在SMP版本中,tlb的刷新函数和单机版本区别较大,有些不再是内嵌函数和宏了)。
pgalloc.h
包括页表项的分配和释放宏/函数,值得注意的是表项高速缓存的使用:
pgd/pmd/pte_quicklist
内核中有许多地方使用类似的技巧来减少对内存分配函数的调用,加速频繁使用的分配。如buffer cache中buffer_head和buffer,vm区域中最近使用的区域。
还有上面提到的tlb刷新的接口
segment.h
定义 __KERNEL_CS[DS] __USER_CS[DS]
参考:
《Understanding the Linux Kernel》的第二章给了一个对linux 的相关实现的简要描述,
物理内存的管理。
2.4中内存管理有很大的变化。在物理页面管理上实现了基于区的伙伴系统(zone based buddy system)。区(zone)的是根据内存的不同使用类型划分的。对不同区的内存使用单独的伙伴系统(buddy system)管理,而且独立地监控空闲页等。
(实际上更高一层还有numa支持。Numa(None Uniformed Memory Access)是一种体系结构,其中对系统里的每个处理器来说,不同的内存区域可能有不同的存取时间(一般是由内存和处理器的距离决定)。而一般的机器中内存叫做DRAM,即动态随机存取存储器,对每个单元,CPU用起来是一样快的。NUMA中访问速度相同的一个内存区域称为一个Node,支持这种结构的主要任务就是要尽量减少Node之间的通信,使得每个处理器要用到的数据尽可能放在对它来说最快的Node中。2.4内核中node相应的数据结构是pg_data_t,每个node拥有自己的memmap数组,把自己的内存分成几个zone,每个zone再用独立的伙伴系统管理物理页面。Numa要对付的问题还有很多,也远没有完善,就不多说了)
基于区的伙伴系统的设计物理页面的管理
内存分配的两大问题是:分配效率、碎片问题。一个好的分配器应该能够快速的满足各种大小的分配要求,同时不能产生大量的碎片浪费空间。伙伴系统是一个常用的比较好的算法。(解释:TODO)
引入区的概念是为了区分内存的不同使用类型(方法?),以便更有效地利用它们。
2.4有三个区:DMA, Normal, HighMem。前两个在2.2实际上也是由独立的buddy system管理的,但2.2中还没有明确的zone的概念。DMA区在x86体系结构中通常是小于16兆的物理内存区,因为DMA控制器只能使用这一段的内存。而HighMem是物理地址超过某个值(通常是约 900M )的高端内存。其他的是Normal区内存。由于linux实现的原因,高地址的内存不能直接被内核使用,如果选择了CONFIG_HIGHMEM选项,内核会使用一种特殊的办法来使用它们。(解释:TODO)。HighMem只用于page cache和用户进程。这样分开之后,我们将可以更有针对性地使用内存,而不至于出现把DMA可用的内存大量给无关的用户进程使用导致驱动程序没法得到足够的DMA内存等情况。此外,每个区都独立地监控本区内存的使用情况,分配时系统会判断从哪个区分配比较合算,综合考虑用户的要求和系统现状。2.4里分配页面时可能会和高层的VM代码交互(分配时根据空闲页面的情况,内核可能从伙伴系统里分配页面,也可能直接把已经分配的页收回reclaim等),代码比2.2复杂了不少,要全面地理解它得熟悉整个VM工作的机理。
整个分配器的主要接口是如下函数(mm.h page_alloc.c):
struct page * alloc_pages(int gfp_mask, unsigned long order) 根据gftp_mask的要求,从适当的区分配2^order个页面,返回第一个页的描述符。
#define alloc_page(gfp_mask) alloc_pages(gfp_mask,0)
unsigned long __get_free_pages((int gfp_mask, unsigned long order) 工作同alloc_pages,但返回首地址。
#define __get_free_page(gfp_mask) __get_free_pages(gfp_mask,0)
get_free_page 分配一个已清零的页面。
__free_page(s) 和free_page(s)释放页面(一个/多个)前者以页面描述符为参数,后者以页面地址为参数。
关于Buddy算法,许多教科书上有详细的描述,第六章对linux的实现有一个很好的介绍。关于zone base buddy更多的信息,可以参见Rik Van Riel 写的" design for a zone based memory allocator"。这个人是目前linuxmm的维护者,权威啦。这篇文章有一点过时了,98年写的,当时还没有HighMem,但思想还是有效的。还有,下面这篇文章分析2.4的实现代码:
http://home.earthlink.net/~jknapka/linux-mm/zonealloc.html。
Slab--连续物理区域管理
单单分配页面的分配器肯定是不能满足要求的。内核中大量使用各种数据结构,大小从几个字节到几十上百k不等,都取整到2的幂次个页面那是完全不现实的。2.0的内核的解决方法是提供大小为2,4,8,16,...,131056字节的内存区域。需要新的内存区域时,内核从伙伴系统申请页面,把它们划分成一个个区域,取一个来满足需求;如果某个页面中的内存区域都释放了,页面就交回到伙伴系统。这样做的效率不高。有许多地方可以改进:
不同的数据类型用不同的方法分配内存可能提高效率。比如需要初始化的数据结构,释放后可以暂存着,再分配时就不必初始化了。
内核的函数常常重复地使用同一类型的内存区,缓存最近释放的对象可以加速分配和释放。
对内存的请求可以按照请求频率来分类,频繁使用的类型使用专门的缓存,很少使用的可以使用类似2.0中的取整到2的幂次的通用缓存。
使用2的幂次大小的内存区域时高速缓存冲突的概率较大,有可能通过仔细安排内存区域的起始地址来减少高速缓存冲突。
缓存一定数量的对象可以减少对buddy系统的调用,从而节省时间并减少由此引起的高速缓存污染。
2.2实现的slab分配器体现了这些改进思想。
主要数据结构
接口:
kmem_cache_create/kmem_cache_destory
kmem_cache_grow/kmem_cache_reap 增长/缩减某类缓存的大小
kmem_cache_alloc/kmem_cache_free 从某类缓存分配/释放一个对象
kmalloc/kfree 通用缓存的分配、释放函数。
相关代码(slab.c)。
相关参考:
http://www.lisoleg.net/lisoleg/memory/slab.pdf :Slab发明者的论文,必读经典。
第六章,具体实现的详细清晰的描述。
AKA2000年的讲座也有一些大虾讲过这个主题,请访问aka主页:www.aka.org.cn
vmalloc/vfree 物理地址不连续,虚地址连续的内存管理
使用kernel页表。文件vmalloc.c,相对简单。
2.4内核的VM(完善中。。。)
进程地址空间管理
创建,销毁。
mm_struct, vm_area_struct, mmap/mprotect/munmap
page fault处理,demand page, copy on write
相关文件:
include/linux/mm.h:struct page结构的定义,page的标志位定义以及存取操作宏定义。struct vm_area_struct定义。mm子系统的函数原型说明。
include/linux/mman.h:和vm_area_struct的操作mmap/mprotect/munmap相关的常量宏定义。
memory.c:page fault处理,包括COW和demand page等。
对一个区域的页表相关操作:
zeromap_page_range: 把一个范围内的页全部映射到zero_page
remap_page_range:给定范围的页重新映射到另一块地址空间。
zap_page_range:把给定范围内的用户页释放掉,页表清零。
mlock.c: mlock/munlock系统调用。mlock把页面锁定在物理内存中。
mmap.c::mmap/munmap/brk系统调用。
mprotect.c: mprotect系统调用。
前面三个文件都大量涉及vm_area_struct的操作,有很多相似的xxx_fixup的代码,它们的任务是修补受到影响的区域,保证vm_area_struct 链表正确。
交换
目的:
使得进程可以使用更大的地址空间。同时容纳更多的进程。
任务:
选择要换出的页
决定怎样在交换区中存储页面
决定什么时候换出
kswapd内核线程:每10秒激活一次
任务:当空闲页面低于一定值时,从进程的地址空间、各类cache回收页面
为什么不能等到内存分配失败再用try_to_free_pages回收页面?原因:
有些内存分配时在中断或异常处理调用,他们不能阻塞
有时候分配发生在某个关键路径已经获得了一些关键资源的时候,因此它不能启动IO。如果不巧这时所有的路径上的内存分配都是这样,内存就无法释放。
kreclaimd 从inactive_clean_list回收页面,由__alloc_pages唤醒。
相关文件:
mm/swap.c kswapd使用的各种参数以及操作页面年龄的函数。
mm/swap_file.c 交换分区/文件的操作。
mm/page_io.c 读或写一个交换页。
mm/swap_state.c swap cache相关操作,加入/删除/查找一个swap cache等。
mm/vmscan.c 扫描进程的vm_area,试图换出一些页面(kswapd)。
reclaim_page:从inactive_clean_list回收一个页面,放到free_list
kclaimd被唤醒后重复调用reclaim_page直到每个区的
zone->free_pages>= zone->pages_low
page_lauder:由__alloc_pages和try_to_free_pages等调用。通常是由于freepages + inactive_clean_list的页太少了。功能:把inactive_dirty_list的页面转移到inactive_clean_list,首先把已经被写回文件或者交换区的页面(by bdflush)放到inactive_clean_list,如果freepages确实短缺,唤醒bdflush,再循环一遍把一定数量的dirty页写回。
关于这几个队列(active_list,inactive_dirty_list,inactive_clean_list)的逻辑,请参照:文档:RFC: design for new VM,可以从lisoleg的文档精华获得。
page cache、buffer cache和swap cache
page cache:读写文件时文件内容的cache,大小为一个页。不一定在磁盘上连续。
buffer cache:读写磁盘块的时候磁盘块内容的cache,buffer cache的内容对应磁盘上一个连续的区域,一个buffer cache大小可能从512(扇区大小)到一个页。
swap cache: 是page cache的子集。用于多个进程共享的页面被换出到交换区的情况。
page cache 和 buffer cache的关系
本质上是很不同的,buffer cache缓冲磁盘块内容,page cache缓冲文件的一页内容。page cache写回时会使用临时的buffer cache来写磁盘。
bdflush: 把dirty的buffer cache写回磁盘。通常只当dirty的buffer太多或者需要更多的buffer而内存开始不足时运行。page_lauder也可能唤醒它。
kupdate: 定时运行,把写回期限已经到了的dirty buffer写回磁盘。
2.4的改进:page cache和buffer cache耦合得更好了。在2.2里,磁盘文件的读使用page cache,而写绕过page cache,直接使用buffer cache,因此带来了同步的问题:写完之后必须使用update_vm_cache()更新可能有的page cache。2.4中page cache做了比较大的改进,文件可以通过page cache直接写了,page cache优先使用high memory。而且,2.4引入了新的对象:file address space,它包含用来读写一整页数据的方法。这些方法考虑到了inode的更新、page cache处理和临时buffer的使用。page cache和buffer cache的同步问题就消除了。原来使用inode+offset查找page cache变成通过file address space+offset;原来struct page 中的inode成员被address_space类型的mapping成员取代。这个改进还使得匿名内存的共享成为可能(这个在2.2很难实现,许多讨论过)。
虚存系统则从freeBSD借鉴了很多经验,针对2.2的问题作了巨大的调整。
文档:RFC: design for new VM不可不读。
由于时间仓促,新vm的很多细微之处我也还没来得及搞清楚。先大致罗列一下,以后我将进一步完善本文,争取把问题说清楚。另外,等这学期考试过后,我希望能为大家提供一些详细注释过的源代码。
[目录]
用户态
用户空间存取内核空间,具体的实现方法要从两个方面考虑,先是用户进程,需要调用mmapp来将自己的一段虚拟空间映射到内核态分配的物理内存;然后内核空间需要重新设置用户进程的这段虚拟内存的页表,使它的物理地址指向对应的物理内存。针对linux内核的几种不同的内存分配方式(kmalloc、vmalloc和ioremap),需要进行不同的处理。
一、Linux内存管理概述
这里说一下我的理解,主要从数据结构说。
1、物理内存都是按顺序分成一页一页的,每页用一个page结构来描述。系统所有的物理页 面的page结
构描述就组成了一个数组mem_map。
2、进程的虚拟地址空间用task_struct的域mm来描述,它是一个mm_struct结构,这个结构包包含了指向?
程页目录的指针(pgd_t * pgd)和指向进程虚拟内存区域的指针(struct vm_area_structt * mmap)
3、进程虚拟内存区域具有相同属性的段用结构vm_area_struct描述(简称为VMA)。进程所所有的VMA?
树组织。
4、每个VMA就是一个对象,定义了一组操作,可以通过这组操作来对不同类型的VMA进行不屯 的处理。
例如对vmalloc分配的内存的映射就是通过其中的nopage操作实现的。
二、mmap处理过程
当用户调用mmap的时候,内核进行如下的处理:
1、先在进程的虚拟空间查找一块VMA;
2、将这块VMA去映射
3、如果设备驱动程序或者文件系统的file_operations定义了mmap操作,则调用它
4、将这个VMA插入到进程的VMA链中
file_operations的中定义的mmap方法原型如下:
int (*mmap) (struct file *, struct vm_area_struct *);
其中file是虚拟空间映射到的文件结构,vm_area_struct就是步骤1中找到的VMA。
三、缺页故障处理过程
当访问一个无效的虚拟地址(可能是保护故障,也可能缺页故障等)的时候,就会产生一个个页故障,?
统的处理过程如下:
1、找到这个虚拟地址所在的VMA;
2、如果必要,分配中间页目录表和页表
3、如果页表项对应的物理页面不存在,则调用这个VMA的nopage方法,它返回物理页面的paage描述结构
(当然这只是其中的一种情况)
4、针对上面的情况,将物理页面的地址填充到页表中
当页故障处理完后,系统将重新启动引起故障的指令,然后就可以正常访问了
下面是VMA的方法:
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, innt
write_access);
};
其中缺页函数nopage的address是引起缺页故障的虚拟地址,area是它所在的VMA,write_acccess是存取
属性。
三、具体实现
3.1、对kmalloc分配的内存的映射
对kmalloc分配的内存,因为是一段连续的物理内存,所以它可以简单的在mmap例程中设置汉 页表的物
理地址,方法是使用函数remap_page_range。它的原型如下:
int remap_page_range(unsigned long from, unsigned long phys_addr, unsigned long size,
pgprot_t prot)
其中from是映射开始的虚拟地址。这个函数为虚拟地址空间from和from+size之间的范围构栽 页表;
phys_addr是虚拟地址应该映射到的物理地址;size是被映射区域的大小;prot是保护标志?
remap_page_range的处理过程是对from到form+size之间的每一个页面,查找它所在的页目侣己 页表(
必要时建立页表),清除页表项旧的内容,重新填写它的物理地址与保护域。
remap_page_range可以对多个连续的物理页面进行处理。<
remap_page_range只能给予对保留的页和物理内存之上的物理地址的访问,当对非保留的页页使?
remap_page_range时,缺省的nopage处理控制映射被访问的虚地址处的零页。所以在分配内内存后,就?
对所分配的内存置保留位,它是通过函数mem_map_reserve实现的,它就是对相应物理页面?
PG_reserved标志位。(关于这一点,参见前面的主题为“关于remap_page_range的疑问”档奶致郏?
因为remap_page_range有上面的限制,所以可以用另外一种方式,就是采用和vmalloc分配档哪 存同样
的方法,对缺页故障进行处理。
3.2、对vmalloc分配的内存的映射
3.2.1 、vmalloc分配内存的过程
(1)、进行预处理和合法性检查,例如将分配长度进行页面对齐,检查分配长度是否过大?
(2)、以GFP_KERNEL为优先级调用kmalloc分配(GFP_KERNEL用在进程上下文中,所以这里里就限制了?
中断处理程序中调用vmalloc)描述vmalloc分配的内存的vm_struct结构。
(3)、将size加一个页面的长度,使中间形成4K的隔离带,然后在VMALLOC_START和VMALLOOC_END之间
编历vmlist链表,寻找一段自由内存区间,将其地址填入vm_struct结构中
(4)、返回这个地址
vmalloc分配的物理内存并不连续
3.2.2 、页目录与页表的定义
typedef struct { unsigned long pte_low; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pgd; } pgd_t;
#define pte_val(x) ((x).pte_low)
3.2.3 、常见例程:
(1)、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),因为内核空间从 3G 到 3G +实实际内存一?
映射到物理地址的0到实际内存
(2)、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中
(3)、#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT))(内核核2.4?
#define VALID_PAGE(page) ((page - mem_map) < max_mapnr)(内核2.4)
第一个宏根据虚拟地址,将其转换为相应的物理页面的page描述结构,第二个宏判断页面是是不是在有?
的物理页面内。(这两个宏处理的虚拟地址必须是内核虚拟地址,例如kmalloc返回的地址#杂?
vmalloc返回的地址并不能这样,因为vmalloc分配的并不是连续的物理内存,中间可能有空空洞?
3.2.4 、vmalloc分配的内存的mmap的实现:
对vmalloc分配的内存需要通过设置相应VMA的nopage方法来实现,当产生缺页故障的时候,,会调用VM
的nopage方法,我们的目的就是在nopage方法中返回一个page结构的指针,为此,需要通过过如下步骤?
(1) pgd_offset_k或者 pgd_offset:查找虚拟地址所在的页目录表,前者对应内核空间档男 拟地址
,后者对应用户空间的虚拟地址
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
#define pgd_offset_k(address) pgd_offset(&init_mm, address)
对于后者,init_mm是进程0(idle process)的虚拟内存mm_struct结构,所有进程的内核 页表都一样
。在vmalloc分配内存的时候,要刷新内核页目录表,2.4中为了节省开销,只更改了进程0档哪 核页目
录,而对其它进程则通过访问时产生页面异常来进行更新各自的内核页目录
(2)pmd_offset:找到虚拟地址所在的中间页目录项。在查找之前应该使用pgd_none判断适 否存在相
应的页目录项,这些函数如下:
extern inline int pgd_none(pgd_t pgd) { return 0; }
extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
return (pmd_t *) dir;
}
(3)pte_offset:找到虚拟地址对应的页表项。同样应该使用pmd_none判断是否存在相应档 中间页目
录:
#define pmd_val(x) ((x).pmd)
#define pmd_none(x) (!pmd_val(x))
#define __pte_offset(address) /
((address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pmd_page(pmd) /
((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))
#define pte_offset(dir, address) ((pte_t *) pmd_page(*(dir)) + /
__pte_offset(address))
(4)pte_present和pte_page:前者判断页表对应的物理地址是否有效,后者取出页表中物物理地址对?
的page描述结构
#define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE))
#define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT))))
#define page_address(page) ((page)->virtual)
下面的一个DEMO与上面的关系不大,它是做这样一件事情,就是在启动的时候保留一段内存存,然后使?
ioremap将它映射到内核虚拟空间,同时又用remap_page_range映射到用户虚拟空间,这样亮 边都能访
问,通过内核虚拟地址将这段内存初始化串"abcd",然后使用用户虚拟地址读出来。
/************mmap_ioremap.c**************/
#include
#include
#include
#include
#include
#include
#include
MODULE_PARM(mem_start,"i");
MODULE_PARM(mem_size,"i");
static int mem_start=101,mem_size=10;
static char * reserve_virt_addr;
static int major;
int mmapdrv_open(struct inode *inode, struct file *file);
int mmapdrv_release(struct inode *inode, struct file *file);
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma);
static struct file_operations mmapdrv_fops =
{
owner: THIS_MODULE,
mmap: mmapdrv_mmap,
open: mmapdrv_open,
release: mmapdrv_release,
};
int init_module(void)
{
if ( ( major = register_chrdev(0, "mmapdrv", &mmapdrv_fops) ) < 0 )
{
printk("mmapdrv: unable to register character device/n");
return (-EIO);
}
printk("mmap device major = %d/n",major );
printk( "high memory physical address 0x%ldM/n",
virt_to_phys(high_memory)/1024/1024 );
reserve_virt_addr = ioremap( mem_start*1024*1024,mem_size*1024*1024);
printk( "reserve_virt_addr = 0x%lx/n", (unsigned long)reserve_virt_addr );
if ( reserve_virt_addr )
{
int i;
for ( i=0;i
reserve_virt_addr[i] = 'a';
reserve_virt_addr[i+1] = 'b';
reserve_virt_addr[i+2] = 'c';
reserve_virt_addr[i+3] = 'd';
}
}
else
{
unregister_chrdev( major, "mmapdrv" );
return -ENODEV;
}
return 0;
}
/* remove the module */
void cleanup_module(void)
{
if ( reserve_virt_addr )
iounmap( reserve_virt_addr );
unregister_chrdev( major, "mmapdrv" );
return;
}
int mmapdrv_open(struct inode *inode, struct file *file)
{
MOD_INC_USE_COUNT;
return(0);
}
int mmapdrv_release(struct inode *inode, struct file *file)
{
MOD_DEC_USE_COUNT;
return(0);
}
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff<
if ( size > mem_size*1024*1024 )
{
printk("size too big/n");
return(-ENXIO);
}
offset = offset + mem_start*1024*1024;
/* we do not want to have this area swapped out, lock it */
vma->vm_flags |= VM_LOCKED;
if ( remap_page_range(vma->vm_start,offset,size,PAGE_SHARED))
{
printk("remap page range failed/n");
return -ENXIO;
}
return(0);
}
使用LDD2源码里面自带的工具mapper测试结果如下:
[root@localhost modprg]# insmod mmap_ioremap.mod
mmap device major = 254
high memory physical address 0x 100M
reserve_virt_addr = 0xc7038000
[root@localhost modprg]# mknod mmapdrv c 254 0
[root@localhost modprg]# ./mapper mmapdrv 0 1024 | od -Ax -t x1
mapped "mmapdrv" from 0 to 1024
000000 61 62 63 64 61 62 63 64 61 62 63 64 61 62 63 64
*
000400
[root@localhost modprg]#
[目录]
内核页目录的初始化
内核页目录的初始化
内核页目录的初始化
/* swapper_pg_dir is the main page directory, address 0x00101000*/
>>> 内核页目录,第0,1项和第768、767项均为映射到物理内存0 -8M 的页目录项
>>> 其页表的物理地址是0x00102000和0x00103000,即下面的pg0和pg1所在的位置
>>> (在启动的时候,将内核映像移到0x0010000处)。
>>> 之所以第0,1项与第768和767相同,是因为在开启分页前的线性地址0 -8M 和开启
>>> 分页之后的 3G -3G + 8M 均映射到相同的物理地址0 -8M
/*
* This is initialized to create an identity-mapping at 0 -8M (for bootup
* purposes) and another mapping of the 0 -8M area at virtual address
* PAGE_OFFSET.
*/
.org 0x1000
ENTRY(swapper_pg_dir)
.long 0x00102007
.long 0x00103007
.fill BOOT_USER_PGD_PTRS-2,4,0
/* default: 766 entries */
.long 0x00102007
.long 0x00103007
/* default: 254 entries */
.fill BOOT_KERNEL_PGD_PTRS-2,4,0
/*
* The page tables are initialized to only 8MB here - the final page
* tables are set up later depending on memory size.
*/
>>> 下面为物理地址0 -8M 的页表项
>>> 从0x4000到0x2000共2k个页表项,映射0 -8M 的物理内存
.org 0x2000
ENTRY(pg0)
.org 0x3000
ENTRY(pg1)
/*
* empty_zero_page must immediately follow the page tables ! (The
* initialization loop counts until empty_zero_page)
*/
.org 0x4000
ENTRY(empty_zero_page)
>>> 进程0的页目录指向swapper_pg_dir
#define INIT_MM(name) /
{ /
mmap: &init_mmap, /
mmap_avl: NULL, /
mmap_cache: NULL, /
pgd: swapper_pg_dir, /
mm_users: ATOMIC_INIT(2), /
mm_count: ATOMIC_INIT(1), /
map_count: 1, /
mmap_sem: __RWSEM_INITIALIZER(name.mmap_sem), /
page_table_lock: SPIN_LOCK_UNLOCKED, /
mmlist: LIST_HEAD_INIT(name.mmlist), /
}
/*
* paging_init() sets up the page tables - note that the first 8MB are
* already mapped by head.S.
*
* This routines also unmaps the page at virtual kernel address 0, so
* that we can trap those pesky NULL-reference errors in the kernel.
*/
void __init paging_init(void)
{
pagetable_init();
__asm__( "movl %%ecx,%%cr3/n" ::"c"(__pa(swapper_pg_dir)));
。。。。。。。。。。。
}
static void __init pagetable_init (void)
{
unsigned long vaddr, end;
pgd_t *pgd, *pgd_base;
int i, j, k;
pmd_t *pmd;
pte_t *pte, *pte_base;
>>> end虚拟空间的最大值(最大物理内存+ 3G )
/*
* This can be zero as well - no problem, in that case we exit
* the loops anyway due to the PTRS_PER_* conditions.
*/
end = (unsigned long)__va(max_low_pfn*PAGE_SIZE);
pgd_base = swapper_pg_dir;
#if CONFIG_X86_PAE
for (i = 0; i < PTRS_PER_PGD; i++)
set_pgd(pgd_base + i, __pgd(1 + __pa(empty_zero_page)));
#endif
>>> 内核起始虚拟空间在内核页目录表中的索引
i = __pgd_offset(PAGE_OFFSET);
pgd = pgd_base + i;
>>> #define PTRS_PER_PGD 1024
>>> 对页目录的从768项开始的每一项
for (; i < PTRS_PER_PGD; pgd++, i++) {
>>> vaddr为第i项页目录项所映射的内核空间的起始虚拟地址,PGDIR_SIZE= 4M
vaddr = i*PGDIR_SIZE;
if (end && (vaddr >= end))
break;
#if CONFIG_X86_PAE
pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
set_pgd(pgd, __pgd(__pa(pmd) + 0x1));
#else
>>> 对两级映射机制,pmd实际上是pgd
pmd = (pmd_t *)pgd;
#endif
if (pmd != pmd_offset(pgd, 0))
BUG();
for (j = 0; j < PTRS_PER_PMD; pmd++, j++) {
vaddr = i*PGDIR_SIZE + j*PMD_SIZE;
if (end && (vaddr >= end))
break;
>>> 假如内核不支持 Page Size Extensions
if (cpu_has_pse) {
。。。。。。。。。。
}
>>> 分配内核页表
pte_base = pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
>>> 对每一项页表项
for (k = 0; k < PTRS_PER_PTE; pte++, k++) {
vaddr = i*PGDIR_SIZE + j*PMD_SIZE + k*PAGE_SIZE;
if (end && (vaddr >= end))
break;
>>> 将页面的物理地址填入页表项中
*pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL);
}
>>> 将页表的物理地址填入到页目录项中
set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base)));
if (pte_base != pte_offset(pmd, 0))
BUG();
}
}
/*
* Fixed mappings, only the page table structure has to be
* created - mappings will be set by set_fixmap():
*/
vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
fixrange_init(vaddr, 0, pgd_base);
#if CONFIG_HIGHMEM
。。。。。。。。。。。。
#endif
#if CONFIG_X86_PAE
。。。。。。。。。。。。
#endif
}
[目录]
内核线程页目录的借用
创建内核线程的时候,由于内核线程没有用户空间,而所有进程的内核页目录都是一样的((某些情况下可能有不同步的情况出现,主要是为了减轻同步所有进程内核页目录的开销,而只是在各个进程要访问内核空间,如果有不同步的情况,然后才进行同步处理),所以创建的内核线程的内核页目录总是借用进程0的内核页目录。
>>> kernel_thread以标志CLONE_VM调用clone系统调用
/*
* Create a kernel thread
*/
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
long retval, d0;
__asm__ __volatile__(
"movl %%esp,%%esi/n/t"
"int $0x80/n/t" /* Linux/i386 system call */
"cmpl %%esp,%%esi/n/t" /* child or parent? */
/* Load the argument into eax, and push it. That way, it does
* not matter whether the called function is compiled with
* -mregparm or not. */
"movl %4,%%eax/n/t"
"pushl %%eax/n/t"
"call *%5/n/t" /* call fn */
"movl %3,%0/n/t" /* exit */
"int $0x80/n"
"1:/t"
:"=&a" (retval), "=&S" (d0)
:"0" (__NR_clone), "i" (__NR_exit),
"r" (arg), "r" (fn),
"b" (flags | CLONE_VM)
: "memory");
return retval;
}
>>> sys_clone->do_fork->copy_mm:
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;
。。。。。。。。
tsk->mm = NULL;
tsk->active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
>>> 如果是内核线程的子线程(mm=NULL),则直接退出,此时内核线程mm和active_mm均为为NULL
oldmm = current->mm;
if (!oldmm)
return 0;
>>> 内核线程,只是增加当前进程的虚拟空间的引用计数
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
。。。。。。。。。。
good_mm:
>>> 内核线程的mm和active_mm指向当前进程的mm_struct结构
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
。。。。。。。
}
然后内核线程一般调用daemonize来释放对用户空间的引用:
>>> daemonize->exit_mm->_exit_mm:
/*
* Turn us into a lazy TLB process if we
* aren't already..
*/
static inline void __exit_mm(struct task_struct * tsk)
{
struct mm_struct * mm = tsk->mm;
mm_release();
if (mm) {
atomic_inc(&mm->mm_count);
if (mm != tsk->active_mm) BUG();
/* more a memory barrier than a real lock */
task_lock(tsk);
>>> 释放用户虚拟空间的数据结构
tsk->mm = NULL;
task_unlock(tsk);
enter_lazy_tlb(mm, current, smp_processor_id());
>>> 递减mm的引用计数并是否为0,是则释放mm所代表的映射
mmput(mm);
}
}
asmlinkage void schedule(void)
{
。。。。。。。。。
if (!current->active_mm) BUG();
。。。。。。。。。
prepare_to_switch();
{
struct mm_struct *mm = next->mm;
struct mm_struct *oldmm = prev->active_mm;
>>> mm = NULL,选中的为内核线程
if (!mm) {
>>> 对内核线程,active_mm = NULL,否则一定是出错了
if (next->active_mm) BUG();
>>> 选中的内核线程active_mm借用老进程的active_mm
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next, this_cpu);
} else {
>>> mm != NULL 选中的为用户进程,active_mm必须与mm相等,否则一定是出错了
if (next->active_mm != mm) BUG();
switch_mm(oldmm, mm, next, this_cpu);
}
>>> prev = NULL ,切换出去的是内核线程
if (!prev->mm) {
>>> 设置其 active_mm = NULL 。
prev->active_mm = NULL;
mmdrop(oldmm);
}
}
}
对内核线程的虚拟空间总结一下:
1、创建的时候:
父进程是用户进程,则mm和active_mm均共享父进程的,然后内核线程一般调用daemonize适头舖m
父进程是内核线程,则mm和active_mm均为NULL
总之,内核线程的mm = NULL;进程调度的时候以此为依据判断是用户进程还是内核线程。
2、进程调度的时候
如果切换进来的是内核线程,则置active_mm为切换出去的进程的active_mm;
如果切换出去的是内核线程,则置active_mm为NULL。
[目录]
用户进程内核页目录的建立
用户进程内核页目录的建立
在fork一个进程的时候,必须建立进程自己的内核页目录项(内核页目录项要
与用户空间的的页目录放在同一个物理地址连续的页面上,所以不能共享,但
所有进程的内核页表与进程0共享?
3G用户,页目录中一项映射4M的空间(一项页目录1024项页表,每项页表对应1个页面4K)# 即:
#define PGDIR_SHIFT 22
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)
>>> sys_fork->do_fork->copy_mm->mm_init->pgd_alloc->get_pgd_slow
#if CONFIG_X86_PAE
。。。。。。。。。。。。。
#else
extern __inline__ pgd_t *get_pgd_slow(void)
{
>>> 分配页目录表(包含1024项页目录),即为一个进程分配的页目录可以映射的空间为10024*4M=4G
pgd_t *pgd = (pgd_t *)__get_free_page(GFP_KERNEL);
if (pgd) {
>>> #define USER_PTRS_PER_PGD (TASK_SIZE/PGDIR_SIZE)
>>> TASK_SIZE为3G大小,USER_PTRS_PER_PGD为用户空间对应的页目录项数目(3G/4M=768?
>>> 将用户空间的页目录项清空
memset(pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));
>>> 将内核页目录表(swapper_pg_dir)的第768项到1023项拷贝到进程的页目录表的第7688项到1023项中
memcpy(pgd + USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER__PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
}
return pgd;
}
#endif
[目录]
内核页目录的同步
内核页目录的同步
当一个进程在内核空间发生缺页故障的时候,在其处理程序中,就要通过0号进程的页目录览 同步本进程的内核页目录,实际上就是拷贝0号进程的内核页目录到本进程中(内核页表与进程0共享,故不需要复制)。如下:
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
。。。。。。。。
>>> 缺页故障产生的地址
/* get the address */
__asm__("movl %%cr2,%0":"=r" (address));
tsk = current;
/*
* We fault-in kernel-space virtual memory on-demand. The
* 'reference' page table is init_mm.pgd.
*/
>>> 如果缺页故障在内核空间
if (address >= TASK_SIZE)
goto vmalloc_fault;
。。。。。。。。。
vmalloc_fault:
{
/*
* Synchronize this task's top level page-table
* with the 'reference' page table.
*/
int offset = __pgd_offset(address);
pgd_t *pgd, *pgd_k;
pmd_t *pmd, *pmd_k;
pgd = tsk->active_mm->pgd + offset;
pgd_k = init_mm.pgd + offset;
>>> /*
>>> * (pmds are folded into pgds so this doesnt get actually called,
>>> * but the define is needed for a generic inline function.)
>>> */
>>> #define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval)
>>> #define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval)
>>> 如果本进程的该地址的内核页目录不存在
if (!pgd_present(*pgd)) {
>>> 如果进程0的该地址处的内核页目录也不存在,则出错
if (!pgd_present(*pgd_k))
goto bad_area_nosemaphore;
>>> 复制进程0的该地址的内核页目录到本进程的相应页目录中
set_pgd(pgd, *pgd_k);
return;
}
>>> extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
>>> {
>>> return (pmd_t *) dir;
>>> }
pmd = pmd_offset(pgd, address);
pmd_k = pmd_offset(pgd_k, address);
>>> 对中间页目录,如果是两级页表,下面的几步操作与上面的重复
if (pmd_present(*pmd) || !pmd_present(*pmd_k))
goto bad_area_nosemaphore;
set_pmd(pmd, *pmd_k);
return;
}
/*
* Switch to real mode and then execute the code
* specified by the code and length parameters.
* We assume that length will aways be less that 100!
*/
void machine_real_restart(unsigned char *code, int length)
{
。。。。。。。。。。。。。
/* Remap the kernel at virtual address zero, as well as offset zero
from the kernel segment. This assumes the kernel segment starts at
virtual address PAGE_OFFSET. */
memcpy (swapper_pg_dir, swapper_pg_dir + USER_PGD_PTRS,
sizeof (swapper_pg_dir [0]) * KERNEL_PGD_PTRS);
/* Make sure the first page is mapped to the start of physical memory.
It is normally not mapped, to trap kernel NULL pointer dereferences. */
pg0[0] = _PAGE_RW | _PAGE_PRESENT;
/*
* Use `swapper_pg_dir' as our page directory.
*/
asm volatile("movl %0,%%cr3": :"r" (__pa(swapper_pg_dir)));
[目录]
mlock代码分析
系统调用mlock的作用是屏蔽内存中某些用户进程所要求的页。
mlock调用的语法为:
int sys_mlock(unsigned long start, size_t len);
初始化为:
len=(len+(start &~PAGE_MASK)+ ~PAGE_MASK)&PAGE_MASK;
start &=PAGE_MASK;
其中mlock又调用do_mlock(),语法为:
int do_mlock(unsigned long start, size_t len,int on);
初始化为:
len=(len+~PAGE_MASK)&PAGE_MASK;
由mlock的参数可看出,mlock对由start所在页的起始地址开始,长度为len(注:len=(len+(start&~PAGE_MASK)+ ~PAGE_MASK)&PAGE_MASK)的内存区域的页进行加锁。
sys_mlock如果调用成功返回,这其中所有的包含具体内存区域的页必须是常驻内存的,或者说在调用munlock 或 munlockall之前这部分被锁住的页面必须保留在内存。当然,如果调用mlock的进程终止或者调用exec执行其他程序,则这部分被锁住的页面被释放。通过fork()调用所创建的子进程不能够继承由父进程调用mlock锁住的页面。
内存屏蔽主要有两个方面的应用:实时算法和高度机密数据的处理。实时应用要求严格的分时,比如调度,调度页面是程序执行延时的一个主要因素。保密安全软件经常处理关键字节,比如密码或者密钥等数据结构。页面调度的结果是有可能将这些重要字节写到外存(如硬盘)中去。这样一些黑客就有可能在这些安全软件删除这些在内存中的数据后还能访问部分在硬盘中的数据。 而对内存进行加锁完全可以解决上述难题。
内存加锁不使用压栈技术,即那些通过调用mlock或者mlockall被锁住多次的页面可以通过调用一次munlock或者munlockall释放相应的页面
mlock的返回值分析:若调用mlock成功,则返回0;若不成功,则返回-1,并且errno被置位,进程的地址空间保持原来的状态。返回错误代码分析如下:
ENOMEM:部分具体地址区域没有相应的进程地址空间与之对应或者超出了进程所允许的最大可锁页面。
EPERM:调用mlock的进程没有正确的优先权。只有root进程才允许锁住要求的页面。
EINVAL:输入参数len不是个合法的正数。
mlock所用到的主要数据结构和重要常量
1.mm_struct
struct mm_struct {
int count;
pgd_t * pgd; /* 进程页目录的起始地址,如图2-3所示 */
unsigned long context;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack, start_mmap;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
struct vm_area_struct * mmap; /* 指向vma双向链表的指针 */
struct vm_area_struct * mmap_avl; /* 指向vma AVL树的指针 */
struct semaphore mmap_sem;
}
start_code、end_code:进程代码段的起始地址和结束地址。
start_data、end_data:进程数据段的起始地址和结束地址。
arg_start、arg_end:调用参数区的起始地址和结束地址。
env_start、env_end:进程环境区的起始地址和结束地址。
rss:进程内容驻留在物理内存的页面总数。
2. 虚存段(vma)数据结构:vm_area_atruct
虚存段vma由数据结构vm_area_atruct(include/linux/mm.h)描述:
struct vm_area_struct {
struct mm_struct * vm_mm; /* VM area parameters */
unsigned long vm_start;
unsigned long vm_end;
pgprot_t vm_page_prot;
unsigned short 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;
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct * vm_next;
/* for areas with inode, the circular list inode->i_mmap */
/* for shm areas, the circular list of attaches */
/* otherwise unused */
struct vm_area_struct * vm_next_share;
struct vm_area_struct * vm_prev_share;
/* more */
struct vm_operations_struct * vm_ops;
unsigned long vm_offset;
struct inode * vm_inode;
unsigned long vm_pte; /* shared mem */
};
vm_start;//所对应内存区域的开始地址
vm_end; //所对应内存区域的结束地址
vm_flags; //进程对所对应内存区域的访问权限
vm_avl_height;//avl树的高度
vm_avl_left; //avl树的左儿子
vm_avl_right; //avl树的右儿子
vm_next;// 进程所使用的按地址排序的vm_area链表指针
vm_ops;//一组对内存的操作
这些对内存的操作是当对虚存进行操作的时候Linux系统必须使用的一组方法。比如说,当进程准备访问某一虚存区域但是发现此区域在物理内存不存在时(缺页中断),就激发某种对内存的操作执行正确的行为。这种操作是空页(nopage)操作。当Linux系统按需调度可执行的页面映象进入内存时就使用这种空页(nopage)操作。
当一个可执行的页面映象映射到进程的虚存地址时,一组vm_area_struct结构的数据结构(vma)就会生成。每一个vm_area_struct的数据结构(vma)代表可执行的页面映象的一部分:可执行代码,初始化数据(变量),非初始化数据等等。Linux系统可以支持大量的标准虚存操作,当vm_area_struct数据结构(vma)一被创建,它就对应于一组正确的虚存操作。
属于同一进程的vma段通过vm_next指针连接,组成链表。如图2-3所示,struct mm_struct结构的成员struct vm_area_struct * mmap 表示进程的vma链表的表头。
为了提高对vma段 查询、插入、删除操作的速度,LINUX同时维护了一个AVL(Adelson-Velskii and Landis)树。在树中,所有的vm_area_struct虚存段均有左指针vm_avl_left指向相邻的低地址虚存段,右指针vm_avl_right指向相邻的高地址虚存段,如图2-5。struct mm_struct结构的成员struct vm_area_struct * mmap_avl表示进程的AVL树的根,vm_avl_height表示AVL树的高度。
对平衡树mmap_avl的任何操作必须满足平衡树的一些规则:
Consistency and balancing rulesJ(一致性和平衡规则):
tree->vm_avl_height==1+max(heightof(tree->vm_avl_left),heightof(
tree->vm_avl_right))
abs( heightof(tree->vm_avl_left) - heightof(tree->vm_avl_right) ) <= 1
foreach node in tree->vm_avl_left: node->vm_avl_key <= tree->vm_avl_key, foreach node in tree->vm_avl_right: node->vm_avl_key >= tree->vm_avl_key.
注:其中node->vm_avl_key= node->vm_end
对vma可以进行加锁、加保护、共享和动态扩展等操作。
3.重要常量
mlock系统调用所用到的重要常量有:PAGE_MASK、PAGE_SIZE、PAGE_SHIFT、RLIMIT_MEMLOCK、VM_LOCKED、 PF_SUPERPRIV等。它们的值分别如下:
PAGE_SHIFT 12 // PAGE_SHIFT determines the page size
PAGE_SIZE 0x1000 //1UL<
RLIMIT_MEMLOCK 8 //max locked-in-memory address space
VM_LOCKED 0x2000 //8*1024=8192, vm_flags
PF_SUPERPRIV 0x00000100 //512,
mlock系统调用代码函数功能分析
下面对各个函数的功能作详细的分析((1)和(2)在前面简介mlock时已介绍过,并在后面有详细的程序流程):
suser():如果用户有效(即current->euid == 0 ),则设置进程标志为root优先权(current->flags |= PF_SUPERPRIV),并返回1;否则返回0。
find_vma(struct mm_struct * mm, unsigned long addr):输入参数为当前进程的mm、需要加锁的开始内存地址addr。find_vma的功能是在mm的mmap_avl树中寻找第一个满足mm->mmap_avl->vm_start<=addr< mm->mmap_avl->vm_end的vma,如果成功则返回此vma;否则返回空null。
mlock_fixup(struct vm_area_struct * vma, unsigned long start, unsigned long end, unsigned int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。
merge_segments(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr):输入参数为当前进程的mm、需要加锁的开始内存地址start_addr和结束地址end_addr。merge_segments的功能的是尽最大可能归并相邻(即内存地址偏移量连续)并有相同属性(包括vm_inode,vm_pte,vm_ops,vm_flags)的内存段,在这过程中冗余的vm_area_structs被释放,这就要求vm_mmap链按地址大小排序(我们不需要遍历整个表,而只需要遍历那些交叉或者相隔一定连续区域的邻接vm_area_structs)。当然在缺省的情况下,merge_segments是对vm_mmap_avl树进行循环处理,有多少可以合并的段就合并多少。
mlock_fixup_all(struct vm_area_struct * vma, int newflags):输入参数为vm_mmap链中的某个vma、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_all的功能是根据输入参数newflags修改此vma的vm_flags。
mlock_fixup_start(struct vm_area_struct * vma,unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_start的功能是根据输入参数end,在内存中分配一个新的new_vma,把原来的vma分成两个部分: new_vma和vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址(new_vma->start和new_vma->end)大小序列把新生成的new->vma插入到当前进程mm的mmap链或mmap_avl树中(缺省情况下是插入到mmap_avl树中)。
注:vma->vm_offset+= vma->vm_start-new_vma->vm_start;
mlock_fixup_end(struct vm_area_struct * vma,unsigned long start, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_end的功能是根据输入参数start,在内存中分配一个新的new_vma,把原来的vma分成两个部分:vma和new_vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把new->vma插入到当前进程mm的mmap链或mmap_avl树中。
注:new_vma->vm_offset= vma->vm_offset+(new_vma->vm_start-vma->vm_start);
mlock_fixup_middle(struct vm_area_struct * vma,unsigned long start, unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_middle的功能是根据输入参数start、end,在内存中分配两个新vma,把原来的vma分成三个部分:left_vma、vma和right_vma,其中vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把left->vma和right->vma插入到当前进程mm的mmap链或mmap_avl树中。
注:vma->vm_offset += vma->vm_start-left_vma->vm_start;
right_vma->vm_offset += right_vma->vm_start-left_vma->vm_start;
kmalloc():将在后面3.3中有详细讨论。
insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp):输入参数为当前进程的mm、需要插入的vmp。insert_vm_struct的功能是按地址大小序列把vmp插入到当前进程mm的mmap链或mmap_avl树中,并且把vmp插入到vmp->inode的i_mmap环(循环共享链)中。
avl_insert_neighbours(struct vm_area_struct * new_node,** ptree,** to_the_left,** to_the_right):输入参数为当前需要插入的新vma结点new_node、目标mmap_avl树ptree、新结点插入ptree后它左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->vma_end大小排序)。avl_insert_neighbours的功能是插入新vma结点new_node到目标mmap_avl树ptree中,并且调用avl_rebalance以保持ptree的平衡树特性,最后返回new_node左边的结点以及它右边的结点。
avl_rebalance(struct vm_area_struct *** nodeplaces_ptr, int count):输入参数为指向vm_area_struct指针结构的指针数据nodeplaces_ptr[](每个元素表示需要平衡的mmap_avl子树)、数据元素个数count。avl_rebalance的功能是从nodeplaces_ptr[--count]开始直到nodeplaces_ptr[0]循环平衡各个mmap_avl子树,最终使整个mmap_avl树平衡。
down(struct semaphore * sem):输入参数为同步(进入临界区)信号量sem。down的功能根据当前信号量的设置情况加锁(阻止别的进程进入临界区)并继续执行或进入等待状态(等待别的进程执行完成退出临界区并释放锁)。
down定义在/include/linux/sched.h中:
extern inline void down(struct semaphore * sem)
{
if (sem->count <= 0)
__down(sem);
sem->count--;
}
up(struct semaphore * sem)输入参数为同步(进入临界区)信号量sem。up的功能根据当前信号量的设置情况(当信号量的值为负数:表示有某个进程在等待使用此临界区 )释放锁。
up定义在/include/linux/sched.h中:
extern inline void up(struct semaphore * sem)
{
sem->count++;
wake_up(&sem->wait);
}
kfree_s(a,b):kfree_s定义在/include/linux/malloc.h中:#define kfree_s(a,b) kfree(a)。而kfree()将在后面3.3中详细讨论。
avl_neighbours(struct vm_area_struct * node,* tree,** to_the_left,** to_the_right):输入参数为作为查找条件的vma结点node、目标mmap_avl树tree、node左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->vma_end大小排序)。avl_ neighbours的功能是根据查找条件node在目标mmap_avl树ptree中找到node左边的结点以及它右边的结点,并返回。
avl_remove(struct vm_area_struct * node_to_delete, ** ptree):输入参数为需要删除的结点node_to_delete和目标mmap_avl树ptree。avl_remove的功能是在目标mmap_avl树ptree中找到结点node_to_delete并把它从平衡树中删除,并且调用avl_rebalance以保持ptree的平衡树特性。
remove_shared_vm_struct(struct vm_area_struct *mpnt):输入参数为需要从inode->immap环中删除的vma结点mpnt。remove_shared_vm_struct的功能是从拥有vma结点mpnt 的inode->immap环中删除的该结点。
[目录]
memory.c
Memory.c中,Linux提供了对虚拟内存操作的若干函数,其中包括对虚拟页的复制、新建页表、清除页表、处理缺页中断等等。
[目录]
copy_page
1.static inline void copy_page(unsigned long from, unsigned long to)
为了节约内存的使用,在系统中,各进程通常采用共享内存,即不同的进程可以共享同一段代码段或数据段。当某一进程发生对共享的内存发生写操作时,为了不影响其它进程的正常运行,系统将把该内存块复制一份,供需要写操作的进程使用,这就是所谓的copy-on-write机制。copy_page就是提供复制内存功能的函数,它调用C语言中标准的内存操作函数,将首地址为from的一块虚拟内存页复制到首地址为to的空间中。
[目录]
clear_page_tables
2、void clear_page_tables(struct task_struct * tsk)
clear_page_table的功能是将传入的结构tsk中的pgd页表中的所有项都清零,同时将二级页表所占的空间都释放掉。传入clear_page_tables的是当前进程的tsk结构,取得该进程的一级页目录指针pgd后,采用循环的方式,调用free_one_pgd清除pgd表。表共1024项。在free_one_pgd中,实际执行的功能只调用一次free_one_pmd(在80x86中,由于硬件的限制,只有两级地址映射,故将pmd与pgd合并在一起)。在free_one_pmd中,函数调用pte_free将对应于pmd的二级页表所占的物理空间释放掉(进程代码、数据所用的物理内存在do_munmap释放掉了)并将pmd赋值为零。
clear_page_table在系统启动一个可执行文件的映象或载入一个动态链接库时被调用。在fs/exec.c中的do_load_elf_binary()或do_load_aout_binary()调用flash_old_exec,后者调用exec_mmap,而exec_mmap调用clear_page_table。其主要功能是当启动一个新的应用程序的时候,将复制的mm_struct中的页表清除干净,并释放掉原有的所有二级页表空间。
[目录]
oom
3、void oom(struct task_struct * task)
返回出错信息。
[目录]
free_page_tables
4、void free_page_tables(struct mm_struct * mm)
在free_page_table中,大部分的代码与clear_page_table中的函数一致。所不同的是,该函数在最后调用了pgd_free(page_dir),即不光释放掉二级页表所占的空间,同时还释放一级页目录所占的空间。这是因为free_page_tables被__exit_mm调用,__exit_mm又被do_exit (kernel/kernel.c)调用。当进程中止、系统退出或系统重起时都需要用do_exit(属于进程管理)将所有的进程结束掉。在结束进程过程中 ,将调用free_page_table将进程的空间全部释放掉,当然包括释放进程一级页目录所占的空间。
[目录]
new_page_tables
5、int new_page_tables(struct task_struct * tsk)
该函数的主要功能是建立新的页目录表,它的主要流程如如下:
·调用pgd_alloc()为新的页目录表申请一片4K空间 。
·将初始化进程的内存结构中从768项开始到1023项的内容复制给新的页表(所有的进程都共用虚拟空间中 3G~4G的内存,即在核心态时可以访问所有相同的存储空间)。
·调用宏SET_PAGE_DIR(include/asm/pgtable.h)将进程控制块tsk->ts->CR3的值改为新的页目录表的首地址,同时将CPU中的CR3寄存器的值改为新的页目录表的首地址,从而使新进程进入自己的运行空间。
·将tsk->mm->pgd改为新的页目录表的首地址。
·new_page_tables被copy_mm调用,而copy_mm被copy_mm_do_fork调用,这两个函数都在kernel/fork.c中。同时,new_page_tables也可以在exec_mmap(fs/exec.c)中调用。即新的进程的产生可以通过两种途径,一种是fork,在程序中动态地生成新的进程,这样新进程的页表原始信息利用copy_mm从其父进程中继承而得,另一种是运行一个可执行文件映象,通过文件系统中的exec.c,将映象复制到tsk结构中。两种方法都需要调用new_page_tables为新进程分配页目录表。
[目录]
copy_one_pte
6、static inline void copy_one_pte(pte_t * old_pte, pte_t * new_pte, int cow)
将原pte页表项复制到new_pte上,其流程如下:
·检测old_pte是否在内存中,如不在物理内存中,调用swap_duplicate按old_pte在swap file中的入口地址,将old_pte复制到内存中,同时把old_pte的入口地址赋给new_pte并返回。反之转向3。
获取old_pte对应的物理地址的页号。
·根据页号判断old_pte是否为系统保留的,如果为系统保留的,这些页为所有的进程在核心态下使用,用户进程没有写的权利,则只需将old_pte指针直接转赋给new_pte后返回。反之则该pte属于普通内存的,则转向4。
·根据传入的C-O-W标志,为old_pte置写保护标志,如果该页是从swap_cache中得来的,将old_pte页置上“dirty”标志。将old_pte赋值给new_pte。
·将mem_map结构中关于物理内存使用进程的个数的数值count加1。
[目录]
copy_pte_range
7、static inline int copy_pte_range(pmd_t *dst_pmd, pmd_t *src_pmd,
unsigned long address, unsigned long size, int cow)
通过循环调用copy_one_pte将从源src_pmd中以地址address开始的长度为size的空间复制给dst_pmd中。如dst_pmd中还未分配地址为address的页表项,则先给三级页表pte表分配4K空间。(每调用一次copy_one_pte复制4K空间。在一次copy_pte_range中最多可复制4M空间)。
[目录]
copy_pmd_range
8、static inline int copy_pmd_range(pgd_t *dst_pgd, pgd_t *src_pgd,
unsigned long address, unsigned long size, int cow)
通过循环调用copy_pte_range将从源src_pgd中以地址address开始的长度为size的空间复制给dst_pgd中。如dst_pgd中还未分配地址为address的页表项,则在一级(同时也是二级)页表中给对应的pmd分配目录项。
[目录]
copy_page_range
9、int copy_page_range(struct mm_struct *dst, struct mm_struct *src,
struct vm_area_struct *vma)
该函数的主要功能是将某个任务或进程的vma块复制给另一个任务或进程。其工作机制是循环调用copy_pmd_range,将vma块中的所有虚拟空间复制到对应的虚拟空间中。在做复制之前,必须确保新任务对应的被复制的虚拟空间中必须都为零。copy_page_range按dup_mmap()->copy_mm()->do_fork()的顺序被调用(以上三个函数均在kernel/fork.c中)。当进程被创建的时候,需要从父进程处复制所有的虚拟空间,copy_page_range完成的就是这个任务。
[目录]
free_pte
9、static inline void free_pte(pte_t page)
虚存页page如在内存中,且不为系统的保留内存,调用free_page将其释放掉(如在系统保留区中,则为全系统共享,故不能删除)。
如page在swap file中,调用swap_free()将其释放。
[目录]
forget_pte
10、static inline void forget_pte(pte_t page)
如page不为空,调用free_pte将其释放。
[目录]
zap_pte_range
11、static inline void zap_pte_range(pmd_t * pmd, unsigned long address,
unsigned long size)
zap为zero all pages的缩写。该函数的作用是将在pmd中从虚拟地址address开始,长度为size的内存块通过循环调用pte_clear将其页表项清零,调用free_pte将所含空间中的物理内存或交换空间中的虚存页释放掉。在释放之前,必须检查从address开始长度为size的内存块有无越过PMD_SIZE.(溢出则可使指针逃出0~1023的区间)。
[目录]
zap_pmd_range
12、static inline void zap_pmd_range(pgd_t * dir, unsigned long address, unsigned long size)
函数结构与zap_pte_range类似,通过调用zap_pte_range完成对所有落在address到address+size区间中的所有pte的清零工作。zap_pmd_range至多清除4M空间的物理内存。
[目录]
zap_page_range
13、int zap_page_range(struct mm_struct *mm, unsigned long address, unsigned long size)
函数结构与前两个函数类似。将任务从address开始到address+size长度内的所有对应的pmd都清零。zap_page_range的主要功能是在进行内存收缩、释放内存、退出虚存映射或移动页表的过程中,将不在使用的物理内存从进程的三级页表中清除。(在讨论clear_page_tables时,就提到过当进程退出时,释放页表之前,先保证将页表对应项清零,保证在处于退出状态时,进程不占用0~3G的空间。)
[目录]
zeromap_pte_range等
14、static inline void zeromap_pte_range(pte_t * pte, unsigned long address,
unsigned long size, pte_t zero_pte)
15、static inline int zeromap_pmd_range(pmd_t * pmd, unsigned long address,
unsigned long size, pte_t zero_pte)
16、int zeromap_page_range(unsigned long address, unsigned long size, pgprot_t prot)
这三个函数与前面的三个函数从结构上看很相似,他们的功能是将虚拟空间中从地址address开始,长度为size的内存块所对应的物理内存都释放掉,同时将指向这些区域的pte都指向系统中专门开出的长度为4K,全为0的物理页。zeromap_page_range在kernel代码中没有被引用,这个函数是旧版本的Linux遗留下来的,在新版本中已经被zap_page_range所替代。
[目录]
remap_pte_range等
17、static inline void remap_pte_range(pte_t * pte, unsigned long address,
unsigned long size, unsigned long offset, pgprot_t prot)
18、static inline int remap_pmd_range(pmd_t * pmd, unsigned long address,
unsigned long size, unsigned long offset, pgprot_t prot)
19、int remap_page_range(unsigned long from, unsigned long offset, unsigned long size,
pgprot_t prot)
这三个函数也同前面的函数一样,层层调用,现仅介绍一下最后一个函数的作用。remap_page_range的功能是将原先被映射到虚拟内存地址from处的,大小为size的虚拟内存块映射到以偏移量offset为起始地址的虚拟内存中,同时将原来的pte、pmd项都清零。该函数也是逐级调用,在remap_pte_range中,通过set_pte将的物理页映射到新的虚拟内存页表项pte上。remap_page_range函数的功能与下文中的remap.c中介绍的功能相近,因此在kernel中也没有用到。
[目录]
put_dirty_page
20、unsigned long put_dirty_page(struct task_struct * tsk, unsigned long page,
unsigned long address)
将虚拟内存页page链接到任务tsk中虚拟地址为address的虚拟内存中,其主要调用的流程如下:put_dirty_page->setup_arg_page->do_load_xxx_binary(xxx为aout或elf,这些函数都在fs/exec.c中),它的功能是将在载入可执行文件的时候,将其相关的堆栈信息、环境变量等复制到当前进程的空间上。
[目录]
handle_mm_fault
21、void handle_mm_fault(struct vm_area_struct * vma, unsigned long address,
int write_access)
用于处理ALPHA机中的缺页中断
[目录]
mmap.c
在mmap.c中,主要提供了对进程内存管理进行支持的函数,主要包括了do_mmap、do_munmap等对进程的虚拟块堆avl数进行管理的函数。
有关avl树的一些操作:
1、static inline void avl_neighbours (struct vm_area_struct * node, struct vm_area_struct * tree, struct vm_area_struct ** to_the_left, struct vm_area_struct ** to_the_right)
寻找avl树tree中的节点node的前序节点和后序节点,将结果放在指针to_the_left和to_the_right中,即使得*to_the_left->next=node,node->next=*to_the_right。在实际搜索中,过程是找到node节点中的左节点的最右节点和右节点的最左节点,采用avl树搜索可以提高效率。
2、static inline void avl_rebalance (struct vm_area_struct *** nodeplaces_ptr, int count)
将由于插入操作或删除操作而造成不平衡的avl树恢复成平衡状态。nodeplaces_ptr是指向的是需要调整的子树的根节点,count是该子树的高度。
static inline void avl_insert (struct vm_area_struct * new_node,
struct vm_area_struct ** ptree)
将新节点new_node插入avl树ptree中,并将该树重新生成平衡avl树。在创建avl树时,将vma模块不断的插入avl树中,构建一个大的avl树。当进程创建时,复制父进程后需要将以双向链表拷贝过来的vma链生成avl树。
4、static inline void avl_insert_neighbours (struct vm_area_struct * new_node, struct vm_area_struct ** ptree, struct vm_area_struct ** to_the_left, struct vm_area_struct ** to_the_right)
将新节点new_node插入avl树ptree中,并将该树重新生成平衡avl树,同时返回该新节点的前序节点和后序节点。
5、static inline void avl_remove (struct vm_area_struct * node_to_delete, struct vm_area_struct ** ptree)
将指定要删除的节点node_to_delete从avl树ptree中删除。并将该树重新生成平衡avl树。该函数在释放虚存空间和归并vma链表是使用。
7、static void printk_list (struct vm_area_struct * vma)
8、static void printk_avl (struct vm_area_struct * tree)
9、static void avl_checkheights (struct vm_area_struct * tree)
10、static void avl_checkleft (struct vm_area_struct * tree, vm_avl_key_t key)
11、static void avl_checkright (struct vm_area_struct * tree, vm_avl_key_t key)
12、static void avl_checkorder (struct vm_area_struct * tree)
13、static void avl_check (struct task_struct * task, char *caller)
这些函数都是系统调试时用以检测avl树结构的正确性
14、static inline int vm_enough_memory(long pages)
通过计算当前系统中所剩的空间判断是否足够调用。可使用的内存包括缓冲存储器、页缓存、主存中的空闲页、swap缓存等。
15、static inline unsigned long vm_flags(unsigned long prot, unsigned long flags)
提供宏功能将页的保护位和标志位合并起来。
16、unsigned long get_unmapped_area(unsigned long addr, unsigned long len)
从虚拟内存address开始找到未分配的连续空间大于len的虚拟空间块,并将该快的首地址返回。
17、unsigned long do_mmap(struct file * file, unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags, unsigned long off)
do_mmap在Linux虚拟内存管理中是一个很重要的函数,它的主要功能是将可执行文件的映象映射到虚拟内存中,或将别的进程中的内存信息映射到该进程的虚拟空间中。并将映射了的虚拟块的vma加入到该进程的vma avl树中。其运行的流程如下,更详细的分析请参阅林涛同学和徐玫峰同学的报告。
检验给定的映射长度len是大于1页,小于一个任务的最大长度3G且加上进程的加上偏移量off不会溢出。如不满足则退出。
如果当前任务的内存是上锁的,检验加上len后是否会超过当前进程上锁长度的界限。如是则退出。
如果从文件映射,检验文件是否有读的权限。如无在退出。
调用get_unmaped取得从地址address开始未映射的连续虚拟空间大于len的虚存块。
如从文件映射,保证该文件控制块有相应的映射操作。
为映射组织该区域申请vma结构。
调用vm_enough_memory有足够的内存。如无则释放6中申请的vma,退出。
如果是文件映射,调用file->f_op_mmap将该文件映射如vma中。
调用insert_vm_struct将vma插入该进程的avl树中。
归并该avl树。
18、void merge_segments (struct mm_struct * mm, unsigned long start_addr,
unsigned long end_addr)
经过对进程虚拟空间不断的映射,在进程中的vma块有许多是可以合并的,为了提高avl树查找的效率,减少avl树中不必要的vma块,通常需要将这些块和并,merge_segments的功能为合并虚拟空间中从start_addr到end_addr中类型相同,首尾相连的vma块。由于只有经过增加操作采有可能合并,所有merge_segments只在do_mmap和unmap_fixup中被调用。该函数的流程如下:
根据起始地址start_addr从找到第一块满足vm_end>start_addr的vma块mpnt。
调用avl_neighbours找到在vma双向链表上与mpnt前后相连的vma块prev和next。
如果prev和mpnt首尾相连,且有同样在swap file中的节点,同样的标志,同样的操作等则将其合并,反之转向6。
调用avl_remove将mpnt从avl树中删除,调整prev的结束地址和后序指针。
将结构mpnt所占的物理空间删除。
prev、mpnt、next依次下移,如未超过end_addr则返回3。
19、static void unmap_fixup(struct vm_area_struct *area, unsigned long addr, size_t len)
释放虚拟空间中的某些区域的时候,将会出现四种情况:
将整个vma释放掉
将vma的前半部分释放掉
将vma的后半部分释放掉
将vma的中间部分释放掉
为了正常维护vma树,当第一种情况是,将整个vma释放掉。同时释放vma结构所占的空间。第二种,释放后半部分,修改vma的相关信息。第二种,释放前半部分,修改vma的相关信息。第四种,由于在vma中出现了一个洞,则需增加一个vma结构描述新出现的vma块。unmap_fixup所执行的工作就是当释放空间时,修正对vma树的影响。
20、int do_munmap(unsigned long addr, size_t len)
do_munmap将释放落在从地址addr开始,长度为len空间内的vma所对应的虚拟空间。do_munmap被系统调用sys_munmap所调用(对sys_munmap如何工作的不甚了解)。下面是该函数的流程:
通过find_vma根据addr找到第一块vma->end>addr的vma块mpnt。
调用avl_neighbours找到mpnt在链表中的相邻指针prev和next。
将检查中所有与虚拟空间addr~addr+len相交的vma块放入free链表中。同时如果该vma链接在共享内存中,则将其从该环形链表中释放出来。
按序搜索free链表,调用unmap_fixup释放空间。
调用zap_page_range将指向释放掉的虚拟空间中的pte页表项清零。
调用kfree