EDA365电子论坛网

标题: Linux内核设计与实现之块I/O层 [打印本页]

作者: pulbieup    时间: 2021-1-27 09:43
标题: Linux内核设计与实现之块I/O层
0 I! X3 Q; g& e- q" v0 K
主要内容:
+ ]) P$ B3 b5 X: M  p3 m* G' J: A5 y1 Y1 u
块设备简介
0 Y4 _7 j0 h  I+ {% W内核访问块设备的方法; h- I1 ^5 |7 `2 u- U
内核I/O调度程序1 m1 g3 C; @. D

' z( m& I* J' r( f8 D
" h# ^' E  D2 j) R$ @6 {/ `/ B1. 块设备简介
  ?4 R$ Z8 K& ^! P/ \3 z! P& Q2 qI/O设备主要有2类:
" R; x& B; \6 D0 q) j7 c) N0 M, y" T+ o. i
字符设备:只能顺序读写设备中的内容,比如 串口设备,键盘; L# x" h# y$ c
块设备:能够随机读写设备中的内容,比如 硬盘,U盘& P7 Y) M- Q1 b  s- Q: n
字符设备由于只能顺序访问,所以应用场景也不多,这篇文章主要讨论块设备。
4 z4 M4 G3 I4 @& C9 _" D7 @
' h6 H* _; Y) S块设备是随机访问的,所以块设备在不同的应用场景中存在很大的优化空间。
8 q# ]8 E' t& P, ]: o. T  L/ k' z0 B3 v. i$ r9 @: {
5 h; N8 J' |, w
- D$ A0 ~7 x0 R/ m1 d( u4 _
块设备中最重要的一个概念就是块设备的最小寻址单元。
- @1 U$ T& x5 K9 P
/ p2 F; o' L7 l块设备的最小寻址单元就是扇区,扇区的大小是2的整数倍,一般是 512字节。: M1 a0 i. D  |  u
. M$ M# C1 f! k/ ~+ ~
扇区是物理上的最小寻址单元,而逻辑上的最小寻址单元是块。
3 g8 o/ I" D+ {# U
* N% w$ x& i! O+ p4 t0 b为了便于文件系统管理,块的大小一般是扇区的整数倍,并且小于等于页的大小。
/ Y$ f# A+ z3 t- o' ]! G* d4 I  I, l# J, \

6 t4 ~1 M7 R) e  N4 f* U: Q- o$ S  L- l. u" v8 u( q8 B$ X
查看扇区和I/O块的方法:
4 E. T$ d; i  H$ x
  ?1 L# H0 |. w7 K复制代码3 f. I- H- x! X4 W" S7 X
[wangyubin@localhost]$ sudo fdisk -l5 Y0 [; Y7 ?3 J# p2 [

1 I) X4 p) P% m) P1 O  i7 bWARNING: GPT (GUID Partition Table) detected on '/dev/sda'! The util fdisk doesn't support GPT. Use GNU Parted.3 W- J4 E, d/ n- `/ X- j

/ T2 O. k% b" {2 V2 `* K- I- m+ w
* u" Q; K5 u  T" w, d, XDisk /dev/sda: 500.1 GB, 500107862016 bytes, 976773168 sectors
$ i) {- x) f% q4 l' FUnits = sectors of 1 * 512 = 512 bytes
) u6 y& ^& ~# N. p  JSector size (logical/physical): 512 bytes / 4096 bytes9 g$ \1 z! h2 }
I/O size (minimum/optimal): 4096 bytes / 4096 bytes- `; D2 N, c1 |% e  Q
Disk identifier: 0x00000000
9 p6 g0 N- Q$ D4 |; ?复制代码0 B9 x4 a. s; Y+ x6 g6 F
上面的 Sector size 就是扇区的值,I/O size就是 块的值! }/ G" W/ b* @$ f
% |6 G7 p5 S, R* b; ^7 X
从上面显示的结果,我们发现有个奇怪的地方,扇区的大小有2个值,逻辑大小是 512字节,而物理大小却是 4096字节。+ I2 B: R% ~& W, ~' _8 h

6 U; b* A0 ]2 G& {: k. K7 A其实逻辑大小 512字节是为了兼容以前的软件应用,而实际物理大小 4096字节是由于硬盘空间越来越大导致的。' O2 y3 P/ i/ R. U$ w

5 M. f" a2 ]  i7 s3 ], c具体的来龙去脉请参考:4KB扇区的原因
$ C8 _7 M: |9 Q2 N& ^; {7 \
: E2 F) [7 p" n# W- t $ ^: @4 S# A' i0 a2 D4 C" `7 O

