IO多路复用的三种机制Select、Poll、Epoll

select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式。

一张图总结一下select,poll,epoll的区别:

 

select

poll

epoll

操作方式

遍历

遍历

回调

底层实现

数组

链表

哈希表

IO效率

每次调用都进行线性遍历,时间复杂度为O(n)

每次调用都进行线性遍历,时间复杂度为O(n)

事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)

最大连接数

1024(x86)或2048(x64)

无上限

无上限

fd拷贝

每次调用select,都需要把fd集合从用户态拷贝到内核态

每次调用poll,都需要把fd集合从用户态拷贝到内核态

调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝

I/O多路复用

I/O多路复用(multiplexing)的本质是通过系统内核缓冲I/O数据,让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作

 

相信大家都了解了Unix五种IO模型,不了解的可以 => 查看这里 

同步I/O,在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。反之是异步IO

阻塞I/O, 等待读写事件是否是阻塞的。

blocking IO同步阻塞IO
nonblocking IO同步非阻塞IO
IO multiplexing - IO多路复用同步非阻塞IO
signal driven IO - 信号驱动IO同步非阻塞IO
asynchronous IO异步IO非阻塞IO

 

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

在介绍select、poll、epoll之前,首先介绍一下Linux操作系统中基础的概念:

  • 用户空间 / 内核空间现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
  • 进程切换为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的,并且进程切换是非常耗费资源的。
  • 进程阻塞正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得了CPU资源),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
  • 文件描述符文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
  • 缓存I/O缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

Select

  select()函数与recv()函数和send()函数不同的是,recv()函数和send()函数可直接操作文件的描述符。但是使用select()函数时,需要先对所要操作的文件描述符进行查询,查看目标文件的描述符是否可以进行读、写、或者错误操作,然后当文件的描述符满足操作的条件时才进行真正的IO操作,即读和写操作。

 select()函数监视的文件描述符可分为3类,分别是readfds,writefds,exceptfds。调用后,select()函数会阻塞,直到有描述符就绪(有数据、可读、可写或者有错误)时,或者超时时才返回,当select()函数返回后,可以通过便利fdset来找到就绪的描述符。需要注意的是,当声明了一个文件描述符集合后,必须用FD_ZERO()函数来将所有位置为0。

      函数select()的原型为:

         int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);

         各参数含义为:

         nfds:整型变量,它比所有文件描述符集合中的最大值大1。使用select的时候必须计算最大值的文件描述符的值,将值通过nfds传入。

         readfds:这个文件描述符集合监视文件集合中的任何文件是否有数据可读,当select()函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符,即可以被recv()函数、read()函数等进行读数据的操作。

         writefds:这个文件描述符集合监视文件集合中的任何文件是否有数据可写,当select()函数返回的时候,writefds将清除其中不可写的文件描述符,只留下可写的文件描述符,即可以被send()函数、write()函数等进行写数据的操作。

         exceptfds:这个文件描述符集合监视文件集合中的任何文件是否发生错误,其实它可以用于其他的用途,例如,监视带外数据OOB,带外数据使用MSG_OOB标志发送到套接字上。当select()函数返回的时候,readfds将清除其中的其他文件描述符,只留下可读OOB数据。

         timeout:用来描述等待描述符就绪需要的事件。设置在select()函数文件集合中的事件没有发生时,最长的等待时间,当超过此时间时,函数会返回。当超时时间为NULL时,表示阻塞操作,会一直等待,直到某个监视的文件集合中的某个文件描述符符合返回条件。当timeout的值为0时,select()会立即返回。timeout告知系统内核等待指定描述符中的任何一个就绪可花费多少时间。其timeval结构体用于指定这段时间的秒数和微秒数。
 

struct timeval
{
    time_t tv_sec;             //秒
    long tv_usec;              //微秒
};

       返回值:成功执行时返回就绪描述符的数目;经过了timeout时长后仍无设备准备好,即超时,返回0;如果出错,返回-1并设置相应的errno,如果select()执行过程中被某个信号中断,返回-1并设置errno为EINTR。

         errorno的取值及含义:

         EBADF:文件描述符无效或该文件已被关闭

         EINVAL:传递了不合法参数

         EINTR:接收到中断信号

         ENOMEM:没有足够内存

         readset,writeset,exceptset都是值-结果参数,即传入指针进去,函数根据指针可以修改对应fd_set。

   通常,操作系统通过宏FD_SETSIZE来声明一个进程中select所能操作的文件描述符的最大数目。在/usr/include/linux/posix_types.h中关于FD_SETSIZE是这样定义的:

