Linux Swap 从 userspace 到 kernel详解

Linux Swap 从 userspace 到 kernel详解

  • 简介
  • 整体架构
    • 涉及的文件介绍
    • 与内存的联系
    • 与IO的联系
  • 主要的数据结构
    • swap_info_struct
    • swp_entry_t
    • swap_extent
    • swap_cluster_info
    • swapper_spaces
    • swap cache
  • Swapout/swapin 流程分析
    • Swapout
    • Swapin
  • 附录

简介

Linux Swap 机制在很早之前已经出现,主要的目的是在很有限的内存大小的机器上虚拟地扩展内存,比如实际4G内存大小的手机或者PC,在配置了4G的Swap空间,那么该机器就相当于8G内存大小。虽然内存大小间接地扩大了一倍,但与真正的8G内存大小的性能效果完全不能对等,其大致的做法是在磁盘上配置4G大小的空间,用于存放最近不再使用的内存数据,当系统需要的时候再从该磁盘上读出来。然而磁盘的读写速度相较于内存的读写速度来说是差了一个甚至几个数量级的,所以,如果碰到系统访问的内存数据正好在磁盘上,就需要把数据从磁盘上读出来,这就会导致系统变慢。

以上的例子中,我们是在磁盘上开劈了一个4G大小的空间用作swap空间,当然也可以在其它的存储设备中开劈swap空间,比如pc上的SSD等等速度相比普通磁盘要快得多的存储设备上,另外也可以是虚拟块设备,比如手机系统中的ZRAM。而Linux Swap机制只是提供了一个很方便的扩充虚拟内存的方法,至于在哪些设备上扩充,完全由用户来决定。

既然是由用户来决定,那么我们先来看一下开辟 linux swap 空间的方法,首先我们需要在已挂载的分区目录里创建一个文件,比如我们在 /data/ 这个目录里创建一个大小为4G的名字为 swapfile.swap 的文件,我们使用dd命令来创建该文件

dd if=/dev/zero of=/data/swapfile bs=1m count=4096

然后在通过 mkswap 命令把 /data/swapfile.swap 文件格式化成swap分区

mkswap /data/swapfile

通过 swapon 命令启动 swap,这条命令里首先会去检查文件类型,是否是swap 分区,所以在使用swapon 命令之前必须通过 mkswap 命令来格式化swap 分区。

swapon /data/swapfile

这样就在 /data/swapfile.swap 开辟了4G大小的swap分区。
如此类推,假如想在SSD上开辟swap分区,那么就在挂载SSD的目录下创建这个文件,并开启它。
到这里,用户决定的swap 分区已经创建好了,接下来内存怎么被交换到swap分区,又是怎么从swap分区读回来的,这些事情就交给 linux 内核完成了,也是本文重点描述的内容。

友情提示:接下来的内容与内存管理和文件系统有关,需要先掌握这两部分的基本知识才比较容易明白。

整体架构

Linux Swap 机制的整体层级架构如下图所示
Linux Swap 从 userspace 到 kernel详解_第1张图片
整套机制是基于内存管理之下建立的,由于swap会对磁盘进行读写,所以设计上与pagecache 相类似地建立了一个 swap cache 的缓存,提高swap的读写效率。这里的swap core 指的是swap的核心层,主要完成管理各个swap分区,决策内存数据需要交换到哪个磁盘上,以及发起读写请求的工作。

当内存管理模块需要回收一个匿名页面的时候,首先先通过swap core 选择合适的swap 分区,并修改pte,指向swap entry的pte,同时把该页面加入到 swap cache 缓存起来,然后再通过swap core 发起回写请求进行回写,等待回写结束后,内存管理模块释放该页面。当用户需要访问处于swap分区的数据时,首先先通过内存管理模块确定pte的swap entry,然后在swap cache 中快速地查找swap entry 对应的物理页面,如果这时物理页面仍末被回收,就能找到对应的页面,则直接修改pte,指向该page,重新建立映射,如果没找到对应的页面,表明物理页面已经被回收了,则在swap core 中通过 swap entry 查找对应的swap 分区和数据地址,最后申请一个page并发起读操作,等待读操作完成后,修改pte,指向该page,重新建立映射。

以上描述的两个过程中,关键点有以下几个:

  1. Swap entry 的数据结构,也就是说 swap core 是怎么组织 swap entry ,又是怎么通过swap entry找到对应在swap分区的数据的。
  2. Swap cache 存储结构,即swap cache 是如何存储各个匿名页面的
  3. Page 的几个标志位,PG_writeback, PG_swapcache, PG_locked,这些标志位在交换过程中有着非常重要的作用,决定了page的生命周期。
    带着以上几个关键点,我们来从细节上了解整个swap世界。

涉及的文件介绍

Swap 涉及的文件都在 kernel-sourcecode/mm/这个目录下,查看该目录的 Makefile 文件发现当CONFIG_SWAP 这个配置项打开时会编译 page_io.c swap_state.c swapfile.c 和swap_ratio.c文件。以下逐一介绍一下,可大致了解一下,具体的调用流程在后面有描述。

  • page_io.c

    与IO相关的文件,里面定义了所有swap机制的io操作函数,主要是以下几个函数

end_swap_bio_write IO 写操作完成后回调的函数
end_swap_bio_read IO 读操作完成后回调的函数
swap_writepage IO 写操作函数
swap_readpage IO 读操作函数
swap_set_page_dirty 设置页面为脏页函数
  • swap_state.c

    与swapcache 相关的文件,也就是构架图中swap cache,里面定义了 swapcache 的增,删,查等函数,以及swap 预读函数。

  • swap_file.c

    这个是 linux swap 机制的核心文件,也就是构架图中swap core,里面包含了所有swap分区的组织方式,交换策略,开启关闭swap,用户使用的 swapon 和swapoff 的系统调用都在该文件中定义。

  • swap_ratio.c

    与swap 分区之间的使用率相关的文件

与内存的联系

在内核线程中,有一个专门用来回收物理页面的线程叫kswapd,它主要的工作之一就是寻找满足回收条件的匿名页面和文件页面,然后回收它们,当然回收他们之前就需要把物理页面的数据保存起来,这个过程称之为交换(以下称swap),文件页面自然而然地就有对应的磁盘保存位置,而对于匿名页面来说,即其数据原本不在磁盘上的情况,我们就需要用到swap分区去保存。所以swap分区只适合交换匿名页面的数据。

从整个 linux 系统内存回收的角度来看的话,我们可以这么认为,如果系统开启了 swap 机制并且用户通过 swapon 指定了 swap 分区,那么内存回收时将会回收匿名页面,否则只会回收文件页面。
我们先初步来了解一下内存回收的入口,所有的内存回收路径最终都会走到以下函数

shrink_page_list(struct list_head *page_list, struct pglist_data *pgdat, struct scan_control *sc, enum ttu_flags ttu_flags, struct reclaim_stat *stat, bool force_reclaim)

