今天决定了下周的计划:
- 在Ubuntu18.04上部署并跑通PMFS;
- 复现《非易失内存文件系统PMFS的性能优化设计与实现》的PMFS实现和测试部分;
- 区分DAX和mmap之间的关系。
今天完成了:
- 在Ubuntu18.04上部署PMDK并调通;
- 区分DAX和mmap之间的关系。
一些小知识点:
mm_struct中mm_count与mm_users的区别
为什么mm_struct的撤销取决于mm_count,主要是为了支持内核进程借用mm_struct。
mm_count是针对进程的,而mm_users是针对线程的,其实在linux中进程和线程之间的区别比较模糊,不如通过上面的例子理解更为直观:比如一个用户进程A有3个线程,那么此时mm_count是1,mm_users是3,这时内核将进程A调出,将一个内核线程B调入,这时系统线程B的task_struct->mm是NULL,因为内核线程并不需要访问用户空间,而mm_struct描述的是用户空间的情况。那么这个时候内核线程B会借用上一个进程A的mm_struct中的页表访问内核空间。
在代码上表现为:
内核线程:task_struct -> mm = NULL, task_struct -> active_mm = <last process task struct> -> mm
用户进程:task_struct -> mm = task_struct -> active_mm
因为内核空间也在页表中,且内核空间对进程来说都是共享的,因此借用并不存在问题。这个时候mm_user为3,mm_count为2,如果此时另一个CPU调度进程A且A执行完退出,那么mm_user为0,mm_count为1,此时B借用A的mm_struct,所以A的mm_struct不会被撤销。如果不维护两个计数器,那么A退出之后,mm_struct就会被撤销,B就无法通过A的mm_struct访问内核空间。
页缓存(page cache)相关
由内存中的空闲页面组成,对应磁盘上的物理块,可以动态调整容量大小,称被缓存的存储设备为后备存储。此时如果使用read这个系统调用,会首先在页缓存中找是否存在对应的页面,如果不命中再访问存储设备,并把读来的数据放到页缓存中。
写页缓存策略:write-through、write-back
缓存回收策略:LRU,LRU的局限显而易见(比如LRU容量为2,页面访问序列为ABC循环,此时命中率为零),但是Linux实现的是双LRU策略,维护一个活跃链表和一个不活跃链表,只换出不活跃链表中的页面,两个链表的维护规则是伪LRU:尾部加入,头部移除。同时动态平衡两个队列的大小,这样避免了只访问一次的页面在页缓存中保存太长的时间而换出访问次数多,但是时间上比较久远的页面。这里其实就是一种折中思想的体现,LRU考虑了时间局部性(访问间隔时间)也考虑了空间局部性(访问次数),其实这种LRU策略就是降低了时间局部性的要求,较为重视空间局部性(访问次数)。
Linux页缓存使用struct address_space来管理缓存和页面IO,address_space其实是vm_area_struct的物理地址对等体,即文件可以拥有多个虚拟地址,但是在物理内存中的地址只有一个。一般address_space会与内核对象相关联,与inode关联时,host就指向inode。
后备存储通过自己的address_space_operation描述自己与页缓存的交互。
老版本内核中struct address_space的page_tree与最近的内核中的i_pages都指向radix tree。
在很早之前的版本中,buffer cache和page cache还是分开管理的,在2.4之后的版本中则统一为page cache进行管理,
flusher线程
在以下几种情况下,脏页写回存储设备:
- 空闲内存低于特定阈值,此时需要将脏页写回腾出内存空间;
- 脏页驻留在内存空间达一定时间后,脏页不会无限储存在内存中;
- 用户进程使用系统调用sync或者fsync时。
以下涉及的具体参数均在2.6内核为基准,更高级的内核可能会有所变动。
在第一种情况下,内存阈值通过dirty_background_ratio设置,低于dirty_background_ratio时flusher_threads唤醒一个或多个flusher线程,线程进一步调用bdi_writeback_all开始将脏页写回磁盘。直到写回的页数满足已经等于传递的参数或者空闲内存数高于dirty_background_ratio才停止。在第二种情况下,flusher线程会在后台定期唤醒,确保内存中不会有长期存在的脏页,这也是一个保证内存一致性的机制。为了尽可能减少磁盘功耗,linux采用一种特殊的页写回机制:laptop_mode,主要是为了避免磁盘旋转仅仅是为了写回页面,尽可能在磁盘运转的过程中完成尽可能多的任务。
bdflush、kupdated和pdflush
简单讲,pdflush thread一定程度上可以说是bdflush和kupdated的结合。
在2.6之前,bdflush负责将dirty buffer写回磁盘,系统后台只有一个bdflush线程,kupdated负责周期性调用bdflush(可能)。
而在2.6之后,pdflush(page dirty flush)则会周期性的动态调整线程数目,之后调整为每个存储设备一个刷新线程,这样简化了拥塞逻辑,flusher线程之后替代了pdflush线程,二者主要区别是针对每个磁盘独立执行回写操作。
避免拥塞
最早的bdflush系统中只有一个,很显然会在回写任务重时导致拥塞,很可能拥塞在一个请求队列中而无法响应其它设备的回写队列。
多个flusher线程来解决这个问题,可以相互独立地将脏页刷回磁盘,但是也会出现多个flusher线程在同一个拥塞队列上挂起的情况,这样会增加内存开销,因此flusher采用拥塞回避策略,优先从那些没有拥塞的队列写回页。目前(2.6)flusher和具体块设备关联,即对应的线程收集对应设备上的脏页,并写回对应磁盘,这样提高了IO公平性,降低了饥饿风险,提高了写回性能。
DAX源码级分析
Linux 内存管理:DAX(Direct Access)机制的作用及实现原理
DAX, mmap(), and a "go faster" flag
上面两篇文章讲的很清楚,不赘述了。
需要解决的知识:
- 优先搜索树:堆与radix tree的结合;