非阻塞 IO
五种 IO 模型与阻塞 IOhttps://blog.csdn.net/Small_entreprene/article/details/148957230?fromshare=blogdetail&sharetype=blogdetail&sharerId=148957230&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link
非阻塞 IO
系统·网络相关接口的参数
有了非阻塞 IO 的理论,我们接下来重点就是如何进行非阻塞 IO 呢?
其实我们想要进行非阻塞 IO,有非常多的做法!比如说:
第一种:在系统部分,有系统调用 open:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
有一个 flags 标志位选项 --- O_NONBLOCK --- 打开文件时使用非阻塞模式!
所以我们可以通过在系统角度打开文件的时候指定特定的选项实现非阻塞的操作文件。
第二种:我们之前在网络中有说过,读取使用read,后面更推荐在网络中读文件使用recv!
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
我们之前的 flags 标志位都是设置为0的,这其实就是阻塞式的 IO ,我们可以将该标志位设置为 --- MSG_DONTWAIT --- 就是非阻塞读。
这时候我们就应该理解为什么recv / sendto 这些接口需要带上 flags 标志位,因为这样会决定我们 IO 是以某种方式进行 IO 的!
所以不管是系统还是网络,在进行 IO 的时候,我们都可以选择进行非阻塞进行读取!
但是这些做法我们都不想用,因为会增加我们的记忆成本,更重要的是,我们有更简单的设置非阻塞 IO 的做法!!!
推荐的做法是直接对文件描述符属性设置非阻塞选项!
fcntl
fcntl
是一个在 Unix 和类 Unix 系统中用于文件控制的系统调用,它提供了多种功能,主要用于操作文件描述符的属性和行为。以下是 fcntl
的主要功能和使用方法:
fcntl
可以执行以下操作:
-
复制文件描述符:通过
F_DUPFD
或F_DUPFD_CLOEXEC
命令,可以复制一个现有的文件描述符。 -
获取/设置文件描述符标志:使用
F_GETFD
和F_SETFD
命令,可以获取或设置文件描述符的标志,如FD_CLOEXEC
。 -
获取/设置文件状态标志:通过
F_GETFL
和F_SETFL
命令,可以获取或设置文件的访问状态标志,如O_RDONLY
、O_WRONLY
、O_RDWR
、O_APPEND
、O_NONBLOCK
等。 -
获取/设置记录锁:使用
F_GETLK
、F_SETLK
和F_SETLKW
命令,可以获取或设置文件的记录锁,用于控制对文件的并发访问。 -
获取/设置异步 I/O 所有权:通过
F_GETOWN
和F_SETOWN
命令,可以获取或设置接收SIGIO
或SIGURG
信号的进程 ID 或进程组 ID。
fcntl
的函数原型如下:
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
-
fd
是要操作的文件描述符。 -
cmd
是要执行的操作命令,如F_DUPFD
、F_GETFD
等。 -
第三个参数的类型和含义取决于
cmd
的值。
一个进程打开文件的时候,会在内核当中创建 file 对象,其中 struct file 中有一个字段:
unsigned int f_flags;
这个字段是用来设置该文件的打开标志的。在 Linux 系统中,当一个进程打开文件时,f_flags
字段会存储文件的打开模式和状态标志,这些标志决定了进程对文件的访问方式和行为。
我们使用系统调用 fcntl 就是根据提供的文件描述符 fd,cmd 我们现在留意 F_GETFL
和 F_SETFL
命令。我们使用 F_GETFL
就可以得到该文件打开时的标志位,然后设置上一些非阻塞的标记,通过 F_SETFL
命令将其设置到内核当中!!!
实现函数 SetNoBlock
以下是基于fcntl
实现的SetNoBlock
函数,用于将文件描述符设置为非阻塞模式的代码:
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>void SetNoBlock(int fd) {int fl = fcntl(fd, F_GETFL); // 使用 F_GETFL 将当前的文件描述符的属性取出来(这是一个位图)if (fl < 0) {perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 然后再使用 F_SETFL 将文件描述符设置回去,设置回去的同时,加上一个 O_NONBLOCK 参数
}
-
fcntl(fd, F_GETFL)
:获取文件描述符fd
的当前标志(属性),这些标志是一个位图。 -
perror("fcntl")
:如果获取标志失败(fcntl
返回值小于 0),打印错误信息。 -
fcntl(fd, F_SETFL, fl | O_NONBLOCK)
:将文件描述符的标志设置为当前标志加上O_NONBLOCK
,使文件描述符变为非阻塞模式。
这个函数的作用是将指定的文件描述符设置为非阻塞模式,便于后续的非阻塞 I/O 操作。
轮询方式读取标准输入
我们下面来做一个样例,先来看看阻塞式 IO :向标准输入中阻塞式的读取数据!一会我们从键盘读数据的方式改成非阻塞,我们就可以看到效果了!
#include <iostream>
#include <unistd.h>int main()
{char buffer[1024];while(true){ssize_t n = read(0, buffer,sizeof(buffer));if(n > 0){buffer[n] = 0;std::cout<< buffer <<std::endl;}//TODO}return 0;
}
我们g++进行编译通过之后,./a.out就会卡住了:
当前的a.out可执行程序一旦运行起来就是一个进程,会在read处卡住,在read处阻塞住的根本原因是0号对应的文件描述符,底层当前没有数据,所以read系统调用开始进入他自己的阻塞等待的过程,在阻塞等待期间,没有数据就绪,就会卡在read处!
后面我们键盘输入相关数据,就会回显:
输入c的时候,回显的是c空行,下一次应该在c下面一行输入的,但是却多了一行,这是因为我们回车进行回显的时候,其实是回显 "c + 回车" !!!
可是以前使用cin / getline /scanf ...这种C/C++式的接口,会自动为我们将 "/r/n" 这样的字符进行过滤!
所以我们可以将:
buffer[n] = 0; --->>> buffer[n-1] = 0;
注意:我们对键盘输入的总是需要按回车的,不按回车的话,键盘就不会给操作系统发中断,操作系统就不会读取键盘中的数据!
上面的 IO 方式就是阻塞 IO !!!
所以0号标准输入,默认就是阻塞的!所以我们现在要将标准输入设置为非阻塞,我们下面设计一个接口 --- SetNonBlock --- 提供一个文件描述符,将该文件描述符设置为非阻塞!
void SetNonBlock(int fd)
{int fl = fcntl(fd, F_GETFL); //先获取一下对应文件的struct file对象的文件标志位if(fl < 0){perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);//O_NONBLOCK : 将fd设置为非阻塞
}
我们在进入main函数的时候,调用 SetNonBlock(0); 将文件描述符对应的文件操作设置为非阻塞,如果是设置成非阻塞了,当前的read要干几件事情?
第一:read需要进行系统调用,检测数据是否准备好!没有准备好,read就会立即返回,我们可以在while循环中打印标志,看看非阻塞的效果:并打印n的值
我们发现在没有数据输入的时候,n的值是-1,也就意味着底层数据没有准备好!
补充:Linux中使用Ctrl + d 就是表示输入结束 --- n = 0 --- 读结束
所以read在阻塞的情况下,n小于0就是读出错,但是在非阻塞的情况下,n小于0就有两种情况了:
- 读取出错
- 底层没有数据准备好
在系统层面上:如果read出错了,-1被返回!并且错误码被设置,为什么不返回一个-2,-3啥的,系统不是这么设计的!错误的信息总类太多了!
严格意义上来说,我们的理解上:底层数据没有准备好并不算是数据读取出错!但是在系统接口设计的时候,底层数据一旦没有就绪,就会以出错的形式返回!但是具体是不是真的出错了,是通过进一步的检测 errno 这个错误码来进行判定!!!
如果 errno 是 EAGAIN or EWOULDBLOCK ,那么就代表底层实际上没错,只是数据没有准备好!!!
所以一旦"读取出错了",我们一定要进行进一步区分!只有非阻塞情况要这么考虑!!!
实际上在我们进行 IO 的时候,一旦进程进程read的时候,底层没有数据就绪,进程当前就会进入休眠状态了!进程在进入休眠状态的时候,其状态叫做 --- S!称为浅度睡眠!在一个进程进行浅度睡眠的时候,他有可能在进行read等待的时候,突然在这个时候发送了一个信号。浅度睡眠也叫做可被中断睡眠,有一个外部信号到来,这个进程就要被唤醒,进行信号捕捉,进行信号处理,一旦处理完毕,这个进程就没有办法回到" S "状态,继续阻塞,往往会回到代码中继续运行了。换句话说,我们在进行 IO 的等的时候,往往可能会被一个信号中断,导致 IO 做不完整!
IO 做不完整,一样的read返回值会返回-1 ,errno 设置为 EINTR ,这个也是我们需要甄别出来的!
所以非阻塞 IO 的基本格式就是:
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstdio>void SetNonBlock(int fd)
{int fl = fcntl(fd, F_GETFL); //先获取一下对应文件的struct file对象的文件标志位if(fl < 0){perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);//O_NONBLOCK : 将fd设置为非阻塞
}int main()
{SetNonBlock(0);char buffer[1024];while(true){ssize_t n = read(0, buffer,sizeof(buffer));if(n > 0){buffer[n-1] = 0;std::cout<< buffer <<std::endl;}else if(n < 0){//1.读取出错 2.底层没有数据准备好if(errno == EAGAIN || errno == EWOULDBLOCK){std::cout << "数据没有准备好......" << std::endl;sleep(1);//做自己的事情continue;}else if(errno == EINTR){continue;//重新读}else{//这是真的read出错了!perror("read");return -1;}}else{break;}//TODO//sleep(1);//std::cout << "."; //这样的话键盘输入回车,并不会打印出来效果,这是因为C++有语言级的输出缓冲区,没有满足刷新条件//std::cout << ".:" << n << std::endl;}return 0;
}
下一篇就要开启多路转接select了!!!