初步被选出来的系统认为满足条件的页面放在 page_list 这个链表中,至于如何选择满足条件的页面,是由内存回收算法LRU决定,这是内存管理方面的知识,这里不作详细地描述。在这个函数中会对 page_list 链表中所有的页面逐一处理,若是匿名的并且没有被 swap cache 缓存的页面,通过 add_to_swap 函数通知 swap core 和 swap cache 把该页面的数据交换出内存,随后在 try_to_unmap 函数中修改该 page 对应的 pte,使其指向 swap entry 的 pte。因此 add_to_swap 函数则是 swap core 和 swap cache 与内存回收之间的桥梁。

而另外的桥梁则是发生在缺页中断中,当进程访问的数据不在物理内存时会进入到缺页中断中,在该中断里,先去判断产生缺页中断的 pte 是否是 swap entry,如果是则调用 do_swap_page 函数把数据从磁盘上交换回内存中。

因此swap core 和 swap cache 与 memory management 之间的桥梁主要有两个,如下表所述

函数 功能 调用时机
add_to_swap 把一个page放入swap cache 的缓存中,并分配一个 swp_entry_t 类型的swap entry,然后绑定该page 内存回收时
do_swap_page 在 swap cache 缓存中寻找对应 swap entry 的page,如果找不到则从swap分区中读出数据,并与虚拟内存地址进行映射 缺页中断时

与IO的联系

接着上一节,在内存回收时,通过add_to_swap 函数把 swap entry 和 page 绑定之后,需要通过 io 操作把page 里面的数据回写到swap entry 指定的swap 分区中,具体的操作是在pageout函数里。

static pageout_t pageout(struct page *page, struct address_space *mapping, struct scan_control *sc)

首先我们先来看一下 pageout 函数中的第二个参数 struct address_space *mapping,该指针是指向 swapper_spaces[][]其中一个元素的地址,该变量是一个全局变量,当用户通过 swapon 命令增加一个swap 分区时,会分配N个 struct address_space 变量,并加到全局数组变量swapper_spaces中,一个struct address_space 管理的swap 空间大小是64M,即一个swap分区分成N个大小为64M的struct address_space,比如192M的swap 分区,在创建时被拆出了3个struct address_space,而全局数组变量swapper_spaces的第一维元素是代表第几个swap分区,第二维存储的是该swap分区所有的64M大小的struct address_space,其结构如下图所示
Linux Swap 从 userspace 到 kernel详解_第2张图片
因此pageout函数传入的mapping 参数就是swap分区中的其中一个address_space,那么接下来的流程就和普通文件的回写流程就基本一样了,通过address_space->a_ops->writepage回调的方式回写page 的数据到磁盘上,实际上是调用了 page_io.c 文件中的 swap_writepage 函数。

我们再来看一下 swap 分区 的 address_space 结构,其实 swap 分区的 address_space 精减了很多,address_space->host 指向的是NULL,address_space->a_ops 指向的是全局变量 swap_aops,在这个struct address_space_operations IO操作集中只实现了writepage, set_page_dirty, 和migratepage三个接口函数。

static const struct address_space_operations swap_aops = {
	.writepage	= swap_writepage,
	.set_page_dirty	= swap_set_page_dirty,
#ifdef CONFIG_MIGRATION
	.migratepage	= migrate_page,
#endif
};

再来看 swap_writepage 函数实现,此处省略掉非关键代码

int __swap_writepage(struct page *page, struct writeback_control *wbc,
		bio_end_io_t end_write_func)
{
	struct bio *bio;
	int ret;
	struct swap_info_struct *sis = page_swap_info(page);
    ......
	ret = 0;
	bio = get_swap_bio(GFP_NOIO, page, end_write_func);
    ......
	bio->bi_opf = REQ_OP_WRITE | wbc_to_write_flags(wbc);
    ......
	submit_bio(bio);
out:
	return ret;
}

通过 get_swap_bio 函数来获取一个bio,然后再通过 submit_bio 来提交一个 bio,剩下的事情就交给block层去完成了,读函数 swap_readpage 也类型于写函数一样,至于 bio 是怎么获取?又怎么跟 swapfile 建立联系的?会在下一节讲数据结构时详细说明。

主要的数据结构

swap_info_struct

这个结构体描述的是一个 swap 分区的具体信息(以下称之为 swap 分区描述符),一个 swap 分区对应一个 swap_info_struct 结构体,该结构体中具体的内容如下:

struct swap_info_struct {
	unsigned long	flags;		/* SWP_USED etc: see above */
	signed short	prio;		/* swap priority of this type */
	struct plist_node list;		/* entry in swap_active_head */
	struct plist_node avail_lists[MAX_NUMNODES];/* entry in swap_avail_heads */
	signed char	type;		/* strange name for an index */
	unsigned int	max;		/* extent of the swap_map */
	unsigned char *swap_map;	/* vmalloc'ed array of usage counts */
	struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
	struct swap_cluster_list free_clusters; /* free clusters list */
	unsigned int lowest_bit;	/* index of first free in swap_map */
	unsigned int highest_bit;	/* index of last free in swap_map */
	unsigned int pages;		/* total of usable pages of swap */
	unsigned int inuse_pages;	/* number of those currently in use */
	unsigned int cluster_next;	/* likely index for next allocation */
	unsigned int cluster_nr;	/* countdown to next cluster search */
	struct percpu_cluster __percpu *percpu_cluster; /* per cpu's swap location */
	struct swap_extent *curr_swap_extent;
	struct swap_extent first_swap_extent;
	struct block_device *bdev;	/* swap device or bdev of swap file */
	struct file *swap_file;		/* seldom referenced */
	unsigned int old_block_size;	/* seldom referenced */
#ifdef CONFIG_FRONTSWAP
	unsigned long *frontswap_map;	/* frontswap in-use, one bit per page */
	atomic_t frontswap_pages;	/* frontswap pages in-use counter */
#endif
	spinlock_t lock;		
	spinlock_t cont_lock;		
	struct work_struct discard_work; /* discard worker */
	struct swap_cluster_list discard_clusters; /* discard clusters list */
	unsigned int write_pending;
	unsigned int max_writes;
};

