欢迎光临
我们一直在努力

LT ET模式epoll读写——Linux结束

@bit::Shadow
✧(≖ ◡ ≖✿ 

目录

LT && ET 

1.LT(Level trigered)水平触发 设计模式

2.ET(Edge trigered)边缘触发 设计模式

为什么epoll_wait 后若不Accept,底层就会一直通知服务端呢?

ET模式与O_NONBLOCK的配合

🔗Epoll_ET_IO示例(千行代码)

ET模式原理

设置非阻塞模式

先明确前提

举例read/recv

举例send/write

为什么不可以通过循环来处理非阻塞?而是要设置fd为非阻塞?

ET与紧急指针的关联

多路转接的发送问题

认识One Thread One Loop(OTOL设计模式)


LT && ET 

LT  ET 是两种内核中的事件通知上层的机制。

1.LT(Level trigered)水平触发 设计模式

只要底层有报文,就会一直通知上层的机制。

若上层仅读取一部分数据,那么LT设计模式下底层仍然会一直通知。

2.ET(Edge trigered)边缘触发 设计模式

底层数据从无到有 / 从有到多才会通知上层。

若上层仅读取了一部分数据(未读取完全)就不读了,后来也没有(到达接受缓冲区的)新增数据那么底层就不会通知上层。

为什么epoll_wait 后若不Accept,底层就会一直通知服务端呢?

        答:对于一个监听listenfd来讲,“可读”的含义是:内核中已完成链接队列里存在等待被Accept的链接(可见wait的底层是先检验Accept信号)。(注意:epoll_wait返回后代表已经将资源拷贝了,从就绪队列内拿取过了)

ET模式与O_NONBLOCK的配合

ET强制要求必须一次性读完(接收的信息)

ET设置

_listenfd

sockfd

使用

O_NONBLOCK

非阻塞下设置以“信号”代替读取完毕的标识

🔗Epoll_ET_IO示例(千行代码)

高度解耦化、可复用性、健壮性、鲁棒性极好

1.EPOLLET fcntl() EPOLLOUT使用 2.重点理解继承、多态机制、分层结构。 3.回指指针_Rptr、handler_t的使用 4._connection的map结构

ET模式原理

设置非阻塞模式

void SetNoBlock(int fd)
{
int fl = fcntl (fd, F_GETFL);
if(fl < 0)
{
LOG (LogLevel::ERROR) << "NoBlock中fd获取失败";
return;
}
int ret = fcntl (fd, F_SETFL, fl | O_NONBLOCK);
if(ret < 0)
{
LOG (LogLevel::ERROR) << "NoBlock 设置失败";
return;
}
// LOG (LogLevel::DEBUG) << "设置NoBlock成功fd: " << fd;
}

非阻塞的(O_NONBLOCK)fd在非阻塞下,当读取fd内无数据时,不会再阻塞等待了而是会直接返回。

在非阻塞模式下,读和写遇到"暂时无法完成"时,行为高度对称:立即返回 -1,errno = EAGAIN / EWOULDBLOCK,不阻塞。下面分别展开。

先明确前提

  • fd 设置了 O_NONBLOCK。

  • 这两个场景都是"当前无法立即完成,但以后可能可以",属于暂时性的,不是真错误。

  • EAGAIN 和 EWOULDBLOCK 在 Linux 上是同一个值,可互换。

举例read/recv

ET + 非阻塞 read/recv时

返回值errno含义处理
n > 0 —— 读到了 n 字节(可能短读) 继续读
n == 0 —— 对端关闭(EOF) 关闭连接,退出循环
n == -1 EAGAIN/EWOULDBLOCK 数据读空了(非阻塞) 退出循环(这次边沿处理完)
n == -1 EINTR 被信号打断 continue 重试
n == -1 其他 真错误 关闭连接,退出

使用read/recv

while (true) {
//或者read
ssize_t n = read(fd, buf, sizeof(buf), 0);

if (n > 0) {
// 1. 读到数据,处理,继续读
// 注意:短读也要继续,不能因为 n < sizeof(buf) 就退出
}
else if (n == 0) {
// 2. 对端关闭(EOF)
// 关闭连接,退出
break;
}
else { // n == -1
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 3. 数据读空了 → 这次边沿处理完毕
break;
}
if (errno == EINTR) {
// 4. 被信号打断 → 重试
continue;
}
// 5. 真错误 → 关闭连接
break;
}
}

