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

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

[复制链接]

该用户从未签到

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

EDA365欢迎您登录!

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

x

; ]7 l2 i9 e; B. F  h主要内容:7 g7 V, w5 W  A% |) m. j
" B, m/ e% i& P2 ~) L5 n
块设备简介
2 ~6 `6 M, z/ @; @1 w5 x7 ]) x内核访问块设备的方法' D+ Q4 y. ~0 z; P8 Y
内核I/O调度程序' w3 [7 ?* i# B9 S
  n; K8 `1 g; ~+ A# t1 D
# b  F4 M7 c4 m5 R$ ^
1. 块设备简介
- X& [7 P4 i! ^3 J1 N6 F& f7 eI/O设备主要有2类:4 q6 f$ e" y& g" W- P; ~

( {4 `' C9 `; s* `& x字符设备:只能顺序读写设备中的内容,比如 串口设备,键盘$ l9 T+ q6 h; Q
块设备:能够随机读写设备中的内容,比如 硬盘,U盘9 ^0 b4 Q6 y7 w2 f
字符设备由于只能顺序访问,所以应用场景也不多,这篇文章主要讨论块设备。
, B; Q, h2 p$ `5 ~2 F3 _) g' _6 o
5 p, \. e0 X% ^% c3 d块设备是随机访问的,所以块设备在不同的应用场景中存在很大的优化空间。  z: }" u0 o4 \4 e8 h& i: k$ p) [

+ I7 ?, Z" @& H 7 V8 R! `' v5 Y1 M9 `8 g9 U) h
2 K' X4 }7 v1 o6 f; Z% r
块设备中最重要的一个概念就是块设备的最小寻址单元。# t5 o- t! r8 r; ?9 H, k, M, L" Y
, Z$ r' L" i" |  X
块设备的最小寻址单元就是扇区,扇区的大小是2的整数倍,一般是 512字节。
  d" B* K, c# J" _) F! b9 d& H. j/ _- y, Z1 s! \
扇区是物理上的最小寻址单元,而逻辑上的最小寻址单元是块。# r9 K& V# v+ B9 c* c

% J& I8 g' ~& L3 y. r  a为了便于文件系统管理,块的大小一般是扇区的整数倍,并且小于等于页的大小。3 H9 E! z# I$ v2 h. X, a: M* I
# T0 }" @/ i& w3 d
+ F0 Z' u# i  H1 s