我们关注一下几个关键的成员变量

  • list
    swap_active_head 链表中的节点
  • prio
    该 swap 分区的优先级
  • type
    该变量指的是第几个 swap 分区,系统一共只允许创建 MAX_SWAPFILES 个 swap 分区,如果该swap 分区是第一个创建的,那么type 就是0,该变量同时也是 swap_info[] 全局变量的索引,即swap_info[0] 就是第一个 swap 分区的 swap_info_struct 结构体。swap_info[] 数组保存着系统中所有swap 分区描述符,即 swap_info_struct 结构体。
  • swap_map
    页槽数组。该变量是 char 型的数组,长度是该 swap 分区的页面个数,即如果该 swap 分区创建时是4G,其长度就是1048576 (4G/4K),swap_map 中每一元素都是一个页槽,linux 内核把 swap 分区以4K为单位,划分出n个存储块,即一个存储块正好能填充一个物理页面的数据,所以一个页槽就代表着一个存储块使用情况,0代表空闲状态,仍没被使用,由于一个物理页面有可能被多个进程 map,所以该值大于0则代表的是有多少个使用了该页面的进程被 unmap,swap_count 函数就是通过swap_map[n] 来得到,值得注意一下该变量有几个特殊值,如下
        #define SWAP_MAP_MAX	0x3e	/* Max duplication count, in first swap_map */
        #define SWAP_MAP_BAD	0x3f	/* Note pageblock is bad, in first swap_map */
        #define SWAP_HAS_CACHE	0x40	/* Flag page is cached, in first swap_map */
    
    SWAP_MAP_MAX 是最大的计数,即页槽的计数不能超过该值,SWAP_MAP_BAD 标明了该存储块为坏块,不能使用,SWAP_HAS_CACHE 其实就是页槽的第6位,如果页槽的第6位被置位,证明该存储块的物理页面在 swap cache 中,否则不在 swap cache 中,即该位应与进出 swap cache 同步。
  • curr_swap_extent
    是一个存储块链表的游标指针
  • first_swap_extent
    存储块链表头,描述连续的存储块的结构体是 struct swap_extent,该结构体中记录着存储块的个数(nr_pages),和该连续的存储块对应的扇区的开始位置(start_block),这里需要注意的是 start_block 需要转换成以512字节为单位的真正扇区号,block io 层才能真正找到数据存储的位置,转换方式也非常简单,即把 start_block 左移( PAGE_SHIFT - 9)位。
  • pages
    代表的是去除掉坏块之后可用存储块的个数
  • inuse_pages
    代表的是已经使用的存储块个数
  • cluster_next
    指向下一个可用存储块的编号
  • bdev
    指向该 swap 分区所处于的块设备 struct block_device 结构体
  • swap_file
    指向 swap 分区的 struct file 结构体

该结构体包含的内容非常多,这里总结一下,不管 swap 分区是一整个块设备还是一个在块设备上创建的文件,主要是把它划分成多个以一个页面为单位的存储块,并用 swap_map 来描述各个存储块的使用情况。然后以高低优先级的顺序保存在 swap_active_head 链表中。

swp_entry_t

到这里,请读者思考一个问题,如果现在把一个物理页面的数据回写到 swap 分区中的其中一个存储块,然后释放该物理页面,在下次应用进程再需要读回刚刚被回写的数据时,如何快速地寻找到那个存储块?

对内存管理比较了解的读者会第一时间想到 pte,那么 linux swap 机制就需要在回收物理页面之前修改该虚拟地址对应的 pte,使之指向一个 swp_entry_t 类型的变量,在下次访问该虚拟地址时进入缺页中断,然后通过 swp_entry_t 来找到 swap 分区中的存储块位置,并从中读回数据,最后再修改 pte 重新指向物理页面。
swap_entry_t 变量就是在整体架构章节中提到的 swap entry 的 pte。以下是该变量的定义

typedef struct {
	unsigned long val;
} swp_entry_t;

该变量可通过 set_page_private 函数保存在 page-> private 中,跟随 page 进行传递,所以如果需要该 page 的swap entry,直接访问该 page 的 private 就能得到,其实它就是一个 unsigned long 类型的值,在这个数值中包含了两个信息,一个是 swap 分区的 type(以下称 swap type),另一个是存储块的编号,即 offset (以下称 swap offset),第2到第7位存放 swap type,第8位到第57位存放 swap offset,最低两位保留不用,第58位必须等于0,因为该位置1是代表无效的pte,可参见源码注释

 *	bits 0-1:	present (must be zero)
 *	bits 2-7:	swap type
 *	bits 8-57: swap offset
 *	bit  58:	PTE_PROT_NONE (must be zero)

内核里有以下api,可以很方便地在 swp_entry_t 变量和 swap typeswap offsetpte 之间转换:

__swp_type(swp_entry_t) 从swp_entry_t中得到swap type
__swp_offset(swp_entry_t) 从swp_entry_t中得到swap offset
__swp_entry(type,offset) 把swap type 和 swap offset 合并成 swp_entry_t
__pte_to_swp_entry(pte) 把pte 格式化成 swp_entry_t
__swp_entry_to_pte(swp) 把swp_entry_t格式化成pte

得到 swap typeswap offset 之后,我们就可以通过全局数组变量 swap_info[swap type] 来得到swap 分区描述符 swap_info_struct,再通过 swap offset 找到对应的存储块描述符 swap_extent,再把swap_extent->start_block 转换成 bio 所需的扇区号,然后通过 submit_bio 函数来发起读请求,最后等待block io层完成读操作。这一过程的关键在于bio的组建,即如何得到扇区号。

swap_extent

存储块描述符,用于描述多个连续的存储块,以及描述与块设备中扇区的映射关系。其数据结构如下

struct swap_extent {
	struct list_head list;
	pgoff_t start_page;
	pgoff_t nr_pages;
	sector_t start_block;
};
  • list
    链表节点,其链表头是 swap 分区描述符的 first_swap_extent 变量
  • start_page
    描述的第一个存储块编号
  • nr_pages
    描述存储块个数
  • start_block
    描述该第一个存储块对应的扇区编号

因此这里涉及到了两个比较关键的概念,一个是存储块,另一个是扇区编号。
存储块的大小为 4KB,扇区的大小为 512Bytes,所以一个存储块中包含 8个 连续的扇区。
存储块的编号是虚拟的,是连续递增的,swp_entry_t中的swap offset 变量所指的就是存储块编号。
扇区编号是真实地指向块设备的扇区号,是 block io 层需要的变量,所以在给定 swap offset 时,可以通过该结构体找到对应的块设备扇区号,从而发起 io 操作

以下通过图解来描述一下这两者的映射关系

Linux Swap 从 userspace 到 kernel详解_第3张图片
黄色小方块代表真正 block 设备的一个扇区,红色方块代表一个 4KB 大小的存储块,一个存储块中包含有 8个 连续的扇区,多个存储块之间如果是连续的,可以合并成一个 swap_extent 结构来描述,如1号2号存储块,其中 start_page 的变量描述的第一个存储块编号,而nr_pages是描述了有多少个连续的存储块,start_block 经过左移(PAGE_SHIFT-9)位得到该存储块对应 block 设备的开始扇区编号。举例说明一下,如果 swp_entry_tswap offset 是2,也就是 2号存储块,那么找到 start_page 小于等于2并且(start_page + nr_pages)大于等于2的 swap_extent,然后通过(start_block+ offset-start_page) 找到真正的扇区编号。这样 bio 就可以组建起来,并发给 block io 层去进行读写操作了。

swap_cluster_info

为了快速地寻找一个空闲的页槽,在ssd等非旋转类磁盘(即非机械硬盘)作为swap分区时,就不能只是简单地全局遍历 swap_map[] 数组了,需要增加一种机制(以下称之为ssd算法)来辅助查找页槽,我们先来看一下如果直接遍历 swap_map[] 数组来的查找空闲页槽会有什么样的问题,如果当前系统有很多进程需要去交换内存,那么就会同时访问 swap_map[] 数组,从而产生资源竞争,就需要全局的 spin_lock 来保护,在这种情况下如果系统中有 n个 cpu 同时访问这个数组,那么只能一个一个地访问,从而阻塞了其它 cpu 的执行,就好比如n个人走一条独木桥一样,他们都走完独木桥就需要花很多时间。解决这个问题的方法也很简单,架设多条独木桥,一次可以通过n个人,其实ssd算法就类似于这种架设多条独木桥的方法。具体的做法是,把所有的页槽组织成以 256个 页槽为一个簇的链表,如下图所示
Linux Swap 从 userspace 到 kernel详解_第4张图片
蓝色小方块是一个页槽,白色大方块是一个簇(cluster),每个簇中都有一个单独的 spin_lock,用于保护这个簇对应的 256个 页槽。如果有两个 cpu 同时访问 swap 分区不同簇的页槽就不会有竞争关系,比如 cpu0 访问第2个页槽,cpu1 访问第260个页槽,这个时候两个cpu申请的 spin_lock 不是同一个,所以可以顺利申请得到,不会产生 spin_lock 竞争,这样就提高了访问效率。
我们先来看一下一个簇的数据结构 struct swap_cluster_info

