【Linux高级 I/O(2)】如何使用阻塞 I/O 与非阻塞 I/O?——select()函数

        上次我们虽然使用非阻塞式 I/O 解决了阻塞式 I/O 情况下并发读取文件所出现的问题,但依然不够完美,使得程序的 CPU 占用率特别高。解决这个问题,就要用到本文将要介绍的 I/O 多路复用方法。

何为 I/O 多路复用

        I/O 多路复用(IO multiplexing)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行 I/O 操作时,能够通知应用程序进行相应的读写操作。I/O 多路复用技术是为了解决:在并发式 I/O 场景中进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的 I/O 系统调用。

        由此可知,I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞 I/O,譬如程序中既要读取鼠标、又要读取键盘,多路读取。

        我们可以采用两个功能几乎相同的系统调用来执行 I/O 多路复用操作,分别是系统调用 select()和 poll()。 这两个函数基本是一样的,细节特征上存在些许差别!

        I/O 多路复用存在一个非常明显的特征:外部阻塞式,内部监视多路 I/O。

select()函数介绍

        系统调用 select()可用于执行 I/O 多路复用操作,调用 select()会一直阻塞,直到某一个或多个文件描述符成为就绪态(可以读或写)。其函数原型如下所示:

#include <sys/select.h>

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

        可以看出 select()函数的参数比较多,其中参数 readfds、writefds 以及 exceptfds 都是 fd_set 类型指针, 指向一个 fd_set 类型对象,fd_set 数据类型是一个文件描述符的集合体,所以参数 readfds、writefds 以及 exceptfds 都是指向文件描述符集合的指针,这些参数按照如下方式使用:

        ⚫ readfds 是用来检测读是否就绪(是否可读)的文件描述符集合;

        ⚫ writefds 是用来检测写是否就绪(是否可写)的文件描述符集合;

        ⚫ exceptfds 是用来检测异常情况是否发生的文件描述符集合。

        Tips:异常情况并不是在文件描述符上出现了一些错误。

        fd_set 数据类型是以位掩码的形式来实现的,但是,我们并不需要关心这些细节、无需关心该结构体成员信息,因为 Linux 提供了四个宏用于对 fd_set 类型对象进行操作,所有关于文件描述符集合的操作都是通过这四个宏来完成的:FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO (),稍后介绍!

        如果对 readfds、writefds 以及 exceptfds 中的某些事件不感兴趣,可将其设置为 NULL,这表示对相应条件不关心。如果这三个参数都设置为 NULL,则可以将 select()当做为一个类似于 sleep()休眠的函数来使用, 通过 select()函数的最后一个参数 timeout 来设置休眠时间。

        select()函数的第一个参数 nfds 通常表示最大文件描述符编号值加 1,考虑 readfds、writefds 以及 exceptfds 这三个文件描述符集合,在 3 个描述符集中找出最大描述符编号值,然后加 1,这就是参数nfds。

        select()函数的最后一个参数 timeout 可用于设定 select()阻塞的时间上限,控制 select 的阻塞行为,可将 timeout 参数设置为 NULL,表示 select()将会一直阻塞、直到某一个或多个文件描述符成为就绪态;也可将其指向一个 struct timeval 结构体对象。

        如果参数 timeout 指向的 struct timeval 结构体对象中的两个成员变量都为 0,那么此时 select()函数不会阻塞,它只是简单地轮训指定的文件描述符集合,看看其中是否有就绪的文件描述符并立刻返回。否则,参数 timeout 将为 select()指定一个等待(阻塞)时间的上限值,如果在阻塞期间内,文件描述符集合中的某一个或多个文件描述符成为就绪态,将会结束阻塞并返回;如果超过了阻塞时间的上限值,select()函数将会返回!

        select()函数将阻塞知道有以下事情发生:

        ⚫ readfds、writefds 或 exceptfds 指定的文件描述符中至少有一个称为就绪态;

        ⚫ 该调用被信号处理函数中断;

        ⚫ 参数 timeout 中指定的时间上限已经超时。

        FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO()

        文件描述符集合的所有操作都可以通过这四个宏来完成,这些宏定义如下所示:

#include <sys/select.h>

