找回密码
 注册
关于网站域名变更的通知
查看: 310|回复: 1
打印 上一主题 下一主题

Linux内核设计与实现之块I/O层

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2021-1-27 09:43 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式

EDA365欢迎您登录!

您需要 登录 才可以下载或查看,没有帐号?注册

x
* ?3 L* X/ h- u. T* v2 x
主要内容:1 f' o8 n" E. e! v5 T0 k% ~: G
$ g, w2 t+ V$ V7 q1 N
块设备简介
6 t0 w* x- s  L/ B( i1 G内核访问块设备的方法' x/ t" m4 c3 h; h, q  E: C3 h; M
内核I/O调度程序
9 R! p5 E. f% G/ q8 P, B, D + p* e/ r7 m. d  k! N/ h+ @* {4 u7 e

$ Z: D2 o4 E$ Y' Q' q1. 块设备简介0 s" p+ v2 C' _# W
I/O设备主要有2类:& Q% R: |6 x$ g$ p3 R( d" r1 v
  p8 g$ X: ~8 }
字符设备:只能顺序读写设备中的内容,比如 串口设备,键盘
" }9 c+ N4 w/ W" T0 Z: }3 U+ n0 n块设备:能够随机读写设备中的内容,比如 硬盘,U盘
1 Z# a" V. V6 m# b9 H字符设备由于只能顺序访问,所以应用场景也不多,这篇文章主要讨论块设备。* e* w% `9 U/ B/ s( i! |

; x, K- y: C8 e+ v9 L( }' u块设备是随机访问的,所以块设备在不同的应用场景中存在很大的优化空间。
  ^9 b2 [7 h. h9 l# V$ ]! a, @2 k  ]+ J7 h- B1 d3 z6 b$ {: [

. ?  H1 u5 K2 Y4 P6 x9 M
+ P! q. n: f! X" S块设备中最重要的一个概念就是块设备的最小寻址单元。
' C3 h; I& v- L8 p% g
0 i" s) B' i' R% V块设备的最小寻址单元就是扇区,扇区的大小是2的整数倍,一般是 512字节。5 o4 R  D/ ?" H- |
. ]& C; ]4 n8 G7 E- ]5 j
扇区是物理上的最小寻址单元,而逻辑上的最小寻址单元是块。/ R7 B, w" a& K/ H* q' e0 s
. d+ `  j) d+ T5 @0 n
为了便于文件系统管理,块的大小一般是扇区的整数倍,并且小于等于页的大小。% A& R. I8 W" @" u$ N9 Y2 C

$ [: z/ C* k# _7 K% i2 A. T
  r! u$ r8 C, a% c
; P* l2 ]2 ~; e9 E; i9 L2 [查看扇区和I/O块的方法:
# ?' C! D9 t& ]: ~! I& d4 [$ p4 ^6 o9 w; G
复制代码
* x- W. O3 f  F9 \& t1 T[wangyubin@localhost]$ sudo fdisk -l+ K) ?3 L& ?% q4 ^6 y% X' S

9 D, D* u6 X4 {0 O$ gWARNING: GPT (GUID Partition Table) detected on '/dev/sda'! The util fdisk doesn't support GPT. Use GNU Parted.8 {4 E' E4 e* J3 n" C% y

5 P: r* L7 L; v% z
( e/ f( f7 I, F0 C1 G2 K) W1 EDisk /dev/sda: 500.1 GB, 500107862016 bytes, 976773168 sectors' i' ?! ~( I2 n+ G' ]: t
Units = sectors of 1 * 512 = 512 bytes. Z  l( o& h. y/ D' o& h
Sector size (logical/physical): 512 bytes / 4096 bytes
+ D$ e7 |- q# l- x. FI/O size (minimum/optimal): 4096 bytes / 4096 bytes
; ?* ]1 O8 F* i/ aDisk identifier: 0x00000000- k* G6 e! s9 e* H: m. d
复制代码
) D/ S5 K0 f+ R2 |( b上面的 Sector size 就是扇区的值,I/O size就是 块的值
. C# N7 H  n" D6 H. Y! Z& e  `( O0 [) p3 a0 c
从上面显示的结果,我们发现有个奇怪的地方,扇区的大小有2个值,逻辑大小是 512字节,而物理大小却是 4096字节。
! `1 S0 ?* s' Y+ T( a6 i
* Y9 g) [2 L0 {% c$ c* H其实逻辑大小 512字节是为了兼容以前的软件应用,而实际物理大小 4096字节是由于硬盘空间越来越大导致的。1 ?6 ^- d! U9 U/ ~9 `  T
) z4 }) i$ q" n8 P
具体的来龙去脉请参考:4KB扇区的原因7 U& J& i4 ?5 x# ?5 g% f. u
6 T$ z( o) F6 F6 r* C
) G2 n0 J1 n! O

