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

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


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

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


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

#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#include 
#include 
#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可以修改成边缘触发。

你可能感兴趣的:(Linux及计算机体系结构)