119-epoll(触发模式)

在前面,有小小的提及到了 epoll 的事件触发模式,当时说默认情况下为水平触发,另外一种情况为边沿触发。而在前面的实验中,也是使用的默认方式,即水平触发。

1. 触发方式

我知道,作为初学者的你已经懵圈了,触发是什么意思?在英文原文,水平触发为 level-triggered 而边沿触发为 edge-triggered. 关键在于 triggered 这个单词。来看看 Collins 词典对 trigger 的解释:


这里写图片描述
图1 trigger

可以看到,trigger 就是手枪上的扳机,它是用来做引爆的!换句话说,level-triggered 和 edge-triggered 就是两种不同的引爆方式!那么,问题来了,如何引爆?引爆什么?(手动黑人问号。。。)

答案就在于前面讲的 IO 事件。之前我们说过,如果缓冲区产生了变化,就会引发 IO 事件。这种缓冲区的变化导致的 IO 事件,就叫 edge-triggered 引爆方式。

level-triggered 是什么意思?正好相反,即使缓冲区没有发生变化,也可能会触发 IO 事件!“可能”这个词用的很谨慎。实际上,分两种情况:

  • 对于读 IO,只要缓冲区有数据可读,即使没产生变化也会触发 IO 事件。如果缓冲区为空,则不会产生 IO 事件。
  • 对于写 IO,只要缓冲区不满,也就随时可写,即使没产生变化,也会触发 IO 事件。

由上面的解释可以知道,edge-triggered 只是 level-triggered 的一个子集。触发方式对于 epoll_wait 函数来说,就是什么时候该返回,而什么时候不返回的问题。所以,可以总结:

  • 如果为 edge-triggered 方式,只有在缓冲区发生变化的情况下,epoll_wait 函数才会返回。
  • 如果为 level-triggered 方式,那么只要缓冲区有数据可读,或者缓冲区有空位可写,则 epoll_wait 就返回。

2. 如何设置 epoll 的触发方式

前面我们已经非常非常的详细的说明了 struct epoll_events 的 events 成员了,它除了设置监听什么事件以外,还可以设置触发方式,如果想要设置 edge-triggered 触发方式,只要执行下面的语句就行了:

ev.events |= EPOLLET;

3. 演示两种触发方式的不同

这里使用上一篇文章的代码,然后修改两处:

  • 第一个地方,添加 EPOLLET 触发模式
for(i = 0; i < 3; ++i) {
  ev.data.fd = fds[i];
  ev.events = EPOLLIN;
  // 添加下面这一行,我们通过命令行传参的方式来控制是使用还是不使用 edge-triggered 方式
  if (argc > 1) ev.events |= EPOLLET;
  if (epoll_ctl(epfd, EPOLL_CTL_ADD, fds[i], &ev) < 0) {
    PERR("epoll_ctl");
  }
}
  • 第二个地方,process 函数
int process(char* prompt, int fd) {
  int n;
  // 用作 read 参数的 buf 缓冲区大小改成了 4 字节
  char buf[4];
  char line[64];
  // 最多读 3 个字节,第 4 字节是用来保存 '\0' 字符的。
  n = read(fd, buf, 3); 
  //...
}

这样做的理由在于,我们不希望一次性把缓冲区读完。

4. 实验

在修改完上面的程序后,开始实验。当然分两种情况了,第一种就是默认的方式,使用 level-triggered 方式。

4.1 level-triggered 实验


这里写图片描述
图2 水平触发

在实验中,管道 a.fifo 的写端首先写入'he'这个单词,epoll_et 中最后通过 read 函数将缓冲区中的这两个字符完全读取,并打印。

接下来,又写入'helloword',此时描述符 fd3 对应的内核缓冲区中就有了 'helloword' 这一串。在 epoll_et 程序中,立即触发了 IO 事件,epoll_wait 返回了。进入了 process 函数。

因为 read 接收缓冲区 buf 大小只有 4,所以 read 的时候最多读入 3 个字节的数据,buf 第 4 字节是用来保存 '\0' 字符的,所以不能被占用。从内核缓冲区读取 'hel' 三个字符后到 buf 后并打印在屏幕上,process 函数结束。

接下来又回到 epoll_wait,前方高能,因为此时还没有任何人向管道写数据,所以描述符 fd3 对应的内核缓冲区中还是只有数据 'loword',根据 level-triggered 的规则,只要缓冲区中有数据,就触发 IO 事件,因此 epoll_wait 又立即返回,后面的事件,其实都差不多了,就不重复了。

向管道 b.fifo 中写入了 'helloepoll' 实际上也经历了这个过程。

4.1 edge-triggered 实验

要使用 edge-triggered,需要在启动 epoll_et 的时候传入一个参数,随便什么都行,我就在后面加了个数字 1.

看图 3,这个过程比 level-triggered 方式要复杂,一步一步来看。首先,向 a.fifo 写入两个字符 'he',然后 epoll_et 中 epoll_wait 返回,读取 'he' 打印。

第二次,向 a.fifo 写入了'helloworld',此时 fd3 对应的内核缓冲区由空变成了有数据 'helloworld',触发了 IO 事件,因此 epoll_wait 函数返回,进入 process 函数后,从缓冲区读取了三个字符,但是,此时缓冲区中还有字符'loworld',接下来又返回到了 epoll_wait,前方高能,虽然缓冲区中有数据,但是此时的触发方式是 edge-triggered!数据没有产生变化,也就是没有增多,因此不会触发 IO 事件,epoll_wait 阻塞了!


这里写图片描述
图3 边沿触发

接下来看图 4,这时候向管道仅仅写入一个字母 'a',fd3 对应的内核缓冲区产生变化了,再由 'loworld' 变成了 'loworlda'的那一瞬间之前(数据 a 在进入缓冲区的过程中,但是还没进入,凭什么这样说,待会儿有图 5 中可以看到),触发了 IO 事件,epoll_wait 函数返回,从缓冲区中读了 3 个字符 'low',接下来,epoll_wait 又阻塞了。


这里写图片描述
图 4 输入字符 a

看图 5,后面又连续一个一个的输入 b, c, d, e,可以看到,最后一次输入 e 的时候,并不是打印 de,而是只打印了个 d,从这一点也可以证明,e 在进入缓冲区之前,IO 事件就已经触发了。


这里写图片描述
图5 依次输入 b, c, d, e

5. 总结

  • edge-triggered 和 level-triggered
  • 掌握设置 epoll 触发模式的方法
  • 掌握这两种触发模式的区别

好了,两个实验都已经演示了,我相信,你已经看到了区别。花了挺大篇幅,不知道你有没有耐心看下去,但是我写了一个多小时。

到了这里,我想你肯定会觉得 level-triggered 触发方式比 edge-triggered 方式要好,为什么,因为它方便啊!为什么还要折腾一个 edge-triggered 方式出来呢?其实吧,在一开始的时候,是没有 level-triggered 的,只有 edge-triggered,这也从侧面验证了 level-triggered 是比较先进的,但从另一个角度讲,它可能存在着缺点。

这里有一些关于两种触发模式的区别,老外写的,很有趣——传送门.

下一讲,我们就探讨如何使用 edge-triggered 方式写出更高效的代码。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值