IO主要分为两步:
- 第一步是等,即等待IO条件就绪。
- 第二步是拷贝,也就是当IO条件就绪后将数据拷贝到内存或外设。
让IO变得高效,最核心的办法就是尽量减少“等”的时间
五种 IO 模型
-
A, 拿着鱼竿去钓鱼,一直盯着鱼漂,鱼漂有动静就收钩
-
B, 拿着鱼竿去钓鱼,时不时看看鱼漂,有动静就收购
-
C, 拿着鱼竿去钓鱼,在鱼漂上弄个铃铛,然后干其他的事情,听到铃铛的声音就收钩
-
D, 拿了一大堆鱼竿过来,都摆弄好,只要有一个鱼漂有动静,就收钩
-
E 是大老板,直接叫人帮忙钓鱼,钓到一定数量的🐟后通知自己,自己过来取🐟
这五种钓鱼方式,就对应了五种 IO 模型
-
A, 阻塞等待 , 等待到数据就立即读取
-
B, 轮询检测 , 检测到数据的时候读取
-
C, 利用铃铛来作为通知方式,听到了信号之后,就去读取数据(信号驱动)
-
D, 一次性检测多个文件描述符(多路转接)
-
E, 没有自己参与钓鱼过程,有别人帮忙监控文件描述符,自己只关心拿走数据(异步 IO)
ABC效率本质上是一样,D 的效率是最高的,ABCD是同步IO
异步IO 和 同步IO
- 异步IO没有参与IO细节,不需要你进行“等”和“拷贝”的操作
- 同步IO有参与IO细节
同步通信 和 异步通信
- 异步通信:在调用发出后,这个调用直接返回,并没有携带结果
- 同步通信:在发出调用后,没有得到结果前,该调用不返回
同步通信 和 同步与互斥
- 线程和进程的同步指的是线程和进程之间有相互制约的关系,让进程/线程能够按照某种特定的顺序访问临界资源
- 而同步IO指的是进程/线程与操作系统之间的关系,谈论的是进程/线程是否需要主动参与IO过程
fcntl-设置非阻塞IO
默认创建的都是阻塞的文件描述符,我们可以使用 fcntl 来将文件描述符设置成非阻塞的
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
- 复制一个现有的描述符(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)
- … 可变参数,传入的cmd值不同,后面追加的参数也不同。
返回值
- 如果函数调用成功,则返回值取决于具体进行的操作。
- 如果函数调用失败,则返回-1,同时错误码会被设置。
测试
定义一个函数,该函数就用于将指定的文件描述符设置为非阻塞状态
bool SetNonBlock(int fd)
{
//传入的cmd值为F_SETFL,获取文件已有状态
int fl = fcntl(fd, F_GETFL);
if (fl < 0){
std::cerr << "fcntl error" << std::endl;
return false;
}
//添加非阻塞标记O_NONBLOCK
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
return true;
}
stdin(0号文件描述符)输入的时候,进程就是处于阻塞状态,等待输入,如果我们改成非阻塞,那么stdin不会阻塞
int main()
{
SetNoBlock(stdin->_fileno);
char buf[1024];
while(true)
{
ssize_t read_size = read(stdin->_fileno, buf, sizeof(buf) - 1);
if(read_size < 0)
{
perror("read err");
sleep(2);
continue;
}
printf("input:%s\n", buf);
buf[0] = '\0';
}
return 0;
}
没有输入就会一直循环打印报错,如果有输出内容就会把输出内容打印
read err: Resource temporarily unavailable
read err: Resource temporarily unavailable
read err: Resource temporarily unavailable
read err: Resource temporarily unavailable
123
input:123
read err: Resource temporarily unavailable