#undef _FD_SETSIZE

#define _FD_SETSIZE 1024

         除此之外,还有4个宏可以用来操作文件描述符的集合:

         FD_ZERO(fd_set* fdset):将指定的文件描述符集合清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于系统在分配内存空间后通常不作清空处理,所以结果是不可知的。

         FD_SET(fd_set* fdset):用于在文件描述符集合中增加一个新的文件描述符

         FD_CLR(fd_set* fdset):用于在文件描述符集合中删除一个文件描述符

         FD_ISSET(int fd,fd_set* fdset):用于检测指定的文件描述符是否在该文件描述符集合中

         在内核中,socket对应struct socket结构,但在返回给用户空间之前,内核做了一个关联,调用get_unused_fd_flags从当前进程中获取一个可用的文件描述符fd,将struct socket结构关联到该fd,并返回fd给用户空间。所以在用户空间中,socket为文件描述符。另外,进程可以打开的文件数是有限的,为1024,所以socket的取值要小于1024。

 select()函数的优缺点:

         优点:select()目前几乎在所有的平台上支持,其良好的跨平台支持也是它的一个优点。

         缺点:select最大的缺陷就是单个进程所打开的fd的数量是有一定限制的,它由FD_SETSIZE设置,默认值是1024。一般来说,这个数目和系统的内存关系很大,32位机默认是1024,64位机默认是2048;对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个socket来完成调度,不管哪个socket是活跃的,都得遍历一遍,这会在无形中浪费很多CPU时间,如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询;需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。


#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <error.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void handle(int clientfds[],int max_fd,fd_set* rset,fd_set* allset)
{
    int nread;
    int i;
    char buffer[1024];
    for(i = 0;i < max_fd;i++)
    {
        if(clientfds[i] != -1)
        {
            if(FD_ISSET(clientfds[i],rset))
            {
                //读取客户端socket流
                nread = read(clientfds[i],buffer,1024);
                if(nread < 0)
                {
                    perror("read:");
                    close(clientfds[i]);
                    FD_CLR(clientfds[i],allset);
                    clientfds[i] = -1;
                    continue;
                }
                if(nread == 0)
                {
                    printf("客户端关闭了连接!\n");
                    close(clientfds[i]);
                    FD_CLR(clientfds[i],allset);
                    clientfds[i] = -1;
                    continue;
                }
                write(clientfds[i],buffer,nread);
            }
        }
    }
}

