
}目录高级IO1.五种IO模型1.1阻塞IO1.2非阻塞IO1.3信号驱动IO1.4多路转接IO1.5 异步IO2.高级IO重要概念3.IO多路转接3.1 select3.2select服务器的初步编写3.2.1理解select执行过程3.2.2select服务器简单代码3.3select的特点和缺点3.2 poll3.3poll的缺点3.4poll的服务器初步编写3.5 epoll3.5.1epoll系统调用接口3.5.2epoll工作原理3.5.3epoll的优点3.5.4epoll的工作方式3.5.5对比LT和ET3.5.6简单的epoll服务器编写4.Reactor模式高级IO重点理解五种IO模型的基本概念, 重点是IO多路转接.掌握select编程模型, 能够实现select版本的TCP服务器.掌握poll编程模型, 能够实现poll版本的TCP服务器.掌握epoll编程模型, 能够实现epoll版本的TCP服务器.理解epoll的LT模式和ET模式.理解select和epoll的优缺点对比.1.五种IO模型在了解IO模型之前一定要理解IO为什么分高效和低效这是因为IO是等数据拷贝其中如何缩短等的时间就区分了高效和低效。1.1阻塞IO阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.阻塞IO是最常见的IO模型1.2非阻塞IO非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用.1.3信号驱动IO信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作1.4多路转接IOIO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.1.5 异步IO异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).小结:任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.2.高级IO重要概念同步通信 vs 异步通信(synchronous communication/asynchronouscommunication)同步和异步关注的是消息通信机制.所谓同步就是在发出一个调用时在没有得到结果之前该调用就不返回. 但是一旦调用返回就得到返回值了; 换句话说就是由调用者主动等待这个调用的结果;异步则是相反调用在发出之后这个调用就直接返回了所以没有返回结果; 换句话说当一个异步过程调用发出后调用者不会立刻得到结果; 而是在调用发出后被调用者通过状态、通知来通知调用者或通过回调函数处理这个调用.另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不想干的概念.进程/线程同步也是进程/线程之间直接的制约关系是为完成某种任务而建立的两个或多个线程这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候.阻塞vs非阻塞阻塞和非阻塞关注的是程序在等待调用结果消息返回值时的状态.阻塞调用是指调用结果返回之前当前线程会被挂起. 调用线程只有在得到结果之后才会返回.非阻塞调用指在不能立刻得到结果之前该调用不会阻塞当前线程.其他高级IO非阻塞IO纪录锁系统V流机制I/O多路转接也叫I/O多路复用,readv和writev函数以及存储映射IOmmap这些统称为高级IO.此处重点讨论的是I/O多路转接3.IO多路转接3.1 selectselect的接口介绍int select(int nfds,fd_set *readfds,fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);参数nfds是需要监视的最大的文件描述符值1【这是为了确定遍历范围】rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合可写文件描述符的集合及异常文件描述符的集合;参数timeout为结构timeval用来设置select()的等待时间NULL则表示select没有timeoutselect将一直被阻塞直到某个文件描述符上发生了事件;仅检测描述符集合的状态然后立即返回并不等待外部事件的发生。特定的时间值如果在指定的时间段里没有事件发生select将超时返回。这中间的三个参数比较重要不在上面的图片解释在后面解释首先先看readfds参数这个参数是一个位图结构用户需要进行输入其比特位从右到左的位置顺序表示fd的数值比特位的内容表示是否关心该位置。而这个参数是一个输入输出型参数输出的时候会告诉用户你关心的fd中有哪些就绪了。怎么告诉呢即修改readfds位图比特位的位置就是fd的数值比特位的内容表示该fd是否就绪1为就绪0为未就绪而后面的写事件参数writefds和异常事件参数eaxceptfds也是一样的。3.2select服务器的初步编写3.2.1理解select执行过程理解select模型的关键在于理解fd_set,为说明方便取fd_set长度为1字节fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.1执行fd_set set; FD_ZERO(set);则set用位表示是0000,0000。2若fd5,执行FD_SET(fd,set);后set变为0001,0000(第5位置为1)3若再加入fd2fd1,则set变为0001,00114执行 select(6,set,0,0,0)阻塞等待5若fd1,fd2上都发生可读事件则select返回此时set变为0000,0011。注意没有事件发生的fd5被清空。3.2.2select服务器简单代码这里的服务器代码其实并不完善只是为了更好的理解select的特性代码文件较多这里直接给个网址在gitee上查看select服务器的简单代码3.3select的特点和缺点select的特点可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)128每bit表示一个文件描述符则我服务器上支持的最大文件描述符是128*81024.将fd加入select监控集的同时还要再使用一个数据结构array保存放到select监控集中的fd一是用于再select 返回后array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空则每次开始select前都要重新从array取得 fd逐一加入(FD_ZERO最先)扫描array的同时取得fd最大值maxfd用于select的第一个参数。备注: fd_set的大小可以调整可能涉及到重新编译内核.select的缺点每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.每次调用select都需要把fd集合从用户态拷贝到内核态这个开销在fd很多时会很大同时每次调用select都需要在内核遍历传递进来的所有fd这个开销在fd很多时也很大select支持的文件描述符数量太小.3.2 poll由于select服务器在实际使用起来还是有一些缺点比如fd有上限每次调用都要重新设置fd_set频繁把fd从用户态拷贝到内核态并且要多次遍历fd、因此poll就出现了。不同与select使用三个位图来表示三个fdset的方式poll使用一个pollfd的指针实现.pollfd结构包含了要监视的event和发生的event不再使用select“参数-值”传递的方式. 接口使用比 select更方便.poll并没有最大数量限制(但是数量过大后性能也是会下降).3.3poll的缺点poll中监听的文件描述符数目增多时和select函数一样poll返回后需要轮询pollfd来获取就绪的描述符.每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.3.4poll的服务器初步编写poll就是更简单的select因此理解了selectpoll自然也能理解了有关poll服务器的简单实现贴个链接不然太长了3.5 epoll3.5.1epoll系统调用接口epoll_createint epoll_create(int size);创建一个epoll的句柄用完之后, 必须调用close()关闭.epoll_ctlint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.第一个参数是epoll_create()的返回值(epoll的句柄).第二个参数表示动作用三个宏来表示.第三个参数是需要监听的fd.第四个参数是告诉内核需要监听什么事.第二个参数的取值:EPOLL_CTL_ADD注册新的fd到epfd中EPOLL_CTL_MOD修改已经注册的fd的监听事件EPOLL_CTL_DEL从epfd中删除一个fdstruct epoll_event结构如下:events可以是以下几个宏的集合EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);EPOLLOUT : 表示对应的文件描述符可以写;EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);EPOLLERR : 表示对应的文件描述符发生错误;EPOLLHUP : 表示对应的文件描述符被挂断;EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.EPOLLONESHOT只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里.3.5.2epoll工作原理每一个epoll对象都有一个独立的eventpoll结构体用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.这些事件都会挂载在红黑树中如此重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn其中n为树的高度).而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系也就是说当响应的事件发生时会调用这个回调方法.这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.在epoll中对于每一个事件都会建立一个epitem结构体.struct epitem{struct rb_node rbn;//红黑树节点struct list_head rdllink;//双向链表节点struct epoll_filefd ffd; //事件句柄信息struct eventpoll *ep; //指向其所属的eventpoll对象struct epoll_event event; //期待发生的事件类型}当调用epoll_wait检查是否有事件发生时只需要检查eventpoll对象中的rdlist双链表中是否有epitem 元素即可.如果rdlist不为空则把发生的事件复制到用户态同时将事件数量返回给用户. 这个操作的时间复杂度是O(1).总结一下, epoll的使用过程就是三部曲:调用epoll_create创建一个epoll句柄;调用epoll_ctl, 将要监控的文件描述符进行注册;调用epoll_wait, 等待文件描述符就绪;下面是个人的总结epoll,是如何解决了poll的缺点的呢这是因为在内核里不再是数组来装文件描述符而是采用了红黑树就绪队列的组合为什么这样就能减少遍历成本呢红黑树减少查找成本而就绪队列就可以减少遍历成本不就绪的fd不会在就绪队列中因此不需要遍历红黑树来找到就绪fd怎么实现让就绪的fd装在就绪队列呢在红黑树的节点类添加两个属性即链表的上下指针可是OS怎么知道节点是否就绪这是通过struct file里面的回调函数指针实现的一旦底层数据通过协议栈传达到缓冲区就一直调用回调函数然后将该fd的节点设置到就绪队列中。这样就完成的将红黑树的节点转移到就绪队列中epoll_ctl就是在红黑树上进行增删改epoll_wait就是从就绪队列中把就绪的fd拿上来【不需要遍历检测只需要遍历是否就绪】3.5.3epoll的优点口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开数据拷贝轻量: 只在合适的时候调用EPOLL_CTL_ADD将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.没有数量限制: 文件描述符数目无上限.3.5.4epoll的工作方式epoll有2种工作方式-水平触发(LT)和边缘触发(ET)有什么区别呢下面举个例子你正在吃鸡, 眼看进入了决赛圈, 你妈饭做好了, 喊你吃饭的时候有两种方式:如果你妈喊你一次, 你没动, 那么你妈会继续喊你第二次, 第三次…(亲妈, 水平触发)如果你妈喊你一次, 你没动, 你妈就不管你了(后妈, 边缘触发)水平触发Level Triggered工作模式当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用epoll_wait 时, epoll_wait 仍然会立刻返回并通知socket读事件就绪.直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.支持阻塞读写和非阻塞读写边缘触发Edge Triggered工作模式当epoll检测到socket上事件就绪时, 必须立刻处理.如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用epoll_wait 的时候, epoll_wait 不会再返回了.也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.只支持非阻塞的读写简单来说就是LT模式下事件就绪后只要数据没有处理完会不断通知事件就绪等待下一次epoll_wait的处理不会将就绪事件从就绪队列移除ET模式下不管数据有没有处理完通知一次后不会在继续通知用户事件就绪将就绪事件从就绪队列移除直到底层数据再次变化的时候会再次变化才会在通知一次事件就绪3.5.5对比LT和ETLT是epoll 的默认行为. 使用ET 能够减少epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比LT 更高效一些. 但是在LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.另一方面, ET 的代码复杂程度更高了LT非常好理解至于这个ET为什么能比LT更高效呢可以看下图理解只会通知一次但是这个不能保证高效主要是会倒逼上层的程序员必须一次处理完所有的数据–这就会让每次的底层的缓冲区数据被清空–TCP可以提供更大的滑动窗口–让对方发送更大的TCP报文提升吞吐量–最终提升·效率提升了底层的数据发送效率而之前TCP协议中报头里有一个PSH这个作用就是让底层数据就绪让上层处理注意ET模式下对应的fd一定是非阻塞的为什么呢因为无法保证一次就能将底层数据全部处理完毕因此就要循环处理直至数据处理完毕但是常态下fd都是阻塞式的fd。而LT模式下阻塞或者非阻塞都可以3.5.6简单的epoll服务器编写具体代码放这里代码链接4.Reactor模式reactor是一种半异步半同步的IO模式是Linux网络下最常用的一种IO设计模式具体理解Reactor模式要通过项目来理解这里是项目链接my_project · WZF-sang/LinuxCode - 码云 - 开源中国