epoll 电平触发、边沿触发

看视频里有一句话有疑问,所以看了下电平触发和边沿触发。这句话是:“在复用IO里必须使用阻塞模式”这句话是错误的

epoll 对文件描述符有两种操作模型:电平触发(LT)、边沿触发(ET)。select 和 poll 模型只有电平触发。

电平触发:也就是只有高电平(1)或低电平(0)时才触发通知。

边沿触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知。

1571123733(1).png

由于上升沿和下降沿是电压发生变化而产生的,所以可以理解为边沿触发只有发生变化才会触发

而高电平低电平是可以一直保持的,可以看到高电平②是比高电平①要长的,是因为IIC传输数据的时候是两条线,有一条时间线,所以在一段时间内可以一直高电平或者低电平也没事。所以电平触发会一直通知,直到状态发生改变。由高电平转到低电平,由有数据变为无数据,否则会一直提示即使缓冲区里面的数据没有读完也是会提示你继续读取。

这里只是以电平的转换过程来理解LT和ET模式,并不是说功能的实现是高低电平,只是一种理解方式或者说是实现的原理上差不多。

电平触发指的是:当存在一个读写事件的时候,epoll会发送消息,也就是会从睡眠状态中醒来,然后继续走代码,如果没有事件发生,会一直堵塞,下面的代码不会触发。如果这个事件到来,比如读事件到来后,可以暂时不做处理,当下次循环的时候,会继续触发这个事件,可以在读取。直到内存中的数据被你读完。这个读的事件在下次循环的时候才不会触发。否则会一直触发。


边沿触发和电平触发恰恰相反,如果你这次事件不处理,那么下次这个事件不会继续通知。所以在性能方面边沿触发要更好一些,因为从睡眠醒来的次数比较少。边缘触发注意的是:如果你这次事件读取的内容没有读取完成,还剩余一部分,那么下次不会触发读事件,但是如果有新的数据写入到文件描述符,那么读事件会再次触发。


水平触发(原文)

1. 对于读操作

只要缓冲内容不为空,LT模式返回读就绪。

2. 对于写操作

只要缓冲区还不满,LT模式会返回写就绪。

边缘触发

1. 对于读操作

(1)当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。

(2)当有新数据到达时,即缓冲区中的待读数据变多的时候。

(3)当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。

2. 对于写操作

(1)当缓冲区由不可写变为可写时。

(2)当有旧数据被发送走,即缓冲区中的内容变少的时候。

(3)当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。

epool 更改LT、ET模式

epoll事件结构

    typedef union epoll_data {
        void    *ptr;
        int      fd;
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
    struct epoll_event {
        uint32_t     events;    /* Epoll events */
        epoll_data_t data;      /* User data variable */
    };

events可以包含两种:

LT:EPOLLIN(默认模式)

ET:EPOLLET

填充相应的方式即可更改触发模式。

代码解释

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
int setnonblocking( int fd )
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}
void addfd( int epollfd, int fd, bool enable_et )
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if( enable_et )
    {
        event.events |= EPOLLET;
    }
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}
void lt( epoll_event* events, int number, int epollfd, int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, false );
        }
        else if ( events[i].events & EPOLLIN )
        {
            printf( "event trigger once\n" );
            memset( buf, '\0', BUFFER_SIZE );
            int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
            if( ret <= 0 )
            {
                close( sockfd );
                continue;
            }
            printf( "get %d bytes of content: %s\n", ret, buf );
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}
void et( epoll_event* events, int number, int epollfd, int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, true );
        }
        else if ( events[i].events & EPOLLIN )
        {
            printf( "event trigger once\n" );
                // ET模式下,需要循环读取数据。
                //直到返回EAGAIN,因为下次不会通知。
            while( 1 ) 
            {
                memset( buf, '\0', BUFFER_SIZE );
                int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
                if( ret < 0 )
                {
                    if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) )
                    {
                        printf( "read later\n" );
                        break;
                    }
                    close( sockfd );
                    break;
                }
                else if( ret == 0 )
                {
                    close( sockfd );
                }
                else
                {
                    printf( "get %d bytes of content: %s\n", ret, buf );
                }
            }
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}
int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );
    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );
    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );
    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );
    ret = listen( listenfd, 5 );
    assert( ret != -1 );
    epoll_event events[ MAX_EVENT_NUMBER ];
    int epollfd = epoll_create( 5 );
    assert( epollfd != -1 );
    addfd( epollfd, listenfd, true );
    while( 1 )
    {
        int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
        if ( ret < 0 )
        {
            printf( "epoll failure\n" );
            break;
        }
    
        lt( events, ret, epollfd, listenfd );
        //et( events, ret, epollfd, listenfd );
    }
    close( listenfd );
    return 0;
}

关于触发模式的选择

1.对于监听的sockfd,最好使用电平触发模式,边沿触发模式会导致高并发情况下,有的客户端会连接不上,因为事件可能只会触发一次。如果非要使用边沿触发,网上有的方案是用while来循环accept()。

2.对于读写的connfd,电平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞,因为有的时候,电平触发已经发送事件,但是这个时候缓冲区的数据还未写,调用recv的时候有可能会出现堵塞状态,这是一种极端情况。

3.对于读写的connfd,边沿触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据,因为如果设置成堵塞,你第一次数据没有读写完,那么不会再次epoll事件,你的recv会一直堵塞下去,直到有新的事件产生,你的recv才会再次收到事件,然后读写数据。如果是非堵塞的话,那么可以用while(1)来一直读数据。而不必等待事件



epoll 电平触发、边沿触发


本站如无特别说明即为原创,转而告知:(https://iwonmo.com/archives/1624.html)

标签: linux, io, 复用

添加新评论