3 t- W+ A# r) T1 U* N4 K2. 内核访问块设备的方法. ?' \+ @  t1 S3 t3 o
内核通过文件系统访问块设备时,需要先把块读入到内存中。所以文件系统为了管理块设备,必须管理[块]和内存页之间的映射。: j( u9 d) r6 o" G( y

" ^" t. u! f# \- `6 `, [内核中有2种方法来管理 [块] 和内存页之间的映射。4 j& u; z6 m2 p: F) H! d
1 e1 \$ |/ [9 z: X; {
缓冲区和缓冲区头
# G& n, V* [% k5 j3 vbio
! V* S  m% Z+ v4 n7 I( X) C) n, H
- M1 e, h7 p1 u$ b" l( J
; I: |* K6 U( _$ A! u2.1 缓冲区和缓冲区头
3 t* f6 E: a! ]2 F2 Y1 G+ ^9 _. F3 ]每个 [块] 都是一个缓冲区,同时对每个 [块] 都定义一个缓冲区头来描述它。& l/ j5 o7 _9 Y% p) n7 g  D9 T

7 g, ]# Q+ g; A0 i( _) i1 W由于 [块] 的大小是小于内存页的大小的,所以每个内存页会包含一个或者多个 [块], g% {1 u, a  g; F% _

0 H, D6 z: ~: M7 ]
$ l1 a' J9 P% R  l+ S  j( u! C$ O  Q1 O
缓冲区头定义在 <linux/buffer_head.h>: include/linux/buffer_head.h7 W9 |1 l8 A! b1 `0 F9 q/ G  H
" q2 r% }0 {, u# X( X! z7 G
复制代码
  c, a( @% E8 |# Pstruct buffer_head {* d1 O6 B+ {: B( t, r7 u# O, }! }
    unsigned long b_state;            /* 表示缓冲区状态 */, u3 P1 p* d# q& \
    struct buffer_head *b_this_page;/* 当前页中缓冲区 */6 f; X0 m4 a7 z  B
    struct page *b_page;            /* 当前缓冲区所在内存页 */
3 F. {2 a$ `$ [* i5 M! Q2 B! A
; v3 R+ e' F# N3 b* j    sector_t b_blocknr;        /* 起始块号 */4 s! F1 |4 p8 w" _9 z! S0 z/ a( d
    size_t b_size;            /* buffer在内存中的大小 */. m& ~+ D* g% v7 G( K9 c
    char *b_data;            /* 块映射在内存页中的数据 */
$ a$ Y0 W& I* e$ V1 Q
7 g1 t/ g& k# d1 N5 v' B    struct block_device *b_bdev; /* 关联的块设备 */
2 `, V% T" x2 _+ _) N7 n    bh_end_io_t *b_end_io;        /* I/O完成方法 */
7 Z0 s% ^4 G& V     void *b_private;             /* 保留的 I/O 完成方法 */9 k) Y+ B( L' U; _& G' s
    struct list_head b_assoc_buffers;   /* 关联的其他缓冲区 */8 X3 L* Q! s9 S! a2 d7 J5 D
    struct address_space *b_assoc_map;    /* 相关的地址空间 */" k# K: k) S# T9 L& _! C
    atomic_t b_count;                    /* 引用计数 */
$ _3 n% q( k* O};) x, l! u$ W; C* P# F7 u5 R
复制代码( p4 }. u5 |  l. b9 ~6 b" `% T: ~5 [
0 q9 R. S! Q0 X0 Y

" f  C& N4 g/ j; \整个 buffer_head 结构体中的字段是减少过的,以前的内核中字段更多。" [, k- X% j* i. _* y3 B+ f2 x
) M2 x. B5 b( d+ f4 o$ n1 I
各个字段的含义通过注释都很明了,只有 b_state 字段比较复杂,它涵盖了缓冲区可能的各种状态。
( ]. Q7 V) u. P9 f) j! W
+ f; r7 R% d; d& n) }7 V1 j/ _复制代码. v) r2 d2 v( H" ?9 A
enum bh_state_bits {
( U% y' B: v9 R9 M- u; z: B: ]    BH_Uptodate,    /* 包含可用数据 */
; ?; W0 Z' m8 c$ m5 @" r    BH_Dirty,    /* 该缓冲区是脏的(说明缓冲的内容比磁盘中的内容新,需要回写磁盘) */8 T9 o7 P' y  O/ Q4 ?) r. q
    BH_Lock,    /* 该缓冲区正在被I/O使用,锁住以防止并发访问 */; s; P$ l$ d& y7 ~9 @9 \  s4 H0 ^* y
    BH_Req,        /* 该缓冲区有I/O请求操作 */5 ?/ {. E; m  Y9 Y0 f) @+ |5 R
    BH_Uptodate_Lock,/* 由内存页中的第一个缓冲区使用,使得该页中的其他缓冲区 */; }' L5 r% A8 ?7 U

$ i& r' [( i6 e4 |# H- T    BH_Mapped,    /* 该缓冲区是映射到磁盘块的可用缓冲区 */
; O+ F8 H1 K* z) N. g" i' A, V    BH_New,        /* 缓冲区是通过 get_block() 刚刚映射的,尚且不能访问 */
# t4 E. s( ~) p5 x8 P% j0 L- C    BH_Async_Read,    /* 该缓冲区正通过 end_buffer_async_read() 被异步I/O读操作使用 */6 t6 c) r* z3 h! ~5 y. Y" b7 z7 V
    BH_Async_Write,    /* 该缓冲区正通过 end_buffer_async_read() 被异步I/O写操作使用 */) e3 Y" _- d* {
    BH_Delay,    /* 缓冲区还未和磁盘关联 */
