I/O模式概述
阻塞I/O 非阻塞I/O I/O多路复用 信号驱动I/O 异步I/O
五程I/O模式区别图:
阻塞和非阻塞
阻塞函数在完成其指定的任务以前不允许程序调用另一个函数。例如,程序执行一个读数据的函数调用时,在此函数完成读操作以前将不会执行下一程序语句。当服
务器运行到accept语句时,而没有客户连接服务请求到来,服务器就会停止在accept语句上等待连接服务请求的到来。这种情况称为阻塞
(blocking)。而非阻塞操作则可以立即完成。比如,如果你希望服务器仅仅注意检查是否有客户在等待连接,有就接受连接,否则就继续做其他事情,则
可以通过将Socket设置为非阻塞方式来实现。非阻塞socket在没有客户在等待时就使accept调用立即返回。
#include
#include
……
sockfd =
socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
……
通过设置socket为非阻塞方式,可以实现"轮询"若干Socket。当企图从一个没有数据等待处理的非阻塞Socket读入数据时,函数将立即返回,
返回值为-1,并置errno值为EWOULDBLOCK。但是这种"轮询"会使CPU处于忙等待方式,从而降低性能,浪费系统资源。而调用
select()会有效地解决这个问题,它允许你把进程本身挂起来,而同时使系统内核监听所要求的一组文件描述符的任何活动,只要确认在任何被监控的文件
描述符上出现活动,select()调用将返回指示该文件描述符已准备好的信息,从而实现了为进程选出随机的变化,而不必由进程本身对输入进行测试而浪费
CPU开销。
1.阻塞I/O --- 最普遍使用的I/O模式。缺省的,一个套接字建立后所处于的模式即是阻塞I/O模式。
图例如下:
上图中,一个进程调用recvfrom 若没有数据报到达本地系统,刚阻塞。直接有数据报到达,才返回。
2.非阻塞I/O ---
设置套接字为非阻塞模式,相当于告诉系统:当请求的I/O操作不能马上完成,不进行休眠等待,马上返回一个错误.
图例如下:
上图中,三次recvfrom调用,仅一次正常返回,其余返回一个EWOULDBLOCK的错误。
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环不停的测试是否一个文件描述符有数
据可读(称做polling).
应用程序不停的polling来检查是否I/O操作已经就绪。这将浪费CPU资源,所有此模式不常用。
3.I/O多路复用
调用select()函数和poll()函数,调用时阻塞,而不是来调用recv的时候阻塞。
当调用select函数阻塞时,select函数等待数据报套接字进入读就绪状态。当select函数返回时,即套接字可以读取数据的时候,就可以调用recvfrom函数来接收数据(将数据拷贝到程序缓冲区)。
多路复用优点:同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()就可以返回。
图例如下:
应用:假设运行一个网络客户端程序,要同时处理套接字传来的网络数据,又要处理本地标准输入输出。当程序处于阻塞状态等待标准输入的数据时,假如服务器端
的程序被kill,那么服务器端的TCP协议会给客户端的TCP协议发送一个FIN数据代表终止连接。但我们这端的程序阻塞等待标准输入的数据上,在它读
取套接字数据之前,它不会看见结束标志。
IO多路技术一般在下面情况中应用:
当一个客户端需要同时处理多个文件描述符的输入输出操作时。
当程序需要同时进行多个套接字的操作时
TCP服务程序同时处理正在侦听网络连接和已经连接好的套接字时。
一个服务器程序同时使用TCP和UDP协议。
一个服务器同时用多种服务并且服务协议不同如:inetd
4.信号驱动I/O模式
使用信号,让内核在文件描述符就绪的时候使用SIGIO信号来通知我们的模式。
首先需要允许套接字使用信号驱动I/O,安装SIGIO的函数.
此程模式,系统调用立即返回,然后程序做其它事情,当数据准备就绪时,系统向进程发送SIGIO信号。在信号处理函数中进行I/O操作。
图例如下:
信号I/O可以使内核在某个文件描述符发生改变的时候发信号通知我们的程序。异步I/O可以提高我们程序进行I/O读写效率。通过使用它,当我们的程序进
行I/O操作的时候,内核可以在初始化I/O操作后立即返回,在进行I/O操作的同时,我们的程序可以做其它事情,真到I/O结束,内核给我们的程序发送
消息通知。
应用实例:如果一个正在进行读写操作的TCP套接字处于信号驱动I/O状态下,那么每当新数据到达本地的时候,将会产生一个SIGIO信号,每当本地套接
字发出的数据被远程确认后,也会产生一个SIGIO信号。对于我们的程序来讲,是无法区分这二个SIGIO有什么区别。在此种情况下使用
SIGIO,TCP套接字应当被设置为无阻塞模式来阻止一个阻塞的read/write/recv/send操作。所以我们考虑一个只进行监听网络连接操
作的套按字上使用异步I/O。
5.异步I/O模式
当我们运行在异步I/O模式下,如果想进行I/O操作,只需要告诉内核我们要进行I/O操作,然后内核马上返回。具体的I/O和数据拷贝全部由内核来完成,我们程序继续执行。当内核完成所有的I/O操作,内核将通知我们程序。
异步I/O和信号驱动I/O区别:
信号驱动I/O模式下,内核在操作可以被操作的时候通知我们的应用程序发送SIGIO信号
异步I/O模式下,内核在所有I/O操作的已经被内核操作结束后才会通知我们的应用程序。
图例如下:
上例中:当我们进行一个IO操作时,传递给内核的文件描述符,我们的缓存区指针和缓存区的大小,一
个偏移量offset,以及在内核结束所有操作后和我们联系的方法。这种调用也是立即返回的,我们的程序
不需要阻塞来等待数据的就绪。我们可以要求系统内核所有的操作结束后(包括从网络上读取信息,然后
拷贝到我们提供给内核的缓存区中)给我们发一个消息。
套接字选项select()函数
int select(int n, fd_set * readfds, fd_set *
writefds, fd_set * exceptfds,
struct time-val * timeout); 用来等待文件描述词状态的改变。
参数n代表readfds,writefds,exceptfds中fd集合中文件描述符中最大的文件描述符数字加1
参数readfds
中的fd集合将由select来监视是否可以读取
writefds 中的...是否可以写入
exceptfds 中的...是否有例外发生
如果你想知道是否可以从标准输入和一些套接字sockfd中读取数据,你就可以把标准输入的文件描述符和相应的sockfd加入readfds中。n的值
设成readfds中文件描述符中最大的那个数字加一,也就是sockfd+1(因为标准输入的文件描述符值为0,所以其它任何的文件描述符都会比0值
大)..当select返回时,readfds将被修改,指示某个文件描述符已经准备被读取,你可以通过FD_ISSSET()宏来测试。实现
fd_set中对应的文件描述符的设置、复位和测试..
使用FD_ISSET()宏,你可以选出select()函数执行的结果。
FD_CLR(inr fd, fd_set* set);用来清除描述词组set中相关fd 的位
FD_ISSET(int fd, fd_set *set);用来测试描述词组set中相关fd的位是否为真
FD_SET(int fd, fd_set*set);用来设置描述词组set中相关fd的位
FD_ZERO(fd_set *set); 用来清除描述词组set的全部位
参数timeout为结构time-val,用来设置select()的等待时间,其结构定义如下
struct time-val{
time_t tv_sec;
time_t tv_usec;
};
在调用select()函数中,如果时间超过time-val参数所设置的时间长度,而还没有文件描述满足你的要
求,那么select()函数将返回,允许继续程序下面的操作。
当select()函数返回时,time-val中的时间将会被设置为执行为select()后还剩下的时间。
注:如果time-val设置为0, select立即返回,同时返回在你集合中的文件描述符状态。
如果timeout参数为NULL,select函数进入阻塞状态,除了文件描述符的状态变化,否则select()
不会返回。
如果你的套接字描述符正在通过listen()函数侦听一个外来的网络连接,则你可以使用select函数来测
试是否存在一个未经处理的新连接。
返回值如果参数timeout设为NULL则表示select()没有timeout。
错误代码执行成功则返回文件描述词状态已改变的个数,如果返回0代表在描述词状态改变前已超过
timeout时间,当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds
和timeout的值变成不可预测。
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足
范例常见的程序片段:
fs_set readset;
FD_ZERO(&readset);
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset){……}
代码演示select
#include
#define
STDIN 0
int
main()
{
struct time-val tv;
fd_set readfds;
tv.tv_sec = 2;
tv.tv_usec = 500000;
FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);
select(STDIN+1, &readfds, NULL, NULL,
&tv);
if(FD_ISSET(STDIN, &readfds)){
printf("A key was pressed!\n");
}
else{
printf("Timed out.\n");
}
}
举个简单的例子,就是从网络上接受数据写入一个文件中。
例子:
int main(int
argc, char *argv[])
{
int sock;
FILE *fp; struct fd_set fds;
struct
time-val timeout= {3,0}; //select等待3秒,3秒轮询,要非阻塞就置0
char
buffer[256] = {0}; //256字节的接收缓冲区
//...假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理
//...主机ip和port都已经给定,要写的文件已经打开
sock =
socket(...);
bind(...);
fp =
fopen(...);
while(1){
FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化
FD_SET(sock,
&fds); //添加描述符
FD_SET(fp,
&fds); //同上
maxfdp =
sock>fp?sock+1:fp+1; //描述符最大值加1
switch(select(maxfdp, &fds, &fds,
NULL, &timeout)){
case
-1:exit(-1);break; //select错误,退出程序
case
0:break; //再次轮询
default:
if(FD_ISSET(sock, &fds)){
//测试sock是否可读,即是否网络上有数据
recvfrom(sock, buffer, 256, ...); //接受网络数据
if(FD_ISSET(fp, &fds)); //测试文件是否可写
fwrite(fp,
buffer, ...); //写入文件
buffer清空;
} //end if
break;