ET和LT模式详解

一、rdlist不空的途径

  epoll_wait返回的条件是rdlist不空,而使rdlist不空的途径有两个。

1、文件描述符状态的改变

【对于读操作】:

  • 当buffer由不可读状态变为可读的时候,即由空变为不空的时候。
    在这里插入图片描述

  • 有新数据到达时,即buffer中的待读内容变多的时候。
    在这里插入图片描述
    【对于写操作】:

  • 当buffer由不可写变为可写的时候,即由满状态变为不满状态的时候。
    在这里插入图片描述

  • 当有旧数据被发送走时,即buffer中待写的内容变少的时候。
    在这里插入图片描述

2、文件描述符的事件位events置1

【对于读操作】:

  • buffer中有数据可读的时候,即buffer不空的时候fd的events的可读位就置1

【对于写操作】:

  • buffer中有空间可写的时候,即buffer不满的时候fd的events的可写位就置1
    在这里插入图片描述

  ET和LT模式下的epitem都可以通过途径1加入rdlist从而唤醒epoll_wait,但LT模式下的epitem还可以通过途径2重新加入rdlist唤醒epoll_wait。

  从源码角度看,ET模式下,文件描述符fd只会加入rdlist一次,所以epoll_wait只会被触发一次,然后移除此epitem;而LT模式下只要满足相应读写条件就会再次加入rdlist,epoll_wait会被触发多次

二、通过demo分析LT模式和ET模式

1、ET模式读和LT模式读(途径1)

// 当用户从控制台有任何输入操作时,输出”hello world!”。
#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要处理的事件
    epfd=epoll_create(1);//只需要监听一个描述符——标准输入
    ev.data.fd=STDIN_FILENO;
    ev.events=EPOLLIN|EPOLLET;//监听读状态同时设置ET模式
    epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev);//注册epoll事件
    for(;;)
   {
     nfds=epoll_wait(epfd,events,5,-1);
     for(int i=0;i<nfds;i++)
     {
        if(events[i].data.fd==STDIN_FILENO)
           cout<<"hello world!"<<endl;
     }
   }
}

在这里插入图片描述

  • 当用户输入一组字符,这组字符被送入buffer,字符停留在buffer中,又因为buffer由空变为不空,所以ET返回读就绪,输出”hello world!”。

  • 之后程序再次执行epoll_wait,此时虽然buffer中有内容可读,但是ET并不返回就绪(只有当buffer由空变为不空或者有新数据到达才返回就绪),导致epoll_wait阻塞。(底层原因是ET下就绪fd的epitem只被放入rdlist一次)。

  • 用户再次输入一组字符,导致buffer中的内容增多(有新数据到达),这将导致fd状态的改变,对应的epitem再次加入rdlist,从而使epoll_wait返回读就绪,再次输出“hello world!”。

ev.events=EPOLLIN; // 默认使用LT模式

在这里插入图片描述
  程序出现死循环,因为用户输入任意数据后,数据被送入buffer且没有被读出,所以LT模式下每次epoll_wait都认为buffer可读返回读就绪。导致每次都会输出”hello world!”。

2、LT模式读(途径2)

#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main(void)
{
    int epfd,nfds;
    char buf[256];
    struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要处理的事件
    epfd=epoll_create(1);//只需要监听一个描述符——标准输入
    ev.data.fd=STDIN_FILENO;
    ev.events=EPOLLIN;//使用默认LT模式
    epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev);//注册epoll事件
    for(;;)
   {
     nfds=epoll_wait(epfd,events,5,-1);
     for(int i=0;i<nfds;i++)
     {
       if(events[i].data.fd==STDIN_FILENO)
       {
          read(STDIN_FILENO,buf,sizeof(buf));// 将缓冲中的内容读出
          cout<<"hello world!"<<endl;
       }
    }
  }
}

在这里插入图片描述
  程序二依然使用LT模式,但是每次epoll_wait返回读就绪的时候我们都将buffer(缓冲)中的内容read出来,所以导致buffer再次清空下次调用epoll_wait就会阻塞。所以能够实现我们所想要的功能——当用户从控制台有任何输入操作时,输出”hello world!”。

3、ET模式写和LT模式写(途径1)

