Linux中多路IO复用

首先要明白为什么要使用 多路IO复用

单进程/单线程要处理多个阻塞事件的时候会面临抉择,设置阻塞还是非阻塞呢?阻塞的话消息可能得不到及时的处理,就像排队买饭前边的饭卡丢了一堆人等他找饭卡,找到后才能接着打饭,非阻塞的话看似合理但是cpu不愿意了,大妈抡起大勺准备往你的碗里盛饭的时候你说饭卡找不到了然后大妈把饭缩回去,一个两个还好,如果大量饭卡丢了的话大妈就要发火了,实际上服务器中的连接大多数都是不活跃的,所以需要思考一种方法,让有饭卡的打饭,饭卡丢的一边呆着

简单的来说多路IO复用就是一个进程或线程同时完成多个文件描述符的监听,使用一个IO事件控制多个IO事件,它可以极大的提高进程/线程的并发性

在linux中多路IO复用可以使用select,poll,epoll来实现

select:
最早产生,甚至出现先于linux,提供便捷高效的监听,兼容性强
缺点:

  1. 轮询监听机制(效率低)
  2. 监听数量有限(1024个,大小可改但要重新编译内核)
  3. 只支持水平触发(高电平触发)
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 8000
#define IP "192.168.1.41"

struct sockaddr_in addr;

/*
实现select水平触发下的消息回射
*/

int init(){
    int lfd=socket(AF_INET,SOCK_STREAM,0);
    addr.sin_port=htons(PORT);
    addr.sin_family=AF_INET;
    inet_pton(AF_INET,IP,&addr.sin_addr);
    bind(lfd,(const struct sockaddr *)&addr,sizeof(addr));
    listen(lfd,64);
    return lfd;
}

int main(){
    socklen_t len;
    char message[1024];
    int lfd=init();
    fd_set all,cur;
    FD_ZERO(&all);
    FD_ZERO(&cur);
    FD_SET(lfd,&all);
    int nfd=lfd+1;
    while(1){
        cur=all;
        int cnt=select(nfd,&cur,NULL,NULL,NULL);
        if(cnt<0){
            perror("select监听异常");
            exit(1);
        }
        if(FD_ISSET(lfd,&cur)&&cnt--){
            len=sizeof(addr);
            int cfd=accept(lfd,(struct sockaddr *)&addr,&len);
            if(cfd<0){
                perror("接收到异常的连接");
                exit(1);
            }
            FD_SET(cfd,&all);
            if(nfd<=cfd+1) nfd=cfd+1;
            cnt--;
        }
        for(int i=lfd+1;i<nfd&&cnt;i++){
            if(FD_ISSET(i,&cur)){
                int ret=read(i,message,sizeof(message));
                if(ret<0){
                    perror("读取数据异常");
                    exit(1);
                }
                else if(!ret){
                    close(i);
                    FD_CLR(i,&all);
                }
                write(i,message,ret);
                cnt--;
            }
        }
    }
}

poll:
出现时间晚于select
和select相比仅仅是摆脱了监听数量的限制,未能解决一些实际问题
问题:

  1. 轮询监听机制
  2. 用户态和内核态间的大量数据拷贝
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAX 1000
#define PORT 8000
#define IP "192.168.1.41"

struct sockaddr_in addr;

/*
实现poll水平触发下的消息回射
*/

int init(){
    int lfd=socket(AF_INET,SOCK_STREAM,0);
    addr.sin_port=htons(PORT);
    addr.sin_family=AF_INET;
    inet_pton(AF_INET,IP,&addr.sin_addr);
    bind(lfd,(const struct sockaddr *)&addr,sizeof(addr));
    listen(lfd,64);
    return lfd;
}

