0%

Linux高级IO

Linux高级IO

阻塞与非阻塞IO

  • 阻塞其实就是进入了休眠状态,交出了 CPU 控制权。

  • 譬如对于某些文件类型(读管道文件、网 络设备文件和字符设备文件),当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读 操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这就是阻塞式 I/O 常见的一种表现;如果是非阻 塞式 I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误!

  • 普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内 返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的;但是对于某些文件 类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O 进行操作。

  • 控制台使用sudo od -x /dev/input/mouse0读取鼠标文件信息

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
char buf[100];
int fd, ret;
/* 打开文件 */
fd = open("/dev/input/event3", O_RDONLY);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 读文件 */
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
if (0 > ret)
{
perror("read error");
close(fd);
exit(-1);
}
printf("成功读取<%d>个字节数据\n", ret);
/* 关闭文件 */
close(fd);
exit(0);
}

  • memset函数的作用是void *memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。上面代码中的作用是初始化为0

  • image-20220125105645727

  • 但是看到读取的并不是要求的100个字节

  • 假如将open的flag更改为O_RDONLY | O_NONBLOCK,那么调用程序假如没读取到的话就会立即报错,

  • image-20220125105756074

  • 所以阻塞式 I/O 的优点在于能够提升 CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU 资源,将 CPU 资源让给别人使用;而非阻塞式则是抓紧利用 CPU 资源,譬如不断地去轮训,这样就会导致 该程序占用了非常高的 CPU 使用率!

键盘

  • 键盘是标准输入设备 stdin,进程会自动从父进程中继承标准输入、标准输出以及标准错 误,标准输入设备对应的文件描述符为 0,所以在程序当中直接使用即可,不需要再调用 open 打开。

并发读取

  • 程序中先读了鼠标,在接着读键盘,所以由此可知,在实际测试当中,需要先动鼠标在按键盘(按 下键盘上的按键、按完之后按下回车),这样才能既成功读取鼠标、又成功读取键盘,程序才能够顺利运行 结束。因为 read 此时是阻塞式读取,先读取了鼠标,没有数据可读将会一直被阻塞,后面的读取键盘将得 不到执行。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define MOUSE "/dev/input/event3"
int main(void)
{
char buf[100];
int fd, ret;
/* 打开鼠标设备文件 */
fd = open(MOUSE, O_RDONLY);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 读鼠标 */
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
/* 读键盘 */
memset(buf, 0, sizeof(buf));
ret = read(0, buf, sizeof(buf));
printf("键盘: 成功读取<%d>个字节数据\n", ret);
/* 关闭文件 */
close(fd);
exit(0);
}

  • 利用下面的代码修改键盘文件的读取类型为不堵塞,并且同上将鼠标的打开方式也设置为非堵塞
int flag;
flag = fcntl(0, F_GETFL); //先获取原来的 flag
flag |= O_NONBLOCK; //将 O_NONBLOCK 标志添加到 flag
fcntl(0, F_SETFL, flag); //重新设置 flag
  • 使用非阻塞 I/O 方式解决了示例代码 13.1.4 出现的问题,但由于程序当中使用轮训方式,故而会 使得该程序的 CPU 占用率特别高,终归还是不太安全,会对整个系统产生很大的副作用,如何解决这样的 问题呢?我们将在下一小节向大家介绍。

多路复用

  • I/O 多路复用(IO multiplexing)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也 就是某个文件)可以执行 I/O 操作时,能够通知应用程序进行相应的读写操作。I/O 多路复用技术是为了解 决:在并发式 I/O 场景中进程或线程阻塞到某个 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);
  • 参数 readfds、writefds 以及 exceptfds 都是 fd_set 类型指针, 指向一个 fd_set 类型对象,fd_set 数据类型是一个文件描述符的集合体,所以参数 readfds、writefds 以及 exceptfds 都是指向文件描述符集合的指针,这些参数按照如下方式使用:

    • readfds 是用来检测读是否就绪(是否可读)的文件描述符集合;
    • writefds 是用来检测写是否就绪(是否可写)的文件描述符集合;
    • exceptfds 是用来检测异常情况是否发生的文件描述符集合。
  • 为 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_ZERO()将参数 set 所指向的集合初始化为空
    • FD_SET()将文件描述符 fd 添加到参数 set 所指向的集合中;
    • FD_CLR()将文件描述符 fd 从参数 set 所指向的集合中移除;
    • 如果文件描述符 fd 是参数 set 所指向的集合中的成员,则 FD_ISSET()返回 true,否则返回 false。
  • 返回值

    • 返回-1 表示有错误发生,并且会设置 errno。
    • 返回 0 表示在任何文件描述符成为就绪态之前 select()调用已经超时,在这种情况下,readfds, writefds 以及 exceptfds 所指向的文件描述符集合都会被清空。
    • 返回一个正整数表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述 符的个数,在这种情况下,每个返回的文件描述符集合都需要检查,通过 FD_ISSET()宏进行检查, 以此找出发生的 I/O 事件是什么。如果同一个文件描述符在 readfds,writefds 以及 exceptfds 中同时 被指定,且它对于多个 I/O 事件都处于就绪态的话,那么就会被统计多次
