内容简介:一直没搞明白 epoll 的机制,以前看不明白 epoll 资料就放弃了。最近重新看这些资料,感觉看明白了大部分。记一下,省的以后又糊涂了。以下内容都是各种资料的小结,以后翻阅省事一点。阻塞模式下,一个线程很难处理多个 I/O 流。比如一个线程要读两个 I/O 事件流,可能 read 第一个I/O 时,因为数据没有就绪,所以整个线程都阻塞了,而第二个 I/O 数据虽然已经就绪,却得不到处理。具体原因个人理解是,线程不知道哪个 I/O 事件已经就绪,只能一个个试。第二个原因是阻塞模式下,如果事件没有就绪,系统
一直没搞明白 epoll 的机制,以前看不明白 epoll 资料就放弃了。最近重新看这些资料,感觉看明白了大部分。记一下,省的以后又糊涂了。以下内容都是各种资料的小结,以后翻阅省事一点。
I/O 模型与 epoll
I/O 流
阻塞模式下,一个线程很难处理多个 I/O 流。比如一个线程要读两个 I/O 事件流,可能 read 第一个I/O 时,因为数据没有就绪,所以整个线程都阻塞了,而第二个 I/O 数据虽然已经就绪,却得不到处理。具体原因个人理解是,线程不知道哪个 I/O 事件已经就绪,只能一个个试。第二个原因是阻塞模式下,如果事件没有就绪,系统调用会阻塞,导致整个线程都阻塞。
非阻塞模式,线程可以通过忙轮询处理多个 I/O 流,同样因为无法知道哪个 I/O 流是否已经就绪,导致很多系统调用都是无效的,效率非常低下。
I/O 多路复用
如果有一个代理,帮助管理多个 I/O 流,当没有可用的 I/O,线程继续阻塞,I/O 就绪时,唤醒线程处理 I/O,效率会大大提高。在 Linux 平台上,select,poll,epoll 就是这个代理。它们之间具体的优缺点就不讲了,这里只讲 epoll 的机制。
I/O 相关的机制,可以参考知乎上面的讨论 I/O与epoll
epoll 基础
以下内容基本上来自 The method to epoll’s madness 和 manpage。
内核内部用数据结构来维护 epoll 的相关信息,epoll 的三个 API 分别操作这些数据结构。
epoll_create
epoll_create 在内核创建 epoll instance(图中下方褐色方块),返回指向这个 epoll instance 的 file descriptor。
epoll_ctl
epoll_ctl 可以让 file descriptor 注册到 epoll instance 中,这些 file descriptor 称为 epoll set(图中 INTEREST LIST)。当 epoll set 里面的 file descriptor 有 I/O 就绪情况下,这些 file descriptor 会放到 READY LIST 里面(图中蓝色部分)。
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd - epoll_create 返回的 epoll file descriptor
- fd - epoll instance 要监听的 file descriptor
- op - 对 file descriptor 的操作
- EPOLL_CTL_ADD - 注册 fd 到 epoll instance,fd 成为 epoll set 一员
- EPOLL_CTL_DEL - 把 fd 从 epoll set 删除,删除后进程无法得到 fd 任何事件的通知。如果 fd 注册到多个epoll instance 中,fd 关闭将导致 fd 从所有 epoll set 中删除
- EPOLL_CTL_MOD - 修改监听 fd 的事件
- event - 事件信息,具体如下所示
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
其中事件类型通过 uint32_t events 的 bit 表示,epoll_data 一般存放发生事件的 fd。
epoll_wait
线程调用 epoll_wait 会一直阻塞,直到 epoll set 里面有 fd 的 I/O 就绪。当 epoll_wait 返回后,线程遍历 evlist,处理 READY LIST 里面的就绪的 I/O 事件。
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
LT & ET
对于 write 来说,当内核缓冲区非满(包括空和有部分数据数据),LT 模式下 EPOLLOUT 会一直触发,当缓冲区从满到非满,ET 模式下 EPOLLOUT 才会触发。对于 read 来说,当缓冲区非空(包括满和有部分数据),LT 模式下 EPOLLIN 会一直触发,当缓冲区从空到非空,ET 模式下 EPOLLIN 才会触发。默认触发方式是 LT,如果是 ET,在 epoll_ctl 函数里面设置参数 event.events | EPOLLET 。
因为 LT & ET 触发方式不同,处理事件的逻辑也不同。先看 manpage 里面的一个例子
1. The file descriptor that represents the read side of a pipe (rfd) is registered on the epoll instance. 2. A pipe writer writes 2 kB of data on the write side of the pipe. 3. A call to epoll_wait(2) is done that will return rfd as a ready file descriptor. 4. The pipe reader reads 1 kB of data from rfd. 5. A call to epoll_wait(2) is done.
在 ET 模式下,这种情况可能导致进程一直阻塞。
- 假设 pipe 刚开始是空的,A端发送 2KB,然后等待B端的响应。
- 步骤2完成后,缓冲区从空变成非空,ET 会触发 EPOLLIN 事件
- 步骤3 epoll_wait 正常返回
- B开始读操作,但是只从管道读 1KB 数据
- 步骤5调用 epoll_wait 将一直阻塞。因为 ET 下,缓冲区从空变成非空,才会触发 EPOLLIN 事件,缓冲区从满变成非满,才会触发 EPOLLOUT 事件。而当前情况不满足任何触发条件,所以 epoll_wait 会一直阻塞。
如何解决呢,一个办法就是步骤4一直读,直到数据全部读完,但是在 blocking IO 下会出现另外一个问题,如果某次读完内核缓冲区后,再次调用 read 时,线程将会阻塞。所以需要设置 fd 是非阻塞的,当调用 read 或者 write 时,当返回 EAGIN/EWOULDBLOCK 后才去调用 epoll_wait。
在LT模式下,步骤4结束后,缓冲区还有数据,所以步骤5的 epoll_wait 不会阻塞,因为 EPOLLIN 事件不会丢失,会一直触发。但是也有一个问题,如果一次读的数据太少,将导致多次调用 epoll_wait,所以效率会有所下降。
为了减少 epoll_wait 调用次数,也可以采用ET的模式,使用非阻塞 IO,然后读写直到返回 EAGIN/EWOULDBLOCK。
LT/ET 在非阻塞处理有一点点不同,具体参考网络大神的总结 epoll LT/ET 深度剖析
reference
- 《Unix环境高级编程》
- I/O与epoll
- epoll LT/ET 深度剖析
- The method to epoll’s madness
以上所述就是小编给大家介绍的《epoll的那些事》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Build Your Own Web Site the Right Way Using HTML & CSS
Ian Lloyd / SitePoint / 2006-05-02 / USD 29.95
Build Your Own Website The Right Way Using HTML & CSS teaches web development from scratch, without assuming any previous knowledge of HTML, CSS or web development techniques. This book introduces you......一起来看看 《Build Your Own Web Site the Right Way Using HTML & CSS》 这本书的介绍吧!