【I/O多路复用】水平触发&边缘触发

一 原理概述

1 概念和原理

LT 模式(Level Triggered) LT 模式是 epoll 的默认模式。在 LT 模式下,当某个文件描述符就绪时,epoll_wait 函数会立即返回,通知应用程序有事件发生。即使应用程序没有立即处理完这些事件,下次调用 epoll_wait 时仍会再次返回这些就绪的文件描述符。

ET 模式(Edge Triggered) ET 模式要求应用程序在处理文件描述符的就绪事件时,必须确保将其处理完毕,否则 epoll_wait 将不会重复通知该文件描述符的就绪状态。ET 模式通过设置 epoll_event 结构体中的 EPOLLET 标志来启用。

2 区别

通知机制:

LT 模式:每当文件描述符就绪时,epoll_wait 将通知应用程序,即使应用程序没有处理完该事件。 ET 模式:只有在文件描述符状态发生变化时,epoll_wait 才会通知应用程序。如果应用程序没有处理完事件,将不会重复通知该文件描述符的就绪状态。

效率:

LT 模式:由于每次文件描述符就绪时都会通知应用程序,因此可能会引起频繁的上下文切换,影响效率。 ET 模式:只在状态变化时通知应用程序,可以减少不必要的上下文切换,提高效率,特别适合处理大量事件和高并发的场景。

3 适用场景

LT 模式适用场景:

对实时性要求不是非常高的应用,例如普通的网络服务器或者需要周期性处理数据的情况。 适合处理一般的数据读取、写入等操作。

ET 模式适用场景:

对事件响应速度要求较高的应用,例如高性能网络服务器,需要快速处理大量连接或数据的情况。 适合处理大数据流、高并发请求等场景,可以减少因为频繁通知而引起的性能开销。

原文链接:epoll水平触发(Level Triggered)和边缘触发(Edge Triggered)详解_epoll水平和边缘触发-CSDN博客

  • select和poll采用水平触发。

  • epoll有水平触发和边缘触发两种机制。

下面两句话是精髓。

水平触发

  • 读事件:如果epol_wait触发了读事件,表示有数据可读,如果程序没有把数据读完,再次调用epoll_wait的时候,将立即再次触发读事件。

  • 写事件:如果发送缓冲区没有满,表示可以写入数据,只要缓冲区没有被写满,再次调用epoll_wait的时候,将立即再次触发写事件。

边缘触发

  • 读事件:epoll_wait触发读事件后,不管程序有没有处理读事件epoll_wait都不会再触发读事件,只有当新的数据到达时,才再次触发读事件。

  • 写事件: epoll_wait触发写事件之后,如果发送缓冲区仍可以写(发送缓冲区没有满),epoll_wait不会再次触发写事件,只有当发送缓冲区由 满 变成 不满 时,才再次触发写事件。

二 边缘触发的监听模式

-------------------------------------------- 监听客户端的边缘触发模式 -----------------------------------------------

示例:

        采用边缘触发后,如果连接队列中有多个练上来的客户端,epoll只通知一次,程序只处理了一个客户端,如果一共300个客户端,epoll只通知了295个,因为还有5个客户端在连接队列中没有被处理,会有几个连接缺失了。为了解决这个问题,处理连接上来的客户端的时候需要加while循环。

如果使用了循环,那么🔴accept一定要用非阻塞的,不然会一直阻塞在accept这里。

🔴如果监听的socket是边缘触发的模式,那么一定要采用非阻塞的socket,处理事件的时候一定要用循环!

修改后代码为:

tcpepoll2.cpp

setnonblocking(listensock); //设置为非阻塞的,因为边缘触发使用了while。
// 如果发生事件的是listensock,表示有新的客户端连上来。
if (evs[ii].data.fd==listensock)
{
    while(true)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int clientsock = accept(listensock,(struct sockaddr*)&client,&len);

        if((clientsock<0)&&(errno==EAGAIN)) break;	//这一句跟上面注释的代码一样,出循环条件。

        printf ("accept client(socket=%d) ok.\n",clientsock);
// static int ii=0;
// printf("这是第%d个连接。\n",++ii);
        // 为新客户端准备读事件,并添加到epoll中。
        ev.data.fd=clientsock;
        ev.events=EPOLLIN;
        epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
    }
}

