【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()返回时意味此时数据是可读取的,所以非阻塞和阻塞两种方式读取数据均不会发生阻塞。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值