Skip to content

面试官问 epoll 什么时候触发可读可写,你答得上来吗 ​

序言 ​

对于 epoll,很多人知道水平触发,边缘触发。 然而对于什么时候会触发可读,什么时候触发可写,似乎就没什么底了。

这篇文章,将水平触发、边缘触发两种模式下,所有会触发可读、可写的场景逐一进行分析。

读完后,面试官问你 epoll 可读、可写的触发场景时,你将了然于胸。

一、可读、可写的判断条件是什么? ​

不论是LT水平触发,还是ET边缘触发,可读和可写的触发条件都是一样的。唯一区别是 LT 水平方式,可读/可写事件会一直触发, 而 ET 边缘触发方式下,只会触发一次。

可读事件触发条件 ​

内核接收缓冲区中,默认情况下,只要收到1字节的数据,就说明可读。LT 水平触发方式下,将会一直触发可读, ET 边缘触发方式下,只会触发一次可读。

默认收到 1 字节就触发可读,这是可以配置的。通过 setsockopt,设置 SO_RCVLOWAT 选项进行调整。如果调整为256,则表示内核接收缓冲区,收到256字节才会触发可读。

默认收到1字节就触发可读事件,目的是为了通知应用层赶紧将数据取走。不然接收缓冲区满,会给对端发送零窗口,导致对端不再发送数据;

可读条件

可写事件触发条件。 ​

可写事件触发比较特殊,并不是内核发送缓冲区只要还有空间,就会触发写事件。

内核发送缓冲区,可用空间,大致还剩余一半以上,才认为是可写的。达不到一半以上,则是不可写的。

举个例子: 内核发送缓冲区大小为100k:

① 当前已经使用了40K, 剩余空间还有60k,大于一半,会触发写事件。

② 当前已经使用了60K, 剩余空间只有40k,不到一半, 不会触发写事件。

之所以要设置一个阈值,剩余空间达到一半才会触发可写事件,是为了攒够足够的数据,避免还剩几个字节就通知你可写,写不了多少就遇到EAGAIN写失败了,频繁的通知你,增加系统调用的开销。

一旦满足可写条件,LT 水平触发方式下,将会一直触发可写。 ET 边缘触发方式下,只会触发一次可写。

可写条件

内核接收缓冲区只要收到1字节就会触发可读; 内核发送缓冲区剩余可用空间在一半以上,才会触发可写。 记住这个条件,不论是LT 水平触发,还是 ET边缘触发,判断条件都是这个。

有了这个条件,我们接下来详细分析下,LT 和 ET 两种模式下,可读和可写的所有触发场景。

二、LT 水平触发方式下,可读场景 ​

内核接收缓冲区中,默认情况下,只要收到1字节的数据,就说明可读。 LT 水平触发方式下,会一直触发可读事件。

LT 水平方式,有 5 种场景会触发可读事件:

场景1: 只要接收缓冲区有 ≥1 字节未读,持续触发可读事件。 ​

只要内核接收缓冲区还有剩余1字节以上的数据,你不去读取,或者没有将缓冲区中的数据全部读取完成,就会持续触发可读事件。只有全部读完后,才停止触发可读事件。

举个例子,接收缓冲区大小为:100K:

时序步骤结果
T0缓冲区无数据,epoll_wait 阻塞🔇
T1对端发来 5K缓冲区 5K
T2epoll_wait 返回 EPOLLIN✅ 触发可读
T3你只 read 了 2K缓冲区剩 3K
T4epoll_wait 立即返回 EPOLLIN✅ 触发可读
T5你 read 了 3K缓冲区 0
T6epoll_wait 阻塞🔇

水平触发 可读场景

场景2: 监听套接字,全连接队列有客户端连接,立马触发可读事件 ​

每个监听套接字,都有一个全连接队列,存放的是客户端三次握手完成后的连接。调用accept时会从全连接队列中取出一个连接。

一旦全连接队列中收到一个客户端连接,就会触发可读事件。你不去读取,或者没有将全连接队列中的数据全部读取完成,就会持续触发可读事件。只有全部读完后,才停止触发可读事件

