linux网络编程 IO多路复用 select epoll

本文以我的小型聊天室为例,对于服务器端的代码,做了三次改进,我将分别介绍阻塞式IO,select,epoll .

一:阻塞式IO

对于聊天室这种程序,我们最容易想到的是在服务器端accept之后,然后fork一个进程或者pthread_create创建一个线程去处理相应的连接,代码如下 :

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

#define PORT  8888
#define MAX_QUEEN_LENGTH 5000

void my_err(const char* msg,int line) 
{
    fprintf(stderr,"line:%d",line);
    perror(msg);
}


int main(int argc,char *argv[])
{
    int i;
    int conn_len;      
    int sock_fd,conn_fd;
    struct sockaddr_in serv_addr,conn_addr;
    char recv_buf[1024];
    int pid;

    if((sock_fd = socket(AF_INET,SOCK_STREAM,0)) == -1) { //第三个参数的意思为0表示自动选择与第二个参数对应的协议.
        my_err("socket",__LINE__); 
        exit(1);
    }

    memset(&serv_addr,0,sizeof(struct sockaddr_in));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) == -1) {
        my_err("bind",__LINE__);
        exit(1);
    }

    if(listen(sock_fd,MAX_QUEEN_LENGTH) == -1) {
        my_err("sock",__LINE__);
        exit(1);
    }

    conn_len = sizeof(struct sockaddr_in);

    while(1) {
        conn_fd = accept(sock_fd,(struct sockaddr *)&conn_addr,&conn_len);
        pid = fork();
        if(pid == 0) {
           //有关conn_fd的操作
        }
    }

    return 0;
}

问题:
当连接变的较多时,我们创建进程或者线程的开销很大,并且系统在不同的线程之间切换处理也非常的耗费资源.因此我们想少开线程.于是,select就是很好的选择,我们可以在一个线程内监控多个文件描述符的状态,下面我们来学习select.

二:select

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

//参数解释
nfds:是select监控的三个文件描述符集合中所包括的最大文件描述符加一
readfds:fd_set类型,用来检测输入是否就绪的文件描述符集合
writefds:同readfds,是用来检测输出是否就绪的文件描述符集合
exceptfds:检测异常情况是否发生文件描述符集合  
timeout:设置select的等待时间,如果两个域都为0,则select不会阻塞.
struct timeval {
    time_t       tv_sec;
    suseconds_t  tv_usec;
};

//有关select监控文件描述符集合的操作
void FD_ZERO(fd_set *fdset);   //清空集合   
void FD_SET(int fd,fd_set *fdset);  //将fd加入fdset集合中 
void FD_CLR(int fd,fd_set *fdset);  //将fd从fdset中清除
int FD_ISSET(int fd,fd_set *fdset); //判断fd是否在集合中

下面我们看有关select的代码

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

#define PORT  8888
#define MAX_QUEEN_LENGTH 5000

void my_err(const char* msg,int line) 
{
    fprintf(stderr,"line:%d",line);
    perror(msg);
}