#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要处理的事件
    epfd=epoll_create(1);//只需要监听一个描述符——标准输出
    ev.data.fd=STDOUT_FILENO;
    ev.events=EPOLLOUT|EPOLLET;//监听读状态同时设置ET模式
    epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev);//注册epoll事件
    for(;;)
   {
      nfds=epoll_wait(epfd,events,5,-1);
      for(int i=0;i<nfds;i++)
     {
         if(events[i].data.fd==STDOUT_FILENO)
             cout<<"hello world!"<<endl;
     }
   }
};

在这里插入图片描述
  我们发现这将是一个死循环。下面具体分析一下这个程序的执行过程:

  • 首先初始buffer为空,buffer中有空间可写,这时无论是ET还是LT都会将对应的epitem加入rdlist,导致epoll_wait就返回写就绪

  • 程序想标准输出输出”hello world!”和换行符,因为标准输出为控制台的时候缓冲是“行缓冲”,所以换行符导致buffer中的内容清空——当有旧数据被发送走时,即buffer中待写的内容变少得时候会触发fd状态的改变。所以下次epoll_wait会返回写就绪。之后重复这个过程一直循环下去。

 cout<<"hello world!"; // 去掉换行符

在这里插入图片描述
  我们看到程序成挂起状态。因为第一次epoll_wait返回写就绪后,程序向标准输出的buffer中写入“hello world!”,但是因为没有输出换行,所以buffer中的内容一直存在,下次epoll_wait的时候,虽然有写空间但是ET模式下不再返回写就绪。这种情况原因就是第一次buffer为空,导致epitem加入rdlist返回一次就绪后移除此epitem,之后虽然buffer仍然可写,但是由于对应epitem已经不在rdlist中,就不会对其就绪fd的events的在检测了。

4、LT模式写(途径2)

#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要处理的事件
    epfd=epoll_create(1);//只需要监听一个描述符——标准输出
    ev.data.fd=STDOUT_FILENO;
    ev.events=EPOLLOUT;//使用默认LT模式
    epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev);//注册epoll事件
    for(;;)
   {
     nfds=epoll_wait(epfd,events,5,-1);
     for(int i=0;i<nfds;i++)
    {
      if(events[i].data.fd==STDOUT_FILENO)
         cout<<"hello world!";
    }
   }
};

在这里插入图片描述
  使用默认的LT模式,程序再次死循环。这时候原因已经很清楚了,因为当向buffer写入”hello world!”后,虽然buffer没有输出清空,但是LT模式下只要buffer有写空间就返回写就绪,所以会一直输出”hello world!”,当buffer满的时候,buffer会自动刷清输出,同样会造成epoll_wait返回写就绪。

三、ET模式下的读写注意事项

  经过前面的分析,我们可以知道,当epoll工作在ET模式下时,对于读操作,如果read一次没有读尽buffer中的数据,那么下次将得不到读就绪的通知,造成buffer中已有的数据无机会读出,除非有新的数据再次到达。对于写操作,主要是因为ET模式下fd通常为非阻塞造成的一个问题——如何保证将用户要求写的数据写完。要解决上述两个ET模式下的读写问题,我们必须实现:

  • 对于读,只要buffer中还有数据就一直读

  • 对于写,只要buffer还有空间且用户请求写的数据还未写完,就一直写

1、解决方法(非阻塞模式)

  • 读: 只要可读, 就一直读, 直到返回 0, 或者 errno = EAGAIN;

  • 写: 只要可写, 就一直写, 直到数据发送完, 或者 errno = EAGAIN

if (events[i].events & EPOLLIN) 
{
	n = 0;
	// 一直读直到返回0或者 errno = EAGAIN
    while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) 
  	{
  		n += nread;
    }
 	if (nread == -1 && errno != EAGAIN) 
	{
		perror("read error");
	}
    ev.data.fd = fd;
    ev.events = events[i].events | EPOLLOUT;
    epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}

