认识IO多路转接IO 多路转接核心作用是一个进程 / 线程同时监听多个文件描述符socket / fd当任意一个描述符就绪就通知应用程序进行处理就绪描述符。它解决了传统阻塞 IO 的痛点阻塞 IO一个线程只能处理一个连接并发量极低多线程 / 多进程资源消耗大线程切换开销高IO 多路转接单线程监听大量描述符无需为每个连接创建线程极大提升并发能力。select、poll、epoll就是系统提供的IO多路复用系统调用。IO多路转接之select认识selectselect系统调用是用来让我们的应用程序监视多个文件描述符的状态变化的,程序会停在select这里等待直到被监视的多个文件描述符里有一个或多个fd就绪就通知上层该fd可以IO了。结论select通过等待多个fd的一种就绪事件通知机制。事件就绪文件描述符可读底层有数据读事件就绪。文件描述符可写底层有空间写事件就绪。select核心思想用户将需要监听的文件描述符集合传给内核。内核轮询所有的文件描述符检测是否有事件就绪。如果有一个或者多个就绪就返回就绪的数量并将集合中未就绪的描述符清空。用户遍历集合找到就绪描述符进行处理IO。本质是我们调用select系统调用让select去和系统做交互。select接口认识相关接口int select(int nfds, fd_set * readfds, fd_set * writefds, fd_set * exceptfds,struct timeval * t timeout);参数解释nfds需要等的最大文件描述符值1。readfds/writefds/exceptfds输入输出型参数。调用时用户告知内核需要监视哪些文件描述符的读/写/异常事件是否就绪。返回时内核告知用户哪些文件描述符的读/写/异常事件已经就绪。timeout调用时由用户设置select的等待时间返回时表示timeout的剩余时间。fd_set类型fd_set文件描述符集合内核提供给用户的数据结构一次可以向fd_set添加多个fd。从上图Linux内核中对fd_set类型的构造我们可以看出它的结构内部封装了一个位图数组使用位图中对应的位置来表示要监视的文件描述符。我们知道在select接口中readfds, writefds, exceptfds,三个参数分别表示读/写/异常他们的类型值都是fd_set。我们以readfds为例来说明用位图怎么表示的。以readfds读文件描述符集为例:假设在程序中我们打开的文件描述符分别为01247位图结构为0000 0000。然后我们想要关心这几个文件描述符的读事件。作为输入型参数我们需要把想要关心的文件描述符设置到位图里边我们设置为1001 0111就可以表示我们把关心所要关心的文件描述符已经设置到位图了。由此也可以看出对于比特位的位置也就是文件描述符对应的编号。比特位表示1证明我们需要关心这个文件描述符读事件否则就不关心。作为输出型参数假设当文件描述符为24的事件就绪了那么select函数返回把没有就绪的比特位清0我们拿到的输出型参数就是0001 0100这时对于位图对应位置的1就表示该事件已经就绪0就表示没有就绪。总上可知fd_set本质就是一张位图结构主要目的就是为了让用户与内核相互传递fd。作为输入参数用户告诉内核你帮我监控哪几个文件描述符什么事件。作为输出参数内核告诉用户哪几个文件描述符已经就绪。细节输入输出参数用的是同一张位图后续肯定会频繁的修改位图。因为某个事件就绪修改了位图但是还有其他的未就绪事件我们需要继续去等待。位图的比特位表示fd编号位图有多少个比特位就决定了select关心多少个fdfd_set是一个数据类型大小固定比特位大小固定select能同时等待的fd有上限fd_set类型大小字节数。后边可用poll、epoll扩展。readfds如果把fd添加到readfds中表示告诉内核只关心该fd的读事件。同时关心读和写需要把fd同时添加到readfds/writefdsfd_set位图操作接口对于位图的操作可以使用现成的接口来直接操作。void FD_CLR(int fd, fd_set *set); // 用来清除描述符集合set中 fd 的位int FD_ISSET(int fd, fd_set *set); // 用来判断set中相关 fd 的位是否存在void FD_SET(int fd, fd_set *set); // 把fd设置到set位图中void FD_ZERO(fd_set *set); // 用来清除set的全部位set位图本身并不真的关心是读还是写只用来在底层当前是否有数据/读写入的条件是否满足满足就修改对应文件描述符位图就行。timeval结构体select最后一个参 数timeout类型是 struct timeval类型。由以上结构可知time_t就是无符号整数以秒为单位代表时间戳第二个代表微秒。timeval结构用于描述一段时间长度如果在这个时间内需要监视的描述符没有事件发生则函数返回返回值为0。假设给select设置时间为{55}表示让select每隔5.5秒返回一次如果5.5秒内没有任何一个文件描述符就绪就超时返回重新开始。如果期间有文件描述符就绪就立刻返回。如果设置为{0,0}就类似于非阻塞轮询检测无论检测众多描述符是否就绪都返回。如果设置为NULL就相当于阻塞等待了只要有一个文件描述符就绪就直接返回。该结构也是一个输入输出型参数如果设定规定时间在此时间内如果有事件就绪那么该结构输出参数就是剩余超出时间。select函数返回值返回值大于0是几 就表示几个fd就绪了。返回值等于0表示超时了timeout设置。不能为null,在timeout fd没有就绪返回值小于0select报错可能文件描述符不合法基于select tcp编写echo_server注意这里只处理读操作。主题函数实现需要整个代码项目的可以私信小编。#pragma once #includeiostream #includesys/select.h #include Common.hpp #includeLog.hpp #includememory //select socket_serve #includeSocket.hpp class SelectServer{ const static int size3sizeof(fd_set)*8; const static int defauID-1; public: SelectServer(int port):_listensockfd(make_uniqueTcpSocket()){ _listensockfd-BuildTcpSocketMethod(port); //初始化辅助数组 for(int i0;isize3;i){ fd_arry[i]defauID; } fd_arry[0]_listensockfd-Fd();//listenfd肯定在最开始 } void start(){ while(true){ //当前的服务器已经设置好监听状态了 //常规构建服务器这里就可以使用accept去获取连接了但在select这里我们不可以这样做。accept本质就是IO如果没有链接就会阻塞 //accept只负责获取链接处理文件描述的读事件。 //但是获取链接需要等所有我们可以把等的操作交给select //我们创建的网络sockfd需要从这个sockfd中读所以我们创建select让os帮我们去从这个文件描述符中读数据。 //要对select调用需要先定义fds集合 fd_set rfds;//先定义fds集合 FD_ZERO(rfds);//做一下清空 //把每一个辅助数组里边要监听的fd设置到rfds中 int maxfddefauID;//最大的fd一直都变因为一直在连接新连接 for(int i0;isize3;i){ if(fd_arry[i]defauID){ continue; } //证明存在描述符 FD_SET(fd_arry[i],rfds); //更新最大的maxfd if(maxfdfd_arry[i]){ maxfdfd_arry[i]; } } PrintFD(); //FD_SET(_listensockfd-Fd(),rfds);//添加当前描述符到该集合中 //struct timeval timeout {0,0}; //任何文件描述符都应该交给select同一管理 /*对于select,rfds输入前可能需要等待的fd很多但是执行完函数他又作为输出参数所以很难保证历史没有就绪的文件描述符再次被内核等待*/ //所以针对中情况一般设计select时会增加一个辅助数组来记录历史存在fds //因为对于rfds每一次对应的位图值都需要对应的修改变化所以我们要每一次都更新 // int selselect(_listensockfd-Fd()1,rfds,nullptr,nullptr,nullptr);//通过select函数把set集合设置到内核。 int selselect(maxfd1,rfds,nullptr,nullptr,nullptr);//通过select函数把set集合设置到内核。 switch(sel) { case -1: //说明select出错了 LOG(LogLevel::ERROR)select error; break; case 0: //说明设置时间超时了 LOG(LogLevel::WARNING)时间超时了.....; break; default: //大于0某个事件就绪了 LOG(LogLevel::DEBUG)有事件就绪......,sel: sel; Dispatcher(rfds); break; } } } //连接管理器 void Acceptor(){ //去处理就绪事件 InetAddr client; int fd_listensockfd-Accept(client); //这里accept不会在阻塞了因位有连接已经就绪select帮我们等待了。 LOG(LogLevel::INFO)get a new link,fd:fdclient is:client.StringAddr(); //这里我们获取了新链接但不能直接进行读取操作/read/recv,因为建立连接并不等于必须要通信所以我们要想法把新连接的fd放到select中交给内核来等待读取就绪 //这里我们直接把获得的新sockfd放入到辅助数组中即可 int pos0; for(;possize3;pos){ if(fd_arry[pos]defauID){ break; } } if(possize3){ LOG(LogLevel::WARNING)server full...; close(fd); }else{ fd_arry[pos]fd; } } void Dispatcher(fd_set rfds){ //处理事件不仅仅是处理事件而且读事件就绪需要处理 //我们需要知道哪个文件描述符已经就绪 for(int i0;isize3;i){ if(fd_arry[i]defauID){ continue; } //fd合法不一定就绪 比特位可能为0 可能为 1 if(FD_ISSET(fd_arry[i],rfds)){ //证明该文件描述符已经就绪 //listensockfd 新连接的到来 也是读事件就绪 //sockfd 数据到了 读事件就绪。 if(fd_arry[i]_listensockfd-Fd()){ //证明是监听就绪 Acceptor(); }else{ //证明是read就绪 Recver(fd_arry[i],i); } } } } //IO处理器 void Recver(int fd,int pos){ char buffer[1024]; int rrecv(fd,buffer,sizeof(buffer)-1,0); if(r0){ buffer[r]0; coutclient say:bufferendl; }else if(r0){ //代表读完数据了 LOG(LogLevel::INFO)client quit....; //不要再select关心该fd fd_arry[pos]defauID; close(fd); }else{ LOG(LogLevel::INFO)Recv err....; fd_arry[pos]defauID; close(fd); } } void PrintFD(){ coutfd_arry[]:; for(int i0;isize3;i){ if(fd_arry[i]defauID){ continue; } coutfd_arry[i] ; } coutendl; } ~SelectServer(){} private: std::unique_ptrSocket _listensockfd; //创建socket int fd_arry[size3];//辅助数组 };select优缺点select的优点单进程下可以同时等待多个文件描述符并且只负责等待实际的拷贝动作由readwrite等接口完成并且最大的好处就是select之后再调用read这些接口不会再被阻塞。select同时等待多个文件描述符可以将“等”的时间重叠提高IO效率。select缺点因为select函数中输入输出型参数比较多所以对应设定的FD_SET集合调用前需要重置调用后又被清除。每次调用select,都要把fd集合从用户态拷贝到内核态调用select后又要吧fd集合从内核态拷贝到用户态。这给开销再fd很多时是非常大的。select中支持fd的数量是有限的一般根os系统有关具体大小sizeof(fd_set)*8IO多路转接之poll认识pollpoll同select作用一样也是只是负责等都是让一个线程同时监听多个文件描述符等待其中一个或多个文件符就绪通知上层。poll接口认识#include poll.hint poll(struct pollfd *fds, nfds_t nfds, int timeout);参数介绍fds:当成一个数组的起始地址。nds:数组元素的个数。timeout:单纯的时间设置ms为单位作用同selectstruct pollfd结构struct pollfd {int fd; // 要监听的文件描述符short events; // 等什么事件POLLIN 读事件 / POLLOUT 写事件short revents; // 内核返回实际发生了什么事件};设置事件的时候 evects | POLLIN判断是否就绪 revects POLLOUT再调用poll函数时只需要关心fd events用户告诉内核你需要帮我关心fd的events事件poll返回成功时只需要关系fd revents内核告诉用户关心的evects事件已经就绪。poll解决了select的问题1.select输入输出参数没有分离poll分离了不用在调用函数前对参数进行重置了。poll输入输出参数分离2.poll等待的fd没有上线。poll的第一个参数是一个动态结构体数组可以随时扩容。改写select代码变成poll服务器#pragma once #includeiostream #includesys/select.h #include Common.hpp #includeLog.hpp #includememory #includepoll.h //select socket_serve #includeSocket.hpp class PollServer{ // const static int size3sizeof(fd_set)*8; const static int size310001; const static int defauID-1; public: PollServer(int port):_listensockfd(make_uniqueTcpSocket()){ _listensockfd-BuildTcpSocketMethod(port); //初始化数组 for(int i0;isize3;i){ fds[i].fddefauID; fds[i].events0; fds[i].revents0; } fds[0].fd_listensockfd-Fd(); fds[0].eventsPOLLIN; } void start(){ while(true){ PrintFD(); int timeout1000; int selpoll(fds,size3,-1); switch(sel) { case -1: //说明select出错了 LOG(LogLevel::ERROR)poll error; break; case 0: //说明设置时间超时了 LOG(LogLevel::WARNING)poll 时间超时了.....; break; default: //大于0某个事件就绪了 LOG(LogLevel::DEBUG)有事件就绪......,sel: sel; Dispatcher(); break; } } } //连接管理器 void Acceptor(){ //去处理就绪事件 InetAddr client; int fd_listensockfd-Accept(client); //这里accept不会在阻塞了因位有连接已经就绪select帮我们等待了。 LOG(LogLevel::INFO)get a new link,fd:fdclient is:client.StringAddr(); //这里我们获取了新链接但不能直接进行读取操作/read/recv,因为建立连接并不等于必须要通信所以我们要想法把新连接的fd放到select中交给内核来等待读取就绪 //这里我们直接把获得的新sockfd放入到辅助数组中即可 int pos0; for(;possize3;pos){ if(fds[pos].fddefauID){ break; } } if(possize3){ LOG(LogLevel::WARNING)server full...; close(fd); }else{ fds[pos].fdfd; fds[pos].eventsPOLLIN; } } void Dispatcher(){ //处理事件不仅仅是处理事件而且读事件就绪需要处理 //我们需要知道哪个文件描述符已经就绪 for(int i0;isize3;i){ if(fds[i].fddefauID){ continue; } //fd合法不一定就绪 比特位可能为0 可能为 1 if( fds[i].revents POLLIN ){ //证明该文件描述符已经就绪 //listensockfd 新连接的到来 也是读事件就绪 //sockfd 数据到了 读事件就绪。 if(fds[i].fd_listensockfd-Fd()){ //证明是监听就绪 Acceptor(); }else{ //证明是read就绪 Recver(i); } } } } //IO处理器 void Recver(int pos){ char buffer[1024]; int rrecv(fds[pos].fd,buffer,sizeof(buffer)-1,0); if(r0){ buffer[r]0; coutclient say:bufferendl; }else if(r0){ //代表读完数据了 LOG(LogLevel::INFO)client quit....; close(fds[pos].fd); //不要再select关心该fd fds[pos].fddefauID; fds[pos].events0; fds[pos].revents0; }else{ close(fds[pos].fd); //不要再select关心该fd fds[pos].fddefauID; fds[pos].events0; fds[pos].revents0; } } void PrintFD(){ coutfds[]: ; for(int i0;isize3;i){ if(fds[i].fddefauID){ continue; } coutfds[i].fd ; } coutendl; } ~PollServer(){} private: std::unique_ptrSocket _listensockfd; //创建socket //int fd_arry[size3];//辅助数组 struct pollfd fds[size3]; };poll的优点struct pollfd结构体可以将select的输入输出型参数分离数据不会覆盖poll可监控的文件描述符没有限制因为数组大小是用户定的也可以进行扩容poll的缺点和select一样poll底层判断哪个文件描述符时也需要遍历fds数组来获取就绪的文件描述符。每次调用poll都伴随着大量的struct pollfd在用户态和内核态之间的转换并且当poll监视的文件描述符很多时效率就很低。IO多路转接之epoll认识epoll同select/poll功能类似epoll也是基于可以同时等待多个文件描述符就绪对上层进行通知的功能。对于poll虽然解决了select函数的文件描述符有限输入输出型参数混乱的情况但poll对于文件描述符的管理仍然是使用数组来进行管理的随着文件描述符的增加内核对其管理的成本就增加了效率就下降了。所以针对这个问题epoll对其进行了改进。但epoll的实现原理相对于poll来讲非常大。epoll相关系统调用接口//创建 epoll 模型返回 epfdepoll文件描述符。int epfd epoll_create(int size);//size0即可//增 / 删 / 改监听 FD文件描述符//向epfd中增删查改//该函数主要是用户告诉内核你要帮我关心epfd模型的哪些事件int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epfd创建的epoll文件描述符op对操作符进行操作EPOLL_CTL_ADD /EPOLL_CTL_MOD /EPOLL_CTL_DELevent指定监听事件EPOLLIN可读、EPOLLOUT可写够用//等待就绪事件//内核告诉用户让我关心的哪个fd上的事件已经就绪int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);epfd创建的实例epoll文件描述符events就绪的文件描述符放到数组里maxevents就绪数组中最大的文件描述符timeout设置等待时间单位毫秒struct epoll_eveny* event结构epoll的核心原理epoll在内核中的核心实现原理主要是维护了一个实例epfd在这个实例中包含两个核心的数据结构和一个回调机制。我们通过函数epoll_create就能创建出一个实例这也就是为什么epoll_create、epoll_wait第一个参数为啥是epoll_create的返回值。红黑树结构在epofd中会维护一个红黑树结构红黑树每个节点存放的是注册的fd以及事件。在上层我们调用的epoll_ctl就是用来维护这张红黑树结构的。根据参数OP具体含义对红黑树节点进行增删该操作。只要对应的实例创建在内核中就会为我们长期维护这个数据结构对这个树的操作也是O(logn)就绪队列结构epoll实例创建之后也会维护一个就绪对立在就绪对立仅保存就绪事件的fd节点。对于监听的文件描述符fd内核通过回调机制将节点从红黑树结构移入到就绪对立结构。对于我们使用的系统调用接口epoll_wait就是用来维护就绪队列的。事件回调机制事件在调用epoll_ctl注册fd时内核自动为队以ing的fd绑定poll_callback回调机制。当网卡接收数据 触发硬件中断数据到来内核协议栈完成收包后套接字缓冲区状态发生变更会自动触发预设的回调函数将就绪 fd 对应的节点移入就绪队列同时唤醒阻塞等待的 epoll_wait。对于节点从红黑树结构转到就绪队列时无需遍历红黑树全部文件描述符仅靠中断驱动、内核主动推送就绪事件摆脱 select/poll 的轮询遍历这也是 epoll 能支撑百万级高并发连接、实现高效 IO 调度的核心底层原理。