int main(int argc,char *argv[])
{
    int i;
    int conn_len;        //
    int sock_fd,conn_fd;
    struct sockaddr_in serv_addr,conn_addr;
    char recv_buf[1024];
    int pid;
    struct timeval tv;   //select第5个参数,可以确定轮询检查的时间
    fd_set rfds;         //select的检查集合
    int conn_fd_array[MAX_QUEEN_LENGTH] = {0};  //客户端连接fd数组
    int conn_amount = 0; //目前客户端连接的数量
    int maxsock,ret;     //maxsock是select的第一个参数

    if((sock_fd = socket(AF_INET,SOCK_STREAM,0)) == -1) { //第三个参数的意思为0表示自动选择与第二个参数对应的协议.
        my_err("socket",__LINE__); 
        exit(1);
    }

    memset(&serv_addr,0,sizeof(struct sockaddr_in));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) == -1) {
        my_err("bind",__LINE__);
        exit(1);
    }

    if(listen(sock_fd,MAX_QUEEN_LENGTH) == -1) {
        my_err("sock",__LINE__);
        exit(1);
    }

    conn_len = sizeof(struct sockaddr_in);
    maxsock = sock_fd;  //目前的maxsock就是listen socket

    while(1) {
        FD_ZERO(&rfds); //清空集合,因为select每次都需要重新检查,所以清空操作每次都需要
        FD_SET(sock_fd,&rfds);   //将listen socket加入rfds集合中
        tv.tv_sec = 1;         //设置时间为1s,即1s后无事件发生就返回              
        tv.tv_usec = 0;

        for(i = 0;i < MAX_QUEEN_LENGTH;i++) {
            if(conn_fd_array[i] != 0) { 
                FD_SET(conn_fd_array[i],&rfds); //将所有都设置
            }
        }

        ret = select(maxsock+1,&rfds,NULL,NULL,&tv); 
        if(ret < 0) {
            my_err("select",__LINE__);
            exit(1);
        } 

        for(i = 0;i < conn_amount;i++) { //conn_amount:连接数量
            if(FD_ISSET(conn_fd_array[i],&rfds)) {
                if(recv(conn_fd_array[i],recv_buf,1024,0) != 1024) {
                    close(conn_fd_array[i]);        //关闭文件描述符
                    FD_CLR(conn_fd_array[i],&rfds); //从rfds中清除
                    conn_fd_array[i] = 0;           //让fd数组中归零
                } else {
                    printf("%s\n",recv_buf);
                }
            }
        }
        if(FD_ISSET(sock_fd,&rfds)) {  //如果有新的连接
            if((conn_fd = accept(sock_fd,(struct sockaddr *)&conn_addr,&conn_len)) <= 0 ) {
                my_err("accept",__LINE__);
                exit(1);
            }

            for(i = 0;i < MAX_QUEEN_LENGTH;i++) { //实现了conn_fd_array[i]中元素重复使用
                if(conn_fd_array[i] == 0) {
                    conn_fd_array[i] = conn_fd;
                    break;
                }
            }
            conn_amount++;
            if(conn_fd > maxsock) {  //始终保证传给select的第一个参数是maxsock+1
                maxsock = conn_fd;
            }
        }
    }

    return 0;
}

问题:
select虽然减小了开销,但是由于文件描述符集合有一个最大容量限制,由常量FD_SETSIZE来限制,在linux上,此值一般为1024,我们无法轻易的修改它,因此select的连接限制一般就是1024以下,这显然是不够用的,因此对于需要大量连接,需要大量文件描述符的情况,epoll更加有用.

三:epoll

epoll的核心数据结构是epoll实例,它和一个打开的文件描述符相关联,这个文件描述符不是用来做IO操作的,它是内核数据结构的句柄.epoll主要有下面三个API

#include<epoll.h>

int epoll_create(int size); 
参数:size指定了我们想要通过epoll案例来检查的文件描述符个数  
返回值:代表创建的epoll实例的文件描述符  

int epoll_ctl(int epfd,int op,int fd,struct epoll *ev);
epfd:是epoll_create函数返回的代表epoll实例的文件描述符  

op:定义了需要执行的操作
EPOLL_CTL_ADD:将描述符fd添加到epoll实例epfd的兴趣列表中去.
EPOLL_CTL_MOD:修改描述符fd上设定的事件,需要用到由ev所指向的结构体的信息.
EPOLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中删除.

events:定义了下面的操作  
EPOLLIN:读操作  
EPOLLOUT:写操作
EPOLLRDHUP:套接字对端关闭  
EPOLLLONESHOT:在完成事件通知之后禁用检查

//event的结构体
struct epoll_event {
    uint32_t    events;
    epoll_data_t data;
};
typedef union epoll_data {
    void *ptr;
    int  fd;                //文件描述符
    unit32_t u32;
    unit64_t u64;
}epoll_data_t;

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

epfd:是epoll_create函数返回的代表epoll实例的文件描述符  
evlist:所指向的结构体数组中返回的是有关就绪状态文件描述符的信息
maxevents:最大的连接数  
timeout: 
  -1:调用将一直被阻塞,直到兴趣列表中文件描述符上有事件发生.
  0:执行一次非阻塞的检查,看兴趣列表中文件描述符上产生了哪个事件
  >0:阻塞最多timeout毫秒,直到文件描述符上有事件发生.