举例:

时序步骤结果
T0accept 队列空,epoll_wait 阻塞🔇
T13 个客户端同时连队列 3 个
T2epoll_wait 返回 EPOLLIN✅ 触发可读
T3你只 accept 了 1 个队列剩 2
T4epoll_wait 立即返回 EPOLLIN✅ 触发可读
T5accept 剩下 2 个队列空
T6epoll_wait 阻塞🔇

处理客户端连接

场景3:对端close, 收到FIN包 ​

对端调用close,正常的执行四次挥手关闭连接。接收端会收到FIN包,POLLIN可读事件会触发,执行read 调用时会返回0,说明对端关闭了。

举例:

时序步骤结果
T0对端 close(),发来 FIN
T1epoll_wait → EPOLLIN|EPOLLRDHUP✅触发读事件
T2read() → 0

如果应用层不去read,则一直会触发可读事件,导致cpu 空转。

时序步骤结果
T0对端 close(),发来 FIN
T1epoll_wait → EPOLLIN|EPOLLRDHUP✅触发读事件
T2epoll_wait → EPOLLIN|EPOLLRDHUP✅触发读事件
T3epoll_wait → EPOLLIN|EPOLLRDHUP✅触发读事件

场景4: 对端异常关闭,收到RST包 ​

对端异常关闭时,会发送RST包,执行的是异常关闭过程。接收端收到RST包后,直接会进入 CLOSE 状态。

收到RST后,也是会触发可读事件的,此时你 read 去读取,会返回失败,错误码为 ECONNRESET

我们来看下,不同TCP状态下,收到RST后的错误码是什么

TCP状态错误码
ESTABLISHED(已连接)ECONNRESET
SYN_SENT(正在连接)ECONNREFUSED
CLOSE_WAIT(已收到对端 FIN)EPIPE
TCP_CLOSE(已经死了)无

那我们如何感知是否收到 RST:

收到 RST,一定会打上EPOLLERR | EPOLLHUP这两个标记。因此我们在epoll_wait返回后,调用getsockopt获取错误码,从而知道是否收到了RST。

if (events & (EPOLLERR | EPOLLHUP)) 
{
    int err = 0;
    getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len);   

    if (conn->IsConnecting()) 
    {
        // 还在 connect 阶段
        switch (err) 
        {
            case ECONNREFUSED:
                // 对端服务没运行起来 → 快速重连是合理的
                FastReconnect(conn, 1000);
                break;
            case ETIMEDOUT:
                // 网络问题 → 过一段时间再重连
                SlowReconnect(conn);
                break;
        }
        // 通知业务层连接失败
        OnConnect(conn, false, err);      
    } 
    else 
    {
        // 已建立的连接出错
        // err == ECONNRESET → 对端异常终止
        // err == ETIMEDOUT  → 重传超时,对端掉线
        ClientDisconnect(conn, err);
    }
    continue;
}

如果收到RST后,还执行read调用,在将内核接收缓冲区数据全部读取完成后,read会失败,返回-1,错误码为ECONNRESET;

执行 write() 也会失败,返回-1,错误码为 ECONNRESET;再继续write,之后返回 EPIPE 错误码

场景5: 本端 shutdown(SHUT_RD) 关闭读 ​

本端 shutdown(SHUT_RD) 关闭读,则立马会触发可读事件。此时你read照样会返回0。

shutdown(SHUT_RD) 只改了一件事:把"缓冲区空"时的返回值从 EAGAIN 变成了 0;内核接收缓冲的数据一个字节都不会丢。

如果你不read,会一直触发可读事件,直到你read为止。

对端close后,会收到FIN包,read会返回0。本端关闭读,read也返回0,那如何区分是哪种场景。

这需要我们自己区分,本端 shutdown(SHUT_RD)时,我们是知道的,可以打一个标记。然后read返回0时,根据这个标记,就可以知道是本地主动关闭读,还是收到对端的FIN包。

bool m_bLocalShutdownRd = false;

// 调用处
void ShutdownRead()
{
    m_bLocalShutdownRd = true;    
    shutdown(fd, SHUT_RD);
}

