可是我们一般选择多是多路复用,而不是异步IO。为什么呢?因为首先异步IO看起来是让别人钓鱼,但是在程序中我们就要创建线程或子进程去做事,本身就要耗费较大资源,而且一旦设计多线程,就可能会出现很多问题,大幅度提高程序的复杂性!所以我们一般更青睐于多路转接的方法! 
   复制一个现有的描述符( cmd=F_DUPFD ) .  
  获得 / 设置文件描述符标记 (cmd=F_GETFD 或 F_SETFD).  
  获得 / 设置文件状态标记 (cmd=F_GETFL 或 F_SETFL).  
  获得 / 设置异步 I/O 所有权 (cmd=F_GETOWN 或 F_SETOWN).  
  获得 / 设置记录锁 (cmd=F_GETLK,F_SETLK 或 F_SETLKW). 
  比如下面的程序就是先获取一个程序的旧的标志位,然后我们再设置其为非阻塞! 
   接下来我们就可以学习select呢 
  返回值:n>0的时候就代表有多少个fd已经就绪,n==0就是没有就绪但是倒计时已经到了,n<0就是出错了! 
   timeval是一个结构体,如下图,分别是秒和微秒! 
    并且timeval还是个输入输出形参数! 
 也即比如我们设置的是5,0也即5秒返回一次,如果过了两秒就有就绪了,那么timeval这个时候就会使3,0. 
  然后我们再来了解最重要的是fd_set是什么? 
  其实就是一张位图,readfds里面响应的位置如果被设置为1,则关心相应事件的读,writefds则关心写! 
  当传入的时候我们就是告诉内核那个fd我们要关心,返回的时候,传出的就是那个fd已经可以读取了!不会产生等待了! 
  我们在这里先讲一下select的一些限制,既然fd_set是结构体,那么其中等待的fd有没有上限呢?是多少呢? 
    不同的系统可能跑出来的是不一样的,但是大差不差! 
  并且我们每一次都要重新给select传表,我们还要用一个辅助数组提前记录哪些fd我们要监听,也是一个开销! 
  所以我们可以总结一下select的优缺点 
  优点:主要是已经实现了多路转接了! 
  缺点: 
 1.fd有上限! 
 2.输入输出型参数多,需要不停的数据拷贝和遍历修改,会导致效率低下! 
 3.而且还要辅助数组记录我们要关心的fd 
  下面是一个基于select的多路转接的单词翻译器的实现! 
     然后我们在学习epoll之前,先看一下poll! 
   第一个参数就是一个struct pollfd的数组 
 第二个参数就是数组中元素的个数 
 第三个参数与select一致 
  至于struct pollfd的结构主要包含关心的fd,还有两个短整形变量! 
  如果想要添加指令就只用将短整数&上对应的宏就可以了!!! 
 其中events是传入给poll的,revents是poll要写入的,我们进行读取就可以! 
  poll相对于select首先克服了数量有限的问题,并且减少了遍历,并且不用我们维护辅助数组了! 
 也更容易编写!!! 
  但是我们仍然需要去不停遍历整个数组,那么当数量增多以后,主要矛盾就从等待变成了遍历了! 
  如果学会了select,那poll难度应该不大,就不再过多赘述!而epoll则在此基础之上又大幅度提高效率,所以下面我们来学习epoll!!! 
       epoll 的事件注册函数 .  
  它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件 ,  而是在这里先注册要监听的事件类型 .  
  第一个参数是 epoll_create() 的返回值 (epoll 的句柄 ).  
  第二个参数表示动作,用三个宏来表示 .  
  第三个参数是需要监听的 fd.  
  第四个参数是告诉内核需要监听什么事 .  
  第二个参数的取值 :  
  EPOLL_CTL_ADD  :注册新的 fd 到 epfd 中;  
  EPOLL_CTL_MOD  :修改已经注册的 fd 的监听事件;  
  EPOLL_CTL_DEL  :从 epfd 中删除一个 fd ;  
  struct epoll_event 结构如下 
    
     events 可以是以下几个宏的集合:  
  EPOLLIN :  表示对应的文件描述符可以读  ( 包括对端 SOCKET 正常关闭 );  
  EPOLLOUT :  表示对应的文件描述符可以写 ;  
  EPOLLPRI :  表示对应的文件描述符有紧急的数据可读  ( 这里应该表示有带外数据到来 );  
  EPOLLERR :  表示对应的文件描述符发生错误 ;  
  EPOLLHUP :  表示对应的文件描述符被挂断 ;  
  EPOLLET :  将 EPOLL 设为边缘触发 (Edge Triggered) 模式 ,  这是相对于水平触发 (Level Triggered) 来说的 . EPOLLONESHOT:只监听一次事件 ,  当监听完这次事件之后 ,  如果还需要继续监听这个 socket 的话 ,  需要 再次把这个socket 加入到 EPOLL 队列里 
  
     收集在 epoll 监控的事件中已经发送的事件 .  
  参数 events 是分配好的 epoll_event 结构体数组 .  
  epoll 将会把发生的事件赋值到 events 数组中  (events 不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存 ).  
  maxevents 告之内核这个 events 有多大,这个  maxevents 的值不能大于创建 epoll_create() 时的 size.  
  参数 timeout 是超时时间  ( 毫秒, 0 会立即返回, -1 是永久阻塞 ).  
  如果函数调用成功,返回对应 I/O 上已准备好的文件描述符数目,如返回 0 表示已超时 ,  返回小于 0 表示函 数失败 
   学了epoll的基本使用以后,我们来学习一下epoll的原理! 
  
     当某一进程调用 epoll_create 方法时, Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个成 员与epoll 的使用方式密切相关 .  
   主要是一个红黑树和一个双链表的队列! 
 每个被检测的fd都会被放入红黑树,一旦事件就绪,就会调用回调函数,让操作系统从红黑树中找到相关的rbn成员然后读取其信息,然后在等待队列中添加,这样就我们进行读取事件的时候就是在等待队列中读取事件了,并且在这个过程中事件主动回调,时间复杂度为o(1),比我们之前的select和poll效率拥有了质的提升!!! 
    总结一下 , epoll 的使用过程就是三部曲 :  
  调用 epoll_create 创建一个 epoll 句柄 ;  
  调用 epoll_ctl,  将要监控的文件描述符进行注册 ;  
  调用 epoll_wait,  等待文件描述符就绪 ; 
     epoll 的优点 ( 和  select  的缺点对应 )  
  接口使用方便 :  虽然拆分成了三个函数 ,  但是反而使用起来更方便高效 .  不需要每次循环都设置关注的文 件描述符,  也做到了输入输出参数分离开  
  数据拷贝轻量 :  只在合适的时候调用  EPOLL_CTL_ADD  将文件描述符结构拷贝到内核中 ,  这个操作并不频 繁( 而 select/poll 都是每次循环都要进行拷贝 )  
  事件回调机制 :  避免使用遍历 ,  而是使用回调函数的方式 ,  将就绪的文件描述符结构加入到就绪队列中 , epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪 .  这个操作时间复杂度 O(1).  即使文件描述 符数目很多,  效率也不会受到影响 . 没有数量限制:  文件描述符数目无上限 
   我们再来讲一下epoll的工作模式! 
  epoll有2种工作方式-水平触发(LT)和边缘触发(ET) 
    假如有这样一个例子 :  
  我们已经把一个 tcp socket 添加到 epoll 描述符  
  这个时候 socket 的另一端被写入了 2KB 的数据  
  调用 epoll_wait ,并且它会返回 .  说明它已经准备好读取操作  
  然后调用 read,  只读取了 1KB 的数据  
  继续调用 epoll_wait......  
  水平触发 Level Triggered  工作模式  
  epoll 默认状态下就是 LT 工作模式 .  
  当 epoll 检测到 socket 上事件就绪的时候 ,  可以不立刻进行处理 .  或者只处理一部分 .  
  如上面的例子 ,  由于只读了 1K 数据 ,  缓冲区中还剩 1K 数据 ,  在第二次调用  epoll_wait  时 ,  epoll_wait  
  仍然会立刻返回并通知 socket 读事件就绪 .  
  直到缓冲区上所有的数据都被处理完 ,  epoll_wait  才不会立刻返回 .  
  支持阻塞读写和非阻塞读写  
  边缘触发 Edge Triggered 工作模式  
  如果我们在第 1 步将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志 , epoll 进入 ET 工作模式 .  
  当 epoll 检测到 socket 上事件就绪时 ,  必须立刻处理 .  
  如上面的例子 ,  虽然只读了 1K 的数据 ,  缓冲区还剩 1K 的数据 ,  在第二次调用  epoll_wait  的时候 ,  
  epoll_wait  不会再返回了 .  
  也就是说 , ET 模式下 ,  文件描述符上的事件就绪后 ,  只有一次处理机会 .  
  ET 的性能比 LT 性能更高 (  epoll_wait  返回的次数少了很多 ). Nginx 默认采用 ET 模式使用 epoll.  
  只支持非阻塞的读写  
  select 和 poll 其实也是工作在 LT 模式下 . epoll 既可以支持 LT,  也可以支持 ET. 
    对比 LT 和 ET  
  LT 是  epoll  的默认行为 .  使用  ET  能够减少  epoll  触发的次数 .  但是代价就是强逼着程序猿一次响应就绪过程中就把 所有的数据都处理完.  
  相当于一个文件描述符就绪之后 ,  不会反复被提示就绪 ,  看起来就比  LT  更高效一些 .  但是在  LT  情况下如果也能做到  
  每次就绪的文件描述符都立刻处理,  不让这个就绪被重复提示的话 ,  其实性能也是一样的 .  
  另一方面 , ET  的代码复杂程度更高了 . 
      理解 ET 模式和非阻塞文件描述符  
  使用  ET  模式的  epoll,  需要将文件描述设置为非阻塞 .  这个不是接口上的要求 ,  而是  " 工程实践 "  上的要求 .  
  假设这样的场景 :  服务器接受到一个 10k 的请求 ,  会向客户端返回一个应答数据 .  如果客户端收不到应答 ,  不会发送第 二个10k 请求 .  
  如果服务端写的代码是阻塞式的read,  并且一次只  read 1k  数据的话 (read 不能保证一次就把所有的数据都读出来 , 参考 man  手册的说明 ,  可能被信号打断 ),  剩下的 9k 数据就会待在缓冲区中 
   此时由于  epoll  是 ET 模式 ,  并不会认为文件描述符读就绪 .  epoll_wait  就不会再次返回 .  剩下的  9k  数据会一直在缓 冲区中.  直到下一次客户端再给服务器写数据 .  epoll_wait  才能返回 但是问题来了.  
  服务器只读到 1k 个数据 ,  要 10k 读完才会给客户端返回响应数据 .  
  客户端要读到服务器的响应 , 才会发送下一个请求 客户端发送了下一个请求,  epoll_wait  才会返回 ,  才能去读缓冲区中剩余的数据 
   所以 ,  为了解决上述问题 ( 阻塞 read 不一定能一下把完整的请求读完 ),  于是就可以使用非阻塞轮训的方式来读缓冲区 , 保证一定能把完整的请求都读出来.  
  而如果是 LT 没这个问题 .  只要缓冲区中的数据没读完 ,  就能够让  epoll_wait  返回文件描述符读就绪 . 
   也即如果设置为非阻塞的时候,我们反复读取,一旦读完就会出错返回,如果阻塞模式,我们就会阻塞在读的地方,这显然是我们不能接受的,所以我们要把读设为非阻塞模式!!! 
    epoll 的使用场景  
  epoll 的高性能 ,  是有一定的特定场景的 .  如果场景选择的不适宜 , epoll 的性能可能适得其反 .  
  对于多连接 ,  且多连接中只有一部分连接比较活跃时 ,  比较适合使用 epoll.  
  例如 ,  典型的一个需要处理上万个客户端的服务器 ,  例如各种互联网 APP 的入口服务器 ,  这样的服务器就很适合 epoll.  
  如果只是系统内部 ,  服务器和服务器之间进行通信 ,  只有少数的几个连接 ,  这种情况下用 epoll 就并不合适 .  具体要根 据需求和场景特点来决定使用哪种IO模型 
   1.epoll惊群效应产生的原因
 在Linux下使用epoll编写过socket的服务端程序,在多线程环境下可能会遇到epoll的惊群效应。那么什么是惊群效应呢。其产生的原因是什么呢?
 在多线程或者多进程环境下,有些人为了提高程序的稳定性,往往会让多个线程或者多个进程同时在epoll_wait监听的socket描述符。当一个新的链接请求进来时,操作系统不知道选派那个线程或者进程处理此事件,则干脆将其中几个线程或者进程给唤醒,而实际上只有其中一个进程或者线程能够成功处理accept事件,其他线程都将失败,且errno错误码为EAGAIN。这种现象称为惊群效应,结果是肯定的,惊群效应肯定会带来资源的消耗和性能的影响。
 那么如何解决这个问题。
 2.惊群问题的解决方法
 多线程环境下解决惊群解决方法
 这种情况,不建议让多个线程同时在epoll_wait监听的socket,而是让其中一个线程epoll_wait监听的socket,当有新的链接请求进来之后,由epoll_wait的线程调用accept,建立新的连接,然后交给其他工作线程处理后续的数据读写请求,这样就可以避免了由于多线程环境下的epoll_wait惊群效应问题。
 最后再把基于epoll的多路转接翻译服务器的代码贴在这里,这篇文章就到底为止了!
 
 感谢观看!