EDA365电子论坛网

标题: 五种I/O 模式 [打印本页]

作者: mutougeda    时间: 2020-10-21 13:57
标题: 五种I/O 模式
  R# `' z9 E# f+ [
在Linux/UNIX 下,有下面这五种I/O 操作方式:$ ~3 J+ o( \: H
   阻塞I/O8 n9 }7 W8 b7 }& W0 b; q- F+ r5 U
   非阻塞I/O
/ j" K4 `+ }" Z  P/ t5 w' M/ b   I/O 多路复用& b' f7 z* D+ U# p/ K
   信号驱动I/O(SIGIO)& \' E* [5 T4 H1 U+ V' s( h; k+ M
   异步I/O
# X. R# U& t' e& X程序进行输入操作有两步:
% e% F* x1 E) M) d) ?) |9 d9 K; Q1 H   等待有数据可以读; U; W1 [3 c: W, _; ], H7 }" Q6 Q
   将数据从系统内核中拷贝到程序的数据区。6 W  D7 O% @- {3 g1 T
对于一个对套接字的输入操作:
, v* d5 }* |4 h' F0 m. d6 ]     第一步一般来说是,等待数据从网络上传到本地,当数据包到达的时候,数据将会从网络层拷贝到内核的缓存中;# O% n; s+ \% R1 w1 b$ p3 S
     第二步是从内核中把数据拷贝到程序的数据区中