// 读事件处理函数,read返回0时
if (nRecv == 0)
{
    if (m_bLocalShutdownRd)
    {
        //本端主动shutdown read
    }
    else
    {
        //对端断开
        OnClientDisconnect();  
    }
}

LT 水平触发方式下, 可读事件触发场景总结:

#场景报吗持续性
1接收缓冲区有 ≥1 字节未读✓持续报,直到读干净
2只读了一部分,还有剩余✓持续报
3缓冲区读空了✗安静
4对端 close(),收到 FIN✓持续报,除非read。read 返回 0
5listen socket,accept 队列非空✓持续报,直到 accept 干净
6对端 RST✓持续报,带 EPOLLERR|EPOLLHUP
7本端 shutdown(SHUT_RD)✓持续报,除非read。read 返回 0
8connect() 未完成(SYN_SENT)✗发起连接端,触发的是写而不是读事件

三、LT 水平触发方式下,可写场景 ​

内核发送缓冲区,可用空间,大致还剩余一半以上,才认为是可写的。达不到一半以上,则是不可写的。

场景1: EPOLL_CTL_ADD 将 fd 注册到epoll时 ​

连接刚建立,整个连接为空闲连接。空闲的 TCP 连接,发送缓冲区几乎总是空的,立即会触发写事件。但前提是,调用者注册了EPOLLOUT,此时就一定会触发写事件

场景2: EPOLL_CTL_MOD 修改 注册到epoll的事件 ​

MOD 修改注册到epoll中的事件,会主动查下当前是否可写。也就是看内核发送缓冲区剩余空间,是否还有一半以上空闲空间。如果有,则可写,会持续触发可写事件。

如果不满足,则此时不会触发可写事件。

EPOLL_CTL_MOD 触发可写

例如: 发送缓冲区大小为100k, mod 不触发写事件。

时序步骤结果
T0发送缓冲区已有数据60k发现只有40k剩余
T1EPOLL_CTL_MOD 修改epoll事件✗ 不触发写事件

发送缓冲区大小为100k, mod 触发写事件场景。

时序步骤结果
T0发送缓冲区已有数据40k
T1EPOLL_CTL_MOD 修改epoll事件发现剩余60k
T2epoll_wait 返回 EPOLLOUT✅ 触发写事件
T3你 write 了 60K缓冲区剩 0K

可以看出,对于写事件,并不是发送缓冲区有空间,就一定会触发写事件。

场景3: 内核发送缓冲区还剩余一半以上的空间 ​

只要内核的发送缓冲,剩余空间还有一半以上的空间,就会触发写事件,且一直会触发。达不到一半以上就不会触发写事件

水平触发,可写场景

例如: 发送缓冲区大小为100k, 只要内核发送缓冲区剩余一半以上空间,持续触发

时序步骤结果
T0发送缓冲区写入10k数据剩余90k,剩余一半以上
T1epoll_wait 返回 EPOLLOUT✅ 触发写事件
T2write 在写入10k数据剩余80k,剩余一半以上
T3epoll_wait 返回 EPOLLOUT✅ 触发写事件

例如: 发送缓冲区大小为100k, 内核发送缓冲区剩余空间不足一半空间,不触发

时序步骤结果
T0发送缓冲区写入40k数据剩余60k,剩余一半以上
T1epoll_wait 返回 EPOLLOUT✅ 触发写事件
T2继续写入20k数据,剩余40k,不足一半
T3epoll_wait 不会返回 EPOLLOUT✗ 不触发写事件

例如: 发送缓冲区大小为100k, 内核发送缓冲区满后,剩余空间降到一半以上

时序步骤结果
T0发送缓冲区写入100k数据,剩余0k✗ 不触发写事件
T1继续 write 返回 EAGAIN
T2收到 ack,腾出40k, 剩余40k,不足一半✗ 不触发写事件
T3收到 ack,腾出20k, 剩余60k,达到一半
T3epoll_wait 返回 EPOLLOUT✅ 触发写事件

缓冲区满后,不会触发写事件。只有等对端确认后,收到对端的ack,腾出一半以上的空闲空间,才会再次触发可写事件。