void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

        这些宏按照如下方式工作:

        ⚫ FD_ZERO()将参数 set 所指向的集合初始化为空;

        ⚫ FD_SET()将文件描述符 fd 添加到参数 set 所指向的集合中;

        ⚫ FD_CLR()将文件描述符 fd 从参数 set 所指向的集合中移除;

        ⚫ 如果文件描述符 fd 是参数 set 所指向的集合中的成员,则 FD_ISSET()返回 true,否则返回 false。

        文件描述符集合有一个最大容量限制,有常量 FD_SETSIZE 来决定,在 Linux 系统下,该常量的值为 1024。在定义一个文件描述符集合之后,必须用 FD_ZERO()宏将其进行初始化操作,然后再向集合中添加我们关心的各个文件描述符,例如:

fd_set fset; //定义文件描述符集合

FD_ZERO(&fset); //将集合初始化为空
FD_SET(3, &fset); //向集合中添加文件描述符 3
FD_SET(4, &fset); //向集合中添加文件描述符 4
FD_SET(5, &fset); //向集合中添加文件描述符 5

        在调用 select()函数之后,select()函数内部会修改 readfds、writefds、exceptfds 这些集合,当 select()函数返回时,它们包含的就是已处于就绪态的文件描述符集合了。譬如在调用 select()函数之前,readfds 所指向的集合中包含了 3、4、5 这三个文件描述符,当调用 select()函数之后,假设 select()返回时,只有文件描述符 4 已经处于就绪态了,那么此时 readfds 指向的集合中就只包含了文件描述符 4。所以由此可知,如果要在循环中重复调用 select(),我们必须保证每次都要重新初始化并设置 readfds、writefds、exceptfds 这些集合。

        select()函数的返回值

        select()函数有三种可能的返回值,会返回如下三种情况中的一种:

       ⚫ 返回-1 表示有错误发生,并且会设置 errno。可能的错误码包括 EBADF、EINTR、EINVAL、EINVAL 以及 ENOMEM,EBADF 表示 readfds、writefds 或 exceptfds 中有一个文件描述符是非法的;EINTR 表示该函数被信号处理函数中断了,其它错误大家可以自己去看,在 man 手册都有相应的记录。

        ⚫ 返回 0 表示在任何文件描述符成为就绪态之前 select()调用已经超时,在这种情况下,readfds, writefds 以及 exceptfds 所指向的文件描述符集合都会被清空。

        ⚫ 返回一个正整数表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符的个数,在这种情况下,每个返回的文件描述符集合都需要检查,通过 FD_ISSET()宏进行检查, 以此找出发生的 I/O 事件是什么。如果同一个文件描述符在 readfds,writefds 以及 exceptfds 中同时被指定,且它多于多个 I/O 事件都处于就绪态的话,那么就会被统计多次,换句话说,select()返回三个集合中被标记为就绪态的文件描述符的总数。

        使用示例

        示例代码演示了使用 select()函数来实现 I/O 多路复用操作,同时读取键盘和鼠标。程序中将鼠标和键盘配置为非阻塞 I/O 方式,本程序对数据进行了 5 次读取,通过 while 循环来实现。由于在 while 循环中会重复调用 select()函数,所以每次调用之前需要对 rdfds 进行初始化以及添加鼠标和键盘对应的文件描述符。

        该程序中,select()函数的参数 timeout 被设置为 NULL,并且我们只关心鼠标或键盘是否有数据可读, 所以将参数 writefds 和 exceptfds 也设置为 NULL。执行 select()函数时,如果鼠标和键盘均无数据可读,则 select()调用会陷入阻塞,直到发生输入事件(鼠标移动、键盘上的按键按下或松开)才会返回。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>

#define MOUSE "/dev/input/event3"

int main(void){
    char buf[100];
    int fd, ret = 0, flag;
    fd_set rdfds;
    int loops = 5;
    /* 打开鼠标设备文件 */
    fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }
    /* 将键盘设置为非阻塞方式 */
    flag = fcntl(0, F_GETFL); //先获取原来的 flag
    flag |= O_NONBLOCK; //将 O_NONBLOCK 标准添加到 flag
    fcntl(0, F_SETFL, flag); //重新设置 flag
    /* 同时读取键盘和鼠标 */
    while (loops--) {
        FD_ZERO(&rdfds);
        FD_SET(0, &rdfds); //添加键盘
        FD_SET(fd, &rdfds); //添加鼠标
        ret = select(fd + 1, &rdfds, NULL, NULL, NULL);
        if (0 > ret) {
            perror("select error");
            goto out;
        }
        else if (0 == ret) {
            fprintf(stderr, "select timeout.\n");
            continue;
        }
        /* 检查键盘是否为就绪态 */
        if(FD_ISSET(0, &rdfds)) {
            ret = read(0, buf, sizeof(buf));
            if (0 < ret)
                printf("键盘: 成功读取<%d>个字节数据\n", ret);
        }
        /* 检查鼠标是否为就绪态 */
        if(FD_ISSET(fd, &rdfds)) {
            ret = read(fd, buf, sizeof(buf));
            if (0 < ret)
                printf("鼠标: 成功读取<%d>个字节数据\n", ret);
        }
    }
    out:
    /* 关闭文件 */
    close(fd);
    exit(ret);
}

        程序中分析 select()函数的返回值 ret,只有当 ret 大于 0 时才表示有文件描述符处于就绪态,并将这些处于就绪态的文件描述符通过 rdfds 集合返回出来,程序中使用 FD_ISSET()宏检查返回的 rdfds 集合中是否包含鼠标文件描述符以及键盘文件描述符,如果包含则表示可以读取数据了。 编译运行:

         代码将鼠标和键盘都设置为了非阻塞 I/O 方式,其实设置为阻塞 I/O 方式也是可以的,因为 select()返回时意味此时数据是可读取的,所以非阻塞和阻塞两种方式读取数据均不会发生阻塞。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/21263.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI 将完全取代前端开发吗?