2 f8 Q5 a4 n# Q
0 S* S  z8 l4 a) K$ P/ C9 u$ ^5 m% D: x' d' m! a
.阻塞I/O 模式
6 |# x4 ~- r- x  H* N3 a     简单的说,阻塞就是"睡眠"的同义词  E# x8 {! j4 I& |5 n7 K* Q
     如你运行上面的listen 的时候,它只不过是简单的在那里等待接收数据。它调用recvfrom()函数,但是那个时候(listener 调用recvfrom()函数的时候),它并没有数据可以接收.所以recvfrom()函数阻塞在那里(也就是程序停在recvfrom()函数处睡大觉)直到有数据传过来阻塞.你应该明白它的意思。$ Z: {* O/ S$ [4 R3 g, h! A
     阻塞I/O 模式是最普遍使用的I/O 模式。大部分程序使用的都是阻塞模式的I/O 。+ y. y# e5 J$ r0 f( l6 K
     缺省的,一个套接字建立后所处于的模式就是阻塞I/O 模式。4 r8 i. v  @$ N9 z& W
     对于一个UDP 套接字来说,数据就绪的标志比较简单:
0 |: ~. S! C0 ~8 O$ k* I         已经收到了一整个数据报
" v+ D* e, }+ }6 k         没有收到。- z. W0 {- }. D
     而TCP 这个概念就比较复杂,需要附加一些其他的变量
- I3 S9 w" y2 A' c/ f3 v' A         一个进程调用recvfrom ,然后系统调用并不返回知道有数据报到达本地系统,然后系统将数据拷贝到进程的缓存中。
$ d' m* N# p' k$ U        (如果系统调用收到一个中断信号,则它的调用会被中断)我们称这个进程在调用recvfrom 一直到从recvfrom 返回这段时间是阻塞的。. l. `- Y1 M3 |3 V( Y* U. Y
         当recvfrom正常返回时,我们的进程继续它的操作。
" c  k; g6 h$ m0 f' d4 }1 w3 q( e% e: e& ^! d

; c2 B- ~! A( d# m- J$ ]
5 G1 z- Q5 l6 Y.非阻塞模式I/O* X" _2 P$ q& p" K9 m3 v
    当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”: L$ o' M% ?. R) E5 E% {, Y' N
   
) L5 i# T4 D& C' G% e    如我们开始对recvfrom 的三次调用,因为系统还没有接收到网络数据,所以内核马上返回一个EWOULDBLOCK的错误。& ]8 P, T8 ], \; H& k5 }
    第四次我们调用recvfrom 函数,一个数据报已经到达了,内核将它拷贝到我们的应用程序的缓冲区中,然后recvfrom 正常返回,我们就可以对接收到的数据进行处理了。
; b2 W# W1 g4 c+ c  
' z5 L+ _8 n% S+ \  ^% c; _3 D    当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做polling)。
6 n* ~$ G: X2 t& a    应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。这种模式使用中不是很普遍9 ~& \9 n! B$ n* B  r% \2 N
9 X2 h* E( _  H7 u2 p: J7 o

7 x/ T4 L) g* U( h+ j* [8 V! v( h& n- M7 J# ]5 _# w( i4 Y7 T$ Q

0 f/ L5 _9 r4 e/ z% l6 [9 Q: S1 R& @+ e  C" Z% V
.I/O 多路复用 select()
" L; j3 X3 J0 v    在使用I/O 多路技术的时候,我们调用select()函数和poll()函数,在调用它们的时候阻塞,而不是我们来调用recvfrom(或recv)的时候阻塞。
! f6 ^( ]6 t5 `* B    当我们调用select 函数阻塞的时候,select 函数等待数据报套接字进入读就绪状态。当select 函数返回的时候,也就是套接字可以读取数据的时候。这时候我们就可以调用recvfrom函数来将数据拷贝到我们的程序缓冲区中。
/ \$ m: s4 c% r( W5 \    和阻塞模式相比较,select()和poll()并没有什么高级的地方,而且,在阻塞模式下只需要调用一个函数:读取或发送,在使用了多路复用技术后,我们需要调用两个函数了:先调用select()函数或poll()函数,然后才能进行真正的读写。% k9 Z' o0 r) r; }2 z2 `3 b, k
    7 ~; e" s5 ~, `' H! s0 p, y: G
    多路复用的高级之处在于,它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回
4 S, {5 R# K  j3 L& l+ ~  p3 D4 a    假设我们运行一个网络客户端程序,要同时处理套接字传来的网络数据又要处理本地的标准输入输出。在我们的程序处于阻塞状态等待标准输入的数据的时候,假如服务器端的程序被kill(或是自己Down 掉了),那么服务器程端的TCP 协议会给客户端(我们这端)的TCP 协议发送一个FIN 数据代表终止连接。但是我们的程序阻塞在等待标准输入的数据上,在它读取套接字数据之前(也许是很长一段时间),它不会看见结束标志.我们就不能够使用阻塞模式的套接字。' h: q. T* P* {$ q# J
    I/O多路技术一般在下面这些情况中被使用:# p8 l6 A; a3 N" r4 S& W( l* O
       当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是标准的输入输出和网络套接字), I/O 多路复用技术将会有机会得到使用。
) a1 Z1 h  S* A, V       当程序需要同时进行多个套接字的操作的时候。( g  [! Y% u5 f9 c5 m+ m
       如果一个TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套接字。
! C3 m, L2 h  f* o! Y, C# @4 i- ~       如果一个服务器程序同时使用TCP 和UDP 协议。
: ~* c) U% n2 Y, h# j: b       如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如inetd就是这样的)。. A# r7 c+ L9 o) c
  
1 U) x. }9 y9 ~* T# b: f    I/O 多路服用技术并不只局限与网络程序应用上。几乎所有的程序都可以找到应用I/O多路复用的地方。, m/ C% u: Q. s6 {- |5 k( E
6 i6 L: x/ T) n' ^6 i6 _
0 ^1 J& B' y9 c' Y7 }, _
) `( g) P# B6 }. [6 F
fcntl()函数
8 x& M2 [5 x6 H+ C% n3 U         当你一开始建立一个套接字描述符的时候,系统内核就被设置为阻塞状态。如果你不想你的套接字描述符是处于阻塞状态的,那么你可以使用函数fcntl()。
* w) W9 `0 t0 ^* b) x+ e3 [' k% B     #include
, w/ t4 T$ x8 k& f     #include
' l- K* x" V* P+ i* x- H: a7 A- N     int fcntl (int fd, int cmd, long arg);
  f4 c' T% {. Y示例:
* s# ^, K% j. }2 B8 R( `, O9 K     sockfd = socket(AF_INET, SOCK_STREAM, 0);8 }5 M8 x; L! K5 A+ L& h
     fcntl(sockfd, F_SETFL, O_NONBLOCK);
/ f0 s$ X  f7 x( g     这样将一个套接字设置为无阻塞模式后,你可以对套接字描述符进行有效的“检测”.2 o, g  r6 P* Y" j( Q% Y, O
     如果你尝试从一个没有接收到任何数据的无阻塞模式的套接字描述符那里读取数据,那么读取函数会马上返回–1 代表发生错误,全局变量errno 中的值为EWOULDBLOCK。
( B/ _# R4 o# c/ n3 n     一般来说,这种无阻塞模式在某些情况下不是一个好的选择。假如你的程序一直没有接收到传过来的数据,那么你的程序就会进行不停的循环来检查是否有数据到来,浪费了大量的CPU 时间,而这些CPU 时间本来可以做其他事情的。
* z* O2 s5 ?( q6 }0 C" Q     另外一个比较好的检测套接字描述符的方法是调用select()函数+ k" F( r' ?* t1 i: x
7 b2 c  I; P( T" }& [3 w" s

; \. k. ~1 L) f  m) E
- l, c6 k" n) r- @4 `) u
1 t* Q, y/ F+ r
. k2 \8 |9 h% d  l套接字选择项select()函数
& w6 S& _" o' q* x9 X/*int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);% P, D8 x4 r, |3 _3 M$ ~: Y, v
*
% Y# q; u5 B2 b5 ?9 ^0 K8 C*  这个技术有一点点奇怪但是它对我们的程序确是非常有用的。
9 `/ ^+ E1 k/ D- `" v, L" a0 N*  假想一下下面的情况:1 P% h/ V  f7 a
*      你写的服务器程序想监听客户端的连接,但是你同时又想从你以前已经建立过的连接中来读取数据。
( @  k3 Y/ r5 ?9 }& B$ {5 L*  你可能会说:“没有问题,我不就是需要使用一个accept()函数和一对儿recv()函数吗?”。
3 T; l: j# p- u! x*  不要这么着急,你要想想,当你调用accept()函数阻塞的时候,你还能调用recv()函数吗?, G4 R' G& M. r' o. I: B3 c; [
*  “使用非阻塞套接字!”你可能会这么说。是的,你可以。但是如果你又不想浪费宝贵的CPU 时间,该怎么办呢?
" D; ^9 ^/ c- {; g+ E*  Select()函数可以帮助你同时监视许多套接字。它会告诉你哪一个套接字已经可以读取数据,4 @& c2 ]5 t$ A% B% k0 C
*  哪个套接字已经可以写入数据,甚至你可以知道哪个套接字出现了错误,如果你想知道的话。
$ c. ?! Z* c( H* , F- O2 u4 J8 A1 l$ D
*  & u& I* z8 O9 E, z" T! {
*5 ?: d, }: f# ~& H3 _$ J( B
* 当select()函数返回的时候,readfds 将会被修改用来告诉你哪一个文件描述符你可以用来读取数据。! X# }8 [! R" ]2 {3 D
*" E1 U0 @2 M+ y$ h3 ]/ O  W
* numfds         是readfds,writefds,exceptfds 中fd 集合中文件描述符中最大的数字加上1 也就是sockfd+1(因为标准输入的文件描述符的值为0 ,所以其他任何的文件描述符都会比标准输入的文件描述符大)。
8 }1 J& Z) H8 m( o& g. `0 R( ]*
% o- a! t9 M) ]: D5 F; O( u/ n* readfds        中的fd 集合将由select 来监视是否可以读取,如果你想知道是是否可以从标准输入和一些套接字(sockfd)中读取数据,你就可以把文件描述符和sockfd 加入readfds 中。& {! p; y- ?2 y# P9 P
* writefds       中的fds 集合将由select 来监视是否可以写入
# U* v4 `9 D! @/ v& q$ \* exceptfds      中的fds 集合将由select 来监视是否有例外发生
& E. J. @% P& e* struct timeval 超时设置。
6 v) c. Y& g1 g*                    一般来说,如果没有任何文件描述符满足你的要求,你的程序是不想永远等下去的.也许每隔1 分钟你就想在屏幕上输出信息:“hello!”。
; M% L/ T" q- z! q( G( G5 ?* |/ Y*                这个代表时间的结构将允许你定义一个超时。! U9 z1 R; Q, v( S& k
*                在调用select()函数中,如果时间超过timeval 参数所代表的时间长度,
. v5 n! n# Y- U" E+ s*                而还没有文件描述符满足你的要求,那么select()函数将回返回,允许你进行下面的操作。
9 c6 w: G7 Y* S4 H*                只需要将tv_sec 设置为你想等待的秒数,然后设置tv_usec 为想等待的微秒数7 F; k4 A5 b4 Q8 s6 w- g1 {" y* u
*                (真正的时间就是tv_sec 所表示的秒数加上tv_usec 所表示的微秒数).注意,是微秒(百万分之一)而不是毫秒.
6 ?8 V, S" j* r7 m# Q! x*                一秒有1,000 毫秒,一毫秒有1,000 微秒。所以,一秒有1,000,000 微秒.
* G6 w4 O; g3 T( S1 ~3 H0 b5 ~  r*                这个timeval 结构定义如下:
2 |+ N+ ]2 G: T: }& Z! s*                struct timeval5 X" D8 o* O$ I, p
*                {
, n& h. R/ ~6 T  Z( I3 l*                    int tv_sec ;   //秒数& H2 n3 f$ y( E- ^% V  b  M) o
*                    int tv_usec ;  //微秒6 e! u& P8 h4 R2 c- b
*                };+ A( _9 c# M. a" J
*                我们拥有了一个以微秒为单位的记时器!但是因为Linux 和UNIX 一样,最小的时间片是100 微秒,所以不管你将tv_usec 设置的多小,实质上记时器的最小单位是100微秒.
9 p6 q3 P( J- L8 P7 d" [*
3 e4 p; p# ]8 e& q- p: N*                如果你将struct timeval 设置为0,则select()函数将会立即返回,同时返回在你的集合中的文件描述符的状态。
7 d% `& N; Q% J' L7 m*
" P9 ?; r0 H/ H8 t- A*                如果你将timeout 这个参数设置为NULL,则select()函数进入阻塞状态,除了等待到文件描述符的状态变化,否则select()函数不会返回。) [* ^& m- e& M& A
*
% e2 d7 X& F1 T*# {$ b: j' X: ~, d
* return        当select()函数返回的时候,timeval 中的时间将会被设置为执行为select()后还剩下的时间。  X( F0 I2 d4 [* R5 m" Y
*7 k: w1 F6 \) _2 u# q4 z5 ~
*- ?5 k- f+ x  Y% \2 p
*3 \4 T4 t5 b+ H3 l9 `* J
*/4 {9 `- @8 ^4 m2 i- v, ^# ~1 @
6 Q8 z9 V( P5 D: }

* C6 `0 O( k6 q使用FD_ISSET() 宏,你可以选出select()函数执行的结果。
5 P  I1 k$ r. p$ H) z在进行更深的操作前,我们来看一看怎样处理这些fd_sets。下面这些宏可以是专门进行这类操作的:7 ?! o2 B( M9 F7 @
  FD_ZERO(fd_set *set)           将一个文件描述符集合清零9 c3 j6 M4 |+ c& V4 j0 Q
  FD_SET(int fd, fd_set *set)    将文件描述符fd 加入集合set 中。% e3 o* K  e2 C/ |+ _8 M) W8 B( z
  FD_CLR(int fd, fd_set *set)    将文件描述符fd 从集合set 中删除.6 V: t2 F$ H. D) p7 y1 @! f
  FD_ISSET(int fd, fd_set *set)  测试文件描述符fd 是否存在于文件描述符set 中.9 u& m) M1 @4 V) L- H
下面这段代码演示了从标准输入等待输入等待2.5 秒.) o: r5 [' j4 A( j8 Q$ a7 ?
#include $ {+ O3 @6 t0 W% O3 U- l1 @0 ~- p3 }
#include
+ D0 f( O  A) k. V0 Q, ]; h#include
3 a& @2 U* `6 x5 X: T! V7 ^/* 标准输入的文件描述符数值 */& }6 t: M" s4 X1 _) W
#define STDIN 0, E" S& n: Z% L1 x% `( ^9 T
main()
7 J0 ^- W: f8 l{
2 i* w/ e0 D: b8 P: v. H7 h# \   fd_set readfds;
% _* Y/ z7 \% A" D: E1 z   struct timeval tv;
! {8 v% u! K" R- a8 e' D! F   /* 设置等待时间为2 秒零500,000 微秒 */- U2 Y# _; r' N
   tv.tv_sec  = 2;( {, ]* G( `& C  t+ u
   tv.tv_usec = 500000;
) }" j( |1 F% {1 c3 u$ y   FD_ZERO(&readfds);
8 p. C5 ]% b- Z* C; h; C   FD_SET(STDIN, &readfds);
2 ?6 S, k" o' t: U& W  a   /* 因为我们只想等待输入,所以将writefds 和execeptfds 设为NULL */4 D; m7 ^/ R; U9 T  m8 l3 B
   /* 程序将会在这里等待2 秒零500,000 微秒,除非在这段时间中标准输入有操作 */8 x5 ^( E5 w8 t
   select(STDIN+1, &readfds, NULL, NULL, &tv);
, V) e2 W+ J" P   /* 测试ST( }/ v) O7 V  x  `% O( i7 r

' E6 ?' T( Y; x, O
$ m' a" F- c$ X. ?, D% ~7 S
* J7 R3 N  x1 H( R  E: \
作者: youOK    时间: 2020-10-21 14:48
五种I/O 模式




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