Linux内核笔记006 - 交换分区

发布者:jmpcall
发布于:2020-06-28 21:43

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()


声明:该文观点仅代表作者本人,转载请注明来自看雪