Understand Swap in & mem_map
2010年10月19日
Understand Swap in & mem_map
mm/memory.c PageReserved
这1000多行代码,有绝大多数函数定义为static,意味着这个模块只提供了少量
的对外接口. 这几个接口是我们理解此模块的一个重要线索.
第一部分 :全局量的仔细解释
I)首先是几个全局变量,下面给出其具体含义:
+================================================= =====================
/*mem_map 中最高地址页面的map number,可以使用的ram全在其中*/
unsigned long max_mapnr;
/*物理内存页面总数(当然包括highmem在内)*/
unsigned long num_physpages;
/*从虚拟地址 high_memory 开始不再是线性映射区
*如果全部可以线性映射其值为物理内存顶端之虚拟地址
*/
void * high_memory;
struct page *highmem_start_page; /*高端物理内存的起始 page(struct page)*/
+================================================= =====================
II) i386下的 mem_map
另外一个重要的全局数组:
==========================================
mem_map_t * mem_map; /*page 结构的总数组*/
==========================================
这个数组存放有全部的struct page结构(i386 not numa),每一个物理内存页面
都对应一个page 机构,存储于此数组.下面来看看i386体系下如何初始化mem_map:
setup.S->asmlinkage void __init start_kernel(void) (init/main.c)
|
+-->setup_arch ---> 处理e820内存报告
--> 关于内存的提示信息
---> 初始化bootmem (init_bootmem)
---> paging_init
+
| +--> pagetable_init(含fix map,vmalloc init)
\ / +--> load cr3
. +--> kmap_init
. +--> free_area_init(zone-buddy初始化)
函数paging_init如此调用free_area_init(zones_size);参数zones_size给出
每一个zone(DMA,NORMAL,HIGH)的大小.再看:
void __init free_area_init(unsigned long *zones_size)
{
free_area_init_core(0, &contig_page_data, &mem_map, zones_size,
0, 0, 0);
}
对于i386此种"连续"内存结构,使用了一个pg_data_t类型的变量:contig_page_data
注意第三个参数是&mem_map.
void __init free_area_init_core(int nid, pg_data_t *pgdat,
struct page **gmap, unsigned long *zones_size ,
unsigned long zone_start_paddr, unsigned long *zholes_size,
struct page *lmem_map)
{
/* pgdat 是contig_page_data, gmap是&mem_map, lmem_map是 0 */
.....
if (lmem_map == (struct page *)0) {
//非numa体系此值为0, 分配mem map数组
lmem_map = (struct page *) alloc_bootmem_node(pgdat, map_size);
lmem_map = (struct page *)(PAGE_OFFSET +
MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET));
}
/*mem_map指向刚分配的内存,即是contig_page_data.node_mem_map*/
*gmap = pgdat->node_mem_map = lmem_map;
pgdat->node_size = totalpages;
pgdat->node_start_paddr = zone_start_paddr;
pgdat->node_start_mapnr = (lmem_map - mem_map);
/*从0号物理内存页开始*/
}
NUMA下的 mem_map
III)NUMA下的 mem_map
而对于NUMA类型的体系,mem_map已经失去了其存在的意义,不过2.4处理的
不够好,代码凌乱,可读性不强.以arm体系为例:(arch/arm/mm/init.c)
void __init paging_init(struct meminfo *mi, struct machine_desc *mdesc)
{
.........
for (node = 0; node node_boot_start, zhole_size);
}
......
}
这里调用的free_area_init_node定义于mm/numa.c
void __init free_area_init_node(int nid, pg_data_t *pgdat, struct page *pmap,
unsigned long *zones_size, unsigned long zone_start_paddr,
unsigned long *zholes_size)
{
int i, size = 0;
struct page *discard;
if (mem_map == (mem_map_t *)NULL)
mem_map = (mem_map_t *)PAGE_OFFSET; /*仅为mem_map指定了一个值,其指向
*的地址并没有存放struct page
*/
free_area_init_core(nid, pgdat, &discard, zones_size, zone_start_paddr,
zholes_size, pmap);
pgdat->node_id = nid;
.......
}
void __init free_area_init_core(int nid, pg_data_t *pgdat,
struct page **gmap, unsigned long *zones_size ,
unsigned long zone_start_paddr, unsigned long *zholes_size,
struct page *lmem_map)
{
/* pgdat 是NODE_pgdat, gmap是&discard, lmem_map还是 0 */
.....
if (lmem_map == (struct page *)0) {
//numa体系此值亦为0, 分配mem map数组
lmem_map = (struct page *) alloc_bootmem_node(pgdat, map_size);
lmem_map = (struct page *)(PAGE_OFFSET +
MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET));
}
/* *gmap再numa下不指向mem_map,被丢弃不用*/
*gmap = pgdat->node_mem_map = lmem_map;
pgdat->node_size = totalpages;
pgdat->node_start_paddr = zone_start_paddr;
pgdat->node_start_mapnr = (lmem_map - mem_map);
/*node_start_mapnr再numa下失去其意义*/
/*变成了lmem_map这个地址到PAGE_OFFSET有多少个页面*/
offset = lmem_map - mem_map; /*减去常量PAGE_OFFSET*/
for (j = 0; j offset = offset; /*失去意义*/
....
zone->zone_mem_map = mem_map + offset; /*又加上常量PAGE_OFFSET所以
*zone_mem_map确实有意义
*/
zone->zone_start_mapnr = offset; /*失去意义*/
...
offset += size;
}
}
从上面的分析,很明显2.4还没有处理好NUMA和discon mem.在NUMA体系下,mem_map
已经失去存在的意义,只是一个跳板.arm的vir_to_page宏就没有使用这个mem_map.
而ia64-sn的使用方法就巨绕:
*******include/asm-ia64/page.h 定义
# define virt_to_page(kaddr) (mem_map + platform_map_nr(kaddr))
mem_map是一个常量PAGE_OFFSET???
*******include/asm-ia64/machvec_sn1.h定义
#define platform_map_nr sn1_map_nr
*******arch/ia64/sn/sn1/setup.c定义
sn1_map_nr (unsigned long addr)
{
#ifdef CONFIG_DISCONTIGMEM
return MAP_NR_SN1(addr);
#else
return MAP_NR_DENSE(addr);
#endif
}
*******include/asm-ia64/sn/mmzone.h定义
#define NODE_DATA(n) (&((plat_node_data + (n))->gendata))
#define NODE_MEM_MAP(nid) (NODE_DATA((nid))->node_mem_map)
#define LOCAL_MAP_NR(kvaddr) \
(((unsigned long)(kvaddr)-LOCAL_BASE_ADDR((kvaddr))) >> PAGE_SHIFT)
#define MAP_NR_SN1(kaddr) (LOCAL_MAP_NR((kaddr)) + \
(((unsigned long)ADDR_TO_MAPBASE((kaddr)) - PAGE_OFFSET) / \
sizeof(mem_map_t)))
仔细分析MAP_NR_SN1就知道其种缘由了:
mem_map + (((unsigned long)ADDR_TO_MAPBASE((kaddr)) - PAGE_OFFSET) / \
sizeof(mem_map_t))
就定位到了pgdat->node_mem_map,而node_mem_map了在numa中依然有效,指向真
实的"mem_map".
pgdat->node_start_mapnr也失去了意义,不过在kernel2.4中大概只有mips64使
用这个值,并且类似ia64的vir_to_page,见include/asm-mips64/pgtable.h
#define mips64_pte_pagenr(x) \
(PLAT_NODE_DATA_STARTNR(PHYSADDR_TO_NID(pte_val(x) )) + \
PLAT_NODE_DATA_LOCALNR(pte_val(x), PHYSADDR_TO_NID(pte_val(x))))
#define pte_page(x) (mem_map+mips64_pte_pagenr(x))
使用这个变量的时候最终还是加上了mem_map!!!
不过从另外一个角度理解mem_map也可以:这紧紧是一个虚拟的值,借助于这个变
量也简化了一些设计,比如使用zone->offset的地方,如:mm/page_alloc.c
static void __free_pages_ok (struct page *page, unsigned long order)
{
.........
base = mem_map + zone->offset;
page_idx = page - base;
....
}
本来zone->offset减去了mem_map,现在加上mem_map,还是指向这个zone所属的
pgdat->zone_mem_map. 虽然失去其本意,也达到了目的,必是2.4向NUMA转化的过
渡性代码.
在kernel2.6,numa体系下,根本不再有mem_map这个全局变量,
pgdat->node_start_mapnr也时刻保持其原意.zone->offset更是消失不见了,直接
换成了指针.一切都变得clean&clear.
注:看64bit代码时突然想到两个64bit值之差赋值给long型的变量是否有问题,后来
参考了:
>
指出在linux的64bit环境中, long是64bit的!!!和windows不同啊.
Page Reserved详解
第二部分 本模块对外接口详解
I)clear_page_tables : 释放一个进程指定范围内的页表,以及关联的pmd,对应
的pgd entry被清空,pgd本身没有被释放.至于页表中是否还有未断开
影射的物理页面,此函数未做检查.(所以调用之前要先调用
zap_page_range). 对于这个函数和其涉及的其他函数请自己阅读.
II)copy_page_range
从一个进程的mm拷贝指定的vm area到另一个进程,拷贝内容包括:为新进程指
定的这段空间分配pmd页面,分配page table页面.从src进程的mm拷贝pte到新分配
的页表.增加copy的pte所引用页面的引用计数,并且处理pte是swap entry的情况(
增加swap page的引用计数). 对于vm area属性是可写但无VM_SHARE属性的情况,将
新进程的pte置写保护,等到新进程写此页面的时候进行COW. 如果vm area具有共享
属性(那当然就是容许在进程间共享此页面)将新设置的pte的dirty位清除:新进程
还未曾访问此页面.
通过上面的分析阅读copy_page_range应该不是问题了.注意在copy_page_range
的时候就为COW设置了伏笔:共享一个page,但是不容许写.
III)zap_page_range:
从一个进程指定的地址开始释放指定大小的虚拟地址空间内
所有已映射页面. clear_page_tables仅释放"cpu meta data",而此函数
仅释放"user used data pages".如果页面被交换到磁盘,则释放磁盘页面.
此函数相关子函数:
zap_pmd_range zap_pte_range free_pte.
都是对mm进行遍历,只有free_pte还有些"业务"逻辑.
static inline int free_pte(pte_t pte)
{
if (pte_present(pte)) {
struct page *page = pte_page(pte);
if ((!VALID_PAGE(page)) || PageReserved(page))
return 0;
/*
* free_page() used to be able to clear swap cache
* entries. We may now have to do it manually.
*/
if (pte_dirty(pte) && page->mapping)
set_page_dirty(page);
free_page_and_swap_cache(page);
return 1;
}
swap_free(pte_to_swp_entry(pte));
return 0;
}
PageReserved检查page是否就有PG_reserved属性.
到底什么是PG_reserved页面
这里说明一下: 其实非常简单,内核不认为PG_reserved属性的页面是"ram"页
面.从而不会在"任何"地方使用这个物理页面. 这里的任何地方不是指真正的所有
的内核程序,而是指不知道PG_reserved的页面是什么页面的代码.
从另外一种意义上说,就是内核的某个地方保留了这个page,不得再做他用.
首先, 参考arch/i386/mm/init.c的分析,函数mem_init,使用函数free_all_bootmem
清除所有未分配出去的页面的PG_reserved位.仔细阅读一下函数对bootmem的初始
化就可以发现:所有非ram页面,在bootmem中已经标记为已分配,PG_reserved置位.
所以free_all_bootmem也没有将非ram页面的PG_reserved标识清除掉.而对于high
mem, mem_init 将非ram页面的PG_reserved位设置上.这就是PG_reserved的第一
个含义:非ram页面.
然后看setup_arch对reserve_bootmem的调用,可以知道,(0, PAGE_SIZE)被保
留,这样不会有"任何"代码可以分配这段内存. 这就是第二个含义:被内核保留用于
特殊用途.
关于zap_page_range不再做其他说明.
user kiobuf 和 remap, zeromap
IV)user kiobuf
在2.6内核,对应的函数叫做get_user_pages,用于AIO.这里的user kiobuf提供
这样一种服务:
从内核读取数据直接映射到用户空间,减少了copy,可以"大大的"提高效率.不过
看来2.4想用于raw io.到时候再仔细分析,这里先看看user kiobuf是如何建立和使
用的.
int map_user_kiobuf(int rw, struct kiobuf *iobuf, unsigned long va, size_t len)
{
.....
int datain = (rw == READ);
/* Make sure the iobuf is not already mapped somewhere. */
if (iobuf->nr_pages)
return -EINVAL;
mm = current->mm;
dprintk ("map_user_kiobuf: begin\n");
//准备扫描指定的区间(遍历ptr到end之间涉及的vma)
ptr = va & PAGE_MASK;
end = (va + len + PAGE_SIZE - 1) & PAGE_MASK;
err = expand_kiobuf(iobuf, (end - ptr) >> PAGE_SHIFT);
if (err)
return err;
down(&mm->mmap_sem);
iobuf->locked = 0;
iobuf->offset = va & ~PAGE_MASK;
iobuf->length = len;
i = 0;
/*
* First of all, try to fault in all of the necessary pages
*/
while (ptr = vma->vm_end) {
/*指定的虚拟地址va必须有一个vam区间对应*/
vma = find_vma(current->mm, ptr);
if (!vma)
goto out_unlock;
if (vma->vm_start > ptr) {
/*必须使用堆栈(fix me)做io buf*/
if (!(vma->vm_flags & VM_GROWSDOWN))
goto out_unlock;
/*先对vma进行调整*/
if (expand_stack(vma, ptr))
goto out_unlock;
}
/*检查一下访问权限*/
if (((datain) && (!(vma->vm_flags & VM_WRITE))) ||
(!(vma->vm_flags & VM_READ))) {
err = -EACCES;
goto out_unlock;
}
}
/*为涉及到的空间影射物理内存页面*/
if (handle_mm_fault(current->mm, vma, ptr, datain) page_table_lock);
/*记录页面的page 结构到 iobuf中,以便后用*/
map = follow_page(ptr);
if (!map) {
spin_unlock(&mm->page_table_lock);
dprintk (KERN_ERR "Missing page in map_user_kiobuf\n");
goto out_unlock;
}
map = get_page_map(map);
if (map) {
flush_dcache_page(map);
/*增加引用计数,以免被swap out对应unmap_kiobuf*/
atomic_inc(&map->count);
} else
printk (KERN_INFO "Mapped page missing [%d]\n", i);
spin_unlock(&mm->page_table_lock);
iobuf->maplist[i] = map;
iobuf->nr_pages = ++i;
ptr += PAGE_SIZE;
}
up(&mm->mmap_sem);
.......
}
与此相关的其他几个函数unmap_kiobuf,lock_kiovec,unlock_kiovec个人认为
极为简单,就不再分析.
V)zeromap_page_range remap_page_range
int zeromap_page_range(unsigned long address, unsigned long size,
pgprot_t prot)
相关函数zeromap_pmd_range,zeromap_pte_range,forget_pte.是一个遍历
当前进程的页面映射表的函数.将指定地址空间的每一项pte都指向zero page页
(一个4k的全0页面),并将pte置写保护,以后可以进行COW.
如果某个pte已经映射了一个页面,就将其释放. 不过这种情况应该极少出现
一般调用此函数做zero map时都已经撤销了映射.
值得注意的是,这个不涉及vma.没有加锁.
int remap_page_range(unsigned long from, unsigned long phys_addr,
unsigned long size, pgprot_t prot)
相关函数remap_pmd_range,remap_pte_range. 作用是将phy_addr开始的连续
物理页面映射到虚存from, 大小为size(page align).这些物理页面都不能是ram
页面.只负责mem map不能管理的页面,和PageReserved的页面.主要应用于驱动将
设备内存映射到内核的一段虚存. 看看下面的函数就知道了:
static remap_pte_range(pte,address, size, phys_addr, pgprot_t prot)
{
......
do {
struct page *page;
pte_t oldpage;
oldpage = ptep_get_and_clear(pte);
page = virt_to_page(__va(phys_addr));
/*只负责映射保留页面和非ram页面*/
if ((!VALID_PAGE(page)) || PageReserved(page))
set_pte(pte, mk_pte_phys(phys_addr, prot));
/*同时负责拆除老的映射页面*/
forget_pte(oldpage);
.....
pte++;
} while (address && (address do_sys_truncate->do_truncate()
{ ......
newattrs.ia_valid = ATTR_SIZE | ATTR_CTIME;
error = notify_change(dentry, &newattrs);
....
}
notify_change(){
......
lock_kernel();
if (inode->i_op && inode->i_op->setattr) //ext2 未设置此op
error = inode->i_op->setattr(dentry, attr);
else {
//所以对ext2来讲执行此公共流程
error = inode_change_ok(inode, attr);
if (!error)
inode_setattr(inode, attr);
}
unlock_kernel();
.......
}
inode_setattr(){
......
if (ia_valid & ATTR_SIZE)
vmtruncate(inode, attr->ia_size);
......
mark_inode_dirty(inode);
}
可见, vmtruncate是其核心. 对于一个文件在进行truncate时要考虑文件已经
使用mmap映射到内存的情况. 将所有的映射一并truncate.此外还要处理page cache
保证page cache不再含有相关数据.最后才是文件本身进行truncate. vmtruncate
即此服务.
void vmtruncate(struct inode * inode, loff_t offset)
{
unsigned long partial, pgoff;
struct address_space *mapping = inode->i_mapping;
unsigned long limit;
if (inode->i_size i_size = offset;
/* 清除page cache中相关的缓冲数据 */
truncate_inode_pages(mapping, offset);
spin_lock(&mapping->i_shared_lock);
/*检查是否存在此文件的mapping*/
if (!mapping->i_mmap && !mapping->i_mmap_shared)
goto out_unlock;
pgoff = (offset + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT;
partial = (unsigned long)offset & (PAGE_CACHE_SIZE - 1);
/*truncate maping*/
if (mapping->i_mmap != NULL)
/*
*始释放指定虚拟地址空间内所有已映射页面.(not clear cpu meta data)
*/
vmtruncate_list(mapping->i_mmap, pgoff, partial);
if (mapping->i_mmap_shared != NULL)
vmtruncate_list(mapping->i_mmap_shared, pgoff, partial);
out_unlock:
spin_unlock(&mapping->i_shared_lock);
/* this should go into ->truncate */
inode->i_size = offset;
/*最后对文件进行truncate,ext2 参考ext2_truncate */
if (inode->i_op && inode->i_op->truncate)
inode->i_op->truncate(inode);
return;
do_expand:
limit = current->rlim[RLIMIT_FSIZE].rlim_cur;
if (limit != RLIM_INFINITY) {
if (inode->i_size >= limit) {
send_sig(SIGXFSZ, current, 0);
goto out;
}
if (offset > limit) {
send_sig(SIGXFSZ, current, 0);
offset = limit;
}
}
inode->i_size = offset;
if (inode->i_op && inode->i_op->truncate)
inode->i_op->truncate(inode);
out:
return;
}
vmtruncate_list就是将truncate语义指定的范围内的已映射页面umap.这里不
再分析.(就是调用zap_page_range)
清除page cache在分析filemap.c的时候已经分析过了,对于文件本身的操作等
到分析buffer.c再议.
Understanding Swap in
VII)Understanding Swap in
接口函数: swapin_readahead,handle_mm_fault,make_pages_presen t.
我们从handle_mm_fault谈起,在分析fault.c的时候提到过一个mm fault产生的
几种情形,首先是进入到do_page_fault的时候
/*
* 有三种情况下执行流到达此函数:
* 1. pmd, pgt, pte 有一个为空, 即未建立映射或已经撤销.(nomal page
cache)
* 2. 页面不在内存(not present)被内核交换到了磁盘(swap space).
* 3. 权限不正确. (内核已经授权或者根本不容许那样访问)
*
* 说明:
* 情况1)涉及的页面或者没有建立映射或者属于nomal address map(like mmap)
* 一般要到page cache中试图寻找.
* try_to_swap_out处理nomal address map的时候,把相应的pte置成0.
*
* 情况2.涉及页面属于swapper_space管理. try_to_swap_out 不会把pte 置0,
而是讲pte置为相应的swp_entry_t并将present位置0.
*
* 再转交下一级函数处理时, 所有非法操作都在这个函数中处理掉了.所谓非法
* 是用户企图进行一次内核(vma)不容许的操作,如vma无写属性,而进程进行了写操
* 作.
*/
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
.........
/*
* 如果处时指令流抵达此地,说明异常点在一个完好的vma中,
* 并且符合OS 赋予用户的权限.
*/
switch (handle_mm_fault(mm, vma, address, write)) {
case 1:
tsk->min_flt++;
break;
case 2:
tsk->maj_flt++;
break;
case 0:
goto do_sigbus;
default:
goto out_of_memory;
}
........
}
上面的分析给出了handle_mm_fault(mm, vma, address, write)执行时所面临的
条件"说明异常点在一个完好的vma中,并且符合OS 赋予用户的权限":vma是经过了
扩展(expand stack)或修改,用户的这次操作应该得到相应的服务.
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
unsigned long address, int write_access)
{
int ret = -1;
pgd_t *pgd;
pmd_t *pmd;
pgd = pgd_offset(mm, address);
pmd = pmd_alloc(pgd, address); /*查找或者分配pmd page*/
if (pmd) {
pte_t * pte = pte_alloc(pmd, address); /*查找或者分配page table*/
if (pte)
ret = handle_pte_fault(mm, vma, address, write_access, pte);
}
return ret;
}
可见在发生页面异常的时候,容许进程被调度,和中断有所不同.这是合理的:可
以看作进程请求内核处理mm fault,就像一个系统调用.
static inline int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct * vma, unsigned long address,
int write_access, pte_t * pte)
{
pte_t entry;
/*
* We need the page table lock to synchronize with kswapd
* and the SMP-safe atomic PTE updates.
*/
spin_lock(&mm->page_table_lock);
entry = *pte;
/*异常属于映射链断裂的情况*/
if (!pte_present(entry)) {
/*
* If it truly wasn't present, we know that kswapd
* and the PTE updates will not touch it later. So
* drop the lock.
*/
spin_unlock(&mm->page_table_lock);
if (pte_none(entry))
return do_no_page(mm, vma, //页面不存在, 就是没有,或者被 try_to_swap_out
address, // 断开, 这种页面一般属于一个nomal address map
write_access,
pte //参见filemap_nopage 了解page cache 的换入
);
return do_swap_page(mm, vma,//not present, 属于swapper_space
address, pte,
pte_to_swp_entry(entry),
write_access
);
}
/*异常属于内核授权,但是cpu不容许的情况,COW*/
//如果是read 时产生陷入或者是pte的映射问题
//或者是一个非法的操作,已经在do_page_fault 过滤掉了
if (write_access) {//由于页面写保护产生的陷入, 而OS 却容许用户写入
if (!pte_write(entry))
return do_wp_page( mm, //就是处理COW 的第二步
vma, //Copy on Write
address, //第一步是建立一个不容许写的页
pte, entry //却在vma 中赋予用户写的权限
);
entry = pte_mkdirty(entry);
}
/*好像一般都满足write_access为1,read not present从
if (!pte_present(entry))就返回了(fix me),read present更是连
handle mem fault都进不了*/
entry = pte_mkyoung(entry);
establish_pte(vma, address, pte, entry);
spin_unlock(&mm->page_table_lock);
return 1;
}
handle_mm_fault 已经修复了映射链上的系统页面,handle_pte_fault主要是修复
映射链上的用户页面:
1)do_no_page:还未建立映射,或者已经被断开还存在于lru cache(page cache)
或者已经回收到了node-zone-buddy.
2)do_swap_page:从lru恢复,如果已经被回收到node-zone-buddy就从磁盘调入.
3)do_wp_page: 处理COW的copy操作,用户现在要写此页面,copy一份给他.
1)do_no_page:
分配一个匿名页面,或者用vm指定的操作寻找对应页面.在设置pte
的时候考虑COW:对于写操作,表示内核容许写(这里不会遭遇COW,cow是另一个处理
函数,handle_pte_fault已经区分的很清楚了),直接将pte置为可写.如果是read操
作,考虑mmap创建的vma(not anonymous page),并且页面已经是共用页面,则我们
不能直接给这个进程写权限,而是要取消写权限这是COW处理中的一环:建立一个写
保护的页面.参照分析filemap.c时对函数filemap_nopage的分析,那里讲的很详细
-->filemap_nopage 对这个read操作的进程直接返回一个共享页面,如果这是第一
个要求访问此页面的进程do_no_page不会取消这个进程的写操作权限.
static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma,
unsigned long address, int write_access, pte_t *page_table)
{
struct page * new_page;
pte_t entry;
if (!vma->vm_ops || !vma->vm_ops->nopage)
return do_anonymous_page(mm, vma, page_table, write_access, address);
/*
* The third argument is "no_share", which tells the low-level code
* to copy, not share the page even if sharing is possible. It's
* essentially an early COW detection.
* 对于mmap建立的vm,此函数是 filemap_nopage
*/
new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, (vma->vm_flags & VM_SHARED)?0:write_access);
if (new_page == NULL) /* no page was available -- SIGBUS */
return 0;
if (new_page == NOPAGE_OOM)
return -1;
++mm->rss;
//其实在上面两个调入函数中(do_anonymous_page vma->vm_ops->nopage)
//也存在cow 的第二步操作,即写操作时do_anonymous_page 返回
//新分配的页面, vma->vm_ops->nopage 不共享页面,而进行copy 操作.
//cow 本质: 写时拷贝,读时共享
//写时拷贝就是刚分析的do_anonymous_page vma->vm_ops->nopage 加上
//do_wp_page
//读时共享就是do_anonymous_page 在read 条件下,返回zero 公共页
//vma->vm_ops->nopage 在read 条件下尽力共享页面.
/*
* This silly early PAGE_DIRTY setting removes a race
* due to the bad i386 page protection. But it's valid
* for other architectures too.
*
* Note that if write_access is true, we either now have
* an exclusive copy of the page, or this is a shared mapping,
* so we can make it writable and dirty to avoid having to
* handle that later.
*/
flush_page_to_ram(new_page);
flush_icache_page(vma, new_page);
entry = mk_pte(new_page, vma->vm_page_prot);
if (write_access) {
entry = pte_mkwrite(pte_mkdirty(entry));
} else if (page_count(new_page) > 1 &&
!(vma->vm_flags & VM_SHARED))
entry = pte_wrprotect(entry);
set_pte(page_table, entry);
/* no need to invalidate: a not-present page shouldn't be cached */
update_mmu_cache(vma, address, entry);
return 2; /* Major fault */
}
2)do_swap_page
对于开始还出的页面,可以从lru恢复,如果已经被回收到node-zone-buddy就从
磁盘调入.
static int do_swap_page(struct mm_struct * mm,
struct vm_area_struct * vma, unsigned long address,
pte_t * page_table, swp_entry_t entry, int write_access)
{
struct page *page = lookup_swap_cache(entry); //先查找swap cache
//如果查找的到,肯定也存在于lru,说不定就要开始换出了
pte_t pte;
if (!page) { /*如果找不到,就从磁盘swap in*/
lock_kernel();
swapin_readahead(entry); /*先预读一部分*/
page = read_swap_cache(entry); /*类似普通文件读写,先读入,并加入swap
cache,并返回指定页面*/
unlock_kernel();
if (!page)
return -1;
flush_page_to_ram(page);
flush_icache_page(vma, page);
}
mm->rss++;
/*得到指定的页面,就开始恢复页面映射*/
pte = mk_pte(page, vma->vm_page_prot);
/*
* Freeze the "shared"ness of the page, ie page_count + swap_count.
* Must lock page before transferring our swap count to already
* obtained page count.
*/
lock_page(page); /*is_page_shared 要求锁定页面,详见filemap.c的分析*/
swap_free(entry);
if (write_access && !is_page_shared(page))
pte = pte_mkwrite(pte_mkdirty(pte));
UnlockPage(page);
set_pte(page_table, pte);
/* No need to invalidate - it was non-present before */
update_mmu_cache(vma, address, pte);
return 1; /* Minor fault */
}
其中预读和读取到swap cache(read_swap_cache)留做以后详解,这里仅仅简单
分析一下预读.
/*
* Primitive swap readahead code. We simply read an aligned block of
* (1 = pager_daemon.swap_cluster
* (1 内存.
全文完.
2010年10月19日
Understand Swap in & mem_map
mm/memory.c PageReserved
这1000多行代码,有绝大多数函数定义为static,意味着这个模块只提供了少量
的对外接口. 这几个接口是我们理解此模块的一个重要线索.
第一部分 :全局量的仔细解释
I)首先是几个全局变量,下面给出其具体含义:
+================================================= =====================
/*mem_map 中最高地址页面的map number,可以使用的ram全在其中*/
unsigned long max_mapnr;
/*物理内存页面总数(当然包括highmem在内)*/
unsigned long num_physpages;
/*从虚拟地址 high_memory 开始不再是线性映射区
*如果全部可以线性映射其值为物理内存顶端之虚拟地址
*/
void * high_memory;
struct page *highmem_start_page; /*高端物理内存的起始 page(struct page)*/
+================================================= =====================
II) i386下的 mem_map
另外一个重要的全局数组:
==========================================
mem_map_t * mem_map; /*page 结构的总数组*/
==========================================
这个数组存放有全部的struct page结构(i386 not numa),每一个物理内存页面
都对应一个page 机构,存储于此数组.下面来看看i386体系下如何初始化mem_map:
setup.S->asmlinkage void __init start_kernel(void) (init/main.c)
|
+-->setup_arch ---> 处理e820内存报告
--> 关于内存的提示信息
---> 初始化bootmem (init_bootmem)
---> paging_init
+
| +--> pagetable_init(含fix map,vmalloc init)
\ / +--> load cr3
. +--> kmap_init
. +--> free_area_init(zone-buddy初始化)
函数paging_init如此调用free_area_init(zones_size);参数zones_size给出
每一个zone(DMA,NORMAL,HIGH)的大小.再看:
void __init free_area_init(unsigned long *zones_size)
{
free_area_init_core(0, &contig_page_data, &mem_map, zones_size,
0, 0, 0);
}
对于i386此种"连续"内存结构,使用了一个pg_data_t类型的变量:contig_page_data
注意第三个参数是&mem_map.
void __init free_area_init_core(int nid, pg_data_t *pgdat,
struct page **gmap, unsigned long *zones_size ,
unsigned long zone_start_paddr, unsigned long *zholes_size,
struct page *lmem_map)
{
/* pgdat 是contig_page_data, gmap是&mem_map, lmem_map是 0 */
.....
if (lmem_map == (struct page *)0) {
//非numa体系此值为0, 分配mem map数组
lmem_map = (struct page *) alloc_bootmem_node(pgdat, map_size);
lmem_map = (struct page *)(PAGE_OFFSET +
MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET));
}
/*mem_map指向刚分配的内存,即是contig_page_data.node_mem_map*/
*gmap = pgdat->node_mem_map = lmem_map;
pgdat->node_size = totalpages;
pgdat->node_start_paddr = zone_start_paddr;
pgdat->node_start_mapnr = (lmem_map - mem_map);
/*从0号物理内存页开始*/
}
NUMA下的 mem_map
III)NUMA下的 mem_map
而对于NUMA类型的体系,mem_map已经失去了其存在的意义,不过2.4处理的
不够好,代码凌乱,可读性不强.以arm体系为例:(arch/arm/mm/init.c)
void __init paging_init(struct meminfo *mi, struct machine_desc *mdesc)
{
.........
for (node = 0; node node_boot_start, zhole_size);
}
......
}
这里调用的free_area_init_node定义于mm/numa.c
void __init free_area_init_node(int nid, pg_data_t *pgdat, struct page *pmap,
unsigned long *zones_size, unsigned long zone_start_paddr,
unsigned long *zholes_size)
{
int i, size = 0;
struct page *discard;
if (mem_map == (mem_map_t *)NULL)
mem_map = (mem_map_t *)PAGE_OFFSET; /*仅为mem_map指定了一个值,其指向
*的地址并没有存放struct page
*/
free_area_init_core(nid, pgdat, &discard, zones_size, zone_start_paddr,
zholes_size, pmap);
pgdat->node_id = nid;
.......
}
void __init free_area_init_core(int nid, pg_data_t *pgdat,
struct page **gmap, unsigned long *zones_size ,
unsigned long zone_start_paddr, unsigned long *zholes_size,
struct page *lmem_map)
{
/* pgdat 是NODE_pgdat, gmap是&discard, lmem_map还是 0 */
.....
if (lmem_map == (struct page *)0) {
//numa体系此值亦为0, 分配mem map数组
lmem_map = (struct page *) alloc_bootmem_node(pgdat, map_size);
lmem_map = (struct page *)(PAGE_OFFSET +
MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET));
}
/* *gmap再numa下不指向mem_map,被丢弃不用*/
*gmap = pgdat->node_mem_map = lmem_map;
pgdat->node_size = totalpages;
pgdat->node_start_paddr = zone_start_paddr;
pgdat->node_start_mapnr = (lmem_map - mem_map);
/*node_start_mapnr再numa下失去其意义*/
/*变成了lmem_map这个地址到PAGE_OFFSET有多少个页面*/
offset = lmem_map - mem_map; /*减去常量PAGE_OFFSET*/
for (j = 0; j offset = offset; /*失去意义*/
....
zone->zone_mem_map = mem_map + offset; /*又加上常量PAGE_OFFSET所以
*zone_mem_map确实有意义
*/
zone->zone_start_mapnr = offset; /*失去意义*/
...
offset += size;
}
}
从上面的分析,很明显2.4还没有处理好NUMA和discon mem.在NUMA体系下,mem_map
已经失去存在的意义,只是一个跳板.arm的vir_to_page宏就没有使用这个mem_map.
而ia64-sn的使用方法就巨绕:
*******include/asm-ia64/page.h 定义
# define virt_to_page(kaddr) (mem_map + platform_map_nr(kaddr))
mem_map是一个常量PAGE_OFFSET???
*******include/asm-ia64/machvec_sn1.h定义
#define platform_map_nr sn1_map_nr
*******arch/ia64/sn/sn1/setup.c定义
sn1_map_nr (unsigned long addr)
{
#ifdef CONFIG_DISCONTIGMEM
return MAP_NR_SN1(addr);
#else
return MAP_NR_DENSE(addr);
#endif
}
*******include/asm-ia64/sn/mmzone.h定义
#define NODE_DATA(n) (&((plat_node_data + (n))->gendata))
#define NODE_MEM_MAP(nid) (NODE_DATA((nid))->node_mem_map)
#define LOCAL_MAP_NR(kvaddr) \
(((unsigned long)(kvaddr)-LOCAL_BASE_ADDR((kvaddr))) >> PAGE_SHIFT)
#define MAP_NR_SN1(kaddr) (LOCAL_MAP_NR((kaddr)) + \
(((unsigned long)ADDR_TO_MAPBASE((kaddr)) - PAGE_OFFSET) / \
sizeof(mem_map_t)))
仔细分析MAP_NR_SN1就知道其种缘由了:
mem_map + (((unsigned long)ADDR_TO_MAPBASE((kaddr)) - PAGE_OFFSET) / \
sizeof(mem_map_t))
就定位到了pgdat->node_mem_map,而node_mem_map了在numa中依然有效,指向真
实的"mem_map".
pgdat->node_start_mapnr也失去了意义,不过在kernel2.4中大概只有mips64使
用这个值,并且类似ia64的vir_to_page,见include/asm-mips64/pgtable.h
#define mips64_pte_pagenr(x) \
(PLAT_NODE_DATA_STARTNR(PHYSADDR_TO_NID(pte_val(x) )) + \
PLAT_NODE_DATA_LOCALNR(pte_val(x), PHYSADDR_TO_NID(pte_val(x))))
#define pte_page(x) (mem_map+mips64_pte_pagenr(x))
使用这个变量的时候最终还是加上了mem_map!!!
不过从另外一个角度理解mem_map也可以:这紧紧是一个虚拟的值,借助于这个变
量也简化了一些设计,比如使用zone->offset的地方,如:mm/page_alloc.c
static void __free_pages_ok (struct page *page, unsigned long order)
{
.........
base = mem_map + zone->offset;
page_idx = page - base;
....
}
本来zone->offset减去了mem_map,现在加上mem_map,还是指向这个zone所属的
pgdat->zone_mem_map. 虽然失去其本意,也达到了目的,必是2.4向NUMA转化的过
渡性代码.
在kernel2.6,numa体系下,根本不再有mem_map这个全局变量,
pgdat->node_start_mapnr也时刻保持其原意.zone->offset更是消失不见了,直接
换成了指针.一切都变得clean&clear.
注:看64bit代码时突然想到两个64bit值之差赋值给long型的变量是否有问题,后来
参考了:
>
指出在linux的64bit环境中, long是64bit的!!!和windows不同啊.
Page Reserved详解
第二部分 本模块对外接口详解
I)clear_page_tables : 释放一个进程指定范围内的页表,以及关联的pmd,对应
的pgd entry被清空,pgd本身没有被释放.至于页表中是否还有未断开
影射的物理页面,此函数未做检查.(所以调用之前要先调用
zap_page_range). 对于这个函数和其涉及的其他函数请自己阅读.
II)copy_page_range
从一个进程的mm拷贝指定的vm area到另一个进程,拷贝内容包括:为新进程指
定的这段空间分配pmd页面,分配page table页面.从src进程的mm拷贝pte到新分配
的页表.增加copy的pte所引用页面的引用计数,并且处理pte是swap entry的情况(
增加swap page的引用计数). 对于vm area属性是可写但无VM_SHARE属性的情况,将
新进程的pte置写保护,等到新进程写此页面的时候进行COW. 如果vm area具有共享
属性(那当然就是容许在进程间共享此页面)将新设置的pte的dirty位清除:新进程
还未曾访问此页面.
通过上面的分析阅读copy_page_range应该不是问题了.注意在copy_page_range
的时候就为COW设置了伏笔:共享一个page,但是不容许写.
III)zap_page_range:
从一个进程指定的地址开始释放指定大小的虚拟地址空间内
所有已映射页面. clear_page_tables仅释放"cpu meta data",而此函数
仅释放"user used data pages".如果页面被交换到磁盘,则释放磁盘页面.
此函数相关子函数:
zap_pmd_range zap_pte_range free_pte.
都是对mm进行遍历,只有free_pte还有些"业务"逻辑.
static inline int free_pte(pte_t pte)
{
if (pte_present(pte)) {
struct page *page = pte_page(pte);
if ((!VALID_PAGE(page)) || PageReserved(page))
return 0;
/*
* free_page() used to be able to clear swap cache
* entries. We may now have to do it manually.
*/
if (pte_dirty(pte) && page->mapping)
set_page_dirty(page);
free_page_and_swap_cache(page);
return 1;
}
swap_free(pte_to_swp_entry(pte));
return 0;
}
PageReserved检查page是否就有PG_reserved属性.
到底什么是PG_reserved页面
这里说明一下: 其实非常简单,内核不认为PG_reserved属性的页面是"ram"页
面.从而不会在"任何"地方使用这个物理页面. 这里的任何地方不是指真正的所有
的内核程序,而是指不知道PG_reserved的页面是什么页面的代码.
从另外一种意义上说,就是内核的某个地方保留了这个page,不得再做他用.
首先, 参考arch/i386/mm/init.c的分析,函数mem_init,使用函数free_all_bootmem
清除所有未分配出去的页面的PG_reserved位.仔细阅读一下函数对bootmem的初始
化就可以发现:所有非ram页面,在bootmem中已经标记为已分配,PG_reserved置位.
所以free_all_bootmem也没有将非ram页面的PG_reserved标识清除掉.而对于high
mem, mem_init 将非ram页面的PG_reserved位设置上.这就是PG_reserved的第一
个含义:非ram页面.
然后看setup_arch对reserve_bootmem的调用,可以知道,(0, PAGE_SIZE)被保
留,这样不会有"任何"代码可以分配这段内存. 这就是第二个含义:被内核保留用于
特殊用途.
关于zap_page_range不再做其他说明.
user kiobuf 和 remap, zeromap
IV)user kiobuf
在2.6内核,对应的函数叫做get_user_pages,用于AIO.这里的user kiobuf提供
这样一种服务:
从内核读取数据直接映射到用户空间,减少了copy,可以"大大的"提高效率.不过
看来2.4想用于raw io.到时候再仔细分析,这里先看看user kiobuf是如何建立和使
用的.
int map_user_kiobuf(int rw, struct kiobuf *iobuf, unsigned long va, size_t len)
{
.....
int datain = (rw == READ);
/* Make sure the iobuf is not already mapped somewhere. */
if (iobuf->nr_pages)
return -EINVAL;
mm = current->mm;
dprintk ("map_user_kiobuf: begin\n");
//准备扫描指定的区间(遍历ptr到end之间涉及的vma)
ptr = va & PAGE_MASK;
end = (va + len + PAGE_SIZE - 1) & PAGE_MASK;
err = expand_kiobuf(iobuf, (end - ptr) >> PAGE_SHIFT);
if (err)
return err;
down(&mm->mmap_sem);
iobuf->locked = 0;
iobuf->offset = va & ~PAGE_MASK;
iobuf->length = len;
i = 0;
/*
* First of all, try to fault in all of the necessary pages
*/
while (ptr = vma->vm_end) {
/*指定的虚拟地址va必须有一个vam区间对应*/
vma = find_vma(current->mm, ptr);
if (!vma)
goto out_unlock;
if (vma->vm_start > ptr) {
/*必须使用堆栈(fix me)做io buf*/
if (!(vma->vm_flags & VM_GROWSDOWN))
goto out_unlock;
/*先对vma进行调整*/
if (expand_stack(vma, ptr))
goto out_unlock;
}
/*检查一下访问权限*/
if (((datain) && (!(vma->vm_flags & VM_WRITE))) ||
(!(vma->vm_flags & VM_READ))) {
err = -EACCES;
goto out_unlock;
}
}
/*为涉及到的空间影射物理内存页面*/
if (handle_mm_fault(current->mm, vma, ptr, datain) page_table_lock);
/*记录页面的page 结构到 iobuf中,以便后用*/
map = follow_page(ptr);
if (!map) {
spin_unlock(&mm->page_table_lock);
dprintk (KERN_ERR "Missing page in map_user_kiobuf\n");
goto out_unlock;
}
map = get_page_map(map);
if (map) {
flush_dcache_page(map);
/*增加引用计数,以免被swap out对应unmap_kiobuf*/
atomic_inc(&map->count);
} else
printk (KERN_INFO "Mapped page missing [%d]\n", i);
spin_unlock(&mm->page_table_lock);
iobuf->maplist[i] = map;
iobuf->nr_pages = ++i;
ptr += PAGE_SIZE;
}
up(&mm->mmap_sem);
.......
}
与此相关的其他几个函数unmap_kiobuf,lock_kiovec,unlock_kiovec个人认为
极为简单,就不再分析.
V)zeromap_page_range remap_page_range
int zeromap_page_range(unsigned long address, unsigned long size,
pgprot_t prot)
相关函数zeromap_pmd_range,zeromap_pte_range,forget_pte.是一个遍历
当前进程的页面映射表的函数.将指定地址空间的每一项pte都指向zero page页
(一个4k的全0页面),并将pte置写保护,以后可以进行COW.
如果某个pte已经映射了一个页面,就将其释放. 不过这种情况应该极少出现
一般调用此函数做zero map时都已经撤销了映射.
值得注意的是,这个不涉及vma.没有加锁.
int remap_page_range(unsigned long from, unsigned long phys_addr,
unsigned long size, pgprot_t prot)
相关函数remap_pmd_range,remap_pte_range. 作用是将phy_addr开始的连续
物理页面映射到虚存from, 大小为size(page align).这些物理页面都不能是ram
页面.只负责mem map不能管理的页面,和PageReserved的页面.主要应用于驱动将
设备内存映射到内核的一段虚存. 看看下面的函数就知道了:
static remap_pte_range(pte,address, size, phys_addr, pgprot_t prot)
{
......
do {
struct page *page;
pte_t oldpage;
oldpage = ptep_get_and_clear(pte);
page = virt_to_page(__va(phys_addr));
/*只负责映射保留页面和非ram页面*/
if ((!VALID_PAGE(page)) || PageReserved(page))
set_pte(pte, mk_pte_phys(phys_addr, prot));
/*同时负责拆除老的映射页面*/
forget_pte(oldpage);
.....
pte++;
} while (address && (address do_sys_truncate->do_truncate()
{ ......
newattrs.ia_valid = ATTR_SIZE | ATTR_CTIME;
error = notify_change(dentry, &newattrs);
....
}
notify_change(){
......
lock_kernel();
if (inode->i_op && inode->i_op->setattr) //ext2 未设置此op
error = inode->i_op->setattr(dentry, attr);
else {
//所以对ext2来讲执行此公共流程
error = inode_change_ok(inode, attr);
if (!error)
inode_setattr(inode, attr);
}
unlock_kernel();
.......
}
inode_setattr(){
......
if (ia_valid & ATTR_SIZE)
vmtruncate(inode, attr->ia_size);
......
mark_inode_dirty(inode);
}
可见, vmtruncate是其核心. 对于一个文件在进行truncate时要考虑文件已经
使用mmap映射到内存的情况. 将所有的映射一并truncate.此外还要处理page cache
保证page cache不再含有相关数据.最后才是文件本身进行truncate. vmtruncate
即此服务.
void vmtruncate(struct inode * inode, loff_t offset)
{
unsigned long partial, pgoff;
struct address_space *mapping = inode->i_mapping;
unsigned long limit;
if (inode->i_size i_size = offset;
/* 清除page cache中相关的缓冲数据 */
truncate_inode_pages(mapping, offset);
spin_lock(&mapping->i_shared_lock);
/*检查是否存在此文件的mapping*/
if (!mapping->i_mmap && !mapping->i_mmap_shared)
goto out_unlock;
pgoff = (offset + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT;
partial = (unsigned long)offset & (PAGE_CACHE_SIZE - 1);
/*truncate maping*/
if (mapping->i_mmap != NULL)
/*
*始释放指定虚拟地址空间内所有已映射页面.(not clear cpu meta data)
*/
vmtruncate_list(mapping->i_mmap, pgoff, partial);
if (mapping->i_mmap_shared != NULL)
vmtruncate_list(mapping->i_mmap_shared, pgoff, partial);
out_unlock:
spin_unlock(&mapping->i_shared_lock);
/* this should go into ->truncate */
inode->i_size = offset;
/*最后对文件进行truncate,ext2 参考ext2_truncate */
if (inode->i_op && inode->i_op->truncate)
inode->i_op->truncate(inode);
return;
do_expand:
limit = current->rlim[RLIMIT_FSIZE].rlim_cur;
if (limit != RLIM_INFINITY) {
if (inode->i_size >= limit) {
send_sig(SIGXFSZ, current, 0);
goto out;
}
if (offset > limit) {
send_sig(SIGXFSZ, current, 0);
offset = limit;
}
}
inode->i_size = offset;
if (inode->i_op && inode->i_op->truncate)
inode->i_op->truncate(inode);
out:
return;
}
vmtruncate_list就是将truncate语义指定的范围内的已映射页面umap.这里不
再分析.(就是调用zap_page_range)
清除page cache在分析filemap.c的时候已经分析过了,对于文件本身的操作等
到分析buffer.c再议.
Understanding Swap in
VII)Understanding Swap in
接口函数: swapin_readahead,handle_mm_fault,make_pages_presen t.
我们从handle_mm_fault谈起,在分析fault.c的时候提到过一个mm fault产生的
几种情形,首先是进入到do_page_fault的时候
/*
* 有三种情况下执行流到达此函数:
* 1. pmd, pgt, pte 有一个为空, 即未建立映射或已经撤销.(nomal page
cache)
* 2. 页面不在内存(not present)被内核交换到了磁盘(swap space).
* 3. 权限不正确. (内核已经授权或者根本不容许那样访问)
*
* 说明:
* 情况1)涉及的页面或者没有建立映射或者属于nomal address map(like mmap)
* 一般要到page cache中试图寻找.
* try_to_swap_out处理nomal address map的时候,把相应的pte置成0.
*
* 情况2.涉及页面属于swapper_space管理. try_to_swap_out 不会把pte 置0,
而是讲pte置为相应的swp_entry_t并将present位置0.
*
* 再转交下一级函数处理时, 所有非法操作都在这个函数中处理掉了.所谓非法
* 是用户企图进行一次内核(vma)不容许的操作,如vma无写属性,而进程进行了写操
* 作.
*/
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
.........
/*
* 如果处时指令流抵达此地,说明异常点在一个完好的vma中,
* 并且符合OS 赋予用户的权限.
*/
switch (handle_mm_fault(mm, vma, address, write)) {
case 1:
tsk->min_flt++;
break;
case 2:
tsk->maj_flt++;
break;
case 0:
goto do_sigbus;
default:
goto out_of_memory;
}
........
}
上面的分析给出了handle_mm_fault(mm, vma, address, write)执行时所面临的
条件"说明异常点在一个完好的vma中,并且符合OS 赋予用户的权限":vma是经过了
扩展(expand stack)或修改,用户的这次操作应该得到相应的服务.
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
unsigned long address, int write_access)
{
int ret = -1;
pgd_t *pgd;
pmd_t *pmd;
pgd = pgd_offset(mm, address);
pmd = pmd_alloc(pgd, address); /*查找或者分配pmd page*/
if (pmd) {
pte_t * pte = pte_alloc(pmd, address); /*查找或者分配page table*/
if (pte)
ret = handle_pte_fault(mm, vma, address, write_access, pte);
}
return ret;
}
可见在发生页面异常的时候,容许进程被调度,和中断有所不同.这是合理的:可
以看作进程请求内核处理mm fault,就像一个系统调用.
static inline int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct * vma, unsigned long address,
int write_access, pte_t * pte)
{
pte_t entry;
/*
* We need the page table lock to synchronize with kswapd
* and the SMP-safe atomic PTE updates.
*/
spin_lock(&mm->page_table_lock);
entry = *pte;
/*异常属于映射链断裂的情况*/
if (!pte_present(entry)) {
/*
* If it truly wasn't present, we know that kswapd
* and the PTE updates will not touch it later. So
* drop the lock.
*/
spin_unlock(&mm->page_table_lock);
if (pte_none(entry))
return do_no_page(mm, vma, //页面不存在, 就是没有,或者被 try_to_swap_out
address, // 断开, 这种页面一般属于一个nomal address map
write_access,
pte //参见filemap_nopage 了解page cache 的换入
);
return do_swap_page(mm, vma,//not present, 属于swapper_space
address, pte,
pte_to_swp_entry(entry),
write_access
);
}
/*异常属于内核授权,但是cpu不容许的情况,COW*/
//如果是read 时产生陷入或者是pte的映射问题
//或者是一个非法的操作,已经在do_page_fault 过滤掉了
if (write_access) {//由于页面写保护产生的陷入, 而OS 却容许用户写入
if (!pte_write(entry))
return do_wp_page( mm, //就是处理COW 的第二步
vma, //Copy on Write
address, //第一步是建立一个不容许写的页
pte, entry //却在vma 中赋予用户写的权限
);
entry = pte_mkdirty(entry);
}
/*好像一般都满足write_access为1,read not present从
if (!pte_present(entry))就返回了(fix me),read present更是连
handle mem fault都进不了*/
entry = pte_mkyoung(entry);
establish_pte(vma, address, pte, entry);
spin_unlock(&mm->page_table_lock);
return 1;
}
handle_mm_fault 已经修复了映射链上的系统页面,handle_pte_fault主要是修复
映射链上的用户页面:
1)do_no_page:还未建立映射,或者已经被断开还存在于lru cache(page cache)
或者已经回收到了node-zone-buddy.
2)do_swap_page:从lru恢复,如果已经被回收到node-zone-buddy就从磁盘调入.
3)do_wp_page: 处理COW的copy操作,用户现在要写此页面,copy一份给他.
1)do_no_page:
分配一个匿名页面,或者用vm指定的操作寻找对应页面.在设置pte
的时候考虑COW:对于写操作,表示内核容许写(这里不会遭遇COW,cow是另一个处理
函数,handle_pte_fault已经区分的很清楚了),直接将pte置为可写.如果是read操
作,考虑mmap创建的vma(not anonymous page),并且页面已经是共用页面,则我们
不能直接给这个进程写权限,而是要取消写权限这是COW处理中的一环:建立一个写
保护的页面.参照分析filemap.c时对函数filemap_nopage的分析,那里讲的很详细
-->filemap_nopage 对这个read操作的进程直接返回一个共享页面,如果这是第一
个要求访问此页面的进程do_no_page不会取消这个进程的写操作权限.
static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma,
unsigned long address, int write_access, pte_t *page_table)
{
struct page * new_page;
pte_t entry;
if (!vma->vm_ops || !vma->vm_ops->nopage)
return do_anonymous_page(mm, vma, page_table, write_access, address);
/*
* The third argument is "no_share", which tells the low-level code
* to copy, not share the page even if sharing is possible. It's
* essentially an early COW detection.
* 对于mmap建立的vm,此函数是 filemap_nopage
*/
new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, (vma->vm_flags & VM_SHARED)?0:write_access);
if (new_page == NULL) /* no page was available -- SIGBUS */
return 0;
if (new_page == NOPAGE_OOM)
return -1;
++mm->rss;
//其实在上面两个调入函数中(do_anonymous_page vma->vm_ops->nopage)
//也存在cow 的第二步操作,即写操作时do_anonymous_page 返回
//新分配的页面, vma->vm_ops->nopage 不共享页面,而进行copy 操作.
//cow 本质: 写时拷贝,读时共享
//写时拷贝就是刚分析的do_anonymous_page vma->vm_ops->nopage 加上
//do_wp_page
//读时共享就是do_anonymous_page 在read 条件下,返回zero 公共页
//vma->vm_ops->nopage 在read 条件下尽力共享页面.
/*
* This silly early PAGE_DIRTY setting removes a race
* due to the bad i386 page protection. But it's valid
* for other architectures too.
*
* Note that if write_access is true, we either now have
* an exclusive copy of the page, or this is a shared mapping,
* so we can make it writable and dirty to avoid having to
* handle that later.
*/
flush_page_to_ram(new_page);
flush_icache_page(vma, new_page);
entry = mk_pte(new_page, vma->vm_page_prot);
if (write_access) {
entry = pte_mkwrite(pte_mkdirty(entry));
} else if (page_count(new_page) > 1 &&
!(vma->vm_flags & VM_SHARED))
entry = pte_wrprotect(entry);
set_pte(page_table, entry);
/* no need to invalidate: a not-present page shouldn't be cached */
update_mmu_cache(vma, address, entry);
return 2; /* Major fault */
}
2)do_swap_page
对于开始还出的页面,可以从lru恢复,如果已经被回收到node-zone-buddy就从
磁盘调入.
static int do_swap_page(struct mm_struct * mm,
struct vm_area_struct * vma, unsigned long address,
pte_t * page_table, swp_entry_t entry, int write_access)
{
struct page *page = lookup_swap_cache(entry); //先查找swap cache
//如果查找的到,肯定也存在于lru,说不定就要开始换出了
pte_t pte;
if (!page) { /*如果找不到,就从磁盘swap in*/
lock_kernel();
swapin_readahead(entry); /*先预读一部分*/
page = read_swap_cache(entry); /*类似普通文件读写,先读入,并加入swap
cache,并返回指定页面*/
unlock_kernel();
if (!page)
return -1;
flush_page_to_ram(page);
flush_icache_page(vma, page);
}
mm->rss++;
/*得到指定的页面,就开始恢复页面映射*/
pte = mk_pte(page, vma->vm_page_prot);
/*
* Freeze the "shared"ness of the page, ie page_count + swap_count.
* Must lock page before transferring our swap count to already
* obtained page count.
*/
lock_page(page); /*is_page_shared 要求锁定页面,详见filemap.c的分析*/
swap_free(entry);
if (write_access && !is_page_shared(page))
pte = pte_mkwrite(pte_mkdirty(pte));
UnlockPage(page);
set_pte(page_table, pte);
/* No need to invalidate - it was non-present before */
update_mmu_cache(vma, address, pte);
return 1; /* Minor fault */
}
其中预读和读取到swap cache(read_swap_cache)留做以后详解,这里仅仅简单
分析一下预读.
/*
* Primitive swap readahead code. We simply read an aligned block of
* (1 = pager_daemon.swap_cluster
* (1 内存.
全文完.