int main(){
    socklen_t len;
    char message[1024];
    int lfd=init();
    struct pollfd p[MAX];
    for(int i=0;i<MAX;i++) p[i].fd=-1;
    p[0].fd=lfd;
    p[0].events=POLLIN;
    int nfd=1;
    while(1){
        int cnt=poll(p,nfd,-1);
        if(cnt<0){
            perror("poll监听异常");
            exit(1);
        }
        if(p[0].revents&POLLIN&&cnt--){
            len=sizeof(addr);
            int cfd=accept(lfd,(struct sockaddr *)&addr,&len);
            if(cfd<0){
                perror("接收到异常的连接");
                exit(1);
            }
            for(int i=1;i<MAX;i++){
                if(p[i].fd==-1){
                    p[i].fd=cfd;
                    p[i].events=POLLIN;
                    if(nfd<i+1) nfd=i+1;
                    break;
                }
            }
        }
        for(int i=1;i<nfd&&cnt;i++){
            if(p[i].fd==-1) continue;
            if(p[i].revents&POLLIN){
                int ret=read(p[i].fd,message,sizeof(message));
                if(ret<0){
                    perror("读取数据异常");
                    exit(1);
                }
                else if(!ret){
                    close(p[i].fd);
                    p[i].fd=-1;
                    p[i].events=0;
                }
                write(p[i].fd,message,ret);
                cnt--;
            }
        }
    }
}

epoll:
在内核中维护一个红黑树存储需要监听的文件描述符及其监听的事件,有事件触发时仅需要将目标文件描述符及事件拷贝到数组中传出即可
事件的触发和处理:

  • 当某个文件描述符上有感兴趣的事件发生时,触发事件,并将该文件描述符加入内核事件表的就绪队列中。

  • epoll 从就绪队列中获取已触发的文件描述符,根据红黑树找到对应的节点,并从节点中获取事件信息。

  • epoll 会将该文件描述符和触发的事件放入一个就绪事件列表中,以等待后续的处理。

优点:

  1. 监听数量没有限制
  2. 不需要用户态和内核态间进行大量的数据拷贝
  3. 不再使用轮询机制改用事件通知机制
  4. 支持水平触发和边沿触发

创建epoll时会有一个文件描述符维持红黑树的根节点,epoll使用结束的时候需要手动释放

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 8000
#define IP "192.168.1.41"

struct sockaddr_in addr;

/*
实现epoll水平触发下的消息回射
*/

int init(){
    int lfd=socket(AF_INET,SOCK_STREAM,0);
    addr.sin_port=htons(PORT);
    addr.sin_family=AF_INET;
    inet_pton(AF_INET,IP,&addr.sin_addr);
    bind(lfd,(const struct sockaddr *)&addr,sizeof(addr));
    listen(lfd,64);
    return lfd;
}

int main(){
    socklen_t len;
    char message[1024];
    int lfd=init();
    struct epoll_event event,events[1024];
    event.data.fd=lfd;
    event.events=EPOLLIN;
    int epoll=epoll_create(100);
    epoll_ctl(epoll,EPOLL_CTL_ADD,lfd,&event);
    while(1){
        int cnt=epoll_wait(epoll,events,sizeof(events)/sizeof(event),-1);
        if(cnt<0) {
            perror("epoll监听异常");
            exit(-1);
        }
        for(int i=0;i<cnt;i++){
            int fd=events[i].data.fd;
            if(fd==lfd){
                len=sizeof(addr);
                int cfd=accept(fd,(struct sockaddr *)&addr,&len);
                if(cfd<0) {
                    perror("接收到异常的连接请求");
                    exit(-1);
                }
                event.data.fd=cfd;
                event.events=EPOLLIN;
                epoll_ctl(epoll,EPOLL_CTL_ADD,cfd,&event);
            }
            else{
                int ret=read(fd,message,sizeof(message));
                if(ret<0) {
                    perror("读取数据异常");
                    exit(-1);
                }
                else if(!ret){
                    close(fd);
                    epoll_ctl(epoll,EPOLL_CTL_DEL,fd,NULL);
                }
                write(fd,message,ret);
            }
        }
    }
}

点击此处

你可能感兴趣的:(linux,运维,多路IO复用)