Linux中多路IO复用

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

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

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

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

select:

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

缺点:

  1. 轮询监听机制(效率低)
  2. 监听数量有限(1024个,大小可改但要重新编译内核)
  3. 只支持水平触发(高电平触发)
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <errno.h>
#include <sys/time.h>

#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. 用户态和内核态间的大量数据拷贝
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include <sys/time.h>

#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使用结束的时候需要手动释放

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>

#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);
            }
        }
    }
}

点击此处

相关推荐
Ven%11 分钟前
centos查看硬盘资源使用情况命令大全
linux·运维·centos
萨格拉斯救世主1 小时前
戴尔R930服务器增加 Intel X710-DA2双万兆光口含模块
运维·服务器
Jtti1 小时前
Windows系统服务器怎么设置远程连接?详细步骤
运维·服务器·windows
TeYiToKu1 小时前
笔记整理—linux驱动开发部分(9)framebuffer驱动框架
linux·c语言·arm开发·驱动开发·笔记·嵌入式硬件·arm
dsywws1 小时前
Linux学习笔记之时间日期和查找和解压缩指令
linux·笔记·学习
yeyuningzi1 小时前
Debian 12环境里部署nginx步骤记录
linux·运维·服务器
上辈子杀猪这辈子学IT2 小时前
【Zookeeper集群搭建】安装zookeeper、zookeeper集群配置、zookeeper启动与关闭、zookeeper的shell命令操作
linux·hadoop·zookeeper·centos·debian
minihuabei2 小时前
linux centos 安装redis
linux·redis·centos
EasyCVR2 小时前
萤石设备视频接入平台EasyCVR多品牌摄像机视频平台海康ehome平台(ISUP)接入EasyCVR不在线如何排查?
运维·服务器·网络·人工智能·ffmpeg·音视频
lldhsds3 小时前
书生大模型实战营第四期-入门岛-1. Linux前置基础
linux