' R1 \& C& |" ]' I2. 内核访问块设备的方法1 p: W3 @7 O, W
内核通过文件系统访问块设备时,需要先把块读入到内存中。所以文件系统为了管理块设备,必须管理[块]和内存页之间的映射。
2 h- h9 R+ U0 J7 C% [- y: |) o0 E3 \5 h* u! v: z1 ^5 e
内核中有2种方法来管理 [块] 和内存页之间的映射。
3 K; h' ?. W: r/ y: |/ t$ T1 ~. v1 S) A+ L& F: R
缓冲区和缓冲区头8 r  e6 f/ p* C( V* a' c$ P
bio
5 l$ G+ q* t6 U: U7 E: T4 C 7 l% j. D; q5 R* p
* ^0 S4 i+ m& a. G/ R. f3 D6 Q
2.1 缓冲区和缓冲区头
- ~7 j( s# h- H3 Q4 b; }# D* _) R每个 [块] 都是一个缓冲区,同时对每个 [块] 都定义一个缓冲区头来描述它。
( a. S* i5 h& p$ }: F! R+ [7 |% _4 q: w4 d
由于 [块] 的大小是小于内存页的大小的,所以每个内存页会包含一个或者多个 [块]
7 s* ^/ j2 }4 e6 s: y% q; ?
& x: |( @9 g9 U
* c) e, c2 j& K( B1 L/ {( _
  s4 d% [2 R( T缓冲区头定义在 <linux/buffer_head.h>: include/linux/buffer_head.h/ J3 ~* ?: T4 S, |% ~+ P
& r6 a9 P" [' R5 \4 U
复制代码6 C: V2 M; r3 F8 E( t# f
struct buffer_head {2 `9 }5 t% }* f$ `+ x: w6 _, k: p. z
    unsigned long b_state;            /* 表示缓冲区状态 */
4 u5 J+ t. F. o8 i    struct buffer_head *b_this_page;/* 当前页中缓冲区 */
" I: B; i4 l4 d3 i  E6 V    struct page *b_page;            /* 当前缓冲区所在内存页 */1 P$ t5 I, Z" [7 F
* Z: K" i7 r) S, P! f2 t4 f9 B
    sector_t b_blocknr;        /* 起始块号 */2 ]; C% M7 @( {6 L
    size_t b_size;            /* buffer在内存中的大小 */; X' x) {# ?- y# C. |2 b# K9 u; {
    char *b_data;            /* 块映射在内存页中的数据 */
2 j) E/ C% T0 o' ~. i7 G1 w/ Q5 f
2 K  r5 {" D5 J7 S5 w" _5 X. v( W    struct block_device *b_bdev; /* 关联的块设备 */% b% o" `3 S7 M, ^  e: c
    bh_end_io_t *b_end_io;        /* I/O完成方法 */
+ u0 ^3 O# X! j; N$ x& Q) [     void *b_private;             /* 保留的 I/O 完成方法 */
- l0 C$ x$ T6 \* g6 ]4 G    struct list_head b_assoc_buffers;   /* 关联的其他缓冲区 */2 V( v8 x! q2 X& q' [: v
    struct address_space *b_assoc_map;    /* 相关的地址空间 */
0 }0 |5 a# [$ D. Z( U: N    atomic_t b_count;                    /* 引用计数 */
6 S, ?0 ?$ T4 [4 M, h};
- G5 F* Y+ L) h$ \3 d复制代码/ a/ l; a% U, Z% a4 [

6 T3 d& h8 y/ m4 n6 l6 ?/ X- b( }! D
整个 buffer_head 结构体中的字段是减少过的,以前的内核中字段更多。
6 q# i2 e$ \" o+ D$ N" h
6 G! ^2 ~. c' o) s: X7 I" c各个字段的含义通过注释都很明了,只有 b_state 字段比较复杂,它涵盖了缓冲区可能的各种状态。3 c; h! m& V2 ], Q7 w  ?4 H; K& z
/ g2 k& V$ z* n/ w: Y
复制代码$ C+ C2 W- p4 q* j
enum bh_state_bits {
$ h$ m- ^# ^9 J4 D. z: c    BH_Uptodate,    /* 包含可用数据 */
6 d% G7 w$ ?8 \, M0 ^" L, K  o    BH_Dirty,    /* 该缓冲区是脏的(说明缓冲的内容比磁盘中的内容新,需要回写磁盘) */
+ m. z3 m" J& [/ i# p1 Q: g    BH_Lock,    /* 该缓冲区正在被I/O使用,锁住以防止并发访问 */+ S$ [! g* X3 i5 b% S2 @
    BH_Req,        /* 该缓冲区有I/O请求操作 */
( Y2 N6 Y+ e& L* n) r3 c8 H  P    BH_Uptodate_Lock,/* 由内存页中的第一个缓冲区使用,使得该页中的其他缓冲区 */. V. L9 ^6 L4 n( ]( e
8 M% K  b  N2 C7 ^. m
    BH_Mapped,    /* 该缓冲区是映射到磁盘块的可用缓冲区 */" |3 W/ e; l6 u! q
    BH_New,        /* 缓冲区是通过 get_block() 刚刚映射的,尚且不能访问 */: G1 ^4 x- r$ E7 v2 F( k: U7 J) R
    BH_Async_Read,    /* 该缓冲区正通过 end_buffer_async_read() 被异步I/O读操作使用 */- r4 u& f3 F4 f" j+ [9 L2 J7 s7 p
    BH_Async_Write,    /* 该缓冲区正通过 end_buffer_async_read() 被异步I/O写操作使用 */
' x9 H  v. O3 K9 F6 m+ \. K2 Y    BH_Delay,    /* 缓冲区还未和磁盘关联 */* z- ]. }! m' l8 E9 d$ K2 m
    BH_Boundary,    /* 该缓冲区处于连续块区的边界,下一个块不在连续 */
# v, L$ b& a9 r    BH_Write_EIO,    /* 该缓冲区在写的时候遇到 I/O 错误 */
1 o( R& ^, |" K+ g1 K9 k    BH_Ordered,    /* 顺序写 */8 I# A1 r* _; l9 x' ?6 v3 i
    BH_Eopnotsupp,    /* 该缓冲区发生 “不被支持” 错误 */
* ~5 R3 m- i0 ~% ^    BH_Unwritten,    /* 该缓冲区在磁盘上的位置已经被申请,但还有实际写入数据 */' k) \$ }, U0 s& ]; A, g( \' N
    BH_Quiet,    /* 该缓冲区禁止错误 */6 I$ D* ~8 T+ \# p4 t$ R
! ?* V5 F% E9 T
    BH_PrivateStart,/* 不是表示状态,分配给其他实体的私有数据区的第一个bit */: z1 X3 i# v! x# e, }3 U5 s# a! h
};* F2 o1 {/ M" [2 o' A0 l9 b  O
复制代码0 I- e" o  T& L2 F2 b
8 D. v! [# j5 d3 l2 C+ ?. a
+ C2 R2 n' T0 J* \
在2.6之前的内核中,主要就是通过缓冲区头来管理 [块] 和内存之间的映射的。
: w$ U' Z4 \4 f$ h/ W4 X9 |- w* s. c# w3 u
用缓冲区头来管理内核的 I/O 操作主要存在以下2个弊端,所以在2.6开始的内核中,缓冲区头的作用大大降低了。
$ u2 L5 z/ Z( v0 a% A3 F  `, R3 m7 H* W
# q# i% D" E4 @9 i- 弊端 1
& _! H! M- e" `$ P; F. @
6 V" f2 A  ~4 ~4 W* D对内核而言,操作内存页是最为简便和高效的,所以如果通过缓冲区头来操作的话(缓冲区 即[块]在内存中映射,可能比页面要小),效率低下。
( A5 t2 m  l: h. d' w' U* g
5 Z; \5 o: A/ [! V# r; o, z4 H而且每个 [块] 对应一个缓冲区头的话,导致内存的利用率降低(缓冲区头包含的字段非常多)6 @: _2 ^, d8 F& @
; O* K6 d3 k! ^+ ]! P, j% t8 j( S
- 弊端 28 z: i( P# E" L) e. A6 q% {
$ \! o9 r) s+ N& f: z8 V
每个缓冲区头只能表示一个 [块],所以内核在处理大数据时,会分解为对一个个小的 [块] 的操作,造成不必要的负担和空间浪费。
$ F6 P0 J/ {; [* p! T: x
* h5 @  Y/ i/ o8 ^" D
8 G1 ^9 ^+ F) u- a# D# \
$ ]: j9 m# O. d1 c2.2 bio" p; }$ H/ I! ]+ u3 o
bio结构体的出现就是为了改善上面缓冲区头的2个弊端,它表示了一次 I/O 操作所涉及到的所有内存页。% }: H3 `9 N: @* k: T6 [9 w

# _4 \( `3 o" h  m7 D复制代码/ X) D; r8 Z' p1 Y5 Z( D0 o7 i$ O
/*
0 R7 m& `2 ~4 t  o9 D * I/O 操作的主要单元,针对 I/O块和更低级的层 (ie drivers and' i  r$ n! h: c( Q- h" ?. d
* stacking drivers)# L4 e6 x) @1 {+ s- H
*/
5 k" O5 U% ]7 Astruct bio {
1 T' }7 F% E" m. W7 R    sector_t        bi_sector;    /* 磁盘上相关扇区 */
7 k7 Y" s2 J) x0 D2 a$ W    struct bio        *bi_next;    /* 请求列表 */
! i. S8 T( V% C) _, m- f  v    struct block_device    *bi_bdev; /* 相关的块设备 */0 q" a1 \* ?/ ]+ x7 [' y4 X. `1 X
    unsigned long        bi_flags;    /* 状态和命令标志 */
4 p( y; s! v: D- w/ g0 D    unsigned long        bi_rw;        /* 读还是写 */
7 \9 L( J5 i+ W" X' @7 V! j* M  s' ^1 @8 d0 t" A
    unsigned short        bi_vcnt;    /* bio_vecs的数目 */
/ U* C# l  E0 T- U    unsigned short        bi_idx;        /* bio_io_vect的当前索引 */
4 V; s3 [* C# C& Z/ ^. O; ~& t/ q2 Q/ }3 `' B
    /* Number of segments in this BIO after
' n0 r& M2 x0 t7 L% I# y     * physical address coalescing is peRFormed.  p5 J( H  M& n
     * 结合后的片段数目
) B7 {2 c9 D4 r$ n9 A, i     */7 ~9 f' B5 I7 X8 x9 Z' S8 K
    unsigned int        bi_phys_segments;+ X# f, m5 Q. ?, |4 U

8 {) e4 T2 B4 R- }- b4 D    unsigned int        bi_size;    /* 剩余 I/O 计数 */* u. u# y, B: X

* `$ x7 L) O7 V3 R5 O  Z3 w2 ~    /*( Y0 x# e# t" Z( K! r& k" u7 o3 x
     * To keep track of the max segment size, we account for the& L7 ?( I3 C! b. ?' m, ?
     * sizes of the first and last mergeable segments in this bio.
1 |1 _0 s: i0 \3 C     * 第一个和最后一个可合并的段的大小6 C, C+ a( S7 {
     */5 u6 ^7 @1 \( K# ]0 q) @7 l
    unsigned int        bi_seg_front_size;' W9 x4 N& O1 i- ^' b/ M
    unsigned int        bi_seg_back_size;, u( v  u1 ~# ?) G# v
9 C. }* h; N, U
    unsigned int        bi_max_vecs;    /* bio_vecs数目上限 */
" ], |4 G2 [7 n  r- `    unsigned int        bi_comp_cpu;    /* 结束CPU */
0 M$ q' L7 r6 e+ e
: C0 C' t9 J& w1 s1 t    atomic_t        bi_cnt;        /* 使用计数 */
& H+ a( `. B" ?9 K. q: f    struct bio_vec        *bi_io_vec;    /* bio_vec 链表 */& o7 k  h1 A! i1 t- k$ c/ D" J
    bio_end_io_t        *bi_end_io; /* I/O 完成方法 */( k6 |9 d; D+ q0 p5 r' X
    void            *bi_private;    /* bio结构体创建者的私有方法 */
3 v) S$ \$ k$ e. K#if defined(CONFIG_BLK_DEV_INTEGRITY)
4 d! F; b3 M3 P; R3 I3 z4 V  D    struct bio_integrity_payload *bi_integrity;  /* data integrity */
6 O: z9 A2 M4 v. t% @, M6 }#endif- z0 t, L/ _- ?* m4 \
    bio_destructor_t    *bi_destructor;    /* bio撤销方法 */$ ]% v; i5 b+ t! v! P/ p7 H
    /*, G! y3 ~" t" F. I' c" |; i
     * We can inline a number of vecs at the end of the bio, to avoid/ [) }. O0 u! I3 [$ Q2 j+ h: w
     * double allocations for a small number of bio_vecs. This member, x5 a9 R, g; H$ V
     * MUST obviously be kept at the very end of the bio.0 T+ y+ q, q: Q7 D
     * 内嵌在结构体末尾的 bio 向量,主要为了防止出现二次申请少量的 bio_vecs' J, I0 X7 r  P
     */
- R  Q+ M: T( F! o    struct bio_vec        bi_inline_vecs[0];
8 r6 i5 m3 ?- H- o) p/ V};1 C% w7 @; i0 s6 ]
复制代码
4 ?0 E1 r; j  C7 s几个重要字段说明:
6 D5 i; {2 B3 r, F
7 {: J2 S  H/ m$ L  x9 R4 o( g2 S% obio 结构体表示正在执行的 I/O 操作相关的信息。+ @; O* {9 N2 k6 Z/ \
bio_io_vec 链表表示当前 I/O 操作涉及到的内存页
6 B) b* K! a$ A( B7 y1 C6 }bio_vec 结构体表示 I/O 操作使用的片段7 h/ O1 J, r: V# Z, T( Q/ x% X
bi_vcnt bi_io_vec链表中bi_vec的个数
, m) t$ B7 ~9 ?& e/ D; m: ?bi_idx 当前的 bi_vec片段,通过 bi_vcnt(总数)和 bi_idx(当前数),就可以跟踪当前 I/O 操作的进度& d( _& O2 u0 A+ V% J

4 x6 O# k: R8 j2 t& ]# u4 Y$ Q' z: Q# i
bio_vec 结构体很简单,定义如下:
( u5 \+ p4 O" d* M, X  ?4 Y9 `/ |6 U) t$ [9 `5 S9 l
struct bio_vec {
, z) J6 V* [* z) s9 Q    struct page    *bv_page;       /* 对应的物理页 */% R' J7 |9 ^- t( m
    unsigned int    bv_len;     /* 缓冲区大小 */. E1 |- [" V' ~
    unsigned int    bv_offset;  /* 缓冲区开始的位置 */
% Q& D8 r6 V( ?& h8 U};
' v9 C7 G7 d' m3 J6 s每个 bio_vec 都是对应一个页面,从而保证内核能够方便高效的完成 I/O 操作
4 O) E3 r, ~. F: f7 G1 T4 F( K  H+ o) \- b
6 C  W" |! i" y' m- Q  V
# v& I+ e  b; z8 g
2.3 2种方法的对比
9 z3 F" G# Q9 R缓冲区头和bio并不是相互矛盾的,bio只是缓冲区头的一种改善,将以前缓冲区头完成的一部分工作移到bio中来完成。
3 y' [# w& K0 k5 N2 Z
4 {+ {+ F* T* ~' hbio中对应的是内存中的一个个页,而缓冲区头对应的是磁盘中的一个块。
; K+ i  I* E8 S3 M& f( m! D% i3 ?  o& y0 Q* _$ M
对内核来说,配合使用bio和缓冲区头 比 只使用缓冲区头更加的方便高效。
2 a- ]' O4 n0 O0 h9 [
' d  F% \7 X+ c) Q- ~bio相当于在缓冲区上又封装了一层,使得内核在 I/O操作时只要针对一个或多个内存页即可,不用再去管理磁盘块的部分。
; i7 u, I& a& x' k8 a! l
6 O$ W- x0 q5 N) O# K: v 4 E8 e# Z) h2 V& A* n% k* @

, k8 _! D; v1 d1 Y6 k% c" J/ H# b使用bio结构体还有以下好处:
( j8 n( a0 @' i/ ^4 l- o7 @, W5 }2 k8 X5 x2 Y9 ~
bio结构体很容易处理高端内存,因为它处理的是内存页而不是直接指针
' [& `, d/ @( N1 @, Tbio结构体既可以代表普通页I/O,也可以代表直接I/O7 r" _- B4 z1 n3 W2 L- o  \
bio结构体便于执行分散-集中(矢量化的)块I/O操作,操作中的数据可以取自多个物理页面
. D0 t! D8 _) s; e4 j) o 0 F1 m, ~6 |" l1 P! \
% }1 z$ `* W; [8 m  q0 ^
3. 内核I/O调度程序
; ?1 M8 L1 u9 v# I/ t) i+ r1 h缓冲区头和bio都是内核处理一个具体I/O操作时涉及的概念。( y: j- {- Y  j2 W

; Q3 \9 j% f1 N5 ]- {2 x* }但是内核除了要完成I/O操作以外,还要调度好所有I/O操作请求,尽量确保每个请求能有个合理的响应时间。
9 m9 a  w4 _' ?- z9 D' l
3 T/ r6 ^: }' t7 J
$ H; k7 D; R2 i8 c- W, P$ y( g% v2 W* u8 H$ A  P% J3 h
下面就是目前内核中已有的一些 I/O 调度算法。. ?1 [9 _( a5 g9 I" O: j
6 `% ^* @1 H" [( A9 [8 {8 h$ }
3.1 linus电梯- J% w4 r+ T* Y: Z4 h2 Y& ^: u
为了保证磁盘寻址的效率,一般会尽量让磁头向一个方向移动,等到头了再反过来移动,这样可以缩短所有请求的磁盘寻址总时间。
8 t# g" d+ B4 O7 X: M0 Y9 o7 B2 E4 [2 Y3 t- `
磁头的移动有点类似于电梯,所有这个 I/O 调度算法也叫电梯调度。. T4 e$ ~4 v% \

4 g8 e& F) C3 wlinux中的第一个电梯调度算法就是 linus本人所写的,所有也叫做 linus 电梯。5 B1 j4 }, B0 D5 U+ b+ `
' R/ j: M2 l! X8 B# o# H' T

: C3 t# s) ?+ G* U5 U
$ E, f4 g: N: _0 N# ]0 ], a6 I  S" ~linus电梯调度主要是对I/O请求进行合并和排序。6 I, {/ j3 Y  \
/ E' {5 P8 U" N
当一个新请求加入I/O请求队列时,可能会发生以下4种操作:
- P. L- ]/ m: O- _. n$ H
$ t7 L, N+ I6 d# U& X4 }) P$ q如果队列中已存在一个对相邻磁盘扇区操作的请求,那么新请求将和这个已存在的请求合并成一个请求, S  i1 ]% z6 Z/ b1 F5 ^
如果队列中存在一个驻留时间过长的请求,那么新请求之间查到队列尾部,防止旧的请求发生饥饿
7 i: X& X" C. r( v9 s如果队列中已扇区方向为序存在合适的插入位置,那么新请求将被插入该位置,保证队列中的请求是以被访问磁盘物理位置为序进行排列的# Q- x* T1 Y: a3 p
如果队列中不存在合适的请求插入位置,请求将被插入到队列尾部$ c# U; [; f% {
- G) a$ ?  X6 K" ]$ a- z2 }) u

* f7 h; b% y9 G& u6 Ilinus电梯调度程序在2.6版的内核中被其他调度程序所取代了。
6 r! c5 o1 P1 m! c7 C; k! o
( R0 ]+ u# y; R' k2 W4 \: S( {5 P
$ V) ?% `& {' H2 ]# H( P) Y
; _: b( {- u, s+ S3.2 最终期限I/O调度
8 r* o/ P9 \( \linus电梯调度主要考虑了系统的全局吞吐量,对于个别的I/O请求,还是有可能造成饥饿现象。
2 n) V# v& A" D# ~1 Z5 [$ n& L
$ t) z9 G6 r5 c: t* e4 |+ P$ L. K而且读写请求的响应时间要求也是不一样的,一般来说,写请求的响应时间要求不高,写请求可以和提交它的应用程序异步执行,, ^. W' Z% A8 Y& S5 p
  g6 E1 z$ s4 \, q% Z- r( F) d+ K- O
但是读请求一般和提交它的应用程序时同步执行,应用程序等获取到读的数据后才会接着往下执行。7 a9 r# w; s' l
' d5 |- z, O' k9 k
因此在 linus 电梯调度程序中,还可能造成 写-饥饿-读(wirtes-starving-reads)这种特殊问题。
- J: {1 \! B" t; N' F  P7 m3 a  s  U% p

5 s/ D3 Y' p& d! ~/ g9 P/ u, ?* A( b- c6 B' C% e' R" E
为了尽量公平的对待所有请求,同时尽量保证读请求的响应时间,提出了最终期限I/O调度算法。
! p0 K0 c; M5 E8 D5 \$ H( `- v4 k1 w: c4 D6 ~
最终期限I/O调度 算法给每个请求设置了超时时间,默认情况下,读请求的超时时间500ms,写请求的超时时间是5s- O% J  \4 C2 Z+ s8 p

0 T! B0 z; `- P7 M' z但一个新请求加入到I/O请求队列时,最终期限I/O调度和linus电梯调度相比,多出了以下操作:  T0 ~7 H5 B5 r% T# E8 X4 W; E

/ W9 n& I, V8 l9 n" y6 G9 v新请求加入到 排序队列(order-FIFO),加入的方法类似 linus电梯新请求加入的方法
" v( S# t# o: ^8 U! p" {根据新请求的类型,将其加入 读队列(read-FIFO) 或者写队列(wirte-FIFO) 的尾部(读写队列是按加入时间排序的,所以新请求都是加到尾部)
1 D; s- Q: ~, g0 T) i调度程序首先判断 读,写队列头的请求是否超时,如果超时,从读,写队列头取出请求,加入到派发队列(dispatch-FIFO)
1 ~* U7 i6 f9 [9 V0 ~  g如果没有超时请求,从 排序队列(order-FIFO)头取出一个请求加入到 派发队列(dispatch-FIFO)7 U4 Y4 F( a! J9 w2 f$ x5 @6 J
派发队列(dispatch-FIFO)按顺序将请求提交到磁盘驱动,完成I/O操作/ @9 Y" }4 J+ F. O
- |! Y0 X- A# p( y* u; H

- _# i  i7 [  F% F8 X; y, z最终期限I/O调度 算法也不能严格保证响应时间,但是它可以保证不会发生请求在明显超时的情况下仍得不到执行。' ^* k( c4 E. x! o. {, V+ |

- P) p+ ^3 P# E/ K5 ^- S9 h最终期限I/O调度 的实现参见: block/deadline-iosched.c
! D: w+ ?' N6 A, N4 @, ~
- d: h% y# `& j3 C( k* e
8 Z0 [7 D/ ^- ?
: H  D! G& ?5 \. A3.3 预测I/O调度6 s$ b7 @) R2 u, g# u! w! a$ M
最终期限I/O调度算法优先考虑读请求的响应时间,但系统处于写操作繁重的状态时,会大大降低系统的吞吐量。
0 a) w: [# M6 Z9 Z) d2 f9 D% a3 t+ {: ]* q, Q
因为读请求的超时时间比较短,所以每次有读请求时,都会打断写请求,让磁盘寻址到读的位置,完成读操作后再回来继续写。" q( C  b! t+ B1 N  m9 y
* K! o5 W; z  C6 z
这种做法保证读请求的响应速度,却损害了系统的全局吞吐量(磁头先去读再回来写,发生了2次寻址操作)  C- C/ Q; f( J, K2 Q

0 |7 I$ O  C0 b- O+ _* l! _3 `
* Y4 d0 [4 n' d( s
1 ~. i& |4 F9 u" l6 _预测I/O调度算法是为了解决上述问题而提出的,它是基于最终期限I/O调度算法的。
2 Z$ W! }% b, l/ G2 b
( Q' e4 E( V6 q6 [1 `, L9 J" U" P) w  I但有一个新请求加入到I/O请求队列时,预测I/O调度与最终期限I/O调度相比,多了以下操作:
. S8 p6 Q$ r. F2 d+ h( }
+ x( }! K) X% F# |" V新的读请求提交后,并不立即进行请求处理,而是有意等待片刻(默认是6ms); G' V2 U  @6 n0 d" U- [
等待期间如果有其他对磁盘相邻位置进行读操作的读请求加入,会立刻处理这些读请求1 Q/ ~  A  s+ C. k
等待期间如果没有其他读请求加入,那么等待时间相当于浪费掉8 J+ K; ~. S9 b8 R& U
等待时间结束后,继续执行以前剩下的请求
" S  ]* T5 @" _ & r+ t9 S0 O. w6 H4 L

. ^- d7 C8 q% Y7 q9 w2 I9 @预测I/O调度算法中最重要的是保证等待期间不要浪费,也就是提高预测的准确性,
# x1 \/ c2 A$ ^: O
- ~; E9 p% D2 k' k2 C4 L目前这种预测是依靠一系列的启发和统计工作,预测I/O调度程序会跟踪并统计每个应用程序的I/O操作习惯,以便正确预测应用程序的读写行为。
+ r) ?5 Y7 L# C  I$ L6 {
# ?( `) I3 R* T+ |& p% c3 U
" u# B" e4 J, O: j. s8 c3 ~- K
4 A# }% D% u% ~0 D. ^, o如果预测的准确率足够高,那么预测I/O调度和最终期限I/O调度相比,既能提高读请求的响应时间,又能提高系统吞吐量。
0 {2 t/ Y, S- Z- {8 R4 h) U  L$ A1 f% n
预测I/O调度的实现参见: block/as-iosched.c- ?$ ?- j9 g' }( \% R

8 l* U, \2 o* x/ {) [2 y
2 ~& \) k) w. N  T9 `; L- G8 q8 T6 Z% g3 P7 B5 {6 r# G
注:预测I/O调度是linux内核中缺省的调度程序。
2 w4 N' {1 o2 Z, K( ?; B. U
  e# s3 Z" d6 h' F . [! k+ ~" B; }& f0 \( F+ d
- l7 C3 o* k) r3 ?
3.4 完全公正的排队I/O调度* {, H* l& Y# _
完全公正的排队(Complete Fair Queuing, CFQ)I/O调度 是为专有工作负荷设计的,它和之前提到的I/O调度有根本的不同。, t4 b" ^9 u. r: ?) l- l

; Z* _5 v4 r5 M: }7 u$ F/ o7 }+ cCFQ I/O调度 算法中,每个进程都有自己的I/O队列,
7 ~$ {9 v1 f7 h( w, \3 e; ]. H
( A1 V8 r4 Z4 m8 mCFQ I/O调度程序以时间片轮转调度队列,从每个队列中选取一定的请求数(默认4个),然后进行下一轮调度。
0 W/ S6 b% N; |0 U) j
/ q8 {$ V. ^, z5 ^. o0 X0 \
5 B& N5 S$ D3 Q6 g* V, e: V# t% {1 D/ T" {$ l' q
CFQ I/O调度在进程级提供了公平,它的实现位于: block/cfq-iosched.c! |# E% N7 ?) v) @2 A, C2 N1 I% E
- _' `2 V' o2 ?4 I- o' `0 {1 L! V
# A7 w- Q5 t) h0 i- H5 x; S' Z% |
# R4 L9 B- Y' ^: _( T
3.5 空操作的I/O调度, [3 I" R& \, I  H& i/ I
空操作(noop)I/O调度几乎不做什么事情,这也是它这样命名的原因。& F, C1 I$ d% j/ x: U
. Y% `; H& j' I* b  t9 g! B; z1 G
空操作I/O调度只做一件事情,当有新的请求到来时,把它与任一相邻的请求合并。
% `6 ]. l7 N" Y+ z2 D% r
+ V4 ^0 Q1 b! h* t
4 B" F  h+ {, Y5 ~3 P$ }$ r; }: w- h3 F/ v# j$ r
空操作I/O调度主要用于闪存卡之类的块设备,这类设备没有磁头,没有寻址的负担。
/ \/ O- ~+ }+ h& `9 i- A. L
  }4 b! P" l  P9 C% m4 [空操作I/O调度的实现位于: block/noop-iosched.c
. A3 P' T1 A; Q! j) ^% \# [" E2 E. T

. m2 H6 t# a. c& |: m( P% c
/ S  \( T8 c% l7 R3.6 I/O调度程序的选择5 x) B" `- w: p- [
2.6内核中内置了上面4种I/O调度,可以在启动时通过命令行选项 elevator=xxx 来启用任何一种。7 [- X, b8 @2 b  S& J

7 ^8 Y/ T+ u! m+ K4 q( u
elevator选项参数如下:
参数
I/O调度程序
as预测
cfq完全公正排队
deadline最终期限
noop空操作
如果启动预测I/O调度,启动的命令行参数中加上 elevator=as

该用户从未签到

2#
发表于 2021-1-27 10:44 | 只看该作者
Linux内核设计与实现之块I/O层
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

推荐内容上一条 /1 下一条

EDA365公众号

关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

GMT+8, 2025-11-24 20:37 , Processed in 0.203125 second(s), 26 queries , Gzip On.

深圳市墨知创新科技有限公司

地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

快速回复 返回顶部 返回列表