LT与ET模式

一、ET 和 LT 模式定义

       对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理此事件,直到该事件被处理。而对于ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理此事件,因为后续的epoll_wait将不会再向应用程序通知这一事件,可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数。

LT:电平触发

       发送端发送helloword,接收端recv只接受五位时,仅仅读取了hello,下一次调用epoll_wait时,未处理完的事件还会被再次触发,将剩下的world读取。

ET:边沿触发

       发送端发送helloword,接收端recv时,仅仅读取了hello,下一次调用epoll_wait时,未处理完的事件不会被再次触发(通知应用程序),每次必须将就绪文件描述符上的事件处理完成。


 二、epoll的事件----EPOLLET 

      LT 是默认的工作方式,这种模式下epoll相当于一个效率较高的poll。当往epoll内核事件表中注册有个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。我们今天就主要将epoll的相比较select和poll所多出来的两个事件,结合ET模式和LT模式来分析对比一下。

 我们首先要明白ET为什么是高效的模式??

  1. 同一个事件ET只会通知一次,LT会通知多次,epoll_wait函数调用多次,epoll_wait的调用需要消耗时间。
  2. LT模式下,epoll_wait因为上一个事件未处理完而直接返回,造成对后续就绪事件的延迟处理。
  3. ET模式内核实现时,内核事件表底层是红黑树,其拥有的链表会将rdlist中就绪的文件描述符通过txlist拷贝给用户空间,并且rdlist会被清空。
  4. LT模式内核实现时,将rdlist中的就绪文件描述符通过txlist拷贝给用户空间,rdlist也会被清空,但是会将未处理的或处理未完成的文件描述符又返回给rdlist,以便下次继续访问。
  5. LT是事件处理完删除,ET是通知后就删除。

三、两者模式之间的对比以及ET代码

      先给大家看一下,ET和LT在处理事件不同之处,待会我会给大家验证下ET模式下的读取数据的情况。

     这就是两者在处理响应事件时候的本质区别,因此ET模式得加一个非阻塞状态,来检验数据是否已经全部被读取完毕。事件检验真理-----我们来看下ET代码的实现。  

     在这之前我们得明白ET模式得满足以下几点:

     1.文件描述符必须设置为非阻塞状态;(因为socket在创建的时候默认为阻塞的)

     2.内核事件表上的文件描述符必须关注EPOLLET事件

     3.当事件发生时,必须以循环的方式处理事件,知道事件处理完成。  

                recv的返回值为<= 0 -----》errno    以及它的两个值 EAGAIN  和 EWOULDBLOCK

//ET模式
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <string.h>

#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#define SIZE 100

int CreateSocket(int port,char *ip)
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);//协议簇  TCP协议
	assert(sockfd != -1);

	struct sockaddr_in ser,cli;
	memset(&ser,0,sizeof(ser));

	ser.sin_family = AF_INET;
	ser.sin_addr.s_addr = inet_addr(ip); //IP地址
	ser.sin_port = htons(port);

	int res = bind(sockfd,(struct sockaddr *)&ser,sizeof(ser));
	assert(res != -1);//绑定失败 1.IP地址不对 2.端口号被占用或者没有权限使用
	listen(sockfd,5); //size = 5 内核维护的已经完成链接客户端的文件描述符个数(6)实际会加一

	return sockfd; 
}

void et(int fd,struct epoll_event event,int epollfd)
{
  if(event.events & EPOLLRDHUP)
  {
     close(fd);
	 epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,NULL);
	 printf("%d  client break link\n",fd);
  }
  else if(event.events & EPOLLIN)
  {
	printf("%d  's  data is :  \n",fd); //修改
    while(1)
	{
		char buff[128] = {0};
		int n = recv(fd,buff,5,0);
		if(n <= 0)
		{
			if((errno == EAGAIN) || (errno == EWOULDBLOCK)) //数据读取完成
			{
				break;
			}
			close(fd);
			epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,NULL);
		}
		printf("%s",buff); // 去掉%d fd 
	} 
	printf("\n");
	send(fd,"OK",2,0);
  }
  else
  {
	  printf("error\n");
  }
}

void SetNonBlock(int fd) //非阻塞状态
{
   int old = fcntl(fd,F_GETFL);
   int new = old | O_NONBLOCK; //临时变量的值给文件描述符,改变内部属性
   fcntl(fd,F_SETFL,new); //原来状态上加非阻塞
}

int main()
{

    int sockfd = CreateSocket(6888,"127.0.0.1");
    int epollfd = epoll_create(5);
    assert(epollfd != -1);

        struct epoll_event event;
	event.events = EPOLLIN;
	event.data.fd = sockfd;

	epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&event);

	while(1)
	{
        struct epoll_event events[SIZE];
	 	int n = epoll_wait(epollfd,events,SIZE,-1);

		if(n <= 0)
		{
			printf("Epoll error\n");
			continue;
		}
        
  		printf("epoll wait return\n");

		//deal link
		int i = 0;
		for(; i < n; ++i)
		{
        	int fd = events[i].data.fd;
	        if(fd == sockfd) //与客户端连接 连接不一定发生
		    {
				struct sockaddr_in cli; 
				int len = sizeof(cli);
				int c = accept(fd,(struct sockaddr*)&cli,&len);
				if(c < 0)
				{
					printf("one client link error\n");
					continue;
				}
				SetNonBlock(c);
               
				event.events = EPOLLIN | EPOLLRDHUP | EPOLLET;
		                event.data.fd = c;
	                        epoll_ctl(epollfd,EPOLL_CTL_ADD,c,&event);
			}
			else
			{
                            et(fd,events[i],epollfd);
			}
		}
					 
	}    
	
}

       对于上述代码。我们要使用fcntl函数,它可以改变已打开的文件的性质,将阻塞设置为非阻塞。它有5种功能,我们使用的就是第三种的F_GETFL功能函数

     我们需要验证ET模式的读取数据的情况,当我把buff中一次读取数据的值设为5时,我输入10个数据,会发现它是一次性读出,如下图:

       满足了ET循环读完buff里数据的特征,因为它不会再次触发。而LT模式它会两次读取完buff里的数据,感兴趣的话大家可以验证下。在《Linux高性能服务器编程》的154页。

三、epoll的另一个事件---EPOLLONESHOT事件

       即使我们使用ET模式,但是一个socket上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。

       比如说在读取完某个socket上的数据后开始处理这些数据,而在数据处理过程中该socket上又有新的数据可读,EPOLLIN会再次被触发,此时另外一个线程被唤醒起来读取这些数据,于是就出现了两个线程同时操作一个socket的局面,这当然不是我们所期望的,这时就可以使用epoll的EPOLLONESHOT事件实现。

       对于注册了EPOLLONESHOT事件的描述符,操作系统最多触发其上注册的一个可读、可写或异常事件。这样,当一个线程在处理某个socket时,其他线程也是不可以有机会操作该socket的,但我们也要反过来思考,注册了该事件后,一旦线程处理完毕。我们应该立即重置此事件,以确保这个socket下一个可读时,其EPLOOIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。

       部分代码如下:

     

       因此尽管一个socket在不同事件可能被不同的线程处理,但可以使用reset_oneshot()函数重置socket上的注册事件。并且同一时刻肯定只有一个线程为它服务,这就保证了连接的完整性,从而避免了很多可能的竞态条件。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值