首页 > 其他分享 >IO多路复用

IO多路复用

时间:2023-07-02 17:24:15浏览次数:50  
标签:struct 多路复用 epoll int fd IO poll events

IO多路复用

IO 多路复用 即 用一个线程监视多个文件句柄,句柄没有就绪时会阻塞应用程序,从而释放 CPU 资源,否则当句柄就绪,能通知到对应程序进行读写操作

  • IO:在操作系统中,数据在内核态和用户态之间的读写操作(大部分情况下指网络 IO
  • 多路:一般指多个 TCP 连接
  • 复用:一个或多个线程资源
  • 整合 IO 多路复用:一个或多个线程处理多个 TCP 连接,无需创建和维护过多的进程或线程

常用的 IO 多路控制方法有 select​、poll ​和 epoll ​三种,三者对比如下,其中 epoll ​性能最好。

image

  • select(轮询 + 遍历):调用 select 会阻塞进程,直到有 fd 就绪。优点:跨平台支持性好;缺点:效率低下,每次都需从用户空间到内核空间拷贝 fd 数组集合(一般单个进程最大 1024,可通过),就绪后仍需轮询

    • 客户端操作服务器时会创建三种文件描述符,分别是 写描述符、读描述符 和 异常描述符,阻塞进程,等数据可读、可写或者出异常、超时的时候都会在内核空间返回,返回后需要在用户空间遍历 文件描述符集合 fdset,找到就绪的 fd,从而出发对应的 IO 操作
  • poll(轮询 + 遍历):同样阻塞,链表方式存储 fd,优点:无最大连接数限制;缺点:fd 越多效率越低

  • epoll:使用红黑树(平衡二叉树)维护 fd,每个 fd 从用户态拷贝到内核态仅需一次(epoll_ctl 时拷贝),优点:将轮询改成了回调,不会随 fd 数量增加导致效率下降;缺点:只能 linux 下环境使用

应用程序使用 poll 示例

image

  • int poll(struct pollfd fds[], nfds_t nfds, int timeout);

    • 参数说明:

      • fds:存放需要检测其状态的文件描述符集;

      • nfds:用于标记数组 fds 中的结构体元素的总数量;

      • timeout:是 poll 函数调用阻塞的时间,单位:毫秒;

        • 如果 timeout==0,那么 poll() 立即返回而不阻塞;设置为负数,poll() 会一直阻塞下去,直到所检测的文件描述符上的感兴趣的事件发生时才返回。
    • 返回值:

      • >0:数组 fds 中准备好读、写或出错状态的那些文件描述符的总数量
      • ==0:此时 poll 超时
      • -1: poll 函数调用失败,同时会自动设置全局变量 errno
驱动中如何实现 poll 方法

应用程序调用 poll()时,内核中会调用每个设备驱动中的 poll 函数,这些底层函数都会调用 poll_wait(),将本设备驱动中的等待队列添加到一个等待队列表中(table),然后判断是否有数据发生,有的话返回一个非零值,没有返回 0.

核心:poll_wait 函数

图例

image

poll 系统调用在内核中的入口函数是 sys_poll();

EPOLL 原理剖析

为什么 epoll 高效

  • 内部使用了红黑树结构管理 fd,查询和增删的时间复杂度 O(logn),实现增删改之后性能的优化和平衡;
  • epoll 池添加 fd 的时候,设置 file_operations->poll,把这个 fd 就绪之后的回调路径安排好。通过事件通知的形式,做到最高效的运行;
  • fd 就绪后其相关结构体(epitem)统一存放在就绪队列,epoll 池处理 fd 时只需遍历就绪链表即可

epoll 触发模式

epoll 支持的事件触发模式有:

  • 水平触发 LT:当有可读事件发生时,服务器不断从 epoll_wait ​中苏醒,直到内核缓冲区的数据被读完
  • 边缘触发 ET:只在事件状态由不可用到可用时苏醒一次(必须搭配非阻塞式 socket 使用),程序需保证一次性将内核缓冲区的数据处理完

epoll 回调机制

poll 事件回调机制则是 epoll 池高效最核心原理。

结构体 struct file_operations ​代表文件调用,文件最基本操作都是以这个框架为基础实现的。

struct file_operations {  
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);  
    __poll_t (*poll) (struct file *, struct poll_table_struct *);  
    int (*open) (struct inode *, struct file *);  
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);  
    // ....  
};

file_operations->poll​ 是定制监听事件的机制实现。通过 poll 机制让上层能直接告诉底层,我这个 fd 一旦读写就绪了,请底层硬件(比如网卡)回调的时候自动把这个 fd 相关的结构体(epitem)放到指定队列中,并且唤醒操作系统。