注&#xff1a;今晨&#xff0c;我浏览 Medium&#xff0c;看到了篇颇为标题党的文章&#xff0c;于是我就将它抛给了 ChatGPT。本篇文章全部由 ChatGPT 所写。同时&#xff0c;我也请 ChatGPT 分享了它对此的观点。 最近&#xff0c;我的同事向我讲述了他与他老板的一次谈话。…

Python机器学习:Scikit-learn和TensorFlow的应用和模型设计

一、引言 Python在机器学习领域中已经成为非常受欢迎的编程语言。Scikit-learn和TensorFlow是Python中应用最广泛的两个机器学习库&#xff0c;它们提供了丰富的机器学习算法和工具&#xff0c;帮助开发人员轻松地构建和训练机器学习模型。本文将详细介绍Scikit-learn和Tensor…

功率放大器的选型原则和方法是什么

功率放大器是一种能够将低电平信号放大到足够高的电平以驱动负载的电子器件。在各种电子设备中&#xff0c;功率放大器被广泛应用&#xff0c;如音响系统、电视广播、汽车音响、射频通信等。因此&#xff0c;正确选型功率放大器非常重要&#xff0c;可以提高设备的性能和可靠性…

如何在linux中配置JDK环境变量

在linux系统部署皕杰报表&#xff0c;因皕杰报表是一款纯java报表工具&#xff0c;运行时需要jre环境&#xff0c;所以要在服务器上配置三个jdk环境变量path、classpath、JAVA_HOME。 那么为什么要配置jdk环境变量呢&#xff1f;因为java软件运行时要用到一些java命令&#xff…

Python挑选出无Labelme标注文件的图片文件

Python挑选出无Labelme标注文件的图片文件 前言前提条件相关介绍实验环境Python挑选出无Labelme标注文件的图片文件代码实现输出结果 前言 本文是个人使用Python处理文件的电子笔记&#xff0c;由于水平有限&#xff0c;难免出现错漏&#xff0c;敬请批评改正。 (https://blog.…

JAVA常用API - Runtime和System

文章目录 前言 大家好,我是最爱吃兽奶,今天给大家带来JAVA常用API中的Runtime类和System类 那么就让我们一起去看看吧! 一、Rubtime 1.Rubtime是什么? 2.Runtime常用方法 Runtime提供了很多方法,在这里演示两个 public static Runtime getRuntime(): 返回当前运行时环境的…

Ada 语言学习(3)复合类型数据——Array

文章目录 Array数据类型声明数组索引数组范围数组复制数组初始化直接赋值通过拷贝赋值不同索引范围但长度相等非指定类型边界收缩 多维数组数组遍历数组切片访问和动态检查直接访问动态检查 数组字面量 Array literal数组拼接两个数组拼接数组和单个值拼接 Array Equality&…

SpringBoot【开发实用篇】---- 整合第三方技术(消息)

SpringBoot【开发实用篇】---- 整合第三方技术&#xff08;消息&#xff09; 消息的概念Java处理消息的标准规范JMSAMQPMQTTKafka 购物订单发送手机短信案例订单业务短息处理业务 SpringBoot整合ActiveMQ安装整合 SpringBoot整合RabbitMQ安装整合&#xff08;direct模型&#x…

加密解密软件VMProtect教程(六):主窗口之控制面板“项目”部分(2)

