网络io之select 、poll、epoll小试

此次讨论的是阻塞的fd

先写一个简单的server端

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <time.h>

#define MAXLEN 4096
int main(){    
    int listenfd,connfd,n;
    struct sockaddr_in servaddr;
    char buf[MAXLEN];

    if((listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1)//创建fd
    {
        printf("create socket error:%s",strerror(errno));
        return -1;
    }


    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(8888);
    
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)//绑定本机地址
    {
        printf("bind socket err:%s\n",strerror(errno));
        return -1;
    }

    if(listen(listenfd,10) == -1)//监听 
    {
        printf("listen socket err:%s\n",strerror(errno));
        return -1;
    }
    struct sockaddr_in client;
    socklen_t len = sizeof(client);

    printf("----waiting for client requst----\n");
    
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) //等待客户端连接
    {
        printf("accept socket err:%s\n",strerror(errno));
        return -1;
    }   

    while(1)
    {
        

        n = recv(connfd,buf,MAXLEN,0);
        if(n > 0)
        {
            printf("----waiting---\n");
            buf[n] = '\0';
            printf("recv msg form client:%s\n",buf);

            send(connfd,buf,n,0);
        }
        else if(n == 0)
        {
            printf("----close--\n");
            close(connfd);
        }
    }
    close(listenfd);
    return 0;
}

 使用客户端去连接的时候发现,这样只能进行一对一的服务,当有第二个客户端连接时sock在队列里,没有accpet去取出。

针对这种情况,进行一下改进,每有一个客户端请求,创建一个线程来accpet,来进行服务

//C10K
void *client_pth(void* cfd)
{
    char buf[MAXLEN];
    int connfd = *(int*)cfd;

    while(1)
    {
        int n = recv(connfd,buf,MAXLEN,0);
        if(n > 0)
        {
            buf[n] = '\0';
            printf("recv client connfd %d:%s\n",connfd,buf);
            send(connfd,buf,n,0);
        }
        else if(n  == 0)
        {

            printf("confd:%d\n",connfd);
            close(connfd);
            break;
        }
        
    }
    return NULL;
}



    printf("----waiting for client request----\n");

    while(1)
    {
        printf("--creat--\n ");
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
        {
            printf("accept socket err:%s\n",strerror(errno));
            return -1;            
        }
        printf("creat pthread\n");
        pthread_t pid;
        pthread_create(&pid,NULL,(void*)client_pth,(void*)&connfd);

    }

但是问题来了,每来一个新的连接,就创建一个新的线程,当并发数较大时,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞), 进程/线程上下文切换消耗大,很有可能导致操作系统崩溃,这就是我们所说的C10K问题的本质,所以这种方式只适用于并发量较小的局域网或者内部使用。

select

由这可以引入一个新方法,那就是select。select就是把所有的fd全部一个函数中去管理,select会阻塞等待某一个fd或多个fd的写事件、读事件或者异常事件触发,从而实现相应事件的操作。

这里我们举个例子,如一栋公寓,里面有很多户人家,他们要收发快递,多线程的方式是这么做的,每户人家都雇一个人专门为他收发快递,如果住户很多,会造成很大的人员开销,于是这些住户商量着,把收发快递的活全部交给一个人统一去管理,这就是select的处理方式

以下只针对读事件和写事件做处理

printf("----waiting for client request----\n");
    fd_set rfd,rset,wfd,wset;
    FD_ZERO(&rfd);
    FD_ZERO(&wfd);
    FD_SET(listenfd,&rfd);
    int max_fd = listenfd;
    while(1)
    {

        printf("start \n");   
        wset = wfd;
        rset = rfd;
        int num = select(max_fd+1,&rset,&wset,NULL,NULL);//监听每个fd的写事件和读事件
        printf("num:%d,max_fd:%d\n",num,max_fd);
        if(FD_ISSET(listenfd,&rset))//当我们监听的fd有写事件过来时,代表有新的客户端请求
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
            {
                printf("accept socket err:%s\n",strerror(errno));
                return -1;            
            }
            FD_SET(connfd,&rfd);
            if(connfd > max_fd) max_fd = connfd;
            printf("max_fd:%d\n",max_fd);
            if(--num == 0) continue;
        }
        for(int i = listenfd+1;i <= max_fd;i++)
        {
            printf("fd:%d\n",i);
            if(FD_ISSET(i,&rset))
            {
                n = recv(i,buf,MAXLEN,0);
                if(n > 0)
                {
                    buf[n] = '\0';
                    printf("recv buf:%s\n",buf);
                    FD_SET(i,&wfd);           
                    //send(i,buf,n,0);
                }
                else if(n == 0) //n=0代表连接断开
                {
                    FD_CLR(i,&rfd); //读集合里除去这个fd
                    printf("close socket fd:%d\n",i);
                    close(i);
                }
                //if(--num == 0) break;
            }
            else if(FD_ISSET(i,&wset))
            {
                printf("send fd:%d\n",i);
                //printf("send buf:%s\n",buf);
                send(i,buf,n,0);
                memset(buf,'\0',sizeof(buf));
                FD_SET(i,&rfd);
                FD_CLR(i,&wfd);//一定要清除写集合
            }
            
        }
    }

