跳至主要內容

epoll的LT模式和ET模式

AkashiNeko原创LinuxIO

1. 电学中的LT和ET

水平触发(Level Trigger,LT)和边缘触发(Edge Trigger,ET)两个词来自电学中触发器的类型。

  • 水平触发(Level Trigger,LT):在输入信号保持在特定电平时触发的触发器。当输入信号的电平达到或保持在预设的水平时,触发器会被触发。

  • 边缘触发(Edge Trigger,ET):在输入信号发生边沿变化时触发的触发器。边缘可以是上升沿(上升边缘)或下降沿(下降边缘),具体取决于触发器的设计。

LT触发和ET触发的时机

epoll 中,可以将fd的就绪看作高电平,没有就绪看作低电平。那么LT模式的触发条件是持续处于高电平,ET模式的触发条件是一次电信号将低电平变为高电平。

2. epoll的LT触发

epoll 的默认工作模式是LT,这与 selectpoll 是相同的。

我们可以用下面的代码简单验证LT模式。

// Linux
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>

// C
#include <stdio.h>
#include <assert.h>

int main() {

    // 创建一个epoll
    int epfd = epoll_create(1);

    // 设置0号fd非阻塞
    const int STDIN_FD = 0;
    int flags = fcntl(STDIN_FD, F_GETFL);
    fcntl(STDIN_FD, F_SETFL, flags | O_NONBLOCK);

    // 将标准输入(fd = 0)加入关注列表
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = STDIN_FD;
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FD, &ev);

    // 读取
    while (1) {
        // 等待标准输入就绪,即用户输入
        assert(epoll_wait(epfd, &ev, 1, -1) == 1);

        // 每次只读4个字节
        char buf[5] = {};
        int ret = read(STDIN_FD, buf, 4);
        printf("读取数据:%s\n", buf);
    }

    return 0;
}

上面的代码中,用 epoll_wait 等待标准输入(fd = 0)上的读事件。当其上有读事件就绪时,使用 read() 读取,但是每次只读4个字节。我们输入 hello,world 作为测试。

LT模式下运行结果
LT模式下运行结果

可以发现,在输入 hello world 后,程序连续调用了多次 read() 进行fd的读取后,程序才被阻塞住。

这是因为,当我们第一次调用 read() 时,只从标准输入缓冲区里读取了4个字节而没有读取完,在下一次循环中调用 epoll_wait 时,由于 epoll 默认使用LT模式,epoll_wait 发现标准输入fd上仍然有数据未被读完,不阻塞立即返回。这样就造成了多次对输入缓冲区的fd进行读取的现象,直到读取完程序才被阻塞。

3. epoll的ET触发

要将 epoll 设为ET模式,只需要在 events 里加入 EPOLLET 标志位。

将fd设为ET触发

在上面代码的第22行加入 EPOLLET 标志位。

ev.events = EPOLLIN | EPOLLET;
完整代码
// Linux
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>

// C
#include <stdio.h>
#include <assert.h>

int main() {

    // 创建一个epoll
    int epfd = epoll_create(1);

    // 设置0号fd非阻塞
    const int STDIN_FD = 0;
    int flags = fcntl(STDIN_FD, F_GETFL);
    fcntl(STDIN_FD, F_SETFL, flags | O_NONBLOCK);

    // 将标准输入(fd = 0)加入关注列表
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET; // 加入EPOLLET标志位
    ev.data.fd = STDIN_FD;
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FD, &ev);

    // 读取
    while (1) {
        // 等待标准输入就绪,即用户输入
        assert(epoll_wait(epfd, &ev, 1, -1) == 1);

        // 每次只读4个字节
        char buf[5] = {};
        int ret = read(STDIN_FD, buf, 4);
        printf("读取数据:%s\n", buf);
    }

    return 0;
}

运行代码可以发现,我们输入了 hello world,程序读取了我们输入的前4个字节后立刻阻塞住了。

继续按下回车,即进行下一次输入后,hello world 剩余的部分才被陆续被读取了。

ET模式下的运行结果
ET模式下的运行结果

这就是ET模式下工作的 epoll,当我们输入 hello world 后,发生了边缘触发,即fd上的读事件有更新。当用户进行一次读取之后,虽然仍然有数据没读完,但是fd上并没有收到新的数据,所以ET模式下的 epoll 并不关注这个没读完的fd,直接进入阻塞状态。

当我们继续按下回车键进行输入,会使标准输入fd就绪,让 epoll 发生边缘触发。再使用 read() 进行读取时,由于上一次读取还有剩余的数据没有读完,所以会读到上一次输入的剩余数据。

4. LT和ET的对比

通过上面的例子,可以看出LT模式下工作的 epoll 会在fd有可读事件时,持续触发可读事件,而在ET模式下,只会触发一次。这意味着在ET模式下收到了可读事件时,由于只触发一次,所以必须在触发时一次性将缓冲区内的数据全部读完。

LT和ET的优缺点

  • 在LT模式下,可以自由决定何时读取数据以及每次读取的多少,但是可能会导致多次触发而影响效率
  • 在ET模式下,必须每次都要将缓冲区中的数据全部读完,其优点是触发次数少,性能相对更好。