下面我们看epoll的使用代码

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

#define PORT 8888
#define MAX_LENGTH 1024

void my_err(const char* msg,int line)
{
    fprintf(stderr,"line:%d",line);
    perror(msg);
    exit(1);
}

int main(int argc,char *argv[])
{
    int i;
    int listen_fd,conn_fd,sock_fd;
    struct sockaddr_in serv_addr,conn_addr;
    struct epoll_event  ev,events[MAX_LENGTH] = {-1};
    int nfds,epollfd;
    int conn_len = sizeof(struct sockaddr_in);
    char recv_buf[1024];

    if((listen_fd = socket(AF_INET,SOCK_STREAM,0)) == -1) {
        my_err("socket",__LINE__);
    }

    memset(&serv_addr,0,sizeof(struct sockaddr_in));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(listen_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) == -1) {
        my_err("bind",__LINE__);
    }
    if(listen(listen_fd,MAX_LENGTH) == -1) {
        my_err("listen",__LINE__);
    }

    if((epollfd = epoll_create(MAX_LENGTH)) == -1) { //创建一个epoll实例
        my_err("epoll",__LINE__);
    }
    ev.events = EPOLLIN;   //设置事件为读
    ev.data.fd = listen_fd; //设置文件描述符为监听套接字
    epoll_ctl(epollfd,EPOLL_CTL_ADD,listen_fd,&ev); .//将监听套接字加入epollfd
    while(1) {
        nfds = epoll_wait(epollfd,events,1024,500); //event是一个数组
        for(i = 0;i < nfds;i++) {
            if(events[i].data.fd == listen_fd) {           //连接请求
                conn_fd = accept(listen_fd,(struct sockaddr *)&conn_addr,&conn_len);
                printf("accept a new collection : %s\n",inet_ntoa(conn_addr.sin_addr));
                ev.data.fd = conn_fd;
                ev.events = EPOLLIN;
                epoll_ctl(epollfd,EPOLL_CTL_ADD,conn_fd,&ev);
            }
            else if(events[i].events & EPOLLIN) {        //读事件
                if((sock_fd = events[i].data.fd) < 0 ) {
                    continue;
                }
                if((i = recv(sock_fd,recv_buf,1024,0)) != 1024) {
                    close(sock_fd);
                    events[i].data.fd = -1;
                } else {
                    printf("%s\n",recv_buf);
                }
                ev.data.fd = sock_fd;
                ev.events = EPOLLOUT;
                epoll_ctl(epollfd,EPOLL_CTL_MOD,sock_fd,&ev);
            }
            else if(events[i].events & EPOLLOUT) {       //写事件
                sock_fd = events[i].data.fd;
                ev.data.fd = sock_fd;
                ev.events = EPOLLIN;                                           epoll_ctl(epollfd,EPOLL_CTL_MOD,sock_fd,&ev);
            }
        }
    }
    return 0;
}

下面我们看下epoll和select的性能对比表(来自Linux/Unix 系统编程手册)

被监视的文件描述符数量select占用CPU时间(秒)epoll占用CPU时间(秒)
100.730.41
1003.00.42
1000350.53
100009300.66

因此我们不难看出epoll的强大,它更加适合需要同时处理许多客户端的服务器,特别是需要监控的文件描述符数量巨大,但是大多数处于空闲状态,只有少部分处于就绪状态.
select和epoll性能差别原因:

  • 每次调用select,内核都必须检查所有被指定的文件描述符,当大量检查时,耗费时间大.
  • 每次调用select,程序都必须传递一个表示所有被检查的文件描述符到内核,内核通过检查文件描述符后,修改这个数据结构返回给程序,但是内核态和用户态之间切换的效率非常低.
  • select完成之后,之后的程序必须检查返回的数据结构中的每一个元素.这样每次循环消耗非常大.
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨博东的博客

请我喝瓶可乐鼓励下~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值