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

本文介绍了epoll的边缘触发(ET)和水平触发(LT)两种模式的区别。边缘触发仅在状态变化时触发事件,而水平触发在满足条件时持续触发。文章通过酱油购买的例子和代码示例解释了两者的工作原理,并讨论了在边缘触发模式下如何避免阻塞以提高效率。同时强调,设置边缘触发时,文件描述符应设为非阻塞并进行循环读取以保持高效。

epoll的边缘触发和水平触发:


epoll的默认模式是水平触发。
先大概了解一下这两种触发模式有什么不同:
水平触发(Level Trigger,也称条件触发)只要满足条件,就触发一个事件(只要有数据还未读完,就会一直触发)
边缘触发(Edge Trigger)每当状态发生变化时就触发一个事件。

可能概念不容易理解,这里举一个例子大概就能明白两者的区别了:比如某个人让你去买几袋酱油,你只买了一袋回去,水平触发的做法就是他让你继续去把剩下的几袋酱油买回来,如果没有完成任务,就一直通知你;边缘触发的做法就是不管完没完成任务,反正他让你买了,买没买完就是你自己的事了,下次买酱油这件事他就不管了,会让你去做其它的事。


通过上面的例子,我们对边缘触发和水平触发有了一个大概的了解,下面通过代码来深入了解:

#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>

#define MAXLINE 10

int main()
{
    pid_t pid;
    int fd[2];
    int i;
    char str[MAXLINE], ch = 'a';
    bzero(str, sizeof(str));
    //使用管道,fd[0]默认是读端,fd[1]默认是写端
    pipe(fd);
    pid = fork();
    if(pid == 0)    //child 负责写端
    {
        close(fd[0]);
        while(1)
        {
            for(i = 0; i < MAXLINE / 2; i++)
            {
                str[i] = ch;
            }
            ch++;
            str[i - 1] = '\n';
            for(; i < MAXLINE; i++)
            {
                str[i] = ch;
            }
            str[i - 1] = '\n';

            write(fd[1], str, sizeof(str));
            sleep(5);
        }

    }
    else if(pid > 0) //parent 负责读端
    {
        close(fd[1]);
        struct epoll_event event;
        struct epoll_event resevent[10];

        int res;
        //调用epoll_create创建红黑树树根
        int efd = epoll_create(10);
        event.data.fd = fd[0];
        //触发方式默认是EPOLLLT
        event.events = EPOLLIN;
        //将读端文件描述符加入epoll监听的树中
        epoll_ctl(efd, EPOLL_CTL_ADD, fd[0], &event);

        while(1)
        {
            //当子进程发送数据时,就会触发事件进行读事件
            res = epoll_wait(efd, resevent, 10, -1);  
            if(resevent[0].data.fd == fd[0])
            {
                int len = read(fd[0], str, MAXLINE/2);
                //写回屏幕
                write(STDOUT_FILENO, str, len);
            }
        }
        close(efd);
    }
    if(pid > 0)
      close(fd[0]);
    else
      close(fd[1]);
    return 0;
}

这段程序运行的结果是:

aaaa
bbbb
bbbb
cccc
....

当我们修改成边缘触发(这里就不贴出完整代码了,因为改动很少),只需要把event.events = EPOLLIN;改成event.events = EPOLLIN | EPOLLET即可。

运行的结果是:

aaaa
bbbb
bbbb
cccc
....

虽然结果是一样的,但是可以发现每过5秒,第一段程序会输出10个字符(包括换行),而第二段程序只会输出5个。而且我们经过分析不难发现str[MAXLINE]数组在水平触发时,每次是全部输出的,而边缘触发情况下,每次只输出了一半,这是因为我们父进程读的时候只读了一半。这就说明了,在水平触发模式下,只要有剩余的数据,epoll_wait会一直通知你,而在边缘触发模式下,则每个文件描述符只会通知一次。


那么问题来了,很明显传过来的数据我们是需要的,为了把数据读完,就需要重复调用read来读取,考虑这样一种情况,每次循环使用read读取10个字节,但是我们的数据总量只有21个字节,那么经过两次读取之后,还剩1个字节,再次读取时,由于不满足就会阻塞(是否阻塞要由设备的属性和设定所定,一般来说,读字符终端、网络的socket描述符、管道等会阻塞,而读磁盘上的文件一般不会)。阻塞在这肯定会影响程序的效率的。

解决方法是,当我们使用边缘触发时,将对应的文件描述符设置为非阻塞即可。

#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define MAXLINE 10

