Linux内存回收之(一)---交换分区

Linux内存回收之(一)---交换分区

Posted by Albert on November 23, 2020

Linux内存回收之(一)—交换分区

一、前言

​ 研究Linux swap机制,对于研究内存回收机制十分重要。我们首先研究Linux 2.6.11版本的实现,之所以选择2.6.11版本,是因为大名鼎鼎的《深入理解Linux内核》就是以该内核版本为基础讲解的(本文针对2.6.11版本的大部分说明都是参考该书的17章-回收页框),然后通过Linux后续的针对swap 的patch来研究整个swap的变更历史。

二、交换分区的作用

​ 交换分区是用来为内存中的非映射页在磁盘上提供备份,从而将非映射页占用的内存释放,当需要使用这些非映射页的时候,再从交换分区中加载到内存中。也就是说就是当内存不足的时候,把一部分硬盘空间虚拟成内存使用,从而解决内存容量不足的情况

2.1 需要进行交换的页

  • ​ 属于进程的匿名线性区的页(进程的堆、栈等)

  • ​ 属于进程的私有映射的脏页

  • ​ 属于IPC共享的内存的页

2.2 交换子系统的主要功能

交换子系统的主要功能总结如下:

  • 在磁盘上建立交换区,用于存放没有磁盘映像的页。
  • 管理交换区空间。当需求发生时,分配与释放页槽(页槽是交换分区中的一页存储)。
  • 提供函数用于从 RAM 中把页换出到交换区或从交换区换入到 RAM(内存) 中。
  • 利用页表项(现已被换出的换出页的页表项)中的换出页标识符(指定交换分区、页槽)跟踪数据在交换区中的位置。

​ 交换是页框回收的一个最高级特性。如果需要确保进程的所有页框都能被 PARA 随意回收,而不仅仅是回收有磁盘映像的页,就必须使用交换。可以用 swapoff 命令关闭交换,但随着磁盘系统负载增加,很快磁盘系统就会瘫痪。交换可以用来扩展内存地址空间,使之被用户态进程有效地使用,但性能比 RAM 慢几个数量级。

三、2.6.11内核中交换分区的实现

​ 从内存中换出的页存放在交换区中,交换区的实现可以使用自己的磁盘分区,也可以使用包含在大型分区中的文件。可定义几种不同的交换区,最大个数由 MAX_SWAPFILES (通常被设置为32)宏确定。如果有多个交换区,就允许系统管理员把大的交换空间分布在几个磁盘上,以使硬件可以并发操作这些交换区; 这一处理还允许在系统运行时不用重新启动系统就可以扩大交换空间的大小。

​ 每个交换区都由一组页槽组成,即一组 4096 字节大小的块组成,每块中包含一个换出的页。交换区的第一个页槽用来永久存放有关交换区的信息,其个数由 swap_header 联合体描述。magic 结构提供了一个字符串,用来把磁盘某部分明确地标记为交换区,它只含有一个字段 magic.magic,该字段含有一个 10 字符的“magic”字符串。magic 结构从根本上允许内核明确地把一个文件或分区标记成交换区,该字符串的内容就是“SWAPSPACE2”,通常位于第一个页槽的末尾。

3.1 交换区描述符

​ 每个活动的交换区在内存中都有自己的 swap_info_struct 描述符。它被放在swap_info全局数组里面.结构体如下:

struct swap_info_struct {
	/**
	 * 交换区标志
	 */
	unsigned int flags;
	/** 
	 * 保护交换区的自旋锁。它防止 SMP 系统上对交换区数据结构的并发访问
	 */
	spinlock_t sdev_lock;
	/**
	 * 指针,指向存放交换区的普通文件或设备文件的文件对象。
	 */
	struct file *swap_file;
	/**
	 * 存放交换区的块设备描述符。
	 */
	struct block_device *bdev;
	/**
	 * 指向交换区的子区链表的头部。
	 */
	struct list_head extent_list;
	/**
	 * 组成交换区的子区数量。
	 */
	int nr_extents;
	/**
	 * 指向最近使用的子区描述符的指针。
	 */
	struct swap_extent *curr_swap_extent;
	/**
	 * 存放交换区的磁盘分区自然块大小。
	 */
	unsigned old_block_size;
	/**
	 * 指向计数器数组的指针,交换区的每个页槽对应一个数组元素。如果这个计数器的值等于0,那么页槽就是空闲的,如果计数器值大于0,
	 那么换出页就是填充了这个页槽,实际上这个值是表示贡享这个换出页的进程数量,如果计数器的值为 SWAP_MAP_MAX(32767),那么存放在该页槽中的页就是“永    久”的,并且不能从相应的页槽中删除。如果计数器的值是 SWAP_MAP_BAD(32768),那么就认为该页槽是有缺陷的,不可用。
	 */
	unsigned short * swap_map;
	/**
	 * 在搜索一个空闲页槽时要扫描的第一个页槽。
	 */
	unsigned int lowest_bit;
	/**
	 * 在搜索一个空闲页槽时要扫描的最后一个页槽。
	 */
	unsigned int highest_bit;
	/**
	 * 在搜索一个空闲页槽时要扫描的下一个页槽。
	 */
	unsigned int cluster_next;

	unsigned int cluster_nr;
	/**
	 * 交换区优先级。表示交换子系统依据该值考虑每个交换区的次序
	 */
	int prio;			/* swap priority */
	/**
	 * 可用页槽的个数。
	 */
	int pages;
	/**
	 * 交换区的大小,以页为单位。
	 */
	unsigned long max;
	/**
	 * 交换区内已用页槽数。
	 */
	unsigned long inuse_pages;
	/**
	 * 指向下一个交换区描述符的指针。
	 */
	int next;			/* next entry on swap list */
};
swap_info 数组包括 MAX_SWAPFILES 个交换区描述符。只有那些设置了 SWP_USED 标志的交换区才被使用,因为它们是活动区域。下图说明了 swap_info 数组(每一个元素代表一个交换区)、一个交换区和相应的计数器数组的情况。如果swap_map中该页槽为0,那么代表这是一个空闲的页槽;如果大于0,说明页槽被多个进程使用;如果页槽等于32768,那么代表一个有缺陷的页槽。

