IO端口复用简介I/O多路复用(multiplexing):本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。
适用场景:高并发的服务器端。
应对并发,常见的思维是创建多线程,每个线程管理一个并发操作,但是弊端很明显,就是多线程需要上下文切换,这个切换的消耗太大,当连接的客户端很多的时候弊端就很突出了。
所示使用单线程的多路复用。
几种方式1.s electLinux提供的select相关函数接口如下:#include <sys/select.h>#include <sys/time.h>int select(int max_fd, fd_set *readset, fd_set *wri teset, fd_set *exceptset, struct timeval *timeout) FD_ZERO(int fd, fd_set* fds) /* 清空集合 */FD_SET(int fd, fd_set* fds) /* 将给定的描述符加入集合 */FD_ISSET(int fd, fd_set* fds) /* 将给定的描述符从文件中删除 */FD_CLR(int fd, fd_set* fds) /* 判断指定描述符是否在集合中 */接口解释:1:select函数的返回值就绪描述符的数目,超时时返回0,出错返回-1。
2:第一个参数max_fd指待测试的fd个数,它的值是待测试的最大文件描述符加1,文件描述符从0开始到max_fd-1都将被测试。
3:中间三个参数readset、writeset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试的可以设置为NULL。
代码演示:sockfd=socket(AF_INET,SOCK_STREAM,0);memset(&addr,0,sizeof(addr));addr.sin_family=AF_INET;addr.sin_port=htons(2000);addr.sin_addr.s_addr=IN ADDR_ANY;bind(sockfd,(struct sockaddr*)&addr,sizeof(addr)); listen(sockfd,5);fd_set rset;int max = 0;int fds[5];for(int i=0;i<5;i++){memset(&client,O,sizeof(client);addrlen=sizeof(client);fds[i]=accept(sockfd,(struct sockaddr*)&client,&addrlen);if(fds[i]>max)max=fds[i];}while(1){FD_ZERO(&rset);for(int i=0;i<5;i++){FD_SET(fds[i],&rset);}puts("round again");select(max+1,&rset,NULL,NULL,NULL);for(int i=0;i<5;i++){if(FD_ISSET(fds[i],&rset)){memset(buffer,0,MAXBUF);read(fds[i],buffer,MAXBUF);puts(buffer);}}}这是一段使用select的端口复用的简单代码。
代码定义一个监听5个客户端的select模型。
从代码中可以看出几个问题:1:FD_ZERO(&rset);for(int i=0;i<5;i++){FD_SET(fds[i],&rset);}每次while循环都要需要重复调用FD_ZERO进行清除,并在FD_SET加入句柄。
2:for(int i=0;i<5;i++){if(FD_ISSET(fds[i],&rset)){memset(buffer,0,MAXBUF);read(fds[i],buffer,MAXBUF);puts(buffer);}}每次读取数据都需要遍历所有句柄,所以就有O(n)的消耗。
3:select模型设计时有最大的上线值1024,可在源码中查看到。
想要更高的并发的话就需要采用多线程了。
4:执行select函数时,存在着用户态和内核态的切换,需要把句柄从内核态切换到用户态。
数量少的时候感知不强烈,高并发的时候能有明显的感觉。
以上4点也就是select模型的缺点,总结如下:1:有上限要求(1024),更多的话就需要多线程了;2:FDSET不能重用,每次都需要FDZERO;3:获取句柄需要从用户态拷贝成内核态,开销大;4:获取消息的形式是遍历。
需要循环获取。
时间复杂度O(n)。
相交于传统的套接字select也存在其优点:1:效率高。
2:跨平台性好,几乎支持所有平台。
2.pollpoll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。
#include <poll.h>int poll(struct pollfd fds[], nfds_t nfds, int timeout);typedef struct pollfd {int fd; /* 需要被检测或选择的文件描述符 */short events; /* 对文件描述符fd 上感兴趣的事件 */short revents; /* 文件描述符fd 上当前实际发生的事件 */} pollfd_t;接口解释:1:poll()函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;2:fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;3:nfds记录数组fds中描述符的总数量;4:timeout是调用poll函数阻塞的超时时间,单位毫秒;5:一个pollfd结构体表示一个被监视的文件描述符,通过传递fds[]指示 poll() 监视多个文件描述符。
其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。
events域中请求的任何事件都可能在revent s域中返回。
合法的事件如下:POLLIN 有数据可读POLLRDNORM 有普通数据可读POLLRDBAND 有优先数据可读POLLPRI 有紧迫数据可读POLLOUT 写数据不会导致阻塞POLLWRNORM 写普通数据不会导致阻塞POLLWRBAND 写优先数据不会导致阻塞POLLMSGSIGPOLL 消息可用当需要监听多个事件时,使用POLLIN | POLLRDNORM设置 events 域;当poll 调用之后检测某事件是否发生时,fds[i].revents & POLLIN进行判断。
代码演示:pollfd pollds[5];for(int i=0;i<5;i++){memset(&client,0,sizeof(client));addrlen=sizeof(client);pollfds[i].fd=accept(sockfd,(structsockaddr*)&client,&addrlen);pollfds[i].events=POLLIN;}sleep(1);while(1){puts("round again");poll(pollfds,5,50000);for(int i=0;i<5;i++){if(pollfds[i].revents&POLLIND){pollfds[i].revents=0;memset(buffer,0,MAX BUF);read(pollfds[i].fd,buffer,MAXBUF);puts(buffer);}}}通过代码可看到,poll模型相较于 select的改进在于使用了自定义的结构pollfd 来存储数据,对比select模型可以观察到几个不同的变化点:1:不再存在上限1024的限定。
2:pollfd直接存储句柄和事件,所以无需再次重复重置句柄poll的优势主要是解决了select模型的前两项缺点1:无上线限制2:无需频繁的遍历重置读取句柄。
3.epollepoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式Linux中提供的epoll相关函数接口如下:#include <sys/epoll.h>int epoll_create(int size);int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);epoll_create函数创建一个epoll句柄,参数size表明内核要监听的描述符数量(参数可忽略,无意义)。
调用成功时返回一个epoll句柄描述符,失败时返回-1。
epoll_ctl函数注册要监听的事件类型。
四个参数解释如下:epfd表示epoll句柄;op表示fd操作类型:EPOLL_CTL_ADD(注册新的fd到epfd中),EPOLL_CTL_MOD(修改已注册的fd的监听事件),EPOLL_CTL_DEL(从epfd 中删除一个fd)fd是要监听的描述符;event表示要监听的事件epoll_event结构体定义如下:struct epoll_event {__uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */ };typedef union epoll_data {void *ptr;int fd;__uint32_t u32;__uint64_t u64;} epoll_data_t;epoll_wait函数等待事件的就绪,成功时返回就绪的事件数目(有1个就直接返回1,有5个就返回5,自身会进行置位,把有消息的排序到前面),调用失败时返回 -1,等待超时返回 0。