# swapfile原理使用和演进

先抄一段来自度娘的定义：

> Swap分区在系统的运行内存不够用的时候，把物理内存中的一部分空间释放出来，以供当前运行的程序使用。

也就是系统提供了一种方法，当系统内存吃紧（或者在必要的时候），可以将内存内容保存到指定的文件上。这样释放出来的内存空间，就可以给到系统其他进程使用了。

本章我们就来看看这个swap工作的基本概念。

## 开启一个swap分区

swap并不是系统中必须的，也就是我们可以不用swap。那么此时当系统内存吃紧时，我估计会把内存扔掉吧。

那如何开启呢？ 下面给出一个简单的例子：

```
# create a file with 256M
sudo dd if=/dev/zero of=myswap bs=32k count=8192
# format myswap to swap file
sudo mkswap myswap
# enable it
sudo swapon myswap
```

怎么样，是不是很简单。我们只要指定一个文件，格式化成swap分区。这样就可以通过swapon系统调用将一个swap分区用起来了。

可以通过很多命令来获取swap分区的使用情况，如top, htop, cat /proc/swaps。不过信息比较简单。

## 初始化一个swap分区

要使用一个swap分区，那就要对这个分区初始化。这个初始化的过程有点长，但是从数据结构的角度来看也不算太复杂。让俺尝试用下面的图来表示。

```
                                         0     1              nr_swapfiles       MAX_SWAPFILES -1
                                         |     |                 |                 |
                                         v     v                 v                 v
        struct swap_info_struct       +-----+-----+-----+-----+-----+-----+-----+-----+
         *swap_info[MAX_SWAPFILES]    |     |     |     |     |     |     |     |     |
                                      |     |     |     |     |     |     |     |     |
                                      +-----+-----+-----+-----+-----+-----+-----+-----+
        struct address_space          +-----+-----+-----+-----+-----+-----+-----+-----+
      *swapper_space[MAX_SWAPFILES]   |     |     |     |     |     |     |     |     |
                                      |     |     |     |     |     |     |     |     |
                                      +-----+-----+-----+-----+-----+-----+-----+-----+
        unsigned int                  +-----+-----+-----+-----+-----+-----+-----+-----+
      nr_swapper_spaces[MAX_SWAPFILES]|     |     |     |     |     |     |     |     |
                                      |     |     |     |     |     |     |     |     |
                                      +-----+-----+-----+-----+-----+-----+-----+-----+
                                                     ^
                                                     |
                                                     |
                                                     +----------------------+
                                                                            |
                                                                            |
                                                                            |
                                      struct swap_info_struct               |
                                      +--------------------------------+    |
                                      |type                         ---|----+
                                      |                                | index in swap_info,
                                      |                                | swapper_spaces, nr_swapper_spaces
                                      |                                |
                                      |flags                           | SWP_USED
                                      |                                |
                                      |prio                            |
                                      |    (signed short)              |
     swap_active_head      <--->      |list                            |
     swap_avail_heads[]    <--->      |avail_lists[0]                  |
                                      |    (struct plist_node)         |
                                      +--------------------------------+
```

对这张图有几点可以说：

* 每个swap分区由一个swap\_info\_struct结构体表示
* 这个结构体的指针将保存在swap\_info\[]数组中，下标是swap\_info\_struct->type。（神奇的名字）
* 对应的还有两个数组swapper\_space\[]和nr\_swapper\_spaces\[]，用来保存对应的address\_space指针。可以一个swap分区可以对应多个address\_space。
* swap\_info\_struct又会链接在两个优先级队列swap\_active\_head和swap\_avail\_heads\[]上。看注释，前者用于swapoff，后者用于找到合适的磁盘空间。

有了大致的数据结构概念，我们可以来看看如何把内存保存到swap分区的。

## 找到空闲的swap空间

要把内存保存到某个地方，要分成两步：

* 找到可以存放的地方
* 放进去

要了解怎么找到一个空闲的空间，就要再看一眼swap\_info\_struct这个数据结构。

```
           struct swap_info_struct
           +--------------------------------+
           |max                             | = maxpages
           |pages                           | = nr_good_pages real available pages
           |inuse_pages                     |
           |                                |
           |highest_bit                     |    ----------------------+
           |lowest_bit                      |    ----+                 |
           |    (unsigned int)              |        |                 |
           |                                |  0     v                 v  maxpages
           |swap_map                        | [  |  |  |  |  |  |  |  |  |  ]
           |    (char *)                    |               ^
           |                                |               |
           |cluster_next                    |    -----------+     indicate next allocation
           |    (unsigned int)              |
           |                                |
           +--------------------------------+
```

出了前面那张图中展示的信息，swap\_info\_struct结构体中还包含了

* max: 整个swap分区页面个数
* pages: 整个swap分区可用的页面个数
* inuse\_pages: 已经用掉了多少页面
* swap\_map: 对每一个页面都有一个字节表示状态

接下来三个都是围绕着swap\_map数组的下标：

* lowest\_bit/highest\_bit: 当前可用范围的最大最小下标
* cluster\_next: 建议的下次可使用的下标