$ [: p5 Z, M5 I3 B, }    BH_Boundary,    /* 该缓冲区处于连续块区的边界,下一个块不在连续 */
# ?: r9 _( N. D$ |  f    BH_Write_EIO,    /* 该缓冲区在写的时候遇到 I/O 错误 */
, P1 H, Q9 i" S- u: z    BH_Ordered,    /* 顺序写 */
7 h3 G5 M, k& f" K    BH_Eopnotsupp,    /* 该缓冲区发生 “不被支持” 错误 */3 V& ]+ h2 o" H  W, k$ o5 [* P
    BH_Unwritten,    /* 该缓冲区在磁盘上的位置已经被申请,但还有实际写入数据 */) s/ y7 f9 U/ b3 ^: C2 j
    BH_Quiet,    /* 该缓冲区禁止错误 */
' K1 _3 [6 L3 e- o6 j6 H8 Y, w
0 E, B3 P1 {- D( U) F    BH_PrivateStart,/* 不是表示状态,分配给其他实体的私有数据区的第一个bit */
, M9 m7 u/ t7 B5 h. e0 n};; |7 d( q( e$ z& m: [9 a0 V
复制代码+ I" D/ N- I; U

! X; E# p, _- E/ |5 ~/ \* b- e
! ~( m' B1 q5 f在2.6之前的内核中,主要就是通过缓冲区头来管理 [块] 和内存之间的映射的。2 |: ^: f* s9 p( G- K+ e* L
3 D+ ]+ h' W0 b
用缓冲区头来管理内核的 I/O 操作主要存在以下2个弊端,所以在2.6开始的内核中,缓冲区头的作用大大降低了。% [" U+ c: d# a# `7 k

4 y2 f% U( ?$ o- 弊端 1
# E# m" M  ?2 s' ^4 {! d& y' r( ]. `5 a4 J2 l, D
对内核而言,操作内存页是最为简便和高效的,所以如果通过缓冲区头来操作的话(缓冲区 即[块]在内存中映射,可能比页面要小),效率低下。" T/ e$ V" j( i  O1 }! i

& _$ w" y) o! v+ c2 x6 f6 Q而且每个 [块] 对应一个缓冲区头的话,导致内存的利用率降低(缓冲区头包含的字段非常多)- I% @0 i, ]. l6 [. G  O

! p6 K% \. d' S& C- 弊端 2
; B! _/ _+ I7 ^, i& s, d$ a. o2 N+ p
每个缓冲区头只能表示一个 [块],所以内核在处理大数据时,会分解为对一个个小的 [块] 的操作,造成不必要的负担和空间浪费。
6 C$ e: h, a' H1 F4 O* e- `' i6 x3 p( w
) o2 b& c/ H# {" ~  r7 z, ]" M
7 j) @$ c7 T3 p* d6 P! O0 E( l
2.2 bio6 B  @7 R' j+ ~' x0 }5 r
bio结构体的出现就是为了改善上面缓冲区头的2个弊端,它表示了一次 I/O 操作所涉及到的所有内存页。% m: L6 W2 R' b; p; E) b
3 j3 ]- [% D1 I: k* s
复制代码
; w6 a( F2 Q9 e; W/ u/*
0 j+ U$ x. C) r6 _ * I/O 操作的主要单元,针对 I/O块和更低级的层 (ie drivers and
, [$ N+ C  m1 _( i# o$ q * stacking drivers). Z/ @" k9 v1 `5 L  L  l& P( K
*/) v9 O7 t2 V/ h! [
struct bio {
: ]/ y( P7 R$ V: ]7 e9 o9 ~! X    sector_t        bi_sector;    /* 磁盘上相关扇区 */1 M% |2 t& u5 q9 }! v1 O
    struct bio        *bi_next;    /* 请求列表 */
7 M% G. y1 [# C/ j0 t6 `    struct block_device    *bi_bdev; /* 相关的块设备 */
8 x6 q/ @' z- k- e. j    unsigned long        bi_flags;    /* 状态和命令标志 */$ d0 h. d" [" y- u9 g- G
    unsigned long        bi_rw;        /* 读还是写 */7 j$ f: d6 o" T) P  z* U8 {- O' h
6 z! p1 x/ }, a! \9 s9 X
    unsigned short        bi_vcnt;    /* bio_vecs的数目 */( j3 J3 m' T( A- Z
    unsigned short        bi_idx;        /* bio_io_vect的当前索引 */
3 E" s$ Q4 P9 X7 E7 J# U9 |
3 A! B3 L4 B( Z, V% o- e" [# V    /* Number of segments in this BIO after& C7 z- M/ @8 W% U" g) t
     * physical address coalescing is performed.
; z# }8 h3 K, s$ i     * 结合后的片段数目0 Q- s" w$ a% w* A6 b! V: w0 N
     */
  o4 j3 C; T1 n7 |    unsigned int        bi_phys_segments;
" g$ G8 }4 P. t, n; ]' M
+ Y' f0 K0 u5 t! D3 I    unsigned int        bi_size;    /* 剩余 I/O 计数 */
0 D* q' m2 P2 Z% M1 m$ E- X+ O
    /*( F" G6 ^; M9 s" B& [$ T- A
     * To keep track of the max segment size, we account for the: S, {# K0 w, n% j# j
     * sizes of the first and last mergeable segments in this bio.
8 T# u( |3 ^) X     * 第一个和最后一个可合并的段的大小
' s. V" j4 g; L4 Z4 v     */
3 H; T& i  o' H! a: t: ~7 w    unsigned int        bi_seg_front_size;# H& P3 _, C5 E. @/ y5 E
    unsigned int        bi_seg_back_size;
6 M, {8 m3 F( O: f! f" `
+ }' G0 Q' V* x+ W* W    unsigned int        bi_max_vecs;    /* bio_vecs数目上限 */: ]- d: D0 w7 b! q6 |
    unsigned int        bi_comp_cpu;    /* 结束CPU */
' w5 t# Z& h$ Y  E! P( G" l
/ y: p8 [5 R1 K    atomic_t        bi_cnt;        /* 使用计数 */
: H4 B( |3 k" }    struct bio_vec        *bi_io_vec;    /* bio_vec 链表 */
5 L2 B) r( \' Z$ ?    bio_end_io_t        *bi_end_io; /* I/O 完成方法 */
( n) t, `2 D9 t; K( d. }% D! s    void            *bi_private;    /* bio结构体创建者的私有方法 */
8 q  z" G2 q  U" F3 t0 D" z& U. T#if defined(CONFIG_BLK_DEV_INTEGRITY)& m$ x( T2 \) Z9 J( p3 n
    struct bio_integrity_payload *bi_integrity;  /* data integrity */
9 A+ ?5 U, o7 A" _4 `0 N0 e! H3 r#endif9 M; N3 L; ]3 l8 w* l+ T6 d2 e% v6 T
    bio_destructor_t    *bi_destructor;    /* bio撤销方法 */! n- E( l: V' l, x$ O
    /*/ t3 e+ o+ X# c
     * We can inline a number of vecs at the end of the bio, to avoid! I" O- c8 |, C. A, R, \
     * double allocations for a small number of bio_vecs. This member
" }# W' A  j- Q     * MUST obviously be kept at the very end of the bio.. Y$ [! z. F. Z
     * 内嵌在结构体末尾的 bio 向量,主要为了防止出现二次申请少量的 bio_vecs: d6 }. ?  A4 R
     */
! m+ W, k8 H+ T3 q    struct bio_vec        bi_inline_vecs[0];
& e* J+ Q) `3 J4 X2 Y};& X1 m' w: u* Q6 M# b* v
复制代码
& U1 `9 L6 k# t8 V7 c7 S几个重要字段说明:
9 R; X1 P! ?& a* F* F) ?. R- ]# n1 N2 R  Q6 b8 Z
bio 结构体表示正在执行的 I/O 操作相关的信息。
: c8 l9 p) _9 Q7 obio_io_vec 链表表示当前 I/O 操作涉及到的内存页; B5 W' _4 ]  F
bio_vec 结构体表示 I/O 操作使用的片段
$ G, Y! {* h% ?1 Pbi_vcnt bi_io_vec链表中bi_vec的个数0 r9 r5 S9 d$ s3 H/ O- w
bi_idx 当前的 bi_vec片段,通过 bi_vcnt(总数)和 bi_idx(当前数),就可以跟踪当前 I/O 操作的进度& m2 ^& K6 e5 C1 O7 n8 @% A

8 z/ Q6 Q8 u2 W% q
3 v; C. ^! i" ]( Q0 u7 D3 v3 \bio_vec 结构体很简单,定义如下:
% x( L& v$ G- `$ d5 w+ k  p" t) k* i; g& b7 s$ M' D9 q
struct bio_vec {1 X9 _- ~. d) t
    struct page    *bv_page;       /* 对应的物理页 */0 D9 f9 @+ _9 d' o8 H) C5 W2 |* _
    unsigned int    bv_len;     /* 缓冲区大小 */
; L5 _- [! I5 P3 {- T# Z' Y    unsigned int    bv_offset;  /* 缓冲区开始的位置 */
6 T% q* `& m. e  X8 o. ]( g( d};: h4 o7 {) G# W3 {: ]0 B
每个 bio_vec 都是对应一个页面,从而保证内核能够方便高效的完成 I/O 操作- J) J* c' v; s. V  j8 A. t+ s& }, K

6 v- @& M) g" P$ o% Z' {4 W! \: Z4 O
7 Y. v# r1 }% B7 l/ r- f1 m
. R! Q7 `$ `+ g4 D$ H( F1 m2.3 2种方法的对比8 r9 M; q+ d, b  r  l
缓冲区头和bio并不是相互矛盾的,bio只是缓冲区头的一种改善,将以前缓冲区头完成的一部分工作移到bio中来完成。
0 z$ E2 ~% C8 Z# y$ f! }" ?1 i  U( l) P6 G- f
bio中对应的是内存中的一个个页,而缓冲区头对应的是磁盘中的一个块。
0 Y( J  [* m; V0 v4 m8 G) K* d6 o) [' S. e  `6 l
对内核来说,配合使用bio和缓冲区头 比 只使用缓冲区头更加的方便高效。
. J" q: F0 M$ m
$ S/ ~) q' a# j2 V2 Obio相当于在缓冲区上又封装了一层,使得内核在 I/O操作时只要针对一个或多个内存页即可,不用再去管理磁盘块的部分。6 v3 I& A. {" ^7 e: E- B+ y4 i6 k/ W6 v

- F2 N6 d3 W: a. @1 `; y# J: m3 f$ }
0 b1 t& @* |; ~5 v0 }4 Y) Z" Y$ r& t; G" N! b
使用bio结构体还有以下好处:
8 E! E5 d8 b' G6 \/ f. `7 `) y
' k; i9 K) D! l- B+ A) b& |! Kbio结构体很容易处理高端内存,因为它处理的是内存页而不是直接指针$ Y1 R! C0 N/ F) Z: O
bio结构体既可以代表普通页I/O,也可以代表直接I/O% y% B: u) f; J9 }1 k
bio结构体便于执行分散-集中(矢量化的)块I/O操作,操作中的数据可以取自多个物理页面8 p/ x4 h# e! r3 H1 c" C7 a) Q* C4 {
3 M- C! M1 O7 _# M6 E
; v& ]2 K: t+ S% Z* K
3. 内核I/O调度程序. Y+ c8 G/ O2 L+ k  w: g
缓冲区头和bio都是内核处理一个具体I/O操作时涉及的概念。/ u) e! e7 l- Z' a; r2 i# z
  Z: S5 p( a8 m
但是内核除了要完成I/O操作以外,还要调度好所有I/O操作请求,尽量确保每个请求能有个合理的响应时间。
. W+ r7 f" `8 h: q( C0 Z# V- {8 O! M0 h1 Z  w

# n. h% P1 s# V, {$ k8 {3 L3 Y/ M3 D5 T* L
下面就是目前内核中已有的一些 I/O 调度算法。
: G. y4 C( a6 ~- @1 Z+ A: p, G/ J; A# }- {$ O0 d$ m$ B+ W
3.1 linus电梯% S9 n1 {" j. \+ r, j9 L4 `
为了保证磁盘寻址的效率,一般会尽量让磁头向一个方向移动,等到头了再反过来移动,这样可以缩短所有请求的磁盘寻址总时间。
6 h% k6 r; f2 ~  g! B! j5 E- q. l/ w) U6 e" P2 f) z
磁头的移动有点类似于电梯,所有这个 I/O 调度算法也叫电梯调度。
" X/ `- N8 u5 [2 B5 i0 @: `: m, u" f9 }. i( j" A0 _/ q
linux中的第一个电梯调度算法就是 linus本人所写的,所有也叫做 linus 电梯。" m+ b0 E) f. u+ y- S# m! v

7 R; N. e' J% r" w+ ?7 u
, Q/ ?& L6 n3 O5 J( R& s
- y. Q/ b$ G, Ylinus电梯调度主要是对I/O请求进行合并和排序。& f) c) m  k2 ~3 L) i
  I0 v; B  o1 O1 P) j3 n
当一个新请求加入I/O请求队列时,可能会发生以下4种操作:; z* Z9 K" u$ l5 `. X* `% g

7 ?, H# M$ O4 T2 r1 i! Y" ~如果队列中已存在一个对相邻磁盘扇区操作的请求,那么新请求将和这个已存在的请求合并成一个请求
# u$ y# _; X) A9 Y& E* l如果队列中存在一个驻留时间过长的请求,那么新请求之间查到队列尾部,防止旧的请求发生饥饿
$ Z. Z& q( I5 G  s3 X  r如果队列中已扇区方向为序存在合适的插入位置,那么新请求将被插入该位置,保证队列中的请求是以被访问磁盘物理位置为序进行排列的
# P& w4 C6 x$ [, {' \/ E如果队列中不存在合适的请求插入位置,请求将被插入到队列尾部$ n3 W! |5 W7 a! g& W

, Q6 x  a, A$ k3 \5 O% [5 r5 e
, H& t: t( f2 _linus电梯调度程序在2.6版的内核中被其他调度程序所取代了。
* }0 s& c$ K8 u5 @0 o6 y6 L8 W+ K4 n
& L& [+ D! I& e9 F0 i- j1 b0 i
* N0 _, Q6 ?% L9 C6 X
3.2 最终期限I/O调度! ?& g4 y9 }* k8 a0 H% D
linus电梯调度主要考虑了系统的全局吞吐量,对于个别的I/O请求,还是有可能造成饥饿现象。/ O3 a0 X& T1 ]0 I4 ^. ?
# Y4 U& w2 V+ N# M% T$ Q* t) T
而且读写请求的响应时间要求也是不一样的,一般来说,写请求的响应时间要求不高,写请求可以和提交它的应用程序异步执行,
( I" f. o: P& {3 O1 s- m5 K# j# z1 W% x
但是读请求一般和提交它的应用程序时同步执行,应用程序等获取到读的数据后才会接着往下执行。
. L* e$ _' r6 e5 j& A6 K  t$ O, b0 d$ u* L% B: }8 r1 G
因此在 linus 电梯调度程序中,还可能造成 写-饥饿-读(wirtes-starving-reads)这种特殊问题。+ y( v% W- g" z: K) A

& o6 d9 i; ^$ }# N" `
# V" A8 }3 B" S
$ U4 K( Y* b% Y5 y7 T为了尽量公平的对待所有请求,同时尽量保证读请求的响应时间,提出了最终期限I/O调度算法。! R5 e8 W5 {! E( `

- ?$ b- A' v* M最终期限I/O调度 算法给每个请求设置了超时时间,默认情况下,读请求的超时时间500ms,写请求的超时时间是5s
( D* l& r1 J$ w9 j& J7 k) \
: c1 }! X- O7 K6 Y- _但一个新请求加入到I/O请求队列时,最终期限I/O调度和linus电梯调度相比,多出了以下操作:
+ |5 q) \9 Q7 @) H% G$ N9 G
1 q; `& H- m: ~. @新请求加入到 排序队列(order-FIFO),加入的方法类似 linus电梯新请求加入的方法
7 R' Z# d" O9 @  ^根据新请求的类型,将其加入 读队列(read-FIFO) 或者写队列(wirte-FIFO) 的尾部(读写队列是按加入时间排序的,所以新请求都是加到尾部)
; d2 i. o" l; X/ s* {+ l调度程序首先判断 读,写队列头的请求是否超时,如果超时,从读,写队列头取出请求,加入到派发队列(dispatch-FIFO). V  X# u4 q8 T+ p9 C+ y$ t
如果没有超时请求,从 排序队列(order-FIFO)头取出一个请求加入到 派发队列(dispatch-FIFO)
: V8 m2 A  ~( r' z" Q2 Y7 f7 ^派发队列(dispatch-FIFO)按顺序将请求提交到磁盘驱动,完成I/O操作
: G; `& j( C. t0 G1 u0 X; o $ \% I- f( i4 o2 ?
6 B! e' ?, a. o; z- n
最终期限I/O调度 算法也不能严格保证响应时间,但是它可以保证不会发生请求在明显超时的情况下仍得不到执行。( H: o3 X/ I  U, h$ G; U& Q0 O+ g3 V) A
# A* c+ s, l. ?
最终期限I/O调度 的实现参见: block/deadline-iosched.c
1 r8 Y2 u, t1 o% O7 ^
  {4 d; W( k7 V 2 ^* o% M1 f+ N
! }, i! h3 Y# i5 l! s8 @7 @
3.3 预测I/O调度
5 d" H$ n  |4 V) Z: r3 ]最终期限I/O调度算法优先考虑读请求的响应时间,但系统处于写操作繁重的状态时,会大大降低系统的吞吐量。
6 H% u  w" P9 n* f3 B
+ r% j: s; }  \  u& T* p# t因为读请求的超时时间比较短,所以每次有读请求时,都会打断写请求,让磁盘寻址到读的位置,完成读操作后再回来继续写。% h( o" H- ]' R* E1 ?

/ U: I& d9 I2 }/ L$ Y这种做法保证读请求的响应速度,却损害了系统的全局吞吐量(磁头先去读再回来写,发生了2次寻址操作)# I! U. y, s1 {+ F$ x

/ q' V/ t( H4 v7 w
4 F3 Z! _. k0 I# M9 {% {
- S) E" n$ W5 }2 }7 j6 r预测I/O调度算法是为了解决上述问题而提出的,它是基于最终期限I/O调度算法的。
- S$ I* M" {( I' ]1 p4 E9 f  R& d2 a, Y8 }: u* ^$ K
但有一个新请求加入到I/O请求队列时,预测I/O调度与最终期限I/O调度相比,多了以下操作:2 i6 r( F/ b$ |; n5 M" Y
  e; _9 |0 `& E5 H
新的读请求提交后,并不立即进行请求处理,而是有意等待片刻(默认是6ms)/ D9 i, C: z- O$ h- \: y
等待期间如果有其他对磁盘相邻位置进行读操作的读请求加入,会立刻处理这些读请求# z% v, L3 E$ R8 \
等待期间如果没有其他读请求加入,那么等待时间相当于浪费掉
  r+ a* i5 Y  N) i) E  N等待时间结束后,继续执行以前剩下的请求2 Q' O4 E5 c6 Q+ `
% O8 ~: d6 M+ `6 j' I

/ n" P1 t/ W) h预测I/O调度算法中最重要的是保证等待期间不要浪费,也就是提高预测的准确性,) s: d! V2 W$ P; v* T

# b/ f4 i: @6 b# o- N9 P目前这种预测是依靠一系列的启发和统计工作,预测I/O调度程序会跟踪并统计每个应用程序的I/O操作习惯,以便正确预测应用程序的读写行为。1 f1 a) @3 Z/ D2 g2 b

! f7 n1 j- b, o9 G( b7 c3 I& _
5 G7 _: i% I- V7 a- Y* b5 R8 W1 r0 I' w: X% s+ |5 s
如果预测的准确率足够高,那么预测I/O调度和最终期限I/O调度相比,既能提高读请求的响应时间,又能提高系统吞吐量。8 j6 x, h$ K/ L
3 @1 J1 S2 Z  F* c+ l% d6 e
预测I/O调度的实现参见: block/as-iosched.c
" d/ x9 V9 _5 f. ^% o. u0 w- P+ L/ K4 F
: Y5 D8 L$ O, M

' W4 i2 T* [# U* _% [8 ]% k7 }  M注:预测I/O调度是linux内核中缺省的调度程序。& T3 a, K/ |3 f; r+ `0 |3 z, L
  H) z5 n% {. }1 A1 l6 u

1 w4 ]2 j6 Q; W( g% ]
+ a9 e* B& [+ X6 h4 _5 v1 L: Q3.4 完全公正的排队I/O调度3 _2 d- X$ x+ q3 y* u- d: i- C
完全公正的排队(Complete Fair Queuing, CFQ)I/O调度 是为专有工作负荷设计的,它和之前提到的I/O调度有根本的不同。
5 ^" f4 G& l  m3 }; j! W$ V' y
; `$ u$ q- j; c5 WCFQ I/O调度 算法中,每个进程都有自己的I/O队列,
% c  V" i2 D0 P3 ]: `* I  |$ {; z) n% \% A, x' B7 D( I% t- n
CFQ I/O调度程序以时间片轮转调度队列,从每个队列中选取一定的请求数(默认4个),然后进行下一轮调度。( ~# X' w6 F, R
6 a- i6 C/ N: E
9 }: ^4 B; n) J. G/ w4 x
2 o, z, D+ o4 c- x
CFQ I/O调度在进程级提供了公平,它的实现位于: block/cfq-iosched.c" V& O7 O" y* v1 ?) }

/ l: ~; C1 H6 K, q / Y% U) f" r) S7 J: c

7 T" Q0 j* M2 Z" Y3.5 空操作的I/O调度
9 V! I' X8 ]# T& ]4 @空操作(noop)I/O调度几乎不做什么事情,这也是它这样命名的原因。
+ r# [9 H7 R& m$ e
+ S' _$ V: A" {9 D空操作I/O调度只做一件事情,当有新的请求到来时,把它与任一相邻的请求合并。2 F9 |8 I* k1 }! B6 M* C

# X0 `: z0 }# U! `7 _) A
) j: |, r% d" @9 _
! y, h, _" j& c0 S5 w空操作I/O调度主要用于闪存卡之类的块设备,这类设备没有磁头,没有寻址的负担。. M$ m. @" x! d

# W+ W# `% x3 h2 i  Q8 r! J5 R空操作I/O调度的实现位于: block/noop-iosched.c
7 \, w' L- T7 o  X8 S7 N
0 k5 ]5 X' u+ j9 ?
$ H' w1 J5 d% A( h; ?( H
% u# U' G3 x3 V7 u: l. e3.6 I/O调度程序的选择
) _; ], M, m" J7 Z$ ^: ]# ^9 _3 B2.6内核中内置了上面4种I/O调度,可以在启动时通过命令行选项 elevator=xxx 来启用任何一种。
; w) U& r* P! b3 i' c* f' g0 U% ?5 U, E. L
elevator选项参数如下:
参数
I/O调度程序
as预测
cfq完全公正排队
deadline最终期限
noop空操作
如果启动预测I/O调度,启动的命令行参数中加上 elevator=as

作者: NingW    时间: 2021-1-27 10:44
Linux内核设计与实现之块I/O层




欢迎光临 EDA365电子论坛网 (https://bbs.eda365.com/) Powered by Discuz! X3.2