本章所有示例代码>>gtihub
17.1 epoll理解及应用
1. 基于select的I/O复用技术速度慢的原因
两点不合理:
- 调用select函数后常见的针对所有文件描述符的循环语句;
- 每次调用select函数时都需要向该函数传递监视对象信息;
“每次调用select函数时向操作系统传递监视对象信息。”
应用程序向操作系统传递数据将对程序造成很大负担,而且无法通过优化代码解决,因此将成为性能上的致命弱点。(有些函数不需要操作系统的帮助就能完成功能,而有些则必须借助于操作系统)
select函数与文件描述符有关,更准确地说,是监视套接字变化的函数。而套接字是由操作系统管理的,所以select函数绝对需要借助于操作系统才能完成功能。
“仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。”(前提是操作系统支持这种处理方式,Linux的支持方式是epoll,Windows的支持方式是IOCP)
2. select也有优点
大多数操作系统都支持select函数,只要满足如下两个条件,可使用select函数:
- 服务器接入者少;
- 程序应具有兼容性;
3. 实现epoll时必要的函数和结构体
epoll函数优点:
- 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句;
- 调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息;
epoll服务器端实现中需要的3个函数:
- epoll_create: 创建保存epoll文件描述符的空间;
- epoll_ctl: 向空间注册并注销文件描述符;
- epoll_wait: 与select函数类似,等待文件描述符发生变化;
select方式中为了保存监视对象的文件描述符,直接声明了fd_set变量。但epoll方式下由操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时使用的函数就是epoll_create。
select方式中,为了添加和删除监视对象文件描述符,需要FD_SET、FD_CLR函数。但在epoll方式中,通过epoll_ctl函数请求操作系统完成。
select方式下调用select函数等待文件描述符的变化,而epoll中调用epoll_wait函数。
select方式中通过fd_set变量查看监视对象的状态变化(事件发生与否),而epoll方式中通过结构体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结构体数组后,传递给epoll_wait函数时,发生变化的文件描述符信息将被填入该数组。
4. epoll_create
#include<sys/epoll.h>
int epoll_create(int size); //成功时返回epoll文件描述符,失败-1
-size: epoll实例的大小,仅供操作系统参考,Linux2.6.8内核版本后将完全忽略该参数;
epoll_create函数创建的资源与套接字相同,也由操作系统管理。因此,该函数和创建套接字的情况相同,也会返回文件描述符,主要用于区分epoll例程,终止时也需调用close函数。
5. epoll_ctl
生成epoll例程后,应在其内部注册监视对象文件描述符,此时使用epoll_ctl函数。
#include<sys/epoll.h>
int epoll_ctl(intepfd, int op, int fd, struct epoll_event *event);
//成功时返回0,失败时返回-1
-epfd: 用于注册监视对象的epoll例程;
-op: 用于指定监视对象的添加、删除和更改等操作;
-fd: 需要注册的监视对象文件描述符;
-event: 监视对象的事件类
调用:
epoll_ctl(A, EPOLL_CTL_ADD, B, C);
“epoll例程A中注册文件描述符B,主要目的是监视参数C中的事件。”
epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);
“从epoll例程A中删除文件描述符B。”
- EPOLL_CTL_ADD : 将文件描述符注册到epoll例程;
- EPOLL_CTL_DEL: 从epoll例程中删除文件描述符;
- EPOLL_CTL_MOD: 更改注册的文件描述符的关注事件发生情况;
epoll_event结构体用于保存发生事件的文件描述符集合。但也可以在epoll例程中注册文件描述符时,用于注册关注的事件。
struct epoll_event event;
……
event.events = EPOLLIN; // 发生需要读取数据的情况(事件)
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
- EPOLLIN: 需要读取数据的情况;
- EPOLLOUT: 输出缓冲为空,可以立即发送数据的情况;
- EPOLLPRI: 收到OOB数据的情况;
- EPOLLRDHUP: 断开连接或半关闭的情况,这在边缘触发方式下非常有用;
- EPOLLERR: 发生错误的情况;
- EPOLLLET: 以边缘触发的方式得到事件通知;
- EPOLLONESHOT: 发生一次事件后,相应文件描述符不再收到事件通知;
可以通过位或运算同时传递多个上述参数。
6. epoll_wait
#include <sys/epoll.h>
int epoll_wait(intepfd, struct epoll_event *events, int maxevents, int timeout); //成功时返回发生事件的文件描述符数,失败时返回-1
-epfd: 表示事件发生监视范围的epoll例程的文件描述符;
-events: 保存发生事件的文件描述符集合的结构体地址值;
-maxevents: 第二个参数中可以保存的最大事件数;
-timeout: 以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件
int event_cnt;
struct epoll_event*ep_event;
……
ep_events =malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
……
event_cnt =epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
……
调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。
17.2 条件触发(Level Trigger)和边缘触发(EdgeTrigger)
1. 条件触发和边缘触发的区别在于发生事件的时间点
如,
服务器端输入缓冲收到50字节的数据时,服务器端操作系统将通知该事件(注册到发生变化的文件描述符)。服务器端读取20字节后还剩30字节的情况下,仍然会注册事件。
边缘触发中输入缓冲收到数据时仅注册1次该事件。即使输入缓冲中还留有数据,也不会再进行注册。
2. 掌握条件触发的事件特性
epoll默认以条件触发方式工作,因此可以通过22-EchoEPLTServer示例验证
3. 边缘触发的服务器端实现中必知的两点
- 通过errno变量验证错误原因;
- 为了完成非阻塞(Non-blocking)I/O,更改套接字特性;
为了在发生错误时提供额外的信息,Linux提供了如下全局变量:
#include <errno.h>
int errno;
“read函数发现输出缓冲中没有数据可读时返回-1,同时在error中保存EAGAIN常量。”
将套接字改为非阻塞方式的方法:
Linux提供更改或读取文件属性的如下方法。
#include <fcntl.h>
int fcntl(intfiledes, int cmd, ……); // 成功返回cmd参数相关值,失败时返回-1
-filedes: 属性/更改目标的文件描述符;
-cmd: 表示函数调用的目的;
fcntl具有可变参数的形式。如果向第二个参数传递F_GETFL,可以获得第一个参数所指的文件描述符属性;如果向第二个参数传递F_SETFL,可以更改文件描述符属性。
若希望将文件(套接字)改为非阻塞模式,需要如下2条语句:
int flag = fcntl(fd,F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
通过第一条语句获取之前设置的属性信息,通过第二条语句在此属性基础上添加非阻塞O_NONBLOCK标志。
4. 实现边缘触发的回声服务器端
边缘触发方式下,以非阻塞方式工作的read& write函数有可能引起服务器端的长时间停顿。因此,边缘触发方式中一定要采用非阻塞read& write函数。
5. 条件触发和边缘触发孰优孰劣
边缘触发可以做到如下这点:
“可以分离接收数据和处理数据的时间点。”
条件触发在输入缓冲收到数据的情况下,如果不读取(延迟处理),则每次调用epoll_wait函数时都会产生相应事件,而且事件数也会累加,服务器端不能承受。
从实现模型的角度看,边缘触发更有可能带来高性能。