举个例子:网卡收发包其实走的异步流程,操作系统把数据丢到一个指定地点,网卡不断的从这个指定地点掏数据处理。请求响应通过中断回调来处理,中断一般拆分成两部分:硬中断和软中断。poll 函数就是把这个软中断回来的路上再加点料,只要读写事件触发的时候,就会立马通知到上层,采用这种事件通知的形式就能把浪费的时间窗就完全消失了。

因此 epoll 池管理的句柄只能是支持了 file_operations->poll​ 的文件 fd,如 socket fd,eventfd,timerfd 等。

使用方法

1、创建 epoll 池

epollcreate​ 负责创建一个池子,一个监控和管理句柄 fd 的池子;

原型

int epoll_create(int size); // 其中参数size已被抛弃,赋值为>=0的值即可

int epoll_create1 (int __flags) // 若flags为0,与上同;
// 否则当包含EPOLL_CLOEXEC等值时,在文件描述符上面设置执行时关闭(FD_CLOEXEC)标志描述符。

执行成功时返回非负文件描述符,失败返回-1,并且将 errno 设置为指示错误

示例

int epfd = epoll_create1(0);

errif(epfd == -1, "epoll create error"); // 定义如下

void errif(bool condition, const char *errmsg) {
    if (condition) {
        perror(errmsg); // 输出错误原因,errmsg先打印,后加上错误原因字符串
        exit(EXIT_FAILURE);
    }
}

2、管理 epoll 池

epollctl​ 负责管理这个池子里的 fd 增、删、改;

原型

int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event);

op 参数说明操作类型:

  • EPOLL_CTL_ADD:添加一个需要监视的描述符
  • EPOLL_CTL_DEL:删除一个描述符
  • EPOLL_CTL_MOD:修改一个描述符

使用

struct epoll_event events[MAX_EVENTS], ev;
bzero(&events, sizeof(events));
bzero(&ev, sizeof(ev));

ev.data.fd = sockfd; // sockfd 由socket创建而来
ev.events = EPOLLIN | EPOLLET; // 监听可读事件;边缘模式ET触发(fd需非阻塞)
setnonblocking(sockfd); // 设置不堵塞,函数定义如下
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // EPOLL_CTL_ADD表添加

// 设置fd为非阻塞模式
void setnonblocking(int fd) {
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); // 先用fcntl(fd, F_GETFL)获取原先状态再设置
}

3、监听 epoll 池

epollwait​ 就是负责打盹的,让出 CPU 调度,但是只要有“事”,立马会从这里唤醒;

原型

int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);

其中 events 是一个 epoll_event 结构体数组,maxevents 是可供返回的最大事件大小,一般是 events 的大小,timeout 表示最大等待时间,设置为-1 表示一直等待。

返回就绪 fd 的个数,无需像 select/poll 一样轮询扫描整个 socket 集合,大大提高检测效率

实现

int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
errif(nfds == -1, "epoll wait error");
for (int i = 0; i < nfds; i++) {
    // 对就绪句柄的处理
}

4、对句柄的处理

在边缘触发模式中,需配合非阻塞的读写函数,因此需对错误码进行处理

  • socket 是阻塞模式时,继续调用 send/recv 函数,程序会阻塞在 send/recv 调用处。
  • 当 socket 是非阻塞模式时,将立即出错并返回,会得到一个相关的错误码,在 Linux 上错误码为 EWOULDBLOCK 或 EAGAIN

Linux 中系统调用的错误都存储于 errno 中,其记录系统的最后一次错误代码。

下文代码是服务器对就绪 fd 集合的处理,他能连接新客户端并转发客户端发的内容。

// 使用了相关自定义类
while(true){
        std::vector<epoll_event> events = ep->poll();
        int nfds = events.size();
        for(int i = 0; i < nfds; ++i){
      
            if(events[i].data.fd == serv_sock->getFd()){        //新客户端连接
                InetAddress *clnt_addr = new InetAddress();  
                Socket *clnt_sock = new Socket(serv_sock->accept(clnt_addr));   
                printf("new client fd %d! IP: %s Port: %d\n", clnt_sock->getFd(), inet_ntoa(clnt_addr->addr.sin_addr), ntohs(clnt_addr->addr.sin_port));
                clnt_sock->setnonblocking();
                ep->addFd(clnt_sock->getFd(), EPOLLIN | EPOLLET); // 将新客户端划入epoll池
              
            } else if(events[i].events & EPOLLIN){      //可读事件
                handleReadEvent(events[i].data.fd);
              
            } else{         //其他事件
                printf("something else happened\n");
            }
        }
    }

对读事件的处理