举例send/write
返回值errno含义处理
n > 0 —— 写入了 n 字节(可能部分写,n < len) 继续写剩余部分
n == 0 —— 几乎不会出现(见下方说明) 视情况,一般不当成功
n == -1 EAGAIN/EWOULDBLOCK 发送缓冲区满(非阻塞) 退出循环,注册 EPOLLOUT 等下次可写
n == -1 EINTR 信号打断 continue 重试
n == -1 EPIPE 对端已关闭(写端) 关闭连接,退出
n == -1 其他 真错误 关闭连接,退出

关键区分:EAGAIN(缓冲区满,稍后再写)、EPIPE(对端关闭)、其他错误,三者语义完全不同。

为什么没有 n == 0 作为"关闭":与 read 不同,send/write 返回 0 不代表对端关闭。len > 0 时正常情况不会返回 0;只有 len == 0(空发送)时才返回 0,那只是"发了 0 字节"。判断对端关闭要看 EPIPE 或 ECONNRESET。

while (还有数据要发) { // 例如 off < len
// 或者 send(fd, buf + off, len – off, 0)
ssize_t n = write(fd, buf + off, len – off);

if (n > 0) {
// 1. 写入了 n 字节,更新偏移,继续写剩余
// 注意:部分写(n < 剩余量)也要继续,不能直接当完成
off += n;
}
else if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 2. 发送缓冲区满 → 这次能写的写完了
// 把剩余数据存到应用层发送缓冲区,
// 注册 EPOLLOUT,等下次可写再继续
RegisterWriteEvent(fd);
break;
}
if (errno == EINTR) {
// 3. 被信号打断 → 重试
continue;
}
if (errno == EPIPE || errno == ECONNRESET) {
// 4. 对端已关闭 → 关闭连接
CloseConnection(fd);
break;
}
// 5. 真错误 → 关闭连接
CloseConnection(fd);
break;
}
else { // n == 0
// 只有 len == 0 时才会到这;正常发送不应出现
break;
}
}

为什么不可以通过循环来处理非阻塞?而是要设置fd为非阻塞?

每次读100若来2000第21次读取则会阻塞

ET与紧急指针的关联

ET模式在于约束程序员的行为与催促底层尽量/尽快读取接受缓冲区内的数据,并在网络层面优化了滑动窗口的大小。(PSH报头(报文)催促)

具体体现在EAGIN信号

多路转接的发送问题

接受缓冲区(不是fd的读事件)默认是不就绪的(因为在任何链接到来前没有内容),发送缓冲区(不是fd的写事件)默认是就绪的(是否 有接收空间 决定)。

1.因为内核中读事件默认是不就绪的,所以我们常设(来嵌入内核)。而写事件是需要按需设置的!!

//客户端
class Channel : public Connection
{
public:
Channel (int fd) : _sockfd (fd)
{
SetNoBlock (fd);
//常设
EvtSet (EPOLLIN | EPOLLET);
FdSet (fd);
}
~Channel ()
{
}
private:
int _sockfd; // 客户端fd

// 输入 输出缓冲区
std::string _inbuf;
std::string _otbuf;
};

2.写事件如何按需设置?/ 如何正确处理发送?

前提:对于“写”是系统面向系统的过程中应用层是难以干涉的!(所以我们使用负反馈来作为写的信号)

        答:直接发送!!!若发送缓冲区被写满,再把写事件设置为就绪关心状态。(交由服务端的不断循环的下一轮wait接收并处理)

// 发送
void Sender()
{
while(true)
{
//. . .
}/*send_while*/

// 理应发送完毕
// 1.满了 EPOLLOUT 2.正常
if(!_otbuf.empty())//没有发送完全
{
RptrGet ()->EnableReadWrite (_sockfd, true, true);//读写事件就绪
}
else
{
RptrGet ()->EnableReadWrite (_sockfd, true, false);
}
}

认识One Thread One Loop(OTOL设计模式)

“一个执行流,一个循环”

将Reactor改为多线程/多进程放在Reactor内的fd层面进行并发就会导致颗粒度大的情况。但是如果我们采用OTOL模式即在Reactor层进行并发管理的话,fd就会被安全、均衡地派发到各个线程/进程内。

One Thread One Loop(单线程单事件循环) 加餐




感谢支持,长期连载 欢迎关注

赞(0)
未经允许不得转载:171主机测评 » LT ET模式epoll读写——Linux结束
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址