swap_info_struct

​ 由于swap分区也是被映射为文件或者是设备文件的,根据struct file *swap_file,该文件的inode就确定了交换分区在磁盘的位置。后续根据页槽号,以及交换分区子区在磁盘的上的起始扇区位置,自然就知道每个页槽在磁盘上的位置,这点可以看Linux虚拟文件系统中相关部分,我们这里不再过多的解释。

​ 在激活交换分区的时候(sys_swapon()),根据swap_file->f_dentry->d_inode可以从inode中获取该文件的所有信息,另外将inode中的相关磁盘信息赋值到swap_info_struct->curr_swap_extent结构体中。(对于磁盘交换分区,只有一个子区,子区里面存放了该子区在磁盘里面的第一个扇区等信息, 对于文件映射的交换分区,可能有多个子区,原理类似,感兴趣的可以直接阅读内核代码)

//根据子区的首页索引、子区的磁盘块那么可以很方便的知道某一个页槽在磁盘中的存放位置
struct swap_extent {
	/**
	 * 子区链表指针。
	 */
	struct list_head list;
	/**
	 * 子区的首页索引。
	 */
	pgoff_t start_page;
	/**
	 * 子区页数。
	 */
	pgoff_t nr_pages;
	/**
	 * 子区的起始磁盘区号。
	 */
	sector_t start_block;
};

3.2 交换分区layout

​ 交换分区的layout如下图所示,其中第0块页槽主要存放swap header, 用于描述交换分区

swa_layout

​ 代码如下:已经标识出每个字段的作用:

/**
 * 每个交换区的第一个页槽用来永久存放有关交换区的信息,用本结构表示。
 */
union swap_header {
	struct {
		char reserved[PAGE_SIZE - 10];
		/**
		 * 交换区标识,一般是"SWAPSPACE2"
		 */
		char magic[10];			/* SWAP-SPACE or SWAPSPACE2 */
	} magic;
	struct {
		/**
		 * 不用于交换算法。存放分区数据、磁盘标签等。
		 */
		char	     bootbits[1024];	/* Space for disklabel etc. */
		/**
		 * 交换区的版本。
		 */
		unsigned int version;
		/**
		 * 可有效使用的最后一个页槽。
		 */
		unsigned int last_page;
		/**
		 * 有缺陷的页槽个数。
		 */
		unsigned int nr_badpages;
		/**
		 * 填充字段。
		 */
		unsigned int padding[125];
		/**
		 * 共637个字节,用来指定有缺陷页槽的位置。
		 */
		unsigned int badpages[1];
	} info;
};

3.3 换出页标识符

swap_out_des

​ 通过在 swap_info 数组中指定交换区的索引和在交换区内指定页槽的索引,可简单而又唯一地标识一个换出页。由于交换区的第一个页(索引为 0)留给 swap_header 联合体,第一个可用页槽的索引就为 1。交换区的最大值由表示页槽的可用位数决定, 在 80x86 体系结构结构上,有 24 位可用,这就限制了交换区的最大大小为:2的24次方 个4 K页面,也就是64 GB 。 \(2 ^ {24} * 4K = 64GB\) ​ 页被换出时,将换出页标识符覆盖掉页表项,后续MMU触发缺页异常,从交换区中将内容又加载到内存中。当页被换出时,其换出标识符就作为页的表项插入页表中,这样在需要时就可以在找到该页。要注意这种标识符的最低位与 Present 标志对应,通常被清除来说明该页目前不在 RAM 中。但是,剩余 31 位中至少一位被置位,因为没有页存放在交换区 0 的页槽 0 中。这样就可以从一个页表项中区分三种不同的情况:

  • 全部为0 ,该页不属于进程的地址空间,或相应的页框还没有分配给进程(请求调页,分配内存、建立页表映射)。
  • 前 31 位不等于 0,最后一位等于 0,该页被换出(这个时候通过相关回调函数换入,如handle_pte_fault中代码所示)
  • 最低位等于 1,该页包含在 RAM 中。

页被换出时,将换出页标识符覆盖掉进程的页表项,后续再次访问的时候MMU触发缺页异常,从交换区中将内容又加载到内存中(SWAP_IN)。如下是80x86触发缺页异常时的部分代码:

static inline int handle_pte_fault(struct mm_struct *mm,
	struct vm_area_struct * vma, unsigned long address,
	int write_access, pte_t *pte, pmd_t *pmd)
{
	pte_t entry;

	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.
		 */
		/**
		 * pte_none返回1,说明页从未被进程访问且没有映射磁盘文件。
		 */
		if (pte_none(entry))
			return do_no_page(mm, vma, address, write_access, pte, pmd);
		/**
		 * pte_file返回1,说明页属于非线性磁盘文件的映射。
		 */
		if (pte_file(entry))
			return do_file_page(mm, vma, address, write_access, pte, pmd);

		/**
		 * 这个页曾经被访问过,但是其内容被临时保存在磁盘上。
		 */
		return do_swap_page(mm, vma, address, pte, pmd, entry, write_access);
	}
    /*
      ...............以下代码忽略.................  
    */
}

​ 由于一个物理页可以属于几个进程的地址空间,所以它可能从一个进程的地址空间被换出,但仍旧保留在主存中;因此可能把同一个页换出多次。当然,一个页在物理上只被换出并存储一次,但后来每次试图换出该页都会增加 swap_map 计数器的值,在试图换出一个已经换出的页时就会调用 swap_duplicate()。该函数只是验证以参数传递的换出页标识符是否有效,并增加相应的 swap_map 计数器的值。

3.3 激活和禁用交换区

3.3.1 激活交换分区