void handleReadEvent(int sockfd){
    char buf[READ_BUFFER];
    while(true){    //由于使用非阻塞IO,读取客户端buffer,一次读取buf大小数据,直到全部读取完毕
        bzero(&buf, sizeof(buf));
        ssize_t bytes_read = read(sockfd, buf, sizeof(buf));
        if(bytes_read > 0){
            printf("message from client fd %d: %s\n", sockfd, buf);
            write(sockfd, buf, sizeof(buf));
        } else if(bytes_read == -1 && errno == EINTR){  //客户端正常中断、继续读取
            printf("continue reading");
            continue;
        } else if(bytes_read == -1 && ((errno == EAGAIN) || (errno == EWOULDBLOCK))){//非阻塞IO,这个条件表示数据全部读取完毕
            printf("finish reading once, errno: %d\n", errno);
            break;
        } else if(bytes_read == 0){  //EOF,客户端断开连接
            printf("EOF, client fd %d disconnected\n", sockfd);
            close(sockfd);   //关闭socket会自动将文件描述符从epoll树上移除
            break;
        }
    }
}

Reference

谈谈你对 IO 多路复用的理解,全面从 select,poll,epoll 来进行综合对比,让你 offer 拿到手软!【Java 面试】_哔哩哔哩_bilibili

FD_CLOEXEC 详解_bemf168 的博客-CSDN 博客

深入理解 Linux 的 epoll 机制 (qq.com)

作为 C++ 程序员,应该彻底搞懂 epoll 高效运行的原理 - 知乎 (zhihu.com)

day03-高并发还得用 epoll | csblog

网络编程:socket 的阻塞模式和非阻塞模式_socket 非阻塞模式__索伦的博客-CSDN 博客

epoll 的 LT 模式(水平触发)和 ET 模式(边沿触发)_epollet_AlbertS 的博客-CSDN 博客

标签:struct,多路复用,epoll,int,fd,IO,poll,events
From: https://www.cnblogs.com/walton/p/io-multi-road-reuse-1kpuwl.html

相关文章

  • IOS开发-NSUserDefaults的基本使用,缓存数据实现数据持久化
    NSUserDefaults是iOS与macOS中的一个存储对象。它用于存储应用程序运行期间和退出后需要保存的数据。NSUserDefaults的特点:-基于键值对:使用字符串作为键名存储数据。-支持的类型:NSString、NSNumber、NSDate、NSArray、NSDictionary等基本数据结构。-存储在本地:数据存储......
  • IOS开发-常用的输出类型总结
    int类型%d或者%ichar型%cfloat型%f(具体限制输出的位数,根据c语言中的规则)oc对象%@(BOOL类型也可以使用%@输出,一般情况不直接输出BOOL的值)NSUInteger类型%lu(该类型是无符号整型,相当于unsignedlong) 举例:intn=1;NSLog(@"%i",n);NSLog(@“%d”,n); char......
  • IOS开发-iOS中电话号码和邮箱判断
    在iOS应用中我们经常会遇到一些个人信息的设置,需要判断用户输入的信息是否正确,电话号码和邮箱等是否符合要求,下面直接上代码:1,判断字符串是否是电话号码:-(BOOL)checkTelNumber:(NSString*)telNumber{NSString*pattern=@"^1+[3578]+\\d{9}";NSPredicate*pred=[N......
  • eclipse new creation file type
    ......
  • Definition of 'Cash Settlement( versus physical delivery of the reference obliga
    Definitionof'CashSettlement(versus physicaldeliveryofthereferenceobligation)Asettlementmethodusedincertainfutureandoptioncontractswhereby,uponexpiryorexercise,thesellerofthefinancialinstrumentdoesnotdelivertheactual......
  • IOS开发-使用UIImageView加载网络图片
    使用UIImageView加载网络图片可以分为三步1.创建UIImageView实例:UIImageView*imgview=[[UIImageViewalloc]init];imgview.frame=CGRectMake((self.view.frame.size.width-100)/2,(self.view.frame.size.height-100)/2,100,100); 2.下载图片数据:NSUR......
  • IOS开发-UIImageView基本用法
    UIImageView是iOS中用于显示图像(图片、gif、svg等)的视图。它的主要功能有:1.显示图片UIImageView可以通过image属性显示一张UIImage类型的图片。可以是本地图片、从网络下载的图片等。2.设置填充模式可以通过contentMode属性设置图片在UIImageView内的显示和填充模式。内容......
  • @Transactional失效——同一个类中方法调用导致,解决方案!
    同一个类中方法调用,导致@Transactional失效开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错......
  • 数列分段 Section II
    数列分段SectionII题目描述对于给定的一个长度为N的正整数数列\(A_{1\simN}\),现要将其分成\(M\)(\(M\leqN\))段,并要求每段连续,且每段和的最大值最小。关于最大值最小:例如一数列\(4\2\4\5\1\)要分成\(3\)段。将其如下分段:\[[4\2][4\5][1]\]第一段和为\(6\),第......
  • IOS开发-UIColor的基本用法
    1.使用预定义的系统颜色UIKit框架内预定义了一些常用的颜色,我们可以直接使用:UIColor*redColor=[UIColorredColor];UIColor*blueColor=[UIColorblueColor];UIColor*greenColor=[UIColorgreenColor];UIColor*blackColor=[UIColorblackColor];UIColor......