目录

一.Epoll功能实现原理

 二.条件触发【LT】与边缘触发【ET】:

 三.Epoll底层:红黑树+链表


Epoll采用多路复用技术(可以监听多个文件描述符),相对于select和poll效率提高了很多;

一.Epoll功能实现原理

Epoll的功能由以下三组函数实现:

A.int epoll_creat(int size);

  生成一个epoll专用的文件描述符,在Linux2.6内核之后,使用红黑树管理所有的文件描述符;

  size参数只是应用程序向操作系统提的建议,操作系统并不一定生成size大小的epoll例程;【epoll例程指保管文件描述符的空间】

  当创建好epoll句柄后,它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

  成功返回epoll文件描述符,失败返回-1;

B.int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);

  向空间注册或者销毁文件描述符;

  成功时返回0,失败时返回-1;

  参数epfd指定注册监听对象的文件描述符;

  参数op是对监听对象的添加删除或更改操作;【op表示动作用三个宏表示】

①. EPOLL_CTL_ADD:将文件描述符注册到epoll例程
②. EPOLL_CTL_DEL:从epoll例程中删除文件描述符
③. EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况

  fd指定需要注册的监视对象文件描述符event指定监视对象的事件类型

  epoll_event结构体如下: 

struct epoll_event
{
    __uint32_t events;
    epoll_data_t data; 

}

typedef union epoll_data
{
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64; 
}epoll_data_t;

  epoll_event的成员events中可以保存的常量及所指的事件类型有以下:
1)EPOLLIN:需要读取数据的情况
2) EPOLLOUT:输出缓冲为空,可以立即发送数据的情况
3) EPOLLPRI:收到OOBO数据的情况 
4) EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用 
5) EPOLLERR:发生错误的情况 
6) EPOLLET:以边缘触发的方式得到事件通知 
7) EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。
因此需要向epoll_ctl函数的第二个参数EPOLL_CTL_MOD,再次设置事件。

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

  等待事件的产生,返回需要处理的事件的数量,并将需处理事件的套接字集合于参数events内,可以遍历events来处理事件

  参数epfd为epoll句柄;events为事件集合;

  参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

  epoll_wait:与select函数类似,等待文件描述符发生变化。操作系统返回epoll_event类型的结构体通知监视对象的变化。timeout函数是为毫秒为单位的等待时间,传递-1时,一直等待直到事件发生。声明足够大的epoll_event结构体数组后,传递给epoll_wait函数时,发生变化的文件符信息将被填入该数组。因此,不需要像select函数那样针对所有文件符进行循环

 二.条件触发【LT】与边缘触发【ET】:

  条件触发:只要引起epoll_wait返回的事件还存在,那么再次调用epoll_wait时,这个事件还会被注册;

  边缘触发:每个事件在刚发生的时候注册一次,之后就不会再被注册,除非又有新的事件发生;

 【比如,一个已连接的socket套接字收到了数据,而读取缓冲区小于接收到的数据,这时,两种触发方式有以下区别:A.条件触发:一次读取之后,套接字缓冲区里还有数据,再调用epoll_wait,该套接字的EPOLL_IN事件还是会被注册;B.边缘触发:一次读取之后,套接字缓冲区里还有数据,再调用epoll_wait,该套接字的EPOLL_IN事件不会被注册,除非在这期间,该套接字收到了新的数据。】

 【Tips:】

  1. 在使用epoll_ctl注册事件的时候,选择边缘触发,|EPOLLET
  2. 处理已发生的边缘触发的事件时,要处理完所有的数据再返回;例如使用了循环的方式读取了套接字中的所有数据
  3. 读/写套接字的时候采用非阻塞式I/O,是因为在边缘触发方式下,以阻塞方式工作的read&write函数有可能引起服务器端的长时间停顿
  4. 那么边缘触发好不好?有什么优点呢?书上说,边缘触发可以分离接收数据和处理数据的时间点。也就是说,在事件发生的时候,我们只记录事件已经发生,而不去处理数据,等到以后的某段时间才去处理数据,即分离接收数据和处理数据的时间点。好奇的我一定会问:条件触发没办法分离接收数据和处理数据的时间点吗?答案是可以的。但存在问题:在数据被处理之前,每次调用epoll_wait都会产生相应的事件,在一个具有大量这样的事件的繁忙服务器上,这是不现实的。