场景4: 非阻塞 connect() 完成 ​

客户端非阻塞方式调用connect,连接服务器时。如何知道是否连接成功呢?

答案是监听可写事件,在可写事件发生时判断是否连接成功。

int retval = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen)
if( retval < 0)
{
  //建立连接失败
}
else
{
  //建立连接成功
}

可写事件中,调用getsockopt判断返回值,小于0说明建连失败。为0说明建立连接成功

不管连接成功还是失败,都是会触发可写事件。

connect 可写

场景5: 本端 shutdown(SHUT_WR) 关闭写 ​

本端 shutdown(SHUT_WR) 关闭写,会给对端发送FIN包,关闭写方向。此时立马会触发可写事件,且持续触发写事件。

关闭写,内核无条件报 EPOLLOUT,完全不看缓冲区水位。这是为了让后续的 write() 能立刻返回 EPIPE 失败,而不是傻等

T0   shutdown(fd, SHUT_WR)
T1   epoll_wait 返回 EPOLLOUT
T2   write发数据, 立即失败,触发EPIPE错误
T3   epoll_wait 继续返回 EPOLLOUT, 持续可写

因此,shutdown(SHUT_WR) 关闭写后,优雅关闭时必须同时 EPOLL_CTL_MOD 摘掉 EPOLLOUT。不然会持续报可写

shutdown(SHUT_WR)
EPOLL_CTL_MOD 移除EPOLLOUT

场景6: 对端异常关闭,收到RST包 ​

对端异常关闭时,会发送RST包,执行的是异常关闭过程。接收端收到RST包后,直接会进入CLOSE状态。

收到RST后,之前介绍会触发可读事件,其实也还会触发可写事件。会返回失败,错误码为ECONNRESET


对LT水平触发方式下,可写事件触发场景做个总结:

#场景剩余空间报吗特点
1连接刚建立、刚 EPOLL_CTL_ADD 进 epoll0/100K✓持续报,每轮都报
2数据全发完,缓冲区排空0/100K✓持续报
3已用不到一半30/100K✓持续报
4非阻塞 connect() 完成0/100K✓持续报
5connect() 进行中(SYN_SENT)—✗握手完成前不报
6已用超过一半70/100K✗
7缓冲区写满100/100K✗安静,不占 CPU
8满了之后 ACK 降到一半以下100K→40K✓恢复持续报
9EPOLL_CTL_MOD 时当前可写> 50K✓持续报
10对端 RST / 出错任意✓带 EPOLLERR
11本端 shutdown(SHUT_WR)任意✓⚠️ 无条件报

四、ET 边缘触发方式下,可读场景 ​

内核接收缓冲区中,默认情况下,只要收到1字节的数据,就说明可读, ET 边缘触发方式下,只会触发一次可读事件。

因此默认情况下,每一个到达的数据包都产生一次边沿,触发一次可读事件

ET 边缘方式,有 7 种场景会触发可读事件:

场景1:每收到一个数据包都产生一次边沿,触发一次可读事件 ​

只要接收到数据,就会触发一次可读,且只会触发一次。

read时需要一次性全部读取完。

如果一次性不读取完全部数据,剩余部分不会再触发可读事件。这和LT是不一样的,LT是会一直触发。

剩余部分需要等下一次收到数据时,再产生一个边缘,才会触发可读事件

边缘触发 可读场景

时序步骤结果
T0接收缓冲区 0
T1对端发来 5K缓冲区 5K
T2epoll_wait → EPOLLIN✅ 触发读事件
T3你只 read 了 2K 就返回缓冲区剩 3K
T4epoll_wait 阻塞🔇 那 3K 还在缓冲区
T5对端又发来 4K缓冲区 3K + 4K = 7K
T6epoll_wait → EPOLLIN✅ 触发读事件
T7这次循环读到 EAGAIN,读完 7k

T4 到 T6 之间那段时间,3K 数据还在内核的接收缓冲区里。需要等对端再次发新数据,产生新边缘,才会触发可读。

