I/O多路复用详解

1、五种I/O模型javascript

一、阻塞I/O模型java

最流行的I/O模型是阻塞I/O模型,缺省情形下,全部套接口都是阻塞的。咱们以数据报套接口为例来说解此模型(咱们使用UDP而不是TCP做为例子的缘由在于就UDP而言,数据准备好读取的概念比较简单:要么整个数据报已经收到,要么尚未。然而对于TCP来讲,诸如套接口低潮标记等额外变量开始活动,致使这个概念变得复杂)。编程

进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回,期间一直在等待。咱们就说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。服务器

二、非阻塞I/O模型网络

进程把一个套接口设置成非阻塞是在通知内核:当所请求的I/O操做非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。也就是说当数据没有到达时并不等待,而是以一个错误返回。app

三、I/O复用模型less

调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。异步

四、信号驱动I/O模型socket

首先开启套接口信号驱动I/O功能, 并经过系统调用sigaction安装一个信号处理函数(此系统调用当即返回,进程继续工做,它是非阻塞的)。当数据报准备好被读时,就为该进程生成一个SIGIO信号。随便可以在信号处理程序中调用recvfrom来读数据报,井通知主循环数据已准备好被处理中。也能够通知主循环,让它来读数据报。函数

五、异步I/O模型

告知内核启动某个操做,并让内核在整个操做完成后(包括将数据从内核拷贝到用户本身的缓冲区)通知咱们。这种模型与信号驱动模型的主要区别是:
信号驱动I/O:由内核通知咱们什么时候能够启动一个I/O操做,
异步I/O模型:由内核通知咱们I/O操做什么时候完成。

2、I/O复用的典型应用场合:

一、当客户处理多个描述字(一般是交互式输入和网络套接口)时,必须使用I/O复用。

二、若是一个服务器要处理多个服务或者多个协议(例如既要处理TCP,又要处理UDP),通常就要使用I/O复用。

3、支持I/O复用的系统调用

目前支持I/O复用的系统调用有select、pselect、poll、epoll:

一、select函数

该函数容许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

格式为:

1 #include <sys/select.h>
2 #include <sys/time.h>
3
4 int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
5
6 返回:就绪描述字的正数目,0-超时,-1-出错

咱们从该函数的最后一个参数开始介绍,它告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

struct timeval{

long tv_sec; //seconds

long tv_usec; //microseconds

};

这个参数有三种可能:

