IO多路复用编程
是同步IO的一种,用一个进程一次等待多个IO就绪事件的发生,加大几率,尽量高效的等。数组
适用场景服务器
(1)当客户处理多个描述字时(通常是交互式输入和网络套接口),必须使用I/O复用。网络
(2)当一个客户同时处理多个套接口时,而这种状况是可能的,但不多出现。多线程
(3)若是一个TCP服务器既要处理监听套接口,又要处理已链接套接口,通常也要用到I/O复用。并发
(4)若是一个服务器即要处理TCP,又要处理UDP,通常要使用I/O复用。socket
(5)若是一个服务器要处理多个服务或多个协议,通常要使用I/O复用。ide
与多进程和多线程技术相比,I/O多路复用技术的最大优点是系统开销小,系统没必要建立进程/线程,也没必要维护这些进程/线程,从而大大减少了系统的开销。函数
select函数测试
该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型以下:
#include <sys/.h>
<sys/>
( maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,返回值:就绪描述符的数目,超时返回0,出错返回-1
函数参数介绍以下:
(1)第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(所以把该参数命名为maxfdp1),描述字0、一、2...maxfdp1-1均将被测试。由于文件描述符是从0开始的。
(2)中间的三个参数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); // 检查集合中指定的文件描述符是否能够读写
fd_set结构体是文件描述符集,该结构体其实是一个整型数组,数组中的每一个元素的每一位标记一个文件描述符。fd_set能容纳的文件描述符 数量由FD_SETSIZE指定,通常状况下,FD_SETSIZE等 于1024,这就限制了select能同时处理的文件描述符的总量。
(3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
这个参数有三种可能:
1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
2)等待一段固定时间:在有一个描述字准备好I/O时返回,可是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
3)根本不等待:检查描述字后当即返回,这称为轮询。为此,该参数必须指向一个timeval结构,并且其中的定时器值必须为0。
(4)返回值状况:
a)超时时间内,若是文件描述符就绪,select返回就绪的文件描述符总数(包括可读、可写和异常),若是没有文件描述符就绪,select返回0;
b)select调用失败时,返回 -1并设置errno,若是收到信号,select返回 -1并设置errno为EINTR。
(5)文件描述符的就绪条件:
在网络编程中,
1)下列状况下socket可读:
a) socket内核接收缓冲区的字节数大于或等于其低水位标记SO_RCVLOWAT;
b) socket通讯的对方关闭链接,此时该socket可读,可是一旦读该socket,会当即返回0(能够用这个方法判断client端是否断开链接);
c) 监听socket上有新的链接请求;
d) socket上有未处理的错误。
2)下列状况下socket可写:
a) socket内核发送缓冲区的可用字节数大于或等于其低水位标记SO_SNDLOWAT;
b) socket的读端关闭,此时该socket可写,一旦对该socket进行操做,该进程会收到SIGPIPE信号;
c) socket使用connect链接成功以后;
d) socket上有未处理的错误。
selelct原理图
说明:
一、select只负责等待IO,不负责对IO进行操做,由recv/send等函数进行
二、select一共有两次系统调用:1)select系统调用 2)recvfrom系统调用
调用select时,会发生如下事情:
从用户空间拷贝fd_set到内核空间;
注册回调函数__pollwait;
遍历全部fd,对所有指定设备作一次poll(这里的poll是一个文件操做,它有两个参数,一个是文件fd自己,一个是当设备还没有就绪时调用的回调函数__pollwait,这个函数把设备本身特有的等待队列传给内核,让内核把当前的进程挂载到其中);
当设备就绪时,设备就会唤醒在本身特有等待队列中的【全部】节点,因而当前进程就获取到了完成的信号。poll文件操做返回的是一组标准的掩码,其中的各个位指示当前的不一样的就绪状态(全0为没有任何事件触发),根据mask可对fd_set赋值;
若是全部设备返回的掩码都没有显示任何的事件触发,就去掉回调函数的函数指针,进入有限时的睡眠状态,再恢复和不断作poll,再做有限时的睡眠,直到其中一个设备有事件触发为止。
只要有事件触发,系统调用返回,将fd_set从内核空间拷贝到用户空间,回到用户态,用户就能够对相关的fd做进一步的读或者写操做了。
select优势
select模型是Windows sockets中最多见的IO模型。它利用select函数实现IO 管理。经过对select函数的调用,应用程序能够判断套接字是否存在数据、可否向该套接字写入据。
如:在调用recv函数以前,先调用select函数,若是系统没有可读数据那么select函数就会阻塞在这里。当系统存在可读或可写数据时,select函数返回,就能够调用recv函数接 收数据了。
能够看出使用select模型,须要两次调用函数。第一次调用select函数第二次socket API。使用该模式的好处是:能够等待多个套接字。
select缺点
最大并发数限制:使用32个整数的32位,即32*32=1024来标识fd;
效率低:每次都会线性扫描整个fd_set,集合越大速度越慢;
内核/用户空间内存拷贝问题。
代码:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/select.h> #include <unistd.h> #include <stdlib.h> #include <string.h> int fds[64]; const fds_nums=sizeof(fds)/sizeof(fds[0]); static int startup(const char *ip,int port) { int sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0) { perror("socket"); exit(2); } int opt = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(port); local.sin_addr.s_addr=inet_addr(ip); if(bind(sock,(struct sockaddr *)&local,sizeof(local))<0) { perror("bind"); exit(3); } if(listen(sock,5)<0) { perror("listen"); exit(4); } return sock; } static void usage(const char *proc) { printf("%s [ip] [port]\n",proc); } int main(int argc,char *argv[]) { if(argc!=3) { usage(argv[0]); exit(1); } int listen_sock=startup(argv[1],atoi(argv[2])); fd_set rset; int i=0; FD_ZERO(&rset); FD_SET(listen_sock,&rset); //initial fds for(;i<fds_nums;i++) { fds[i]= -1; } fds[0]=listen_sock; int done=0; while(!done) { //reset current rset int max_fd= -1; for(i=0;i<fds_nums;i++) { if(fds[i]>0) { FD_SET(fds[i],&rset); max_fd=max_fd<fds[i]?fds[i]:max_fd; } } //struct timeval _ti={5,0}; switch(select(max_fd+1,&rset,NULL,NULL,NULL)) { case 0: printf("time out...\n"); break; case -1: perror("select"); break; default: for(i=0;i<fds_nums;i++) { //listen_fd if(i==0&&FD_ISSET(listen_sock,&rset)) { //printf("there\n"); struct sockaddr_in peer; socklen_t len=sizeof(peer); int newfd=accept(listen_sock,(struct sockaddr *)&peer,&len); if(newfd>0) { printf("get a new client$ socket->%s:%d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port)); } int j=0; for(j;j<fds_nums;j++) { if(fds[j]== -1) { fds[j]=newfd; break; } } //mfull of queue if(j==fds_nums) { close(newfd); } } else//normal accept_fd { // printf("there\n"); if(FD_ISSET(fds[i],&rset)) { char buf[1024]; memset(buf,'\0',sizeof(buf)); ssize_t _s=read(fds[i],buf,sizeof(buf)-1); if(_s>0) { buf[_s-1]='\0'; printf("client$ %s\n",buf); } else if(_s==0) { printf("%d is read done..\n",fds[i]); close(fds[i]); fds[i]= -1; } else{ perror("read"); } } } } break; } } return 0; }