激活交换分区完成了以下的事情:

  1. 检查进程是否具有权限激活交换分区。

  2. 在交换区描述符 swap_info 数组的前 nr_swapfiles 个元素中查找 SWP_USED 标志为 0(即对应的交换区不是活动的)的第一个描述符。

    初始化这个描述符的字段,即把 flags 置为 SWP_USED,把 lowest_bit 和 highest_bit 置为 0。

  3. 如果 swap_flags 参数为新交换区指定了优先级,则设置描述符的 prio 字段。否则,就把所有活动交换区中最低位的优先级减 1 后赋给这个字段(这样就假设最后一个被激活的交换区在最慢的块设备上)。如果没有其它交换区是活动的,就把该字段设置为 -1。

  4. 调用 filp_open() 打开由 specialfile 参数指定的文件。把 filp_open() 返回的文件对象地址存放在交换区描述符的 swap_file 字段。

  5. 检查 swap_info 中其它的活动交换区,确认该交换区还未被激活。检查交换区描述符的 swap_file->f_mapping 字段中存放的 address_space 对象地址。 如果交换区已被激活,则返回错误码。

  6. 调用 read_cache_page() 读入存放在交换区页槽 0 的 swap_header 描述符。 参数为:swap_file->f_mapping 指向的 address_space 对象、页索引 0、文件 readpage 方法的地址(存放在 swap_file->f_mapping->a_ops->readpage)和指向文件对象 swap_file 的指针。然后等待直到页被读入内存。

  7. 根据存放在 swap_header 联合体的 info.last_page 字段中的交换区大小,初始化交换区描述符的 lowest_bit 和 hightest_bit 字段。

  8. 通过访问第一个页槽中的 info.last_page 和 info.nr_badpages 字段计算可用页槽的个数,并把它存入交换区描述符的 pages 字段。而且把交换区中的总页数赋给 max 字段。

  9. 为新交换区建立子区链表 extent_list(如果交换区建立在磁盘分区上,则只有一个子区),并相应地设定交换区描述符的 nr_extents 和 curr_swap_extent 字段。这里会确定交换分区的中子区在磁盘上的起始扇区,block数等,然后将其赋值给swap_info_struct->curr_swap_extent,这样我们通过页槽索引号,和起始扇区就可以构造bio向磁盘写入和读出页槽的数据了。

  10. 把交换区描述符的 flags 字段设为 SWP_ACTIVE。

  11. 更新 nr_good_pages、nr_swap_pages 和 total_swap_pages 三个全局变量。

  12. 把新交换区描述符插入 swap_list 变量所指向的链表中。

感兴趣的可以直接阅读源码。觉得源码枯燥的可以直接略过,源码就是上面描述的所有步骤的详细流程:

/**
 *
激活交换区系统调用。
 *		specialfile:		设备文件或分区的路径径名(用户态地址空间),或指向实现交换区的普通文件的路径名。
 *		swap_flags:			由一个单独的SWAP_FLAG_PREFER位加上交换区优先级的31位组成。只有在SWAP_FLAG_PREFER位置位时,优先级才有效。
 */