所以 ET 漏读的表现往往不是"彻底不工作",而是延迟诡异地抖动。低负载时卡,高负载时反而正常(因为新包不断制造边沿)。

场景2:每收到一个客户端连接,触发一次可读事件 ​

每个监听套接字,都有一个全连接队列,存放的是客户端三次握手后的连接。调用accept时会从全连接队列中取出一个连接。

一旦全连接队列中收到一个客户端连接,就会触发可读事件。收到一个连接就触发一次读事件。

因此,需要你循环 accept 将连接全部取出。

如果没有将全连接队列中的数据全部读取完成,就不会再触发可读事件。

剩余部分需要等下一次收到新客户端连接时,再产生一个边缘,才会触发可读事件

时序步骤结果
T0accept 全连接队列空
T13 个客户端几乎同时连上队列 3 个
T2epoll_wait 返回一次 EPOLLIN✅ 触发读事件
T3你只 accept(fd) 一次队列剩 2
T4epoll_wait 阻塞🔇 2 个客户端被阻塞
T5第 4 个客户端连上新边沿
T6epoll_wait → EPOLLIN✅ 触发读事件
T7accept 3 个客户端

表现:客户端 A 连上后一直没反应,直到客户端 B 也来连,A 才突然活了。

边缘触发 客户端连接

场景3:对端close, 收到FIN包 ​

对端调用close,正常的执行四次挥手关闭连接,接收端会收到FIN包。此时POLLIN可读事件会触发。

执行read调用时会收到0,说明对端关闭了。

如果应用层不去read,则不会再触发可读事件。读事件只会触发一次

时序步骤结果
T0对端 close(),发来 FIN
T1epoll_wait → EPOLLIN|EPOLLRDHUP✅触发读事件
T2read() → 0

读事件触发,不去read,则不会再触发读事件了。而LT 是每轮都触发读事件,每轮 read 返回 0 → CPU 100%

时序步骤结果
T0对端 close(),发来 FIN
T1epoll_wait → EPOLLIN|EPOLLRDHUP✅触发读事件
T2不去read,不会再触发,epoll_wait 也照常阻塞🔇

场景4:对端异常关闭,收到RST包。 ​

对端异常关闭时,会发送RST包,执行的是异常关闭过程。接收端收到RST包后,直接会进入CLOSE状态。

收到RST后,也是会触发可读事件的,会返回EPOLLIN|EPOLLERR|EPOLLHUP 几种 epoll mask。

此时你read去读取,会返回失败,错误码为ECONNRESET

场景5: EPOLL_CTL_MOD 修改 注册到epoll的事件 ​

MOD 修改注册到epoll中的事件,会主动检查当前是否可读。也就是看内核接收缓冲区是否有数据,只要还有一字节以上的数据,就是可读,触发读事件。相当于人为制造了一次边缘

如果不满足,则此时不会触发可读事件。

时序步骤结果
T0收到了40k数据
T1epoll_wait → EPOLLIN✅ 触发读事件
T2你只 read 了 20K 就返回缓冲区剩 20K
T3剩余20k,不再触发读事件
T4epoll_ctl(MOD)未读 20K ≥ 1 → 可读
T5epoll_wait 立即返回 EPOLLIN✅ 触发读事件
T6read 读取剩余20K

因为接收侧的门槛低到 1 字节,只要还有未读数据,EPOLL_CTL_MOD 就必然触发可读事件。

EPOLL_CTL_MOD 触发可读

但别把 MOD 当常规手段。它能救场,不代表该这么用:

① 白白多一次系统调用

epoll_ctl 不是免费的。每处理一次事件多一次 MOD,高并发下是实打实的开销。

② 治标不治本

你 MOD 一次读 20K,剩 20K;再 MOD 一次读 20K,等于把 ET 退化成了一个每轮还要额外调一次 epoll_ctl 的、比 LT 更慢的 LT。 ET 的全部性能优势就是这么被抵消掉的。

③ 掩盖真问题

需要靠 MOD 才能把数据读完,说明读循环本身写错了。

场景6: EPOLL_CTL_ADD 将 fd 注册到epoll时 ​