int main()
{
  pid_t pid;
  int fd[2];
  int i;
  char str[MAXLINE], ch = 'a';
  bzero(str, sizeof(str));
  //使用管道,fd[0]默认是读端,fd[1]默认是写端
  pipe(fd);
  pid = fork();
  if(pid == 0)  //child 负责写端
  {
    close(fd[0]);
    while(1)
    {
      for(i = 0; i < MAXLINE / 2; i++)
      {
        str[i] = ch;
      }
      ch++;
      str[i - 1] = '\n';
      for(; i < MAXLINE; i++)
      {
        str[i] = ch;
      }
      str[i - 1] = '\n';

      write(fd[1], str, sizeof(str));
      sleep(5);
    }

  }
  else if(pid > 0) //parent 负责读端
  {
    close(fd[1]);
    //设置非阻塞
    int flag = fcntl(fd[0], F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(fd[0], F_SETFL, flag);
    struct epoll_event event;
    struct epoll_event resevent[10];

    int res;
    //调用epoll_create创建红黑树树根
    int efd = epoll_create(10);
    event.data.fd = fd[0];
    //触发方式默认是EPOLLLT
    event.events = EPOLLIN;
    //将读端文件描述符加入epoll监听的树中
    epoll_ctl(efd, EPOLL_CTL_ADD, fd[0], &event);

    int len;
    while(1)
    {
      //当子进程发送数据时,就会触发事件进行读事件
      res = epoll_wait(efd, resevent, 10, -1);  
      if(resevent[0].data.fd == fd[0])
      {
        //循环读取,直到读完为止
        while((len = read(fd[0], str, MAXLINE/2)) > 0)
        {
            write(STDOUT_FILENO, str, len);
        }            
      }
    }
    close(efd);
  }
  if(pid > 0)
    close(fd[0]);
  else
    close(fd[1]);
  return 0;
}

边缘触发比水平触发更高效的原因不会让同一个文件描述符多次被处理,比如有些文件描述符已经不需要再读写了,但是在水平触发下每次都会返回,而边缘触发只会返回一次。
最后提醒一点,如果设置边缘触发,则必须将对应的文件描述符设置为非阻塞模式并且循环读取数据。否则会导致程序的效率大大下降。
poll和epoll默认采用的都是水平触发,只是epoll可以修改成边缘触发。

### 原理 - **边缘触发(Edge Triggered,ET)**:当被监控的文件描述符上有可读写事件发生时,`epoll_wait()` 会通知处理程序去读写。若这次没有把数据全部读写完(如读写缓冲区太小),下次调用 `epoll_wait()` 时不会再次通知,直到该文件描述符上出现第二次可读写事件才会再次通知 [^2]。 - **水平触发(Level Triggered,LT)**:是epoll的默认模式。只要被监控的文件描述符上有可读写的数据,`epoll_wait()` 就会持续通知处理程序去读写,直到数据被处理完 [^1]。 ### 区别 - **通知机制**:边缘触发只在事件状态变化(由不可读写变为可读写)时通知一次;水平触发只要文件描述符处于可读写状态就会持续通知 [^2]。 - **数据处理要求**:边缘触发要求在一次通知中尽可能多地处理数据,因为下次通知可能要等下一次状态变化;水平触发则不需要一次性处理完数据,后续还会继续通知。 - **效率**:边缘触发效率较高,系统不会充斥大量不关心的就绪文件描述符;水平触发可能会产生较多的通知,效率相对较低 [^2]。 ### 应用 - **边缘触发**:适用于需要高效处理大量并发连接的场景,如高性能服务器,尤其是需要快速响应处理数据的场景。在边缘触发方式下,建议读到 `read return -1, errno = EAGAIN`(读完了)或 `read return 0`(连接关闭了) 。从Linux 2.6.17 开始,可以用 `EPOLLRDHUP` 来检测socket的正常关闭 [^4]。示例代码如下: ```python import select import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setblocking(0) server.bind(('localhost', 8888)) server.listen(5) epoll = select.epoll() epoll.register(server.fileno(), select.EPOLLIN | select.EPOLLET) try: while True: events = epoll.poll() for fileno, event in events: if fileno == server.fileno(): conn, addr = server.accept() conn.setblocking(0) epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET) elif event & select.EPOLLIN: data = b'' while True: try: chunk = conn.recv(1024) if not chunk: break data += chunk except BlockingIOError: break if data: print(f"Received: {data.decode()}") finally: epoll.unregister(server.fileno()) epoll.close() server.close() ``` - **水平触发**:适用于对数据处理实时性要求不高,或者处理逻辑相对简单的场景,因为它的编程模型更简单,不需要一次性处理完所有数据。设置水平触发只需 `ev.events = EPOLLIN` ,这是默认的触发模式 [^1]。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值