VMProtect 是新一代软件保护实用程序。VMProtect支持德尔菲、Borland C Builder、Visual C/C、Visual Basic&#xff08;本机&#xff09;、Virtual Pascal和XCode编译器。 同时&#xff0c;VMProtect有一个内置的反汇编程序&#xff0c;可以与Windows和Mac OS X可执行文件一起…

solidworks2020及麦迪工具箱安装

1、麦迪工具箱安装 1&#xff09;下载 下载链接&#xff1a;www.maidiyun.com/download 下载今日制造 2&#xff09;安装 由于电脑上安装了杀毒软件&#xff0c;会直接删除解压后的安装包&#xff0c;因此需要关闭杀毒软件或者在被删除后进入杀毒软件的隔离区将该文件添加…

Tomcat安装与使用

Tomcat 是HTTP服务器&#xff0c;用于使用HTTP协议。 1、下载Tomcat 下载链接&#xff1a;https://tomcat.apache.org/ 进入官网后&#xff0c;根据自己想要下载的版本进行下载&#xff0c;我这里选择下载的版本是Tomcat 8. 点击选择自己想要下载的对应版本&#xff0c;下载Z…

Netty入门

Netty入门 1. 概述 1.1 Netty是什么&#xff1f; Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.以上片段摘自官网&#xff0c;Netty 是一个异步的、基于事…

三十六、链路追踪、配置中心

1、链路追踪 在一次调用链路中&#xff0c;可能设计到多个微服务&#xff0c;如果在线上&#xff0c;某个微服务出现故障&#xff0c;如何快速定位故障所在额微服务呢。 可以使用链路追踪技术 1.1链路追踪介绍 在大型系统的微服务化构建中&#xff0c;一个系统被拆分成了许多微…

chatgpt赋能Python-python3_排序

Python3 排序指南&#xff1a;介绍、说明和实践 Python3是当今最受欢迎的编程语言之一&#xff0c;拥有许多可用于各种任务的库和框架。其中之一是它自带的排序函数&#xff0c;在数据分析和机器学习等领域中非常有用。 在本篇文章中&#xff0c;我们将简要介绍Python3的排序和…

基于AT89C51单片机的贪吃蛇游戏设计

点击链接获取Keil源码与Project Backups仿真图: https://download.csdn.net/download/qq_64505944/87778030 源码获取 主要内容: 设计一个贪吃蛇游戏,使其具有以下游戏规则:①当没有改变方向时,贪吃蛇沿原来路径一直前进②贪吃蛇无法回头,只能异于当前方向改变行动③蛇…

第7章链接:如何动态连接共享库、从应用程序中加载和链接共享库

文章目录 7.10 动态链接共享库静态库的缺点何为共享库共享库的"共享"的含义动态链接过程 7.11 从应用程序中加载和链接共享库运行时动态加载和连接共享库的接口 dlopen函数 dlsym函数 dlclose函数 dlerror动态加载和链接共享库的应用程序示例 7.12 *与位置无关的代码…

强大,Midjourney Imagine API接口,AI画画的福音!

前几天跟大家分享过一篇 ”让chatGPT教你AI绘画|如何将chatGPT与Midjourney结合使用&#xff1f;“&#xff0c;但是由于许多小伙伴们使用Midjourney还有许多困难&#xff0c;又要上网&#xff0c;还要注册Discord&#xff0c;MJ的使用成本很高&#xff0c;让大家望而却步&…

链表题目强化练

目录 前言&#xff08;非题目&#xff09; 两数相加 删除链表的倒数第N个结点 环形链表 相交链表 合并 K 个升序链表 复制带随机指针的链表 前言&#xff08;非题目&#xff09; 初学者在做链表的题目时有一个特点&#xff0c;就是每看一个链表的题都觉得很简单&#x…

Python程序员职业现状分析,想提高竞争力,就要做到这六点

现今程序员群体数量已经高达几百万&#xff0c;学历和收入双高&#xff0c;月薪普遍过万。今天&#xff0c;我们就围绕90后程序员人群分析、职业现状、Python程序员分析等&#xff0c;进行较为全面的报告分析和观点论述。 一、程序员人群分析 人数规模上&#xff1a;截当前程…

【设计原则与思想:总结课】38 | 总结回顾面向对象、设计原则、编程规范、重构技巧等知识点

到今天为止&#xff0c;设计原则和思想已经全部讲完了&#xff0c;其中包括&#xff1a;面向对象、设计原则、规范与重构三个模块的内容。除此之外&#xff0c;我们还学习了贯穿整个专栏的代码质量评判标准。专栏的进度已经接近一半&#xff0c;马上就要进入设计模式内容的学习…