LT(水平触发)和ET(边缘触发)

本文介绍了epoll的两种工作模式:水平触发(LT)和边缘触发(ET)。详细阐述了这两种模式下的工作原理及应用场景,并通过示例代码展示了如何在实际编程中应用ET模式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

以下内容均为本人学习笔记,若有不当,感谢指出
上一篇学习了epoll 的基本概念和使用:epoll
今天学习了解epoll 的两种工作模式:水平触发 和 边缘触发

一、LT模式(Level Triggered)

epoll默认为LT工作模式,所以event.events选项中EPOLLET而没有LT

工作模式如下:
- 若缓冲区中有10k数据,我们可以多次进行读取
- 比如先读取2k数据,再次调用epoll_wait(),并且会立刻通知socket读事件就绪,可以再次读取剩余的数据
- 再次调用epoll_wait

一、ET模式(Edge Triggered)

ET工作模式即我们将添加进epoll描述符时候使用EPOLLET标志,epoll进入工作模式
工作模式如下:
- 若缓冲区中有10k数据,第一次只读取了1k
- 再次调用epoll_wait,已经不是就绪状态了,
- ET模式下,只有当缓冲区中数据由无到有,由少变多时才会进行读取数据
- 支持阻塞和非阻塞的读写

ET模式带来的问题
1. 因为只有当缓冲区中数据由无到有,由少变多时才会区读取数据,
所以一次要将缓冲区中的数据读完,否则剩下的数据可能就读不到了。
正常的读取数据时,我们若是要保证一次把缓冲区的数据读完,意为本次读被阻塞时即缓冲区中没有数据了,可是我们 epoll 服务器要处理多个用户的请求,read()不能被阻塞,所以采用非阻塞轮询的方式读取数据。

2.若轮询的将数据读完,对方给我们发9.5k的数据,我们采取每次读取1k的方式进行轮询读取,在读完9k的时候,下一次我们读到的数据为0.5k,我们就知道缓冲区中数据已经读完了就停止本次轮询。
但还有一种情况,对方给我们发的数据为10k,我们采取每次读取1k的方式轮询的读取数据,当我们已经读取了10k的时候,并不知道有没有数据了,我们仍旧还要尝试读取数据,这时read()就被阻塞了。

结论:epoll在ET模式下必须以非阻塞轮询的方式进行读取数据

三、epoll应用场景

适合用epoll的应用场景:对于连接特别多,活跃的连接特别少,这种情况等的时间特别久,典型的应用场景为一个需要处理上万的连接服务器,例如各种app的入口服务器,例如qq

不适合epoll的场景:连接比较少,数据量比较大,例如ssh

epoll 的惊群问题:因为epoll 多用于 多个连接,只有少数活跃的场景,但是万一某一时刻,epoll 等的上千个文件描述符都就绪了,这时候epoll 要进行大量的I/O,此时压力太大。

上一片篇中实现epoll的LT模式

四、epoll的ET模式举例

注意文件描述符设为非阻塞,加入epoll描述符中时,加EPOLLET标志

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

#include <sys/epoll.h>
#include <fcntl.h>


//使用epoll实现多路复用


//将文件描述符设置为非阻塞
void SetNoBlock(int fd)
{
  int flag =  fcntl(fd,F_GETFL);
  fcntl(fd,F_SETFL, flag | O_NONBLOCK);
}
//启动服务器
int server_start(const char * ip,const short port)
{
  int sock = socket(AF_INET,SOCK_STREAM,0);
  if(sock < 0)
  {
    perror("socket");
    return -1;
  }

  sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = inet_addr(ip);
  addr.sin_port = htons(port);

  int ret = bind(sock,(sockaddr *)&addr,sizeof(addr));
  if(ret < 0)
  {
    perror("bind");
    return -1; 
  }

  ret = listen(sock,5);
  if(ret < 0)
  {
    perror("listen");
    return -1;
  }
  return sock;
}


