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)
{
    // 数据处理...
}

显然函数是阻塞的,执行到这里就变成了单线程阻塞于此,所以我们需要将这里的文件描述符设置为非阻塞模式

  1. 先将文件描述符设置为非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;                                                        
fcntl(cfd, F_SETFL, flag);
  1. 在监听的地方,将通信的文件描述符设置为非阻塞
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);
                }
            }

  1. 另一个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;
}
posted @ 2023-11-11 04:32  LiviaYu  阅读(49)  评论(0)    收藏  举报