epoll/poll/epoll & 高级IO详解

五种IO模型

阻塞IO

  • 阻塞IO:在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式

非阻塞IO

  • 非阻塞IO:如果内核的还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码
  • 非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询,这对CPU来说是较大的浪费,只有特定的场景下才使用。

信号驱动

  • 信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作

IO多路转接

  • IO多路转接:虽然从流程图上看起来和阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态

异步IO

  • 异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据),不需要自己主动获取
  • 任何IO过程,都包含两个步骤,第一是等待,第二是拷贝,而且在实际的应用场景中,等待消耗的时间玩往往高于拷贝的时间,让IO更高效,最核心的办法就是让等待的时间尽量少

钓鱼

  • 阻塞
  • 非阻塞:钓鱼的时候玩玩手机,再看看鱼
  • 信号:鱼缸上绑铃铛,铃铛响了了
  • IO多路转接:阻塞的,扎了一排鱼杆,一眼不眨的等鱼
  • 异步IO:鱼竿是自动的,可以自己钓鱼,鱼钓上来就通知

高级IO的重要概念

同步通信和异步通信

同步和异步关注的是消息通信机制

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不反悔,但是一旦返回,就得到了返回值了,换句话说,就是由调用者主动等待这个调用过程
  • 异步则相反,调用在出发之后,这个调用就直接返回了,所以没有返回结果,换句话说,当一个异步过程调用出发后,调用者不会立刻得到结果,而是在调用出发后,被调用者通过状态,通知调用者,或通过回调函数处理这个调用
  • 这里的同步和多线程的同步和互斥中的同步不是一个东西,不要混淆
  • 例如epoll的回调函数机制,通过回调函数来直接修改数组中的标记,调用epoll不需要返回结果,这个场景就是异步。

例如

假设你去吃饭,点了一个炒菜
(1)同步就是你点饭后,一直在那里等着厨师给你做,直到厨师做完,你自己把饭端走,即就是说结果由你自己主动获取
(2)异步,就是点饭之后,去找一个位置作者,当服务员告诉你的饭做好了,你去把饭端过来,即就是说,结果由服务员通知你,你自己使被动的得知调用结果,如果服务员不通知,你是不知道结果的
  • 在于这个结果你是怎么知道的
  • 是主动的还是被动的知道的

阻塞和非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
  • 关注调用函数的时候是否阻塞

同步阻塞

  • 调用结果由调用者主动获取,且是阻塞式的等待,即调用者会主动的关注调用结果,一直在那看着

同步非阻塞

  • 调用结果是由调用者主动获取,但是是非阻塞的等着,轮询的方式,时不时回来看一下执行的结果。

异步阻塞

  • 调用结果是被调用者通知给调用者的,而且是阻塞式的在哪里等着,并且自己不光等着,也不会看执行得怎么样了,而是等待被调用者的通知

异步非阻塞I

  • 调用结果是由被调用者通知给调用者就行了,调用者自己可以去干其他事,不需要主动的获取结果,同时自己也不会被阻塞。

等女朋友的例子:

同步阻塞

  • 等女朋友,单纯的等,啥都不干,就一直看着她有没有做完事
    同步非阻塞
  • 等女朋友的时候还一般等一边打游戏,打会游戏然后看看她做完事没
    异步阻塞
  • 等女朋友,然后什么也不做,就一直等,但是也不会关注她什么时候收拾好,等她收拾好了会主动通知你,然后你就知道她收拾好了
    异步非阻塞
  • 等女朋友,然后你还边敲代码,你也不用去关注她收拾好没,她会主动通知你,然后你就知道她完事了

fcntl

  • 一个文件描述符,默认都是阻塞IO,fcntl有能力让阻塞变成阻塞
函数原型
#include 
#include 
int fcntl(int fd,int cmd,.../*arg*/);

根据传入的cmd值不同,后面追加的参数也不相同
,fcntl函数有五种功能

  • 复制一个现有的文件描述符 (cmd = F_DUPFD)
  • 获得/设置文件描述符标记 (cmd = F_GETFD 或 F_SETFD)
  • 获得/设置文件状态标记 (cmd = F_GETFL或 F_SETEL)
  • 获得/设置异步I/O所有权 (cmd = F_GETOWN或F_SETOWN)
  • 获得/设置记录锁 (cmd = F_GETLK,F_SETLK 或 F_SETLKW)

我们此处只是用三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞

基于fcntl实现一个SetNoBlock函数
//传入文件描述符
void SetNoBlock(int fd)
{ 
  //保存修改前的状态信息(一个位图)
  int f1 = fcntl(fd,F_GETFL);//get file
  if(f1<0)
  {
    cerr<<"fcntl"<

select

  • select系统调用是让我们的程序监控多个文件描述符的状态变化的
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

select函数原型

#include 
int select(int nfds,fd_Set *readfds,fd_set * writefds,fd_set* exceptfds,struct timeval * timeout);

参数解释

  • 参数nfds是需要监视的最大的文件描述符值+1
  • rdset,wrset,exset分别对于于需要检测的可读文件描述符的集合,可写文件描述符的集合以及异常文件描述符集合
  • 参数timeout为解雇timeval,用来设置select的等待时间

参数timeout取值

  • NULL表示select没有timeout,一直将阻塞,直到有某个文件描述符上发生了事件
  • 0 仅检测文件描述符的集合状态,然后立即返回,并不等待外部时间的发生
  • 特定的是时间值,如果在指定时间没有事件发生,select将超时返回

fd_set的结构

  • 其实fd_set的结构就是一个整数数组,更严格的说,就是一个"位图",使用位图中对应的位来表示要监视的文件描述符
  • 提供一组操作fd_set的接口,来比较方便的操作位图
void FD_CLR(int fd ,set* set);          //用来清除描述词组set中相关fd的位       
int FD_ISSET(int fd,fd_set* set);       //用来测试描述词组set中相关的fd的位是否为真
void FD_SET(int fd,fd_set* set);        //用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set* set)               //用来清除描述词组set的全部位

关于timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0

struct timeval
{
    __time_t tv_set;//这里设置秒
    __suseconds_t tv_usec;//这里设置微妙
};

函数返回值

  • 执行成功则返回文件描述词状态已改变的个数
  • 如果返回0代表文件描述符状态改变已经超过timeout时间,没有返回
  • 当有错误发生时则返回-1,错误原因存在errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测

错误值可能

  • EBADF 文件描述符为无效的或该文件已经寡女i
  • EINTR 此调用被信号所中断
  • EINVAL 参数n为负值
  • ENOMEN 核心内存不足

select用法

  • 自己建立一个集合
  • 可以监控三种集合,read ,write except(异常事件)
  • 一般是使用read集合
  • 监视多个文件描述符的变化
  • select自己是阻塞的,直到被监视的文件描述符有一个或多个发生了状态改变

select程序流程

实现多个客户端的连接和数据处理(聊天)
1.新连接的描述符会被覆盖,因此需要一个数组保存
2.将监听socket描述符添加到数组中
3.定义一个select可读事件描述符集合
4.将数组中可用的描述符全都添加到集合中,并选择出最大的描述符
5.定义一个select等待的超时事件
6.select开始监控描述符的状态改变
    1)select出错返回
    2)select超时返回
    3)代表有描述符可读,但是我们不知道哪一个描述符可读,但是select返回之前干了一件事,将没有就绪的描述符从集合中移除,意味着现在集合中存在的描述符都是就绪描述符
    4)现在判断数组中的哪一个描述符还继续在集合中,如果在,就代表这个描述符是就绪状态的
        1))如果这个就绪的描述符是监听描述符,代表有新连接,接受链接
        2))如果不是,代表连接有数据到来,接受数据

关于socket的就绪条件

###读就绪

  • socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读写该文件描述符,并且返回值大于0
  • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
  • 监听的socket上有新的连接请求
  • socket上有未处理的错误

写就绪

  • socket内核中,发送缓冲区中的可用字节大于等于低水位标记,SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
  • socket的写操作被关闭,对于一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
  • socket使用非阻塞connect连接成功或失败之后
  • socket上有未读取的错误

异常就绪

  • socket上收到带外数据,关于带外数据,和TCP紧急模式相关,回忆TCP协议头中,有一个紧急指针的字段

select缺点

  • 0.select能够监控的最大描述符个数是有限的
  • 1.每次都需要将监控的描述符从用户态中拷贝到内核态
    ,效率下降
  • 2.select并不会直接告诉我们哪一个描述符就绪了,而是将未就绪的描述符从集合中移除,因此需要遍历所有的描述符就是否在集合中,才知道到底哪一个就绪,需要遍历,描述符太多就会效率下降
  • 3.因为select每次都会修改监控集合的内容,因此每次调用select之前需要重新添加描述符到集合中
  • 4.因为以上的原因,因此代码编写稍显复杂

select优点

  • 跨平台,windows下也有select
  • 多路复用和多线程/多进程对比
  • 在资源足够的欠款下,多线程多进程可以并行,但是多路复用同一时间仅能接受一个请求,只能并发
  • 但是多路复用省资源
  • 多路复用技术用于有大量连接,但是同一时间只有少量活跃连接的情况