struct swap_cluster_info {
	spinlock_t lock;	
	unsigned int data:24;
	unsigned int flags:8;
};

dataflags 两个位域组成一个 unsigned int 的类型的变量,低8位为flags,高24位是data。

  • data:如果该簇是空闲的,这个变量代表的是下一个空闲的簇编号,如果该簇不是空闲的,那么该变量代表是已经分配的页槽数
  • flags:有三种flag,如下:
#define CLUSTER_FLAG_FREE 1 /* This cluster is free */
#define CLUSTER_FLAG_NEXT_NULL 2 /* This cluster has no next cluster */
#define CLUSTER_FLAG_HUGE 4 /* This cluster is backing a transparent huge page */
第三种flag先本文不作介绍,需要开启CONFIG_THP_SWAP这个配置才用到。

在 swap 分区描述符中有一个数组保存所有簇,该数组变量是 cluster_info,其数组的索引值就是空闲簇的 data,如下图所示,data 变量指向的是下一个空闲簇在 cluster_info 数组中的索引值,最后一个空闲簇的data等于自身的索引值。
Linux Swap 从 userspace 到 kernel详解_第5张图片
swap_info_struct 结构中还有另一个变量,是 free_clusters,它是一个所有空闲簇的链表,但这个链表结构与常规的结构不同,如上图所示,free_clusters 中只有两个 struct swap_cluster_info 类型的变量,一个保存的是第一个空闲簇,另一个保存的是最后一个空闲簇,这个链表是通过 data 变量来连接起来,并不是指针的方式。由于 data 变量是指向下一个空闲簇的编号,所以通过 data 变量就可以把所有空闲簇都连接起来。这样做的目的是可以节省变量的存储空间,因为swap分区会比较大,所需要记录的页槽太多。
以下列出ssd算法的几个关键操作函数,如向 clusters 链表中增加一个簇,删除一个簇,判断链表是否为空,判断簇的状态等等

cluster_list_add_tail(struct swap_cluster_list *list, struct swap_cluster_info *ci, unsigned int idx) ci指向swap_info_struct->cluster_info,idx 代表的是全局簇数组cluster_info的索引号,即往list中插入一个swap_info_struct->cluster_info[idx]簇
cluster_list_del_first(struct swap_cluster_list *list, struct swap_cluster_info *ci) ci指向swap_info_struct->cluster_info。从list链表里删除第一个簇
struct swap_cluster_info * lock_cluster(struct swap_info_struct *si,unsigned long offset) 获取offset页槽对应的簇的spin_lock,并返回该簇指针
unlock_cluster(struct swap_cluster_info *ci) 解锁ci 簇的cispin_lock,需要与lock_cluster成对使用
cluster_list_empty(struct swap_cluster_list *list) 判断该list 是否为空
cluster_list_first(struct swap_cluster_list *list) 返回该list的第一个簇索引号
alloc_cluster(struct swap_info_struct *si, unsigned long idx) 分配簇索引号为idx的簇,即把簇从free_clusters的链表中取出
free_cluster(struct swap_info_struct *si, unsigned long idx) 释放一个簇索引号为idx的簇,即把簇放回free_clusters的链表中

swapper_spaces

swapper_spaces 在整体架构章节中有提到过,它是一个二维数组,其元素是 struct address_space 类型的结构体变量,第一维代表第几个 swap 分区,即与 swap_info_struct 中的type 同步,至于第二维的描述可回顾整体架构章节中的与IO的联系一节。所以在给定 swp_entry_t 之后就可以通过以下宏得到 struct address_space

#define swap_address_space(entry)			    \
	(&swapper_spaces[swp_type(entry)][swp_offset(entry) \
		>> SWAP_ADDRESS_SPACE_SHIFT])

struct address_space 这个结构体的作用是管理一个最大为 64MBswap cache,以及提供一套操作函数集(a_ops)给io回写使用。以下分析一下swapper_spaces初始化的代码

int init_swap_address_space(unsigned int type, unsigned long nr_pages)
{
	struct address_space *spaces, *space;
	unsigned int i, nr;

	nr = DIV_ROUND_UP(nr_pages, SWAP_ADDRESS_SPACE_PAGES); //求出该swap分区需要多少个64M大小的address_space
	spaces = kvzalloc(sizeof(struct address_space) * nr, GFP_KERNEL); //为该swap分区分配空间用于存储address_space
	if (!spaces)
		return -ENOMEM;
	for (i = 0; i < nr; i++) { //遍历所有的address_space 并初始化它
		space = spaces + i;
		INIT_RADIX_TREE(&space->page_tree, GFP_ATOMIC|__GFP_NOWARN); //初始化swap cache 的树
		atomic_set(&space->i_mmap_writable, 0);
		space->a_ops = &swap_aops;  // 初始化a_ops,使其指向swap_aops 的操作集
		/* swap cache doesn't use writeback related tags */
		mapping_set_no_writeback_tags(space);
		spin_lock_init(&space->tree_lock);
	}
	nr_swapper_spaces[type] = nr;
	rcu_assign_pointer(swapper_spaces[type], spaces); //把该swap 分区的所有address_spaces 存入swapper_spaces中

	return 0;
}

swap cache

swap cache 指的是交换缓存区,它的作用类似 page cache,主要是提高读写效率。swap cache 的数据结构是基数树,其根节点是 address_space->page_tree,基数树的数据结构在这里不展开描述,以下列出与 page cache 增,删,查的操作函数,以及什么时候会加入到 swap cache,什么时候会从中删除,即增加和删除的时机。

__add_to_swap_cache(struct page *page, swp_entry_t entry) 增加一个page 到swap cache 中,并把entry 保存到page->private变量中
__delete_from_swap_cache(struct page *page) 从page cache 中删掉page
lookup_swap_cache(swp_entry_t entry, struct vm_area_struct *vma, unsigned long addr) 从swap cache查找entry 指定的page

把page加入交换缓存区的时机:

  1. 本来在物理内存没有数据,需要从 swap 分区中把数据读回时,即 swapin,需要加入到 swap cache 中。
  2. 系统回收内存时,选择一个匿名页面进行回收,则先把该page 放入swap cache

把 page 从交换缓存区中删除掉的时机:

  1. 该 page 已经没有进程需要了,根据 page_swapped(page) 来判断。
  2. 该 page 的 PG_writebackPG_dirty 都为 0 时,并且系统急需回收内存时。
  3. 该 page 发生了写时复制,或者发生写访问异常时,并且只一本进程使用该 page, 即写访问的方式调用 do_swap_page