这里有一个小坑,当写事件被触发后,如果不及时清除,会一直触发写事件,这是我不了解写事件的触发条件造成的,当套接口发送缓冲区有空间容纳数据(大多数时候缓冲区是不满的,会不断产生可写事件)所以每次写完之后要及时清除写集合。

但是,select的容纳个数也是有限的,在内核里FD_SET的最大数默认为1024,改的话需要重新编译内核,这不是一个好办法,或者是多创建几个select来扩容,这里先不谈。还有一种方法则是

poll

        我们知道select是通过一个集合去管理的fd,而集合都是有固定大小的,但poll是通过链表的形式管理,所以没有最大fd的限制。

下面实现poll:


#define POLL_SIZE 1024

    printf("----waiting for client request----\n");

    struct pollfd fds[POLL_SIZE] = {0};
    fds[0].fd = listenfd;
    fds[0].events = POLLIN;

    int max_fd = listenfd;
    for(int i = 1; i < POLL_SIZE; i++)
    {
        fds[i].fd = -1;
    }

    while(1)
    {
        int num = poll(fds,max_fd+1,-1);
        
        if(fds[0].revents & POLLIN)
        {
            struct sockaddr_in client;
            socklen_t len;
            if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
            {
                printf("accept socket err:%s\n",strerror(errno));
                return -1; 
            }
            printf("accpet fd:%d\n",connfd);
            fds[connfd].fd = connfd;
            fds[connfd].events = POLLIN;

            if(connfd > max_fd) max_fd = connfd;
            if(--num == 0) continue;

        }
        for(int i = listenfd+1; i <= max_fd; i++)
        {
            printf("fd:%d\n",i);
            if(fds[i].revents & POLLIN)
            {
                n = recv(i,buf,MAXLEN,0);
                if(n > 0)
                {
                    buf[n] = '\0';
                    printf("recv buf:%s\n",buf);
                    fds[i].events = POLLOUT;
                }
                else if(n == 0)
                {
                    fds[i].fd = -1;
                    printf("close socket fd:%d\n",i);
                    close(i);
                }
                if(--num == 0) break;
            }
            else if(fds[i].revents & POLLOUT)
            {
                printf("send fd:%d\n",i);
                send(i,buf,n,0);
                //fds[i].events = 0;
                fds[i].events = POLLIN;
                if(--num == 0) break;
            }
            
            
            
        }
        
    }

由代码可以看出select和poll的实现方式相差不大,都是通过阻塞等待某一个fd或多个fd的事件触发后,轮询查找相应的fd进行操作。由于每次事件触发,都会去遍历一遍fd,从而导致多次的无用遍历,造成额外的cpu开销。

 接着上面收发快递的例子,select和poll的处理方式是由一个快递员统一管理收发快递,但没有告诉快递员某个快递应该送到哪一户,于是快递员只能一家一家的询问,很耗费快递员的精力,如果我们在收发快递的时候告诉快递员该往哪家送,那快递员的活就会轻松不少,这就是下面要实现的

epoll

        epoll把select、poll的主动轮询为被动通知,当有事件发生时,被动接收通知。每个事件都有一个关联的fd,这样每次的事件操作都是有意义的

printf("----waiting for client request----\n");

    int epfd = epoll_create(1);
    struct epoll_event events[POLL_SIZE] = {0};
    struct epoll_event ev;
    
    ev.events = EPOLLIN;
    ev.data.fd = listenfd;

    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

    while(1)
    {
        int num = epoll_wait(epfd,events,POLL_SIZE,-1);
        if (num == -1) continue;
        for(int i = 0; i < num; i++)
        {
            int clientfd = events[i].data.fd;
            if(clientfd == listenfd)
            {
                struct sockaddr_in client;
                socklen_t len;
                if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
                {
                    printf("accept socket err:%s\n",strerror(errno));
                    return -1; 
                }
                printf("accpet fd:%d\n",connfd);
                ev.data.fd = connfd;
                ev.events = EPOLLIN;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
            else if(events[i].events & EPOLLIN)
            {
                n = recv(clientfd,buf,MAXLEN,0);
                if(n > 0)
                {
                    buf[n] = '\0';
                    printf("recv buf:%s\n",buf);
                    ev.data.fd = clientfd;
                    ev.events = EPOLLOUT;
                    epoll_ctl(epfd,EPOLL_CTL_MOD,clientfd,&ev);
                }
                else if(n == 0)
                {
                    ev.data.fd = clientfd;
                    ev.events = EPOLLHUP;
                    epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,&ev);
                    close(clientfd);
                }
            }
            else if(events[i].events & EPOLLOUT)
            {
                printf("send fd:%d\n",clientfd);
                send(clientfd,buf,n,0);
                ev.data.fd = clientfd;
                ev.events = EPOLLIN;
                epoll_ctl(epfd,EPOLL_CTL_MOD,clientfd,&ev);
            }
        }
    }

epoll方法能有效的解决poll和select的一些缺陷,如fd数目限制,cpu开销过大的问题,在海量连接并发的时候起到很大作用。

总结

  • select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
  • select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

select、poll、epoll这三种都是io多路复用的用法,实际上在recv与send时还是堵塞的,本质上还是同步io,异步io的用法后续在更新

参考

https://zhuanlan.zhihu.com/p/272891398

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值