文章目录
- 一、IO的概念
- 二、IO的五种模型
- 2.1 概念
- 2.2 对比五种IO
- 三、非阻塞IO
- 3.1 fcntl文件描述符控制
- 3.2 以非阻塞轮询方式读取标准输入
一、IO的概念
前面我们说过其实IO就是拷贝数据。
先说一下读取的接口:
当系统调用
read/recv
的时候会有两种情况
①没有数据,阻塞等待。
②有数据,read/recv拷贝完成后返回。
阻塞的本质就是等待资源(缓冲区)就绪。而且写数据也需要的等待(发送缓冲区被写满)。
由此得出IO不仅仅是拷贝数据:
IO = 等待资源就绪 + 拷贝数据
而我们说的IO效率低并不是拷贝的效率低,而是等的时间长。
所以有一个概念叫做高效IO,它的本质就是减少等待的时间(等待的比重)。
二、IO的五种模型
2.1 概念
先举个例子:
现在有几个人在钓鱼:
张三下勾后就一直死盯着鱼鳔,什么都不做,等待着鱼上钩。
李四下勾后一会看看书一会看看鱼鳔一会玩玩手机。
王五在钓竿上挂了个铃铛,下勾后就做自己的事情,铃铛响了头也不抬就钓上了鱼。
赵六有很多鱼竿,全部下勾后就一直遍历看是否有鱼上钩。
田七自己不钓,让别人钓,钓完后通知田七即可,田七最后直接获得鱼。
作为旁观者,我们认为只要一个人等待的时间少那么他的钓鱼效率就高。
由此判断赵六的效率最高,因为他的鱼竿多,鱼上钩的概率大,等待的时间就少。
把上述场景类比到计算机:
张三:阻塞IO
李四:非阻塞IO
王五:信号驱动IO
赵六:多路转接/复用
田七:异步IO
这几个人就相当于进程,田七雇佣的人就是操作系统,鱼就是数据,鱼塘就是内核空间,鱼鳔就是数据就绪的事件,鱼竿就是文件描述符,钓鱼的整个动作就是read/recv调用。
- 阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式。
- 非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询, 这对CPU来说是较大的浪费,一般只有特定场景下才使用
- 信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。
- IO多路转接: 虽然看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。
进程受阻于select调用,select只负责等(无拷贝能力),当文件描述符就绪时(select返回时),用其他的IO类接口完成拷贝。
- 异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少
2.2 对比五种IO
- 阻塞IO、非阻塞IO、信号驱动IO的对比
阻塞IO、非阻塞IO、信号驱动IO它们三个在IO上效率上没有区别(只有一个鱼竿)。
那在其他方面呢?在其他方面 非阻塞IO、信号驱动IO可以做更多的事情。
而阻塞IO和非阻塞IO"钓鱼"是一样的,不同的是等待的方式。
- 阻塞IO与非阻塞IO的对比
阻塞IO当数据资源没有准备好的时候会把进程放到等待队列中挂起,得到结果后才能返回。
而非阻塞IO当数据资源没有准备好的时候会直接返回(得知了数据资源没准备好)。
- 信号驱动IO有没有等待?
等了,只不过等待的方式不一样。
- 同步IO与异步IO
除了异步IO,其他几种IO都是进程自己参与了IO的过程(钓 + 等),所以称为同步IO。
而因为田七没有参与IO的任何一个阶段,所以称作异步IO。
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果。
异步则是相反, 调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后, 被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
- 为什么多路复用IO高效?
因为减少了等待的比重。
三、非阻塞IO
打开文件时默认都是以阻塞的方式打开的,如果要以非阻塞的方式打开某个文件,需要在使用open函数打开文件时携带O_NONBLOCK
或O_NDELAY
选项,此时就能够以非阻塞的方式打开文件。
3.1 fcntl文件描述符控制
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl函数的作用是对文件描述符进行控制操作。它可以实现文件锁定、非阻塞I/O、修改文件状态标志等功能。
参数说明:
fd
:已经打开的文件描述符。
cmd
:需要进行的操作。
…
:可变参数,传入的cmd值不同,后面追加的参数也不同。
fcntl函数常用的5种功能与其对应的cmd取值如下:
复制一个现有的描述符(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)。
- 具体实现非阻塞流程
先调用fcntl函数获取该文件描述符对应的文件状态标记(这是一个位图),此时调用fcntl函数时传入的cmd值为F_GETFL
。
在获取到的文件状态标记上添加非阻塞标记O_NONBLOCK
设置回去。
void setNoBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
3.2 以非阻塞轮询方式读取标准输入
先来看看阻塞式输入的情况:
int main()
{
char buf[1024];
while(1)
{
std::cout << "[input]# ";
fflush(stdout);
ssize_t s = read(0, buf, sizeof buf - 1);
if(s > 0)
{
// 正常读取
buf[s] = '\0';
std::cout << "[echo]# " << buf << std::endl;
}
else if(s == 0)
{
// 输入完了
std::cout << "read end" << std::endl;
break;
}
else
{
// -1
}
}
return 0;
}
可以看到如果我们没输入,它就会阻塞等待。
输入[Ctrl + d]
就表示输入结束:
接下来看看非阻塞:
bool setNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
std::cerr << "fcntl: " << strerror(errno) << std::endl;
return false;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
return true;
}
int main()
{
setNonBlock(0);
char buf[1024];
while(1)
{
std::cout << "[input]# ";
fflush(stdout);
ssize_t s = read(0, buf, sizeof buf - 1);
if(s > 0)
{
// 正常读取
buf[s] = '\0';
std::cout << "[echo]# " << buf << std::endl;
}
else if(s == 0)
{
// 输入完了
std::cout << "read end" << std::endl;
break;
}
else
{
// -1
}
sleep(1);
}
return 0;
}
可以看到一个现象就是我输入我的,它打印它的。
所以我们可以在不输入的时候执行其他任务。
typedef std::function<void()> func_t;
void TaskA()
{
std::cout << "TaskA" << std::endl;
}
void TaskB()
{
std::cout << "TaskB" << std::endl;
}
void TaskC()
{
std::cout << "TaskC" << std::endl;
}
void ExecOther(std::vector<func_t>& v)
{
for(auto& func : v)
{
func();
}
}
int main()
{
std::vector<func_t> cbs;// 回调方法
cbs.push_back(TaskA);
cbs.push_back(TaskB);
cbs.push_back(TaskC);
setNonBlock(0);
char buf[1024];
while(1)
{
std::cout << "[input]# ";
fflush(stdout);
ssize_t s = read(0, buf, sizeof buf - 1);
if(s > 0)
{
// 正常读取
buf[s] = '\0';
std::cout << "[echo]# " << buf << std::endl;
}
else if(s == 0)
{
// 输入完了
std::cout << "read end" << std::endl;
break;
}
else
{
// -1
}
// 执行其他任务
ExecOther(cbs);
sleep(1);
}
return 0;
}
- 当read返回值是-1时如何区分是错误还是底层没有数据?
观察上面的代码,read出错和底层没有数据都会返回-1,那么怎么区分它们呢?
通过错误码。
else
{
// -1
std::cout << "errno: " << strerror(errno) << std::endl;
}
表示资源没有准备好。、
当read函数以非阻塞方式读取标准输入时,当底层数据不就绪时,read函数是以出错的形式返回的,此时的错误码会被设置为EAGAIN
或EWOULDBLOCK
。
此外,调用read函数在读取到数据之前可能会被其他信号中断,此时read函数也会以出错的形式返回,此时的错误码会被设置为EINTR
,此时应该重新执行read函数进行数据的读取。
int main()
{
std::vector<func_t> cbs;// 回调方法
cbs.push_back(TaskA);
cbs.push_back(TaskB);
cbs.push_back(TaskC);
setNonBlock(0);
char buf[1024];
while(1)
{
std::cout << "[input]# ";
fflush(stdout);
ssize_t s = read(0, buf, sizeof buf - 1);
if(s > 0)
{
// 正常读取
buf[s] = '\0';
std::cout << "[echo]# " << buf << std::endl;
}
else if(s == 0)
{
// 输入完了
std::cout << "read end" << std::endl;
break;
}
else
{
// -1
if(errno == EAGAIN)
{
// 底层没有数据
// 执行其他任务
ExecOther(cbs);
}
else if(errno == EINTR)
{
// 被信号中断
continue;
}
else
{
std::cout << "errno: " << strerror(errno) << std::endl;
break;
}
}
sleep(1);
}
return 0;
}