三.Epoll底层:红黑树+链表

  首先是epoll_creat函数创建出epoll文件描述符,底层会创建出红黑树和链表来对文件描述符进行管理,红黑树存储所监控的文件描述符的节点数据,链表中存储就绪文件描述符的节点数据,;

  Epoll_ctl函数进行新的文件描述符的添加工作,首先判断红黑树上是否存在这个文件描述符结点,如果有就立即返回,如果没有就在红黑树上进行插入操作,并且告诉内核注册回调函数

  当接收到某个文件描述符传递过来的数据的时候,内核会把这个结点插入到就绪链表中,epoll_wait会接收到数据,并且把数据拷贝到用户空间,清空链表

  Epoll_wait清空就绪链表之后会检查文件描述符是哪一种模式?LT/ET;如果是LT模式,【检查】并且这个结点上如果有时间没有处理或者没有处理完,那么会把这个结点重新放到刚刚删除,并且重新准备的新的就绪链表中,epoll_wait立即返回;如果是ET模式,那么不会进行检查,只会调用一次;

【边缘触发更有可能带来高性能,但不能简单地认为“只要使用边缘触发就一定能提高速度”】

【每个epollfd在内核中有一个对应的eventpoll结构对象.其中关键的成员是一个readylist(eventpoll:rdllist)和一棵红黑树(eventpoll:rbr).

  一个fd被添加到epoll中之后(EPOLL_ADD),内核会为它生成一个对应的epitem结构对象.epitem被添加到eventpoll的红黑树中.红黑树的作用是使用者调用EPOLL_MOD的时候可以快速找到fd对应的epitem。调用epoll_wait的时候,将readylist中的epitem出列,将触发的事件拷贝到用户空间.之后判断epitem是否需要重新添加回readylist.

 epitem重新添加到readylist必须满足下列条件:

  1. epitem上有用户关注的事件触发.
  2. epitem被设置为水平触发模式(如果一个epitem被设置为边界触发则这个epitem不会被重新添加到readylist中,在什么时候重新添加到readylist请继续往下看).
  3. 如果epitem被设置为EPOLLONESHOT模式,则当这个epitem上的事件拷贝到用户空间之后,会将这个epitem上的关注事件清空(只是关注事件被清空,并没有从epoll中删除,要删除必须对那个描述符调用EPOLL_DEL),也就是说即使这个epitem上有触发事件,但是因为没有用户关注的事件所以不会被重新添加到readylist中.

  epitem被添加到readylist中的各种情况(当一个epitem被添加到readylist如果有线程阻塞在epoll_wait中,那个线程会被唤醒):

  1. 对一个fd调用EPOLL_ADD,如果这个fd上有用户关注的激活事件,则这个fd会被添加到readylist.
  2. 对一个fd调用EPOLL_MOD改变关注的事件,如果新增加了一个关注事件且对应的fd上有相应的事件激活,则这个fd会被添加到readylist.
  3. 当一个fd上有事件触发时(例如一个socket上有外来的数据)会调用ep_poll_callback(见eventpoll::ep_ptable_queue_proc)

  如果触发的事件是用户关注的事件,则这个fd会被添加到readylist中.

  了解了epoll的执行过程之后,可以回答一个在使用边界触发时常见的疑问.在一个fd被设置为边界触发的情况下,

  调用read/write,如何正确的判断那个fd已经没有数据可读/不再可写.epoll文档中的建议是直到触发EAGAIN错误.而实际上只要你请求字节数小于read/write的返回值就可以确定那个fd上已经没有数据可读/不再可写.

  最后用一个epollfd监听另一个epollfd也是合法的,epoll通过调用eventpoll::ep_eventpoll_poll来判断一个epollfd上是否有触发的事件(只能是读事件).

 

【在秋招时候看了Epoll的源码,因为学习Tornado框架,现在又复习了一遍~】

【为什么总感觉这篇博客排版好丑啊!字好大!!】