|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
本帖最后由 thinkfunny 于 2020-4-17 18:35 编辑
0 I ~5 v/ l2 H1 i- k; e. q: X# d2 }' ]; q$ C2 m: L
以下内容的分析是基于2.6.32及其后的内核.
1 |2 u% c* f/ `. B# j1 e* i! J, f$ Q1 C
我们在linux上总是要保存数据,数据要么保存在文件系统里(如ext3),要么就保存在裸设备里。我们在使用这些数据的时候都是通过文件这个抽象来访问的,操作系统会把我们需要的数据提交给我们,而我们则无需和块设备打交道。
% U, p9 `7 Z9 C4 y" C6 l5 ^7 m O# x1 {0 `* {( c+ M
从下图,我们可以清除的看到:
" q0 x8 C2 T( W; L# D, i8 W& z4 ~6 p5 M3 P3 }7 O" c, [" G( T
5 P3 \5 o. O; K u9 \( @
. q2 A/ A9 t* N8 w' \& S
I/O子系统是个层次很深的系统,数据请求从用户空间最终到达磁盘,经过了复杂的数据流动。
( B5 O3 L& d. L8 w0 d
% M* m. \; P; M, l对设驱开发人员或与此相关的设计人员,特别是IO很密集,我们就需要搞清楚IO具体是如何动作的,免得滥用IO和导致设计问题。
; Z" }$ |3 k& I) c; m
$ w. [& b( ~2 H( _# u$ E1 H
6 |8 D! ]/ R4 P% o! g( ^. V
7 l C$ q0 `- s7 AIBM developworks中,〈read系统调用剖析〉阐述就很清楚。' z2 c6 a' X( _: j- k% A
7 J2 d7 @+ }0 g: @8 u( A5 iread系统调用的处理分为用户空间和内核空间处理两部分。其中,用户空间处理只是通过0x80中断陷入内核,接着调用其中断服务例程,即sys_read以进入内核处理流程。5 ?! U; ?2 e c
( ]/ X2 a+ h) r' {8 L
对于read系统调用在内核的处理,如上图所述,经过了VFS、具体文件系统,如ext2、页高速缓冲存层、通用块层、IO调度层、设备驱动层、和设备层。其中,VFS主要是用来屏蔽下层具体文件系统操作的差异,对上提供一个统一接口,正是因为有了这个层次,所以可以把设备抽象成文件。具体文件系统,则定义了自己的块大小、操作集合等。引入cache层的目的,是为了提高IO效率。它缓存了磁盘上的部分数据,当请求到达时,如果在cache中存在该数据且是最新的,则直接将其传递给用户程序,免除了对底层磁盘的操作。通用块层的主要工作是,接收上层发出的磁盘请求,并最终发出IO请求(BIO)。IO调度层则试图根据设置好的调度算法对通用块层的bio请求合并和排序,回调驱动层提供的请求处理函数,以处理具体的IO请求。驱动层的驱动程序对应具体的物理设备,它从上层取出IO请求,并根据该IO请求中指定的信息,通过向具体块设备的设备控制器发送命令的方式,来操纵设备传输数据。设备层都是具体的物理设备。; \( L% p. L# R2 C0 G5 ?
6 i( x, ~# r$ g1 X0 i: X5 B; h& x
( {- w5 F/ ~$ x
8 J! }$ q5 ~1 O+ ]( s9 IVFS层:
5 K6 `4 s$ a6 b
5 \+ Q0 b1 G% G- y; C5 G内核函数sys_read是read系统调用在该层的入口点。6 z' L. ?4 o3 X5 r8 w8 r5 A
2 U6 o" r; S: H5 P$ T$ f它根据文件fd指定的索引,从当前进程描述符中取出相应的file对象,并调用vfs_read执行文件读取操作。
) d$ t7 G- J5 }& o
4 t( Y* s/ M, L! o0 Bvfs_read会调用与具体文件相关的read函数执行读取操作,file->f_op.read。, c: l6 F- e) @! j/ u
) V$ a) v. |' W3 J% B
然后,VFS将控制权交给了ext2文件系统。(ext2在此作为示例,进行解析)
/ F. G: P G- _5 a% g! O& U4 m4 h5 h, f* w2 D8 {. o4 A6 \# z6 @) X
# o8 x% \. u7 c+ P
0 ~0 ~! c/ S6 S0 d# ^
Ext2文件系统层的处理
& x& V8 z9 A0 S+ ~! V
6 f1 x* E$ L* f: V- m# u通过ext2_file_operations结构知道,上述函数最终会调用到do_sync_read函数,它是系统通用的读取函数。所以说,do_sync_read才是ext2层的真实入口。
( d+ v, ^ V3 W4 v7 Z* ^3 b" S% \+ y* ~& ^
该层入口函数 do_sync_read 调用函数 generic_file_aio_read ,后者判断本次读请求的访问方式,如果是直接 io (filp->f_flags 被设置了 O_DIRECT 标志,即不经过 cache)的方式,则调用 generic_file_direct_IO 函数;如果是 page cache 的方式,则调用 do_generic_file_read 函数。它会判断该页是否在页高速缓存,如果是,直接将数据拷贝到用户空间。如果不在,则调用page_cache_sync_readahead函数执行预读(检查是否可以预读),它会调用mpage_readpages。如果仍然未能命中(可能不允许预读或者其它原因),则直接跳转readpage,执行mpage_readpage,从磁盘读取数据。
! ^- q8 r i$ o+ Y
5 @2 E1 o& I9 ?; }/ h在mpage_readpages(一次读多个页)中,它会将连续的磁盘块放入同一个BIO,并延缓BIO的提交,直到出现不连续的块,则直接提交BIO,再继续处理,以构造另外的BIO。
1 j, o! c. F; ~' H* \: _5 H& ~% u! i# G
+ V$ p) g; q* ^, K
" d5 ~+ w1 N5 J( t文件的 page cache 结构
9 J" L/ ~) O( e% g
0 Z3 w1 [, W" a/ h图5显示了一个文件的 page cache 结构。文件被分割为一个个以 page 大小为单元的数据块,这些数据块(页)被组织成一个多叉树(称为 radix 树)。树中所有叶子节点为一个个页帧结构(struct page),表示了用于缓存该文件的每一个页。在叶子层最左端的第一个页保存着该文件的前4096个字节(如果页的大小为4096字节),接下来的页保存着文件第二个4096个字节,依次类推。树中的所有中间节点为组织节点,指示某一地址上的数据所在的页。此树的层次可以从0层到6层,所支持的文件大小从0字节到16 T 个字节。树的根节点指针可以从和文件相关的 address_space 对象(该对象保存在和文件关联的 inode 对象中)中取得(更多关于 page cache 的结构内容请参见参考资料)。
+ m" a5 R4 p" U' V& d
. G$ `# p8 N: r' g$ P+ N# Y8 z5 ]
: a6 H: G* r& ?# ?) X% ?4 I) n! b o
w9 ?4 \2 M, ?: |, j9 r: F5 q- Y0 @mpage处理机制就是page cache层要处理的问题。
! G) N- F" [, Q8 F V/ q4 X/ _5 y$ q" t: c9 T# E4 s
1 ^ n, S. j' P8 ?) Q) t) s
: s8 ]5 K7 ^0 k Y5 [$ d! M通用块层
" P, ^' n& u* I
- V) ]+ d' t4 U* |0 a. a) x: D% W* @在缓存层处理末尾,执行mpage_submit_bio之后,会调用generic_make_request函数。这是通用块层的入口函数。
+ g: F0 M t! @7 T( P( K4 s# I# E3 O/ Z3 Y
它将bio传送到IO调度层进行处理。
% c( T) N- A; y3 ?) m5 E5 e$ v$ k" |2 T) O/ L% K# g u
& N* e) E9 d6 I6 ~* w( \( n
/ p* s- |' Y9 g. s: T# \0 rIO调度层 t) x# x/ k/ l: F, J: ]
( n3 K% W" \$ `; `; W. ]
对bio进行合并、排序,以提高IO效率。然后,调用设备驱动层的回调函数,request_fn,转到设备驱动层处理。
4 U9 ^( X: J" t& {* y- c5 ^! y
8 I9 T/ a2 L2 F) M5 S; J: j+ S: ?/ I( e
设备驱动层6 ~6 c+ w' f- Y$ |6 Y$ T
6 t4 A6 F) {" M, {) G
request函数对请求队列中每个bio进行分别处理,根据bio中的信息向磁盘控制器发送命令。处理完成后,调用完成函数end_bio以通知上层完成。- ?, @& O; F7 t
$ p( i8 Y+ G* o2 ]% s, f
1 c D8 I7 p& \& e+ Q2 f$ X
Q$ F- w: E/ s7 k% \/ Y8 a+ E8 D( R. n, B8 W# M
|
|