目录
[1. 五种IO模型](#1. 五种IO模型)
[2. 高级IO重要概念](#2. 高级IO重要概念)
[2.1 同步通信 vs 异步通信(synchronous communication/ asynchronouscommunication)](#2.1 同步通信 vs 异步通信(synchronous communication/ asynchronouscommunication))
[2.2 阻塞 VS 非阻塞](#2.2 阻塞 VS 非阻塞)
[3. 非阻塞IO](#3. 非阻塞IO)
[3.1 fcntl](#3.1 fcntl)
[3.2 实现函数SetNoBlock](#3.2 实现函数SetNoBlock)
[3.3 代码的实现](#3.3 代码的实现)
1. 五种IO模型

五种IO模型就是解决IO通信时,如何高效通信的问题。
例子:钓鱼
钓鱼 = 等 + 钓 !!! 高效的钓鱼 = 等的比重,非常低,单位时间内,钓鱼的效率很高!!!

- 阻塞IO: 在内核将数据准备好之前, 系统调用会⼀直等待. 所有的套接字, 默认都是阻塞方式。
阻塞IO是最常见的IO模型。

- 非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询。这对CPU来说是较大的浪费, ⼀般只有特定场景下才使用。

- 信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应⽤程序进行IO操作。(用的不多,SIGIO信号默认是没有开启的)

- IO多路转接: 虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个⽂件描述符的就绪状态。

select 、poll、epoll 只解决等的问题,拷贝依旧是之前老的IO函数来做,但是一次只能等到 1个fd。select 、poll、epoll一次能等待多个 fd。

- 异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据,不太常用)。

小结
任何IO过程中, 都包含两个步骤. 第⼀是等待, 第二是拷贝。而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间。 让IO更高效, 最核心的办法就是让等待的时间尽量少。
2. 高级IO重要概念
2.1 同步通信 vs 异步通信(synchronous communication/ asynchronous
communication)
同步和异步关注的是消息通信机制。
- 所谓同步,就是在发出⼀个调⽤时,在没有得到结果之前,该调⽤就不返回. 但是⼀旦调⽤返回,就得到返回值了; 换句话说,就是由调⽤者主动等待这个调⽤的结果;
- 异步则是相反,调⽤在发出之后,这个调⽤就直接返回了,所以没有返回结果; 换句话说,当⼀个异步过程调⽤发出后,调⽤者不会⽴刻得到结果; ⽽是在调⽤发出后,被调⽤者通过状态、通知来通知调⽤者,或通过回调函数处理这个调⽤。
另外, 在之前的多进程多线程的时候, 也提到同步和互斥. 这⾥的同步通信和进程之间的同步是完全不相⼲的概念。
- 进程/线程同步也是进程/线程之间直接的制约关系
- 是为完成某种任务而简历的两个或多个线程,这个线程需要在某些位置上协调他们的⼯作次序而等待、传递信息所产⽣的制约关系.。尤其是在访问临界资源的时候。
在以后看到 "同步" 这个词的时候,一定要搞清楚是同步通信异步通信的同步,还是同步与互斥中的同步。二者是没有任何关系的。同步IO和异步IO的区分:只要参加了 等 + 拷贝的任意一项就是同步IO,一项都没参加的话就是异步IO。
什么叫做IO模型?
模型就是一种规律,IO的情况只有这五种!!要么是五种当中的一种,要么是任意多个的组合!大部分都是阻塞式IO!
2.2 阻塞 VS 非阻塞
阻塞和⾮阻塞关注的是程序在等待调⽤结果(消息,返回值)时的状态。
- 阻塞调⽤是指调⽤结果返回之前,当前线程会被挂起. 调⽤线程只有在得到结果之后才会返回。
- 非阻塞调⽤指在不能⽴刻得到结果之前,该调⽤不会阻塞当前线程。
3. 非阻塞IO
3.1 fcntl
例子:scanf()
scanf() 函数,键盘不进行输入的话,就会被阻塞住,阻塞住的原因是读条件不就绪,读条件不就绪就会阻塞住。阻塞是OS自动做的。
代码:将阻塞变为非阻塞

open() 函数中的 flag 参数,默认是阻塞的方式打开的,flag当中可以设置一个选项:O_NONBLOCK

recv() 函数中的 flag 参数也是可以进行设置的:

一旦设置了,如果底层文件描述符没有数据的话,此时recv就会出错返回。
如何设计非阻塞?将 fd 直接设置为非阻塞的方式 -> fcntl(上层即便用阻塞的接口,它也是非阻塞的。任何文件在底层都有 struct file 结构体,包含了 int flag 、int mode,一个文件究竟是阻塞还是非阻塞本质就是 struct file 结构体中的flag标志位是否被设置NONBLOCK,此时上层是阻塞的接口来调用的话,依旧是非阻塞的。)

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 实现函数SetNoBlock
cpp
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置fd为非阻塞!
}
之前是阻塞的就是因为没有设置上面的代码中的标志位。
返回值:失败返回 -1

3.3 代码的实现
以 read() 为例,因为 read() 是默认的阻塞式的IO。
cpp
// testNonBlock.cc
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
void SetNonBlock(int sockfd)
{
int f1 = fcntl(sockfd, F_GETFL);
if (f1 < 0)
{
perror("fcntl");
return;
}
fcntl(sockfd, F_SETFL, f1 | O_NONBLOCK); // O_NONBLOCK:设置fd为非阻塞
}
int main()
{
// 默认标准输入是阻塞的!
char buffer[64];
while (true)
{
ssize_t n = read(0, buffer, sizeof(buffer) - 1); // sizeof(buffer):期望读取的大小 n:实际读取的大小
if (n > 0)
{
buffer[n] = 0;
printf("##%s", buffer);
}
else if (n == 0)
{
printf("read end of file!\n");
}
else
{
perror("read");
}
}
}
运行结果:阻塞式IO

变为非阻塞式IO:

运行结果:没有卡住

以上信息是它打出来的:



#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:

调用系统函数失败,最关心的就是为什么会失败???全局变量:errno,当一个库函数或系统调用失败,该函数自动设置errno,表明该函数所对应的错误码!!!多进程中errno会写时拷贝,多线程中errno是线程局部存储,errno变量每个线程一旦修改,调用库函数,修改errno,每个线程都有自己的errno,互相不冲突。errno会线程安全的在多线程多进程中使用。错误码就会告诉我们是真的出错了还是其他情况!!!
cpp
printf("read error, reat ret: %ld, errno:%d\n", n, errno);

将错误码准换成错误码的字符串描述:

cpp
printf("read error, reat ret: %ld, errno:%d, err msg:%s\n", n, errno, strerror(errno));

之前用到的perror函数,就是拿着错误码转成错误码描述,上个函数出错了,将全局变量errno设置了。
cpp
// errno = 11 -> EAGAIN or EWOULDBLOCK (两个宏值)


cpp
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
void SetNonBlock(int sockfd)
{
int f1 = fcntl(sockfd, F_GETFL);
if (f1 < 0)
{
perror("fcntl");
return;
}
fcntl(sockfd, F_SETFL, f1 | O_NONBLOCK); // O_NONBLOCK:设置fd为非阻塞
}
int main()
{
SetNonBlock(0); // 设置非阻塞只需要设置一次就行了
// 默认标准输入是阻塞的!
char buffer[64];
while (true)
{
ssize_t n = read(0, buffer, sizeof(buffer) - 1); // sizeof(buffer):期望读取的大小 n:实际读取的大小
if (n > 0)
{
buffer[n] = 0;
printf("##%s", buffer);
}
else if (n == 0)
{
printf("read end of file!\n");
break;
}
else
{
// IO非阻塞,你就是不输入 -> 读条件不满足 -> 以后讨论IO问题,全部站在内存(进程)视角,看待IO
// read 会以出错形式返回!
// 读条件不满足算真的出错了吗?---- 严格来说不算出错!!!
// 为什么read()返回值 < 0 ???
// a.真的IO出错了
// b.其他情况,被迫返回小于0!!!
// perror("read"); // Resource temporarily unavailable
// printf("read error, reat ret: %ld, errno:%d, err msg:%s\n", n, errno, strerror(errno));
// EAGAIN or EWOULDBLOCK
// errno = 11 -> EAGAIN or EWOULDBLOCK (两个宏值)
if(errno == EAGAIN || errno ==EWOULDBLOCK )
{
printf("你的数据没有准备好,下次再来!!\n");
// 做其他事情 !
continue;
}
else
{
perror("raed error\n");
}
sleep(1);
}
}
}
轮询检测:



在读数据之前,IO函数可能会被信号给中断。回来的时候,继续read()

cpp
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
printf("你的数据没有准备好,下次再来!!\n");
// 做其他事情 !
sleep(1);
continue;
}
if (errno == EINTR)
{
sleep(1);
// 信号中断,回来的时候,继续read
continue;
}
else
{
perror("raed error\n");
}

为什么用标准输入来演示,为什么不用标准输出???
---- 因为标准输出,大部分的情况下输出条件都是满足的!!输出缓冲区默认是空的,输出条件默认是满足的,所以printf这些接口不会卡住,看不到卡住的现象,很难看到现象。标准输入没有数据时,比较容易看到现象。