标题: 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
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