连接刚建立,使用 EPOLL_CTL_ADD 将 fd 注册到 epoll。如果此时内核接收缓冲区提前收到了客户端的数据,此时也是会立即触发可读事件。且只会触发一次。相当于人为制造了一次边缘

场景7: 本端 shutdown(SHUT_RD) 关闭读 ​

本端 shutdown(SHUT_RD) 关闭读,则立马会触发可读事件。此时你read照样会返回0。

如果你不read,就不会再触发可读事件

对端close后,会收到FIN包,read会返回0。本端关闭读,read也返回0,那如何区分是哪种场景。

这需要我们自己区分,本端 shutdown(SHUT_RD)时,我们是知道的,可以打一个标记。然后read返回0时,根据这个标记,就可以知道是本地主动关闭读还是收到对端的FIN包。


ET 边缘触发方式下,可读事件触发场景总结:

#边沿频率
1EPOLL_CTL_ADD 时缓冲区已有数据一次性
2EPOLL_CTL_MOD 且当前可读每次 MOD
3新数据到达收到一次触发一次
4新连接进入 accept 队列收到一次触发一次
5收到 FIN一次
6RST / 出错一次
7本端 shutdown(SHUT_RD)一次

五、ET 边缘触发方式下,可写场景 ​

ET边缘触发方式下,可写必须满足这两个条件:

  • 内核发送缓冲区,可用空间,大致还剩余一半以上,才认为是初步可写的。达不到一半以上,则是不可写的。

  • 必须先写满过一次(write 返回 EAGAIN),后续 ACK 释放空间到水位以上,才会给你一个 EPOLLOUT 边沿。

场景1: EPOLL_CTL_ADD 将 fd 注册到epoll时 ​

连接刚建立,整个连接为空闲连接。空闲的 TCP 连接,发送缓冲区几乎总是空的,立即会触发写事件。但前提是,调用者注册了EPOLLOUT,此时才一定会触发写事件。相当于制造了一次边缘

场景2: EPOLL_CTL_MOD 修改 注册到epoll的事件 ​

MOD 修改注册到epoll中的事件,会主动查下当前是否可写。也就是看内核发送缓冲区剩余空间,是否还有一半以上空闲空间。如果是,则可写,会触发一次可写事件,也只会触发一次。相当于制造了一次边缘

如果不满足,则此时不会触发可写事件。

EPOLL_CTL_MOD 制造边缘 触发可写

例如: 发送缓冲区大小为100k, mod 不触发写事件

时序步骤结果
T0发送缓冲区已有数据60k
T1EPOLL_CTL_MOD 修改epoll事件, 发现只有40k剩余❌ 不触发写事件

例如: 发送缓冲区大小为100k, mod 触发写事件

时序步骤结果
T0发送缓冲区已有数据40k
T1EPOLL_CTL_MOD 修改epoll事件发现剩余60k
T2epoll_wait 返回 EPOLLOUT✅ 触发写事件
T3你 write 了 60K缓冲区剩 0K

可以看出,对于写事件,并不是发送缓冲区有空间,就一定会触发写事件。

场景3: 内核发送缓冲区还剩余一半以上的空间 ​

只要内核的发送缓冲,剩余空间还有一半以上的空间,就会触发写事件,且只触发一次。

达不到一半以上就不会触发写事件

边缘触发写事件

例如: 发送缓冲区大小为100k, 只要内核发送缓冲区剩余一半以上空间,只触发一次

时序步骤结果
T0发送缓冲区写入10k数据剩余90k,,剩余一半以上
T1epoll_wait 返回 EPOLLOUT✅ 触发写事件
T2write 再写入10k数据,剩余80k,剩余一半以上❌ 不触发写事件
T3write 再写入10k数据,剩余70k,剩余一半以上❌ 不触发写事件

例如: 发送缓冲区大小为100k, 内核发送缓冲区剩余空间不足一半空间,不触发

时序步骤结果
T0发送缓冲区写入40k数据剩余60k,剩余一半以上
T1epoll_wait 返回 EPOLLOUT✅ 触发写事件
T2继续写入20k数据, 剩余40k,不足一半❌ 不触发写事件

