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] = {
[ ... MAX_SWAPFILES - ] = {
.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 = ( << ), /* is slot in swap_info[] used? */
SWP_WRITEOK = ( << ), /* ok to write to this swap? */
SWP_DISCARDABLE = ( << ), /* swapon+blkdev support discard */
SWP_DISCARDING = ( << ), /* now discarding a free cluster */
SWP_SOLIDSTATE = ( << ), /* blkdev seeks are cheap */
SWP_CONTINUED = ( << ), /* swap_map has count continuation */
SWP_BLKDEV = ( << ), /* its a block device */
SWP_FILE = ( << ), /* set after swap_activate success */
/* add others here before... */
SWP_SCANNING = ( << ), /* 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源码

linux物理页面的换入换出简析的更多相关文章

  1. Linux内核RCU(Read Copy Update)锁简析

    在非常早曾经,大概是2009年的时候.写过一篇关于Linux RCU锁的文章<RCU锁在linux内核的演变>,如今我承认.那个时候我尽管懂了RCU锁,可是我没有能力用一种非常easy的描 ...

  2. Linux驱动之USB总线驱动程序框架简析

    通用串行总线(USB)是主机和外围设备之间的一种连接.USB总线规范有1.1版和2.0版,当然现在已经有了3.0版本.USB1.1支持两种传输速度:低速为1.5Mbps,高速为12Mbps.USB2. ...

  3. Linux驱动之平台设备驱动模型简析(驱动分离分层概念的建立)

    Linux设备模型的目的:为内核建立一个统一的设备模型,从而有一个对系统结构的一般性抽象描述.换句话说,Linux设备模型提取了设备操作的共同属性,进行抽象,并将这部分共同的属性在内核中实现,而为需要 ...

  4. Linux中 /proc/[pid] 目录各文件简析

    Linux 内核提供了一种通过 proc 文件系统,在运行时访问内核内部数据结构.改变内核设置的机制.proc 文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间.它以文件系统的方式为访问系 ...

  5. Linux内存管理 (4)分配物理页面

    专题:Linux内存管理专题 关键词:分配掩码.伙伴系统.水位(watermark).空闲伙伴块合并. 我们知道Linux内存管理是以页为单位进行的,对内存的管理是通过伙伴系统进行. 从Linux内存 ...

  6. Linux 目录结构学习与简析 Part2

    linux目录结构学习与简析 by:授客 QQ:1033553122 ---------------接Part 1-------------- #1.查看CPU信息 #cat /proc/cpuinf ...

  7. Linux VFS机制简析(二)

    Linux VFS机制简析(二) 接上一篇Linux VFS机制简析(一),本篇继续介绍有关Address space和address operations.file和file operations. ...

  8. Linux网络性能优化方法简析

    Linux网络性能优化方法简析 2010-12-20 10:56 赵军 IBMDW 字号:T | T 性能问题永远是永恒的主题之一,而Linux在网络性能方面的优势则显而易见,这篇文章是对于Linux ...

  9. Linux驱动之中断处理体系结构简析

    S3C2440中的中断处理最终是通过IRQ实现的,在Linux驱动之异常处理体系结构简析已经介绍了IRQ异常的处理过程,最终分析到了一个C函数asm_do_IRQ,接下来继续分析asm_do_IRQ, ...

随机推荐

  1. IOS设备信息与机型对照表

    http://blog.csdn.net/olsQ93038o99S/article/details/78374343 参考别人的文章吧....

  2. 重启oracle方法一二三

    startup nomount alter database mount alter database open Linux:方法1 用root以ssh登录到linux,打开终端输入以下命令: cd ...

  3. HeadFirst Jsp 09 (JSTL)

    JSTL (jsp standard tag library) 标准标记库 JSTL 安装, 注意你的每一个项目都需要一个 JSTL副本, 并把它放在WEB-INF/lib 目录下, 在 Tomcat ...

  4. 【BZOJ】1016: [JSOI2008]最小生成树计数(kruskal+特殊的技巧)

    http://www.lydsy.com/JudgeOnline/problem.php?id=1016 想也想不到QAQ 首先想不到的是:题目有说,具有相同权值的边不会超过10条. 其次:老是去想组 ...

  5. 【BZOJ】1648: [Usaco2006 Dec]Cow Picnic 奶牛野餐(dfs)

    http://www.lydsy.com/JudgeOnline/problem.php?id=1648 水题.. dfs记录能到达的就行了.. #include <cstdio> #in ...

  6. C# 将MSMQ消息转换成Json格式

    PS:主要就是一个配置文件和一个转换函数 配置文件app.config  之前要ADD reference -->   system.configuration & using.syst ...

  7. querySelectorAll 和getElementsByClassName的区别

    querySelectorAll 返回的是映射 改变其值不会改变document 而getElementsByClassName 改变它就会改变document 摘自JavaScript权威指南(jQ ...

  8. sdut 2158:Hello World!(第一届山东省省赛原题,水题,穷举)

    Hello World! Time Limit: 1000MS Memory limit: 65536K 题目描述 We know that Ivan gives Saya three problem ...

  9. Python+PyQt5:停靠组件QDockWidget的实现

    QMainWindow主窗体中放置停靠组件QDockWidget的实现流程: 第一步:创建QMainWindow窗体,这是承载QDockWidget的主体 第二步:创建QDockWidget组件实例, ...

  10. Mac 安装Bower

    1.安装bower,得首先安装node: 1 brew install npm  //npm是nodejs的程序包管理器,如果安装过nodejs,可忽略此步. 2.安装Git(因为需要从Git仓库获取 ...