int main()
{
    //设置服务器的端口号为6888
    short s_port = 6888;
    //设置默认监听队列的长度为1024
    int backlog = 1024;
    //地址结构
    struct sockaddr_in clientaddr;          //客户端地址
    struct sockaddr_in serveraddr;           //服务器地址
    //存放客户端通信描述符的数组
    int clientfds[FD_SETSIZE];
    //所监听的描述符
    int listen_fd;
    //描述符的集合
    fd_set allset,rset;
    //用来记录select()函数的返回值
    int nselect;
    //用来记录最大的描述符
    int max_fd;
    //用来记录客户端的socket描述符
    int client_fd;
    //缓冲区长度
    char buffer[1024];
    //地址结构的长度
    int socketlength = sizeof(struct sockaddr_in);

    //创建TCPsocket
    listen_fd = socket(PF_INET,SOCK_STREAM,0);
    //如果创建失败
    if(listen_fd < 0)
    {
        perror("socket:");
        return -1;
    }

    int opt = 1;
    //将监听的端口设置为可以复用的
    if(setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)) < 0)
    {
        perror("setsockopt:");
    }

    //socket地址结构
    bzero(&serveraddr,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(s_port);

    //绑定
    int nbind = bind(listen_fd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr_in));
    if(nbind == -1)
    {
        perror("bind:");
        return -1;
    }

    //监听
    int nlisten = listen(listen_fd,backlog);
    if(nlisten < 0)
    {
        perror("listen:");
        return -1;
    }

    //初始化存放客户端通信描述符的数组
    int i = 0;
    for(;i < FD_SETSIZE;i++)
    {
        clientfds[i] = -1;
    }

    //清理描述符集合
    FD_ZERO(&allset);

    //将监听的socket描述符加入集合
    FD_SET(listen_fd,&allset);
    max_fd = listen_fd;
    printf("服务器正在监听端口%d......\n",s_port);

    while(1)
    {
        rset = allset;
        //等待select事件发生
        nselect = select(max_fd+1,&rset,NULL,NULL,NULL);
        if(nselect < 0)
        {
            perror("select:");
            return -1;
        }

        //处理客户端的连接
        if(FD_ISSET(listen_fd,&rset))        //检测监听的描述符是否存在于描述符集合中
        {
            client_fd = accept(listen_fd,(struct sockaddr*)&clientaddr,&socketlength);
            if(client_fd < 0)
            {
                perror("accept:");
                continue;
            }
            sprintf(buffer,"接受来自%s:%d\n",inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
            printf(buffer);

            //将客户端的socket描述符加入数组中
            for(i = 0;i < FD_SETSIZE;i++)
            {
                if(clientfds[i] == -1)
                {
                    clientfds[i] = client_fd;
                    break;
                }
            }
            //如果达到了最大连接数
            if(i == FD_SETSIZE)
            {
                printf("已达到最大连接数!\n");
                close(client_fd);
            }

            if(client_fd > max_fd)
            {
                max_fd = client_fd;
            }

            //将socket加入集合中
            FD_SET(client_fd,&allset);
            if(--nselect <= 0)
            {
                continue;
            }
        }
        //处理客户端收据的收发
        handle(clientfds,max_fd,&rset,&allset);
    }
    return 0;
}

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define max(a,b) ((a) > (b) ? (a) : (b))

void handle(int client_fd)
{
    FILE* fp = stdin;
    //发送队列和接收队列,设置为1024
    char sendqueue[1024],receivequeue[1024];
    fd_set rset;
    FD_ZERO(&rset);
    int max_fd = max(fileno(fp),client_fd) + 1;
    for(;;)
    {
        FD_SET(fileno(fp),&rset);
        FD_SET(client_fd,&rset);

        int nselect = select(max_fd,&rset,NULL,NULL,NULL);
        if(nselect == -1)
        {
            perror("select:");
            continue;
        }
        if(FD_ISSET(client_fd,&rset))
        {
            //接收到服务器的响应
            int nread = read(client_fd,receivequeue,1024);
            if(nread == 0)
            {
                printf("服务器关闭了连接!\n");
                break;
            }
            else if(nread == -1)
            {
                perror("read:");
                break;
            }
            else
            {
                write(STDOUT_FILENO,receivequeue,nread);
            }
        }
        if(FD_ISSET(fileno(fp),&rset))
        {
            //标准输入可读
            if(fgets(sendqueue,1024,fp) == NULL)
            {
                break;
            }
            else
            {
                write(client_fd,sendqueue,strlen(sendqueue));
            }
        }
    }
}