三 边缘触发的读事件 

---------------------------------------- 下面是边缘触发模式的读事件处理方式 ------------------------------------

思路:监听到客户端后:①设置非阻塞②设置边缘触发。后面对客户端发来的报文处理的时候:使用循环读取接收缓冲区中的内容。

tcpepoll2.cpp

// 如果发生事件的是listensock,表示有新的客户端连上来。
if (evs[ii].data.fd==listensock)
{
    while(true)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int clientsock = accept(listensock,(struct sockaddr*)&client,&len);

        if((clientsock<0)&&(errno==EAGAIN)) break;

        printf ("accept client(socket=%d) ok.\n",clientsock);
// static int ii=0;
// printf("这是第%d个连接。\n",++ii);
        // 为新客户端准备读事件,并添加到epoll中。
        setnonblocking(clientsock); //客户端的socket设置为非阻塞。
        ev.data.fd=clientsock;
        // ev.events=EPOLLIN;      // LT-水平触发。
        ev.events=EPOLLIN|EPOLLET;  // ET-边缘触发。
        epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
    }
}
else
{
    // 如果是客户端连接的socke有事件,表示有报文发过来或者连接已断开。
    char buffer[1024]; // 存放从客户端读取的数据。
    memset(buffer,0,sizeof(buffer));
    int readn;
    char *ptr = buffer;
    while(true)
    {
        if((readn=recv(evs[ii].data.fd,ptr,5,0))<=0)
        {
            // 重点:读取不到数据的时候返回-1;客户端断开的时候读取到0!别搞反了。
            if((readn<0)&&(errno==EAGAIN))
            {   //如果数据被读取完了,把接收到的报文内容原封不动的发回去。
                send(evs[ii].data.fd,buffer,strlen(buffer),0);
                printf("recv(eventfd=%d):%s\n",evs[ii].data.fd,buffer);
            }
            else    // 客户端断开的时候读取到0!别搞反了。
            {
                // 如果客户端的连接已断开。
                printf("client(eventfd=%d) disconnected.\n",evs[ii].data.fd);
                close(evs[ii].data.fd);            // 关闭客户端的socket
            }

            break;  //跳出循环
        }
        else
            ptr=ptr+readn;      //buffer的位置指针后移。
    }
}

对于读事件:

水平触发的代码写起来简单一些,可以采用阻塞的socket;边缘触发必须使用非阻塞的socket,代码写起来麻烦一些。 注意 水平触发的代码不能用于边缘触发,但是边缘触发的代码可以用于水平触发???是的。意思就是边缘触发的程序逻辑,可以直接将socket写为水平触发,程序逻辑是正确的。反之就不可以。

四 边缘触发的写事件

---------------------------------------- 下面是边缘触发模式的写事件处理方式 ------------------------------------

边缘触发的写事件的验证思路:修改增加epoll德socket事件为EPOLLOUT|EPOLLET,然后在触发写事件后循环发送1kw个报文,客户端接收一个报文后usleep(1000),1ms,那么服务端发送缓冲区会填满。发现填满后阻塞了0.1s后又开始写,说明:只有当发送缓冲区由 满 变成 不满 时,才再次触发写事件

// 如果发生事件的是listensock,表示有新的客户端连上来。
if (evs[ii].data.fd==listensock)
{
    while(true)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int clientsock = accept(listensock,(struct sockaddr*)&client,&len);

        if((clientsock<0)&&(errno==EAGAIN)) break;

        printf ("accept client(socket=%d) ok.\n",clientsock);
// static int ii=0;
// printf("这是第%d个连接。\n",++ii);
        // 为新客户端准备读事件,并添加到epoll中。
        setnonblocking(clientsock); //客户端的socket设置为非阻塞。
        ev.data.fd=clientsock;
        // ev.events=EPOLLOUT;      // LT-水平触发。
        ev.events=EPOLLIOUT|EPOLLET;  // ET-边缘触发。
        epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
    }
}
else
{
    printf("触发了写事件!\n");
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值