#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进行检测,然后检测结果之后对每个文件使用宏定义进行判断是否是这个文件处于就绪状态。
  • 鼠标和键盘都设置为了非阻塞 I/O 方式,其实设置为阻塞 I/O 方式也是可以的,因 为 select()返回时意味着此时数据是可读取的,所以以非阻塞和阻塞两种方式读取数据均不会发生阻塞

poll()函数

  • 系统调用 poll()与 select()函数很相似,但函数接口有所不同。在 select()函数中,我们提供三个 fd_set 集 合,在每个集合中添加我们关心的文件描述符;而在 poll()函数中,则需要构造一个 struct pollfd 类型的数 组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情 况)。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:指向一个 struct pollfd 类型的数组,数组中的每个元素都会指定一个文件描述符以及我们对该文件 描述符所关心的条件,稍后介绍 struct pollfd 结构体类型。
  • nfds:参数 nfds 指定了 fds 数组中的元素个数,数据类型 nfds_t 实际为无符号整形。
  • timeout:该参数与 select()函数的 timeout 参数相似,用于决定 poll()函数的阻塞行为,具体用法如下:
    • 如果 timeout 等于**-1,则 poll()会一直阻塞**(与 select()函数的 timeout 等于 NULL 相同),直到 fds 数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号时返回。
    • 如果 timeout 等于 0,poll()不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态。
    • 如果 timeout 大于 0,则表示设置 poll()函数阻塞时间的上限值,意味着 poll()函数最多阻塞 timeout 毫秒,直到 fds 数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号为止。
  • pollfd结构体
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
  • events 和 revents 都是位掩码

    • 初始化 events 来指 定需要为文件描述符 fd 做检查的事件

    • revents 变量由 poll()函数内部进行设置,用于 说明文件描述符 fd 发生了哪些事件(注意,poll()没有更改 events 变量)

    • 如果我们对某个文件描述符上的事件不感兴趣,则可将 events 变量设置为 0;另外,将 fd 变量设置为 文件描述符的负值(取文件描述符 fd 的相反数-fd),将导致对应的 events 变量被 poll()忽略,并且 revents 变量将总是返回 0,这两种方法都可用来关闭对某个文件描述符的检查。

    • image-20220125133340426

  • 返回值与select类似,略

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#define MOUSE "/dev/input/event3"
int main(void)
{
char buf[100];
int fd, ret = 0, flag;
int loops = 5;
struct pollfd fds[2];
/* 打开鼠标设备文件 */
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
/* 同时读取键盘和鼠标 */
fds[0].fd = 0;
fds[0].events = POLLIN; //只关心数据可读
fds[0].revents = 0;
fds[1].fd = fd;
fds[1].events = POLLIN; //只关心数据可读
fds[1].revents = 0;
while (loops--)
{
ret = poll(fds, 2, -1);
if (0 > ret)
{
perror("poll error");
goto out;
}
else if (0 == ret)
{
fprintf(stderr, "poll timeout.\n");
continue;
}
/* 检查键盘是否为就绪态 */
if (fds[0].revents & POLLIN)
{
ret = read(0, buf, sizeof(buf));
if (0 < ret)
printf("键盘: 成功读取<%d>个字节数据\n", ret);
}
/* 检查鼠标是否为就绪态 */
if (fds[1].revents & POLLIN)
{
ret = read(fd, buf, sizeof(buf));
if (0 < ret)
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
}
}
out:
/* 关闭文件 */
close(fd);
exit(ret);
}