按照我现在的理解，这个过程就是当有请求时就从cluster\_next下标去找swap\_map中第一个空闲的块。然后把这个块分配出去。

具体的算法在scan\_swap\_map\_slots()中，还有写不是特别清楚。待后续了解后再来补充。

## 写入swap分区

找到了swap分区中对应的块，接下来就要把我们想要释放的内存写入。这样才能最后释放对应的内存。

个人感觉这个动作可以在多个地方完成，先找出其中之一记录一下：

```
  shrink_page_list()
    ...
    add_to_swap()
      entry = get_swap_page()                        (1)
        ...
        get_swap_pages()
      add_to_swap_cache(page, entry, GFP_xxx)        (2)
    mapping = page_mapping(page)
    pageout(page, mapping)
      mapping->a_ops->writepage(page, wbc)           (3)
```

此处省去众多，只留下我认为比较关键的步骤。

* (1) 从swap分区中找到一个空闲块，这里还有些优化暂时跳过
* (2) 将页加入到address\_space中，就是swapper\_space\[]数组中保存的
* (3) 调用address\_space对应的函数将页写入

这里因为是一个swapfile，所以对应的a\_ops就是swap\_aops。在函数init\_swap\_address\_space()中初始化。

到这里基本算是了解了swap分区的基本工作流程，当然还有更多细节有待挖掘。

## 基本概念

到这里我们要补充几个基本概念，了解一下重要的几个常用数据结构的样子。

### swap\_count

在“找到空闲的swap空间”这部分，我们可以看到swap\_map这个数组用来保存对应页面的状态信息。包括了：

* 被映射的数目
* 是否有cache
* 是不是坏的

这这一小节我们主要来看swap\_count是如何存储**被映射的数目**的。

在 commit 570a335b8e22 swap\_info: swap count continuations中，引入了采用链式的存储数目的方法。这样呢就导致了swap\_map这个字节有了两种格式：

```
    First swap_map:

        COUNT_CONTINUED
         |    SWAP_HAS_CACHE
         |     |
         v     v
       +----+----+----+----+----+----+----+----+
       |    |    |    |    |    |    |    |    |
       +----+----+----+----+----+----+----+----+

    Continuations:

        COUNT_CONTINUED
         |
         |
         v
       +----+----+----+----+----+----+----+----+
       |    |    |    |    |    |    |    |    |
       +----+----+----+----+----+----+----+----+
```

总的来说，也就是第一个swap\_map的格式和后续的会不一样。差别主要是第一个字节中的SWAP\_HAS\_CACHE被占用来表达了一个特殊含义，而后续的swap\_map可以有7个bit来表达数目信息。 而每个swap\_map的最高位COUNT\_CONTINUED用来表示后续是否还有更高位的信息。

这样一来 后续swap\_map能表达的数目信息由低7位表达，也就是 SWAP\_CONT\_MAX 0x7f(0111 1111)。 但是呢，第一个swap\_map能表达的数目信息却不是低6位的全部，而是 SWAP\_MAP\_MAX 0x3e(0011 1110)。因为SWAP\_MAP\_BAD 0x3f(0011 1111)用来表达一个特殊含义。

所以这些定义确实有点让人需要花一些时间去理解。我把这些特殊的定义稍微总结一下：

```
Bit indication in first swap_map:

   SWAP_HAS_CACHE  0x40(0100 0000)

Bit indication in all swap_map(?):

   COUNT_CONTINUED 0x80(1000 0000)

Special value in first swap_map:

   SWAP_MAP_MAX    0x3e(0011 1110)
   SWAP_MAP_BAD    0x3f(0011 1111)
   SWAP_MAP_SHMEM  0xbf(1011 1111)

Special value in swap_map continuation:

   SWAP_CONT_MAX   0x7f(0111 1111)
```

其实呢，了解了数据结构，就基本能理解后续代码是如何运作的了。这部分主要的函数是swap\_count\_continued()，这个函数相当于一个对于swap\_map的加减法。而函数add\_swap\_count\_continuation()则是在需要的时候扩展相对应的计数空间。当然里面用的招数是链接page->lru，有点高难度了。

## swp\_entry

这个swp\_entry就是一个索引，用它就可以找到对应的swap\_map和swap cache中保存的那个page。

总的来说，这个swp\_entry就是将swap\_info\_struct->type和offset编码存放起来。大致格式如下图所示

```
    +-+-------------+---------------------------------+
    | |             |                                 |
    +-+-------------+---------------------------------+
      ^             ^                                 ^
      | type(5bits) |       index/offset              |
```

具体将type和offset编码，以及从swp\_entry解码的函数为：

编码: (type, offset) -> swp\_entry

> entry = swp\_entry(type, offset)

解码: swp\_entry -> (type, offset)

> type = swp\_type(entry) offset = swp\_offset(entry)

而这个swp\_entry在把page释放到设备的时候就会写到pte中。所以这是一个非常重要的结构。