asmlinkage long sys_swapon(const char __user * specialfile, int swap_flags)
{
	struct swap_info_struct * p;
	char *name = NULL;
	struct block_device *bdev = NULL;
	struct file *swap_file = NULL;
	struct address_space *mapping;
	unsigned int type;
	int i, prev;
	int error;
	static int least_priority;
	union swap_header *swap_header = NULL;
	int swap_header_version;
	int nr_good_pages = 0;
	unsigned long maxpages = 1;
	int swapfilesize;
	unsigned short *swap_map;
	struct page *page = NULL;
	struct inode *inode = NULL;
	int did_down = 0;

	/**
	 * 检查当前进程是否具有CAP_SYS_ADMIN权限。
	 */
	if (!capable(CAP_SYS_ADMIN))
		return -EPERM;
	
	//对操作的swap描述符进行加锁操作
	swap_list_lock();

	//存放所有交换区的数组
	p = swap_info;
	/**
	 * 在交换区数据组中查找SWP_USED标志为0的第一个一个描述符。
	 */
	for (type = 0 ; type < nr_swapfiles ; type++,p++)
		if (!(p->flags & SWP_USED))
			break;
	error = -EPERM;
	/*
	 * Test if adding another swap device is possible. There are
	 * two limiting factors: 1) the number of bits for the swap
	 * type swp_entry_t definition and 2) the number of bits for
	 * the swap type in the swap ptes as defined by the different
	 * architectures. To honor both limitations a swap entry
	 * with swap offset 0 and swap type ~0UL is created, encoded
	 * to a swap pte, decoded to a swp_entry_t again and finally
	 * the swap type part is extracted. This will mask all bits
	 * from the initial ~0UL that can't be encoded in either the
	 * swp_entry_t or the architecture definition of a swap pte.
	 */
	if (type > swp_type(pte_to_swp_entry(swp_entry_to_pte(swp_entry(~0UL,0))))) {
		swap_list_unlock();
		goto out;
	}
	if (type >= nr_swapfiles)
		nr_swapfiles = type+1;
	INIT_LIST_HEAD(&p->extent_list);
	/**
	 * 找到交换区索引,初始化描述符。
	 */
	p->flags = SWP_USED;
	p->nr_extents = 0;
	p->swap_file = NULL;
	p->old_block_size = 0;
	p->swap_map = NULL;
	p->lowest_bit = 0;
	p->highest_bit = 0;
	p->cluster_nr = 0;
	p->inuse_pages = 0;
	spin_lock_init(&p->sdev_lock);
	p->next = -1;
	/**
	 * 如果参数为新交换区指定了优先级,则设置描述符的prio字段。
	 */
	if (swap_flags & SWAP_FLAG_PREFER) {
		p->prio =
		  (swap_flags & SWAP_FLAG_PRIO_MASK)>>SWAP_FLAG_PRIO_SHIFT;
	} else {
		/**
		 * 否则将交换区的优先级设置为当前优先级中最低优先级再减一。也就是说,新交换区是最低优先级。
		 */
		p->prio = --least_priority;
	}
	swap_list_unlock();
	/**
	 * 从用户态地址空间复制specialfile参数所指向的字符串。
	 */
	name = getname(specialfile);
	error = PTR_ERR(name);
	if (IS_ERR(name)) {
		name = NULL;
		goto bad_swap_2;
	}
	/**
	 * 打开指定的文件。
	 */
	swap_file = filp_open(name, O_RDWR|O_LARGEFILE, 0);
	error = PTR_ERR(swap_file);
	if (IS_ERR(swap_file)) {
		swap_file = NULL;
		goto bad_swap_2;
	}

	/**
	 * 将打开的文件描述符存放在swap_file中。
	 */
	p->swap_file = swap_file;
	mapping = swap_file->f_mapping;

	//指向inode
	inode = mapping->host;

	error = -EBUSY;
	/**
	 * 检查交换区,以确认该交换区没有被激活。
	 */
	for (i = 0; i < nr_swapfiles; i++) {
		struct swap_info_struct *q = &swap_info[i];

		if (i == type || !q->swap_file)
			continue;
		if (mapping == q->swap_file->f_mapping)
			goto bad_swap;
	}

	error = -EINVAL;
	/**
	 * 检查打开文件是否为一个块设备文件。
	 */
	if (S_ISBLK(inode->i_mode)) {
		bdev = I_BDEV(inode);
		/**
		 * bd_claim将交换子系统设置成块设备的占有者。如果块设备已经有一个占有者,则返回错误码。
		 */
		error = bd_claim(bdev, sys_swapon);
		if (error < 0) {
			bdev = NULL;
			goto bad_swap;
		}
		/**
		 * 把块设备的当前块大小存放在交换区描述符的old_block_size字段,然后把设备的块设备大小设成页的大小。
		 */
		p->old_block_size = block_size(bdev);
		error = set_blocksize(bdev, PAGE_SIZE);
		if (error < 0)
			goto bad_swap;
		/**
		 * 将块设备描述符存入交换区描述符的bdev字段。
		 */
		p->bdev = bdev;
	} else if (S_ISREG(inode->i_mode)) {
		/**
		 * 交换区是一个普通文件。
		 */
		p->bdev = inode->i_sb->s_bdev;
		down(&inode->i_sem);
		did_down = 1;
		/**
		 * 检查文件索引节点i_flags字段中的S_SWAPFILE字段。如果该标志置位,说明文件已经用做交换区,返回失败。
		 */
		if (IS_SWAPFILE(inode)) {
			error = -EBUSY;
			goto bad_swap;
		}
	} else {
		goto bad_swap;
	}

	swapfilesize = i_size_read(inode) >> PAGE_SHIFT;

	/*
	 * Read the swap header.
	 */
	if (!mapping->a_ops->readpage) {
		error = -EINVAL;
		goto bad_swap;
	}
	/**
	 * 读取存放在交换区页槽0中的swap_header描述符。
	 */
	page = read_cache_page(mapping, 0,
			(filler_t *)mapping->a_ops->readpage, swap_file);
	if (IS_ERR(page)) {
		error = PTR_ERR(page);
		goto bad_swap;
	}
	wait_on_page_locked(page);
	if (!PageUptodate(page))
		goto bad_swap;
	kmap(page);
	swap_header = page_address(page);

	/**
	 * 检查最后10个字符,以确定版本号。
	 */
	if (!memcmp("SWAP-SPACE",swap_header->magic.magic,10))
		swap_header_version = 1;
	else if (!memcmp("SWAPSPACE2",swap_header->magic.magic,10))
		swap_header_version = 2;
	else {
		printk("Unable to find swap-space signature\n");
		error = -EINVAL;
		goto bad_swap;
	}
	
	switch (swap_header_version) {
	case 1:
		printk(KERN_ERR "version 0 swap is no longer supported. "
			"Use mkswap -v1 %s\n", name);
		error = -EINVAL;
		goto bad_swap;
	case 2:
		/* Check the swap header's sub-version and the size of
                   the swap file and bad block lists */
		if (swap_header->info.version != 1) {
			printk(KERN_WARNING
			       "Unable to handle swap header version %d\n",
			       swap_header->info.version);
			error = -EINVAL;
			goto bad_swap;
		}

		p->lowest_bit  = 1;
		/*
		 * Find out how many pages are allowed for a single swap
		 * device. There are two limiting factors: 1) the number of
		 * bits for the swap offset in the swp_entry_t type and
		 * 2) the number of bits in the a swap pte as defined by
		 * the different architectures. In order to find the
		 * largest possible bit mask a swap entry with swap type 0
		 * and swap offset ~0UL is created, encoded to a swap pte,
		 * decoded to a swp_entry_t again and finally the swap
		 * offset is extracted. This will mask all the bits from
		 * the initial ~0UL mask that can't be encoded in either
		 * the swp_entry_t or the architecture definition of a
		 * swap pte.
		 */
		maxpages = swp_offset(pte_to_swp_entry(swp_entry_to_pte(swp_entry(0,~0UL)))) - 1;
		/**
		 * 根据last_page确定交换区的大小。
		 */
		if (maxpages > swap_header->info.last_page)
			maxpages = swap_header->info.last_page;
		/**
		 * 根据交换区大小设置lowest_bit和highest_bit
		 */
		p->highest_bit = maxpages - 1;

		error = -EINVAL;
		if (swap_header->info.nr_badpages > MAX_SWAP_BADPAGES)
			goto bad_swap;
		
		/* OK, set up the swap map and apply the bad block list */
		/**
		 * 分配新交换区相关的计数器数组。并将它存放在交换区描述符的swap_map中。
		 */
		if (!(p->swap_map = vmalloc(maxpages * sizeof(short)))) {
			error = -ENOMEM;
			goto bad_swap;
		}

		error = 0;
		/**
		 * 根据bad_pages字段中存放的有缺陷的页槽链表把计数器数组元素初始化成0或者SWAP_MAP_BAD。
		 */
		memset(p->swap_map, 0, maxpages * sizeof(short));
		for (i=0; i<swap_header->info.nr_badpages; i++) {
			int page = swap_header->info.badpages[i];
			if (page <= 0 || page >= swap_header->info.last_page)
				error = -EINVAL;
			else
				p->swap_map[page] = SWAP_MAP_BAD;
		}
		/**
		 * 计算可用页槽数。
		 */
		nr_good_pages = swap_header->info.last_page -
				swap_header->info.nr_badpages -
				1 /* header page */;
		if (error) 
			goto bad_swap;
	}
	
	if (swapfilesize && maxpages > swapfilesize) {
		printk(KERN_WARNING
		       "Swap area shorter than signature indicates\n");
		error = -EINVAL;
		goto bad_swap;
	}
	if (!nr_good_pages) {
		printk(KERN_WARNING "Empty swap-file\n");
		error = -EINVAL;
		goto bad_swap;
	}
	p->swap_map[0] = SWAP_MAP_BAD;
	p->max = maxpages;
	p->pages = nr_good_pages;

	/**
	 * 为新交换区建立子区链表,并设置交换区描述符的nr_externs和curr_swap_extent字段。
	 */
	error = setup_swap_extents(p);
	if (error)
		goto bad_swap;

	down(&swapon_sem);
	swap_list_lock();
	swap_device_lock(p);
	/**
	 * 设置flag标志为SWP_ACTIVE,然后更新几个全局变量。
	 */
	p->flags = SWP_ACTIVE;
	nr_swap_pages += nr_good_pages;
	total_swap_pages += nr_good_pages;
	printk(KERN_INFO "Adding %dk swap on %s.  Priority:%d extents:%d\n",
		nr_good_pages<<(PAGE_SHIFT-10), name,
		p->prio, p->nr_extents);

	/* insert swap space into swap_list: */
	/**
	 * 将交换区插入swap_list链表中。
	 */
	prev = -1;
	for (i = swap_list.head; i >= 0; i = swap_info[i].next) {
		if (p->prio >= swap_info[i].prio) {
			break;
		}
		prev = i;
	}
	p->next = i;
	if (prev < 0) {
		swap_list.head = swap_list.next = p - swap_info;
	} else {
		swap_info[prev].next = p - swap_info;
	}
	swap_device_unlock(p);
	swap_list_unlock();
	up(&swapon_sem);
	error = 0;
	goto out;
bad_swap:
	if (bdev) {
		set_blocksize(bdev, p->old_block_size);
		bd_release(bdev);
	}
bad_swap_2:
	swap_list_lock();
	swap_map = p->swap_map;
	p->swap_file = NULL;
	p->swap_map = NULL;
	p->flags = 0;
	if (!(swap_flags & SWAP_FLAG_PREFER))
		++least_priority;
	swap_list_unlock();
	destroy_swap_extents(p);
	vfree(swap_map);
	if (swap_file)
		filp_close(swap_file, NULL);
out:
	if (page && !IS_ERR(page)) {
		kunmap(page);
		page_cache_release(page);
	}
	if (name)
		putname(name);
	if (did_down) {
		if (!error)
			inode->i_flags |= S_SWAPFILE;
		up(&inode->i_sem);
	}
	return error;
}