Swapout/swapin 流程分析

了解了关键数据结构以及swap分区的组织方式之后,接下来通过代码分析swapout 与 swapin 的流程

Swapout

Swapout 的入口是在shrink_page_list, 即当系统需要回收物理内存时发生swapout 的动作,先来看一下一个匿名页面回收的整个过程框图
Linux Swap 从 userspace 到 kernel详解_第6张图片
整个回收过程分为两次 shrink,这里面的原因是因为IO的写速度会很慢,不能阻塞内存回收的进程,所以第一次shrink只是发起了一个回写请求然后就返回了,等待IO的回写操作完成后,第二次回收该匿名页面时,再把它回收掉。
先来看一下第一次shrink的过程,以及page的状态变化过程

static unsigned long shrink_page_list(struct list_head *page_list,
				      struct pglist_data *pgdat,
				      struct scan_control *sc,
				      enum ttu_flags ttu_flags,
				      struct reclaim_stat *stat,
				      bool force_reclaim)
{
	LIST_HEAD(ret_pages); //初始化返回的链表,即把此次shrink无法回收的页面放入该链表中
	LIST_HEAD(free_pages); //初始化回收的链表,即把此次shrink 可以回收的页面放入该链表中
…
	while (!list_empty(page_list)) {
…
		page = lru_to_page(page_list); 
		list_del(&page->lru); // 从 page_list 中取出一个 page,page_list 需要回收的page链表

		if (!trylock_page(page))  //先判断是用否有别的进程在使用该页面,如果没有则设置PG_lock,并返回1, 这个flag多用于io读, 但此时第一次shrink时大多数情况下是没有别的进程在使用该页面的,所以接着往下走
			goto keep;

		may_enter_fs = (sc->gfp_mask & __GFP_FS) ||
			(PageSwapCache(page) && (sc->gfp_mask & __GFP_IO));

		if (PageAnon(page) && PageSwapBacked(page)) { //判断是否是匿名页面并且不是lazyfree的页面,显然这个条件是满足的
			if (!PageSwapCache(page)) { //判断该匿名页面是否是 swapcache ,即通过page的 PG_swapcache 的flag 来判断,此时该页面第一次 shrink,所以这里是否,进入if里面的流程
                  …
				if (!add_to_swap(page)) //为该匿名页面创建swp_entry_t,并存放到page->private变量中,把page放入 swap cache,设置page的PG_swapcache和PG_dirty的flag,并更新swap_info_struct的页槽信息,该函数是通往 swap core 和swap cache的接口函数,下面会分析
				{
                            …
						goto activate_locked; // 失败后返回
				}
…
				/* Adding to swap updated mapping */
				mapping = page_mapping(page); // 根据page中的swp_entry_t获取对应的swapper_spaces[type][offset],这里可回顾一下数据结构章节中的swapper_spaces的介绍。
			}
		} else if (unlikely(PageTransHuge(page))) {
			/* Split file THP */
			if (split_huge_page_to_list(page, page_list))
				goto keep_locked;
		}

		/*
		 * The page is mapped into the page tables of one or more
		 * processes. Try to unmap it here.
		 */
		if (page_mapped(page)) {
			enum ttu_flags flags = ttu_flags | TTU_BATCH_FLUSH;

			if (unlikely(PageTransHuge(page)))
				flags |= TTU_SPLIT_HUGE_PMD;
			if (!try_to_unmap(page, flags, sc->target_vma)) { // unmap, 即与上层的虚拟地址解除映射关系,并修改pte,使其值等于 page->private,即swp_entry_t变量,等到swapin 时就直接把pte强制类型转换成swp_entry_t 类型的值,就可以得到entry了。
				nr_unmap_fail++;
				goto activate_locked;
			}
		}

		if (PageDirty(page)) { //由于add_to_swap 函数最后把该页面设置为脏页面,所以该if成立,进入if里面
			…
						/*
			 * Page is dirty. Flush the TLB if a writable entry
			 * potentially exists to avoid CPU writes after IO
			 * starts and then write it out here.
			 */
			try_to_unmap_flush_dirty();
			switch (pageout(page, mapping, sc)) { // 发起 io 回写请求,并把该page 的flag 设置为PG_writeback,然后把PG_dirty清除掉
		    ……
			case PAGE_SUCCESS: //如果请求成功,返回 PAGE_SUCCESS
				if (PageWriteback(page))  //该条件成立,跳转到 keep
					goto keep;
                   ……
			}
		}
……
keep:
		list_add(&page->lru, &ret_pages); //把该页面放到 ret_pages链表里,返回时会把该链表中的所有页面都放回收lru 链表中,即不回收页面
		VM_BUG_ON_PAGE(PageLRU(page) || PageUnevictable(page), page);
	}
……

	list_splice(&ret_pages, page_list);
……
	return nr_reclaimed;
}
接下来再看一下add_to_swap函数实现
int add_to_swap(struct page *page)
{
	swp_entry_t entry;
	int err;
     ……
	entry = get_swap_page(page); //为该页面分配一个swp_entry_t,并更新swap_info_struct的页槽信息
	if (!entry.val)
		return 0;
    ……
	err = add_to_swap_cache(page, entry,
			__GFP_HIGH|__GFP_NOMEMALLOC|__GFP_NOWARN); //把页面加入到swap cache 中,设置PG_swapcache,并把entry 保存到page->private变量中,跟随page传递
	/* -ENOMEM radix-tree allocation failure */
	
	set_page_dirty(page); // 设置该页面为脏页

	return 1;
     ……
}

再来看一下 get_swap_page 函数,分析一下 swp_entry_t 是如何分配出来的,页槽信息怎么更新。kernel 4.14的内核在实现该函数时因为增加了槽缓存的机制,会比较复杂,其实即是预先分配好几个swp_entry_t 缓存起来,需要时从缓存分配,不过我们抓住关键几个步骤就可以了,先不考虑槽缓存

swp_entry_t get_swap_page(struct page *page)
{
	swp_entry_t entry, *pentry;
	struct swap_slots_cache *cache;

	entry.val = 0;
     ……
	get_swap_pages(1, false, &entry); // 1代表需要分配一个存储块,false 代表

	return entry;
}

直接跳到 get_swap_pages 函数,该函数的目的有两个,一个是找出一个合适的 swap 分区,另一个是从 swap 分区中快速地找到合适的存储块,即 swap offset。

