计算机网络知识点(五) I/O 多路复用:函数select、poll和epoll 什么是IO多路复用IO即为网络I/O多路即为多个TCP连接复用即为共用一个线程或者进程模型最大的优势是系统开销小不必创建也不必维护过多的线程或进程。IO复用(IOmultiplexing)使得内核一旦发现进程指定的一个或多个IO条件就绪也就是说输入已准备好被读取或者描述符已能承接更多的输出)它就通知进程。应用场景I/O 多路复用通常用于以下场景的网络应用当客户端处理多个描述符时通常是交互式输入和网络套接字当客户端同时处理多个套接字时这是可能的但很少见如果 TCP 服务器同时处理侦听套接字及其连接的套接字如果服务器同时处理 TCP 和 UDP如果服务器处理多个服务可能还有多个协议I/O 多路复用不限于网络编程。许多重要的应用程序发现需要这些技术。输入/输出模型我们首先检查 Unix 下可用的五种 I/O 模型的基本区别[1]blockingIO - 阻塞I/O[2]nonblockingIO - 非阻塞I/O[3]IOmultiplexing – I/O多路复用select和poll[4]signaldrivenIO - 信号驱动I/O[5]asynchronousIO - 异步I/OPOSIX aio函数输入操作通常有两个不同的阶段等待数据准备好。这涉及等待数据到达网络。当数据包到达时它被复制到内核中的缓冲区中。将数据从内核复制到进程。这意味着将准备好的数据从内核缓冲区复制到我们的应用程序缓冲区。阻塞 I/O 模型最流行的 I/O 模型是阻塞 I/O 模型我们在前面部分的所有示例中都使用了该模型。默认情况下所有套接字都是阻塞的。场景如下图所示使用UDP而不是TCP作为例子的原因在于就UDP而言数据准备好读取的概念比较简单:要么整个数据报已经收到要么还没有。然而对于TCP来说诸如套接字低水位标记( low-water mark等额外变量开始起作用导致这个概念变得复杂。在这个例子中,我们把recvfrom函数视为系统调用,因为我们正在区分应用进程和内核。不论它如何实现(在源自Berkeley的内核上是作为系统调用在System V内核上是作为调用系统调用getmsg的函数)一般都会从在应用进程空间中运行切换到在内核空间中运行一段时间之后再切换回来。非阻塞 I/O 模型当套接字设置为非阻塞时我们是在告诉内核当我请求的 I/O 操作在不让进程进入睡眠状态的情况下无法完成时不要让进程进入睡眠状态而是返回一个错误。对于前三个recvfrom没有要返回的数据内核立即返回一个错误EWOULDBLOCK。我们第四次调用recvfrom数据报准备好了它被复制到我们的应用程序缓冲区并recvfrom成功返回。然后我们处理数据。当应用程序处于循环调用recvfrom这样的非阻塞描述符时它被称为轮询。应用程序不断地轮询内核以查看某些操作是否已准备好。这通常会浪费 CPU 时间但偶尔会遇到这种模型通常在专用于一个功能的系统上。非阻塞IO图示视频演示非阻塞模式下前三次recvfrom返回 EWOULDBLOCK 错误第四次数据就绪后成功读取并处理。该演示展示了应用程序不断轮询内核的过程。I/O 复用模型有了IO复用(IO multiplexing)我们就可以调用select或poll阻塞在这两个系统调用中的某一个之上而不是阻塞在真正的IO系统调用上。我们阻塞于select调用等待数据报套接字变为可读。当select返回套接字可读这一条件时我们调用recvfrom把所读数据报复制到应用进程缓冲区。与阻塞型IO比较IO复用并不显得有什么优势事实上由于使用select需要两个而不是单个系统调用IO复用还稍有劣势。使用select的优势在于我们可以等待多个描述符就绪。异步 I/O 模型异步 I/O由 POSIX 规范定义各种标准中出现的实时功能的各种差异共同形成了当前的 POSIX 规范。我们也可以用信号让内核在描述符就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动式IO ( signal-driven I/O)。我们首先开启套接字的信号驱动式I/O功能并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回我们的进程继续工作也就是说它没有被阻塞。当数据报准备好读取时内核就为该进程产生一个sIGIo信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报并通知主循环数据已准备好待处理也可以立即通知主循环让它读取数据报。无论如何处理SIGIO信号这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行只要等待来自信号处理函数的通知:既可以是数据已准备好被处理也可以是数据报已准备好被读取。各种I/O模型的比较对比上述5种不同的IO模型。可以看出前4种模型的主要区别在于第一阶段因为它们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间进程阻塞于recvfrom调用。相反异步I/O模型在这两个阶段都要处理从而不同于其他4种模型。同步IO和异步IO对比POSIX把这两个术语定义如下:同步IO操作(synchronous IO opetation导致请求进程阻塞直到IO操作完成;异步IO操作 asynchronous I/O opetation不导致请求进程阻塞。selectselect 是操作系统提供的系统调用函数select()用来等待文件描述词普通文件、终端、伪终端、管道、FIFO、套接字及其他类型的字符型状态的改变。是一个轮循函数循环询问文件节点可设置超时时间超时时间到了就跳过代码继续往下执行。#includesys/select.h#includesys/time.h#defineFD_SETSIZE1024#defineNFDBITS(8*sizeof(unsignedlong))#define__FDSET_LONGS(FD_SETSIZE/NFDBITS)// fd_set结构体定义位图实现typedefstruct{unsignedlongfds_bits[__FDSET_LONGS];}fd_set;// select函数原型intselect(intmax_fd,// 最大文件描述符1fd_set*readset,// 读描述符集合fd_set*writeset,// 写描述符集合fd_set*exceptset,// 异常描述符集合structtimeval*timeout// 超时时间);// 返回值就绪描述符的数目// 操作宏FD_ZERO(fd,fd_set*fds)// 清空集合FD_SET(intfd,fd_set*fds)// 将给定的描述符加入集合FD_ISSET(intfd,fd_set*fds)// 判断指定描述符是否在集合中FD_CLR(intfd,fd_set*fds)// 将给定的描述符从集合中删除select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是单个进程可监视的fd数量被限制即能监听端口的大小有限。一般来说这个数目和系统内存关系很大具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048。对socket进行扫描时是线性扫描即采用轮询的方法效率较低当套接字比较多的时候每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。需要维护一个用来存放大量fd的数据结构这样会使得用户空间和内核空间在传递该结构时复制开销大。select效果图示poll相关函数定义#includepoll.hintpoll(structpollfd*fds,nfds_t nfds,inttimeout);#includesignal.h#includepoll.hintppoll(structpollfd*fds,nfds_t nfds,conststructtimespec*tmo_p,constsigset_t*sigmask);// pollfd结构体structpollfd{intfd;// 文件描述符shortevents;// 关注的事件输入shortrevents;// 返回的事件输出};和select用三组文件描述符不同的是poll只有一个pollfd数组数组中的每个元素都表示一个需要监听IO操作事件的文件描述符。events参数是我们需要关心的事件revents是所有内核监测到的事件。poll本质上和select没有区别它将用户传入的数组拷贝到内核空间然后查询每个fd对应的设备状态如果设备就绪则在设备等待队列中加入一项并继续遍历如果遍历完所有fd后没有发现就绪设备则挂起当前进程直到设备就绪或者主动超时被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。它没有最大连接数的限制原因是它是基于链表来存储的但是同样有一个缺点大量的fd的数组被整体复制于用户态和内核地址空间之间而不管这样的复制是不是有意义。poll还有一个特点是水平触发如果报告了fd后没有被处理那么下次poll时会再次报告该fd。epoll相关函数定义如下#includesys/epoll.h// 创建epoll实例intepoll_create(intsize);intepoll_create1(intflags);// 注册/修改/删除监听事件intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);// 等待事件就绪intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);intepoll_pwait(intepfd,structepoll_event*events,intmaxevents,inttimeout,constsigset_t*sigmask);epoll有EPOLLLT和EPOLLET两种触发模式LT是默认的模式ET是高速模式。LT模式下只要这个fd还有数据可读每次 epoll_wait都会返回它的事件提醒用户程序去操作ET模式下它只会提示一次直到下次再有数据流入之前都不会再提示了无论fd中是否还有数据可读。所以在ET模式下read一个fd的时候一定要把它的buffer读完或者遇到EAGAIN错误。epoll效果演示视频演示epoll 使用回调机制只有活跃的文件描述符才会触发事件通知。演示了 ET边缘触发和 LT水平触发两种模式的区别以及 epoll 的高效性。边缘触发模式ET:当被监控的Socket描述符上有可读事件发生时服务器只会从 epoll_wait中苏醒一次即使进程没有调用read函数从内核读取数据也依然只苏醒一次因此我们程序要保证一次性将内核缓冲区的数据读取完只有第一次满足条件的时候才触发之后就不会再传递同样的事件了。水平触发模式LT:当被监控的Socket上有可读事件发生时服务器不断地从 epoll_wait中苏醒直到内核缓冲区数据被read函数读完才结束目的是告诉我们有数据只要满足事件的条件比如内核中有数据需要读,就一直不断地把这个事件传递给用户。epoll的优点没有最大并发连接的限制能打开的FD的上限远大于10241G的内存上能监听约10万个端口效率提升不是轮询的方式不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数即Epoll最大的优点就在于它只管你活跃的连接而跟连接总数无关因此在实际的网络环境中Epoll的效率就会远远高于select和poll。内存拷贝利用mmap()文件映射内存加速与内核空间的消息传递即epoll使用mmap减少复制开销。epoll缺点epoll只能工作在linux下。select和epoll的区别:select和poll采用轮询的方式检查就绪事件每次都要扫描整个文件描述符复杂度O(N);epoll采用回调方式检查就绪事件只会返回有事件发生的文件描述符的个数复杂度O(1);select 只工作在低效的LT模式,epoll可以在ET高效模式工作;epoll是Linux所特有而select则应该是POSIX所规定一般操作系统均有实现;select单个进程可监视的fd数量有限即能监听端口的大小有限64位是2048;epoll没有最大并发连接的限制能打开的fd的上限远大于2048 (1G的内存上能监听约10万个端口);select 内核需要将消息传递到用户空间都需要内核拷贝动作;epoll通过内核和用户空间共享一块内存来实现的。