epoll是实现I/O多路复用的一种方法,为了深入了解epoll的原理,我们先来看下epoll水平触发(level trigger,LT,LT为epoll的默认工作模式)与边缘触发(edge trigger,ET)两种工作模式。
使用脉冲信号来解释LT和ET可能更加贴切。Level是指信号只需要处于水平,就一直会触发;而edge则是指信号为上升沿或者下降沿时触发。说得还有点玄乎,我们以生活中的一个例子来类比LT和ET是如何确定读操作是否就绪的。
水平触发
儿子:妈妈,我收到了500元的压岁钱。
妈妈:嗯,省着点花。
儿子:妈妈,我今天花了200元买了个变形金刚。
妈妈:以后不要乱花钱。
儿子:妈妈,我今天买了好多好吃的,还剩下100元。
妈妈:用完了这些钱,我可不会再给你钱了。
儿子:妈妈,那100元我没花,我攒起来了
妈妈:这才是明智的做法!
儿子:妈妈,那100元我还没花,我还有钱的。
妈妈:嗯,继续保持。
儿子:妈妈,我还有100元钱。
妈妈:…
接下来的情形就是没完没了了:只要儿子一直有钱,他就一直会向他的妈妈汇报。LT模式下,只要内核缓冲区中还有未读数据,就会一直返回描述符的就绪状态,即不断地唤醒应用进程。在上面的例子中,儿子是缓冲区,钱是数据,妈妈则是应用进程了解儿子的压岁钱状况(读操作)。
边缘触发
儿子:妈妈,我收到了500元的压岁钱。
妈妈:嗯,省着点花。
(儿子使用压岁钱购买了变形金刚和零食。)
儿子:
妈妈:儿子你倒是说话啊?压岁钱呢?
这个就是ET模式,儿子只在第一次收到压岁钱时通知妈妈,接下来儿子怎么把压岁钱花掉并没有通知妈妈。即儿子从没钱变成有钱,需要通知妈妈,接下来钱变少了,则不会再通知妈妈了。在ET模式下, 缓冲区从不可读变成可读,会唤醒应用进程,缓冲区数据变少的情况,则不会再唤醒应用进程。
我们再详细说明LT和ET两种模式下对读写操作是否就绪的判断。
水平触发
1. 对于读操作
只要缓冲内容不为空,LT模式返回读就绪。
2. 对于写操作
只要缓冲区还不满,LT模式会返回写就绪。
边缘触发
- 对于读操作
(1)当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。
(2)当有新数据到达时,即缓冲区中的待读数据变多的时候。
(3)当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。
- 对于写操作
(1)当缓冲区由不可写变为可写时。
(2)当有旧数据被发送走,即缓冲区中的内容变少的时候。
(3)当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。
实验
实验1
实验1对标准输入文件描述符使用ET模式进行监听。当我们输入一组字符并接下回车时,屏幕中会输出”hello world”。
#include <unistd.h>
#include <stdio.h>
#include <sys/epoll.h>
int main()
{
int epfd, nfds;
struct epoll_event event, events[5];
epfd = epoll_create(1);
event.data.fd = STDIN_FILENO;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);
while (1) {
nfds = epoll_wait(epfd, events, 5, -1);
int i;
for (i = 0; i < nfds; ++i) {
if (events[i].data.fd == STDIN_FILENO) {
printf("hello world\n");
}
}
}
}
输出:
$ ./epoll1
a
hello world
abc
hello world
hello
hello world
ttt
hello world
当用户输入一组字符,这组字符被送入缓冲区,因为缓冲区由空变成不空,所以ET返回读就绪,输出”hello world”。
之后再次执行epoll_wait,但ET模式下只会通知应用进程一次,故导致epoll_wait阻塞。
如果用户再次输入一组字符,导致缓冲区内容增多,ET会再返回就绪,应用进程再次输出”hello world”。
如果将上面的代码中的event.events = EPOLLIN | EPOLLET;改成event.events = EPOLLIN;,即使用LT模式,则运行程序后,会一直输出hello world。
实验2
实验2对标准输入文件描述符使用LT模式进行监听。当我们输入一组字符并接下回车时,屏幕中会输出”hello world”。
#include <unistd.h>
#include <stdio.h>
#include <sys/epoll.h>
int main()
{
int epfd, nfds;
char buf[256];
struct epoll_event event, events[5];
epfd = epoll_create(1);
event.data.fd = STDIN_FILENO;
event.events = EPOLLIN; // LT是默认模式
epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);
while (1) {
nfds = epoll_wait(epfd, events, 5, -1);
int i;
for (i = 0; i < nfds; ++i) {
if (events[i].data.fd == STDIN_FILENO) {
read(STDIN_FILENO, buf, sizeof(buf));
printf("hello world\n");
}
}
}
}
./a.out
hello nihao
hello world
yes
hello world
ok
hello world
dfjlsfls
hello world
good
hello world
实验2中使用的是LT模式,则每次epoll_wait返回时我们都将缓冲区的数据读完,下次再调用epoll_wait时就会阻塞,直到下次再输入字符。
如果将上面的程序改为每次只读一个字符,那么每次输入多少个字符,则会在屏幕中输出多少个“hello world”。有意思吧。
#include <unistd.h>
#include <stdio.h>
#include <sys/epoll.h>
int main()
{
int epfd, nfds;
char buf[256];
struct epoll_event event, events[5];
epfd = epoll_create(1);
event.data.fd = STDIN_FILENO;
event.events = EPOLLIN; // LT是默认模式
epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);
while (1) {
nfds = epoll_wait(epfd, events, 5, -1);
int i;
for (i = 0; i < nfds; ++i) {
if (events[i].data.fd == STDIN_FILENO) {
read(STDIN_FILENO, buf, 2);
printf("hello world\n");
}
}
}
}
输出:
./a.out
hel
hello world
hello world
nihao wohao
hello world
hello world
hello world
hello world
hello world
hello world
yesi
hello world
hello world
hello world
ye
hello world
hello world
good
hello world
hello world
hello world
实验3
实验3对标准输入文件描述符使用ET模式进行监听。当我们输入任何输入并接下回车时,屏幕中会死循环输出”hello world”。
#include <unistd.h>
#include <stdio.h>
#include <sys/epoll.h>
int main()
{
int epfd, nfds;
struct epoll_event event, events[5];
epfd = epoll_create(1);
event.data.fd = STDIN_FILENO;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);
while (1) {
nfds = epoll_wait(epfd, events, 5, -1);
int i;
for (i = 0; i < nfds; ++i) {
if (events[i].data.fd == STDIN_FILENO) {
printf("hello world\n");
event.data.fd = STDIN_FILENO;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &event);
}
}
}
}
实验3使用ET模式,但是每次读就绪后都主动对描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件,由上面的描述我们可以知道,会再次触发读就绪,这样就导致程序出现死循环,不断地在屏幕中输出”hello world”。但是,如果我们将EPOLL_CTL_MOD 改为EPOLL_CTL_ADD,则程序的运行将不会出现死循环的情况。