上一篇文章非常详细的讲解了两种触发模式。那么,到底是使用水平触发模式好呢,还是使用边沿触发模式好呢?
1. 边沿触发 + 非阻塞
再讨论这个问题前,我们还有一个问题没解决,就是上一篇文章中使用边沿模式还遗留了一个问题,即因为 read 的接收缓冲区太小(只有 4 字节),这导致每次缓冲区的数据读取不完,从而在下次数据到来前,即使缓冲区还有剩余数据未读取,epoll_wait 函数也会阻塞。
解决此问题的方法就是使用 while 循环反复从缓冲区读数据,直到全部读完为止,但是这容易发生危险!因为我们不知道什么时候缓冲区数据什么时候读完,一旦真的读完了,read 就会发生阻塞。所以,这里应该改用 Non-blocking,即非阻塞的方式去 read,如果 read 返回值 < 0 同时 errno = EAGAIN 或 errno = EWOULDBLOCK 了,就说明缓冲区数据读完了,此时应该正常退出读循环。
所以,上一篇文章的代码只需要修改两个地方。为了以示区别,修改后的程序命名为 epoll_et_nio.c.
- 修改以非阻塞的方式打开管道
fds[1] = open("a.fifo", O_RDONLY | O_NONBLOCK);
printf("open pipe: fd = %d\n", fds[1]);
fds[2] = open("b.fifo", O_RDONLY | O_NONBLOCK);
- 修改 process 函数,以循环方式 read
int process(char* prompt, int fd) {
int n;
char buf[4];
char line[64];
sprintf(line, "%s say: ", prompt);
// 开始循环 read
while(1) {
n = read(fd, buf, 3);
if (n < 0) {
// 如果 errno 的值是 EAGAIN 或 EWOULDBLOCK,说明缓冲区数据读完了。
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
PERR("read");
}
else if (n == 0) {
// 对端关闭
sprintf(line, "%s closed\n", prompt);
puts(line);
return 0;
}
else if (n > 0) {
buf[n] = 0;
// 把从 read 读到的内容串接到 line 后面。
strcat(line, buf);
}
}
puts(line);
return n;
}
接下来就可以编译运行了。
图1 运行结果
2. 两种触发模式的优缺点
假设每次 read 都不能一次性缓冲区数据读完。
首先从程序运行的角度上看,水平模式+阻塞读,只要缓冲区还有数据,每次都会触发 epoll_wait 函数返回。
边沿模式+非阻塞读,只触发一次 epoll_wait 返回,然后 read 循环读。
那么效率就体现在
while { epoll_wait + read }
epoll_wait + while {read}
个人不对这两种谁快做讨论,因为目前还没有谁证明过后者比前者快。
另一方面,对于水平模式来讲,如果监听了 EPOLLOUT 事件,可能会导致每次都会触发该事件,而边沿模式则不会。所以水平模式下总触发 EPOLLOUT 事件感觉就是在浪费 CPU.
有很多大名鼎鼎的网络库或框架,都使用了水平触发模式,而 Nginx 使用了边沿触发模式。
所有这两种方式各有利弊,如果你想了解更多,请自行谷歌,这里还有知乎上的一篇文章是关于对这两种触发模式的讨论——传送门.
3. 总结
- 掌握边沿触发+非阻塞 IO