1. EPOLL原理介绍
EPOLL是Linux内核提供的的I/O复用函数,简单地说就说使用一个文件描述符来监控多个文件描述符上的事件,当文件描述符上的事件发生时,会通知用户。如果没有这种多路复用机制,那么,面对多个Socket同时通信的场景可能就需要多线程了,但是每个SOcket上并不是一直都有数据,使用多线程多浪费资源呀。
Epoll把用户关心的“文件描述符”放在内核里面一个事件表中,此时就需要一个单独“文件描述符”来标识内核中的这个事件表。另外EPOLL有两种工作模式:水平触发(Level Triggered,LT)和边缘触发(Edge Triggered,ET),LT模式是EPOLL默认的工作模式,EPOLL和异步,非阻塞结合使用。
简单讲一下原理,Epoll的核心是两个数据结构: 红黑树 和 就绪链表(双向链表)。然后结合内核回调机制,实现了高性能的事件管理。红黑树存储所有通过epoll_ctl()添加的文件句柄,树中的每个节点会关联一个结构体(保存了这个文件描述符的上需要监控的事件)。就绪的事件会被保存到储在就绪链表中,当就绪链表不为空时,内核会通知用户,用户可以通过epoll_wait()函数获取就绪事件。
2. EPOLL使用
2.1. 创建EPOLL对象
|
|
返回一个文件描述符,代表内核事件表。
2.2. 操作内核事件表
|
|
epfd:内核事件表文件描述符。
op:操作类型。
fd:要操作的文件描述符。
event:指向 epoll_event 结构体的指针,用于指定要添加、修改或删除的事件。
其中,op(操作类型)有三种:
- EPOLL_CTL_ADD:往内核事件表中添加事件。
- EPOLL_CTL_MOD:修改事件。
- EPOLL_CTL_DEL:删除事件。
其中,event(关心的事件)的类型是struct epoll_event,结构体定义如下:
|
|
events 是一个位图,有EPOLLIN、EPOLLOUT、EPOLLET、EPOLLONESHOT。
data 是epoll_data_t类型,它是一个联合体,定义如下:
|
|
由于这是一个联合体,所以这四个成员,我们只能使用其中的一个。
2.3. 等待事件
|
|
epoll_wait在一段超时时间内,等待一组文件描述符上的事件,返回就绪的文件描述符的数量。
- timeout: 参数指定等待的时间,单位是毫秒。如果timeout为-1,则表示无限等待。
- maxevents: 参数指定最多等待的事件数,必须大于0。
epoll_wait检测到事件,就会将所有的就绪事件从内核事件表拷贝到events数组中,并返回就绪事件的数量,也就是说event中的事件都是就绪的。
3. ET和LT模式对比
对于Socket读事件,只要Socket上的数据没有读完,就会一直触发EPOLL事件,而对于ET模式,Socket上每来一次数据就会触发一次EPOLLIN事件,如果上一次触发后,未将 socket 上的数据读完,也不会再触发,除非再新来一次数据。对于 socket 写事件,如果 socket 的 TCP 窗口一直不饱和,会一直触发 EPOLLOUT 事件;而对于边缘模式,只会触发一次,除非 TCP 窗口由不饱和变成饱和再一次变成不饱和,才会再次触发 EPOLLOUT事件。根据以上分析来看,如果采用ET模式就必须在收到事件后一次性将数据读取完,但如果采用默认模式,就可以根据业务每次收取固定的字节数,多次收取,显然相对于默认的LT模式,ET模式能够减少同一个事件被触发的次数,效率比LT模式高。
3.1. 问题
多线程场景下,我们使用ET模式监听一个Socket上的读事件,当数据到达时触发EPOLLIN事件,我们在一个独立线程(或进程)中读取该Socket上的数据,但是我们还没有读取完,该Socket又有新数据到达,此时另一个线程(或进程)被唤醒来读取该Socket上的数据,那么就有两个线程(进程)同时操作同一个Socket,这样好吗,这样不好,但是如何解决呢?此时就该EPOLLONESHOT出场了。
4. EPOLLONESHOT事件
显然,一次性事件,就是触发以后,需要手动重新注册,给文件描述符注册EPOLLONESHOT事件,就可以保证同一时间只有一个人在使用。
|
|
注意:
listening socket是不能使用EPOLLONESHOT的,否则后续的客户段连接请求就不会再触发listening socket的EPOLLIN事件了。
5. 其他小代码
1. 将文件描述符设置为非阻塞
|
|
2. 事件注册
|
|
3. 判断数据是否读取完了
|
|