I/O多路复用之 epoll

Posted niliusha

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了I/O多路复用之 epoll相关的知识,希望对你有一定的参考价值。

epoll 系统调用


1. 内核事件表

epoll使用一系列函数来完成任务,把用户关心的文件描述符中的事件放到内核里的一个事件表中,因此不用像select、poll那样每次调用都要重复传入文件描述符集或事件表。epoll需要一个文件描述符来唯一标识该事件表,该文件描述符使用epoll_create函数创建:

    #include <sys/epoll.h>
    int epoll_create( int size );

size 标记事件表大小。该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

epoll_ctl函数用于操作epoll的内核事件表:

    #include <sys/epoll.h>
    int epoll_ctl( int epfd, int op, int fd, struct epoll_event * event )

fd参数是要操作的文件描述符,op参数指定操作类型(如下):

  • EPOLL_CTL_ADD,往事件表中注册fd上的事件
  • EPOLL_CTL_DEL,删除fd上的注册事件
  • EPOLL_CTL_MOD,修改fd上的注册事件

event参数指定被操作的事件,它是epoll_event结构类型指针。epoll_event的定义如下:

    struct epoll_event
    {
        __uint32_t events;  //epoll事件
        epoll_data_t data;  //用户数据
    }

其中events成员描述事件类型(如 EPOLLIN表示数据可读事件)。epoll_data_t 定义如下:

    typedef union epoll_data
    {
        void * ptr;
        int fd;
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;

epoll_data_t 是一个共用体,其4个成员使用最多的是fd,它指定事件所从属的目标文件描述符。各成员不能同时使用。


2. epoll_wait 函数

epoll_wait 函数在一段超时时间内等待一组文件描述符上的事件,其原型如下:

#include <sys/epoll.h>
int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout ) 

该函数成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。timeout参数为延时时间。maxevents参数指定最多监听多少个事件,它必须大于0。
epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd指定)中复制到它的第二个参数events指向的数组,该数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll那样既传入用户注册事件,又输出内核检测到的就绪事件。这极大提高了应用程序索引就绪文件描述符的效率。

    /* 如何索引 epoll 返回的就绪文件描述符 */
    
    int ret = epol_wait( epoll_fd, events, MAX_EVENT_NUMBER, -1 );
    // 仅遍历就绪的ret个文件描述符
    for ( int i = 0; i < ret; i++ )
    {
        int sock_fd = events[i].data.fd;  //events中的所有文件描述符都已就绪
        handle( sock_fd );  //处理就绪的sock_fd
    }

3. LT和ET模式

LT : 电平触发(默认工作方式)
ET :边沿触发(高效工作方式)

对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当下一次调用epoll_wait()时,epoll_wait()还会再次向应用程序通知此事件,直至该事件被处理。
对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait()将不再向应用程序通知该事件。
可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率会比LT模式高。


4. code

/*  该程序以  “ ./a.out ip_number port_number ”   方式使用
**  通过telnet连接到该运行程序 
 */

#include <iostream>
#include <cstdio>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <unistd.h>
#include <signal.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <sys/epoll.h>
using namespace std;

const int MAX_EVENT_NUMBER = 1024;
const int BUFFER_SIZE = 10;

void err( int );
void addfd( int , int , bool );
int setnonblocking( int );
void lt( epoll_event * , int , int , int );
void et( epoll_event * , int , int , int );


int main(int argc, char * argv[]) {
    if ( argc < 3 ) {
        cout << "usage:\n";
        return 1;
    }

    const char *ip = argv[1];
    const int port = atoi( argv[2] );

    struct sockaddr_in address;
    memset( &address, 0, sizeof( address ) );
    address.sin_family = AF_INET;
    address.sin_port = htons( port );
    inet_pton( AF_INET, ip, &address.sin_addr );

    int sock_fd = socket( AF_INET, SOCK_STREAM, 0 );
    if ( sock_fd < 0) {
        err( __LINE__ );
    }

    int ret = bind( sock_fd, ( struct sockaddr * )&address, sizeof( address ) );
    if ( ret < 0 ) {
        err( __LINE__ );
    }

    ret = listen( sock_fd, 5 );
    if ( ret < 0 ) {
        err( __LINE__ );
    }

    epoll_event events[MAX_EVENT_NUMBER];  //内核事件表,通过epoll_wait()函数发生作用

    int epoll_fd = epoll_create( 5 );
    if ( epoll_fd < 0 ) {
        err( __LINE__ );
    }

    addfd( epoll_fd, sock_fd, true );

    while( true ) {
        ret = epoll_wait( epoll_fd, events, MAX_EVENT_NUMBER, -1 );
        if ( ret < 0 ) {
            cout << "epoll failure\n";
            break;
        }

        lt( events, ret, epoll_fd, sock_fd );  //使用LT模式
        // et( events, ret, epoll_fd, sock_fd );  //使用ET模式
    }

    close( sock_fd );

    return 0;
}

void err(int line) {
    cout << "error: line " << line << endl;
}

void addfd( int epoll_fd, int fd, bool enable_et ) {
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if ( enable_et ) {
        event.events |= EPOLLET;
    }
    epoll_ctl( epoll_fd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}

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 lt( epoll_event * events, int number, int epoll_fd, int listen_fd) {
    char buf[ BUFFER_SIZE ];
    for( int i = 0; i < number; i++ ) {
        int sock_fd = events[i].data.fd;
        if ( sock_fd == listen_fd ) {  //有新的连接请求
            struct sockaddr_in client;
            socklen_t client_length = sizeof( client );
            int conn_fd = accept( sock_fd, ( struct sockaddr * )&client,
                                    &client_length );
            addfd( epoll_fd, conn_fd, false );
        } else if ( events[i].events & EPOLLIN ) {  //还存在未读数据
            cout << "event trigger once\n";
            memset( buf, 0, sizeof( buf ) );
            int ret = recv( sock_fd, buf, BUFFER_SIZE - 1, 0 );
            if ( ret <= 0 ) {
                close( sock_fd );
            }
            printf( "get %d bytes of content: -%s-\n", ret, buf );
        } else {
            cout << "something else happened\n";
        }
    }
}

void et( epoll_event * events, int number, int epoll_fd, int listen_fd) {
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ ) {
        int sock_fd = events[i].data.fd;
        if ( sock_fd == listen_fd ) {  //有新的连接请求
            struct sockaddr_in client;
            socklen_t client_length = sizeof( client );
            int conn_fd = accept( sock_fd, ( struct sockaddr * )&client,
                                    &client_length );
            addfd( epoll_fd, conn_fd, true );
        } else if ( events[i].events & EPOLLIN ) {  //还存在未读数据
            printf("event trigger once\n");
            while( true ) {
                memset( buf, 0, sizeof( buf ) );
                int ret = recv( sock_fd, buf, BUFFER_SIZE - 1, 0 );
                if ( ret < 0 ) {
                    /* 数据已全部读取完毕 */
                    if ( errno == EAGAIN || errno == EWOULDBLOCK ) {
                        cout << "read later\n";
                        break;
                    }
                    close( sock_fd );
                    break;
                } else if ( !ret ) {
                    close( sock_fd );
                } else {
                    printf("got %d bytes of content: -%s-\n", ret, buf);
                }
            }
        }
    }
}

以上是关于I/O多路复用之 epoll的主要内容,如果未能解决你的问题,请参考以下文章

I/O多路复用之epoll

I/O多路复用之 epoll

I/O多路复用之epoll

I/O多路复用之epoll

高级I/O---多路复用---epoll

socket编程:多路复用I/O服务端客户端之epoll