深入理解Linux虚拟内存管理(二)(中):https://developer.aliyun.com/article/1597765
10.2 页面高速缓存
页面高速缓存是一个包含页面的数据结构集,这些页面对应于普通文件、块设备或交换分区。在高速缓存中,基本上存在四种类型的页面:
- 读内存映象文件时所产生的页面碎片。
从块设备上读出的块,或从文件系统中读出的块,它们都被封装到一个成为缓冲区页面的特殊页面中。块的数量随块大小和系统中页面大小而变化。
在第 11 章中将讨论匿名页面,这些页面存放于一种称为交换高速缓存的特殊页面高速缓存中,交换高速缓存在后援存储器中分配槽的时候用于换出页面。
属于共享内存区的页面在处理方式上和匿名页面类似。它们之间惟一的区别是在第一次写到页面后,共享页面才被添加到交换高速缓存中,并马上在后援存储设备中保留一份空间。
高速缓存存在的主要原因是为了减少不必要的读磁盘操作。从磁盘读出的页面被存储在一个基于 address_space 结构和偏移地址的页面哈希表里,在磁盘访问前都要先查找这个表。把页面放入这个高速缓存有两个原因:第一个原因是为了减少不必要的读磁盘操作。从磁盘读出的页面是存放在一个基于 address_space 结构和偏移地址的页面哈希表中的,第二个原因是由于页面高速缓存中具有队列,这就为磁盘替换算法选择丢弃或换出页面打下了基础。这里提供了一个负责操作页面高速缓存的 API 函数,如表 10.1 所列。
10.2.1 页面高速缓存哈希表
系统要求被快速地定位在高速缓存中的页面。为了简化这一点,页面都被插入了一个叫 page_hash_table 的表中, 而 page->next_hash 和 page→pprev_hash 则用于处理冲突。
该表在 mm/filemap.c 中声明如下:
atomic_t page_cache_size = ATOMIC_INIT(0); unsigned int page_hash_bits; struct page **page_hash_table;
在系统初始化时,函数 page_cache_init() 将系统中物理页面的数量作为一个参数,并分配该表。该表所申请的大小(htable_size)足够保存系统中每个结构页面的指针,它的计算公式如下:
htable_size=num_physpages * sizeof(struct page * )
为了分配这样一个表,系统首先分配一块足够大的顺序空间来存放整个表。它从 0 开始计算并逐步增大该值,直到 2order > htable_size。这大致可以用下列简单方程的整数部分来粗略地表示:
系统尝试使用 __get_free_pages() 来分配这些顺序页面。如果分配失败,则尝试分配序号较小的页面,如果同样没有可以分配的页面,则系统瘫痪。
page_hash_bits 的值是基于表的大小的,并被哈希函数 _page_hashfn() 所使用。 这个值通过连续除以 2 得到,如果是在实数域,它等于
这样就使得该表是一个 2 的幂次哈希表,从而避免了在哈希函数中经常使用的取模运算。
10.2.2 索引节点队列
// include/linux/fs.h 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 */ int gfp_mask; /* how to allocate the pages */ };
索引节点队列是 address_space 结构中的一部分,这在 4.4.2 小节中已有介绍。该结构包含三个链表:clean_pages 是一个由干净页面组成的链表,这些页面均与索引节点相关;dirty_pages 是一个由自从链表与磁盘同步以来已经被写过的页面所组成的链表;locked_pages 则是由那些正处于锁定状态的页面所组成的。这三种链表合起来被认为就是给定的某种映射的索引节点队列,并且 page→list 字段用来链接队列上的页面。页面通过 add_page_to_inode_queue() 函数添加到索引节点队列中的 clean_pages 链表上,并通过 remove_page_from_inode_queue() 函数完成移出操作。
10.2.3 向页面高速缓存添加页面
从文件或块设备读取页面时,通常将页面添加到页面高速缓存中以避免更多的磁盘 I/O。大多数的文件系统都使用较高层次的函数 generic_file_read() 来完成它们的 file_operations→read() 操作,如图 10.2 所示。在第 12 章中涉及的共享内存文件系统,是一个值得关注的例外。 一般情况下,文件系统都是通过页面高速缓存来进行它们的操作的。在这一节中,我们介绍 generic_file_read() 如何完成操作,以及如何把页面添加到页面高速缓存中的。
对于普通 I/O,generic_file_read() 函数在调用 do_generic_file_read() 函数之前会进行少量的基本检查工作。通过调用 __find_page_nolock() 函数并上锁 pagecache_lock 来搜索页面高速缓存,以查看该页面是否已经存在于高速缓存中。如果不在,一个新的页面将通过 page_cache_alloc() 函数分配,其过程一般通过 alloc_pages() 函数封装后再通过 __add_to_page_cache() 函数添加到页面高速缓存中。在页面高速缓存中形成页面帧后,再通过调用 generic_file_readahead() 函数(实质上是调用 page_cache_read() 函数)从磁盘读取页面。 它使用 mapping→a_ops→readpage() 这样的方法来读取页面,其中 mapping 指的是管理文件的 address_space。readpage() 函数是用来从磁盘读取页面的文件系统专有函数。
若没有进程与匿名页映射,系统会将匿名页添加到交换高速缓存中,这将在 11.4 节有更进一步的讨论。持续地尝试将它们换出时,由于它们没有可用来映射的 address_space 或者文件中的任何偏移量,这样将无法把它们添加到页面高速缓存的哈希表中。即便如此,也要注意到这些页面依旧存在于 LRU 链表中。一旦进入交换高速缓存中,匿名页面和有文件后援的页面之间的惟一实际差异便是匿名页面采用 swapper_space 作为其 struct address_space。
共享内存页面在下面两种情况下会被添加到页面高速缓存中。第 1 种情况是在调用
shmem_getpage_locked() 函数时,不论它是从交换分区中取出或者是在其中分配页面,其原因就是该页面是第 1 次被引用。第 2 种情况是当换出代码调用 shmem_unuse() 函数时。当一个交换分区被禁止时以及一个有后援交换区的页面被发现不属于任何进程时就会出现第 2 种情况。与共享内存相关的索引节点都会被穷尽搜索,直至找到正确的页面。在这两种情况下,add_to_page_cache() 函数将会把该页面添加到页面高速缓存中,如图 10.3 所示。
10.3 LRU 链表
正如 10.1 节中所谈到的,LRU 链表包含两个链表,一个称为 active_list,而另一个称为 inactive_list。它们在 mm/page_alloc.c 中声明,并由 pagemap_lru_lock 自旋锁保护。一般地,它们分别存储着经常使用和不经常使用的页面,或者说,active_list 包含系统中所有的工作集,而 inactive_list 则包含回收候选集。在表 10.2 中列出了操作 LRU 链表的 API 函数。
10.3.1 重新填充 inactive_list
在高速缓存收缩时,refill_inactive() 函数会把页面从 active_list 移到 inactive_list 中 。 它有一个参数表示移动页面的数量,该参数通过在 shrink_caches() 函数中求比率得到,而这个比率依赖于 nr_pages,即 active_list 中的页面数量,还有 inactive_list 中的页面数量。需要移动的页面数量计算公式为:
这样可以保证 active_list 大约是 inactive_list 大小的 2/3,要移动的页面数量是由一个基于我们需要换出的页面数量(nr_pages)的比率决定的。
页面从 active_list 的尾部取出。如果设置了 PG_referenced 标志位,则该标志位要被清除,然后将页面放置到 active_list 的首部,因为它最近被使用过且依旧活跃。这种情况有时被称为链表旋转。如果没有设置该标志位,则该页面被移到 inactive_list,PG_referenced 标志位会被设置,这样使得该页面在需要时能很快地添加到 active_list 中。
10.3.2 从 LRU 链表回收页面
shrink_cache() 函数是替换算法的一部分,它把页面从 inactive_list 中取出,并决定它们应该如何被换出。nr_pages 和 priority 用于决定多少工作需要做的初始参数。nr_pages 初始值为 SWAP_CLUSTER_MAX,目前在 mm/vmscan.c 中定义为 32。而 priority 的初始值为 DEF_PRIORITY,目前在 mm/vmscan. c 中定义为 6。
max_scan 和 max_mapped 这两个参数决定了该函数要做多少工作,并根据 priority 调整。每当没有足够的空闲页面却又调用了 shrink_caches() 函数时,priority 会逐渐减小直至达到最高优先级 1。
变量 max_scan 表示该函数扫描的最大页面数,它可简单计算如下
其中 nr_inactive_pages 表示 inactive_list 中的页面数量。 这就意味着在最小优先级 6 时,inactive_list 的页面中最多只有 1/6 会被扫描到,而在最大优先级时,inactive_list 中所有页面都会被扫描到。
第二个参数是 max_mapped,它决定了在所有进程换出前,允许多少进程页面存在于页面高速缓存中。它可以在 max_scan 的 1/10 或者以下计算公式
中取较小的那个值来计算。
或者说,在最低优先级时,映射页面被允许使用的最大数量是 max_scan 的 1/10 或者是待替换页面数(nr_pages)的 16 倍中那个较小的数值。 而在高优先级时,它是 max_scan 的 1/10 或是待换出页面的 512 倍。
从上面可以看出,该函数基本上是一个很庞大的 for 循环,它会从 inactive_list 的尾部开始扫描至多 max_scan 个页面,来释放掉 nr_pages 个页面,或直至 inactive_list 为空。在扫描每个页面时,它会检查是否需要重新调度它自己,以保证换出页面不会一直独占 CPU。
对于链表中每一种类型的页面,函数会作出不同的处理决定。这些不同的页面类型以及对应的处理方式如下:
由进程映射的页面
这会跳转到 page_mapped 标志位,稍后还会再谈到。max_mapped 计数减 1。如果 max_mapped 减到 0,进程的页表会被线性搜索并且使用 swap_out() 函数完成换出。
被锁定,并且 PG_launder 位也被设置了的页面
这样的页面由于在进行 I/O 而被锁定,因此应当可以直接跳过。然而,如果 PG_launder 位同时被设置了,则意味着这是第二次发现该页面被锁定,因此等 I/O 完成后再去除该页面会更好一些。该页面的引用可以通过 page_cache_get() 函数得到,以保证该页面不会被过早地释放,然后调用 wait_on_page() 函数进行睡眠,直至 I/O 完成。在 I/O 完成后,通过 page_cache_release() 函数将引用计数减 1。当引用计数减到 0 时,页面将会被回收。
脏页面
脏页面没有被任何进程映射,没有缓冲区,属于某个设备或文件映射。由于该页面属于某个文件或设备映射,它就有一个可用的有效函数 writepage() ,通过 page→mapping→a_ops→writepage 得到 。在开始 I/O 的时候,清除 PG_dirty 位,并设置 PG_launder 位。调用 writepage() 函数同步页面及其后援文件前,系统要调用 page_cache_get() 函数得到该页面的引用计数,而后调用 page_cache_release() 函数清除该引用。请注意,这种情况也会引起交换高速缓存中有后援存储的异步页面的同步操作,因为交换高速缓存页面使用 swapper_space 作为 page->mapping。该页面依旧在 LRU 链上。当其再度被发现时,如果 I/O 已经完成了,很容易就可以释放掉它,然后页面将被回收。如果 I/O 还没有完成,正如前面所述,内核会等待 I/O 完成。
具有缓冲区与磁盘数据相关联的页面
用一个引用指针指向该页面,并使用 try_to_release_page() 函数来尝试释放该页面。如果成功并且它是一个匿名页(无 page→ maping),则该页面从 LRU 链上移除,并调用 page_cache_released() 函数来减少引用计数。只有一种情况匿名页面会和高速缓存区相关联,那就是当其有后援的交换文件时,因为页面必须以块对齐的方式写出。在另一方面,如果它有后援的文件或设备,系统就可以在其计数达到 0 时,只需要简单地释放掉其引用指针,便释放掉了该页面。
被多个进程映射的匿名页
第一种情况里被计数的同一个 page_mapped 标志位,在被释放掉以前,解锁 LRU 链,同时解锁该页面。或者说,max_mapped 计数将减少,在其达到 0 时,调用 swap_out 函数。
没有被任何进程引用的页面
这是最后一种情况,但不是非得明确提出的。如果该页在交换高速缓存中,它将从交换高速缓存中移除,因为它现在正在和其后援存储器进行同步,而且没有任何的进程引用它。如果它是一个文件中的某部分,则它会从索引节点队列中移除,然后会从页面高速缓存中删除,并被释放掉。
10.4 收缩所有的高速缓存
负责收缩各种高速缓存的函数是 shrink_caches(),它使用很简单的步骤释放一些内存(参考图 10.4)。任意一次允许写回磁盘的页面数量最大值是 nr_pages,它由 try_to_free_pages_zone() 函数初始化为 SWAP_CLUSTER_MAX。有了这样的限制后,如果 kswapd 调度大量的页面写回到磁盘 ,它会偶尔睡眠一下以允许 I/O 开始。随着页面的释放,nr_pages 会相应地减小。
待做的工作量也取决于 priority,它由 try_to_free_pages_zone() 函数初始化为 DEF_PRIORIRY。如果每次释放的页面不够数量,priority 会减小到最高优先级 1。
该函数会首先调用 kmem_cache_reap() 函数(见 8.1.7 小节)来选择一个待减小的 slab 高速缓存。如果释放的页面的数量达到 nr_pages,就表示工作完成了,然后函数返回。否则它会试着从其他高速缓存中释放页面以达到 nr_pages 个。
如果没有涉及到其他的高速缓存,在调用 shrink_cache() 函数来从 incative_list 尾部回收页面以减小页面高速缓存以前,refill_inactive() 函数会将页面从 active_list 移到 inactive_list。
最后, 它会减小三种特殊的高速缓存:dcache(shrink_dcache_memory() ) 高速缓存,icache(shrink_icache_memory() ) 高速缓存以及 dqcache(shrink_dqcache_memory() ) 高速缓存。这些对象本身都很小,但是级联效应会使大量的页面以缓冲区和磁盘高速缓存的形式释放。
10.5 换出进程页面
如图 10.5 所示,当在页面高速缓存中找到了 max_mapped 个页面时,swap_out() 就会被调用以启动换出进程页面的操作。系统从 swap_mm 指向的 mm_struct 和 mm->swap_address 地址开始向前搜索页表,直到释放 nr_pages 个页面。
除了 active_list 部分的页面和最近被引用的页面会被跳过外,进程映射的页面不管处于链表中的哪个位置,也不管什么时候被最后引用过,都会被检查。检查激活状态页面的开销是不小的,但是与为了获得引用某一结构页面的 PTE 而线性查找所有进程的开销比起来,这种开销又是微不足道的。
一旦决定了要换出某个进程的页面,就会试图换出至少 SWAP_CLUSTER 个页面,并且 mm_struct 的整个链表仅会被检查一遍,这样避免了在没有页面时无休止地循环。大块写出页面增加了在进程地址空间近邻的页面写到相邻磁盘槽的几率。
标志位 swap_mm 被初始化以指向 init_mm,而 swap_adress 在首次使用时被初始化为 0。 当 swap_address 等于 TASK_SIZE 时,说明某个进程已经被完全地搜索了一遍。一旦选择从某个进程换出页面,它的 mm_struct 的引用指针增加 1,使得它不会过早地被释放。然后系统会以选定的 mm_struct 作为参数来调用 swap_out_mm() 。这个函数遍历该进程持有的每个 VMA,并且在其上调用 swap_out_vma()。这样就避免了必须遍历整个很稀疏的页表的情形。swap_out_pgd() 和 swap_out_pmd() 遍历给定 VMA 的页表直到最后在实际页面以及 PTE 上调用 try_to_swap_out() 。
函数 try_to_swap_out() 首先检查以确保该页面或者不是 active_list 的一部分,或者该页面最近被引用过了,或者是我们并不关心的某个管理区的一部分。一旦确定了这是一个待换出的页面,该页面便会从该进程的页表中移出。接着检查刚移除的 PTE 以确定它是否是脏页面。如果它是脏页面,则会更新 struct page 标志位来反映这一点,以使它与后援存储设备同步。如果该页面已经在交换高速缓存中,就更新它的 RSS 位,并去掉对它的引用。否则,就把该页面加入到换出高速缓存中。在后援存储设备上分配空间和换出页面的过程将在第 11 章进一步讨论。
10.6 页面换出守护程序(kswapd)
在系统启动时,一个 kswapd 的内核线程从 kswapd_init() 中启动。大多数情形下它都处于睡眠状态,一旦运行,它就不断地执行在 mm/vmscan.c 中的 kswapd() 函数。这个守护程序负责在内存大小剩下很少时回收页面。从历史经验上来看,kswapd 经常是每 10 s 被唤醒一次。现在它由物理页面分配器在管理区中空闲页的 pages_low 大小达到了某个值时唤醒(见 2.2.1 小节)。
正是这个守护程序完成了大多数诸如保持页面高速缓存正确,收缩 slab 高速缓存并在需要的时候替换出进程的任务。与 Solaris[MM01] 中的替换守护程序不同,kswpad 不是随着内存压力的增大而增加被唤醒的频率,而是不断地在释放页面直到空闲页的 pages_high 大小达到阀值。在极端的内存压力下,所有的进程会通过调用 balance_classzone(),然后 balance_classzone() 调用 try_to_free_pages_zone(),来同步地完成 kswapd 的工作。在图 10.6 中,当物理页面分配器在和 kswapd 同步相同的任务量,分配处于重负的管理区时也会调用 try_to_free_pages_zone()。
当 kswapd 被唤醒时,它执行下列步骤:
调用 kswapd_can_sleep(),它遍历所有的管理区,检查在结构 zone_t 中的 need_balance 字段。如果有 need_balance 被设置,则它不能睡眠。
如果不能睡眠,则会被移出 kswapd_wait 等待队列。
调用 kswapd_balance(),它遍历所有的管理区。如果 need_balance 字段被设置,它将使用 try_to_free_pages_zone() 来释放管理区内的页面,直到达到 pages_high 阀值。
由于 tq_disk 的任务队列的运行,所以页面队列都将清除。
将 kswapd 重新加入 kswapd_wait 队列并且返回第一步。
10.7 2.6 中有哪些新特性
守护进程 kswapd
如同已经在 2.8 节中说过的一样,现在系统中每一个内存节点都拥有一个 kswapd。这些守护进程现在仍然由 kswapd() 启动,它们执行一样的代码,但是现在它们都只限于在局部的节点上执行。2.6 中 kswapd 的改变主要与 kswapd-per-node 的改变有关。
kswapd 的基本操作没有变。一且该守护进程被唤醒,它就会在其监管的 pgdat 上调用 balance_pgdat() 。balance_pgdat() 有两中操作模式。如果以 nr_pages == 0 的条件调用,则它就会不断地试着释放局部 pgdat 中各个管理区的页面,直到达到了 pages_high 的值。如果以某个 nr_pages 的条件调用,则它就会不断地试着释放 nr_pages 和 MAX_CLUSTER_MAX * 8 中那个较小值的页面。
平衡管理区
balance_pgdat() 释放页面时调用的两个主要函数是 shrink_slab() 和 shrink_zone() 。shrink_slab() 在 8.8 节中已经讨论过了,不再重复。balance_pgdat() 调用函数 shrink_zone() 来释放一定数量的页面,所要释放页面的确切数量取决于释放页面的紧急程度。该函数与 2.4 中的几乎一样。refill_inactive_zone() 用于把一定数量的页面从 zone→active_list 移到 zone→inactive_list 中。注意在 2.8 节中说过,2.6 中的 LRU 链表是每个区域都有的,与 2.4 中 LRU 链表是全局的情况不同。系统调用 shrink_cache() 移出 LRU 中的页面并且回收。
页面换出压力
2.4 中页面换出操作的优先级由扫描的页面数量决定。2.6 中由 zone_adj_pressure() 调整 zone→pressure 字段指示为替换而需要扫描的页面数量,而这个调整通常是逐渐减弱的。当需要更多的页面时,zone→pressure 的值就会达到最高值 DEF_PRIORITY << 10,然后随着时间慢慢降低。通常这个值影响着为替换而需要在管理区中扫描的页面数量。这样做的目的是启动页面替换然后逐渐降低压力,而不是短时间突然进行大量替换。
控制 LRU 链表
2.4 中当需要从 LRU 链表中移出页面的时候就需要获得一个自旋锁,这样就会引起很激烈的加锁竞争。所以,为了缓解这种竞争程度,2.6 中使用了 struct pagevec 结构来处理涉及到 LRU 链表的操作,允许在 LRU 链表中批量加入或移出多达 PAGEVEC_SIZE 个页面。
具体情形是这样的,当 refill_inactive_zone() 或 shrink_cache() 要移出页面 时,它们先获得 zone→lru_lock 锁,然后移出一部分页面,并把它们存储在一个临时链表中。系统只在要移出的页面链表组织好以后,才会调用 shrink_list() 进行实际的页面释放工作,这个过程中大部分工作不需要获得 zone→lru_lock 锁就可以完成。
当要加入页面时,系统就会调用 pagevec_init() 来初始化一个新的页面向量结构体,然后调用 pagevec_add() 将所有要加入的页面加入到这个向量中,接着调用 pagevec_release() 成批地加入到 LRU 链表中。
相当一部分与 pagevec 结构体有关的 API 声明可以在 <linux/pagevec.h> 中找到,在 mm/swap.c 中有它大部分的实现。
第11章 交换管理
由于 Linux 使用空闲内存作硬盘数据的缓冲,所以就有必要释放进程所占用的私有页面或匿名页面。这些页面与普通磁盘文件中的页面不同,不能简单地将它们先丢弃,将来再读取。取而代之的是 Linux 把这些页面复制到后援存储器,有时也称为交换区。本章详细介绍 Linux 如何使用和管理后援存储器。
严格意义上,Linux 并不进行交换操作,字面意义上的 “交换” 通常是指复制整个进程地址空间到磁盘,而 “换页” 指的是复制出单独的页面。实际上在当前硬件的支持下,Linux 实现的是换页,也就是传统意义上讨论的文档中所谓的交换。为了与 Linux 中对这个词的使用惯例一致,这里也称之交换。
存在交换区有两个主要原因。首先,交换区扩充了内存以供进程使用。虚拟内存和交换区的存在使得一个大型的进程即使部分常驻内存也可以运行。由于可能换出旧的页面,以及请求换页要确保页面在必要时能重新载入,所以被访问的内存可能很名易就超过 RAM 的大小。
某些读者可能会认为只要内存容量足够大,就不需要交换区。这就引出了我们需要交换区的第二个原因。一个进程在开始运行时所引用的大量页可能只是为了初始化,在后来的运行期间这些页面将不再使用。把这些页面交换出去可以空闲出更多的磁盘缓冲区,这样做比把它们常驻内存却不使用更能提高系统效率。
这并不是指交换区没有缺点。最重要也是最明显的缺点是访问磁盘的操作非常耗时。若没有交换区和高性能磁盘,那些频繁访问大量内存的进程就只可能在较大内存的支持下才能在合理的时间内完成。所以,正如我们在第 10 章所讨论的一样,如何选择正确的页面交换出去是非常重要的。同样,这也是为什么相关的页面应该存放在交换区毗邻位置的原因,因为这样进程在预读的时候能同时把相关页面换入内存。下面我们从 Linux 如何描述一个交换区开始。
本章首先讲述 Linux 所维护的每个活动交换区的结构以及如何在磁盘上组织交换区信息。然后是 Linux 如何在页面换出后的交换区定位该页,以及如何分配交换槽。接下来讨论交换高速缓存,它对共享页面非常重要。最后本章可以让你了解到如何激活和禁止交换区,内存的页面如何换出到交换区又如何换入到内存,以及如何读写交换区。
11.1 描述交换区
每个活动的交换区,无论是一个文件或分区,都由一个 swap_info_struct 结构描述。系统中该结构都存储在一个静态声明的 swap_info 数组中,它有 MAX_SWAPFILES(一般被定义为 32)个元素项。这意味着运行系统中最多存在 32 个交换区。swap_info_struct 结构在 <linux/swap.h> 中声明如下 :
/* * The in-memory structure used to track swap areas. */ 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; 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; int next; /* next entry on swap list */ };
下面我们简要解释这个比较大的结构的每个字段。
flags:有两个可取的值。SWP_USED 表示此 swap 区域处于激活状态。SWP_WRITEOK 被定义为 3,最低两位包含了 SWP_USED。标志位为 SWP_WRITEOK 时,表示 Linux 准备对此交换区进行写操作,因为写之前必须先激活交换区。
swap_device:该交换区所占用的磁盘设备信息,在交换区为普通文件时,其值为 NULL。
sdev_lock:和 Linux 中其他结构一样,swap_info_struct 也需要保护。sdev_lock 就是一个用来保护此结构的自旋锁,它主要是保护 swap_map。它通过 swap_device_lock() 和 swap_device_unlock() 进行上锁和解锁。
swap_file:实际的特殊文件作为交换区被挂载到系统的 dentry。如果交换区是一个分区的话,则 swap_file 是 /dev 目录下一个 dentry。在禁止一个交换区时,此字段用于确认正确的 swap_info_struct 。
vfs_mount:这是设备或文件作为交换区存储位置相对应的 vfs_mode 对象。
swap_map:这是一个非常大的数组,其中一个项对应每个交换项,或者对应每个交换区中页面大小的槽。一个项作为页槽用户数量的引用计数。交换缓存以单个用户进行计数,这里一个 PTE 被换出到槽中时算一个用户。如果计数为 SWAP_MAP_MAX,将永久地分配该槽而如果其值为 SWAP_MAP_BAD,将不再使用该槽。
lowest_bit:交换区中最低的可用空闲槽,它一般在线性扫描减少搜索空间时开始。由此可知不可能有空闲槽低于该标记值。
highest_bit:交换区中最高的可用空闲槽。如同 lowest_bit,高于该标记值不可能有空闲槽。
cluster_next:下一个被使用的簇块偏移量。交换区通过在簇块中分配页面,使得相关页能有机会存储在一起。
cluster_nr:簇内剩余的供分配的页面数量。
prio:每个交换区都有一个优先级,就存储在该字段里面。交换区按照优先级顺序进行组织,并决定如何使用它们。缺省状态下,系统按照活跃程度的顺序来分配优先级,当然系统管理员也可以使用 swapon-f 来指定优先级。
pages:由于可能不能使用交换文件中某些页面,此字段用于记录交换区中可用的页面数量。该值不同于 max,因为标记为 SWAP_MAP_BAD 的槽并没有计数。
max:交换区中槽的总数。
next:swap_info 数组中用于指向系统中下一个交换区的下标。
虽然这个交换区存放在一个数组中,但它同时也存放在一个称为 swap_list 的伪链表中,它是一个很简单的类型,在 <linux/swap.h> 中声明如下:
struct swap_list_t { int head; /* head of priority-ordered swapfile list */ int next; /* swapfile to be used next */ };
swap_list_t→head 字段使用具有最高优先级的交换区。swap_list_t→next 是下一个将被使用的交换区。尽管搜索一个合适的交换区要按照优先级顺序,但在必要的情况下,在数组中查询速度还是很快。
每个交换区在磁盘上都划分出大量页面大小的槽。例如在 X86 机上,每个页面大小为 4096 个字节。第一个槽由于存放有交换区的基本信息,故被保留且不可写。该交换区的第一个 1 KB 用于存放分区的磁盘标签,为用户空间的工具提供信息。剩下的空间用于存放交换区的其他信息,在系统程序 mkswap 创建交换区时,这些剩余的交换区将被填充。交换区的信息由一个 union swap_header 表示,在 <linux/swap.h> 中定义如下:
/* * Magic header for a swap area. The first part of the union is * what the swap magic looks like for the old (limited to 128MB) * swap area format, the second part of the union adds - in the * old reserved area - some extra information. Note that the first * kilobyte is reserved for boot loader or disk label stuff... * * Having the magic at the end of the PAGE_SIZE makes detecting swap * areas somewhat tricky on machines that support multiple page sizes. * For 2.5 we'll probably want to move the magic to just beyond the * bootbits... */ union swap_header { struct { char reserved[PAGE_SIZE - 10]; 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]; unsigned int badpages[1]; } info; };
各字段描述如下。
magic:联合结构的 magic 字段,仅用于鉴别 magic 字符申。这个字符串的存在使得分区必定有一个可使用的交换区,并用于决定交换区的版本。如果字符串为 SWAP-SPACE,则交换文件格式版本为 1,如果为 SWAPSPACE2,则版本为 2。由于数组保留得很大,所以一般从页的末端开始读取该 magic 字符串。
bootbits:保留区,用于存放分区信息,如磁盘标签。
version:交换区的版本号。
last_page:交换区中最后一个可用页面。
nr_badpages:交换区中已知的坏页面数都存储在这个字段中。
padding:通常一个磁盘扇区为 512 B。 version,last_page 和 nr_badpages 这 3 个字段占用了 12 B,padding 字段用于填充扇区剩下的 500 B。
badpages:页面的剩下部分用于存放至多 MAX_SWAP_BADPAGES 个坏页槽。在系统程序 mkswap 打开 -c 开关检查交换区时,填充这些槽。
MAX_SWAP_BADPAGES 是一个编译时常数,它随着结构的改变而变化,但是根据当前的结构,由下面这个简单的公式可以知道它为 637。
其中,1 024 是 bootlock 的大小,512 是 padding 的大小,10 是 magic 字符串的大小,其中 magic 字符串用于鉴别交换文件的格式。
11.2 映射页表项到交换项
在一个页面换出时,Linux 使用相应的页表项 PTE(page table entry)来存放足够用于再次在磁盘上定位该页的信息。很显然,PTE 本身并不能够大到精确保存交换页面位置的信息,但它只要存放交换槽在 swap_info 数组的下标以及在 swap_map 中的偏移量就够了。这正是 Linux 的处理方式。
每个 PTE,无论其结构如何,都必须大到能够存放一个 swap_entry_t 变量。swap_entry_t 在 <linux/shmem_fs.h> 定义如下:
/* * A swap entry has to fit into a "unsigned long", as * the entry is hidden in the "index" field of the * swapper address space. * * We have to move it here, since not every user of fs.h is including * mm.h, but mm.h is including fs.h via sched .h :-/ */ typedef struct { unsigned long val; } swp_entry_t;
linux 分别提供了宏 pte_to_swp_entry() 和 swp_entry_to_pte() 来分别转换 PTE 到 swap_info 数组元素的映射和 swap_info 数组元素到 PTE 的映射。
// include/asm-i386/pgtable.h #define pte_to_swp_entry(pte) ((swp_entry_t) { (pte).pte_low }) #define swp_entry_to_pte(x) ((pte_t) { (x).val })
在不同体系结构下 Linux 都必须可以确定 PTE 在内存还是已经换出。为了说明,我以 x86 为例。在 x86 中,swp_entry_t 中的第 0 位预留给 _PAGE_PRESENT 标志位,第 7 位预留给 _PAGE_PROTNONE 标志位,需要这两位的原因已在 3.2 节解释过。1~6 位标识 swap_info 数组中下标的类型,它由宏 SWP_TYPE() 返回。
8~31 位则代表交换文件内的页号,也就是在 swap_map 中的偏移量。在 x86 中,这意味着交换文件的页号占用 24 位,它限制了交换区大小为 64 GB。宏 SWP_OFFSET() 用于分离偏移。
为了将类型和其相应的偏移编号存放到 swp_entry_t 中,Linux 使用了宏 SWP_ENTRY() 。这些宏之间的关系如图 11.1 所示。
标识类型的那 6 位必须允许 32 位系统中存在 64 个交换区,而不是 MAX_SWAPFILES 的中限制的 32 个交换区。MAX_SWAPFILES 的限制是由于 vmalloc 地址空间有消耗。如果交换区具有可能的最大尺寸,则 swap_map 需要 32 MB(224 * sizeof(short) )空间,不要忘记每个页面都有一个短整型的引用数。在 MAX_SWAPFILES 个最大尺寸的交换文件存在时,就要 1 GB 虚拟 malloc 空间,而这样分割用户内核线性地址空间几乎是不可能的。
这意味着不值得这样为支持 64 个交换区而增加系统处理复杂度,但是在某些情况下,即使没有增加整个有效交换区,存在大量的小交换区也可以提高系统执行效率。一些现代化的机器已经拥有许多独立的磁盘,可以在这些磁盘间创建大量的独立块设备。在这种情况下,创建大量分布在磁盘间的小交换区,可以大大加强页调度过程的并行度,这对那些交换密集的应用很重要。
11.3 分配一个交换槽
所有指定大小的槽都由数组 swap_info_struct→swap_map 跟踪 ,它的元素类型为 unsigned short。在共享页且 0 页空闲的情形下,数组中每项都是一个槽使用者的引用计数。如果项值为 SWAP_MAP_MAX,则对应的页将永久保留给这个槽。尽管元素值不可能为 SWAP_MAP_MAX,但还是这样做,主要是为防止引用数溢出。如果项值为 SWAP_MAP_BAD,则对应的页将不能再使用。
寻找和分配一个交换项的任务分为两个步骤。如图 11.2 所示,首先调用高层函数 get_swap_page() 。从当前交换文件索引 swap_list_next 处开始在分配区域中寻找可分配的交换槽。找到后,记录下一个待用的交换区,并返回分配的表项。
scan_swap_map() 函数负责查找交换图。原理上,这是很简单的一步,因为它线性查找空闲槽并返回,可以肯定的是,它的实现还要彻底一点。
Linux 将磁盘中的 SWAPFILE_CLUSTER 个页分配到一个簇中。它在交换区中顺序分配 SWAPFILE_CLUSTER 个页面,然后在 swap_info_struct cluster_nr 记录簇中已分配的页面数,在 swap_info_struct cluster_next 中记录交换文件当前偏移量。在分配好一个连续的块后,系统寻找一个空闲的尺寸为 SWAPFILE_CLUSTER 的表项,供下一个簇使用。如果找到了一足够大的块,系统将会把它作为另一个簇大小的序列。
如果交换区中找不到足够大的空闲族,系统将从 swap_info_struct lowest_bit 位置进行首次空闲搜索。这样做的目的是使得在同一时间交换出去的页位置靠近,当然前提是同一时间
交换出去的页是相关的。这个前提初看上去很奇怪,但若考虑到页替换算法在线性扫描交换出去页的地址空间要使用很大的交换区,就很合理了。如果不需要扫描大量的空闲块并使用它们,则扫描就会退化成首次空闲搜索,速度也不需提高。但是若需要,退出中的进程可能释放很大的块的槽。
11.4 交换区高速缓存
前面讲过,Linux 无法快速完成页面结构到每个引用它的 PTE 的映射,所以,由多个进程共享的页面不能简单地换出。这就导致如果不同步磁盘数据,就没有条件判断某个 PTE 引用的页面是否因为其他进程的换出而得到了更新,因此会丢失更新。
为了解决这个问题,共享页在后援存储器中保留一个槽作为交换高速缓存的一部分。交换高速缓存有一些与其有关的 API,如表 11.1 所列。交换高速缓存是一个纯概念性的东西,因为它只是页面高速缓存的特殊形式。页面高速缓存页面与交换高速缓存页面的一个区别是交换高速缓存总是使用 swapper_space 作为 page->mapping 的地址空间。另外一个区别如图 11.3 所示,交换高速缓存中的页面通过 add_to_swap_cache() 加到交换高速缓存中,而不是使用 add_to_page_cache()。
匿名页只有在交换出去时才是交换高速缓存的一部分。同时这意味着,系统在首次写属于共享内存区的页时才把它们加入到交换高速缓存中。变量 swapper_space 在 swap_state.c 中如下声明:
static struct address_space_operations swap_aops = { writepage: swap_writepage, sync_page: block_sync_page, }; struct address_space swapper_space = { LIST_HEAD_INIT(swapper_space.clean_pages), LIST_HEAD_INIT(swapper_space.dirty_pages), LIST_HEAD_INIT(swapper_space.locked_pages), 0, /* nrpages */ &swap_aops, };
一个页面在 page mapping 设置为 swapper_space 时才成为交换高速缓存的一部分。swapper_space 是 address_space 类型,管理交换文件。宏 PageSwapCache() 测试 page mapping 是否为 swapper_space。Linux 中同步交换区和内存间页面的代码相同,因为它们都使用后援文件来实现同步。它们共享页面高速缓存代码,之间的区别仅是使用到的函数不同。
作为后援存储地址空间的 swapper_space 使用 swap_ops 作为它的 address_space->a_ops。在一般意义下,系统使用 page->index 字段而不是文件偏移来存储 swp_entry_t 结构。类型为 address_space_operations 的结构体 swap_aops 在 swap_state.c 中如下声明:
static struct address_space_operations swap_aops = { writepage: swap_writepage, sync_page: block_sync_page, };
当一个页面加到交换高速缓存中后,系统调用 get_swap_page() 来分配一个可用的交换页目录项,然后使用 add_to_swap_cache() 将它加入到页高速缓存中,接着把它标志为脏数据。脏页清洗程序处理该页时,就把它写入到磁盘中。这个过程如图 11.4 所示。
接下来共享 PTE 的页交换将调用 swap_duplicate(),它仅将 swap_map 相应页的引用计数加 1。如果 PTE 由于被写过而被硬件标志为 “脏数据” ,则其对应的交换页目录项位将被清除,且页结构由 set_page_dirty 标志为 “脏页” ,这时系统在页被删除之前将进行磁盘复制同步在删除对页的所有引用之前,系统会检查磁盘数据是否和页数据一致。
在页面的引用计数最后到 0 时,该页面就可以从页面高速缓存中删除,但交换映射计数将等于磁盘槽所属的 PTE 的计数,所以该槽将不会过早地被释放掉。而是由同一个 LRU 清除并在最后销毁,这种逻辑已在第 10 章中有描述。
另一方面,如果系统在已交换到交换区的页上产生页中断,则 do_swap_page() 函数将调用 lookup_swap_cache() 检查页是否在交换高速缓存中。如果在,则更新 PTE 指向该页,将页引用计数加 1,然后调用 swap_free() 释放交换槽。
11.5 从后援存储器读取页面
读页面时的主要函数是 read_swap_cache_async(),它主要在发生缺页中断时调用,如图 11.5 所示。此函数首先调用 find_get_page() 寻找交换高速缓存。通常情况下,交换高速缓存通过 lookup_swap_cache() 函数完成查找,但该函数在更新执行的查询时也会更新统计的数量。由于需要多次查询高速缓存,所以 Linux 使用了 find_get_page() 。
如果其他进程有页面的映射,则该页面已经在交换区中或者多个进程同时都在同一页上发生缺页中断。如果交换高速缓存中不存在页面,系统就会从后援存储器中分配一页并填入数据。由于在交换高速缓存中的操作都是对页面的操作,在 alloc_page() 分配页面后,就由 add_to_swap_cache() 将其加入到交换高速缓存中。如果不能把页面加入到交换高速缓存中,系统就会再次查找一遍交换高速缓存,保证其他进程不会向交换高速缓存中加入数据。
为了从后援存储器中读入信息,它会调用 rw_swap_page(),这将在 11. 7 节讨论。在这个函数调用结束后,系统调用 page_cache_release() 释放 find_get_page() 找到的页面。
11.6 向后援存储器写页面
在向磁盘写页面时,系统就使用 address_space→a_ops 找到相应的写出函数。在使用后援存储时,address_space 即为 swapper_space,swap_aops 中就包含有交换操作 。由于 swap_aops 的写出函数的缘故,所以由 swap_aops 注册 swap_writepage() 函数,如图 11.6 所示。
函数 swap_writepage() 因为写进程是否是交换高速缓存页面的最后使用者的不同而表现为不同的操作,它通过调用 remove_exclusive_swap_page() 确定这一点。 函数 remove_exclusive_swap_page() 通过检查被操作页获取的 pagecache_lock 的数量知道页面的引用计数,从而得知是否存在其他进程也在使用被操作页。如果没有,该页将从交换高速缓存中移走并释放。
在 remove_exclusive_swap_page() 将页面从交换高速缓存中移走并释放后,因为已经不再使用该页 ,所以 swap_writepage() 将释放页锁,唤醒所有等待该锁的进程。如果页面还在交换高速缓存中,则调用 rw_swap_page() 将页的内容写到备份空间中。
11.7 读/写交换区域的块
读写交换区的高层函数是 rw_swap_page()。此函数确保所有操作在交换高速缓存中完成以避免丢失更新。rw_swap_page_base() 是完成实际工作的核心函数。
它首先检查操作是否为读。如果是,则调用 ClearPageUptodate() 清除 uptodate 标志位,因为 I/O 请求写入数据的页面显然不是过时的页面。此标志位会在页成功后从磁盘读入时再次置位。然后调用 get_swaphandle_info() 获得文件索引节点的交换分区的设备。这些是在块一级所需要的,而在这一级上有实际 I/O 操作。
核心函数既可以在交换分区运行,也可以在文件一级运行。因为它使用了块一级的函数 brw_page() 完成实际的磁盘 I/O。如果交换区是一个文件,就调用 bmap() 把操作页所在的文件系统所有块组成的列表填充为一个本地数组。请注意文件系统可能有自己存储文件和磁盘的方法,可能和磁盘分区的直接将信息写入到磁盘的方法不一致。如果后援存取区是一个分区,由于没有涉及到文件,就不需要 bmap(),这时只需要一次页面大小块的 I/O 操作。
在确定需要读入或写入一块时,系统使用 brw_page() 执行平常的块 I/O 操作。由于所有的 I/O 都是异步进行的,所以所有函数很快就可以返回。I/O 操作完成后,在块一层就会解锁页面,系统将唤醒所有处于等待状态的进程。
11.8 激活一个交换区
现在你已经知道什么是交换区,它是怎样组织以及页是怎样记录的,现在我们来看看它们是如何链接在一起以激活一个区域的。激活一个交换区的操作从概念上说很简单:打开一个文件,从磁盘获得交换首部的信息,填写 swap_info,将它加入到交换列表中。
函数 sys_swapon() 负责激活交换区。它有两个参数,交换区对应文件的路径和一组标志位。当系统激活交换区时,大内核锁(BKL,Big Kernel Lock)启动,阻止任何程序在该函数执行时进入到核心空间。该函数比较大,但可以分成下面几个简单的步骤:
在 swap_info 数组中找到一个空闲项,对其进行缺省初始化。
调用 user_path_walk(),该函数遍历提供的文件路径目录树,用文件的有效数据填充 namidata 结构,例如存放在 vfsmount 中的目录项和文件系统的信息。
填充涉及到交换区的大小和如何找到交换区的 swap_info 结构。如果交换区是一个分区,则块大小在计算大小之前为 PAGE_SIZE。如果是一个文件,信息直接从索引节点处获得。
确定空间是否未被激活。如果未激活,则从内存中分配一页并读取交换区第一页信息。这页包含很多信息,如可用槽,怎样用坏项填充 swap_info 中的 swap_map。
系统调用 vmalloc() 为 swap_info_struct->swap_map 分配内存,并将每个可用槽的项初始化为 0,不可用槽对应的项初始化为 SWAP_MAP_BAD。理想情况下,首部信息的文件格式为版本 2,因为版本 1 限制交换区在页大小为 4 KB 的系统结构下,小于 128 MB。如 x86 。
验证头节点信息与实际的交换区匹配,在这之后,在 swap_info_struct 中填充类似页面最大数量和可用页面数等其余的信息,修改全局统计参数 nr_swap_pages 和 total_swap_pages。
至此,交换区已激活并初始化,在交换区的逻辑列表中插入代表此交换区的新元素,仍遵循优先级排序的顺序。
在函数结束时,释放 BKL,此时系统拥有一个新的用于换页的交换区。
11.9 禁止一个交换区
与激活一个交换区比,禁止交换区的代价非常高。这主要是因为交换区不能被随便地移去,每个交换出去的页必须再次交换回来。正是由于没有快速的方法把 struct page 映射到每个引用它的 PTE 上,所以也没有快速的方法映射交换项到 PTE。这要求遍历所有的进程页表以找到需要禁止的交换区的相关 PTE,然后将其交换回去。当然,这意味着如果物理内存被禁止,此操作将失败。
可以肯定,函数 sys_swapoff() 负责禁止交换区。该函数主要是更新 swap_info_struct。try_to_unuse() 负责将每个交换出去的页交换回来,但它执行的代价非常高。在 swap_map 中所使用的每一个槽,都必须遍历进程的页表来搜索它。在极端情况下,所有属于 mm_structs 的页表都需要被遍历。因此,一般地,禁止一个交换区的任务如下:
调用 user_path_walk() 获得需禁止的交换文件信息,并启动 BKL 将相应的 swapinfo_struct 从交换列表中移走。并更新全局变量 nr_swap_pages(有效交换页)和 total_swap_pages(交换项总数)。完成后,释放 BKL。
从交换列表中释放 swap_info_struct,修改全局统计参数 nr_swap_pages(可用交换页
面数)和 total_swap_pages(交换项总数),成功后,可以以再次释放 BKL。
调用 try_to_unuse(),将需要禁止的交换区域中所有页交换回去。该函数遍历交换映射表并调用 find_next_to_unuse() 定位下个已用的交换页。对于找到的每个已用的页,执行下面的步骤:
调用 read_swap_cache_async() 为磁盘上分配保存该页的空间。理想情况下,空间应该已在交换高速缓存里分配好,若没有,将调用页分配器进行分配。
等待所有的页被交换回去,并对其加锁。加锁后,为每一个有引用该页的 PTE 的进程调用 unuse_process() 。该函数遍历页表查找相关的 PTE,然后更新它指向 page。如果页面是一个共享内存页,且没有其他的引用,则调用 shmem_unuse() 释放所有被永久映射的页。
释放那些永久性映射的存储槽,有人认为存储槽不会被永久保留,但是这种风险仍然存在。
如果在意外情况下交换映射表仍存在页的引用,则从交换高速缓存中删除此页以防止 try_to_swap_out() 引用该页。
如果没有足够的内存将所有的项交换回去,则不能简单地删除交换区,而是将其重新插入到系统中。如果成功地将所有项交换回去,则 swap_info_struct 处于未定义状态,swap_map 的空间将由 vfree() 释放。
11.10 2.6 中有哪些新特性
为实现扩展区,在 struct swap_info_struct 中改变的最重要的部分是增加了一个名为 extent_list 的链表,以及一个叫做 curr_swap_extent 高速缓存字段。
扩展区由 struct swap_extent 表示,它把交换区的一串连续的页面映射为磁盘上的一片磁盘块。这些扩展区由函数 setup_swap_extents() 在交换时启用。对一个块设备只启用一个扩展区,这不是为了提高性能,而是为了使系统一致对待块设备或普通文件所使用的交换区。
它们与交换文件有很大的不同,交换文件会启用多个扩展区,表示在块中连续的多个页面。当查找处于某个偏移值的文件时,系统会遍历一遍扩展区列表。为减少搜索时间,最后一次搜索的扩展区将会缓存在 swap_extent->curr_swap_extent 。