I/O多路转接之poll

  • 相比select就是没有最大描述符上限
  • 备注: fd_set的大小可以调整,可能涉及到重新编译内核. 感兴趣的同学可以自己去收集相关资料

poll函数接口

#include 
int poll(struct pollfd* fds,nfds_t nfds,int timeout);

struct pollfd
{
    int fd;
    short events;
    short revents;
}

示例:
int ret = poll(events,10,1000);
//events是一个pollfd数组,10是代表数组的长度,1000表示超时时间是1000毫秒
  • fds是一个poll函数监听的结构列表,每个元素中,包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合
  • nfds表示fds数组的长度
  • timeout表示poll函数的超时时间,单位是毫秒

events和revents的取值

事件          描述                            是否可作为输入  是否可作为输出
POLLIN        数据                               是               是
POLLRDNORM    普通数据可读                       是               是
POLLRDBAND    优先级带数据可读                   是               是
POLLPRI       高优先级数据可读,比如TCP外带数据  是               是
POLLOUT       数据,可写                         是               是
POLLWANORM    普通数据可写                       是               是
POLLRDHUP     TCP连接被对方关闭,或者对方关闭    是               是
POLLERR       错误                               否               是
POLLHUP       挂起                               否               是
POLLNVAL      文件描述符没有打开                 否               是

返回结果

  • 返回值小于0,表示出错
  • 返回值等于0,表示poll函数等待超时
  • 返回值大于0,表示poll由于监听的文件描述符就绪而返回

poll优点

  • pollfd结构包含了要监视的event和发生的event,不再使用select"参数-值"传递的方式,接口使用比select方便
  • poll并没有最大数量限制 (但是=数量过大后性能也是会下降)

poll缺点

  • poll中监听的文件描述符数目增多时
  • 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量增长,其效率也会线性下降

使用poll监控标准输入

#include 
#include 
#include 


int main()
{
  struct pollfd poll_fd;
  poll_fd.fd =0;
  poll_fd.events=POLLIN;//表示监听的是输入时间

  for(;;)
  {
    int ret = poll (&poll_fd,1,1000);//超时时间是1000毫秒
    if(ret< 0)
    {
      perror("poll");
      continue;
    }
    if(ret == 0)
    {
      printf("poll timeout\n");
    }
    if(poll_fd.revents==POLLIN)
    {
      char buf[1024] = {0};
        read(0,buf,sizeof(buf)-1);
        printf("stdin:%s",buf);
    }
  }
  return 0;
}

epoll

  • epoll是poll的改进版本
  • 它几乎解决了select和poll的所有缺点,是Linux2.6下性能最好的多路I/O就绪通知方法

epoll系统调用

epoll_create & epoll_ctl

int epoll_create(int size)
//创建一个epoll的句柄
//自从Linux2.6.8之后,size参数是被忽略的也就是说,这个size在这个版本后是什么值不重要了,内部不会因为这个值限制监听描述符的数量
// 用完之后,必须调用close关闭,避免资源的浪费

int epoll_ctl(int epfd,int op,int fd, struct epoll_event* event);
// epoll的事件注册函数
// 它不同于select是在监听事件告诉内核要监听什么类型的时间,而是在这里先注册要监听的事件的类型
// epfd 是epoll_create的返回值,epoll的句柄
// op 表示动作,用三个宏表示   
// 三个宏(EPOLL_CTL_ADD 注册新的fd)  (EPOLL_CTL_MOD 修改已经注册的fd的监听事件) (EPOLL_CTL_DEL 从epfd中删除一个fd)
//  fd,是需要监听的fd 
// event 是告诉内核需要监听什么事

epoll_event结构

image
image

centos7中
image

event的集合

  • EPOLLIN 表示对应的文件描述符可以读
  • EPOLLIN 表示对应的文件描述符可以写
  • EPOLLPRI 表示对应的文件描述符有紧急的数据可读
  • EPOLLERR 表示对应的文件描述符发生错误
  • EPOLLHUP 表示对应的文件描述符被挂断
  • EPOLLET 将EPOLL设为边缘触发模式,这是相对于水平触发来说的
  • EPOLLONESHOT 只监听一次时间,当监听完事这次事件后,如果还需要继续监听的话,需要再次把这个socket假如到EPOLL队列里

epoll_wait

int epoll_wait(int epfd,struct epoll_event* events,int maxevents ,int timeout);