3.3.2 禁用交换分区

禁用交换分区完成了以下事情:

  1. 验证进程是否有权限禁止交换分区。
  2. 调用 filp_open() 打开 specialfile 参数确定的文件,返回文件对象的地址。
  3. 扫描交换区描述符链表 swap_list,比较由 filp_open() 返回的文件对象地址与活动交换区描述符的 swap_file 字段中的地址,如果不一致,说明传给函数的是一个无效参数,返回一个错误码。
  4. 调用 cap_vm_enough_memory() 检查是否由足够的空闲页框把交换区上存放的所有页换入。如果不够,交换区就不能禁用,然后释放文件对象,返回错误码。这只是粗略的检查,但可使内核免于许多无用的磁盘操作。当执行该项检查时,cap_vm_enough_memory() 要考虑由 slab 高速缓存分配且 SLAB_RECLAIM_ACCOUNT 标志置位的页框,这样的页(可回收的页)的数量存放在 slab_reclaim_pages 变量中。
  5. 从 swap_list 链表中删除该交换区描述符。
  6. 从 nr_swap_pages 和 total_swap_pages 的值中减去存放在交换区描述符的 pages 字段的值。
  7. 把交换区描述符 flags 字段中的 SWAP_WRITEOK 标志清 0。这可禁止 PFRA 向交换区换出更多的页。
  8. 调用 try_to_unuse() 强制把该交换区中剩余的所有页都移到 RAM 中,并相应地修改使用这些页的进程的页表扫描进程所有页表项,并用这个新页框的物理地址替换页表中每个出现的换出页标识符,这种做法实在是太低效了,后面会研究下新内核的处理方式)。当执行该函数时,当前进程的 PF_SWAPOFF 标志置位。该标志置位只有一个结果:如果页框严重不足,select_bad_process() 就会强制选中并删除该进程。
  9. 一直等待交换区所在的块设备驱动器被卸载。这样在交换区被禁用之前,try_to_unuse() 发出的读请求会被驱动器处理。
  10. 如果在分配所有请求的页框时 try_to_unuse() 失败,那么就不能禁用该交换区。因此,sys_swapoff() 执行下列步骤: a. 把该交换区描述符重新插入 swap_list链表,并把它的 flags 字段设置为 SWP_WRITEOK。 b. 把交换区描述符中 pages 字段的值加到 nr_swap_pages 和 total_swap_pages 变量以恢复其原址。 c. 调用 filp_close() 关闭在第 3 步中打开的文件,并返回错误码。
  11. 否则,所有已用的页槽都已经被成功传送到 RAM 中。因此,执行下列步骤: a. 释放存有 swap_map 数组和子区描述符的内存区域。 b. 如果交换区存放在磁盘分区,则把块大小恢复到原值,该原值存放在交换区描述符的 old_block_size 字段。 而且,调用 bd_release() 使交换子系统不再占有该块设备。 c. 如果交换区存放再普通文件中,则把文件索引节点的 S_SWAPFILE 标志清 0。 d. 调用 filp_close() 两次,第一次针对 swap_file 文件对象,第二次针对第 3 步中 filep_open() 返回的对象。

代码如下:感兴趣的可以去内核源阅读,觉得源码枯燥的可以直接略过,源码就是上面描述的所有步骤的详细流程:

/**
 * 使指定的交换区无效。
 */
