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

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

[复制链接]

该用户从未签到

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

EDA365欢迎您登录!

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

x
, l  p; @. o2 n# E4 S
主要内容:/ F% N7 V9 u( K! _
) ~/ N# F/ y% W4 M+ C6 J# H# I: G! H
块设备简介
/ E7 ^7 m$ _( n" k6 Y内核访问块设备的方法/ e1 r. s% {% X! K3 M9 O4 g- Z
内核I/O调度程序
4 s& Q+ A% A/ A% u7 ~+ s) o
+ z! e  k( p; Z: s. e$ m
  a% J  a: D4 X* k1. 块设备简介& |/ L* `! z- c4 u  x' `
I/O设备主要有2类:
6 Y% U9 l8 b  \8 I: ?$ I3 W) e+ n% N' @
字符设备:只能顺序读写设备中的内容,比如 串口设备,键盘/ o4 s& {8 h- T) v% y% f& B9 O
块设备:能够随机读写设备中的内容,比如 硬盘,U盘
! N( X5 H- C) q! K! C: E2 _2 o# ]字符设备由于只能顺序访问,所以应用场景也不多,这篇文章主要讨论块设备。
  n0 _" g- r' y* p( p, p0 v  K$ c3 [  {" _# L. d
块设备是随机访问的,所以块设备在不同的应用场景中存在很大的优化空间。
6 \. {" G# Z' H/ O: A, {5 `. G' t" m, t% ~( v; j5 @
/ F: X* l" O+ J  f  A
. V5 o  t# q- M& G: B" Y- H
块设备中最重要的一个概念就是块设备的最小寻址单元。
* T5 A5 W, D8 V( A5 ?
/ q6 R9 K3 W- U# I块设备的最小寻址单元就是扇区,扇区的大小是2的整数倍,一般是 512字节。
# b% u  z" q( J5 z$ X7 z8 ]8 j# G2 ~4 L- {1 [+ Q0 Y
扇区是物理上的最小寻址单元,而逻辑上的最小寻址单元是块。
$ i/ E" A. G  y
& k% K, b) \; x+ ^5 O8 j# g为了便于文件系统管理,块的大小一般是扇区的整数倍,并且小于等于页的大小。, @; x( S# H# C5 S9 a" {

/ j1 j  P3 G6 Y1 r: b
; H( F# w6 A$ Z. W7 F4 D9 D# i% C% F( c" g) @
查看扇区和I/O块的方法:* Z- ^/ @) O4 }8 ?* P
$ Q' s& `' |$ [" `
复制代码
- D$ @6 w  T( e[wangyubin@localhost]$ sudo fdisk -l( d/ x7 f6 K1 z9 H4 t
2 H& A9 m, u% X
WARNING: GPT (GUID Partition Table) detected on '/dev/sda'! The util fdisk doesn't support GPT. Use GNU Parted.$ R& _. V' \( n: }/ x6 b- h

2 y& W- \- [1 H, o
. t' p6 ]- t+ A) ^* uDisk /dev/sda: 500.1 GB, 500107862016 bytes, 976773168 sectors* |* D5 D( o; k- u" b2 r/ ]
Units = sectors of 1 * 512 = 512 bytes
- }2 M5 i2 `/ d* aSector size (logical/physical): 512 bytes / 4096 bytes6 r' V- y0 C' U9 V* k
I/O size (minimum/optimal): 4096 bytes / 4096 bytes2 p! W' }4 G4 l5 z" k+ E
Disk identifier: 0x00000000
; b& i. \1 Z* f: {3 t9 ^复制代码
5 N1 |# O4 s; }: @% p上面的 Sector size 就是扇区的值,I/O size就是 块的值5 _3 o9 r* Y$ g8 @! l4 B' B6 h0 x  x

$ B, b# `" p+ q" |从上面显示的结果,我们发现有个奇怪的地方,扇区的大小有2个值,逻辑大小是 512字节,而物理大小却是 4096字节。
" A9 B0 G0 M4 E$ J
7 _$ j5 d9 g9 y& }$ k6 {- V其实逻辑大小 512字节是为了兼容以前的软件应用,而实际物理大小 4096字节是由于硬盘空间越来越大导致的。8 I' M  X  b# U: z# ]; d
+ K, c- q. W* S7 D0 v! D
具体的来龙去脉请参考:4KB扇区的原因
- `4 q! I/ x% O/ J$ R7 }% i
2 e' _8 r6 I( l* _! Y
9 c/ w  G5 U) I" E2 E, _2 N- I, E1 A. M  K
2. 内核访问块设备的方法* W6 P- {" h2 K
内核通过文件系统访问块设备时,需要先把块读入到内存中。所以文件系统为了管理块设备,必须管理[块]和内存页之间的映射。8 D1 a2 S( m$ z4 u# ^8 A

/ h" N: v. w9 u9 a内核中有2种方法来管理 [块] 和内存页之间的映射。
- N, K3 B" G' D, S1 K. o3 q1 O! q" N
缓冲区和缓冲区头
; B+ S& D; x- m+ Zbio
! [! w9 X3 F2 _! Z
2 Q# e7 |% s# `# q5 m) w) [( O' [& U6 F
. W' ?% L* O& S# f2.1 缓冲区和缓冲区头
1 ~. `0 I0 i  c- X& E3 V每个 [块] 都是一个缓冲区,同时对每个 [块] 都定义一个缓冲区头来描述它。
5 F( O; @) _2 i; W! L7 V; n2 Z3 c5 E5 @
由于 [块] 的大小是小于内存页的大小的,所以每个内存页会包含一个或者多个 [块]' C0 c6 U( \" U* p5 z

4 P5 I1 D5 I# A2 [8 L" \" C6 G ' Q4 W( M% T6 {/ [( `: T

$ S3 e. Y+ e4 x6 b缓冲区头定义在 <linux/buffer_head.h>: include/linux/buffer_head.h0 U8 h- y7 h9 b, m: z6 o

8 `3 `/ v/ [* i5 {复制代码- V) W. Z7 x4 Z& [& x! b5 b8 T' b
struct buffer_head {0 I* Y6 M$ |9 H4 \, p) \* p% n
    unsigned long b_state;            /* 表示缓冲区状态 */
$ D5 ?# Q' I0 ]5 t$ d' m    struct buffer_head *b_this_page;/* 当前页中缓冲区 */
) H$ L& z/ F9 R, ]# Z    struct page *b_page;            /* 当前缓冲区所在内存页 */: U5 P6 c; O# V5 r! Z! f$ r0 l
6 r) P; b: H9 A$ M; _; D7 r
    sector_t b_blocknr;        /* 起始块号 */, S( y, ~1 C+ X! ?4 g
    size_t b_size;            /* buffer在内存中的大小 */+ }, M9 E# r5 D" Y
    char *b_data;            /* 块映射在内存页中的数据 */% d% o, O" B. B1 t

: W. P4 ~  B; n$ y0 \% m9 _% U/ X' f    struct block_device *b_bdev; /* 关联的块设备 */
& N4 _8 M/ x# s+ U+ _    bh_end_io_t *b_end_io;        /* I/O完成方法 */
) f! X) E  U( [8 G$ o; ?9 S8 _5 v- P- N& v     void *b_private;             /* 保留的 I/O 完成方法 */8 s& M) H7 D6 f! u
    struct list_head b_assoc_buffers;   /* 关联的其他缓冲区 */
9 v2 x4 T5 l8 d    struct address_space *b_assoc_map;    /* 相关的地址空间 */
0 p! A# F  l2 Q9 Y    atomic_t b_count;                    /* 引用计数 */
  k5 z/ V6 c( p};' G& T. n# T0 p+ g/ \8 p
复制代码
: c2 D9 h0 e" s# n! D0 R8 s
; d, {$ C2 S8 }( p4 D. |3 W; b& x( H% v. _" z' ^6 E$ F) _
整个 buffer_head 结构体中的字段是减少过的,以前的内核中字段更多。
# `4 c& s& Z' ^' q  v1 I( A7 H% D; e1 k! M4 |; [
各个字段的含义通过注释都很明了,只有 b_state 字段比较复杂,它涵盖了缓冲区可能的各种状态。7 f- ~7 y: F4 d" p, }% ]7 C2 G
$ e. \( T8 T4 r
复制代码
" D6 e* [( Y3 @" G& q0 B, `! Kenum bh_state_bits {- @+ x% w. r$ n
    BH_Uptodate,    /* 包含可用数据 */
. O' W- l) Y, K! O) q2 }' K! E    BH_Dirty,    /* 该缓冲区是脏的(说明缓冲的内容比磁盘中的内容新,需要回写磁盘) */
$ |5 Q2 q! i! X4 r' }4 D/ @4 {    BH_Lock,    /* 该缓冲区正在被I/O使用,锁住以防止并发访问 */
. o0 `2 ^3 ^6 T! z/ ~) a    BH_Req,        /* 该缓冲区有I/O请求操作 */
5 S3 O* A$ R4 q0 h9 o6 K7 s9 g    BH_Uptodate_Lock,/* 由内存页中的第一个缓冲区使用,使得该页中的其他缓冲区 */
/ n/ }% [% D' E  J0 v) ~/ g1 ]0 z. \% o, B1 b& H- H
    BH_Mapped,    /* 该缓冲区是映射到磁盘块的可用缓冲区 */
" S  x9 |6 I9 h    BH_New,        /* 缓冲区是通过 get_block() 刚刚映射的,尚且不能访问 */0 S5 j/ X$ _1 S
    BH_Async_Read,    /* 该缓冲区正通过 end_buffer_async_read() 被异步I/O读操作使用 */
! a  j0 F1 x+ t/ B0 F+ \    BH_Async_Write,    /* 该缓冲区正通过 end_buffer_async_read() 被异步I/O写操作使用 */% u! `( @  m) F: h% O
    BH_Delay,    /* 缓冲区还未和磁盘关联 */" I- B" \' ^7 A! ^$ ^8 [! p
    BH_Boundary,    /* 该缓冲区处于连续块区的边界,下一个块不在连续 */
& r; D' V  n5 z  d: T% ?6 ~1 K2 W    BH_Write_EIO,    /* 该缓冲区在写的时候遇到 I/O 错误 */
  y0 f3 B2 e+ `( V; J* z    BH_Ordered,    /* 顺序写 */. ^/ c' y/ C  L3 O9 V& j3 E% g
    BH_Eopnotsupp,    /* 该缓冲区发生 “不被支持” 错误 */
$ ^! b  J4 t! n+ S/ F- v    BH_Unwritten,    /* 该缓冲区在磁盘上的位置已经被申请,但还有实际写入数据 */
- z: f$ r; [( }3 T  ^3 ?0 n. n    BH_Quiet,    /* 该缓冲区禁止错误 */0 [, y! ~7 I" Y, H: X- @% S* \
5 y& S4 Q. L0 a: q. c
    BH_PrivateStart,/* 不是表示状态,分配给其他实体的私有数据区的第一个bit */
5 Y* d1 E( G$ v: g* o  _};  v+ [& R9 |6 Z
复制代码
* c' _) E; @& r/ r # u4 U4 k1 W/ q1 Y4 j/ g( @- K

0 o7 M4 N, d+ P7 e& ?: q在2.6之前的内核中,主要就是通过缓冲区头来管理 [块] 和内存之间的映射的。, t: ?3 I- C: _* S# L6 {
, S* L5 t3 M( U" t% l* P
用缓冲区头来管理内核的 I/O 操作主要存在以下2个弊端,所以在2.6开始的内核中,缓冲区头的作用大大降低了。
  c% c  @5 B% D- k) d3 U  Y8 E7 \& w: [9 C8 F
- 弊端 1- G$ H* @2 T1 G+ p* J

4 u9 k- t; g- L  Z对内核而言,操作内存页是最为简便和高效的,所以如果通过缓冲区头来操作的话(缓冲区 即[块]在内存中映射,可能比页面要小),效率低下。
" C1 ~4 ~3 q: f3 h
8 U  O' p# e$ }) s* p% g3 P而且每个 [块] 对应一个缓冲区头的话,导致内存的利用率降低(缓冲区头包含的字段非常多)
0 i5 R; U$ Y. `7 q/ K4 b* E
2 O- K3 J, k" ^- }* h' q, ]2 h4 R- 弊端 2
1 U, i, }9 X. x3 R, Z5 L% q, i! D8 r# \3 _; [7 n
每个缓冲区头只能表示一个 [块],所以内核在处理大数据时,会分解为对一个个小的 [块] 的操作,造成不必要的负担和空间浪费。$ R# M; U7 K8 S- t) s( |. m

4 [" C& H' z* A0 h" r
: q) |' Q0 H  d) y( d, }1 [7 \# G( D6 l4 J1 \) Z
2.2 bio
: [; j: n- l' t. A! X/ q5 E+ Dbio结构体的出现就是为了改善上面缓冲区头的2个弊端,它表示了一次 I/O 操作所涉及到的所有内存页。
5 W4 I) ]* M+ Q' M- L% e
: ?; d$ I8 A+ I复制代码+ z. ?  z' G$ T$ q+ l; P1 \/ S" V7 \
/*
& _) c% K' q8 \! S4 [2 p+ I  i * I/O 操作的主要单元,针对 I/O块和更低级的层 (ie drivers and
; @: ?+ q# V+ J9 A5 n * stacking drivers)! q3 m4 ]- @7 r$ y& M) p' R1 V
*/
. S* t* o  |6 S  Y# H( }' t; _; nstruct bio {
" m8 ^: L% S+ n  c* q9 b$ N    sector_t        bi_sector;    /* 磁盘上相关扇区 */8 y0 O4 k  D/ F; F
    struct bio        *bi_next;    /* 请求列表 */$ A6 ~2 ^! X+ d# r
    struct block_device    *bi_bdev; /* 相关的块设备 */
% \1 }  ^2 ^( L# l# W; n: F0 J8 }    unsigned long        bi_flags;    /* 状态和命令标志 */' x2 N7 o/ o* V/ h! W) u
    unsigned long        bi_rw;        /* 读还是写 */
# o5 N  f$ |! _. M2 q
2 K4 }0 t, s0 U) E, y    unsigned short        bi_vcnt;    /* bio_vecs的数目 */
0 V% s$ Y- ^2 }0 P6 D    unsigned short        bi_idx;        /* bio_io_vect的当前索引 */
. X+ Y+ F+ g% u' \' V3 M/ _) o! ]$ ~! y- z$ X
    /* Number of segments in this BIO after
1 x, U8 ^, A/ R* o# E     * physical address coalescing is peRFormed.1 L% |2 x" D$ j1 A' x, m6 N( V
     * 结合后的片段数目; W: L  k/ H. r$ U
     */
( x) w# k+ d( D, e+ n    unsigned int        bi_phys_segments;8 ]1 ^# d: K8 ?# _
" j& K0 Z6 m( t0 V8 t; p, B
    unsigned int        bi_size;    /* 剩余 I/O 计数 */
: H$ ]$ V5 `- s% R+ y& R5 ?5 ]! M2 ~( L7 x
    /*
9 R4 v/ H$ c. v- ]. V! ~! T. t     * To keep track of the max segment size, we account for the6 V: J5 {5 `( C3 g0 T  s
     * sizes of the first and last mergeable segments in this bio.( u7 S0 V. c7 Q6 L
     * 第一个和最后一个可合并的段的大小) X; o2 g) {" v
     */
4 S! R" w* R  t) m) m- R  Z    unsigned int        bi_seg_front_size;
$ j2 V. x# Q. F7 E3 \2 Y    unsigned int        bi_seg_back_size;' d" W; I) f# [- Q( [# }$ l- T4 J5 Y

: S3 `  @' }) J, \9 h  ?! `    unsigned int        bi_max_vecs;    /* bio_vecs数目上限 */
' P4 x3 V( v( i2 g; Z) ~8 p    unsigned int        bi_comp_cpu;    /* 结束CPU */
4 U; s4 T6 s" @9 u2 k& e" H5 T% B9 N: G: r  M# u( N  e) {
    atomic_t        bi_cnt;        /* 使用计数 */+ K7 t9 _4 i7 M/ ^5 E
    struct bio_vec        *bi_io_vec;    /* bio_vec 链表 */
" j1 R9 S: w# H    bio_end_io_t        *bi_end_io; /* I/O 完成方法 */
& Y1 N. R+ F$ d) W/ E    void            *bi_private;    /* bio结构体创建者的私有方法 */
5 |9 c% `6 w1 n3 c- H! t#if defined(CONFIG_BLK_DEV_INTEGRITY)% J* |2 X+ N8 r! m" h$ B$ J8 Z- i
    struct bio_integrity_payload *bi_integrity;  /* data integrity */6 p7 E3 P- ^9 U9 z$ [% z
#endif" x$ I0 ~: N# o  H
    bio_destructor_t    *bi_destructor;    /* bio撤销方法 */3 N; E0 f' e' ~2 E( W
    /*
* H3 k  p1 d9 @- ]' J+ T     * We can inline a number of vecs at the end of the bio, to avoid
, [5 M& F& j* @0 u: Q: k     * double allocations for a small number of bio_vecs. This member7 L7 L8 t; Y- `2 ~" Y: t$ U
     * MUST obviously be kept at the very end of the bio.# M# Q5 @; W8 T9 k) N+ Y' r
     * 内嵌在结构体末尾的 bio 向量,主要为了防止出现二次申请少量的 bio_vecs
4 E% c6 |6 O2 E8 S, z) A     */
. j  K& {9 k8 {0 t2 f' s    struct bio_vec        bi_inline_vecs[0];% C( w( w" `. v
};$ {; b* Z: A" A# u# V- j( s
复制代码
, j; v/ i% J# }$ J. y7 w4 K0 j几个重要字段说明:
" p" d) K! H$ b. X! ^9 C( r- R$ _) K6 R  B3 \( J
bio 结构体表示正在执行的 I/O 操作相关的信息。( A" N2 {" s" T: N% F, a( w
bio_io_vec 链表表示当前 I/O 操作涉及到的内存页8 u2 y/ |, v# F# A% R) E
bio_vec 结构体表示 I/O 操作使用的片段
# E. \$ p$ X$ I" _bi_vcnt bi_io_vec链表中bi_vec的个数0 o, @& x4 ~, h
bi_idx 当前的 bi_vec片段,通过 bi_vcnt(总数)和 bi_idx(当前数),就可以跟踪当前 I/O 操作的进度
, H* X1 i: x: d; D & E; w( F9 `- Z! H9 m9 H
# _: i0 D, x1 i
bio_vec 结构体很简单,定义如下:
, p" F# t: `. e- L: o0 C
% t  r6 P. w) D: |1 Zstruct bio_vec {
/ b8 J& d* g8 i8 A" F$ n% F    struct page    *bv_page;       /* 对应的物理页 */9 U7 c8 h% E% }. r
    unsigned int    bv_len;     /* 缓冲区大小 */7 [" o% y. f: v' ^
    unsigned int    bv_offset;  /* 缓冲区开始的位置 */) b  x  ]8 R* R. i
};
+ X( G8 Y& I1 t5 Y每个 bio_vec 都是对应一个页面,从而保证内核能够方便高效的完成 I/O 操作' v6 R9 O! B3 n  U

6 t* z& n$ a6 O 3 q* x/ a( O4 T+ o4 O# u2 d
5 ]" O2 p7 {, X' `' E7 S
2.3 2种方法的对比
/ p% `; n* {' n& p! ]缓冲区头和bio并不是相互矛盾的,bio只是缓冲区头的一种改善,将以前缓冲区头完成的一部分工作移到bio中来完成。- K1 L& n" {( @( m1 I
0 n  F% l) z* e4 a& G) K4 N
bio中对应的是内存中的一个个页,而缓冲区头对应的是磁盘中的一个块。9 Z- Z* X) y9 y# G( g2 L/ T8 T; c% q

, Q% P& y, P( M, x! o1 ?1 @对内核来说,配合使用bio和缓冲区头 比 只使用缓冲区头更加的方便高效。
& l- y' [7 f- z8 |7 M% i8 @5 ^* g
bio相当于在缓冲区上又封装了一层,使得内核在 I/O操作时只要针对一个或多个内存页即可,不用再去管理磁盘块的部分。
) c7 @  N# i, P! Q: o3 h5 S9 L
& x: `2 ^- \/ r' L! a3 D
/ {, D+ }/ [1 o$ ?% P1 n2 h* b; P4 `& [
使用bio结构体还有以下好处:
+ H. ~! D6 y+ h" d# b
+ t; ]  P3 N& q! ibio结构体很容易处理高端内存,因为它处理的是内存页而不是直接指针
' u) X2 f! E+ _! o# v5 J$ ibio结构体既可以代表普通页I/O,也可以代表直接I/O0 \" V3 P! t" ^( t8 v2 y# u
bio结构体便于执行分散-集中(矢量化的)块I/O操作,操作中的数据可以取自多个物理页面, T- z* j. M& D2 ^" b0 H
0 m7 t# D) ?& g3 s: B$ U7 {

8 Q; b+ p: h* M3. 内核I/O调度程序
1 [  l+ I. F7 _( Y3 K. W缓冲区头和bio都是内核处理一个具体I/O操作时涉及的概念。; r, f: ~; C& s, L6 L

9 Q( f' l$ \6 r3 b! K' R9 @) e" X但是内核除了要完成I/O操作以外,还要调度好所有I/O操作请求,尽量确保每个请求能有个合理的响应时间。
: }6 s2 T& ~2 x
9 C, E# t( O: ^: F* W3 F- y : y& f( M1 ]; o) O9 g7 A5 y* f

  a7 O& F  q3 M9 y0 {下面就是目前内核中已有的一些 I/O 调度算法。# e) F$ k- c" d1 @% @3 [

, o# H+ f3 ~6 ^3 \- Z3.1 linus电梯( l: J( f/ |. i; c2 L: C! a$ C( M' y
为了保证磁盘寻址的效率,一般会尽量让磁头向一个方向移动,等到头了再反过来移动,这样可以缩短所有请求的磁盘寻址总时间。, m% \" p4 a4 g- T& m1 g% w
& L+ o: O% G2 A4 {
磁头的移动有点类似于电梯,所有这个 I/O 调度算法也叫电梯调度。
7 P) s% S/ O+ P- [5 @7 f$ y+ f; q) h5 k4 h2 L9 o! T/ [8 {' z/ G; r8 v
linux中的第一个电梯调度算法就是 linus本人所写的,所有也叫做 linus 电梯。
+ X- `( S0 V% Q) S: {( n/ K; r* z  Z" s! y

" ?/ \: t/ w4 o$ C& `7 z% f7 d, T+ d8 N9 n
linus电梯调度主要是对I/O请求进行合并和排序。8 C4 |/ H) r% C( m7 `' G. O6 a( d

# I. e+ z# U/ Y) U当一个新请求加入I/O请求队列时,可能会发生以下4种操作:& l$ h. D  [, Y; N: p

; f: Y# V, M% Y7 ]3 {/ X$ V如果队列中已存在一个对相邻磁盘扇区操作的请求,那么新请求将和这个已存在的请求合并成一个请求. K  g' C8 W5 t; K
如果队列中存在一个驻留时间过长的请求,那么新请求之间查到队列尾部,防止旧的请求发生饥饿
) `( j) |9 f' Q9 ]" T如果队列中已扇区方向为序存在合适的插入位置,那么新请求将被插入该位置,保证队列中的请求是以被访问磁盘物理位置为序进行排列的
+ h/ }- N4 K7 l7 Q# h1 c5 S如果队列中不存在合适的请求插入位置,请求将被插入到队列尾部9 W' q4 K; q7 `7 J# r3 C3 o+ m' O1 t

2 \* z0 d6 ]; r# g# k/ t( ~' c6 H8 W+ {4 x4 B) J* i8 {
linus电梯调度程序在2.6版的内核中被其他调度程序所取代了。
% u, L1 L8 I# K2 h* p$ s& s
- R! O$ c' d9 \% M) N% w
9 ?* M# a' c6 p4 }# Y  ~7 X) q8 ?6 {8 U9 s% x9 {0 Y' s
3.2 最终期限I/O调度
1 C* D8 |6 s( ]0 N; A0 slinus电梯调度主要考虑了系统的全局吞吐量,对于个别的I/O请求,还是有可能造成饥饿现象。! g9 h- c1 P" N" U5 x* P3 V! K

4 [; S0 z- z) Y1 d) M( [4 L- s而且读写请求的响应时间要求也是不一样的,一般来说,写请求的响应时间要求不高,写请求可以和提交它的应用程序异步执行,
2 O1 \( T9 K4 G8 ~, u8 B1 ]. v% }6 u* a' \* Q
但是读请求一般和提交它的应用程序时同步执行,应用程序等获取到读的数据后才会接着往下执行。( V- _/ d9 \4 X) U2 b
+ }& }0 l; u$ d4 p% v1 e
因此在 linus 电梯调度程序中,还可能造成 写-饥饿-读(wirtes-starving-reads)这种特殊问题。
- a3 B4 T1 ?5 h  r! y. z1 R0 O* ?. n- u) L1 f/ l5 s

  W) V6 Q1 X5 @
& @/ q6 ?1 o2 F- W( T, Q. n; g0 A为了尽量公平的对待所有请求,同时尽量保证读请求的响应时间,提出了最终期限I/O调度算法。
6 [* M# p; p+ L. r/ U* H8 M" \5 E; L3 D
最终期限I/O调度 算法给每个请求设置了超时时间,默认情况下,读请求的超时时间500ms,写请求的超时时间是5s
2 C6 p0 F3 I! {$ a* p/ g+ N' R
- j& T$ w& U3 e: h但一个新请求加入到I/O请求队列时,最终期限I/O调度和linus电梯调度相比,多出了以下操作:+ W: j$ I7 }+ D5 K
9 P* N, U4 [2 L5 m. z4 Q2 j
新请求加入到 排序队列(order-FIFO),加入的方法类似 linus电梯新请求加入的方法
9 B: p/ D5 o8 n8 T5 d3 O5 X* \根据新请求的类型,将其加入 读队列(read-FIFO) 或者写队列(wirte-FIFO) 的尾部(读写队列是按加入时间排序的,所以新请求都是加到尾部): l. v. C' |3 q) M- C1 [2 i
调度程序首先判断 读,写队列头的请求是否超时,如果超时,从读,写队列头取出请求,加入到派发队列(dispatch-FIFO)9 Y  x6 ^: I. P5 V1 H/ k9 q
如果没有超时请求,从 排序队列(order-FIFO)头取出一个请求加入到 派发队列(dispatch-FIFO)
  s5 j  W7 l2 v+ i6 ]2 M4 Z7 k' |派发队列(dispatch-FIFO)按顺序将请求提交到磁盘驱动,完成I/O操作* A- K" F( c! q& M, r2 T; D
8 c( z2 T" f/ [) E
* s$ W; b, p8 w( c$ Z
最终期限I/O调度 算法也不能严格保证响应时间,但是它可以保证不会发生请求在明显超时的情况下仍得不到执行。# o5 j  ?0 r6 k
0 Q5 g2 e) D2 |" s2 E, J( e  X7 |
最终期限I/O调度 的实现参见: block/deadline-iosched.c
! \/ ]% q4 R) d7 S; F2 c; C5 Z. g; y9 }- }. M9 M
% z, N9 b# k0 C$ F

: g! R* k/ t, h$ j4 `3.3 预测I/O调度
) Q$ h, h' q- W7 q$ ]0 P2 i. i最终期限I/O调度算法优先考虑读请求的响应时间,但系统处于写操作繁重的状态时,会大大降低系统的吞吐量。' U0 s! [$ m- }# Q
# p( k' y5 l+ F( N6 v
因为读请求的超时时间比较短,所以每次有读请求时,都会打断写请求,让磁盘寻址到读的位置,完成读操作后再回来继续写。
$ T$ h& {6 P$ X1 @; k* o# Z* R0 t$ Z2 y/ O/ R4 }
这种做法保证读请求的响应速度,却损害了系统的全局吞吐量(磁头先去读再回来写,发生了2次寻址操作)
' D, N9 |/ }* \7 C  w  ?; {9 f
. q0 ]6 _3 s  G# Q3 I) B- s & y. W( F+ c6 u4 N3 z8 _5 r* {

8 X5 q' u  R! ?预测I/O调度算法是为了解决上述问题而提出的,它是基于最终期限I/O调度算法的。4 w* ]4 k/ U) A" T3 b2 r
; ]  n; }# S: W; a5 j' a8 M' p
但有一个新请求加入到I/O请求队列时,预测I/O调度与最终期限I/O调度相比,多了以下操作:" @: N+ d  H% V* L3 C7 |# T/ I% h- t% W
- l2 k% ^: x' I3 Y% w5 _
新的读请求提交后,并不立即进行请求处理,而是有意等待片刻(默认是6ms)6 C& @) B7 _0 H/ h" X5 @" p8 E
等待期间如果有其他对磁盘相邻位置进行读操作的读请求加入,会立刻处理这些读请求
0 G8 P7 U+ c$ n等待期间如果没有其他读请求加入,那么等待时间相当于浪费掉: l. X, h' e# {8 U* M( l. I
等待时间结束后,继续执行以前剩下的请求  ~1 f! R+ ~! z; F9 _. u

8 q2 V8 b: ?; m6 |4 K* k: b0 a# j4 @; _; S$ v) X, \1 ]9 p5 K
预测I/O调度算法中最重要的是保证等待期间不要浪费,也就是提高预测的准确性," O; q. u  G" m$ B" x; T; g
( V  d6 f+ B0 a- [6 w- g9 s, }
目前这种预测是依靠一系列的启发和统计工作,预测I/O调度程序会跟踪并统计每个应用程序的I/O操作习惯,以便正确预测应用程序的读写行为。  I+ ]$ \+ P9 d

7 L: H  _+ w, {) P  W( a ; K: ^0 v  ]' `1 H* L% _5 J

8 B; e3 R9 g$ \* F) N  L- h; K1 a如果预测的准确率足够高,那么预测I/O调度和最终期限I/O调度相比,既能提高读请求的响应时间,又能提高系统吞吐量。3 W* S. C0 n, R5 T4 Z# C

8 n. N1 b. S$ ]4 j+ A预测I/O调度的实现参见: block/as-iosched.c
: Z' E. J2 Y/ P4 }+ ^7 d3 l  M- W( S; n, Z
) u/ X7 i- h# ~
- e' `, {* p( X9 H7 T
! x" p5 \% y& \注:预测I/O调度是linux内核中缺省的调度程序。
7 [( `) f9 o. H1 E9 J# y3 f9 M/ m; H. R5 r& Z' E

/ w( J9 p, [7 }8 H8 o( P! _0 r- C6 W* }1 Z5 F% h) n
3.4 完全公正的排队I/O调度
* }- r, U+ y9 g4 Y' ^" p( c完全公正的排队(Complete Fair Queuing, CFQ)I/O调度 是为专有工作负荷设计的,它和之前提到的I/O调度有根本的不同。% J$ B* }$ |4 N, R

5 W: A. n# ~7 a" Q2 i4 }2 ?CFQ I/O调度 算法中,每个进程都有自己的I/O队列,% M% G) |5 n- n# U) |' b

0 P8 H: ?$ |2 C! o) a  x4 |( T& XCFQ I/O调度程序以时间片轮转调度队列,从每个队列中选取一定的请求数(默认4个),然后进行下一轮调度。3 A1 L, L$ h5 d' P3 b  h; Y0 F

, M1 D( Z. H" L% {' ]
0 R! p. [2 b( `* D+ u& x; a# C# j8 o9 U' _% R2 r( V! A
CFQ I/O调度在进程级提供了公平,它的实现位于: block/cfq-iosched.c
0 Z: c; d$ _) u9 N! j- e; d; ~! V' `5 F- @" ^

+ r5 [! _5 ?+ U; `6 E, ^
. N' v. @7 p- V! f3.5 空操作的I/O调度  ~$ x+ W) w7 O
空操作(noop)I/O调度几乎不做什么事情,这也是它这样命名的原因。3 H$ h4 G, [, i, ]

; b+ c+ G5 M, E( @9 R/ u空操作I/O调度只做一件事情,当有新的请求到来时,把它与任一相邻的请求合并。
# m6 N' }- T, E& J9 M* Y
6 d; q7 v! d2 Z+ w- d2 w9 \ & X+ q) _* i' F  q( x+ a
8 m$ ?. o: a* z1 ~
空操作I/O调度主要用于闪存卡之类的块设备,这类设备没有磁头,没有寻址的负担。8 l5 z( a2 a2 J$ ^

$ I% }( t( q7 Y; A空操作I/O调度的实现位于: block/noop-iosched.c
. k4 Y' A7 |' _+ G7 ?: J& h6 G1 [! Q% ~/ J* l

6 |, D  \4 x3 P; C% O; {: t+ T! j! N! @9 R5 J* e8 X, h' t
3.6 I/O调度程序的选择
, x+ e7 |: c" U* M' Y/ b2.6内核中内置了上面4种I/O调度,可以在启动时通过命令行选项 elevator=xxx 来启用任何一种。
3 K- x/ v) h7 ~# u% B, \! p) @/ ~  g5 R5 s) M: I) M4 Z% O" ?
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 18:17 , Processed in 0.171875 second(s), 26 queries , Gzip On.

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

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

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