int get_swap_pages(int n_goal, bool cluster, swp_entry_t swp_entries[])
{
…

start_over:
	node = numa_node_id(); //获取numa 节点号
	plist_for_each_entry_safe(si, next, &swap_avail_heads[node], avail_lists[node]) { //以优先级高到低遍历所有swap分区
         ……
start:
		spin_lock(&si->lock);
		if (!si->highest_bit || !(si->flags & SWP_WRITEOK)) { //判断该swap分区是否还有空闲页槽,以及判断该swap分区是否可写的,SWP_WRITEOK这个flag是在swapon 的时候设置的,一般都带这个flag
			spin_lock(&swap_avail_lock);
			if (plist_node_empty(&si->avail_lists[node])) {
				spin_unlock(&si->lock);
				goto nextsi;
			}
			WARN(!si->highest_bit,
			     "swap_info %d in list but !highest_bit\n",
			     si->type);
			WARN(!(si->flags & SWP_WRITEOK),
			     "swap_info %d in list but !SWP_WRITEOK\n",
			     si->type);
			__del_from_avail_list(si);  //如果没有空闲页槽,则直接从swap_avail_heads链表中删除掉
			spin_unlock(&si->lock);
			goto nextsi; //跳到下一个swap分区
		}
		if (cluster) { // 这里是false ,因此走else路径,当内核的CONFIG_THP_SWAP的功能打开时,这里才是true
			if (!(si->flags & SWP_FILE))
				n_ret = swap_alloc_cluster(si, swp_entries);
		} else
			n_ret = scan_swap_map_slots(si, SWAP_HAS_CACHE,
						    n_goal, swp_entries);  //走到这里,说明已经找到合适的swap分区,即第一个目的已经完成,接下来通过该函数去寻找合适的 offset,最终把两者合并后返回swp_entries。
		……
nextsi:
		if (plist_node_empty(&next->avail_lists[node])) 
			goto start_over;
	}

	spin_unlock(&swap_avail_lock);

check_out:
	if (n_ret < n_goal)
		atomic_long_add((long)(n_goal - n_ret) * nr_pages,
				&nr_swap_pages);
noswap:
	return n_ret;
}

scan_swap_map_slots 比较复杂,其做法是通过 ssd算法 快速地查找到可用页槽编号。

static int scan_swap_map_slots(struct swap_info_struct *si,
			       unsigned char usage, int nr,
			       swp_entry_t slots[])
{
     ……
	scan_base = offset = si->cluster_next; //获取下一个可用存储块的编号,即空闲页槽的编号

	/* SSD algorithm */
	if (si->cluster_info) {
		if (scan_swap_map_try_ssd_cluster(si, &offset, &scan_base)) //通过簇来辅助查找空闲页槽的编号,并重新设置offset变量
			goto checks; //如果找到则跳到下面的 checks
		else
			goto scan; //如果找不到,则跳到scan,全局地遍历 swap_map 看还有没有空闲的页槽
	}
     ……
checks:
	if (si->cluster_info) {  //这个if代码段中主要的功能是确保 offset 所处属的簇必须是空闲簇链表free_cluster的第一个簇
		while (scan_swap_map_ssd_cluster_conflict(si, offset)) {
		/* take a break if we already got some slots */
			if (n_ret)
				goto done;
			if (!scan_swap_map_try_ssd_cluster(si, &offset,
							&scan_base))
				goto scan;
		}
	}
	
	ci = lock_cluster(si, offset);   //获取簇的spin_lock
	/* reuse swap entry of cache-only swap if not busy. */
	if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) { //如果该swap分区已经满了,则尝试回收部分页槽
		int swap_was_freed;
		unlock_cluster(ci);
		spin_unlock(&si->lock);
		swap_was_freed = __try_to_reclaim_swap(si, offset); //回收页槽
		spin_lock(&si->lock);
		/* entry was freed successfully, try to use this again */
		if (swap_was_freed)
			goto checks;  //如果回收到了页槽,则返回check 继续分配页槽
		goto scan; /* check next one */
	}
     ……
	si->swap_map[offset] = usage;  //更新页槽的第6位,即SWAP_HAS_CACHE
	inc_cluster_info_page(si, si->cluster_info, offset);  //把该页槽对应的簇从free_clusters的链表中取出来,并更新其data字段,即为1
	unlock_cluster(ci);  //解锁 spin_lock

	swap_range_alloc(si, offset, 1);  //更新swap_info_struct中的inuse_pages 变量,即加1
	si->cluster_next = offset + 1;  //更新下一个可用页槽的编号,即往后移一位
	slots[n_ret++] = swp_entry(si->type, offset);  //把 swap分区的type 和页槽篇号offset合并成swp_entry_t保存在slots[]中,返回给该函数调用者

	/* got enough slots or reach max slots? */
	if ((n_ret == nr) || (offset >= si->highest_bit))
		goto done;  //slot[] 填满后直接返回,这里在不考虑页槽缓存的情况下,slot[]数组中只需要一个swp_entry_t就可以了
……

done:
	si->flags -= SWP_SCANNING;
	return n_ret;

scan:
	spin_unlock(&si->lock);
	while (++offset <= si->highest_bit) {  //从当前的页槽号往后查找
		if (!si->swap_map[offset]) {  //页槽值为0,代表该页槽是空闲状态
			spin_lock(&si->lock);
			goto checks;   //找到后跳到 checks 更新页槽信息后直接返回
		}
		if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) {
			spin_lock(&si->lock);
			goto checks;  //如果swap 分区满了,则跳回checks 回收页槽
		}
          ……
	}
	offset = si->lowest_bit;
	while (offset < scan_base) {  //从当前的页槽号往前查找,下面的逻辑与上面的while 逻辑一样
		if (!si->swap_map[offset]) {
			spin_lock(&si->lock);
			goto checks;
		}
		if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) {
			spin_lock(&si->lock);
			goto checks;
		}
		if (unlikely(--latency_ration < 0)) {
			cond_resched();
			latency_ration = LATENCY_LIMIT;
		}
		offset++;
	}
	spin_lock(&si->lock);
    // 两个while循环已经代表了全局地扫描了swap_map数组,如果程序走到这,说明该swap分区里已经没有了可用页槽
no_page:
	si->flags -= SWP_SCANNING;
	return n_ret;
}

我们再来看一下ssd算法的核心函数 scan_swap_map_try_ssd_cluster,先来看一下它的参数

  • struct swap_info_struct *si
    指向swap分区描述符
  • unsigned long *offset
    ssd算法查找到的空闲页槽编号
  • unsigned long *scan_base
    ssd算法查找到的空闲页槽编号

函数的返回值是个bool类型,true代表已经找到空闲页槽编号,false 代表没有找到。