//处理连接socket就绪
//封装处理非阻塞轮询accept()
void Process_listen_socket(int epfd,int listen_socket,sockaddr_in  * peer,socklen_t peer_len)
{
  //就进行非阻塞式轮询accept()

  //我们看到这里返回的events数据中并没有文件描述符
  //所以我们最开始将data.fd赋值为文件描述符就是在这里用到
  while(1)
  {
    int new_socket = accept(listen_socket,(sockaddr *)peer,&peer_len);

    if(new_socket < 0 && errno ==EAGAIN)
    {
      //因为将文件描述符都设置为非阻塞,这里的accpt()为非阻塞的
      //这里就需要轮询式的进行accpet()
      //这里的listen()函数的第二个参数为5,表示排队等待连接的客户端最多有5个
      //说明已经将所有的文件描述符进行accpet
      perror("accpet");
      return;
    }
    //如果创建new_socket成功之后
    //就将new_socket 加入到epfd中,让epoll_wait()再去监视new_socket的状态

    //将文件描述符设置为非阻塞
    SetNoBlock(new_socket);

    epoll_event event ;
    event.data.fd = new_socket;
    event.events = EPOLLIN | EPOLLET;
    epoll_ctl(epfd,EPOLL_CTL_ADD,new_socket,&event); 

  }
}


//封装Read,实现非阻塞的轮询式的read()
ssize_t Read(int sock,char * buf,ssize_t max_size)
{
  if(buf == NULL || max_size <= 0)
  {
    return -1;
  }
  ssize_t total_size = 0;
  while(total_size < max_size)
  {
    //控制每次读取都不能使buf越界
    //剩余空间如果大于1024,就一次性读取1024,否则就读取剩余的空间大小
    int len = (max_size - total_size) > 1024 ? 1024 :(max_size - total_size);
    ssize_t read_size = read(sock,buf+total_size,len);
    if(read_size < 0 && errno == EAGAIN)
    {
      //这里的read()为非阻塞的
      //说明为缓冲区中没有数据资源
      //非阻塞轮询就结束了
      printf("data not ready\n");
      break;
    }

    //如果这里为读取失败,read_size < 0 errno != EAGAIN
    //让其再次尝试读取

    if(read_size == 0)
    {
      printf("rad done\n");
      break;
    }
    else
    {
      //正常读取的情况
      //修改total_size
      total_size += read_size; 
    }
  }
  return total_size;
}


//处理连接上socket,进行读取数据
void Process_accept_socket(int epfd, int acc_socket,sockaddr_in * peer)
{
  char buf[1024 * 10] = {0};
  //非阻塞轮询进行读取
  ssize_t read_size = Read(acc_socket,buf,sizeof(buf)-1); 

  if(read_size <= 0 )
  {
    close(acc_socket);
    epoll_ctl(epfd,EPOLL_CTL_DEL,acc_socket,NULL);
    printf("client[%s:%d] disconnect!\n",inet_ntoa(peer->sin_addr),peer->sin_port);
    return;
  }
  else
  {
    //正常读取的情况
    buf[read_size] = '\0';
    printf("[client %s:%d.%d]say:%s",inet_ntoa(peer->sin_addr),ntohs(peer->sin_port),acc_socket,buf);
    //回显服务
    //将收到的内容发送给服务器
    write(acc_socket,buf,strlen(buf));
  }
}

//主函数
int main(int argc,char * argv[])
{
  //检验命令行参数是否正确
  if(argc != 3)
  {
    printf("Usage : ./server ip port\n");
    return 1;
  }

  //一、启动服务器
  int listen_socket = server_start(argv[1],atoi(argv[2]));

  if(listen_socket < 0)
  {
    printf("server start failed\n");
    return 2;
  }
  //将文件描述符设置为非阻塞
  SetNoBlock(listen_socket);

  printf("server start ok\n");

  //1.创建epoll对象
  int epfd = epoll_create(256);
  if(epfd < 0)
  {
    perror("epoll_create");
    return 3;
  }
  epoll_event event;
  event.data.fd = listen_socket;//此处将要监听的文件描述符放在data中,后面取值要用到
  //event.events = EPOLLIN;//关心的事件为读事件
  event.events = EPOLLIN | EPOLLET;//关心的事件为读,并且设置为边缘触发
  //2.将listen_socket添加到epfd中
  epoll_ctl(epfd,EPOLL_CTL_ADD,listen_socket,&event);


  //二、进行事件循环
  while(true)
  {
    //创建事件数组,返回已经就绪的文件
    epoll_event events[2];
    int size = epoll_wait(epfd,events,sizeof(events)/sizeof(events[0]),-1);
    if(size < 0)
    {
      perror("epoll_wait");
      continue;
    }

    if(size == 0)
    {
      printf("time out\n");
      continue;
    }

    //epoll_wait()成功返回后
    int i = 0;
    for(i = 0; i < size ;++i)
    {
      sockaddr_in peer;
      socklen_t peer_len =  sizeof(peer);
      if(events[i].data.fd == listen_socket)
      {
      //(a)listen_socket就绪
      //连接事件就绪
        Process_listen_socket(epfd,listen_socket,&peer,peer_len);
        continue;
      }//end if(events[i].data.fd == listen_socket)
      else
      {
     //(b)new_sock就绪
     //读事件就绪
        Process_accept_socket(epfd,events[i].data.fd,&peer);
      }//end else
    }//end for()
  }//end while(true)

  close(epfd);
  close(listen_socket);

}// end main