(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,咱们把该参数设置为空指针。

(2)等待一段固定时间:在有一个描述字准备好I/O时返回,可是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。

(3)根本不等待:检查描述字后当即返回,这称为轮询。为此,该参数必须指向一个timeval结构,并且其中的定时器值必须为0。

中间的三个参数readset、writeset和exceptset指定咱们要让内核测试读、写和异常条件的描述字。若是咱们对某一个的条件不感兴趣,就能够把它设为空指针。struct fd_set能够理解为一个集合,这个集合中存放的是文件描述符,可经过如下四个宏进行设置:

void FD_ZERO(fd_set *fdset); //清空集合

void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中

void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除

int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否能够读写 ?

目前支持的异常条件只有两个:

(1)某个套接口的带外数据的到达。

(2)某个已置为分组方式的伪终端存在可从其主端读取的控制状态信息。

第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(所以咱们把该参数命名为maxfdp1),描述字0、一、2...maxfdp1-1均将被测试。

一个应用select的例子:

001 /**
002 *TCP回射服务器客户端程序
003 */
004 #include <stdio.h>
005 #include <stdlib.h>
006 #include <unistd.h>
007 #include <sys/socket.h>
008 #include <sys/types.h>
009 #include <netinet/in.h>
010 #include <netdb.h>
011 #include <string.h>
012 #include <math.h>
013 #include <sys/select.h>
014 #include <sys/time.h>
015
016 #define SERVER_PORT 3333 //服务器端口号
017
018 void str_cli(FILE *fp, int sockfd)
019 {
020 int maxfdp1, stdineof;
021 fd_set rset;
022 char buf[BUFSIZ];
023 int n;
024
025 stdineof = 0;
026 FD_ZERO(&rset);
027
028 while(1)
029 {
030 if( stdineof == 0 )
031 FD_SET(fileno(fp),&rset);
032 FD_SET(sockfd, &rset);
033
034 maxfdp1 = ((fileno(fp) > sockfd) ? fileno(fp) : sockfd) + 1;
035
036 select(maxfdp1, &rset, NULL, NULL, NULL);
037
038 if( FD_ISSET(sockfd, &rset) )
039 {
040 if( (n = read(sockfd, buf, BUFSIZ)) == 0 )
041 if( stdineof == 1 )
042 return;
043 else
044 perror("server terminated prematurely");
045 write(fileno(stdout), buf, n);
046 }
047
048 if( FD_ISSET(fileno(fp), &rset))
049 {
050 if( (n = read(fileno(fp), buf, BUFSIZ)) == 0 )
051 {
052 stdineof = 1;
053 shutdown(sockfd, SHUT_WR);
054 FD_CLR(fileno(fp), &rset);
055 continue;
056 }
057 write(sockfd, buf, n);
058 }
059 }
060 }
061
062 int main(int argc, char *argv[])
063 {
064 int sockfd[5];
065 struct sockaddr_in servaddr;
066 struct hostent *hp;
067 char buf[BUFSIZ];
068
069 if( argc != 2 )
070 {
071 printf("Please input %s <hostname>\n", argv[0]);
072 exit(1);
073 }
074
075 int i;
076 for(i = 0; i < 5; ++i)
077 {
078
079 //建立socket
080 if( (sockfd[i] = socket(AF_INET, SOCK_STREAM,0)) < 0 )
081 {
082 printf("Create socket error!\n");
083 exit(1);
084 }
085
086 //设置服务器地址结构
087 bzero(&servaddr, sizeof(servaddr));
088 servaddr.sin_family = AF_INET;
089 if( (hp = gethostbyname(argv[1])) != NULL )
090 {
091 bcopy(hp->h_addr, (struct sockaddr*)&servaddr.sin_addr, hp->h_length);
092 }
093 else if(inet_aton(argv[1], &servaddr.sin_addr) < 0 )
094 {
095 printf("Input Server IP error!\n");
096 exit(1);
097 }
098 servaddr.sin_port = htons(SERVER_PORT);
099
100 //链接服务器
101 if( connect(sockfd[i],(struct sockaddr*)&servaddr, sizeof(servaddr)) < 0 )
102 {
103 printf("Connect server failure!\n");
104 exit(1);
105 }
106 }
107 str_cli(stdin, sockfd[0]);
108
109 exit(0);
110 }

注:本章内容摘自<Unix 网络编程>第六章。

二、pselect函数

pselect函数是由POSIX发明的,现在许多Unix变种都支持它。

1 #include <sys/select.h>
2 #include <signal.h>
3 #include <time.h>
4
5 int pselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec *timeout, const sigset_t *sigmask);
6 返回:就绪描述字的个数,0-超时,-1-出错

pselect相对于一般的select有两个变化:

一、pselect使用timespec结构,而不使用timeval结构。timespec结构是POSIX的又一个发明。

struct timespec{

time_t tv_sec; //seconds

long tv_nsec; //nanoseconds

};

这两个结构的区别在于第二个成员:新结构的该成员tv_nsec指定纳秒数,而旧结构的该成员tv_usec指定微秒数。

二、pselect函数增长了第六个参数:一个指向信号掩码的指针。该参数容许程序先禁止递交某些信号,再测试由这些当前被禁止的信号处理函数设置的全局变量,而后调用pselect,告诉它从新设置信号掩码。

关于第二点,考虑下面的例子,这个程序的SIGINT信号处理函数仅仅设置全局变量intr_flag并返回。若是咱们的进程阻塞于select调用,那么从信号处理函数的返回将致使select返回EINTR错误。然而调用select时,代码看起来大致以下:

相关文章
相关标签/搜索