int main()
{
    char* s_iaddr = "127.0.0.1";
    int s_port = 6888;
    int client_fd;
    char buffer[1024];
    struct sockaddr_in serveraddr;

    client_fd = socket(AF_INET,SOCK_STREAM,0);
    bzero(&serveraddr,sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(s_port);
    inet_pton(AF_INET,s_iaddr,&serveraddr.sin_addr);

    //建立连接
    if(connect(client_fd,(struct sockaddr*)&serveraddr,sizeof(serveraddr)) < 0)
    {
        perror("connect:");
        return -1;
    }
    printf("---回射服务器的客户端---\n");
    handle(client_fd);
    close(client_fd);
    printf("exit\n");
    exit(0);
}

Poll

poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。

下面是pll的函数原型:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

typedef struct pollfd {
        int fd;                         // 文件描述符,如果小于0。这个文件描述符将会被进程忽略
        short events;                   // 注册的事件,即感兴趣的事件。可以是一个事件,也可以是多个事件的按位或
        short revents;                  // 实际发生的事件,由内核来赋值。需要与某种类型的事件按位与来确定某种事件是否发生
} pollfd_t;

poll改变了文件描述符集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的文件描述符集合限制远大于select的1024

【参数说明】

 struct pollfd *fds fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;

一个pollfd结构体表示一个被监视的文件描述符,通过传递fds指示 poll() 监视多个文件描述符。其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域

nfds_t nfds 记录数组fds中描述符的总数量

【返回值】int 函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;

poll事件类型如下表:
poll事件类型

自Linux内核2.6.17开始,GUN为poll系统调用增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求之后触发,使用POLLRDHUP时,需要在代码最开始处定义 _GUN_SOURCE。
当然我们也可以通过read来判断对方是否关闭了连接,当read返回0时,表示对方请求关闭连接。
 

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
int main(){
    struct pollfd fdset[1];
    fdset[0].fd = 0;
    fdset[0].events = POLLIN;
    int ret;
    char buf[100];
    while(1){
        ret = poll(fdset, 1, -1);
        if(fdset[0].revents & POLLIN){
            ret = read(0, buf, 100);
            printf("读取到%d个字节的数据%s", ret, buf);
        }
    }
    return 0;
}

Epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll操作过程需要三个接口,分别如下:

1

2

3

4

#include <sys/epoll.h>

int epoll_create(int size);

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

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

(1) int epoll_create(int size);

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

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

epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

1

2

3

4

struct epoll_event {

 __uint32_t events; /* Epoll events */

 epoll_data_t data; /* User data variable */

};

events可以是以下几个宏的集合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

工作模式

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

编写一个服务器回射程序echo,练习epoll过程。

//
// Created by ken.li on 2021/5/20.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/types.h>

#define IPADDRESS  "127.0.0.1"
#define PORT    8787
#define MAXSIZE   1024
#define LISTENQ   5
#define FDSIZE   1000
#define EPOLLEVENTS 100

//函数声明
//创建套接字并进行绑定
static int socket_bind(const char* ip,int port);
//IO多路复用epoll
static void do_epoll(int listenfd);
//事件处理函数
static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf);
//处理接收到的连接
static void handle_accpet(int epollfd,int listenfd);
//读处理
static void do_read(int epollfd,int fd,char *buf);
//写处理
static void do_write(int epollfd,int fd,char *buf);
//添加事件
static void add_event(int epollfd,int fd,int state);
//修改事件
static void modify_event(int epollfd,int fd,int state);
//删除事件
static void delete_event(int epollfd,int fd,int state);

int main(int argc,char *argv[])
{
    int listenfd;
    listenfd = socket_bind(IPADDRESS,PORT);
    listen(listenfd,LISTENQ);
    do_epoll(listenfd);
    return 0;
}

static int socket_bind(const char* ip,int port)
{
    int listenfd;
    struct sockaddr_in servaddr;
    listenfd = socket(AF_INET,SOCK_STREAM,0);
    if (listenfd == -1)
    {
        perror("socket error:");
        exit(1);
    }
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET,ip,&servaddr.sin_addr);
    servaddr.sin_port = htons(port);
    if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)
    {
        perror("bind error: ");
        exit(1);
    }
    return listenfd;
}

static void do_epoll(int listenfd)
{
    int epollfd;
    struct epoll_event events[EPOLLEVENTS];
    int ret;
    char buf[MAXSIZE];
    memset(buf,0,MAXSIZE);
    //创建一个描述符
    epollfd = epoll_create(FDSIZE);
    //添加监听描述符事件
    add_event(epollfd,listenfd,EPOLLIN);
    for ( ; ; )
    {
        //获取已经准备好的描述符事件
        ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
        handle_events(epollfd,events,ret,listenfd,buf);
    }
    close(epollfd);
}

static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
    int i;
    int fd;
    //进行选好遍历
    for (i = 0;i < num;i++)
    {
        fd = events[i].data.fd;
        //根据描述符的类型和事件类型进行处理
        if ((fd == listenfd) &&(events[i].events & EPOLLIN))
            handle_accpet(epollfd,listenfd);
        else if (events[i].events & EPOLLIN)
            do_read(epollfd,fd,buf);
        else if (events[i].events & EPOLLOUT)
            do_write(epollfd,fd,buf);
    }
}
static void handle_accpet(int epollfd,int listenfd)
{
    int clifd;
    struct sockaddr_in cliaddr;
    socklen_t cliaddrlen;
    clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
    if (clifd == -1)
        perror("accpet error:");
    else
    {
        printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
        //添加一个客户描述符和事件
        add_event(epollfd,clifd,EPOLLIN);
    }
}

