2017-04-25
我们都知道,现代操作系统使用分页机制和虚拟内存,同时为了提高物理页面的利用率,采用了请求调页的机制,即物理内存的分配只有在真正需要的时候才会进行,比如发生了真正的读写操作,而普通内存的alloc,并不会和物理内存有什么关系。启动一个程序时,装载器把进程可执行文件映射到进程的虚拟地址空间中,注意是虚拟地址空间,从入口函数开始分配一定数量的物理内存页,让其运行。事实上,某个程序可以使用的物理内存页数量基本是固定的(一般运行过程中),当执行到的代码没有在内存中,就会发生pagefault,进而由内存管理器把相应页面调入到内存,如果进程物理页面没有空间,就考虑换出某些页面。而不止是代码,进程中动态申请的内存也有可能由于内存紧张被换出到外存,在需要的时候在调入到内存中。合理的换入换出机制能够充分发挥现代操作系统多任务的优势,各个任务均能够正常的运行。但是换入换出机制毕竟需要和磁盘打交道,磁盘IO一直以来都是性能的瓶颈所在,所以设计一个良好的换入换出机制显得异常重要。
下面主要从两个方面介绍换入换出机制。首先,提高磁盘IO效率的方法之一就是建立缓存,在这方面设计到的缓存主要由页缓存、交换缓存、LRU缓存。另一方面,我们不能为了换出而换出,而应该换出哪些真正需要被换出的页面,避免被换出的页面下一刻又要访问的现象,怎么评判需要被换出,根据局部性原理,LRU算法是比较合理的,但是如何评价一个页面的使用频度却是一个不容易实现的问题。所以问题2在就i在于一个合理的换入换出算法。
LRU 链表
在NUMA系统上,每个区域zone都关联一组LRU链表,这些链表记录了物理页面的状态,物理页面的回收工作就是以这些链表为依据。在zone结构中,存在struct lruvec lruvec;字段,lruvec保存了保存了这些链表的链表头,看下该结构
struct lruvec { struct list_head lists[NR_LRU_LISTS]; struct zone_reclaim_stat reclaim_stat; #ifdef CONFIG_MEMCG /*其关联的区域zone*/ struct zone *zone; #endif };
首个字段lists是一个数组,记录这些链表,都有哪些链表呢?
enum lru_list { LRU_INACTIVE_ANON = LRU_BASE, LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, LRU_UNEVICTABLE,//不可换出 NR_LRU_LISTS };
看到这里根据物理页存储的内容,分为了几种类型,anon后缀是匿名映射,而file后缀是文件映射。二者区别就是在回收对应内存的时候,换出到磁盘文件还是swap分区。除此之外,还有一些inactive和active之分,inactive是不活跃的页面,而active是活跃的页面。具体来讲活跃的页面是页表正在映射的页面,不活跃的页面是已经断开映射,但是还未换出到磁盘的页面。不活跃的页面又有clean和dirty之分,干净的不活跃可以直接作用可用页面,而脏页还需要和磁盘文件做同步或者换出到swap分区。因为于鏊换出文件映射的内存最多需要同步下数据,不需要把整页都换出,所以在实际物理内存回收时,如果文件映射类的内存足够,就优先回收此类内存,这点以后还会讲到。上面讲的匿名和文件映射都是可回收的,Linux单独为不可回收的页面提供了一个链表LRU_UNEVICTABLE,在扫描物理页面进行回收时,不会扫描该链表。
LRU缓存
LRU链表虽然直接管理处于各种状态的页面,但是频繁的对链表进行操作,对性能势必造成一定的影响,所以linux为每个CPU都关联了一个CPU LRU缓存,如果当前CPU调入内存一个页面,则首先把该页面记录到其CPU LRU缓存中,看下pagevec结构
struct pagevec { unsigned long nr; unsigned long cold; struct page *pages[PAGEVEC_SIZE]; };
nr记录已有的page数目,code标记冷热页,pages是一个指向拥有14个page指针的数组。以从swap分区中换入一个页面为例,在把page加入到交换缓存后,就会把page加入到当前CPU的LRU缓存。当满了的时候,会自动把LRU缓存中的page添加到对应的LRU链表中,且最初添加的时候以LRU_INACTIVE_ANON状态添加的。每个CPU对应4种pagevec
/*当添加新的页面时,加入此缓存*/ static DEFINE_PER_CPU(struct pagevec[NR_LRU_LISTS], lru_add_pvecs); /*不活跃头到不活跃尾部*/ static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs); /*从活跃到不活跃*/ static DEFINE_PER_CPU(struct pagevec, lru_deactivate_pvecs); #ifdef CONFIG_SMP /*非活动到活动*/ static DEFINE_PER_CPU(struct pagevec, activate_page_pvecs);
由于每个CPU都有对应的变量,所以不需要对数据进行同步机制,减少了对锁的争用,但是为了前后访问的一致性,即进程或者线程在访问CPU变量期间,不能被调度出去,因为下次再被调度就不能确定在哪个CPU上了,所以在访问CPU变量前要禁用抢占。lru_add_pvecs是一个pagevec数组,根具体的LRU链表对应,当有新页面添加时,加入到LRU中后也会加入到该缓存中。从这点看着有点像一个总的管理区。而lru_rotate_pvecs,当需要把页面从不活跃链表的头部移动到不活跃链表的尾部时,先把页面添加到该缓存,如果缓存满了再对缓存中的页面集体移动到对应LRU链表的尾部。lru_deactivate_pvecs缓存记录即将从活跃链表移动到不活跃链表的页面。同样的道理,当系统企图把一个页面从活跃链表移动到不活跃链表时,先添加到lru_deactivate_pvecs缓存,当缓存满了在操作具体的LRU链表。最后一个activate_page_pvecs仅在SMP架构下实现,在SMP架构下,实现思路和上面类似,当系统要把一个页面从不活跃链表转入到活跃链表时,先添加到该缓存,在缓存满了后,在移动LRU链表。在单处理下,直接操作了LRU链表。注意上面记录的page指针,相当于在具体操作执行先,先在某个地方登记,page架构中lru节点组成LRU链表。当然,并不是只有在缓存满了才会操作LRU链表,有lru_add_drain、lru_add_drain_all分别对当前CPU和所有CPU实现缓存的刷新。在换入页面函数swapin_readahead中,在执行换入操作后就调用了lru_add_drain函数,统一对缓存进行刷新。
和外存的交互
涉及到物理内存的回收,根据前面LRU链表的种类,回收的页面主要来自于两个地方:进程的文件映射区和进程的匿名映射区。文件映射顾名思义对应这某个磁盘文件,而匿名映射这里就是广义的匿名映射了,即不对应固定磁盘文件,内存多为进程动态分配的堆,栈,以及私有映射等。对于前者,其物理页面记录在普通页缓存中,页缓存本身通过基数树radix tree管理,每个叶子节点直接关联page指针,从文件方面,每个文件在内存中对应一个inode,而每个inode会关联一个address_space结构管理文件在内存中的映射,address_space中关联了对应的基数树,从进程来讲,每个file结构也关联了address_space结构,所以同样可以寻找到对应的基数树;而后者,物理页面信息记录在交换缓存中,由于不对应固定的磁盘文件,就没有对应的inode,而交换缓存本质也是页缓存的一种,也通过基数树管理,也就对应一个address_space结构。所以,内核中把所有交换缓存的address_space记录在一个全局数组中,定义如下
struct address_space swapper_spaces[MAX_SWAPFILES] = { [0 ... MAX_SWAPFILES - 1] = { .page_tree = RADIX_TREE_INIT(GFP_ATOMIC|__GFP_NOWARN), .a_ops = &swap_aops, .backing_dev_info = &swap_backing_dev_info, } };
数组中共有MAX_SWAPFILES个元素,对于MAX_SWAPFILES,规定了交换的类型数目。交换 类型和偏移被编码进PTE中,在32位下,5位表示类型偏移,27位在交换缓存中定位具体的page,也就限制了交换缓存页面的最大数目。考虑到本人翻译水平有限,特贴出原文:
而对于MAX_SWAPFILES 的定义如下,后面两个分别为支持NUMA 内存迁移和考虑到坏页面的情况。
#define MAX_SWAPFILES ((1 << MAX_SWAPFILES_SHIFT) - SWP_MIGRATION_NUM - SWP_HWPOISON_NUM)
当MMU根据页表对虚拟地址进行转化,发现PTE最后一位为0而其他位不为0时,就说明该页不在物理内存中,如果该页是被换出到交换分区,把pte转化成swap_entry_t,对物理页面进行查找,需要调用do_swap_page函数,把页面进行调入(swap_entry_t最后会进行介绍)。该函数首先会检查交换缓存,如果缓存中有对应页面,则直接返回,不需要进行磁盘IO;如果没有,再针对具体的磁盘快进行读取。这里涉及到一个预读机制,之前也说过,磁盘寻道时间比IO时间还要长,如果仅仅读取一个页面,则代价有点大,所以一次读取多个页面到物理内存,下次访问的时候就不必进行IO了。如果pte对应的是文件映射,则调用do_nonlinear_fault函数,该函数负责把文件指定偏移处的内容读取到内存。
三、交换区的管理
3.1 内核支持
linux下交换区可以是交换分区也可以是交换文件即swap files,每个交换区对应一个swap_info_struct结构,所有的swap_info_struct结构通过一个全局数组swap_info来管理。struct swap_info_struct *swap_info[MAX_SWAPFILES];,然而数组记录的仅仅是指针。交换区的数量和上文中address_space的数量是一直的,都是MAX_SWAPFILES。swap_info_struct结构如下
struct swap_info_struct { unsigned long flags; /* SWP_USED etc: see above */ signed short prio; /* swap priority of this type */ signed char type; /* strange name for an index */ /*下一个交换区在swap_info数组中的索引,按照优先级排列*/ signed char next; /* next type on the swap list */ unsigned int max; /* extent of the swap_map */ /*据说是标记每一个共享页面的进程数目*/ unsigned char *swap_map; /* vmalloc'ed array of usage counts */ /*low和high之间的可以使用,对应于swap_map*/ 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 */ unsigned int lowest_alloc; /* while preparing discard cluster */ unsigned int highest_alloc; /* while preparing discard cluster */ /*交换文件所用*/ struct swap_extent *curr_swap_extent;//指向上一次使用的swap_extend struct swap_extent first_swap_extent;//双链表的head 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; /* * protect map scan related fields like * swap_map, lowest_bit, highest_bit, * inuse_pages, cluster_next, * cluster_nr, lowest_alloc and * highest_alloc. other fields are only * changed at swapon/swapoff, so are * protected by swap_lock. changing * flags need hold this lock and * swap_lock. If both locks need hold, * hold swap_lock first. */ };
结构中已经有了比较详细的注释,这里简要描述下,flags记录当前交换区的属性,主要由一下几种
enum { SWP_USED = (1 << 0), /* is slot in swap_info[] used? */ SWP_WRITEOK = (1 << 1), /* ok to write to this swap? */ SWP_DISCARDABLE = (1 << 2), /* swapon+blkdev support discard */ SWP_DISCARDING = (1 << 3), /* now discarding a free cluster */ SWP_SOLIDSTATE = (1 << 4), /* blkdev seeks are cheap */ SWP_CONTINUED = (1 << 5), /* swap_map has count continuation */ SWP_BLKDEV = (1 << 6), /* its a block device */ SWP_FILE = (1 << 7), /* set after swap_activate success */ /* add others here before... */ SWP_SCANNING = (1 << 8), /* refcount in scan_swap_map */ };
交换区是分优先级的,在交换文件时,高优先级的交换区将被优先使用,所有的交换区按照优先级通过next字段连接起来,next实际上是下一个swap_info_struct结构在数组中的索引。swap_map是一个短整型数组,对应于每一个交换区的槽位(页面),记录页面共享进程数目。lowest_bit和high_bit记录一个区间,在区间外面没有可用的槽位,以此加速对空闲槽位的扫描。前面提到,不仅是交换分区,还有交换文件,对于交换文件,由于其对应的不一定是一个连续的磁盘块(很大程度上不是),因此不能用单纯的swap_info_struct结构描述,在swap_info_struct结构中,嵌入了first_swap_extent字段,该字段记录交换文件的首个区块,是一个swap_extent结构,一个交换文件的所有对应swap_extent结构通过链表连接,为了避免每次访问都重新扫描链表,在swap_info_struct中还有个swap_extent类型的指针curr_swap_extent,用以记录上次访问的swap_extent。swap_extent结构如下:
struct swap_extent { struct list_head list; pgoff_t start_page;//起始页面 pgoff_t nr_pages;//页面数量 sector_t start_block;//起始块 };
意义很明显,这里就不多说了。
3.2 交换区的创建
创建交换区的任务由用户空间发起,具体来说有个mkswap工具,该工具会执行以下操作:
1、将所需交换区的长度除以所述机器的页长度,以确定其中能够容纳的页面数。
2、检查坏页面
3、把坏块地址的列表写入到交换区首页
4、在第一页末尾设置SWAPSPACE标记
5、可用槽位(页面)数目也保存在交换区头部
用户空间工具仅仅完成了准备工作,需要把这些信息提供给内核,在内核中注册交换区。为此,内核提供了系统调用swapon,在swapfile.c文件中。该函数负责把交换区注册进内核。代码比较长,就不在列举。函数大致流程如下,首先用alloc_swap_info分配一个swap_info_struct结构,具体先分配一块内存,然后在swap_info数组中找到一个空闲位置,把结构地址写进表项。是否空闲的判断依据就是根据SWP_USED标识。然后获取交换文件名,打开文件获得文件描述符。下一步检查文件描述符对应的映射是否已经存在,如果存在就退出。接下来读取交换文件中的首个 页面,该页面记录了swap_header的信息。然后从中获取maxpages。下面一个重要工作是创建swap_map,是一个无符号整形的数组,每个页面对应一个。最先还记录坏槽位,不坏的页面就记录共享该页面的进程数目,不过现在坏槽位都不检查了。下面一个重要的函数是setup_swap_map_and_extents,该函数根据header中记录的坏页面信息,填充swap_map,当然,如果为空就略过。之后如果有nr_goog_pages,就设置swap_info_struct结构中的相关信息,如max、pages,还要通过setup_swap_extents函数,为一段连续的pages映射到连续的磁盘快中,返回的是磁盘快的数量,如果最先建立的是交换分区,则这里就仅仅需要一个块,而如果对应的是磁盘文件,就需要对文件涉及的各个磁盘快分别建立结构,并关联起来。 回到swapon函数中,在此之后就设置swap_info_struct的优先级并和swap_map建立联系了。
总结:唉,其实本篇想详细介绍下物理页面回收的,但是看代码发现涉及到的方面太多,很难一篇文章介绍完,只能先简要分析下,自己以后针对性的进行分析了!
参考:
深入linux内核架构
linux内核3.10.1源码