I/O多路复用:select、poll、epoll


目前支持I/O多路复用的系统调用有select、poll、epoll,I/O多路就是 通过一种机制,一个线程可以监视多个文件描述符,一旦文件描述符就绪,能够通知程序进行相应的读写操作。select、poll、epoll本质上都是同步I/O操作,因为他们都需要在读写事件就绪后由线程自己负责读写操作,整个读写过程使阻塞的。而异步I/O是不需要自己进行读写的,内核会负责将数据从内核拷贝到用户空间。

多路指网络连接,复用指的是同一个线程。

如果使用多线程+阻塞IO达到类似的效果,但是每个socket对应一个线程,对于长连接来说,线程资源一直不会释放,就造成性能问题。

此时用IO多路复用,使用一个线程去监视多个socket,只有真正有读写事件的时候才会去通过用户线程执行。

适用场景:

  1. 客户端需要同时处理多个socket请求
  2. 客户端程序需要同时处理用户输入和网络连接
  3. 服务器需要同时处理监听socket和连接socket

select

int select (int n,fd_set *readfds,fd_set *weitefds,fd_set *exceptfds,struct timeval *timeout);

优点:

  1. select目前几乎所有的平台上都支持,跨平台性

缺点:

  1. 单个线程可以监视的文件描述符的数量存在最大限制,它是由一个FD_SETSIZE设置的,默认最大值为1024.
  2. fd集合在内核中被置位过,与传入的fd不可重用,需要重新设置希望内核监视的标志位
  3. 每次调用select都需要将集合从用户态拷贝到内核态,响应时再从内核态拷贝回用户态
  4. 每次调用时,内核需要进行一次遍历标志位,传回用户态,用户也需要进行一个遍历,O(n)

poll

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

不同于select使用位图来表示fdset的方式,poll则是定义了一个结构体来存储相关事件的信息。

struct pollfd{
    int fd;            //需要监视的文件描述符
    short events;	   //需要监视的事件
    short revents;	   //发生的事件
};

优点:

  1. poll使用 pollfd数组代替了bitmap,没有最大数量限制
  2. 利用结构体,每次只需要修改revents即可。pollfd可以重用

缺点:

  1. 每次调用poll时,还是需要将pollfd数组从用户态拷贝到内核态,返回时也一样
  2. 每次对pollfd数组查询时都需要进行遍历查询,随着监视的文件描述符不断增大,效率也线性降低

epoll

int epoll_create(int size);  //创建一个epoll的文件描述符

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);
struct eventpoll{
    struct rb_root rbr; //红黑树,存储需要监控的事件
    struct list_head rdlist; //双链表存储需要返回给用户的已响应事件
};
struct epoll_event{
    __uint32_t events; //监听的事件
    epoll_data_t data; //监听的文件描述符
};

事件响应过程:

  • epoll再内核中使用红黑树来跟踪进程的所有待检测的文件描述符,把需要监控的文件描述符用epoll_ctl()函数加入到内核中的红黑树中。增删查的事件复杂度都是 O(logn)。每次只需要传入新的待检测文件描述符,旧的文件描述符都还存储在内核中,减少了内核和用户空间之间大量的数据拷贝和内存分配。
  • epoll通过事件驱动的机制,内核中维护了一个链表来记录就绪事件。当有个事件响应时,通过回调函数内核会将其加入到就绪事件列表中。当用户调用 epoll_wait()函数时,只返回有事件响应的文件描述符对象。

事件触发模式:

  • 边缘触发(ET),当监视的文件描述符上有事件发生,只会从 epoll_wait()中苏醒一次,即使内核缓缓冲区中还有数据没读完,也只会苏醒一次,所有ET模式下需要一次性将内核缓冲区的数据读取完。
  • 水平触发(LT),当监视的文件描述符上有事件发生,会不断的从 epoll_wait()中苏醒,知道内核缓冲区的数据被读完。

举个例子,你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信提醒你,这个方式就是边缘触发;

如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式。

边缘触发的效率比水平触发的效率高,可以减少 epoll_wait()的系统调用函数。

select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发。

优点:

  1. 监听的文件描述符不受限制
  2. epoll内核使用红黑树维护所有的fd,查询效率高,返回的是响应事件的链表
  3. epoll的效率不会因为监视的文件描述符的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个文件描述符的回调函数实现的。只有就绪的fd才会执行回调函数。

代码

select代码

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>

int main(){
    //创建socket
    int lfd = socket(PF_INET,SOCK_STREAM,0);

    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    //绑定
    bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));

    //监听
    listen(lfd,8);

    //创建一个fd_set集合,存放的是需要检测的文件描述符,前三位默认012被占用
    fd_set rdset,tmp;
    //将集合中的位清空
    FD_ZERO(&rdset);
    //将监听文件描述符加入到集合中
    FD_SET(lfd,&rdset);
    //当前最大的文件描述符,select时使用
    int maxfd = lfd;
    
    while(1)
    {
        //需要备份
        tmp = rdset;
        //调用select,检测哪些文件描述符有数据
        int ret = select(maxfd+1,&tmp,NULL,NULL,NULL);
        if(ret==-1)
        {
            perror("select");
            exit(-1);
        }
        //设置非阻塞时可能会返回0
        else if(ret==0){
            continue;
        }
        else if(ret>0)
        {
            //检查监听文件描述符
            if(FD_ISSET(lfd,&tmp)){
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
                //添加需要监听的文件描述符
                FD_SET(cfd,&rdset);

                maxfd = maxfd>cfd?maxfd:cfd;
            }
            for(int i=lfd+1;i<=maxfd;i++)
            {
                if(FD_ISSET(i,&tmp)){
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(i, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(i);
                        FD_CLR(i, &rdset);
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(i, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}

poll代码

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>


int main() {
    int lfd = socket(PF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    bind(lfd,(struct sockaddr *)&saddr,sizeof(saddr));

    listen(lfd,8);
    //建立pollfd数组,数组大小由自己定义,没有限制
    struct pollfd fds[1024];
    for(int i=0;i<1024;i++)
    {
        //初始化每个pollfd对象
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;
    int nfds = 0;
    while(1){
        int ret = poll(fds,nfds+1,-1);
        if(ret == -1) {
            perror("poll");
            exit(-1);
        } else if(ret == 0) {
            continue;
        } else if(ret > 0) {
            if(fds[0].revents & POLLIN){
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                for(int i=1;i<1024;i++)
                {
                    if(fds[i].fd==-1)
                    {
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }
                // 更新最大的文件描述符的索引
                nfds = nfds > cfd ? nfds : cfd;
            }
            for(int i=1;i<=nfds;i++)
            {
                if(fds[i].fd!=-1 && fds[i].revents & POLLIN){
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}

	

epoll代码

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main(){
    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    //调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);
    //创建epoll对象中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    //将监听文件描述符添加到
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);
    //返回的可操作文件描述符
    struct epoll_event epevs[1024];
    while(1){
        int ret = epoll_wait(epfd,epevs,1024,-1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);
        for(int i=0;i<ret;i++)
        {
            int curfd = epevs[i].data.fd;
            if(curfd==lfd){
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
                char cliIP[16];
                inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,cliIP,sizeof(cliIP));
                unsigned short clientPort = ntohs(cliaddr.sin_port);
                printf("client ip is : %s, prot is %d\n", cliIP, clientPort);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);
            }else{
                if(!(epevs[i].events & EPOLLIN)){
                    continue;
                }
                // 有数据到达,需要通信
                char buf[5] = {0};
                int len = read(curfd, buf, sizeof(buf)-1);
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    //从实例中删除这个文件描述符
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

版权声明:本文为why18767183086原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。