什么是io复用?
它是内核提供的一种同时监控多个文件描述符状态改变的一种能力;例如当进程需要操作多个IO相关描述符时(例如服务器程序要同时查看监听socket和大量业务socket是否有数据到来),需要内核能够监控这许多描述符,一旦这些描述符有就绪(或者状态改变了)就告诉主动告诉进程哪些描述符已经就绪,这样站在进程的角度,就不需要挨个的查看每个描述符是否就绪。
比如
如果用监控来自10根不同地方的水管(I/O端口)是否有水流到达(即是否可读),那么需要10个人(即10个线程或10处代码)来做这件事。如果利用某种技术(比如摄像头)把这10根水管的状态情况统一传达到某一点,那么就只需要1个人在那个点进行监控就行了。由于I/O多路复用是在单一进程的上下文中的,因此每个逻辑流程都能访问该进程的全部地址空间,所以开销比多进程低得多;缺点是编程复杂度高
优点:开销低。
缺点:编程复杂度高。
IO的五个模型(unix系统下面)
(1)阻塞IO模型
-
(2)非阻塞IO模型
(3)IO复用模型
(4)信号驱动IO模型
前面四种都是同步IO复用
(5)异步IO模型
五种IO模型对比
参考大神博客:https://blog.csdn.net/xiexievv/article/details/44976215
select模式
函数接口
监听描述符事件,如果描述符集合中没有就绪,等待;反之,函数返回,把描述符集合清空,并设置已经就绪的描述符(设置为
1
)。工作模式
select通过轮询来检测各个集合中的描述符(fd)的状态,如果描述符的状态发生改变,则会在该集合中设置相应的标记位;如果指定描述符的状态没有发生改变,则将该描述符从对应集合中移除。因此,select的调用复杂度是线性的,即O(n)。
select的限制:
(1)前面提到FD_SETSIZE宏,这个宏是操作系统定义的。在linux下面通常是1024,也就是说select最多只能管理1024个描述符。如果大于1024的个描述,select将会产生不可预知的行为。那在没有poll或epoll的情况下,怎样使用select来处理连接数大于1024的情况呢?答案是使用多线程技术,每个线程单独使用一个select进行检测。这样的话,你的系统能够处理的并发连接数等于线程数*1024。早期的apache就是这种技术来支撑海量连接的。
(2)需要修改传入的参数数组
(3)不能指定某个有数据的socket
(4)线程不安全
函数原型
/* According to POSIX.1-2001 */ #include <sys/select.h> /* According to earlier standards */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
函数参数
NO 参数 含义 1 nfds 需要监视的最大的文件描述符值+1 2 readfds 需要检测的可读文件描述符的集合 3 writefds 需要检测的可写文件描述符的集合 4 exceptfds 需要检测的异常文件描述符的集合 5 timeout 超时时间 函数返回值
失败返回-1,获取到数据返回>0,超过时间返回0。
宏定义
Select工作流程
1:用FD_ZERO宏来初始化我们感兴趣的fd_set。
也就是select函数的第二三四个参数。
2:用FD_SET宏来将套接字句柄分配给相应的fd_set。
如果想要检查一个套接字是否有数据需要接收,可以用FD_SET宏把套接接字句柄加入可读性检查队列中
3:调用select函数。
如果该套接字没有数据需要接收,select函数会把该套接字从可读性检查队列中删除掉,
4:用FD_ISSET对套接字句柄进行检查。
如果我们所关注的那个套接字句柄仍然在开始分配的那个fd_set里,那么说明马上可以进行相应的IO操 作。比如一个分配给select第一个参数的套接字句柄在select返回后仍然在select第一个参数的fd_set里,那么说明当前数据已经来了, 马上可以读取成功而不会被阻塞。
No. 参数 含义 1 FD_ZERO(fd_set *fdset)
清空文件描述符集 2 FD_SET(int fd,fd_set *fdset)
设置监听的描述符(把监听的描述符设置为1) 3 FD_CLR(int fd,fd_set *fdset)
清除监听的描述符(把监听的描述符设置为0) 4 FD_ISSET(int fd,fd_set *fdset)
判断描述符是否设置(判断描述符是否设置为1) 5 FD_SETSIZE
256 结构体fd_set 描述符号集
typedef struct fd_set { u_int fd_count; /* how many are SET? */ SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */ } fd_set;
编码流程
定义描述符集
-
清空描述符集
设置指定的描述符并获取最大的描述符值+1
等待描述符就绪
判断已就绪的描述符,并做对应处理
-
伪代码
// 定义描述符集 fd_set rset; // 清空描述符集 FD_ZERO(&rset); // 设置描述符 FD_SET(fd1,&rset); FD_SET(fd2,&rset); // 获取最大描述符+1 int maxfdp1 = max(fd1,fd2) + 1; // 等待描述符就绪 if(select(maxfdp1,&rset,NULL,NULL,NULL) > 0) { // 判断已就绪的描述符 if(FD_ISSET(fd1,&rset)){ // do somthing } if(FD_ISSET(fd2,&rset)){ // do somthing } }
sever.cpp文件
void show_info(int connfd){ struct sockaddr_in local_addr; bzero(&local_addr,sizeof(local_addr)); socklen_t local_addr_len = sizeof(local_addr); getsockname(connfd,(struct sockaddr*)&local_addr,&local_addr_len); printf("server local %s:%d\n",inet_ntoa(local_addr.sin_addr),ntohs(local_addr.sin_port)); struct sockaddr_in peer_addr; bzero(&peer_addr,sizeof(peer_addr)); socklen_t peer_addr_len = sizeof(peer_addr); getpeername(connfd,(struct sockaddr*)&peer_addr,&peer_addr_len); printf("server peer %s:%d\n",inet_ntoa(peer_addr.sin_addr),ntohs(peer_addr.sin_port)); } int main(int argc,char* argv[]){ if(3 != argc){ printf("usage:%s <ip> <#port>\n",argv[0]); return 1; } int listenfd = socket(AF_INET,SOCK_STREAM,0); if(-1 == listenfd){ perror("listenfd open err"); return 1; } printf("socket create OK\n"); int flag = 1; setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag)); struct sockaddr_in local_addr; bzero(&local_addr,sizeof(local_addr)); local_addr.sin_family = AF_INET; local_addr.sin_addr.s_addr = inet_addr(argv[1]); local_addr.sin_port = htons(atoi(argv[2])); if(-1 == bind(listenfd,(struct sockaddr*)&local_addr,sizeof(local_addr))){ perror("bind err"); return 1; } printf("bind OK\n"); if(-1 == listen(listenfd,10)){ perror("listen err"); return 1; } printf("listen OK\n"); fd_set rfds; FD_ZERO(&rfds); FD_SET(listenfd,&rfds); int maxfdp1 = listenfd + 1; int connfds[FD_SETSIZE-1]; size_t connfds_cnt = 0; for(;;){ int i; FD_ZERO(&rfds); FD_SET(listenfd,&rfds); for(i=0;i<connfds_cnt;i++){ FD_SET(connfds[i],&rfds); printf("FD_SET(%d)\n",connfds[i]); } printf("before select:%lu\n",rfds); if(-1 == select(maxfdp1,&rfds,NULL,NULL,NULL)){ perror("select error"); return 1; } printf("after select:%lu\n",rfds); if(FD_ISSET(listenfd,&rfds)){ printf("listenfd ready\n"); struct sockaddr_in remote_addr; bzero(&remote_addr,sizeof(remote_addr)); socklen_t remote_addr_len = sizeof(remote_addr); int connfd = accept(listenfd,(struct sockaddr*)&remote_addr,&remote_addr_len); if(-1 == connfd){ perror("accept err"); //return 1; } if(connfds_cnt+1 == FD_SETSIZE-1){ fprintf(stderr,"connfd size over %d\n",FD_SETSIZE-1); close(connfds[i]); }else{ connfds[connfds_cnt++] = connfd; maxfdp1 = max(connfd,maxfdp1-1)+1; show_info(connfd); } } for(i=0;i<connfds_cnt;i++){ if(-1 == connfds[i]){ continue; } if(FD_ISSET(connfds[i],&rfds)){ char buf[BUFSIZ]; bzero(buf,BUFSIZ); ssize_t len; if((len = read(connfds[i],buf,BUFSIZ-1)) == -1){ perror("read err"); //return 1; } if(0 == len){ printf("close %d\n",connfds[i]); close(connfds[i]); FD_CLR(connfds[i],&rfds); memcpy(connfds+i,connfds+i+1,connfds_cnt-i-1); connfds_cnt--; i--;//数组发生变化,重新判断i的fd continue; } printf("server recv:%s\n",buf); int fd = open(buf,O_RDONLY); if(-1 == fd){ perror("open file err"); } struct stat file_stat; fstat(fd,&file_stat); if(-1 == sendfile(connfds[i],fd,NULL,file_stat.st_size)){ perror("sendfile err"); } printf("server send file %s ok\n",buf); close(fd); } } } close(listenfd); }
select模型图示:
tips:
要定义一个数组来存放所有建立联系的套结字描述符,每次查寻的是这个。