Linux下IO多路复用(三)

epoll

一、epoll说明

  1. epoll 是在 Linux 2.6 才引进的,而且它并不适用于其它 Unix-like 系统。它提供了一个与select 和 poll 函数相似的功能;
  2. select 可以在某一时间监视最大达到 FD_SETSIZE 数量的文件描述符, 通常是由在 libc 编译时指定的一个比较小的数字;
  3. poll 在同一时间能够监视的文件描述符数量并没有受到限制,即使除了其它因素,更加的是我们必须在每一次都扫描所有通过的描述符来检查其是否存在己就绪通知,它的时间复杂度为 O(n) ,是缓慢的;
  4. epoll不存在这个问题,它只会对活跃的socket进行操作,这是因为在内核实现中,epoll是根据每个fd上面的callback函数实现的,而且fd以红黑树和链表的结构存储。因此,只有活跃的socket才会主动去调用callback函数,其他idle状态socket则不会。在这一点上,epoll实现了一个伪AIO,其内部推动力在内核;
  5. 无论是select,poll还是epoll,它们都需要内核把fd消息通知给用户空间。因此,如何避免不必要的内存拷贝就很重要了。对于该问题,epoll通过内核与用户空间mmap同一块内存来实现。

所以在高并发场景epoll更加适用,当然在并发量比较小的情况下,还是select和poll更合适。

二、函数原型

1
2
3
4
5
6
//创建一个epoll的句柄
int epoll_create(int size);
//epoll的事件注册函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//收集在epoll监控的事件中已经发生的事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll_ctl()

int epfd

epoll_create函数的返回值。

int op

表示动作类型。有三个宏来表示:

1
2
3
EPOLL_CTL_ADD  //注册新的fd到epfd中;
EPOLL_CTL_MOD //修改已经注册的fd的监听事件;
EPOLL_CTL_DEL //从epfd中删除一个fd。

int fd

需要监听的fd。

struct epoll_event *event

需要监听的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 保存触发事件的某个文件描述符相关的数据
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
};

Epoll events

1
2
3
4
5
6
7
EPOLLIN  //表示对应的文件描述符可读(包括对端Socket);
EPOLLOUT //表示对应的文件描述符可写;
EPOLLPRI //表示对应的文件描述符有紧急数据可读(带外数据);
EPOLLERR //表示对应的文件描述符发生错误;
EPOLLHUP //表示对应的文件描述符被挂断;
EPOLLET //将EPOLL设为边缘触发(Edge Triggered),这是相对于水平触发(Level Triggered)而言的。
EPOLLONESHOT //只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket,需要再次

返回值

  1. 成功时,返回大于0;
  2. 失败时,返回-1;

epoll_wait()

int epfd

epoll_create函数的返回值。

struct epoll_event *events

分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据赋值到这个event数组中,不会去帮助我们在用户态分配内存)。

int maxevents

maxevents告诉内核这个events数组有多大,这个maxevents的值不能大于创建epoll_create时的size.

int timeout

  1. 是一个用毫秒表示的时间,是指定poll在返回前没有接收事件时应该等待的时间。

  2. 如果值为-1,epoll就永远都不会超时。如果整数值为32个比特,那么最大的超时周期大约是30分钟。

    1. INFTIM //永远等待
    2. 0 //立即返回,不阻塞进程
    3. 正值 //等待指定数目的毫秒数

返回值

  1. 成功时,epoll()返回结构体中revents域不为0的文件描述符个数;
  2. 如果在超时前没有任何事件发生,poll()返回0;
  3. 失败时,poll()返回-1;

三、epoll工作模式

  1. LT模式(Level Triggered,水平触发)
    该模式是epoll的缺省工作模式,其同时支持阻塞和非阻塞socket。内核会告诉开发者一个文件描述符是否就绪,如果开发者不采取任何操作,内核仍会一直通知。

  2. ET模式(Edge Triggered,边缘触发)
    该模式是一种高速处理模式,当且仅当状态发生变化时才会获得通知。在该模式下,其假定开发者在接收到一次通知后,会完整地处理该事件,因此内核将不再通知这一事件。注意,缓冲区中还有未处理的数据不能说是状态变化,因此,在ET模式下,开发者如果只读取了一部分数据,其将再也得不到通知了。正确的做法是,开发者自己确认读完了所有的字节(一直调用read/write直到出错EAGAGIN为止)。

Nginx默认采用的就是ET(边缘触发)。

四、epoll流程小结

  1. 执行epoll_create时,创建了红黑树和就绪list链表;
  2. 执行epoll_ctl时,如果增加fd,则检查在红黑树中是否存在,存在则立即返回,不存在则添加到红黑树中,然后向内核注册回调函数,用于当中断事件到来时向准备就绪的list链表中插入数据。
  3. 执行epoll_wait时立即返回准备就绪链表里的数据即可。

五、伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
struct epoll_event event;
//事件数组
struct epoll_event eventList[MAXCN];
int epollfd = epoll_create(MAXCN);//
int timeout=3000;
event.events = EPOLLIN|EPOLLET;
event.data.fd = serverfd;

setNonBlocking(serverfd); //配置非阻塞模式

if(epoll_ctl(epollfd, EPOLL_CTL_ADD, serverfd, &event) < 0)
{
//error
return;
}
while(1)
{
int ret = epoll_wait(epollfd, eventList, MAXCN, timeout);
if(ret < 0)
{
//error
break;
}
else if(ret == 0)
{
//timeout
continue;
}
//直接获取了事件数量,给出了活动的流,这里是和poll区别的关键
for(int i=0; i<ret; i++)
{
//错误退出
if ((eventList[i].events & EPOLLERR) || (eventList[i].events & EPOLLHUP)
|| !(eventList[i].events & EPOLLIN))
{
//error
continue;
}
if (eventList[i].data.fd == serverfd)
{
int clientfd = accept...
if(maxfd >= MAXCN)
{
//超出链接数量
//关闭
}
else
{
//加入epoll监视
setNonBlocking(clientfd); //配置非阻塞模式
}
}
else
//callReadBackFunC();
}
}
if(epollfd)
close(epollfd);

六、epoll完整代码示例