asmlinkage long sys_swapoff(const char __user * specialfile)
{
	struct swap_info_struct * p = NULL;
	unsigned short *swap_map;
	struct file *swap_file, *victim;
	struct address_space *mapping;
	struct inode *inode;
	char * pathname;
	int i, type, prev;
	int err;

	/**
	 * 验证当前进程是否具有CAP_SYS_ADMIN权限。
	 */
	if (!capable(CAP_SYS_ADMIN))
		return -EPERM;

	/**
	 * 拷贝用户态空间的specialfile参数。
	 */
	pathname = getname(specialfile);
	err = PTR_ERR(pathname);
	if (IS_ERR(pathname))
		goto out;

	/**
	 * 打开文件。返回文件对象地址。
	 */
	victim = filp_open(pathname, O_RDWR|O_LARGEFILE, 0);
	putname(pathname);
	err = PTR_ERR(victim);
	if (IS_ERR(victim))
		goto out;

	mapping = victim->f_mapping;
	prev = -1;
	swap_list_lock();
	/**
	 * 扫描交换区描述符链表,比较文件对象地址与活动交换区描述符的swap_file。如果不一致,说明传给函数的是一个无效参数,返回错误码。
	 */
	for (type = swap_list.head; type >= 0; type = swap_info[type].next) {
		p = swap_info + type;
		if ((p->flags & SWP_ACTIVE) == SWP_ACTIVE) {
			if (p->swap_file->f_mapping == mapping)
				break;
		}
		prev = type;
	}
	if (type < 0) {
		err = -EINVAL;
		swap_list_unlock();
		goto out_dput;
	}
	/**
	 * 调用security_vm_enough_memory,检查是否有足够的空闲页框把交换区上存放的所有页换入。
	 */
	if (!security_vm_enough_memory(p->pages))
		vm_unacct_memory(p->pages);
	else {
		err = -ENOMEM;
		swap_list_unlock();
		goto out_dput;
	}
	/**
	 * 将交换区从swap_list中删除。
	 */
	if (prev < 0) {
		swap_list.head = p->next;
	} else {
		swap_info[prev].next = p->next;
	}
	if (type == swap_list.next) {
		/* just pick something that's safe... */
		swap_list.next = swap_list.head;
	}
	/**
	 * 调整两个全局变量。
	 */
	nr_swap_pages -= p->pages;
	total_swap_pages -= p->pages;
	/**
	 * 清除该标志后,将不会再向交换区换出更多的页。
	 */
	p->flags &= ~SWP_WRITEOK;
	swap_list_unlock();
	current->flags |= PF_SWAPOFF;
	/**
	 * 调用try_to_unuse函数强制把这个交换区中剩余的所有页都移到RAM中。并相应地修改这些页的进程的页表。
	 * 当执行该函数时,当前进程的PF_SWAPOFF标志置位。
	 */
	err = try_to_unuse(type);
	current->flags &= ~PF_SWAPOFF;

	/* wait for any unplug function to finish */
	/**
	 * 等待交换区所在的块设备驱动器被卸载,这样在交换区被禁用前,try_to_unuse发出的读请求会被驱动器处理。
	 */
	down_write(&swap_unplug_sem);
	up_write(&swap_unplug_sem);

	/**
	 * try_to_unuse返回失败。不能关闭交换区。
	 */
	if (err) {
		/* re-insert swap space back into swap_list */
		swap_list_lock();
		/**
		 * 将交换区重新插入到swap_list链表。
		 */
		for (prev = -1, i = swap_list.head; i >= 0; prev = i, i = swap_info[i].next)
			if (p->prio >= swap_info[i].prio)
				break;
		p->next = i;
		if (prev < 0)
			swap_list.head = swap_list.next = p - swap_info;
		else
			swap_info[prev].next = p - swap_info;
		/**
		 * 调整全局变量。
		 */
		nr_swap_pages += p->pages;
		total_swap_pages += p->pages;
		p->flags |= SWP_WRITEOK;
		swap_list_unlock();
		goto out_dput;
	}
	/**
	 * 运行到这里,说明所有页槽都已经被成功传送到RAM中。
	 */
	down(&swapon_sem);
	swap_list_lock();
	drain_mmlist();
	swap_device_lock(p);
	swap_file = p->swap_file;
	p->swap_file = NULL;
	p->max = 0;
	swap_map = p->swap_map;
	p->swap_map = NULL;
	p->flags = 0;
	/**
	 * 释放子区描述符。
	 */
	destroy_swap_extents(p);
	swap_device_unlock(p);
	swap_list_unlock();
	up(&swapon_sem);
	/**
	 * 释放swap_map数组。
	 */
	vfree(swap_map);
	inode = mapping->host;
	if (S_ISBLK(inode->i_mode)) {
		/**
		 * 交换区在磁盘分区,恢复块大小为原值。
		 */
		struct block_device *bdev = I_BDEV(inode);
		set_blocksize(bdev, p->old_block_size);
		/**
		 * 交换区不再占有该块设备。
		 */
		bd_release(bdev);
	} else {
		/**
		 * 交换区在普通文件中,则把文件索引节点的S_SWAPFILE标志清0.
		 */
		down(&inode->i_sem);
		inode->i_flags &= ~S_SWAPFILE;
		up(&inode->i_sem);
	}
	/**
	 * 关闭两个文件:swap_file和victim。
	 */
	filp_close(swap_file, NULL);
	err = 0;

out_dput:
	filp_close(victim, NULL);
out:
	return err;
}

3.4 交换高速缓存

交换高速缓存引入的目的:

  • 多重换入(两个进程可能同时换入同一个共享匿名页)
  • 同时换入换出(一个进程可能换入正由 PFRA 换出的页)

3.4.1 多重换入

考虑一下共享同一换出页的两个进程这种情形。

swap_cache1

上图是假设现在页被放入交换分区初始时候的情况,这个时候进程的页表项实际存放的是换出页标识符。如下图,当第一个进程B试图访问页时,内核开始换入页操作,第一步就是检查页框是否在交换高速缓存中,假定页框不在交换高速缓存中,内核会分配一个新页框并把它插入交换高速缓存, 并设置该页描述符的PG_locked 标记,然后开始I/O操作,从交换区读入页的数据

swap_cache2

同时,第二个进程访问该匿名页,内核开始换入操作,检查涉及的页框是否在交换高速缓存中。现在页框在交换高速缓存,因此内核只是访问页框描述符,在 PG_locked (页被锁定,例如:在磁盘IO操作中涉及的页)标志清 0 之前(即 I/O 数据传输完毕之前),让当前进程睡眠。这一次并不涉及IO操作。只是等待交换分区中的页被正在换入到交换高速缓存中。

