什么是IO?
IO是指计算机系统与外部世界进行数据交换的过程。在计算机中,IO通常用于与外部设备通信,这些设备包括键盘、鼠标、打印机、显示器、网络等。通过IO操作,计算机系统可以接收来自外部设备的输入数据,也可以将处理后的数据输出到外部设备。
在网络通信上,主要表示在计算机与计算机之间能通过互联网来进行数据交换,从而实现远程数据与资源的共享。
五种IO模型
阻塞IO
用户线程在发起IO请求后会阻塞,直到IO操作完成(内核数据准备好)并返回结果。这种方式简单但效率低下,特别是在高并发场景下。
非阻塞IO
如果内核还没有准备好数据,内核也会进行返回,并且返回一个EWOULDBLOCK错误码;
非阻塞IO需要在代码中循环,不断的查询读写这个文件描述符,这种操作称为轮询。在特定场景中,才会进行使用,会比较浪费CPU资源。
信号驱动IO
当内核将数据准备好时,内核会向应用进程发送一个SIGIO信号,通知程序进行IO操作。
IO多路转接
系统调用允许单个线程同时监视多个文件描述符的IO就绪状态,从而提高了IO操作的效率。
异步IO
异步IO是指系统内核主动发起IO请求,用户空间的线程被动接受IO操作的结果。这种方式下,用户线程无需等待IO操作完成即可继续执行其他任务,当IO操作完成后,内核会通知用户线程处理结果。
小结
任何IO过程,都包含两个步骤:等待数据+拷贝数据。
在实际应用中,等待消耗的时间往往大于拷贝消耗的时间。
所以,要想提高IO的效率,就要减少等待的消耗时间。
高级IO的重要概念
同步IO
同步IO是指需要等待前面程序完成后,才能继续执行后面的程序。这意味着当程序发起一个IO请求时,它会阻塞当前线程的执行,直到IO操作完成。
特点:
- 阻塞性:同步IO在读写数据时会阻塞程序的执行,导致程序无法充分利用CPU资源。
- 顺序性:同步IO按照程序指定的顺序依次执行IO操作,前一个IO操作完成后,下一个IO操作才会开始。
- 简单性:同步IO的实现相对简单,因为程序只需按照顺序执行IO操作,无需处理复杂的并发逻辑。
同步IO在IO操作期间会阻塞程序执行,导致CPU资源无法得到充分利用。
当IO操作时间较长时,程序会处于等待状态,造成CPU资源的浪费。
同步IO适用于一些简单的、不需要高并发处理的场景。例如,批处理作业、简单计算和查询程序等。
在这些场景中,同步IO的阻塞性和顺序性并不会对程序的性能产生太大影响。
异步IO
异步IO是指在进行数据读写操作时,程序无需等待IO操作完成。程序会发起IO操作,并立即返回一个标识符或回调函数,用于指示IO操作的状态或结果。
特点:
- 非阻塞性:异步IO允许程序在发起IO请求后继续执行其他任务,无需等待IO操作完成。
- 并发性:异步IO可以同时处理多个IO请求,提高了程序的并发处理能力。
- 复杂性:异步IO的实现相对复杂,因为程序需要处理IO完成的通知和管理异步操作的状态。这通常涉及到事件循环、回调函数等概念。
异步IO可以充分利用CPU资源,因为程序可以在IO操作期间执行其他任务。
异步IO通过并发处理多个IO请求,提高了系统的整体性能和资源利用率。
异步IO适用于需要处理大量并发IO请求的场景。例如,Web服务器、数据库访问、网络编程等。
在这些场景中,异步IO的非阻塞性和并发性能够显著提高程序的执行效率和并发处理能力。
非阻塞IO的实现
对于C语言/C++来说,默认都是阻塞IO,如果要改为非阻塞IO,就要通过特定函数来进行实现;
fcntl()
在Linux系统中用于操作文件描述符的库函数。
#include <fcntl.h>
#include <unistd.h>
int fcntl(int fd, int cmd, ... /* arg */ );
参数:
- fd: 文件描述符,是一个非负整数,表示要操作的文件。
- cmd: 一个命令,用于指定要执行的操作。不同的命令需要不同数量的附加参数。
- … /* arg */: 根据 cmd 命令的不同,可能需要一个或多个附加参数。这些参数通常是一个指向数据的指针。
常用命令
- F_DUPFD: 复制文件描述符。
- F_GETFD: 获取文件描述符标志。
- F_SETFD: 设置文件描述符标志。
- F_GETFL: 获取文件状态标志(如只读、非阻塞等)。
- F_SETFL: 设置文件状态标志。
- F_GETLK: 获取文件锁的状态。
- F_SETLK: 设置文件锁(非阻塞)。
- F_SETLKW: 设置文件锁(阻塞)。
#include<iostream>
#include<unistd.h>
#include<fcntl.h>
void SetNonBlock(int fd)
{
int fl = ::fcntl(fd, F_GETFL);//获取fd当前文件描述符状态,默认为阻塞IO
if(fl < 0)
{
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);//将其设置为非阻塞IO
}
int main()
{
char buffer[1024];
SetNonBlock(0);//设置成非阻塞的
while (true)
{
//读取标准输入流的数据
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
if(s > 0)
{
buffer[s] = 0;
std::cout << "Echo# " << buffer << std::endl;
}
else
{
//问题:我怎么知道是底层IO条件不就绪,还是读取错误了呢???
// 底层IO条件就绪和读取错误采用的是同样返回值操作的
if(errno == EWOULDBLOCK || errno == EAGAIN)
{
std::cout << "底层数据没有就绪, 下次在试试吧! 做做其他事情!" << std::endl;
sleep(1);
continue;
}
std::cout << "读取错误... s : " << s << " errno: " << errno << std::endl;
sleep(1);
}
}
return 0;
}
在阻塞IO中,read函数如果没有接收到输入的数据,那么就会一直阻塞,直到有数据的输入;
非阻塞IO中,如果没有输入数据,那么read函数也不会阻塞,会因为while循环进行轮询,不断的执行当前主函数,一旦read函数收到数据,那么就会打印对应数据。