if (events[i].events & EPOLLOUT) 
{ 
	int nwrite, data_size = strlen(buf);
	n = data_size;
	while (n > 0) 
	{
		nwrite = write(fd, buf + data_size - n, n);
		if (nwrite < n) 
		{
			if (nwrite == -1 && errno != EAGAIN) 
			{
				perror("write error");
			}
			break;
		}
		n -= nwrite;
	}
	ev.data.fd=fd; 
	ev.events=EPOLLIN|EPOLLET; 
	epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);  //修改sockfd上要处理的事件为EPOLIN
} 

  使用这种方式一定要使每个连接的套接字工作于非阻塞模式,因为读写需要一直读或者写直到出错(对于读,当读到的实际字节数小于请求字节数时就可以停止),而如果你的文件描述符如果不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞(最后一次read肯定要返回0,表示缓冲区没有数据可读了,因此最后一次read会阻塞)。这样就不能在阻塞在epoll_wait上了,造成其他文件描述符的任务饿死。

2、上述方法中写操作的改进

  仔细分析上述的写操作,我们发现这种方式并不很完美,因为写操作返回EAGAIN就终止写,但是返回EAGAIN只能说名当前buffer已满不可写,并不能保证用户(或服务端)要求写的数据已经写完。那么如何保证对非阻塞的套接字写够请求的字节数才返回呢(阻塞的套接字直到将请求写的字节数写完才返回)?

  我们需要封装socket_write()的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_write()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试

ssize_t socket_write(int sockfd, const char* buffer, size_t buflen)
{
	ssize_t tmp;
  	size_t total = buflen;
  	const char* p = buffer;
  	while(1)
  	{
    	tmp = write(sockfd, p, total);
    	if(tmp < 0)
    	{
	      // 当send收到信号时,可以继续写,但这里返回-1.
 	     if(errno == EINTR)
  	      return -1;
  	    // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满,
  	    // 在这里做延时后再重试.
 	    if(errno == EAGAIN)
 	    {
 	    	usleep(1000);
        	continue;
      	}
      	return -1;
    }
    if((size_t)tmp == total)
    	return buflen;
    total -= tmp;
    p += tmp;
  }
  return tmp;//返回已写字节数
}

四、ET模式下的accept注意事项

  考虑这种情况:多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理

  解决办法是用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept 返回 -1 并且 errno 设置为 EAGAIN 就表示所有连接都处理完

while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, 
		(size_t *)&addrlen)) > 0)
{
	handle_client(conn_sock);
}
if (conn_sock == -1)
{
	if (errno != EAGAIN && errno != ECONNABORTED && 
		errno != EPROTO && errno != EINTR)
		perror("accept");
}

五、多路IO复用accept为什么应该工作在非阻塞模式?

  如果accept工作在阻塞模式,考虑这种情况: TCP 连接被客户端夭折,即在服务器调用 accept 之前(此时select等已经返回连接到达读就绪),客户端主动发送 RST 终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在 accept 调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept 调用上(实际应该阻塞在select上),就绪队列中的其他描述符都得不到处理。

  解决办法是把监听套接口设置为非阻塞, 当客户在服务器调用 accept 之前中止某个连接时,accept 调用可以立即返回 -1这是源自 Berkeley 的实现会在内核中处理该事件,并不会将该事件通知给 epoll,而其他实现把 errno 设置为 ECONNABORTED 或者 EPROTO 错误,我们应该忽略这两个错误。

六、LT模式下会不停触发socket可写事件,如何处理?

  • 需要向socket写数据的时候才把socket加入epoll,等待可写事件。接受到可写事件后,调用write或者send发送数据。当所有数据都写完后,把socket移出epoll

  • 使用ET模式(边沿触发),这样socket有可写事件,只会触发一次。

  • 在epoll_ctl()使用EPOLLONESHOT标志,当事件触发以后,socket会被禁止再次触发

七、使用ET和LT的区别

  • LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况

  • ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况

  下面举一个列子来说明LT和ET的区别(都是非阻塞模式,阻塞就不说了,效率太低):

  • 采用LT模式下,如果accept调用有返回就可以马上建立当前这个连接了,再epoll_wait等待下次通知,和select一样。

  • 但是对于ET而言,如果accpet调用有返回,除了建立当前这个连接外,不能马上就epoll_wait还需要继续循环accpet,直到返回-1,且errno==EAGAIN。

转自:http://blog.chinaunix.net/uid/25601623.html

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值