swap_cache3

3.4.2 同时换入换出

shrink_list() 要开始换出一个匿名页,就必须当 try_to_unmap() 从进程(所拥有该页的进程)的用户态页表中成功删除了该页后才可以。但是当换出的写入操作还在执行时,可能有某个进程要访问该页,而产生换入操作。在写入磁盘前,待换出页由 shrink_list()存放在交换高速缓存。考虑页 P 由两个进程(A 和 B)共享。

swap_concurr

​ 最初,两个进程的页表项都引用该页框,该页有 2 个拥有者,如图 a 所示。 当 PFRA 选择回收页时,shrink_list() 把页框插入交换高速缓存,如图 b 所示,现在页框有 3 个拥有者,而交换区中的页槽只被交换高速缓存引用。然后 PFRA 调用 try_to_unmap() 从这两个进程的页表项中删除对该页框的引用。一旦该函数结束,该页框就只有交换高速缓存引用它,而引用页槽的为这两个进程和交换高速缓存,如图 c 所示。假定:当页中的数据写入磁盘时,进程 B 访问该页,即它要用该页内部的线性地址访问内存单元。那么,缺页异常处理程序发现页框在交换高速缓存,并把物理地址放回 B 的页表项, 这个时候页高速缓存就不会被释放回伙伴系统如图 d 所示。相反,如果缓存操作结束,而没有并发换入操作,shrink_list() 则从交换高速缓存删除该页框并把它释放到伙伴系统,如图 e 所示。交换高速缓存可被视为一个临时区域,该区域存有正在被换入后缓存的匿名页描述符。当换入或缓存结束时(对于共享匿名页,换入换出操作必须对共享该页的所有进程进行),匿名页描述符就可以从交换高速缓存删除。

3.4.3 交换高速缓存实现原理

​ 页高速缓存的核心就是一组基树,借助基树,算法就可以从 address_space 对象地址(即该页的拥有者)和偏移量值推算出页描述符的地址。在交换高速缓存中页的存放方式是隔页存放,并有下列特征:

  • 页描述符的 mapping 字段为 NULL
  • 页描述符的 PG_swapcache 标志置位
  • **private 字段存放与该页有关的换出页标识符**

​ 此外,当页被放入交换高速缓存时,页描述符的 count 字段和页槽引用计数器的值都增加,因为交换高速缓存既使用页框,也使用页槽。最后,交换高速缓存的所有页只使用一个 swapper_space 地址空间,因此只有一个基树(由 swapper_space.page_tree 指向)对交换高速缓存中的页进行寻址。swapper_space 地址空间的 nrpages 字段存放交换高速缓存中的页数。

​ 交换高速缓存和页高速缓存在2.6.11的内核版本中使用的数据结构都是基数树,关于页高速缓存请参考下:页高速缓存

img

​ 所有交换高速缓存的所有页的页描述符都存放在由 swapper_space.page_tree为根的基数树中,索引是通过换出页标识符来进行索引的,叶子节点就是该换出页标识符对应的页描述符。知道了页描述符,也就知道了物理页。于是将进程的页表项,重新设置为该页的地址,那么后续MMU就可以再次访问这块内存了。

3.4.4 实际的换入换出流程

3.4.4.1 换出页

交换高速缓存插入页框

​ 换出操作的第一步就是准备交换高速缓存。如果 shrink_list() 确认某页为匿名页,且交换高速缓存中没有相应的页框(页描述符的 PG_swapcache 标志清 0),内核就调用 add_to_swap()add_to_swap() 在交换区中分配一个新页槽,并把一个页框(其页描述符地址作为参数传递)插入交换高速缓存。add_to_swap()函数执行下述步骤。

  1. 调用 get_swap_page() 分配一个新页槽,失败(如没有发现空闲页槽)则返回 0。
  2. 调用 __add_to_page_cache(),参数为页槽索引、页描述符地址和一些分配标志。
  3. 将页描述符中的 PG_uptodatePG_dirty 标志置位,从而强制 shrink_list() 把页写入磁盘。
  4. 返回 1(成功)。

更新页表项

​ 一旦 add_to_swap() 结束,shrink_list() 就调用 try_to_unmap(),它确定引用匿名页的每个用户态页表项地址,然后将换出页标识符写入其中

页写入交换区

​ 为完成换出操作需执行的下一个步骤是将页的数据写入交换区。这一 I/O 传输由 shrink_list() 激活,它检查页框的 PG_dirty 标志是否置位,然后执行 pageout()pageout() 建立一个 writeback_control 描述符,且调用页 address_space 对象的 writepage 方法。而 swapper_state 对象的 writepage 方法是 swap_writepage() 实现。swap_writepage() 执行如下步骤。

  1. 检查是否至少有一个用户态进程引用该页。如果没有,则从交换高速缓存删除该页,并返回 0。这一检查之所以必须做,是因为一个进程可能会与 PFRA 发生竞争并在 shrink_list() 检查后释放一页。
  2. 调用 get_swap_bio() 分配并初始化一个 bio 描述符。函数从换出页标识符算出交换区描述符地址,然后搜索交换子区链表,以找到页槽的初始磁盘扇区。bio 描述符将包含一个单页数据请求(页槽),其完成方法设为 end_swap_bio_write()
  3. 置位页描述符的 PG_writeback 标志和交换高速缓存基树的 writeback 标记,并将 PG_locked 标志清 0。
  4. 调用 submit_bio(),参数为 WRITE 命令和 bio 描述符地址。

​ 一旦 I/O 数据传输结束,就执行 end_swap_bio_write()。该函数唤醒正等待页 PG_writeback 标志清 0 的所有进程,清除 PG_writeback 标志和基树中的相关标记,并释放用于 I/O 传输的 bio 描述符。

交换高速缓存中删除页框

​ 缓存操作的最后一步还是由 shrink_list() 执行。如果它验证在 I/O 数据传输时没有进程试图访问该页框,就调用 delete_from_swap_cache() 从交换高速缓存中删除该页框。因为交换高速缓存是该页的唯一拥有者,该页框被释放到伙伴系统。

3.4.4.1 换入页