监控到有就绪文件后

  • 参数events是分配好的epoll_event结构体数组
  • epoll将会把发生的事件复制到events数组中 (evens不可以是空指正,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)
  • maxevents告知内核我们的events有多大,这个maxevnets的值不能大于创建epoll_create时的size
  • 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞)
  • 如果调用函数成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时,返回小于0表示函数失败

epoll工作原理

-== 当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体(管理两个结构)==,这个结构体中有两个成员与epoll的使用方式密切相关

struct eventpoll
{
    /* 红黑树的根节点,这棵树中储存着所有添加到epoll中的需要监听的事件
    插入的时候也不用担心有重复的问题了,因为红黑树本身就有查重的功能*/
    struct rb_root rbr;
    /*
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
    epoll_wait调用成功内核就将双链表中的数据拷贝到用户给的缓冲区中(events数组)*/
    struct list_head rdlist;
}
  • 每个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象添加进来的时间
  • 这些事件都会挂载在红黑树中,如此,重复添加的时间就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)
  • 所有添加到epoll中的时间都会与设备驱动程序建立回调关系,也就是薯片,当响应的时间发生时会调用或者回调方法
  • 这个回调方法在内核中叫做ep_poll_callback,它会将发生的事件添加到rdlist双链表中
  • 在epoll中,对于每一个事件,都会建立一个eptiem结构体(有两个结构的两个节点),红黑树的每个节点都是基于epitem结构体中的rbn成员(红黑树的节点)
  • image

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双链表中是否有eptiem的元素即可
  • 如果rdlist不为空,则把发生的时间复制到用户态,同时将事件数量返回给用户,这个操作的时间复杂度是O(1)

epoll的使用三步

  • 调用epoll_create创建一个epoll句柄
  • 调用epoll_ctl,将要监控的文件描述符进行注册
  • 调用epoll_wait,等待文件描述符就绪

epoll的优点 (和select的缺点对应)

  • 接口方便使用,虽然拆分成了三个函数,但是反而使用起来更加的方便,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开
  • 数据拷贝少:只需要在合适的时候调用 EPOLL_CTL_ADD 将文件描述符拷贝到内核中,这个操作并不频繁 (而select/poll每次都需要循环的拷贝)
  • 事件回调机制,避免使用遍历主动去知道,而是使用回调函数的方法,将就绪的文件描述符结构加入到就绪队列,epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪,这个操作时间复杂度O(1),即使文件描述符数目很多,效率也不会收到影响

容易认识的误区

有人说,epoll使用的是内存映射机制

  • 内存映射机制是内核直接将就绪队列通过mmap的方式映射到用户态,避免了拷贝内存这样额外性能开销
    这种说法不准确,我们定义的是 struct epoll_event是我们在用户空间中分配好的内存,势必还是需要将内核的数据拷贝到这个用户空间的内存中的,这个拷贝过程还是存在的

epoll的工作方式

假设为1代表有数据

水平触发:

  • 只要满足触发条件,就会提醒 为1状态就触发

边沿触发

  • 每次新数据到来,仅提醒一次 因此需要每次将缓冲数据全部读取,因此需要每次将缓冲区的数据全部读取,因为全部读取的话,一不小心就会导致recv阻塞,所以需要将socket描述符属性设置为非阻塞 从0跳到1才触发,并且触发处理后不管有没有数据又回到0状态

水平触发Level Triggered工作模式

epoll 默认状态下就是LT工作模式

  • 当epoll检测到socket上事件就绪的时候,可以不立刻进行处理,或者只处理一部分
  • 如上面的例子,由于只读了1K数据,缓冲区中还剩下1K数据,在第二次调用epoll_wait时,epoll_wait依然会立刻返回并通知socket读时间就行
  • 知道缓冲区上所有的数据都被处理完,epoll_wait才不会立刻返回
  • 支持阻塞读写和非阻塞读写

边缘触发 Edge Triggered工作模式

如果我们在第1步讲socket添加到epoll描述符的时候使用EPOLLET标志,epoll进入ET工作模式

  • 检测到就绪事件,立刻处理
  • 如果没读完,下一次不会再通知,必须等新的就绪通知
  • 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不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区,
    保证一定能把完整的请求都读出来.

epoll的使用场景 (高并发)

epoll得到高性能,是有一定的特定场景的,如果场景选择不适宜,epoll的性能可能适得其反

  • 对于多连接,且多连接中只有一部分连接比较活跃时,比较适合使用epoll
    典型的需要除了处理上万个客户端的服务器,例如各种互联网APP的入口服务器,这样的服务器就很适合epoll,如果只是系统内部,服务器和服务器之间通信,只有少数的几个连接,这种情况下epoll就不适合

epoll惊群问题

产生原因

