【Linux】47.高级IO(1)
文章目录
- 1. 高级IO
- 1.1 五种IO模型
- 1.2 高级IO重要概念
- 1.2.1 同步通信 vs 异步通信
- 1.2.2 阻塞 vs 非阻塞
- 1.3非阻塞IO
- 1.3.1 fcntl
- 1.3.2 实现函数SetNoBlock
- 1.3.3 轮询方式读取标准输入
- 1.3.4 I/O多路转接之select
- 1.3.4.1 初识select:
- 1.3.4.2 select函数原型
- 1.3.4.3 理解select执行过程
1. 高级IO
1.1 五种IO模型
[钓鱼例子]
- 阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.
阻塞IO是最常见的IO模型.
- 非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用
- 信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.
- IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件
描述符的就绪状态.
- 异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
小结:
任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少
1.2 高级IO重要概念
在这里, 我们要强调几个概念
1.2.1 同步通信 vs 异步通信
同步和异步关注的是消息通信机制.
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
- 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不想干的概念.
- 进程/线程同步也是进程/线程之间直接的制约关系
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候.
同学们以后在看到 “同步” 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步与互斥的同步
1.2.2 阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
- 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.
1.3非阻塞IO
1.3.1 fcntl
一个文件描述符, 默认都是阻塞IO
函数原型如下.
#include <unistd.h>
#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同.
fcntl函数有5种功能:
- 复制一个现有的描述符(cmd=F_DUPFD).
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.
1.3.2 实现函数SetNoBlock
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞
void SetNoBlock(int fd) { int fl = fcntl(fd, F_GETFL); // 获取文件描述符当前的标志if (fl < 0) { // 如果获取失败perror("fcntl"); // 打印错误信息return; // 退出函数}fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置文件描述符为非阻塞模式
}
- 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
- 然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.
1.3.3 轮询方式读取标准输入
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>// 将文件描述符设置为非阻塞模式
void SetNoBlock(int fd) {int fl = fcntl(fd, F_GETFL); // 获取文件描述符当前的标志if (fl < 0) { // 如果获取失败perror("fcntl"); // 打印错误信息return; // 退出函数}fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置文件描述符为非阻塞模式
}int main() {SetNoBlock(0); // 将标准输入(文件描述符0)设置为非阻塞模式while (1) { // 无限循环char buf[1024] = {0}; // 定义并初始化缓冲区ssize_t read_size = read(0, buf, sizeof(buf) - 1); // 尝试从标准输入读取数据if (read_size < 0) { // 如果读取失败(在非阻塞模式下,没有数据可读会返回-1)perror("read"); // 打印错误信息sleep(1); // 休眠1秒continue; // 继续下一次循环}printf("input:%s\n", buf); // 打印读取到的输入内容}return 0; // 程序结束(实际上这行代码不会执行,因为有无限循环)
}
这段程序演示了非阻塞I/O的工作方式:
- 首先将标准输入(文件描述符0)设置为非阻塞模式
- 在无限循环中,程序尝试从标准输入读取数据
- 由于是非阻塞模式,当没有输入数据时,
read()
不会阻塞等待,而是立即返回-1,同时设置errno为EAGAIN或EWOULDBLOCK- 程序捕获这个错误,打印错误信息,然后休眠1秒后再次尝试读取
- 当有数据输入时,
read()
会成功读取数据并返回读取的字节数,然后程序打印输入的内容这种模式允许程序在没有输入时继续执行其他任务(在本例中只是休眠),而不是被阻塞在输入操作上,适用于需要同时处理多个I/O事件的场景。
1.3.4 I/O多路转接之select
1.3.4.1 初识select:
系统提供select函数来实现多路复用输入/输出模型.
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
1.3.4.2 select函数原型
select的函数原型如下: #include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数解释:
- 参数nfds是需要监视的最大的文件描述符值+1;
- rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;
- 参数timeout为结构timeval,用来设置select()的等待时间
参数timeout取值:
NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
关于fd_set结构
其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符.
提供了一组操作fd_set的接口, 来比较方便的操作位图
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
关于timeval结构
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
函数返回值:
- 执行成功则返回文件描述词状态已改变的个数
- 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
- 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。
错误值可能为:
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数n 为负值。
- ENOMEM 核心内存不足
1.3.4.3 理解select执行过程
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1) *
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。
注意:没有事件发生的fd=5被清空。