总结以上两点,映射过程中,如果pte的P位为1,pte由cpu硬件逻辑使用,映射结果为一个物理内存页面,如果pte的P位为0,pte由内核的缺页异常处理函数do_page_fault()使用,映射结果为一个交换分区中的盘上页面,do_page_fault()会重新分配物理内存页面并建立映射,再根据盘上页面换入数据,这样,跳回导致异常的那条指令重新执行,就能成功访问内存了:
"临阵磨枪"是一种最容易实现的方式,就是每次遇到缺页异常,需要"腾出"一个页面的时候,才进行换出操作,就是说换出操作和换入操作发生在同一时刻,每次调用do_page_fault()函数,必然执行一次写磁盘和读磁盘操作,导致每次缺页异常处理的时间较长。
Linux内核基于"临阵磨枪"方案,做了3点优化:
① 创建一个kswapd进程,它只会在系统空闲时执行(设置低优先级即可,见第四章),如果页面紧张,就会事先挑选一些页面换出,这样就将本要到缺页异常时做的事,转移到空闲时处理了;
② 步骤①中挑选换出页面的依据是"最近最少使用(LRU)"的页面,比如平常用电脑时,打开一个网页,经常很长时间才关掉,或者应用程序中有内存泄漏的bug也是比较常见的,LRU算法就能将这些情况占用的页面挑选出来,为此,cpu在硬件层提供了一个特性,即pte的A标志位,如果A标志位为1,表示上次到本次切换到kswapd进程之间,cpu至少访问过一次该页面,相反就是kswapd挑选的页面。Linux内核在软件层也加了一个特性,即struct page的age成员,这样页面老化程度的梯度就更多,挑选的页面就更满足LRU的性质。然而也可能出现,网页内容所在的页面刚刚换出,就被重新浏览了,但这只是小概率情况,所以LRU方式总体上还是大赚的;
③ 如果某个换入页面在内存中没有被写过,当它又被挑选为换出页面时,就不用再写磁盘了,相应的,Linux内核在page结构中设计了flags成员,它的每个位的含义如下图,PG_dirty位就是用于记录换入页面的"干净/脏"(PG_active、PG_inactive_dirty、PG_inactive_clean表示page->lru所在的链表,PG_dirty才是描述页面的"干净/脏",PG_inactive_dirty位为1,并不意味PG_dirty位一定为1,稍后代码分析中需要注意这一点),同时,换入操作不会释放盘上页面,盘上页面一般比较充足(便宜),而且换出操作的时间代价较高,所以盘上页面直到对应的内存页面释放,才会释放。
到此为止,学习到的概念和原理,已经足够理解稍后的代码部分了,这里再对页面的不同状态做一下总结:
① 空闲:page->list挂在某个zone的"free_area(页面块)或者inactive_clean_list(不活跃+干净的单页面)",页面分配函数,只会从这两个地方获取页面;
② 分配:页面分配函数将page->list从空闲区域取下,page->count设置为1(如果分配的是页面块,只设置第一个页面);
③ 活跃:分配后紧接着就通过page->lru挂到全局的active_list,并会跟一个或多个进程的虚拟页面建立映射,每次建立映射,page->count加1;
④ 不活跃+脏:"干净/脏"由page->flags的PG_dirty标志位记录,脏页面被kswapd进程挑选为换出页面时,会先断开pte映射,并通过page->lru挂到inactive_dirty_list,如果某个页面刚进入inactive_dirty_list,又受到访问被do_page_fault()函数恢复了pte映射,并不会立即被转移到inactive_list,而是留到空闲时由page_launder()函数转移(可能是为了尽快回到导致异常的指令);
⑤ 不活跃+干净:由page_launder()函数从inactive_dirty_list转移到某个zone的的inactive_clean_list,也属于空闲页面,可以合并成块回到某个free_area,也可以作为单页面直接分配。
struct swap_info_struct { unsigned int flags; kdev_t swap_device; spinlock_t sdev_lock; struct dentry * swap_file; struct vfsmount *swap_vfsmnt; unsigned short * swap_map; // swap_map[]数组,下标为盘上页面在当前交换文件内的索引,值为盘上页面的引用计数 unsigned int lowest_bit; unsigned int highest_bit; // [lowest_bit,highest_bit]区间之外,一定不为空闲页面,包括用于持久化记录交换文件本身信息的页面,和已分配的页面,但并不代表[lowest_bit,highest_bit]区间之内,没有已分配页面 unsigned int cluster_next; unsigned int cluster_nr; // 用于描述当前文件包含的cluster,一个cluster包含了一组扇区,间接的描述了当前文件包含的扇区,不影响内核学习,如果希望深度了解硬盘,可以查阅详细资料 int prio; /* swap priority */ int pages; // 当前交换文件包含盘上页面的个数 unsigned long max; int next; /* next entry on swap list */ };
__swap_free() |- type = SWP_TYPE(entry) |- struct swap_info_struct *p = &swap_info[type] |- offset = SWP_OFFSET(entry) |- p->swap_map[offset] -= count
#ifdef CONFIG_DISCONTIGMEM // 即使单内存,也可以打开该宏,CONFIG_NUMA只是一种特殊的不连续 alloc_pages() // gfp_mask:策略;2^order:要分配的页面块包含的页面个数 | // tmp | // ↓ | // pgdat_list: 0 - 0 - 0 - 0 |- alloc_pages_pgdat() | |- __alloc_pages() #else |----- __alloc_pages() | // 分配单个页面,愿意等,不是分配任务本身 |- if (order == 0 && (gfp_mask & __GFP_WAIT) && !(current->flags & PF_MEMALLOC)) | |- 可以从"不活跃干净队列"直接分配,可能造成盘上内容失效,但不用等待空闲页面释放 | // 不活跃页面、空闲页面都不足了 |- if (inactive_shortage() > inactive_target / 2 && free_shortage()) | |- wakeup_kswapd() | // 不活跃脏页面过多 |- else if (free_shortage() && nr_inactive_dirty_pages > free_shortage() && nr_inactive_dirty_pages >= freepages.high) | |- wakeup_bdflush() |- 根据策略指定的zonelist,进行分配 | |- if (z->free_pages >= z->pages_low) | | |- rmqueue() | | | |- 从相应大小的free_area开始分配 | | | |- expand() // 分割 | | | |- set_page_count() // 页面块第一个page计数设置为1 | |-else if (z->free_pages < z->pages_min && waitqueue_active(&kreclaimd_wait)) | | |- wake_up_interruptible() // kreclaimd() | // PAGES_HIGH -> PAGES_LOW -> schedule() -> PAGES_MIN() |- __alloc_pages_limit() | |- reclaim_page() // 从inactive_clean_list队列回收页面 | |- rmqueue() |- if (!(current->flags & PF_MEMALLOC)) | | // 问题少 | | // 碎片化严重:inactive_clean_pages/inactive_dirty_pages | |- page_launder() // page_launder()就是为内存分配本身服务的,内部也会分配内存,所以设置PF_MEMALLOC,否则会不断递归到以上的判断 | | |- reclaim_page() | | |- __free_page() | | |- rmqueue() | |- wakeup_kswapd() | |- try_to_free_pages() |- 系统出错 #endif
kswapd() |- if (inactive_shortage() || free_shortage()) | | // 断开映射,活跃->不活跃,准备换出 | |- do_try_to_free_pages() | |- // kswapd_done挂"定时任务" | |- if (free_shortage() || nr_inactive_dirty_pages > nr_free_pages() + nr_inactive_clean_pages()) | | |- page_launder() | | | // do_swap_page()恢复pte映射的页面,不立即加入active_list | | |- if (根据Access、age、引用计数,判断为活跃) | | | |- add_page_to_active_list() | | |- loop0: 只处理已经是clean的页面 | | |- loop1: PG_dirty标志位清0,用户计数+1(目的??) | | | | // 干净页面也会到inactive_dirty_list?? | | | |- if (PageDirty()) | | | | // try_to_swap_out()将page转移到inactive_dirty_list的时候,设置了page->mapping | | | | // 写完之后,为什么不移到clean_list?? | | | |- page->mapping->a_ops->writepage() | | | |- page_cache_release()->__free_page() | | | // 如果干净并且是读写缓冲页面 | | |- if (page->buffers) | | | |- try_to_free_buffers() | | | |- page_cache_release() -> __free_page() | | | | // 特殊情况 | | | |* add_page_to_inactive_clean_list() | | | |* add_page_to_active_list() | | | |* add_page_to_inactive_dirty_list() | | | // refill_inactive_scan(),将active_list中的磁盘预读页面,移到inactive_dirty_list | | |- if (page->mapping && !PageDirty(page)) | | |- del_page_from_inactive_dirty_list() | | |- add_page_to_inactive_clean_list() | |- if (free_shortage() || inactive_shortage()) | |- shrink_dcache_memory() | |- shrink_icache_memory() | | // user: waitqueue_active(&kswapd_done) | |- refill_inactive() | | | // 操作active_list中的页面 | | |- refill_inactive_scan() | | | | // 根据Access、age、引用计数 | | | | // 磁盘预读页面,都是先挂在active_list,使用计数为1,所以active_list中的page->count不一定都大于1 | | | |- deactivate_page_nolock() | | |- shrink_dcache_memory() | | |- shrink_icache_memory() | | |- swap_out() | | |- 除init进程内存信息init_mm,查看所有进程的mm,找到rss最大的 | | |- mmswap_out_mm() | | |- swap_out_vma() | | |- swap_out_pgd() | | |- swap_out_pmd() | | | // "page_table" -> "ppte" | | | // 返回值:bug/为了考查当前进程所有页面?? | | | // https://www.cs.helsinki.fi/linux/linux-kernel/2001-00/0589.html | | |- try_to_swap_out() | | | // 检查自上次try_to_swap_out()以来,CPU有没有访问过这个pte映射的页面 | | |- pte_page() | | |- if (ptep_test_and_clear_young()) | | | |- age_page_up() | | | // do_swap_page()恢复pte映射的页面,不立即加入active_list | | |- if (!PageActive()) | | | | // 即使找不到可以换出的页面,也减少了寿命 | | | |- age_page_down_ageonly() | | | // 清空pte,并返回原来的值(多CPU) | | |- ptep_get_and_clear() | | |- if (PageSwapCache()) | | | | // 原来在swapper_space.clean_pages | | | |- if (pte_dirty()) | | | | | // 通过page->list,链入swapper_space.dirty_pages | | | | |- set_page_dirty() | | | | // 盘上页面计数+1 | | | |- swap_duplicate() | | | |- set_pte() | | | | // 转移到inactive_dirty_list | | | | // 为什么不直接链入zone->inactive_clean_list?? | | | | // 因为紧接着就会调用一次__free_page(),不希望在紧接着的__free_page()中被释放,否则就跳过了"老化"过程 | | | |- deactivate_page() | | | | // 只是为释放做准备,这里不是真的释放,只是起计数-1的作用,不可能减到0 | | | |- page_cache_release() -> __free_page() | | | // 之前跟swapper_space没有联系 | | |- if (page->mapping) | | | | // mmap()或文件缓冲,本来就有对应的文件空间 | | | |- set_page_dirty() | | | // 分配盘上页面 | | |- get_swap_page() | | | // 设置page->mapping=swapper_space | | |- add_to_swap_cache() | | | |- add_to_page_cache_locked() | | | |- add_page_to_inode_queue() // page->mapping = swapper_space | | | // 接接着从swapper_space.clean_pages转移到swapper_space.dirty_pages | | |- set_page_dirty() | | | // 转移到inactive_dirty_list | | |- goto set_swap_pte | |* kmem_cache_reap() | // 不活跃脏 -> 不活跃干净 -> 空闲 |- refill_inactive_scan() |- if (!free_shortage() || !inactive_shortage()) | |- interruptible_sleep_on_timeout() // 主动让出cpu,否则一直白循环,等到时钟中断 |- else if (out_of_memory()) |- oom_kill() add_page_to_inode_queue() // page->mapping = mapping rw_swap_page_nolock() // page->mapping = &swapper_space read_swap_cache_async()/try_to_swap_out() |- add_to_page_cache_locked() |- add_page_to_inode_queue() // page->mapping = mapping try_to_swap_out() |- deactivate_page() | |- deactivate_page_nolock() | |- add_page_to_inactive_dirty_list() |- add_to_swap_cache() |- add_to_page_cache_locked() |- add_page_to_inode_queue() // page->mapping = mapping // 页面缓冲结构(交换、读写缓存) struct address_space { struct list_head clean_pages; /* list of clean pages */ struct list_head dirty_pages; /* list of dirty pages */ struct list_head locked_pages; /* list of locked pages */ unsigned long nrpages; /* number of total pages */ struct address_space_operations *a_ops; /* methods */ struct inode *host; /* owner: inode, block_device */ struct vm_area_struct *i_mmap; /* list of private mappings */ struct vm_area_struct *i_mmap_shared; /* list of shared mappings */ spinlock_t i_shared_lock; /* and spinlock protecting it */ };
handle_pte_fault() |* do_no_page() |- do_swap_page() | | // "役备":pte->p=0,只表示映射断开了,页面可能还在内存(不活跃/活跃队列) | |- lookup_swap_cache() | | // 如果完全换出了,重新从文件读回来,预读页面暂时链入active_list | |* swapin_readahead() | | | // for() | | |- read_swap_cache() | | |- read_swap_cache_async() | | | // 还在swapper_space中,直接返回 | | |- lookup_swap_cache() | | | // 否则分配页面,添加到swapper_space,并从文件读入内容(涉及文件系统) | | |* __get_free_page() -> lookup_swap_cache() | | |* add_to_swap_cache() | | |* rw_swap_page() | | // 如果swapin_readahead()分配失败,可能"礼让"过其它进程运行 | |* read_swap_cache() | | // 释放盘上页面 | |- swap_free() | // 因没有写权限导致异常,处理cow |- do_wp_page()