本文转自网络文章,内容均为非盈利,版权归原作者所有。
转载此文章仅为个人收藏,分享知识,如有侵权,马上删除。
原文作者:jmpcall
专栏地址:https://zhuanlan.kanxue.com/user-815036.htm
1. 交换分区概念
一个cpu,同一时刻只能执行一条指令,执行某个进程时,其它进程一定是停止状态。那么,当cpu切换到进程B运行时,如果系统中已经没有空闲的物理内存页面了,内核就会选择一个其它进程占用的物理内存页面,将其内容备份到磁盘上,这样就可以先腾出来给当前正在运行的进程B使用了,这个过程叫做"页面换出",当切换到进程A运行时,再重新分配一个物理内存页面(如果也没有空闲物理内存页面,同样执行换出操作),并且从磁盘上恢复换出的内容,这个过程叫做"页面换入"。用于备份内存数据的磁盘空间,就是"交换分区"。
不过,以上描述仅仅是为了快速了解"交换分区"的含义,Linux内核对"交换"过程的实现非常精细,采用的并不是这种最简单粗暴的思路。另外,换出过程是由程序执行"页面换出"函数主动触发的,而换入过程则依赖cpu在执行地址映射的过程中触发,所以,80386为此提供了一项硬件特性:当页表页最低位为0时,会穿过一道"门",执行内核设置的缺页异常处理函数:
- 目录项以及当页表项最低位为1时,含义是由cpu硬件层的设计决定:"目录项/页表项 & 0xfffff000"为所指物理内存页面的地址(4K对齐),低12位为所指物理内存页面的属性(目录项和页表项的属性位稍有区别):
目录项有的"PS位"为1时,表示直接指向目标页面,并且页面大小为4M,使映射过程少了一层,所以页表项中不需要包含这个位;另外,目录项中不包含D位,而页表项中包含。关于页表项中的属性位,目前需要知道的有:
① 第0位(Present位)
表示映射页面是否在内存中,不在的话有2种情况,一种是整个pte为0,即"Linux内核笔记005"中映射断开,或根本就没有建立映射的情况,另一种只是P位为0,但pte整体值不为0,就是今天要学习的"物理内存页面换出到交换分区"的情况。
② 第1位(Writable位)
表示物理内存页面是否可写(page结构中的flags成员,也包含一个标志位,用于表示物理内存页面是否可写,但完全由软件层使用,vm_area_struct结构中的vm_flags成员,又包含用于表示虚拟地址区间是否可写的标志位,随着学习到更多情景,就会逐渐理解它们的作用)。
③ 第5位(Accessed位)
cpu每次通过目录项/页表项映射得到一个物理内存页面时,就会自动将该位设置为1,具体作用见稍后内容。
④ 第6位(Dirty)
cpu每次写访问一个物理内存页面,就会自动将相应pte的Dirty位设置为1,具体作用见稍后内容。
- 当页表项最低位为0时,含义是由软件层的设计决定,但前提是,cpu在这种情况会跳转到内核指定的缺页异常处理函数执行:Linux内核的缺页异常处理函数为do_page_fault(),它按4K大小将每个交换分区划分成一组盘上页面,用pte1~8位索引整个系统中的某个交换分区,用高24位索引指定交换分区中的某个盘上页面,同时,内核还对这种情况下的pte换了一个命名——"swp_entry_t表项":
总结以上两点,映射过程中,如果pte的P位为1,pte由cpu硬件逻辑使用,映射结果为一个物理内存页面,如果pte的P位为0,pte由内核的缺页异常处理函数do_page_fault()使用,映射结果为一个交换分区中的盘上页面,do_page_fault()会重新分配物理内存页面并建立映射,再根据盘上页面换入数据,这样,跳回导致异常的那条指令重新执行,就能成功访问内存了:
2. 物理页面的使用和周转
书中强调了3种页面的含义,它们都是地址按4K对齐,大小为4K的空间,区别是所在的空间不同:
① 虚拟页面:在虚拟内存空间中,虚拟内存空间的大小由指针类型的位置决定,是虚拟地址的集合,用于描述程序的逻辑;
② 内存页面:在物理内存空间中,物理内存空间的大小由主板上安插的内存条决定,真正用于存储进程需要的数据;
③ 盘上页面:在交换分区(文件)中,安装操作系统时划分,进入系统后,可以通过命令打开/关闭,用于辅助存储内存数据。
CPU的保护模式与实模式,是两套不同的硬件逻辑,保护模式下才有虚拟地址,并且,直接出现在程序中的地址,也一定是虚拟地址,必须映射到物理地址才能访问。Linux内核笔记005已经学习过管理区的概念,当物理页面处于空闲状态时,是通过struct page"对象"的list成员,挂在所属管理区的某个free_area链表中,接下来的学习内容,是介绍物理页面更多的状态,以及与交换分区、更多链表之间的联系。
- 内存性质
书中按存储内容和分配方式,对物理页面的性质进行了归纳,转换成表格的形式为:
Linux内核的镜像是elf文件,代码段、数据段(全局变量)的内容都在文件中,计算机通电后,BIOS中的程序,会从引导分区将内核镜像的内容加载到内存,然后才跳转到内核的代码执行,所以在内核执行之前就已经确定了,由于这部分是整个系统运行依赖的最基本数据,在关机之前都不会释放,同时为了整个系统的效率可控,也不会被换出;
内核也经常需要动态分配内存(kmalloc()、vmalloc()、alloc_page()区别暂时不用关心,下一集就会播放),用户进程的创建与销毁就是一个特例,比如在命令行执行./a.out,就会进入shell进程的内核态,分配进程管理结构task_struct(紧接着新进程的内核栈,每个进程都有独立的内核栈,属于第四章内容),并且根据a.out文件分配及加载代码段、数据段,另外还会分配一块默认大小的用户栈,而a.out进程正常或异常结束后,又会进入父进程的内核态回收该进程占用的所有内存;
(内核的概念实在太多了,而且非常抽象,我有时候会是把整个系统想象成一个完整的"大程序",这样视野自然就"抬高"了,然后只关注这个"大程序"的运行过程:开机被加载->切换到保护模式运行->创建init进程->启动shell->在shell进程内核态创建新进程->在某个进程内核态切换进程执行,以及发生中断等情况时,这个"大程序"具体做了什么。这样,就可以验证并加深对概念的理解,更可以专注在内核的逻辑上,避免不自觉的站在应用程序的角度去恐惧内核)
内核中有些动态分配的内存,用完后只会断开pte映射,并不会直接放回free_area,这样不是自己用不了,别人也分配不到了吗?
换出操作也可以将物理页面回收到free_area,同直接释放的区别是,如果物理页面不是文件缓存页面,换出操作结束,会在交换分区占用一个盘上页面,这个小小的代价,可以换到巨大的性能收益:内核采用这种方式处理的页面,存储的内容一定是读取比较耗时的(比如文件的dentry、inode信息,或者文件的内容,是需要从磁盘读取的),用完后记录对应的磁盘页面地址,放入一个LRU队列,如果系统内存紧张,换出机制会很快将它回收到free_area,如果系统内存不紧张(通常情况下,系统的内存使用率不会过高),它用会一直保持内容留在LRU队列中,再次到访问相应磁盘页面的时候,就可以从队列中找到它,不用重新读取磁盘了。
需要明白的是,这种情况只是巧妙利用了换出机制,让物理页面能尽量赖着不回free_area,换出机制的设计本意并不是用于这种情况,而是腾出近期可能不会被访问的内存(选择的依据是最近最少使用(LRU)),处理的是正在使用的页面,它先会断开pte映射,将页面转移到LRU队列,然后才将LRU队列中的页面内容,保存到磁盘,将页面转换为可分配状态。内核空间相比单个进程的用户空间,访问频率要高的多,所以换出的实现,不会断开内核地址的pte映射,也就是书上说"用户空间页面才会被换出"的原因,"用户空间页面"是指在至少一个进程的用户空间有映射的页面(那么零拷贝抓包,从内核态映射到用户态的报文缓存,也是有可能被换出的)。
- 交换策略
"临阵磨枪"是一种最容易实现的方式,就是每次遇到缺页异常,需要"腾出"一个页面的时候,才进行换出操作,就是说换出操作和换入操作发生在同一时刻,每次调用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
3. 物理内存页面的分配
#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
4. 换出
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 */
};
5. 换入
换入操作最开始的入口为do_page_fault()函数,do_page_fault()有些部分分支,在Linux内核笔记005已经看到过了,换入的情景,会执行到handle_pte_fault()函数。换入的页面,如果还在内存,通过lookup_swap_cache()可以直接找到,因为换出时,会将(swp_entry, page)这对信息存到一个hash表中,通过swp_entry可以找到page,而如果页面已经不在内存,就真的要从磁盘读入了。
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()