完。

&lt;think>嗯,用户问的是epollLTET模式的区别。我得先回忆一下这两种触发模式的工作机制。LT水平触发ET边缘触发。记得LT模式下,只要文件描述符处于就绪状态,epoll_wait就会不断通知,而ET只在状态变化时通知一次。这样的话,LT可能更适合初学者,因为不用担心事件没处理完,而ET需要一次处理完所有事件,否则会丢失后续的通知。 接下来需要详细解释两者的区别。首先,触发条件的不同。LT只要可读或可写就会触发,而ET只在状态变化的时候触发,比如从不可读变为可读。然后,处理方式的不同,ET必须非阻塞读取,直到返回EAGAIN,而LT可以分多次处理。性能方面,ET减少了epoll_wait的调用次数,可能更高效,但编程难度大,容易漏事件。 要给出代码示例的话,LT的代码可能比较简单,循环读取直到没有数据。而ET需要设置非阻塞,并且循环读取直到错误,比如EAGAIN出现。同时,ET模式需要在添加到epoll时使用EPOLLET标志。 还要注意ET模式下,如果没有处理完所有数据,之后就不会再收到通知,所以必须一次处理完。而LT的话,如果这次没处理完,下次epoll_wait还会返回该事件。 可能用户在使用ET时遇到问题,比如数据没有读完,导致程序卡住。这时候需要确保在ET模式下正确处理所有可用数据。另外,ET通常非阻塞socket一起使用,避免在读取时阻塞。 最后,相关的问题可能包括如何选择两种模式,ET模式下必须注意什么,为什么ET需要非阻塞IO,以及两种模式的应用场景。这些问题可以帮助用户更深入理解两者的使用场景注意事项。&lt;/think>&lt;&lt; epollLT(Level Triggered)ET(Edge Triggered)是两种I/O事件触发模式,主要区别体现在事件通知机制编程实现方式上。 1. **触发条件** - **LT模式**:只要文件描述符对应的缓冲区有未处理的数据,就会持续触发事件通知。 - **ET模式**:仅在缓冲区状态发生变化时触发一次事件(如从无数据到有数据)。 2. **事件处理方式** - **LT模式**:可以分多次处理数据,未处理完的数据会持续触发后续事件。 - **ET模式**:必须一次性处理完所有数据(直到返回`EAGAIN`错误),否则未处理的数据将不再触发事件。 3. **性能差异** - **ET模式**:减少`epoll_wait()`调用次数,适合高并发场景,但编程复杂度更高。 - **LT模式**:实现简单,但可能因未及时处理数据导致多次触发事件。 **代码示例(ET模式必须使用非阻塞IO)** ```cpp // LT模式示例(默认模式) struct epoll_event event; event.events = EPOLLIN; // 默认LT模式 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event); // ET模式示例 event.events = EPOLLIN | EPOLLET; // 显式设置ET模式 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event); // ET模式必须配合非阻塞IO fcntl(sock_fd, F_SETFL, fcntl(sock_fd, F_GETFL, 0) | O_NONBLOCK); // ET模式下的读取逻辑 while (true) { ssize_t count = read(fd, buf, sizeof(buf)); if (count == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { break; // 数据已读完 } // 处理其他错误... } // 处理数据... } ``` **关键区别总结** | 特性 | LT模式 | ET模式 | |--------------------|----------------------------|----------------------------| | 触发频率 | 持续触发直到数据被处理 | 仅状态变化时触发一次 | | 编程复杂度 | 简单 | 需要完整处理所有数据 | | 资源效率 | 可能产生更多系统调用 | 更高效但需要正确实现 | | 适用场景 | 简单场景/长连接 | 高性能服务器/短连接 |
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值