# y7 B  k! G& r3 s% w, J. s查看扇区和I/O块的方法:
7 p7 |" e4 S+ {4 {; j( v  z
9 O* n: _) i0 N* V' C; |复制代码
( i" t* V8 r' |[wangyubin@localhost]$ sudo fdisk -l( G7 X1 u1 P- `' o6 Z7 d
5 q: K# f3 f/ ]: l/ B) K, u' G
WARNING: GPT (GUID Partition Table) detected on '/dev/sda'! The util fdisk doesn't support GPT. Use GNU Parted.
1 f# A+ H/ v+ s- ^; X6 ]$ _# Q" }8 w" O' Y7 z1 P; Z+ w
0 D/ W, Z7 C9 t1 z
Disk /dev/sda: 500.1 GB, 500107862016 bytes, 976773168 sectors
/ @) {, z+ y# n9 Q; v  D0 B9 mUnits = sectors of 1 * 512 = 512 bytes/ G+ ]' O0 u% K
Sector size (logical/physical): 512 bytes / 4096 bytes
1 m& S! p$ @( t. u- U, H) I; B8 II/O size (minimum/optimal): 4096 bytes / 4096 bytes' t7 r+ D- B0 p
Disk identifier: 0x00000000" O2 V7 F7 P. E7 I7 \4 s! Q
复制代码  l1 N& A( D4 J% R9 F0 t
上面的 Sector size 就是扇区的值,I/O size就是 块的值' V5 B8 {9 X' W: Q
) Q7 A% H# K6 d) w# i
从上面显示的结果,我们发现有个奇怪的地方,扇区的大小有2个值,逻辑大小是 512字节,而物理大小却是 4096字节。$ f  T6 H* f1 A6 @& z
9 q4 Q: b  X+ P- D3 P8 j% @
其实逻辑大小 512字节是为了兼容以前的软件应用,而实际物理大小 4096字节是由于硬盘空间越来越大导致的。
2 H* D* `  L% s
- Y/ ]. b4 r7 X具体的来龙去脉请参考:4KB扇区的原因
" L% `1 V1 _& |3 o2 I. Q; x3 Q2 m- a1 Y' t  O0 c- q
0 p& N" l6 e/ e( u- W! b0 ~% u
0 ?& e7 H% D  W1 z. y9 G+ \
2. 内核访问块设备的方法3 e; B- S4 q: r! N
内核通过文件系统访问块设备时,需要先把块读入到内存中。所以文件系统为了管理块设备,必须管理[块]和内存页之间的映射。
8 x4 H7 y' [$ w" j1 I" |  @$ R. }: I* J7 c6 f3 b2 x
内核中有2种方法来管理 [块] 和内存页之间的映射。$ a5 ?, l* S. G  B5 t
% z8 p3 T/ C. [5 l3 e
缓冲区和缓冲区头
0 F: f& M# d0 fbio
+ k2 Q% Y( g; ^' y% W5 ]6 u( H" l ( t- d  d  a* G( P( P
! B1 P" G. n7 t( |& T+ M
2.1 缓冲区和缓冲区头
, ^6 g: J. f9 i# R3 s3 P每个 [块] 都是一个缓冲区,同时对每个 [块] 都定义一个缓冲区头来描述它。
9 X7 W) e' h2 Y% S6 W# T. ?; A" t
, t9 K$ i7 x7 {5 k2 i由于 [块] 的大小是小于内存页的大小的,所以每个内存页会包含一个或者多个 [块]; F" F0 m% T* |. s# i, \
1 }0 u' ?" c- t" |4 `& S7 i7 t
, m+ a4 Y0 x9 ^. W
1 L; f' y' V0 z4 r! I
缓冲区头定义在 <linux/buffer_head.h>: include/linux/buffer_head.h/ |4 m$ G8 n* E9 K( {8 D# G6 c
0 e. A8 \# O" _, l: C0 H1 {
复制代码# H# p3 z0 K7 ~. ^7 X
struct buffer_head {
( V7 e! c. d" E. H+ _' i. g    unsigned long b_state;            /* 表示缓冲区状态 */
3 H/ x8 F- b6 a    struct buffer_head *b_this_page;/* 当前页中缓冲区 */
. H+ I2 J' }/ H0 U9 [    struct page *b_page;            /* 当前缓冲区所在内存页 */
, z! ]( J# e9 Z* m3 I! w+ C* J7 X  @  u4 U( n- k' y: P
    sector_t b_blocknr;        /* 起始块号 *// d$ K) A3 a2 M% s& G; f
    size_t b_size;            /* buffer在内存中的大小 */
( }. h4 L; Q- G% a( E    char *b_data;            /* 块映射在内存页中的数据 */+ \7 r: D# C! |. |2 t; x3 O
% f! o& r* M0 d4 \$ Q$ I! S3 [
    struct block_device *b_bdev; /* 关联的块设备 */, W( e9 \8 L; F
    bh_end_io_t *b_end_io;        /* I/O完成方法 */
& `& l2 G7 U/ a6 w     void *b_private;             /* 保留的 I/O 完成方法 */
  m- o( p) }! D# w* a! k" t    struct list_head b_assoc_buffers;   /* 关联的其他缓冲区 */
5 T0 E6 E8 Y) U: W' W    struct address_space *b_assoc_map;    /* 相关的地址空间 */8 {( y4 v, W- R" V5 }* k0 M
    atomic_t b_count;                    /* 引用计数 */; v8 K5 \2 U( @7 k; `" Q+ e6 L/ Q! d
};6 x* x  j* S- A7 y
复制代码
% A4 ~1 P* ]3 p; f4 s
! _" z; t7 p3 y7 e# i
/ S* ?" z' h8 Q% Z' q) n: L整个 buffer_head 结构体中的字段是减少过的,以前的内核中字段更多。
* a! S' S4 Y/ E' z+ k" A
3 x' ?7 Y# C4 [' i, y各个字段的含义通过注释都很明了,只有 b_state 字段比较复杂,它涵盖了缓冲区可能的各种状态。
9 d, V' T% }2 q8 V# J0 z- D, I  a8 H9 Z- _7 ^! ]
复制代码
2 v* a. u' q* p( d! venum bh_state_bits {4 K3 O  n& J1 j
    BH_Uptodate,    /* 包含可用数据 */
! H! P2 [0 u& k6 V5 ^7 S    BH_Dirty,    /* 该缓冲区是脏的(说明缓冲的内容比磁盘中的内容新,需要回写磁盘) */
, m/ N8 x. M: e) w5 t8 A    BH_Lock,    /* 该缓冲区正在被I/O使用,锁住以防止并发访问 */
5 Q$ A7 O2 _. i$ d9 t    BH_Req,        /* 该缓冲区有I/O请求操作 */
  j8 ?5 _( s& [5 `( ~5 A( \& e    BH_Uptodate_Lock,/* 由内存页中的第一个缓冲区使用,使得该页中的其他缓冲区 */+ I. j& R8 c9 G- f5 E
% j0 P# L; _3 H9 g
    BH_Mapped,    /* 该缓冲区是映射到磁盘块的可用缓冲区 */: B! K2 X- _6 ~" y4 k( R
    BH_New,        /* 缓冲区是通过 get_block() 刚刚映射的,尚且不能访问 */
, k# o8 _" W2 p5 b    BH_Async_Read,    /* 该缓冲区正通过 end_buffer_async_read() 被异步I/O读操作使用 */! G1 G8 {( ^, w$ k3 X2 s* J( p
    BH_Async_Write,    /* 该缓冲区正通过 end_buffer_async_read() 被异步I/O写操作使用 */1 M" G" Y! t. X3 o( p6 O
    BH_Delay,    /* 缓冲区还未和磁盘关联 */
0 I8 g) w7 E- R/ [# P. c    BH_Boundary,    /* 该缓冲区处于连续块区的边界,下一个块不在连续 */% Y% ~9 Z7 f7 c& `7 g# `
    BH_Write_EIO,    /* 该缓冲区在写的时候遇到 I/O 错误 */
+ W0 E, x' A, f8 U    BH_Ordered,    /* 顺序写 */
( ]& ^  o9 {' a+ O" c: ^# s7 u    BH_Eopnotsupp,    /* 该缓冲区发生 “不被支持” 错误 */# ?" n; j  R; @2 r# Q
    BH_Unwritten,    /* 该缓冲区在磁盘上的位置已经被申请,但还有实际写入数据 */
5 I2 \6 A( L$ Z    BH_Quiet,    /* 该缓冲区禁止错误 */! O1 p, f9 P) `% e

& T1 N0 Z( D, t2 z* D3 F    BH_PrivateStart,/* 不是表示状态,分配给其他实体的私有数据区的第一个bit */& A4 f* c0 t  J
};
3 g& O" w% k% c& x& G复制代码
) Q( U1 r7 @. N; k1 W# S5 p
; ?! Z# Y/ h7 |* d3 n' c$ I! K' [& h, Q- o
在2.6之前的内核中,主要就是通过缓冲区头来管理 [块] 和内存之间的映射的。
8 d, E0 X1 P% l0 n1 r, H) W& Q+ H8 ]0 q- F
用缓冲区头来管理内核的 I/O 操作主要存在以下2个弊端,所以在2.6开始的内核中,缓冲区头的作用大大降低了。2 N8 g" @- R% e# R, N* i6 A

4 L) F1 x: W: |* G- 弊端 1) R( B$ N9 }0 G
; g( Y* J& H3 p1 O
对内核而言,操作内存页是最为简便和高效的,所以如果通过缓冲区头来操作的话(缓冲区 即[块]在内存中映射,可能比页面要小),效率低下。% L9 v# }1 y9 u2 b5 `2 v. h% I
6 i  X, C3 I: X
而且每个 [块] 对应一个缓冲区头的话,导致内存的利用率降低(缓冲区头包含的字段非常多)
2 k; V$ Q" P# U% C, Z  h
* s1 U& y* [5 J! F- 弊端 2
6 h8 e) ^0 C" c- j
8 u7 |! B. A8 Q# w# T: u; z# h: ]每个缓冲区头只能表示一个 [块],所以内核在处理大数据时,会分解为对一个个小的 [块] 的操作,造成不必要的负担和空间浪费。
) j, i! B" K& a6 ?  ^/ a* T7 J4 N2 \# Z6 B

) ?4 B# j' i5 R. }* _) r4 k) o: j) s; M2 x
2.2 bio9 E" o( _* a$ h; ~  X& i  q
bio结构体的出现就是为了改善上面缓冲区头的2个弊端,它表示了一次 I/O 操作所涉及到的所有内存页。" F* [0 k$ u7 s; J

$ @. S6 l2 s* S$ f& Z8 |复制代码; r! p" T+ ]& y/ Z+ t5 A
/*
1 v' R1 r2 u6 G! _ * I/O 操作的主要单元,针对 I/O块和更低级的层 (ie drivers and
* C' l/ q5 \4 m+ \0 Y * stacking drivers)" f+ S  A! T, d$ ~; w9 |% v
*/0 V: B$ ~- N# W' R
struct bio {/ r, |2 p* z; y1 @# J; \
    sector_t        bi_sector;    /* 磁盘上相关扇区 */6 {; v$ X) h. m) j! T( g) J9 q
    struct bio        *bi_next;    /* 请求列表 */
$ e5 `% U1 f7 T6 r1 Q; H    struct block_device    *bi_bdev; /* 相关的块设备 */: a9 N& e2 l4 Q7 E. Z' ~2 d# Z& g
    unsigned long        bi_flags;    /* 状态和命令标志 */
# q- E4 a. u  z+ l: N    unsigned long        bi_rw;        /* 读还是写 */
, F% l& d, o; O) C, Y/ s5 |7 [% b; y2 J4 D; p
    unsigned short        bi_vcnt;    /* bio_vecs的数目 */1 s3 k7 U0 G, C! J' f0 K* s
    unsigned short        bi_idx;        /* bio_io_vect的当前索引 */$ C2 t* B) W# u& I$ S( N% }
, I- W7 W! I# K7 |: }2 @5 b* e
    /* Number of segments in this BIO after
9 D: H$ a7 S* K; |  U     * physical address coalescing is peRFormed./ o6 d( j7 Q2 G+ ?- p4 u4 T
     * 结合后的片段数目9 `; `/ z0 m7 e% j: \; x
     */- p6 \8 E, M6 N% y2 s% M
    unsigned int        bi_phys_segments;
$ R' a8 \! U% K) h) v/ Y# U7 p7 L: }  X9 @# T; k$ s. e* z
    unsigned int        bi_size;    /* 剩余 I/O 计数 */
/ ]( M, G( D4 s. F& }+ `' I  T5 }8 T+ |& ^& j+ ?5 c; w
    /*. ~0 H- A2 \* [5 ?0 A, }
     * To keep track of the max segment size, we account for the
8 J: G; x+ q1 t" {9 b  r' D, b     * sizes of the first and last mergeable segments in this bio.8 r1 r* |5 U' L9 N9 E( y* B
     * 第一个和最后一个可合并的段的大小1 p; }- ~) S, \+ O
     */
9 o+ J" H# a9 Z+ g* S# z    unsigned int        bi_seg_front_size;
. o+ b: x0 X" `    unsigned int        bi_seg_back_size;
! Q* K8 T6 T( S  U
$ A) Y$ x! R0 q7 a/ ~    unsigned int        bi_max_vecs;    /* bio_vecs数目上限 */
! h' j1 W; ]1 o$ ~6 y" P6 w0 v4 ?    unsigned int        bi_comp_cpu;    /* 结束CPU *// G! s- B( S3 a: Q0 {4 ?
9 \, J& J0 A; H0 W3 b" ]( j) u" l& y
    atomic_t        bi_cnt;        /* 使用计数 */0 M( Q* ]+ e5 X; h* b
    struct bio_vec        *bi_io_vec;    /* bio_vec 链表 */" y$ g" z( @- s  X7 x2 ~
    bio_end_io_t        *bi_end_io; /* I/O 完成方法 */
! u9 ?$ s+ p( s    void            *bi_private;    /* bio结构体创建者的私有方法 */
; w, J- Q2 |' r9 t#if defined(CONFIG_BLK_DEV_INTEGRITY)8 j" F. ^6 U+ C* {, v( ^  L! L3 B
    struct bio_integrity_payload *bi_integrity;  /* data integrity */  Y% @0 ?. F, F0 y
#endif
, n; ]5 |0 e, t    bio_destructor_t    *bi_destructor;    /* bio撤销方法 */
5 [1 ~8 x2 d0 n" o8 g4 @    /*/ m/ o: b( J9 y0 R0 @
     * We can inline a number of vecs at the end of the bio, to avoid8 d9 k# K6 T% i: W
     * double allocations for a small number of bio_vecs. This member
  i9 D, E+ N" \3 q     * MUST obviously be kept at the very end of the bio.3 e4 A$ W- w) Y# B9 s  \2 B7 i
     * 内嵌在结构体末尾的 bio 向量,主要为了防止出现二次申请少量的 bio_vecs1 U; B$ u0 Z2 n+ K+ P* ~; q. g
     */
3 d" q- r% z/ |# h    struct bio_vec        bi_inline_vecs[0];
/ N/ ?7 k9 P; T( Z6 O};# g+ b3 Y! I6 P% s# m" O
复制代码+ b7 @# _. T, w: e5 k! U* ]& C
几个重要字段说明:' w: _% Y" y. y: f/ n$ f  n- ]
4 [1 z  L4 A" }  E1 j& J. x
bio 结构体表示正在执行的 I/O 操作相关的信息。
0 p, F# d1 W+ u! L+ W8 B) h7 bbio_io_vec 链表表示当前 I/O 操作涉及到的内存页0 x# |4 E6 |7 Z" ]/ p$ c+ ~
bio_vec 结构体表示 I/O 操作使用的片段- {1 F  ?) w9 _
bi_vcnt bi_io_vec链表中bi_vec的个数
" N) J) O+ q$ t& }8 V7 Ibi_idx 当前的 bi_vec片段,通过 bi_vcnt(总数)和 bi_idx(当前数),就可以跟踪当前 I/O 操作的进度( ]3 v5 |% l& z! G. z5 Y

0 ]8 y3 v! z8 j/ q' k0 V+ o
/ h3 h6 K' V% t& K( b% L/ A9 ebio_vec 结构体很简单,定义如下:6 O3 [# L; {& }& a
- Z8 y1 b1 t% G  ~& e
struct bio_vec {
1 {* K5 H" q0 `$ u& W. T, j    struct page    *bv_page;       /* 对应的物理页 */
$ H4 d  s: Q9 Z# J    unsigned int    bv_len;     /* 缓冲区大小 */- L" _4 A3 u# w- _2 t
    unsigned int    bv_offset;  /* 缓冲区开始的位置 */0 r. o: @1 V; L& M  Q) ?1 T* o
};
5 S( C  U" y, U" m7 z每个 bio_vec 都是对应一个页面,从而保证内核能够方便高效的完成 I/O 操作; ]9 N6 ^5 y' G! E% l4 z- Q

! [% `9 i& O7 T; r$ b ) T0 l! R  H: w5 }- @

+ o- [# s6 J6 w" j6 M0 L2.3 2种方法的对比: C- z3 \. Q! v( s9 p
缓冲区头和bio并不是相互矛盾的,bio只是缓冲区头的一种改善,将以前缓冲区头完成的一部分工作移到bio中来完成。
' y" [, ^8 r4 Y+ J7 p5 P& a; d" U0 a: E) s- u
bio中对应的是内存中的一个个页,而缓冲区头对应的是磁盘中的一个块。8 A' q' \3 t7 r3 d3 O& R( {

3 _; m& R4 X4 b* P' j6 Q5 B对内核来说,配合使用bio和缓冲区头 比 只使用缓冲区头更加的方便高效。% }/ @! q' `* n- x

  n* `. i3 q( K' P# i1 @bio相当于在缓冲区上又封装了一层,使得内核在 I/O操作时只要针对一个或多个内存页即可,不用再去管理磁盘块的部分。
, a* u* D. ~6 n. K( s* I: k4 n* H# V: `1 l4 \( @
4 [4 D. l+ b5 }9 l) A8 p1 ~

, E4 @( k, p; j4 I9 X! ?+ A" _; o使用bio结构体还有以下好处:' o0 N1 F; V6 F, [/ h6 n
. F; @3 p5 Q6 i2 r; n8 t
bio结构体很容易处理高端内存,因为它处理的是内存页而不是直接指针3 U- t* v$ Q5 F! V, T
bio结构体既可以代表普通页I/O,也可以代表直接I/O2 E4 {2 x) [* q
bio结构体便于执行分散-集中(矢量化的)块I/O操作,操作中的数据可以取自多个物理页面4 w) r2 J7 z' `# i( P6 o! M

6 J6 W# N2 z1 X( D; m  \7 T# }/ _. z; Q2 f$ \% ?# m
3. 内核I/O调度程序
5 {, d- R- t7 V7 p% r/ D2 A缓冲区头和bio都是内核处理一个具体I/O操作时涉及的概念。
' w0 c8 N. a: f
0 ]8 M2 K* ~* m5 c" d: j+ S但是内核除了要完成I/O操作以外,还要调度好所有I/O操作请求,尽量确保每个请求能有个合理的响应时间。
. Q- i7 A: O" Z$ B, D
( C) Y2 _+ T! g: l
1 A, H2 T- w: P3 h  x5 B# X: w6 Z2 R) T' \4 Y8 w3 l
下面就是目前内核中已有的一些 I/O 调度算法。
* U" c  i0 w  B* w/ ~
7 O& J, G2 @9 m6 Z! C3.1 linus电梯4 F0 W) \' S7 `' u0 l0 v* x
为了保证磁盘寻址的效率,一般会尽量让磁头向一个方向移动,等到头了再反过来移动,这样可以缩短所有请求的磁盘寻址总时间。
  p. z  d; d' d$ F- A  k: U  \! v  {- R0 I: r& W/ R
磁头的移动有点类似于电梯,所有这个 I/O 调度算法也叫电梯调度。
( o3 Z, P- |' c
! y* {8 J) P* G: `2 Llinux中的第一个电梯调度算法就是 linus本人所写的,所有也叫做 linus 电梯。1 b/ j0 d! \9 ]5 ]3 f

  t, Y& ~& [* B3 X! m+ [ ! P9 c1 s, b% i& A4 K& s7 }& ]) _# u
' b6 k+ ?- g8 c; q) u1 c! E1 W
linus电梯调度主要是对I/O请求进行合并和排序。
+ |1 d& \1 T# q% @. s
) w: O4 o0 ?6 [1 p8 @1 t当一个新请求加入I/O请求队列时,可能会发生以下4种操作:- D+ T2 o  h" g  X

; ?3 Z) s+ c4 J7 G+ J如果队列中已存在一个对相邻磁盘扇区操作的请求,那么新请求将和这个已存在的请求合并成一个请求
" ]" P% @# M. }$ `! b如果队列中存在一个驻留时间过长的请求,那么新请求之间查到队列尾部,防止旧的请求发生饥饿" K! U0 i+ Z& K: q% F8 u
如果队列中已扇区方向为序存在合适的插入位置,那么新请求将被插入该位置,保证队列中的请求是以被访问磁盘物理位置为序进行排列的5 I; f7 {- {  C3 b. \; P
如果队列中不存在合适的请求插入位置,请求将被插入到队列尾部; H6 L$ x  P" W0 p
: J: \$ V3 A& D9 T6 g
& t0 D4 f7 f. y; U( B
linus电梯调度程序在2.6版的内核中被其他调度程序所取代了。0 a: c- j5 E1 l0 d* L
$ v7 k  j+ H8 M, X. {3 \
0 ^5 W6 T* m* Z; T  v( y% t
% W6 V8 p; a: U5 L- H  ^6 W& j
3.2 最终期限I/O调度! n- e! P: t* ^8 I2 t8 b
linus电梯调度主要考虑了系统的全局吞吐量,对于个别的I/O请求,还是有可能造成饥饿现象。, K+ W) P# W7 M! r! s# H
* x2 Q' A( ^6 [8 n  @8 k3 Z
而且读写请求的响应时间要求也是不一样的,一般来说,写请求的响应时间要求不高,写请求可以和提交它的应用程序异步执行,
( B  W( i8 }! x% S; w1 f: `/ I& ?4 s  K# \! {# O
但是读请求一般和提交它的应用程序时同步执行,应用程序等获取到读的数据后才会接着往下执行。/ D- S  P( c# E* N; O# e

4 H# y" M9 [+ \& L1 \) A& a7 x因此在 linus 电梯调度程序中,还可能造成 写-饥饿-读(wirtes-starving-reads)这种特殊问题。
+ |' }# j! \3 c' A* W! x$ M0 y: G" X: D9 n+ O
' z9 U+ w, s, Y. v( s0 v

) N$ x: B; p8 s4 n) L" O为了尽量公平的对待所有请求,同时尽量保证读请求的响应时间,提出了最终期限I/O调度算法。
. n/ x3 A" b7 R) G  \' f/ C: h+ `
$ }) a/ m- S- {" S4 J/ ~  @最终期限I/O调度 算法给每个请求设置了超时时间,默认情况下,读请求的超时时间500ms,写请求的超时时间是5s
) Y) K- `! s8 [4 J3 X4 ]
& X) W. |' d; T. W- u$ d2 S但一个新请求加入到I/O请求队列时,最终期限I/O调度和linus电梯调度相比,多出了以下操作:
& I! D  P- i3 g
; Q! n4 m) E. w( c新请求加入到 排序队列(order-FIFO),加入的方法类似 linus电梯新请求加入的方法
2 i2 p0 f" z: K' @根据新请求的类型,将其加入 读队列(read-FIFO) 或者写队列(wirte-FIFO) 的尾部(读写队列是按加入时间排序的,所以新请求都是加到尾部)% G7 k( k1 W7 P8 M$ p% j  n* E
调度程序首先判断 读,写队列头的请求是否超时,如果超时,从读,写队列头取出请求,加入到派发队列(dispatch-FIFO)
1 O! ^- n* ^2 g1 O如果没有超时请求,从 排序队列(order-FIFO)头取出一个请求加入到 派发队列(dispatch-FIFO)
  `$ c9 r) B1 b' A# F2 {% |派发队列(dispatch-FIFO)按顺序将请求提交到磁盘驱动,完成I/O操作
" {: b5 e8 W4 ^# s6 f0 ^
. j' k1 Z" r% M5 F
  X) w) Y' l! a7 d最终期限I/O调度 算法也不能严格保证响应时间,但是它可以保证不会发生请求在明显超时的情况下仍得不到执行。
/ S- D7 |* p+ z, e
2 D- v4 a( P, U最终期限I/O调度 的实现参见: block/deadline-iosched.c
# f- c! D; J& d( V+ z' C& C; V
4 t; ^. E/ h* ?- | ) M! Z! A# w! |8 j: v$ A3 Y
; Z; Q( [7 W5 \# S  M
3.3 预测I/O调度
$ c- k+ c1 E6 L& \4 `& d最终期限I/O调度算法优先考虑读请求的响应时间,但系统处于写操作繁重的状态时,会大大降低系统的吞吐量。; ^' A) V" f2 i7 \, N2 b
3 h- F  \' d' j( Q/ z
因为读请求的超时时间比较短,所以每次有读请求时,都会打断写请求,让磁盘寻址到读的位置,完成读操作后再回来继续写。- S% R/ p! C0 Q3 B& V
3 i, c( I: t' H5 y
这种做法保证读请求的响应速度,却损害了系统的全局吞吐量(磁头先去读再回来写,发生了2次寻址操作)
3 y+ e) o! z# T
. {7 R. p& ~& z! J# m 5 j, X& O  s' R5 C
5 X# g5 z. c& U
预测I/O调度算法是为了解决上述问题而提出的,它是基于最终期限I/O调度算法的。2 l5 V* s" ?; k9 Q7 B5 x3 }

3 N6 |/ |7 Z/ W) p3 I. M1 b但有一个新请求加入到I/O请求队列时,预测I/O调度与最终期限I/O调度相比,多了以下操作:
( t+ i# j. y) G+ c+ Y7 j! _$ T( f+ X+ s; i  C
新的读请求提交后,并不立即进行请求处理,而是有意等待片刻(默认是6ms): Q( f# r" \3 {3 T# @( A
等待期间如果有其他对磁盘相邻位置进行读操作的读请求加入,会立刻处理这些读请求- F0 d: e( `7 r* u. {) g2 F
等待期间如果没有其他读请求加入,那么等待时间相当于浪费掉
/ U/ W# Y+ h8 ^  s6 V6 ], R等待时间结束后,继续执行以前剩下的请求3 r, W5 b) x( H2 t  K
4 _$ F( v4 y# A" h
& C2 P$ Q" T* R& M: E# c2 I
预测I/O调度算法中最重要的是保证等待期间不要浪费,也就是提高预测的准确性," X5 c7 I9 W+ M7 A- U2 W$ r
' ]$ w( W# R, Y9 H2 V6 K3 N( O
目前这种预测是依靠一系列的启发和统计工作,预测I/O调度程序会跟踪并统计每个应用程序的I/O操作习惯,以便正确预测应用程序的读写行为。
0 C; f# m" W2 j$ f, i) y' c- m8 S" c2 T1 ?- ]0 s

: [0 R& Q1 o" f% C  U
/ {, |* e' Z# a  j. y如果预测的准确率足够高,那么预测I/O调度和最终期限I/O调度相比,既能提高读请求的响应时间,又能提高系统吞吐量。% z2 b. N4 W% ]& D
; A* Q+ a! `3 g9 K
预测I/O调度的实现参见: block/as-iosched.c
+ \) h5 d# \; q; W: V0 K, ?
: [5 q5 ]$ W3 n3 m
! B0 \; i4 m, J  j' h, H+ e4 g
注:预测I/O调度是linux内核中缺省的调度程序。3 h% ?5 d8 L: E
1 S" R& n5 Q$ i# L6 R1 `

2 Y, u8 m. s# j% w- f+ @7 G8 \# I# g# s4 T! D  m) C( V! y- L7 u' K$ `
3.4 完全公正的排队I/O调度
- [# O3 |( V) r+ l: |& U9 M完全公正的排队(Complete Fair Queuing, CFQ)I/O调度 是为专有工作负荷设计的,它和之前提到的I/O调度有根本的不同。/ h4 h$ m' Q) u6 L# o

$ T5 i' G3 n% B2 Z" c* UCFQ I/O调度 算法中,每个进程都有自己的I/O队列,
$ X- K  d% S& ^6 Y% q  \( Z' b3 o! Y9 R* N) B3 D
CFQ I/O调度程序以时间片轮转调度队列,从每个队列中选取一定的请求数(默认4个),然后进行下一轮调度。. J5 z% s7 j5 h: V5 h) p
7 I7 _" X* |' D* G$ U9 O
+ O$ h+ E$ x6 `4 d
$ ]( |5 U8 j3 {0 I6 N& D6 _& Q
CFQ I/O调度在进程级提供了公平,它的实现位于: block/cfq-iosched.c
! d4 |% |9 A- I! T' k, R6 v/ d8 p9 Y3 t
+ ^$ w% j5 h: ~, r8 K, m
! q" S; S* T# b7 L% M1 v+ A+ m  F
3.5 空操作的I/O调度& S" C* c. g; K& C1 {& ]3 B% a
空操作(noop)I/O调度几乎不做什么事情,这也是它这样命名的原因。" C/ @% n" \7 m2 J
1 k& a4 m/ c6 z  Y. N
空操作I/O调度只做一件事情,当有新的请求到来时,把它与任一相邻的请求合并。9 r! p* Q4 w. L, k9 s* @
! O* H* P- P( N# ~$ ~$ d
% @7 |1 ~7 t, H( \9 x
9 j! l2 V5 @1 k+ I
空操作I/O调度主要用于闪存卡之类的块设备,这类设备没有磁头,没有寻址的负担。7 I2 K# l0 g4 t; H' ]
6 m) L4 \' Y3 ~' s( M& ~. e
空操作I/O调度的实现位于: block/noop-iosched.c% A# Y  L6 |+ o9 u

. {" C4 N. k3 p( {$ C' j) ^   ~. `" g# g1 s- |* S3 s/ ^% D) B& Z
) ^" o* g, \2 \
3.6 I/O调度程序的选择
4 A. G5 s1 @* X5 f2.6内核中内置了上面4种I/O调度,可以在启动时通过命令行选项 elevator=xxx 来启用任何一种。7 u" y5 k' h/ g, a3 l* J- q5 N
) m5 z5 N* |( V+ e
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:17 , Processed in 0.203125 second(s), 27 queries , Gzip On.

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

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

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