|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
6 f! x. L6 L; e% s2 t0 L- L' I
主要内容:1 \; ?$ X: F, b! _9 o
0 x' c) N) |6 X
- 缓存简介
- 页高速缓存
- 页回写+ m' e" }& U' E% T" `
* C" {1 E% K1 s4 A- h
' Q; O) W( c4 b: |5 }0 j* l
1. 缓存简介
* Y! E6 D0 y+ d7 U% G在编程中,缓存是很常见也很有效的一种提高程序性能的机制。
& |: q5 m+ V, R" c* U9 y) j$ G) N# ^$ {; a; v1 s7 ?. }$ `
linux内核也不例外,为了提高I/O性能,也引入了缓存机制,即将一部分磁盘上的数据缓存到内存中。8 \- V' V: _2 o, D7 @) J2 ?9 @
) Q* J4 {, U* x4 @0 L/ p3 w; z. h
3 N& ^: Z% N* I+ W
/ u$ _" r7 t4 m4 f+ q1.1 原理
s; E2 O( J; U
0 T H! _4 T' V- q$ N之所以通过缓存能提高I/O性能是基于以下2个重要的原理:! q' Y& f+ Q* e, z- j0 I) }: q5 H* W# u
) u& ], W7 D8 P, v% D6 p% v F. B# M
- CPU访问内存的速度远远大于访问磁盘的速度(访问速度差距不是一般的大,差好几个数量级)
- 数据一旦被访问,就有可能在短期内再次被访问(临时局部原理)
1 ?' d- G, ?4 M8 i6 G9 m
6 `' c2 v& ?* U& Z/ G6 g/ v' q+ M3 G4 t! A
1.2 策略5 T1 D" `3 \8 s6 @9 w' m
n- @* n( Y) m2 W! x% S0 ^缓存的创建和读取没什么好说的,无非就是检查缓存是否存在要创建或者要读取的内容。3 L2 P$ s+ j$ g3 L l
5 o, o5 `1 Z: \) V: l但是写缓存和缓存回收就需要好好考虑了,这里面涉及到「缓存内容」和「磁盘内容」同步的问题。
5 D/ @' Y& e1 _' K7 i/ P/ \, j+ A" Y( m1 L
1.2.1 「写缓存」常见的有3种策略) w4 O5 p5 S0 j( C7 ^. ]
+ f5 D* V6 h; |: U- 不缓存(nowrite) :: 也就是不缓存写操作,当对缓存中的数据进行写操作时,直接写入磁盘,同时使此数据的缓存失效
- 写透缓存(write-through) :: 写数据时同时更新磁盘和缓存
- 回写(copy-write or write-behind) :: 写数据时直接写到缓存,由另外的进程(回写进程)在合适的时候将数据同步到磁盘$ |: i; ?7 e0 z
1 ] y9 }# a9 |9 }$ w8 m. j1 _
3 H0 E* s" C! r; C& R! V: O3种策略的优缺点如下:
* B$ ]) ]9 X# j" w. ~
* P/ Y) q8 u4 I- h8 u% p2 s0 ~5 h策略 | 复杂度 | 性能 | 不缓存 | 简单 | 缓存只用于读,对于写操作较多的I/O,性能反而会下降 | 写透缓存 | 简单 | 提升了读性能,写性能反而有些下降(除了写磁盘,还要写缓存) | 回写 | 复杂 | 读写的性能都有提高(目前内核中采用的方法) |
1 U- o# a. e- g& }8 C7 n% [' e, k$ T
' S7 o2 ]0 ?' C, C. S1.2.2 「缓存回收」的策略4 H& Z y2 J# f9 v6 p! d
& \! F# u6 {6 x. `! s& g/ N: q
- 最近最少使用(LRU) :: 每个缓存数据都有个时间戳,保存最近被访问的时间。回收缓存时首先回收时间戳较旧的数据。
- 双链策略(LRU/2) :: 基于LRU的改善策略。具体参见下面的补充说明
" D4 D8 t2 _- E* ]$ _: \( X( q$ g3 G 8 L2 \& `! L. |/ d6 ^* }4 x
4 W0 h$ Z6 {6 m- Q" A k; {补充说明(双链策略):3 h: H8 A& j0 D6 l
8 {* A% y; c( l( o* Q0 w
双链策略其实就是 LRU(Least Recently Used) 算法的改进版。+ D7 v2 B: R) d, u7 B- U! ~
6 t; y# P5 h4 z$ [它通过2个链表(活跃链表和非活跃链表)来模拟LRU的过程,目的是为了提高页面回收的性能。
2 q/ u/ v- y3 B, L+ u/ ^# d# v. k+ i' n, l Z/ {
页面回收动作发生时,从非活跃链表的尾部开始回收页面。$ X0 Q6 P2 u" i7 Z, j
2 b, V/ c& F* V" R) w 9 ~$ O; j* U5 {- w* l
, y+ s3 J6 `! I7 _9 ?/ i双链策略的关键就是页面如何在2个链表之间移动的。
+ g. W7 O/ v- o5 F$ h
; g8 ]& \7 ^, I+ a# f$ ~双链策略中,每个页面都有2个标志位,分别为1 R1 {# l, [ X% }
1 O+ V1 Y5 V4 o1 Y: O8 T' m7 y
PG_active - 标志页面是否活跃,也就是表示此页面是否要移动到活跃链表5 x! j# B& w# H+ M( E% J" k
* R9 s3 R8 g( [5 r
PG_referenced - 表示页面是否被进程访问到
6 ~) S' `8 y6 a/ E0 T+ H$ m# C
a$ n1 m' v4 b" _' r0 U# G $ E1 r* O( E7 R. X
6 j! h* {& s* T1 x+ y4 Z( P
页面移动的流程如下:
, y- M8 C+ @7 S* e& }7 k( J$ U/ L7 C% U* S9 Y0 R
- 当页面第一次被被访问时,PG_active 置为1,加入到活动链表
- 当页面再次被访问时,PG_referenced 置为1,此时如果页面在非活动链表,则将其移动到活动链表,并将PG_active置为1,PG_referenced 置为0
- 系统中 daemon 会定时扫描活动链表,定时将页面的 PG_referenced 位置为0
- 系统中 daemon 定时检查页面的 PG_referenced,如果 PG_referenced=0,那么将此页面的 PG_active 置为0,同时将页面移动到非活动链表
8 M. q' k: ~9 D0 m6 p ?3 J
# k, J& u( l2 v2 E' h - v: f+ Y& u& [: ~3 p6 o7 j
8 \) b* S* f9 _
2. 页高速缓存
\( t7 }( ^; ^9 x. k! Q
; k% W. u1 {4 p1 ]; ~) A2 a故名思义,页高速缓存中缓存的最小单元就是内存页。
; I* g7 T+ { F2 Y& ^; ^' e, Z# _& r: b
但是此内存页对应的数据不仅仅是文件系统的数据,可以是任何基于页的对象,包括各种类型的文件和内存映射。' D9 P6 i& [% u% }+ Z D
) q9 r% j9 k* L& x ) d% N4 q2 T" @/ _/ d- ]8 P( Y9 ]
5 P6 _. U. V. J: ], f' n
2.1 简介1 D& D+ ~9 ]. W4 ^; D B3 x+ t
% U. e9 v# |3 r! [3 a/ w页高速缓存缓存的是具体的物理页面,与前面章节中提到的虚拟内存空间(vm_area_struct)不同,假设有进程创建了多个 vm_area_struct 都指向同一个文件,9 h7 P/ }; Q: W: w6 t
; j7 E; L2 n0 U: L# k那么这个 vm_area_struct 对应的 页高速缓存只有一份。
% G( N; w. X) ^! E* ?
! _1 Y* Y w0 B# P: h# X5 N5 \也就是磁盘上的文件缓存到内存后,它的虚拟内存地址可以有多个,但是物理内存地址却只能有一个。( v W5 T6 r; ?1 [* E' `, d
! I3 n: q" M' i! y9 B2 @
3 U* V2 h: Z% L S" D7 I* h$ S* s5 h/ ]. l# t
为了有效提高I/O性能,页高速缓存要需要满足以下条件:+ s# b: o4 |1 a+ U6 z* I
0 F0 a! H$ l( i
- 能够快速检索需要的内存页是否存在
- 能够快速定位 脏页面(也就是被写过,但还没有同步到磁盘上的数据)
- 页高速缓存被并发访问时,尽量减少并发锁带来的性能损失
R1 T, W5 s) W/ v) [ [ $ f% Z2 r9 w# p- O( F* z! H4 A
3 @- {6 ~; r% }6 u6 g. k下面通过分析内核中的相应的结构体,来了解内核是如何提高 I/O性能的。$ K1 f- W' N: n6 ]6 x
; D5 b% A3 o" @8 q8 _$ V5 |: @
. w2 @ X* \$ _; U1 N: \
, v' ~# x" @0 a( P" O! n) p
2.2 实现8 k2 P1 E; m( e5 n1 D8 C
@( X0 f1 L. c% A0 i4 k9 L) d3 Y
实现页高速缓存的最重要的结构体要算是 address_space ,在 <linux/fs.h> 中
1 ?: U9 m. F% G$ s4 j' A
0 }/ G/ x6 J- [& u7 \$ ]- struct address_space {
- struct inode *host; /* 拥有此 address_space 的inode对象 */
- struct radix_tree_root page_tree; /* 包含全部页面的 radix 树 */
- spinlock_t tree_lock; /* 保护 radix 树的自旋锁 */
- unsigned int i_mmap_writable;/* VM_SHARED 计数 */
- struct prio_tree_root i_mmap; /* 私有映射链表的树 */
- struct list_head i_mmap_nonlinear;/* VM_NONLINEAR 链表 */
- spinlock_t i_mmap_lock; /* 保护 i_map 的自旋锁 */
- unsigned int truncate_count; /* 截断计数 */
- unsigned long nrpages; /* 总页数 */
- pgoff_t writeback_index;/* 回写的起始偏移 */
- const struct address_space_operations *a_ops; /* address_space 的操作表 */
- unsigned long flags; /* gfp_mask 掩码与错误标识 */
- struct backing_dev_info *backing_dev_info; /* 预读信息 */
- spinlock_t private_lock; /* 私有 address_space 自旋锁 */
- struct list_head private_list; /* 私有 address_space 链表 */
- struct address_space *assoc_mapping; /* 缓冲 */
- struct mutex unmap_mutex; /* 保护未映射页的 mutux 锁 */
- } __attribute__((aligned(sizeof(long))));9 K4 O/ @8 M$ O" T- Q7 I5 f
4 ?1 ? }, n: E$ r! m1 \. ]& x7 P+ R; \1 r4 d8 e
, ] B3 u3 K/ a1 T( A
补充说明:
: T1 t, `* ~# c" G. v8 i% p* {! M- w5 D2 O4 n6 V- r
- inode - 如果 address_space 是由不带inode的文件系统中的文件映射的话,此字段为 null
- page_tree - 这个树结构很重要,它保证了页高速缓存中数据能被快速检索到,脏页面能够快速定位。
- i_mmap - 根据 vm_area_struct,能够快速的找到关联的缓存文件(即 address_space),前面提到过, address_space 和 vm_area_struct 是 一对多的关系。
- 其他字段主要是提供各种锁和辅助功能
* a8 s; T: T( D# {. o ( C8 C/ z* q2 n2 e/ J! B% _& l7 M
' C) }; b4 w7 o2 O: P/ j! i" z2 j
此外,对于这里出现的一种新的数据结构 radix 树,进行简要的说明。 ]$ H7 W) R& \' j3 n0 D( Q
* y: J% r7 N! |/ Z% `* q0 Fradix树通过long型的位操作来查询各个节点, 存储效率高,并且可以快速查询。
2 z9 V7 S( Q. }5 B' D" M6 o& q% i! o0 M- `& `# i( B
linux中 radix树相关的内容参见: include/linux/radix-tree.h 和 lib/radix-tree.c& \5 r x8 _4 }4 F( \7 s
& P0 b$ [2 B* q" ~; c" @
下面根据我自己的理解,简单的说明一下radix树结构及原理。9 q, j- H! d8 |3 _+ z3 E
) M% X& g0 `2 ^9 S/ K; T
! S0 ]/ V# q4 t( t; c
$ K/ U! p( a( K9 X2 G; z2.2.1 首先是 radix树节点的定义) F/ k* Z! A P9 k4 [
9 a# ~ C0 U, i9 U0 B. X9 j- /* 源码参照 lib/radix-tree.c */
- struct radix_tree_node {
- unsigned int height; /* radix树的高度 */
- unsigned int count; /* 当前节点的子节点数目 */
- struct rcu_head rcu_head; /* RCU 回调函数链表 */
- void *slots[RADIX_TREE_MAP_SIZE]; /* 节点中的slot数组 */
- unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; /* slot标签 */
- };' ?2 ]6 m8 j1 m, h1 ~
% G* O) e. O$ P: z. V
' u" }/ _* a. q& a0 t) L7 Y! Q l5 W5 M* Y! s
弄清楚 radix_tree_node 中各个字段的含义,也就差不多知道 radix树是怎么一回事了。* H6 K. _$ q. X: a# N" z' g
Y$ }7 A5 V' C. e0 Q) f* p
- height 表示的整个 radix树的高度(即叶子节点到树根的高度), 不是当前节点到树根的高度
- count 这个比较好理解,表示当前节点的子节点个数,叶子节点的 count=0
- rcu_head RCU发生时触发的回调函数链表
- slots 每个slot对应一个子节点(叶子节点)
- tags 标记子节点是否 dirty 或者 wirteback
( y* d9 c) X& ]* O4 O( g2 [ B
* a* E7 c$ T& P1 n) b. Y u- d
" [" d3 H7 z9 Z0 O( i; l& m2.2.2 每个叶子节点指向文件内相应偏移所对应的缓存页
: L T. {0 q) b) A" G. A d* }; t1 U2 |- T
比如下图表示 0x000000 至 0x11111111 的偏移范围,树的高度为4 (图是网上找的,不是自己画的)
7 S$ ^# d/ a( A( ], Q: B/ r( h$ }% m4 u1 {* p; l- v4 x
: H1 @8 ]9 Z. D/ P. j
0 q/ r+ e" ?& _- H % R0 k) b& Z& k6 E
6 a! l9 r$ X* C; J Z' j2.2.3 radix tree 的叶子节点都对应一个二进制的整数,不是字符串,所以进行比较的时候非常快
' F, |) I/ P" |9 k G( m0 ]8 }+ W1 D: f: b
其实叶子节点的值就是地址空间的值(一般是long型)) ], }# ?; `1 p) C3 h# G
! E' f7 u! [3 Q' w g7 O4 Y- y! _ 9 k; x$ f n8 E
6 \2 [+ [/ [5 P3 G' E1 y
3. 页回写
2 s3 k4 X+ v' F2 r* A# \
* s3 x( n( l5 A由于目前linux内核中对于「写缓存」采用的是第3种策略,所以回写的时机就显得非常重要,回写太频繁影响性能,回写太少容易造成数据丢失。6 {9 |2 V% m z
+ P1 [! X; m3 [7 E# u
: r) n& k$ ?5 b
% O, x1 k9 S9 _% l3.1 简介9 n' c- Z- G: z9 i* B9 h9 |) [
6 Q, P" K8 c/ ~4 N! ^
linux 页高速缓存中的回写是由内核中的一个线程(flusher 线程)来完成的,flusher 线程在以下3种情况发生时,触发回写操作。+ U3 x" e1 h% }# A
% ^6 N5 z$ Y" I j1 `! G1. 当空闲内存低于一个阀值时
9 Y& k4 W1 Y, s1 t/ H% f' U( B) m8 p! `3 C. V" ^
空闲内存不足时,需要释放一部分缓存,由于只有不脏的页面才能被释放,所以要把脏页面都回写到磁盘,使其变成干净的页面。
. W0 a. Q8 E/ Z P6 k- B) ]( ~. W2 Z9 x
2. 当脏页在内存中驻留时间超过一个阀值时
: m" I7 r" ^6 Z! T
1 N2 _+ [( K6 s+ E: [5 C' n7 L 确保脏页面不会无限期的驻留在内存中,从而减少了数据丢失的风险。
3 \' Y/ |1 y) K4 U% z# P9 ~3 k$ n- T# ]( E8 F0 A! i
3. 当用户进程调用 sync() 和 fsync() 系统调用时
, c' u# G8 W. E% z+ W" A5 G
9 t9 F8 v8 o; A5 D) `3 p 给用户提供一种强制回写的方法,应对回写要求严格的场景。
. H1 L H9 d) P v3 _- Q: g; N7 s" m4 @' @ F
+ q( _' ^( D( |, }+ M0 C/ L, Q( A
页回写中涉及的一些阀值可以在 /proc/sys/vm 中找到4 E% J- R% m+ S# ]; W
) ^+ P" G( d2 L# @; p0 Y下表中列出的是与 pdflush(flusher 线程的一种实现) 相关的一些阀值
, B, o4 d! @# f% x ^% h* m
" e$ l% H6 }1 `; g7 s J& ?' \阀值 | 描述 | dirty_background_ratio | 占全部内存的百分比,当内存中的空闲页达到这个比例时,pdflush线程开始回写脏页 | dirty_expire_interval | 该数值以百分之一秒为单位,它描述超时多久的数据将被周期性执行的pdflush线程写出 | dirty_ratio | 占全部内存的百分比,当一个进程产生的脏页达到这个比例时,就开始被写出 | dirty_writeback_interval | 该数值以百分之一秒未单位,它描述pdflush线程的运行频率 | laptop_mode | 一个布尔值,用于控制膝上型计算机模式 |
, n& K6 u, r7 t7 Q: N! z" g/ l+ c3 a+ c2 b0 O3 q
: |# A& a0 E) y( u+ @5 S3.2 实现 b# p, [9 T) j& h
. K5 w6 ?2 j( }" t0 Lflusher线程的实现方法随着内核的发展也在不断的变化着。下面介绍几种在内核发展中出现的比较典型的实现方法。0 Y7 `* w" \2 f
1 B! c+ g1 C) W- B
1. 膝上型计算机模式
! v& w1 R2 j. X; b
6 a, z& T5 X! E5 M3 i# Y: I; I这种模式的意图是将硬盘转动的机械行为最小化,允许硬盘尽可能长时间的停滞,以此延长电池供电时间。
. z) a$ ~/ d }: C& n, m& O& ^( C! |$ h \4 P y. o
该模式通过 /proc/sys/vm/laptop_mode 文件来设置。(0 - 关闭该模式 1 - 开启该模式)
5 p) u& r$ d: l
( n/ @( h( x' ]& P* z- J4 W
& R3 _% X+ j) Y* g4 ]* H+ l. w5 g2 C+ P* u1 | Z L3 e l: ?. {' q& w
2. bdflush 和 kupdated (2.6版本前 flusher 线程的实现方法)3 Y' q. r4 k( l% S c4 x" `
9 s0 Z$ `7 n5 x1 a% `
bdflush 内核线程在后台运行,系统中只有一个 bdflush 线程,当内存消耗到特定阀值以下时,bdflush 线程被唤醒- t* \, F: |' ?/ a
. v2 U' f! \* y" E7 Z1 E& D% Gkupdated 周期性的运行,写回脏页。: ~5 l, x( g2 T( w0 {1 C& F, P" I
& m3 u" d- U' K/ H) s ~
8 b% R% _$ @4 U; a/ m9 v; p' r. C$ ~8 n. P/ g
bdflush 存在的问题:5 P$ D. M: M% ^ c
* }# G6 i |! Z. q( N整个系统仅仅只有一个 bdflush 线程,当系统回写任务较重时,bdflush 线程可能会阻塞在某个磁盘的I/O上,
- v' K7 l5 Z1 u. M+ z$ ~' A [3 s! [/ z; U5 z) V4 h0 a4 p9 l
导致其他磁盘的I/O回写操作不能及时执行。8 t7 `9 E0 o3 d2 x
, r" f4 Q% g3 G
; M! V7 W% @. Z1 W% M$ |
" |( E, h' L* i3. pdflush (2.6版本引入) h+ ^' h5 Z4 G
6 d( `" `' B1 l4 c( ]8 K+ I; \5 Apdflush 线程数目是动态的,取决于系统的I/O负载。它是面向系统中所有磁盘的全局任务的。8 a. S) A$ M: W
4 {' ^$ d. u) d) R6 _ H' [
$ T, g5 p, Z/ c. q/ X
& Y7 N: B& _# `# _pdflush 存在的问题:8 s. D* S+ k5 |% F) t- v
; S$ f& P2 U5 g: ^pdflush的数目是动态的,一定程度上缓解了 bdflush 的问题。但是由于 pdflush 是面向所有磁盘的,
' |" l% h- ^+ y. k6 F0 [1 f9 E) \( O" t$ I2 a- @/ t
所以有可能出现多个 pdflush 线程全部阻塞在某个拥塞的磁盘上,同样导致其他磁盘的I/O回写不能及时执行。6 u5 G" U% W0 U* j- p e0 `
6 V, p/ r; T- Y; q7 I' G: |$ A $ D* ^, z3 J! a: J0 k
7 P+ z4 H! Z' `5 R: v$ J- a4. flusher线程 (2.6.32版本后引入)0 w4 q [7 o( `
* c; J# X9 Q: m9 @* T* D" Bflusher线程改善了上面出现的问题:
6 n4 p0 `) n, Y4 v A
( j5 R) ]: O% y7 k首先,flusher 线程的数目不是唯一的,这就避免了 bdflush 线程的问题+ n- Q, e: K `+ ]% ?0 p
, c4 ?& [ I* }3 K- R) W. }0 M
其次,flusher 线程不是面向所有磁盘的,而是每个 flusher 线程对应一个磁盘,这就避免了 pdflush 线程的问题. {. v: v* A) O. X ~( h
|
|