例如: 发送缓冲区大小为100k, 内核发送缓冲区满后,剩余空间降到一半以上

时序步骤结果
T0发送缓冲区写入100k数据, 剩余0k❌ 不触发写事件
T1继续 write 返回 EAGAIN
T2收到 ack,腾出40k, 剩余40k,不足一半❌ 不触发写事件
T3收到 ack,腾出20k, 剩余60k,达到一半
T4epoll_wait 返回 EPOLLOUT✅ 触发写事件

还有一种场景,也是很多人有疑问的。如果发送缓冲区剩余空间从不到一半,到超过一半。如果没有产生过边缘,还是不会触发可写事件。

未产生边缘 不可写

例如: 发送缓冲区大小为100k,

时序步骤结果
T0发送缓冲区写入10k数据, 剩余90k❌ 不触发写事件
T1write 再写入50k数据,剩余40k,不足一半❌ 不触发写事件
T2收到 ack,腾出20k, 剩余60k,达到一半❌ 不触发写事件

T2 时刻,对端确认了,腾出了空间,剩余空间都在一半以上,还是不会触发可写事件。

这是因为,没满足这个条件:

必须先写满过一次(write 返回 EAGAIN),后续 ACK 释放空间到水位以上,才会给你一个 EPOLLOUT 边沿。

而这个场景下,自始至终内核发送缓冲区都是空闲的,没写满过,也就不会有边缘。

场景4: 非阻塞 connect() 完成 ​

客户端非阻塞方式调用connect,连接服务器时。如何知道是否连接成功呢? 答案是监听可写事件,在可写事件发生时判断是否连接成功。

int retval = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen)
if( retval < 0)
{
  //建立连接失败
}
else
{
  //建立连接成功
}

可写事件中,调用getsockopt判断返回值,小于0说明建连失败。为0说明建立连接成功

因此,不管连接成功还是失败,也是会触发可写事件。

connect 可写

场景5: 本端 shutdown(SHUT_WR) 关闭写 ​

本端 shutdown(SHUT_WR) 关闭写,会给对端发送FIN包,关闭写方向。此时立马会触发可写事件,且只触发一次写事件。

关闭写,内核无条件报 EPOLLOUT,完全不看缓冲区水位。这是为了让后续的 write() 能立刻返回 EPIPE 失败,而不是傻等

T0   shutdown(fd, SHUT_WR)
T1   epoll_wait 返回 EPOLLOUT
T2   write发数据, 立即失败,触发EPIPE错误

边缘触发方式下,调用shutdown(SHUT_WR) 只会触发一次可写事件。

场景6: 对端异常关闭,收到RST包 ​

对端异常关闭时,会发送RST包,执行的是异常关闭过程。接收端收到RST包后,直接会进入CLOSE状态。

收到RST后,之前介绍会触发可读事件,其实也还会触发可写事件。会返回失败,错误码为ECONNRESET


对ET边缘触发方式下,可写事件触发场景做个总结:

#边沿说明
1EPOLL_CTL_ADD 时当前就可写触发一次写事件
2EPOLL_CTL_MOD剩余一半空间,触发可写,人造边沿
3非阻塞 connect() 完成从"未连接不可写"翻转到"已连接可写"
4已用不到一半触发一次写
5写满后 ACK 回来腾出一半空间触发一次写
6对端 RST / 出错带 EPOLLERR 触发一次写事件
7对端close 收到FIN触发一次写
8本端 shutdown(SHUT_WR)无条件触发写事件

六、写在最后 ​

读完文章,我想你对 epoll LT 水平触发、ET 边缘触发种方式下,所有触发可读、可写的场景已经有所了解了。当你在看网络相关的源码时,也就能理解人家为什么那么写。

我尽量站在读者的角度把来龙去脉讲清楚了,但每个人理解不一样。有任何疑问,可以在评论区里讨论,也可以联系作者。

如何联系作者

  • yudongtc 这个博客每周更新一篇高并发干货,图解方式讲解,需要可以浏览器访问。

  • 关注高并发成长记,加入linux网络编程交流群,一起讨论。

基于 VitePress 构建