​ 当进程试图对一个已缓存到磁盘的页进行寻址时,必然会发生页的换入。在以下条件发生时,缺页异常处理程序就会触发一个换入操作(如果页已经在交换分区,实际上该进程访问的这个虚拟地址所对应的页表项已经是页换出描述符了):

  • 引起异常的地址所在的页是一个有效的页,即它属于当前进程的一个线性区。
  • 页不在内存中,即页表项中的 Present 标志被清除。
  • 与页有关的页表项不为空,但 Dirty 位清 0,这意味着页表项包含一个换出页标识符。

​ 如果上面的所有条件都满足,则 handle_pte_fault() 调用 do_swap_page() 换入所需页。

do_swap_page()

参数:

  • mm,引起缺页异常的进程的内存描述符地址。
  • vma,address 所在的线性区的线性区描述符地址。
  • address,引起异常的线性地址。
  • page_table,映射 address 的页表项的地址。
  • pmd,映射 address 的页中间目录的地址。
  • orig_pte,映射 address 的页表项的内容。
  • write_access,一个标志,表示试图执行的访问是读操作还是写操作。

​ 与其他函数相反,do_swap_page() 不返回 0。如果页已经在交换高速缓存中就返回 1(次错误),如果页已经从交换区读入就返回 2(主错误),如果在进行换入时发生错误就返回 -1。关于缺页异常的major faultminor falut请参考这里,这里不再过多的阐述。函数执行下述步骤:

  1. orig_pte 获得换出页标识符。
  2. 调用 pte_unmap() 释放任何页表的临时内核映射,该页表由 handle_mm_fault() 建立。访问高端内存页表需要进行内核映射。 释放内存描述符的 page_table_lock 自旋锁。
  3. 调用 lookup_swap_cache() 检查交换高速缓存是否已经含有换出页标识符对应的页;如果页已经在交换高速缓存中,就跳到第 6 步。
  4. 调用 swapin_readhead() 从交换区读取至多 2n 个页的一组页,其中包括所请求的页。值 n 存放在 page_cluster 变量中,通常等于 3。 其中每个页是通过调用 read_swap_cache_async() 读入的。
  5. 再一次调用 read_swap_cache_async()换入由引起缺页异常的进程所访问的页。此步看起来多余,其实不然。swapin_readahead() 可能在读取请求的页时失败,如,因为 page_cluster 被置为 0,或者该函数试图读取一组含空闲或有缺陷页槽(SWAP_MAP_BAD)的页。另一方面,如果 swapin_readahead() 成功,这次对 read_swap_cache_async() 的调用就会很快结束,因为它在交换高速缓存找到了页。
  6. 尽管如此,如果请求的页还是没有被加到交换高速缓存,那么,另一个内核控制路径可能已经代表这个进程的一个子进程换入了所请求的页。这种情况的检查可通过临时获取 page_table_lock 自旋锁,并把 page_table 所指向的表项与 orig_pte 进行比较来实现。如果二者有差异,则说明这一页已经被某个其它的内核控制路径换入,因此,函数返回 1(次错误);否则,返回 -1(失败)。
  7. 至此,页已经在高速缓存中。如果页已经被换入(主错误),函数就调用 grab_swap_token() 试图获得一个交换标记。
  8. 调用 mark_page_accessed() 并对页加锁。并获取 page_table_lock 自旋锁。
  9. 检查另一个内核控制路径是否代表这个进程的一个子进程换入了所请求的页。如果是,就释放 page_table_lock 自旋锁,打开页上的锁,并返回 1(次错误)。
  10. 调用 swap_free() 减少 entry 对应的页槽的引用计数器。
  11. 检查交换高速缓存是否至少占满 50%(nr_swap_pages 小于 total_swap_pages 的一半)。如果是,则检查页是否被引起异常的进程(或其一个子进程)拥有;如果是,则从交换高速缓存中删除该页。
  12. 增加进程的内存描述符的 rss 字段。
  13. 更新页表项以便进程能找到该页。这一操作的实现是通过把所请求的页的物理地址和在线性区的 vm_page_prot 字段所找到的保护位写入 page_table 所指向的页表中完成。此外,如果引起缺页的访问是一个写访问,且造成缺页的进程页的唯一拥有者,则函数还要设置 DirtyRead/Write 标志以防无用的写时复制错误。
  14. 打开页上的锁。
  15. 调用 page_add_anon_rmap() 把匿名页插入面向对象的反向映射数据结构。
  16. 如果 write_access 参数等于 1,则函数调用 do_wp_page() 复制一份页框。
  17. 释放 mm->page_table_lock 自旋锁,并返回 1(次错误)或 2(主错误)。

read_swap_cache_async

功能:换入一个页

参数:

  • entry,换出页标识符。
  • vma,指向该页所在线性区的指针。
  • addr,页的线性地址。

​ 在访问交换分区前,该函数必须检查交换高速缓存是否已经包含了所要的页框。该函数本质上执行下列操作:

  1. 调用 radix_tree_lookup(),搜索 swapper_sapce 对象的基树,寻找由换出页标识符 entry 给出位置的页框。如果找到该页,递增它的引用计数器,返回它的描述符地址。
  2. 页不在交换高速缓存。调用 alloc_page() 分配一个新的页框。如果没有空闲的页框可用,则返回 0(表示系统没有足够的内存)。
  3. 调用 add_to_swap_cache() 把新页框的页描述符插入交换高速缓存,并对页加锁。
  4. 如果 add_to_swap_cache() 在交换高速缓存找到页的一个副本,则前一步可能失败。如,进程可能在第 2 步阻塞,因此允许另一个进程在同一个页槽上开始换入操作。这种情况下,该函数释放在第 2 步分配的页框,并从第 1 步重新开始。
  5. 调用 lru_cache_add_active() 把页插入 LRU 的活动链表。
  6. 新页框的页描述符现在已在交换高速缓存。调用 swap_readpage() 从交换区读入该页数据。 该函数与 swap_writepage() 相似,将页描述符的 PG_uptodate 标志清 0,调用 get_swap_bio() 为 I/O 传输分配与初始化一个 bio 描述符,再调用 submit_bio() 向块设备子系统层发出 I/O 请求。
  7. 返回页描述符的地址。

四、最新内核中交换分区的实现