✨个人主页:熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】
目录
[1、五种 IO 模型](#1、五种 IO 模型)
[2、高级 IO 重要概念](#2、高级 IO 重要概念)
[2.1、同步通信 vs 异步通信](#2.1、同步通信 vs 异步通信)
[2.2、阻塞 vs 非阻塞](#2.2、阻塞 vs 非阻塞)
[2.3、其他高级 IO](#2.3、其他高级 IO)
[3、非阻塞 IO](#3、非阻塞 IO)
1、五种 IO 模型
首先回顾我们前面学习的几个概念:
1、网络通信需要使用到进程通信!
2、进程间通信的本质就是IO!
3、IO ,input(输入) && output(输出)
4、站在进程角度 && 站在内存的角度
IO如何理解?read/recv/send/write
IO = 等 + 拷贝
什么叫做高效的IO呢?
本质就是单位时间内,减少等的比重!
此处使用一个钓鱼的例子来讲解五种IO模型!
1.1、阻塞IO
阻塞 IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.
阻塞 IO 是最常见的 IO 模型.
1.2、非阻塞IO
非阻塞 IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返EWOULDBLOCK 错误码.
- 非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对 CPU 来说是较大的浪费, 一般只有特定场景下才使用.
1.3、信号驱动IO
信号驱动 IO: 内核将数据准备好的时候, 使用 SIGIO 信号通知应用程序进行 IO操作.
1.4、多路转接IO
多路转接 IO : 虽然从流程图上看起来和阻塞 IO 类似. 实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态.
1.5、异步IO
异步 IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
1.6、小结
任何 IO 过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让 IO 更高效, 最核心的办法就是让等待的时间尽量少.
2、高级 IO 重要概念
2.1、同步通信 vs 异步通信
同步和异步关注的是消息通信机制.
- 所谓同步 ,就是在发出一个调用 时,在没有得到结果之前,该调用就不返回.但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
- 异步 则是相反,调用 在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
另外, 我们回忆在讲多进程多线程 的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不相干的概念.
- 进程/线程同步也是进程/线程之间直接的制约关系
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候.
uu们以后在看到 "同步 " 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步与互斥的同步.
2.2、阻塞 vs 非阻塞
阻塞和非阻塞 关注的是程序在等待调用结果(消息,返回值)时的状态.
- 阻塞调用 是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
- 非阻塞调 用指在不能立刻得到结果之前,该调用不会阻塞当前线程.
2.3、其他高级 IO
非阻塞 IO,纪录锁,系统 V 流机制,I/O 多路转接(也叫 I/O 多路复用),readv 和writev 函数以及存储映射 IO(mmap) ,这些统称为高级 IO.
3、非阻塞 IO
非阻塞IO(Non-blocking I/O)是一种I/O操作模式,允许程序在发起I/O请求后不必等待该请求完成,而是可以继续执行其他任务。
一个文件描述符, 默认都是阻塞 IO,要实现非阻塞IO,需要对fd进行控制操作,因此需要使用fcntl()函数!
3.1、fcntl
fcntl()
fcntl - 对已打开的文件描述符进行各种控制操作
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
参数:
- fd:文件描述符,表示要操作的文件或套接字。
- cmd :操作命令,它是一个整型值,通常以
F_
开头,后跟一些特定的字符来表示不同的操作。例如,F_DUPFD
用于复制文件描述符,F_GETFD
用于获取文件描述符的标志,等等。 - arg :这是一个可选参数,其类型和含义取决于
cmd
的值。例如,对于F_DUPFD
命令,arg
是一个整型值,表示要复制的文件描述符的最小值;对于F_SETFL
命令,arg
是一个整型值,表示要设置的文件状态标志。
常用命令及其功能:
- F_DUPFD :复制一个现有的文件描述符。返回一个新的文件描述符,该描述符与原始描述符共享相同的文件偏移量、访问模式和文件状态标志。新描述符是最小的大于或等于
arg
的一个可用描述符。 - F_GETFD :获取文件描述符的标志。返回的文件描述符标志通常用于控制文件描述符在exec调用时的行为。例如,如果返回的标志与
FD_CLOEXEC
进行按位与运算的结果不为0,则在exec调用时该文件描述符将被关闭。 - F_SETFD :设置文件描述符的标志。通过
arg
参数设置文件描述符的标志。例如,可以通过将arg
设置为FD_CLOEXEC
来设置文件描述符在exec调用时被关闭的标志。 - F_GETFL:获取文件状态标志。返回的文件状态标志包括文件的访问模式(读、写或读写)、是否设置了非阻塞标志、是否设置了同步I/O标志等。
- F_SETFL :设置文件状态标志。通过
arg
参数设置文件的状态标志。例如,可以通过将arg
设置为O_NONBLOCK
来设置非阻塞I/O标志。 - F_GETOWN:获取当前正在接收SIGIO或SIGURG信号的进程ID或进程组ID。
- F_SETOWN:设置将接收SIGIO和SIGURG信号的进程ID或进程组ID。
- F_GETLK 、F_SETLK 、F_SETLKW :这些命令用于获取、设置或等待文件记录锁。它们需要传递一个指向
flock
结构的指针作为arg
参数。flock
结构包含了锁的类型、起始位置、长度等信息。
返回值:
fcntl函数的返回值取决于执行的命令 。如果命令执行成功,则返回0或特定的正值(如F_DUPFD
返回新的文件描述符)。如果命令执行失败,则返回-1,并设置errno
来指示错误的原因。
传入的 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).
我们此处只是用第三种功能, 获取/设置文件状态标记 , 就可以将一个文件描述符设置为非阻塞.
3.2、阻塞IO
实现非阻塞IO之前,我们需要实现阻塞IO进行对比差别,此处使用read()函数进行从标准输入流中轮询读取数据!
主函数
int main()
{
char buffer[1024];
SetNonBlock(0); // 设置非阻塞等待
while (true)
{
printf("Enter# "); // 没有回车不会直接刷新缓冲器数据,需手动刷新
fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
printf("echo# %s", buffer); // 会输入\n无需手动加
}
else if (n == 0)
{
printf("read done\n");
break;
}
else
{
perror("read");
break;
}
}
return 0;
}
1、当我们没有输入的时候,读取会卡在这里
2、而输入之后数据会回显到显示器
3、输入回车之后会读取到从标准输入流发送过来的数据
运行结果
过程分析
3.3、SetNoBlock()
将文件描述符fd设置成非阻塞,先获取文件状态标志,再设置文件状态标志!
void SetNonBlock(int fd)
{
int fl = fcntl(fd,F_GETFL); // 获得文件状态标记,返回文件描述符标志
if(fl < 0)
{
std::cout << "fcntl error" << std::endl;
return;
}
::fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 将fd设置成非阻塞
}
3.4、非阻塞IO主函数
主函数(代码一)
int main()
{
char buffer[1024];
SetNonBlock(0); // 设置非阻塞等待
while (true)
{
printf("Enter# "); // 没有回车不会直接刷新缓冲器数据,需手动刷新
fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
printf("echo# %s", buffer); // 会输入\n无需手动加
}
else if (n == 0)
{
printf("read done\n");
break;
}
else
{
perror("read");
break;
}
}
return 0;
}
直接向阻塞等待的方式判断,会直接报错退出程序,因为非阻塞等待的返回值也是小于0 ,此处可以分别打印返回值(返回值是long int类型,需使用%ld)和错误码调试一下!
调试代码
在perror()函数与bread之前增加调试打印信息即可!
printf("%ld\n",n); // 打印read返回值
printf("%d\n",errno); // 打印错误码
因此我们可以使用错误码来区分底层不就绪还是真的出错!!!
主函数(代码二)
// 非阻塞版本
int main()
{
char buffer[1024];
SetNonBlock(0); // 设置非阻塞等待
while (true)
{
printf("Enter# "); // 没有回车不会直接刷新缓冲器数据,需手动刷新
fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
printf("echo# %s", buffer); // 会输入\n无需手动加
}
else if (n == 0)
{
printf("read done\n");
break;
}
else
{
// 如果是非阻塞,底层数据就没有就绪,IO接口,会以错误形式返回
// 所以,如何区分 底层不就绪 vs 真的出错呢? 根据errno错误码
// EWOULDBLOCK; EAGAIN; // 11号错误码
if (errno == EWOULDBLOCK)
{
sleep(1);
std::cout << "底层数据没有就绪,开始轮询检测" << std::endl;
std::cout << "可以做其他事情" << std::endl;
// do other thing
continue;
}
else
{
perror("read");
break;
}
}
}
return 0;
}
为了不让刷屏,我们可以每隔一秒打印一次做其他事情的消息!
小优化
我们可能还会遇到信号中断的情况,信号中断也需要继续轮询检查,此处可以再做一个判断!
主函数(代码三)
// 非阻塞版本
int main()
{
char buffer[1024];
SetNonBlock(0); // 设置非阻塞等待
while (true)
{
printf("Enter# "); // 没有回车不会直接刷新缓冲器数据,需手动刷新
fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
printf("echo# %s", buffer); // 会输入\n无需手动加
}
else if (n == 0)
{
printf("read done\n");
break;
}
else
{
// 如果是非阻塞,底层数据就没有就绪,IO接口,会以错误形式返回
// 所以,如何区分 底层不就绪 vs 真的出错呢? 根据errno错误码
// EWOULDBLOCK; EAGAIN; // 11号错误码
if (errno == EWOULDBLOCK)
{
sleep(1);
std::cout << "底层数据没有就绪,开始轮询检测" << std::endl;
std::cout << "可以做其他事情" << std::endl;
// do other thing
continue;
}
// 被系统中断的系统调用
else if (errno == EINTR)
{
continue;
}
else
{
perror("read");
break;
}
// EWOULDBLOCK; EAGAIN; // 11号错误码
// perror("read");
// printf("%ld\n",n); // 打印read返回值
// printf("%d\n",errno); // 打印错误码
// break;
}
}
return 0;
}
总结:
1、IO = 等(多路转接的作用) + 拷贝
2、为了等待多个fd,等待fd上面的新事件就绪,通知程序员,事件已经就绪,可以进行IO拷贝了!
3、新事件就绪:OS底层有数据了(读事件就绪),或者OS底层有空间了(写事件就绪)!