IO epoll的LT ET
目录
LT 水平模式(默认模式)
水平模式可以简称为LT模式,LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。
在这种做法中,内核通知使用者哪些文件描述符已经就绪,之后就可以对这些已就绪的文件描述符进行IO操作了。
如果我们不作任何操作,内核还是会继续通知使用者。
特点:
- 读事件:如果文件描述符对应的读缓冲区还有数据,读事件就会被触发,epoll_wait()解除阻塞
- 当读事件被触发,epoll_wait()解除阻塞,之后就可以接收数据了
- 如果接收数据的buf很小,不能全部将缓冲区数据读出,那么读事件会继续被触发,直到数据被全部读出,如果接收数据的内存相对较大,读数据的效率也会相对较高(减少了读数据的次数)
- 因为读数据是被动的,必须要通过读事件才能知道有数据到达了,因此对于读事件的检测是必须的
- 写事件:如果文件描述符对应的写缓冲区可写,写事件就会被触发,epoll_wait()解除阻塞
- 当写事件被触发,epoll_wait()解除阻塞,之后就可以将数据写入到写缓冲区了
- 写事件的触发发生在写数据之前而不是之后,被写入到写缓冲区中的数据是由内核自动发送出去的
- 如果写缓冲区没有被写满,写事件会一直被触发
- 因为写数据是主动的,并且写缓冲区一般情况下都是可写的(缓冲区不满),因此对于写事件的检测不是必须的
示例代码(buf较小,一次无法接收所有数据)
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
int main()
{
printf("%s 向你问好!\n", "IOepoll");
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
return -1;
}
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (sockaddr*)&saddr, sizeof(saddr));
if (ret == -1)
{
perror("bind error");
return -1;
}
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen error");
return -1;
}
//创建epoll示例
int epfd = epoll_create(1);
if (epfd == -1)
{
perror("epoll_create error");
return -1;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);
while (1)
{
int num = epoll_wait(epfd, evs, size, -1);
printf("num = %d \n", num);
for (int i = 0; i < num; i++)
{
int fd = evs[i].data.fd;
if (fd == lfd)
{
int cfd = accept(fd, NULL, NULL);
//struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = cfd;
//这一步会做拷贝,所以说ev用一个也是可以的
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
else//通信的描述符
{
char buf[5];
int len = recv(fd, buf, sizeof(buf), 0);
if (len == -1)
{
perror("recv error");
return -1;
}
else if (len == 0)
{
printf("client disconnect...\n");
//先删除再做操作
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
}
printf("read buf = %s \n", buf);
for (int i = 0; i < len; i++)
{
buf[i] = toupper(buf[i]);
}
printf("after buf = %s \n", buf);
ret = send(fd, buf, strlen(buf) + 1, 0);
if (ret == -1)
{
perror("send error");
return -1;
}
}
}
}
close(lfd);
return 0;
}
这样如果发送了很多数据,第一次没有处理完的数据就会留在cfd的读缓冲区,epoll的lt模式就会在下一次中直接通知epoll事件
例如客户端发送
123adfadfaf
服务端就会显示
num = 1
num = 1
read buf = 123ad
after buf = 123AD
num = 1
read buf = fadfa
after buf = FADFA
num = 1
read buf = f
after buf = F
ET 边沿模式
边沿模式可以简称为ET模式,ET(edge-triggered)是高速工作方式,只支持no-block socket。
在这种模式下,当文件描述符从未就绪变为就绪时,内核会通过epoll通知使用者。
然后它会假设使用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(only once)。
如果我们对这个文件描述符做IO操作,从而导致它再次变成未就绪,当这个未就绪的文件描述符再次变成就绪状态,内核会再次进行通知,并且还是只通知一次。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。
特点:
- 读事件:当读缓冲区有新的数据进入,读事件被触发一次,没有新数据不会触发该事件
- 如果有新数据进入到读缓冲区,读事件被触发,epoll_wait()解除阻塞
- 读事件被触发,可以通过调用read()/recv()函数将缓冲区数据读出
- 如果数据没有被全部读走,并且没有新数据进入,读事件不会再次触发,只通知一次
- 如果数据被全部读走或者只读走一部分,此时有新数据进入,读事件被触发,并且只通知一次
- 写事件:当写缓冲区状态可写,写事件只会触发一次
- 如果写缓冲区被检测到可写,写事件被触发,epoll_wait()解除阻塞
- 写事件被触发,就可以通过调用write()/send()函数,将数据写入到写缓冲区中
- 写缓冲区从不满到被写满,期间写事件只会被触发一次
- 写缓冲区从满到不满,状态变为可写,写事件只会被触发一次
综上所述:epoll的边沿模式下 epoll_wait()检测到文件描述符有新事件才会通知,如果不是新的事件就不通知,通知的次数比水平模式少,效率比水平模式要高。
ET模式设置
边沿模式不是默认的epoll模式,需要额外进行设置。epoll设置边沿模式是非常简单的,
epoll管理的红黑树示例中每个节点都是struct epoll_event类型,只需要将EPOLLET添加到结构体的events成员中即可:
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 设置边沿模式
如何处理每次接收只通知一次呢
将接收函数写到一个循环之中 非阻塞模式
如果只是简单的
while(1)
{
int len=recv();
}
或者
int len = 0;
while((len = recv(curfd, buf, sizeof(buf), 0)) > 0)
{
// 数据处理...
}
显然函数是阻塞的,执行到这里就变成了单线程阻塞于此,所以我们需要将这里的文件描述符设置为非阻塞模式
- 先将文件描述符设置为非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
- 在监听的地方,将通信的文件描述符设置为非阻塞
if(curfd == lfd)
{
// 建立新的连接
int cfd = accept(curfd, NULL, NULL);
// 将文件描述符设置为非阻塞
// 得到文件描述符的属性
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
// 通信的文件描述符检测读缓冲区数据的时候设置为边沿模式
ev.events = EPOLLIN | EPOLLET; // 读缓冲区是否有数据
ev.data.fd = cfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if(ret == -1)
{
perror("epoll_ctl-accept");
exit(0);
}
}
- 另一个bug
因为设置为非阻塞了,如果读缓冲区已经读完了,那么此时再读的时候就会出现读异常
此时函数调用就失败了,返回-1,对应的全局变量 errno 值为 EAGAIN 或者 EWOULDBLOCK如果打印错误信息会得到如下的信息:Resource temporarily unavailable
// 非阻塞模式下recv() / read()函数返回值 len == -1
int len = recv(curfd, buf, sizeof(buf), 0);
if(len == -1)
{
if(errno == EAGAIN)
{
printf("数据读完了...\n");
}
else
{
perror("recv");
exit(0);
}
}
如果发送端发送的数据有上限,那么接收端只要指定一个更大的buff就可以了
示例代码
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
printf("%s 向你问好!\n", "IOepoll");
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
return -1;
}
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (sockaddr*)&saddr, sizeof(saddr));
if (ret == -1)
{
perror("bind error");
return -1;
}
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen error");
return -1;
}
//创建epoll示例
int epfd = epoll_create(1);
if (epfd == -1)
{
perror("epoll_create error");
return -1;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);
while (1)
{
int num = epoll_wait(epfd, evs, size, -1);
printf("num = %d \n", num);
for (int i = 0; i < num; i++)
{
int fd = evs[i].data.fd;
if (fd == lfd)
{
int cfd = accept(fd, NULL, NULL);
//struct epoll_event ev;
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_GETFL, flag);
ev.events = EPOLLIN|EPOLLET;
ev.data.fd = cfd;
//这一步会做拷贝,所以说ev用一个也是可以的
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
else//通信的描述符
{
char buf[5];
while (1)
{
int len = recv(fd, buf, sizeof(buf), 0);
if (len == -1)
{
if (errno==EAGAIN)
{
printf("数据读完了...\n");
break;
}
else
{
perror("recv");
return -1;
}
}
else if (len == 0)
{
printf("client disconnect...\n");
//先删除再做操作
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
}
printf("read buf = %s \n", buf);
for (int i = 0; i < len; i++)
{
buf[i] = toupper(buf[i]);
}
printf("after buf = %s \n", buf);
ret = send(fd, buf, strlen(buf) + 1, 0);
if (ret == -1)
{
perror("send error");
return -1;
}
}
}
}
}
close(lfd);
return 0;
}