在多线程或者多进程环境下,有些人为了提高程序的稳定性,往往会让多个线程或者多个进程同时在epoll_wait监听的socket描述符。当一个新的链接请求进来时,操作系统不知道选派那个线程或者进程处理此事件,则干脆将其中几个线程或者进程给唤醒,而实际上只有其中一个进程或者线程能够成功处理accept事件,其他线程都将失败,且errno错误码为EAGAIN。这种现象称为惊群效应,结果是肯定的,惊群效应肯定会带来资源的消耗和性能的影响。
那么如何解决这个问题

多线程下的解决方法 (不然多个线程epoll_wait)

这种情况,不建议让多个线程同时在epoll_wait监听的socket,而是让其中一个线程epoll_wait监听的socket,当有新的链接请求进来之后,由epoll_wait的线程调用accept,建立新的连接,然后交给其他工作线程处理后续的数据读写请求,这样就可以避免了由于多线程环境下的epoll_wait惊群效应问题。

多进程 (捕获EAGAIN无视惊群 | 互斥锁+负载均衡技术)

  • lighttpd的解决思路是无视惊群效应,仍然采用master/workers模式,每个子进程仍然管自己在监听的socket上调用epoll_wait,当有新的链接请求发生时,操作系统仍然只是唤醒其中部分的子进程来处理该事件,仍然只有一个子进程能够成功处理此事件==,那么其他被惊醒的子进程捕获EAGAIN错误==,并无视。
  • nginx的解决思路:在同一时刻,永远都只有一个子进程在监听的socket上epoll_wait,其做法是,创建一个全局的pthread_mutex_t,在子进程进行epoll_wait前,则先获取锁。代码如下:
//负载能力+锁
ngx_int_t  ngx_trylock_accept_mutex(ngx_cycle_t *cycle)  
{  
    //先尝试获取锁
    if (ngx_shmtx_trylock(&ngx_accept_mutex))
    {
    //是否有能力获取监听时间
    if (ngx_enable_accept_events(cycle) == NGX_ERROR) 
        {  
            ngx_shmtx_unlock(&ngx_accept_mutex);  
            return NGX_ERROR;  
        }  
  
        ngx_accept_mutex_held = 1;  
        return NGX_OK;  
    }  
  
    if (ngx_accept_mutex_held)
    {  
        if (ngx_disable_accept_events(cycle) == NGX_ERROR)
        {  
            return NGX_ERROR;  
        }  
  
        ngx_accept_mutex_held = 0;  
    }  
    return NGX_OK;  
}  

且只有在ngx_accept_disabled < 0 时,才会去获取全局锁,即只有在子进程的负载能力在一定的范围下才会尝试去获取锁,并进入epoll_wait监听的socket。

//处理负载能力 无负载能力就不能获取锁了,因为服务器不行了,承受不起了
void  ngx_process_events_and_timers(ngx_cycle_t *cycle)  
{   
    if (ngx_accept_disabled > 0)
    {  
        ngx_accept_disabled--;    
    }

else

{  
        if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {  
            return;  
        }     
    }  
}



ngx_accept_disabled = ngx_cycle->connection_n / 8  - ngx_cycle->free_connection_n;

表示当子进程的连接数达到连接总数的7/8时,是不会尝试去获取全局锁,只会专注于自己的连接事件请求。

select poll epoll对比

描述符

  • slect 描述符有限 描述符多了性能降低 需要轮询找出就绪描述符
  • poll 描述符无上限 描述符多了性能降低 需要轮询找出就绪的描述符,效率降低
  • epoll 描述符无上限 描述符多了性能不会降低 拿到了就是就需要的,不需要无畏的轮询
  • 事件触发回调就是直接在数组中添加了就绪的文件描述符

编码

  • select 需要每次重新添加描述符到集合,并且需要从用户态拷贝到内核态
  • poll 编码相对简单 并且每次需要从用户状拷贝到内核态
  • 编码相对简单,但是事件仅需定义一次,并且向内核拷贝一次即可

平台

  • 跨平台
  • linux
  • linux

边沿出发和水平出发

  • 边沿出发只在有新数据的时候提醒,要是这次不读完,剩下的数据下次不会提醒了,出发又有新的数据来了
  • 水平触发只要有数据就提醒

优点

  • 用的是红黑树,不会有重复,也不限制大小
  • wait的时候返回了大小,就不用遍历是否就绪
  • 不会修改最初的集合,不用每次都重新设置集合
    对于多路转接技术来说:都是仅适用于大量连接,但是同一时间仅有少量活跃的情况

小知识点

gcc 加上 -Wall 选项 显示警告

你可能感兴趣的:(Linux)