此外对其中的type部分要进一步展开，这个type占用5个bit，但是真正能用来表示swap设备的没有这么多。这个值如下定义：

```
#define MAX_SWAPFILES \
	((1 << MAX_SWAPFILES_SHIFT) - SWP_DEVICE_NUM - 	SWP_MIGRATION_NUM - SWP_HWPOISON_NUM)
```

也就是这些位中会拿出一部分SWP\_DEVICE\_NUM, SWP\_MIGRATION\_NUM, SWP\_HWPOISON\_NUM来表示某些对页做的特殊的操作。比如对于迁移的页面，就会用SWP\_MIGRATION\_READ|WRITE来表示。

## 演进

基本概念差不多是上面描述的样子了，接下来我们看看更加具体的细节。而这些细节大多是一步步演进过来的。

### swap\_cluster\_info for SSD

有好些个优化是基于ssd的，主要的思想是将swap切分成一个个小的cluster，每个cluster是256个页。当然之前也有这个cluster概念，但是并没有用数据结构将它独立出来。

相关的commit有：

```
815c2c543d3a 2013-09-11 swap: make swap discard async
2a8f94493432 2013-09-11 swap: change block allocation algorithm for SSD

6b53491598a4 2016-10-02 mm, swap: add swap_cluster_list
```

前两个是Shaohua的主要贡献，第三个是HuangYing将某些数据结构做了清理，让人更加容易读懂。所以我们现在看到的代码是在HuangYing修改后的版本。

其中重要的数据结构是：

* cluster\_info
* free\_clusters
* discard\_clusters

用一张图来展示这三者和swap\_map之间的关系会比较好的看清这个的用途：

```
    struct swap_info_struct
    +--------------------------------+
    |                                |    0                                                 maxpages
    |swap_map                        |   [  |  |  |  |  |   |  |  |  |  |  |  |  |  |  |  |  ]
    |    (char *)                    |                
    |                                |
    |cluster_info                    | [maxpages/SWAPFILE_CLUSTER]
    |    (swap_cluster_info*)        |
    |    +---------------------------+
    |    |lock                       |
    |    |    (spinlock_t)           |   +---------+   +---------+   +---------+   +---------+
    |    |data:24                    |   |1        |   |3        |   |count    |   |         |
    |    |flags:8                    |   |FREE     |   |FREE     |   |         |   |FREE     |
    |    |    (unsigned int)         |   +---------+   +---------+   +---------+   +---------+
    |    |                           |    ^      |       ^     |                     ^
    |    +---------------------------+    |      |       |     |                     |
    |                                |    |      +-------+     +---------------------+
    |free_clusters                   |    |                                          |
    |discard_clusters                |    |                                          |
    |    (swap_cluster_list)         |    |                                          |
    |    +---------------------------+    |                                          |
    |    |head                     --|----+                                          |
    |    |tail                       |                                               |
    |    |   (swap_cluster_info)   --|-----------------------------------------------+
    |    |                           |
    |    +---------------------------+
    |                                |
    +--------------------------------+
```

我想看到这里基本上能明白了，也就是

* 对swap\_map中，每256个page，创建一个swap\_cluster\_info。形成一个数组叫cluster\_info。
* 然后有两个链表，free\_clusters和discard\_clusters。一个用来分配，一个用来discard

还有两点需要说明：

* 只有整个cluster都为空，才会加入到free\_clusters链表
* 初始化时的free\_cluster链表不是顺序的，为了防止false cache line sharing

关于这个false cache line sharing，可以参考以下两个commit

```
4b3ef9daa4fc 2017-02-22 mm/swap: split swap cache into 64MB trunks
235b62176712 2017-02-22 mm/swap: add cluster lock
```

### 分割swap cache

这个方法的理念和cluster类似，也是切分。之前切分的是swap\_map，这次切分的是address\_space。也就是swapper\_space\[]。

这个工作就是由下面这个提交完成的。

> 4b3ef9daa4fc 2017-02-22 mm/swap: split swap cache into 64MB trunks

这么一说出来，好像也不怎么神秘了。就是那么一回事儿。

### 一次多分几个

这个优化的理念和之前的，虽然做法上略有不同，但是根本还是一样的。-- 减少频繁得拿锁。

先来看以下这个改进的提交：

> 36005bae205d 2017-02-22 mm/swap: allocate swap slots in batches

其实呢这个改进还要个readahead结合起来说。当有预取的时候，我们会一次性从swap中拿出SWAP\_BATCH个entry。改进之前，每次拿一个entry，都会拿一次锁再释放。这样对锁的竞争就比较大。而这次的改进则是对SWAP\_BATCH个entry只拿一次锁，这样就减少了竞争。

## swappiness控制交换

为了减少匿名页回收对系统性能造成的影响，系统有两个参数调整回收时究竟多少比例回收匿名页。

* /proc/sys/vm/swappiness
* /sys/fs/cgroup/memory/memory.swappiness

而这两个参数在代码中通过mem\_cgroup\_swappiness()来获取，其功效就是用来计算回收比例。具体的作用我们将在回收部分统一来看。