static void do_read(int epollfd,int fd,char *buf)
{
    int nread;
    nread = read(fd,buf,MAXSIZE);
    if (nread == -1)
    {
        perror("read error:");
        close(fd);
        delete_event(epollfd,fd,EPOLLIN);
    }
    else if (nread == 0)
    {
        fprintf(stderr,"client close.\n");
        close(fd);
        delete_event(epollfd,fd,EPOLLIN);
    }
    else
    {
        printf("read message is : %s",buf);
        //修改描述符对应的事件,由读改为写
        modify_event(epollfd,fd,EPOLLOUT);
    }
}

static void do_write(int epollfd,int fd,char *buf)
{
    int nwrite;
    nwrite = write(fd,buf,strlen(buf));
    if (nwrite == -1)
    {
        perror("write error:");
        close(fd);
        delete_event(epollfd,fd,EPOLLOUT);
    }
    else
        modify_event(epollfd,fd,EPOLLIN);
    memset(buf,0,MAXSIZE);
}

static void add_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

static void delete_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

static void modify_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}

客户端也用epoll实现,控制STDIN_FILENO、STDOUT_FILENO、和sockfd三个描述符

#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>

#define MAXSIZE   1024
#define IPADDRESS  "127.0.0.1"
#define SERV_PORT  8787
#define FDSIZE    1024
#define EPOLLEVENTS 20

static void handle_connection(int sockfd);
static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_write(int epollfd,int fd,int sockfd,char *buf);
static void add_event(int epollfd,int fd,int state);
static void delete_event(int epollfd,int fd,int state);
static void modify_event(int epollfd,int fd,int state);

int main(int argc,char *argv[])
{
    int         sockfd;
    struct sockaddr_in servaddr;
    sockfd = socket(AF_INET,SOCK_STREAM,0);
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);
    connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
    //处理连接
    handle_connection(sockfd);
    close(sockfd);
    return 0;
}


static void handle_connection(int sockfd)
{
    int epollfd;
    struct epoll_event events[EPOLLEVENTS];
    char buf[MAXSIZE];
    int ret;
    epollfd = epoll_create(FDSIZE);
    add_event(epollfd,STDIN_FILENO,EPOLLIN);
    for ( ; ; )
    {
        ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
        handle_events(epollfd,events,ret,sockfd,buf);
    }
    close(epollfd);
}

static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf)
{
    int fd;
    int i;
    for (i = 0;i < num;i++)
    {
        fd = events[i].data.fd;
        if (events[i].events & EPOLLIN)
            do_read(epollfd,fd,sockfd,buf);
        else if (events[i].events & EPOLLOUT)
            do_write(epollfd,fd,sockfd,buf);
    }
}

static void do_read(int epollfd,int fd,int sockfd,char *buf)
{
    int nread;
    nread = read(fd,buf,MAXSIZE);
    if (nread == -1)
    {
        perror("read error:");
        close(fd);
    }
    else if (nread == 0)
    {
        fprintf(stderr,"server close.\n");
        close(fd);
    }
    else
    {
        if (fd == STDIN_FILENO)
            add_event(epollfd,sockfd,EPOLLOUT);
        else
        {
            delete_event(epollfd,sockfd,EPOLLIN);
            add_event(epollfd,STDOUT_FILENO,EPOLLOUT);
        }
    }
}

static void do_write(int epollfd,int fd,int sockfd,char *buf)
{
    int nwrite;
    nwrite = write(fd,buf,strlen(buf));
    if (nwrite == -1)
    {
        perror("write error:");
        close(fd);
    }
    else
    {
        if (fd == STDOUT_FILENO)
            delete_event(epollfd,fd,EPOLLOUT);
        else
            modify_event(epollfd,fd,EPOLLIN);
    }
    memset(buf,0,MAXSIZE);
}

static void add_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

static void delete_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

static void modify_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}

总结

epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值