static bool scan_swap_map_try_ssd_cluster(struct swap_info_struct *si,
	unsigned long *offset, unsigned long *scan_base)
{
    ……
new_cluster:
	cluster = this_cpu_ptr(si->percpu_cluster);
	if (cluster_is_null(&cluster->index)) {  //这个if代码段主要是从free_clusters链表中获取第一个空闲簇,并赋值到per_cpu变量si->percpu_cluster,之后会对这个空闲簇进行可用页槽的查找
		if (!cluster_list_empty(&si->free_clusters)) {
			cluster->index = si->free_clusters.head;
			cluster->next = cluster_next(&cluster->index) *
					SWAPFILE_CLUSTER;
		} else if (!cluster_list_empty(&si->discard_clusters)) {
			/*
			 * we don't have free cluster but have some clusters in
			 * discarding, do discard now and reclaim them
			 */
			swap_do_scheduled_discard(si);
			*scan_base = *offset = si->cluster_next;
			goto new_cluster;
		} else
			return false;
	}

	found_free = false;

	/*
	 * Other CPUs can use our cluster if they can't find a free cluster,
	 * check if there is still free entry in the cluster
	 */
	tmp = cluster->next;  //获取当前可分配的簇
	max = min_t(unsigned long, si->max,
		    (cluster_next(&cluster->index) + 1) * SWAPFILE_CLUSTER);  //获取该簇的最大页槽索引
	if (tmp >= max) {
		cluster_set_null(&cluster->index);
		goto new_cluster;
	}
	ci = lock_cluster(si, tmp);  //加锁,保护该簇对应的256个页槽
	while (tmp < max) {
		if (!si->swap_map[tmp]) {
			found_free = true;  //找到一个空闲的页槽,跳出循环
			break;
		}
		tmp++;
	}
	unlock_cluster(ci);  //解锁
	if (!found_free) {
		cluster_set_null(&cluster->index);
		goto new_cluster;  //如果仍然没找到页槽,则返回new_cluster,从free_clusters链表中取出新的空闲簇再进行查找
	}
	cluster->next = tmp + 1;  //更新下一个可分配的簇
	*offset = tmp;  //把已经找到的页槽更新到offset 变量
	*scan_base = tmp; //把已经找到的页槽更新到scan_base 变量
	return found_free;
}

到此,基本的ssd算法已经分析完毕,读者可能会感觉到有些吃力,但只要抓住减少 spin_lock 竞争这一条主要目的去阅读这部分的代码,是比较好理解的。后面内核版本中还有一些针对提高 swap 页槽查找以及分配效率的修改,有兴趣的读可以看一下附录文章1 ,相信可以加深对本文的理解。
以上是第一次 shrink 的流程,至此,page 只是发起了 io 回写申请,然后就直接返回到 lru 链表中,并没有真正地回收页面。
在第一次 shrink 流程执行完后,其 pageflag 状态如下

PG_writeback PG_swapcache PG_dirty PG_active
1 1 0 0

真正的回收页面发生在第二次shrink到该page时,即io回写完成之后,我们先来看一下 bio->bi_end_io 回调函数,即io回写完成后的回调函数,请注意pageflag状态变换

void end_swap_bio_write(struct bio *bio)
{
	struct page *page = bio->bi_io_vec[0].bv_page;  //从bio中获取page,即发起io回写的page

	if (bio->bi_status) {  //bio发生错误会进入该if代码段,在些分析中省略
		…
	}
	end_page_writeback(page);  //核心函数
	bio_put(bio);
}

再来看 end_page_writeback 函数

void end_page_writeback(struct page *page)
{
	/*
	 * TestClearPageReclaim could be used here but it is an atomic
	 * operation and overkill in this particular case. Failing to
	 * shuffle a page marked for immediate reclaim is too mild to
	 * justify taking an atomic operation penalty at the end of
	 * ever page writeback.
	 */
	if (PageReclaim(page)) {
		ClearPageReclaim(page);  //清除PG_reclaim标记位
		rotate_reclaimable_page(page);
	}

	if (!test_clear_page_writeback(page))  //清除PG_writeback标记位
		BUG();

	smp_mb__after_atomic();
	wake_up_page(page, PG_writeback);  //唤醒正在等该page的PG_writeback标记位的进程
}

所以,当page的io回写完成后,其PG_writeback和PG_reclaim两个标记位会被清除掉,那么当page 第二次shrink时,其flag状态应该就是如下表所示

PG_writeback PG_swapcache PG_dirty PG_active
0 1 0 0

我们再来看一下第二次shrink的流程,其实可以跳过很多if判断,直接进入到回收流程

static unsigned long shrink_page_list(struct list_head *page_list,
				      struct pglist_data *pgdat,
				      struct scan_control *sc,
				      enum ttu_flags ttu_flags,
				      struct reclaim_stat *stat,
				      bool force_reclaim)
{
	……
	while (!list_empty(page_list)) {
	     ……
		if (PageWriteback(page)) {  //跳过该判断
			……
		}

		
		if (PageAnon(page) && PageSwapBacked(page)) { 
			if (!PageSwapCache(page)) {   //跳过该判断
				
			}
		} 

		if (page_mapped(page)) {  //由于在第一次shrink的时候unmap了,所以此时page_mapped等于0,跳过该判断
             ……
		}

		if (PageDirty(page)) {  //跳过该判断
			……
		}
		……
		if (PageAnon(page) && !PageSwapBacked(page)) {   //非lazyfree的页面,走else
	        ……
		} else if (!mapping || !__remove_mapping(mapping, page, true)) //这里进入到__remove_mapping中,把page从swapcache 中移除掉,并清除页槽的第6位,即SWAP_HAS_CACHE
			goto keep_locked;
		……
free_it:
    ……
		list_add(&page->lru, &free_pages);  //把该page加入到free_pages 链表中等待回收
    ……  //接下来的事情就是回收free_pages链表中的所有page,
	return nr_reclaimed;
}

以上是整个swapout的流程,这里总结一下,第一次去shrink匿名页面时,会先为page里的数据分配一块swap分区的存储块,然后发起一个io写请求,请求把page的数据写入到这块存储块中,同时把该page放入swap cache 中,接着返回,并等待io写操作完成,第二次去shrink该page时,从swap cache 中删除该page,并回收它。

Swapin

swapin 的入口是do_swap_page,由于物理页面被回收了,所以进程再次访问一块虚拟地址时,就会产生缺页中断,最终进入到 do_swap_page,在这个函数中会重新分配新的页面,然后再从swap分区读回这块虚拟地址对应的数据。具体请看以下代码分析

int do_swap_page(struct vm_fault *vmf)
{
	……
	entry = pte_to_swp_entry(vmf->orig_pte);  //从pte中获取swap entry,即把orig_pte 强制类型转换成swp_entry_t类型
    ……
	page = lookup_swap_cache(entry, vma, vmf->address);  //在 swap cache 中查找entry 对应的page
	swapcache = page;

	if (!page) {  //如果在swap cache中没找到,则进入if代码段
		struct swap_info_struct *si = swp_swap_info(entry);  //获取swap分区描述符

          ……
			page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vmf);  //分配一个page,并从swap分区中读出数据填充到page中,再把page放入swap cache中缓存,此时page的PG_lock被置位了,需要等待IO读操作完成才清零,即page被lock住,如果别人想lock该page,则需要等待该page被unlock
			swapcache = page;
		……
	}

	locked = lock_page_or_retry(page, vma->vm_mm, vmf->flags);  //此时尝试去lock该page,成功则返回1,失败则返回0
    ….
	if (!locked) {  //显然此时返回是0,即page的IO读操作仍末完成。
		ret |= VM_FAULT_RETRY;  //设置返回标记为retry
		goto out_release; //返回重新尝试 do_swap_page,但在重新尝试do_swap_page时则可以从page cache 中直接获取到该page,不需要再从swap分区中读数据了
	}
     //程序走到这表明该page的IO读操作已经完成

	……
	pte = mk_pte(page, vmf->vma_page_prot);  //根据page的物理地址,以及该page的保护位生成pte
	if ((vmf->flags & FAULT_FLAG_WRITE) && reuse_swap_page(page, NULL)) {  //如果该缺页中断为写访问异常时,并且page只有一个进程使用,则把该page从swap cache 中删除,并清除对应在swap分区中的数据,下面会分析reuse_swap_page函数
		pte = maybe_mkwrite(pte_mkdirty(pte), vmf->vma_flags);  //设置pte中的可写保护位和PTE_DIRTY位
		vmf->flags &= ~FAULT_FLAG_WRITE;
		ret |= VM_FAULT_WRITE;
		exclusive = RMAP_EXCLUSIVE;
	}
	……
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);  更新该虚拟地址对应的pte
	……
		do_page_add_anon_rmap(page, vma, vmf->address, exclusive);  //建立新的匿名映射
		mem_cgroup_commit_charge(page, memcg, true, false);
		activate_page(page);  //把该page放入active anonymouns lru链表中
	……

	swap_free(entry);  //更新页槽的counter,即减一,如果counter等于0,说明需要该存储块数据的人已经全部读回到内存,并且该page也不在swap cache 中,那么直接清除存储块数据,即回收页槽,释放更多的swap空间
	if (mem_cgroup_swap_full(page) ||
	    (vmf->vma_flags & VM_LOCKED) || PageMlocked(page))
		try_to_free_swap(page);  //如果swap分区满了,则尝试回收无用页槽
	unlock_page(page);  //解锁 PG_lock
	……
