EPOLL中的ET,LT及IO阻塞

前言

EPOLL是LINUX独有的I/O复用函数。其将用户关心的文件描述符(fd)上的事件放在内核里的一个事件表中。Select和Poll会在每次调用的时候线性扫描全部的Socket集合,而Epoll是通过向内核去注册回调事件的方式,只有活跃的socket才会主动的调用callback函数,如果不是所有socket都活跃的情况下,效率相较于select/poll有很大的提升。

epoll

epoll主要为事件驱动,有三个主要的系统调用:

  • epoll_create: 创建一个epoll句柄,占用一个fd
  • epoll_ctl:向句柄注册epoll事件,参数包括,epoll的句柄值,执行的动作(注册,修改,删除),需要监听的fd,需要监听的事件(struct epoll_event)
  • epoll_wait:收集在epoll监控的事件中已经发生的事件,返回就绪的事件数目,同时将就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中,使用的时候直接遍历数组即可,不需要遍历所有已注册的文件描述符

epoll对fd的操作有LT(水平触发,默认)和ET(边缘触发,高效)两种方式

image-20210201161749592

区别在于:使用LT的fd,当epoll_wait检测到fd上有事件发生通知应用程序时,应用程序可以不立即处理。当程序下一次调用epoll_wait时,仍然会向应用程序通知这件事,直到事件被处理。而使用ET的fd,当有事件发生,epoll_wait通知应用程序时,应用程序必须立即处理该事件,后续的epoll_wait调用将不再向应用程序通知这件事情

服务端循环调用epoll_wait,监听客户端fd是否有可读事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (1)
{
printf("开始epoll_wait\n");
int ret = epoll_wait(epollfd, events, Max_EVENT_NUMBER, -1);
printf("侦测到事件发生\n");
if (ret < 0)
{
printf("epoll failure\n");
break;
}
//使用et或者lt模式处理
lt(events, ret, epollfd, server);
//et(events, ret, epollfd, server);
}

使用LT

lt的处理代码,当lt监测到可读事件时从缓冲区读取数据

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
void lt(epoll_event *events, int number, int epollfd, int listenfd)
{
printf("lt开始\n");
char buf[BUF_SIZE];
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;
if (sockfd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
addfd(epollfd, connfd, false);
}
else if (events[i].events & EPOLLIN)
{
memset(buf, '\0', BUF_SIZE);
int ret = recv(sockfd, buf, BUF_SIZE - 1, 0);
if (ret <= 0)
{
close(sockfd);
continue;
}
printf("server got %d bytes of normal data '%s'\n", ret, buf);
}
else
{
printf("something else happened\n");
}
}
printf("lt结束\n");
}

测试:使用客户端连接服务端,发送“123456789”(9字节)的数据,服务端设置了一次从缓冲区读取4字节

image-20210201163137531

可以看到第一次epoll_wait检测到事件发生,应用程序从缓冲区中读取了4字节,接着下一次epoll_wait仍然会通知应用程序有可读事件

使用ET

et模式下由于事件只会通知一次,所以我们需要在et模式的处理函数中,对缓冲区进行循环读取,一次性将数据读取完毕

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
void et(epoll_event *events, int number, int epollfd, int listenfd)
{
printf("et开始\n");
char buf[BUF_SIZE];
printf("active num is %d\n", number);
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;
printf("fd is %d\n", sockfd);
if (sockfd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
addfd(epollfd, connfd, true);
}
else if (events[i].events & EPOLLIN)
{
printf("event trigger once\n");
//使用循环将缓冲区榨干
while (1)
{
memset(buf, '\0', BUF_SIZE);
int ret = recv(sockfd, buf, BUF_SIZE - 1, 0);
if (ret < 0)
{
if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
{
printf("read later\n");
break;
}
close(sockfd);
break;
}
else if (ret == 0)
{
close(sockfd);
}
else
{
printf("server got %d bytes of normal data '%s'\n", ret, buf);
}
}
}
else
{
printf("something else happened\n");
}
}
printf("et结束\n");
}

测试:步骤同上

image-20210201164422364

可以看到在一个Et的工作流程中将缓冲区的数据全部读取

注意

使用Et模式的文件描述符,一定要设置成非阻塞,设置方法:

1
2
3
4
5
6
7
8
//使用ET模式的文件描述符都应该是非阻塞的。如果文件描述符是阻塞的,读和写操作都会因为没有后续事件而处于阻塞状态
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL); //获取文件描述符的状态
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option); //改变文件描述符状态为非阻塞
return old_option;
}

原因在于,上面的ET处理代码里在while循环中不断的用recv()函数读取缓冲区,如果设置为阻塞的话,当文件描述符上没有内容可读,或者没有空间可写,那么读和写事件会因为没有后续事件而一直阻塞。而当一个非阻塞IO当你去读写时,无论可不可以读写,都会立即返回。返回成功的话就说明读写完成,返回失败的话就会设置相应的errno状态码,这时就可以根据errno状态码来进行进一步处理。如ET代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while (1)
{
...
int ret = recv(sockfd, buf, BUF_SIZE - 1, 0);
if (ret < 0)
{
if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
{
printf("read later\n");
break;
}
close(sockfd);
break;
}
...
}

EAGAIN:应用程序现在没有数据可读请稍后再试。或者当系统调用(比如fork)没有足够资源的时候,返回EAGAIN提示你再试一次

EWOULDBLOCK:同EAGAIN,Windows情况下

image-20210201165414844