【I/O复用】epoll的使用及三组I/O复用函数比较

复习一下,随手写写。
对于三组I/O复用函数的总结,包括epoll的ET和LT。
本文为阅读《linux高性能服务器编程》后的总结笔记。

I/O复用技术使得一个程序可以监听多个文件描述符,是网络编程并发必不可少的技术,其主要运用场景有:
1、同时处理多个socket;
2、同时处理用户输入和网络连接;
3、TCP客户端同时监听socket和连接socket等
select函数是三个中最古老的,之后是poll,最后是epoll,他们的综合性能来说当然也是依次提升的。因此详细的api只贴出epoll,最后给出三者比较。

epoll系统调用

epoll为Linux特有,在mac OS上有kqueue与其类似,epoll是一组函数。
首先它将用户关心的文件描述符上的事件放在内核的一个事件表中,这个表的实际实现为红黑树结构,之后用一个额外的文件描述符来表示这个事件表,及可以看做是红黑树的树根。

epoll API

首先是创建事件表的文件描述符。

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

size参数表示事件表要多大,调用成功返回一个文件描述符。
之后使用下面的函数操作事件表:

			int epoll_ctl(int epfd, int op, int fd, struct epoll_event 
			*event);

倒着来说,event参数表示事件:

           struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };

epoll_event 结构体中events成员表示事件类型,和poll函数的类似,前面多个E,如:

       EPOLLIN
              The associated file is available for read(2) opera‐
              tions.

       EPOLLOUT
              The associated file is available for write(2) oper‐
              ations.
       .....
       注意其中EPOLLET和EPOLLONESHOOT两个类型为epoll特有,很重要,之后细讲。

data成员表示用户数据,它是一个联合体,最常用的是fd即我们关心的是一个文件描述符,例如socket。当然通过void*指针可以实现更加复杂的功能,这也提供了很大的拓展性。

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

op和fd两个参数一起来说,fd表示本次操作的类型,有增、删、改这三种操作,对应宏如下,fd即为本次被操作的文件描述符:

       EPOLL_CTL_ADD
              Register the target file descriptor fd on the epoll
              instance referred to by the  file  descriptor  epfd
              and  associate  the  event  event with the internal
              file linked to fd.

       EPOLL_CTL_MOD
              Change the event event associated with  the  target
              file descriptor fd.

       EPOLL_CTL_DEL
              Remove  (deregister)  the target file descriptor fd
              from the epoll instance referred to by  epfd.   The
              event  is  ignored  and  can  be NULL (but see BUGS
              below).

最后epfd就是我们用epoll_create创造出来的树根啦。
epoll最主要的函数就是下面的epoll_wait,它和poll、select函数比较像,在一段超时时间内等待一组文件描述符上的事件。

       int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);

这里maxevents表示最多监听的文件描述符数量,timeout为超时时间单位是毫秒,-1为阻塞等待,0为立即返回。
第二个参数为一个数组,当检测到事件,会把事件表上所有的就绪事件拷贝到数组中。

epoll的ET和LT

Level-triggered and edge-triggered,即水平触发与边缘触发。
LT为默认模式,此时当一个事件到了未被处理,则下一次调用epollwait时还会触发提示,而ET只会提示一次,因此需要程序立即处理,但是这样降低了一个事件被重复触发的次数,提高了效率。
具体代码:

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 )
        {
        //ET模式下,用while(1)将数据一次读完            
        	printf( "event trigger once\n" );
            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" );
        }
    }
}

EPOLLONESHOOT

这里引用书中原话:
在这里插入图片描述
源码如下:
关键步骤贴上了详细注释

#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 1024
struct fds
{
   int epollfd;
   int sockfd;
};

//设置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;
}

//向epoll事件表中注册fd,并通过bool变量来设置是否开启oneshot
void addfd( int epollfd, int fd, bool oneshot )
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    if( oneshot )
    {
        event.events |= EPOLLONESHOT;
    }
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}

//重置该fd,使其EPOLLIN事件能够被触发且仅触发一次
void reset_oneshot( int epollfd, int fd )
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event );
}

//线程回调函数,循环读取sockfd上的数据,直到recv读取返回EAGAIN 错误
//该错误在非阻塞IO时表示做某操作(如read、recv、fork等)而没有数据可读。
//此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
void* worker( void* arg )
{
    int sockfd = ( (fds*)arg )->sockfd;
    int epollfd = ( (fds*)arg )->epollfd;
    printf( "start new thread to receive data on fd: %d\n", sockfd );
    char buf[ BUFFER_SIZE ];
    memset( buf, '\0', BUFFER_SIZE );
    while( 1 )
    {
        int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
        if( ret == 0 )
        {
            close( sockfd );
            printf( "foreiner closed the connection\n" );
            break;
        }
        else if( ret < 0 )
        {
            if( errno == EAGAIN )
            {
                reset_oneshot( epollfd, sockfd );
                printf( "read later\n" );
                break;
            }
        }
        else
        {
            printf( "get content: %s\n", buf );
            sleep( 5 );
        }
    }
    printf( "end thread receiving data on fd: %d\n", sockfd );
}

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 );
    //listenfd作为监听socket,不能设置oneshot,否则只会响应一个客户连接
    addfd( epollfd, listenfd, false );

    while( 1 )
    {
        int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
        if ( ret < 0 )
        {
            printf( "epoll failure\n" );
            break;
        }
    
        for ( int i = 0; i < ret; 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 );
                //设置每个连接为oneshot
                addfd( epollfd, connfd, true );
            }
            else if ( events[i].events & EPOLLIN )
            {
                pthread_t thread;
                fds fds_for_new_worker;
                fds_for_new_worker.epollfd = epollfd;
                fds_for_new_worker.sockfd = sockfd;
                pthread_create( &thread, NULL, worker, ( void* )&fds_for_new_worker );
            }
            else
            {
                printf( "something else happened \n" );
            }
        }
    }

    close( listenfd );
    return 0;
}

在这里插入图片描述

三组函数的比较

  • 操作方式及效率:
    select是遍历,需要遍历fd_set每一个比特位(= MAX_CONN),O(n);poll是遍历,但只遍历到pollfd数组当前已使用的最大下标(≠ MAX_CONN),O(n);epoll是回调,O(1)。

  • 最大连接数:
    select为1024/2048(一个进程打开的文件数是有限制的);poll无上限;epoll无上限。

  • fd拷贝:
    select每次都需要把fd集合从用户态拷贝到内核态;poll每次都需要把fd集合从用户态拷贝到内核态;epoll调用epoll_ctl时拷贝进内核并放到事件表中,但用户进程和内核通过mmap映射共享同一块存储,避免了fd从内核赋值到用户空间。

  • 其他:
    select每次内核仅仅是通知有消息到了需要处理,具体是哪一个需要遍历所有的描述符才能找到。epoll不仅通知有I/O到来还可通过callback函数具体定位到活跃的socket,实现伪AIO。
    在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值