out:
	return ret;
   ……
}

接下来分析一下两个核心函数 swapin_readaheadreuse_swap_page
swapin_readahead 最终会进入swap_cluster_readahead函数或者swap_vma_readahead函数,两者的区别在于预读的方式,前者是预读 page 对应的物理地址前后范围的数据,后者是预读虚拟地址前后范围的数据,其实现上相差不大,所以我们先来分析 swap_cluster_readahead,至于另一个函数有兴趣的读者可参考此分析自行查阅代码。

struct page *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
				struct vm_fault *vmf)
{
	……
	mask = swapin_nr_pages(offset) - 1;  // 获取需要预读的页槽个数
	if (!mask) 
		goto skip;  //如果不需要预读,则只读当前entry指向的页槽,跳到skip

	do_poll = false;
	/* Read a page_cluster sized and aligned cluster around offset. */
	start_offset = offset & ~mask;  //得到预读开始的页槽
	end_offset = offset | mask;  //得到预读结束的页槽
     ……
	for (offset = start_offset; offset <= end_offset ; offset++) { 从开始的页槽一个个地预读
		/* Ok, do the async read-ahead now */
		page = __read_swap_cache_async(
			swp_entry(swp_type(entry), offset),
			gfp_mask, vma, addr, &page_allocated);  //分配新的page,并放入swap cache,设置PG_swapcache,把entry 保存到page->private变量中,跟随page传递,置位页槽的第6位,即SWAP_HAS_CACHE
……
		if (page_allocated) { //该变量为true,代表page cache中没有页槽对应的page,是重新分配的page,则需要把页槽所指向的swap分区中的数据读出来放到page中。
			swap_readpage(page, false);  //发起io读请求,该实现与swapout时调用的swap_writepage类似,这里不再分析,这里再提醒一下,等待io读操作完成后该page的PG_lock才会被清零,具体的清零操作是在bio的回调函数end_swap_bio_read中完成。读者可回忆一下io写操作完成后清零page的PG_writeback位。
			……
		}
		put_page(page);
	}
     ……
skip:
	return read_swap_cache_async(entry, gfp_mask, vma, addr, do_poll);  //这里只读一个页槽的数据,其实现与上面预读多个页槽的逻辑基本一样,这里不再分析
}

__read_swap_cache_async 这个函数主要完成以下几个工作

  1. 再次从 swap cache 查找页槽对应的 page,若找到,则直接返回
  2. 若仍没有找到,则分配一个新的 page,并设置页槽第6位,即 SWAP_HAS_CACHE
  3. 设置 pagePG_lockPG_swapback,并把page加入到 swap cache
  4. page存入 inactive anonymouns lru 链表中
  5. 返回page的指针
struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
			struct vm_area_struct *vma, unsigned long addr,
			bool *new_page_allocated)
{
	do {
		found_page = find_get_page(swapper_space, swp_offset(entry));  //第一步
		if (found_page)
			break;
         ……
		if (!new_page) {
			new_page = alloc_page_vma(gfp_mask, vma, addr);   //第二步中分配新的page
			if (!new_page)
				break;		/* Out of memory */
		}

		……
		err = swapcache_prepare(entry);  //第二步中的设置页槽第6位
		……
		/* May fail (-ENOMEM) if radix-tree node allocation failed. */
		__SetPageLocked(new_page);   //第三步
		__SetPageSwapBacked(new_page);  //第三步
		err = __add_to_swap_cache(new_page, entry);  //第三步中的加入page到swap cache中
		if (likely(!err)) {
              ……
			lru_cache_add_anon(new_page);  //第四步
			*new_page_allocated = true;
			return new_page;  //第5步,返回page指针
		}
		……
	} while (err != -ENOMEM);

	if (new_page)
		put_page(new_page);
	return found_page;
}

接下来看另一个核心函数 reuse_swap_page,该函数的作用是尝试把只有一个进程使用的页槽回收掉。

bool reuse_swap_page(struct page *page, int *total_map_swapcount)
{
	……
	count = page_trans_huge_map_swapcount(page, &total_mapcount, &total_swapcount); //获取所有使用该paga的进程数量,返回count为总数量,total_mapcount为已经与虚拟地址map 的进程数量,total_swapcount 为还没与虚拟地址map 的数量
	if (total_map_swapcount)
		*total_map_swapcount = total_mapcount + total_swapcount;  //更新形参total_map_swapcount
	if (count == 1 && PageSwapCache(page) &&
	    (likely(!PageTransCompound(page)) ||
	     /* The remaining swap count will be freed soon */
	     total_swapcount == page_swapcount(page))) {  //这里的判断有三个,分别是使用该page的进程总数为1,即只有一个进程使用; 该page是在swap cache 中; 当前进程的虚拟地址仍没有与该page建立映射,即仍未map。当这三个条件都满足时则认为该页槽指向的存储块数据再也不需要了,可以清除掉。
		if (!PageWriteback(page)) {  //该page是否正在回写磁盘,如果已经回写完成则回收页槽,否则走else,返回false,表明页槽没回收成功。
			page = compound_head(page); 
			delete_from_swap_cache(page);  //从swap cache中删除,并回收页槽
			SetPageDirty(page);  //把page的PG_dirty位置1
		} else {
			swp_entry_t entry;
			struct swap_info_struct *p;

			entry.val = page_private(page);
			p = swap_info_get(entry);
			if (p->flags & SWP_STABLE_WRITES) {
				spin_unlock(&p->lock);
				return false;
			}
			spin_unlock(&p->lock);
		}
	}

	return count <= 1;
}

delete_from_swap_cache 这个函数其实有两部分组成,__delete_from_swap_cache和put_swap_page,__delete_from_swap_cache函数是从swap cache 中删除一个page的核心函数,put_swap_page功能是把页槽所指向的存储块数据清除。

附录


  1. https://lwn.net/Articles/704478/ ↩︎

你可能感兴趣的:(linux,memory,management)