IO模型介绍(select、poll、epoll)
什么是IO?
IO中的I就是input,O就是output,IO模型即输入输出模型,而比较常听说的便是磁盘IO,网络IO。
什么是操作系统的IO?
我们如果需要对磁盘进行读取或者写入数据的时候必须得有主体去操作,这个主体就是应用程序。 应用程序是不能直接进行一些读写操作(IO)的,因为用户可能会利用此程序直接或者间接的对计算机造成破坏,只能交给底层软件—操作系统.也就是说应用程序想要对磁盘进行读取或者写入数据,只能通过操作系统对上层开放的API来进行。在任何一个应用程序里面,都会有进程地址空间,该空间分为两部分,一部分称为用户空间(允许应用程序进行访问的空间),另一部分称为内核空间(只能给操作系统进行访问的空间,它受到保护)。
应用程序想要进行一次IO操作分为两个阶段:
•IO调用:应用程序进程向操作系统内核发起调用【1】。
•IO执行:操作系统内核完成IO操作【2】。
操作系统完成一次IO操作包括两个过程:
•数据准备阶段:内核等待I/O设备准备好数据(从网卡copy到内核缓冲区)【3】。
•数据copy阶段:将数据从内核缓冲区copy到用户进程缓冲区【4】。
应用程序一次I/O流程如下:
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-02-29-17-25FErjeAQTMnatHT0.png)
一个完整的IO过程包括以下几个步骤:
1.应用程序进程向操作系统发起IO调用请求。
2.操作系统准备数据,外部设备的数据通过网卡加载到内核缓冲区。
3.操作系统拷贝数据,即将内核缓冲区的数据copy到用户进程缓冲区。
而一次IO的本质其实就是: 等待 + 拷贝
IO模型有哪些?
1.阻塞式 IO:
服务端为了处理客户端的连接和数据处理:
伪代码具体如下:
listenfd = socket(); // 打开一个网络通信套接字
bind(listenfd); // 绑定
listen(listenfd); // 监听
while(true) {
buf = new buf[1024]; // 读取数据容器
connfd = accept(listenfd); // 阻塞 等待建立连接
int n = read(connfd, buf); // 阻塞 读数据
doSomeThing(buf); // 处理数据
close(connfd); // 关闭连接
}
上面的伪代码中我们可以看出,服务端处理客户端的请求阻塞在两个地方,一个是 accept、一个是 read ,我们这里主要研究 read 的过程,可以分为两个阶段:等待读就绪(等待数据到达网卡 & 将网卡的数据拷贝到内核缓冲区)、读数据。
阻塞IO流程如下:
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-02-29-17-31fvwrM26bLJiP229t.png)
2.非阻塞式 IO:
非阻塞式 IO 我们应该让操作系统提供一个非阻塞的 read() 函数,当第一阶段读未就绪时返回 -1 ,当读已就绪时才进行数据的读取。
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询(for(connfd : arr)). 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用.
伪代码具体如下:
arr = new Arr[];
listenfd = socket(); // 打开一个网络通信套接字
bind(listenfd); // 绑定
listen(listenfd); // 监听
while(true) {
connfd = accept(listenfd); // 阻塞 等待建立连接
arr.add(connfd);
}
// 异步线程检测 连接是否可读
new Tread(){
for(connfd : arr){
buf = new buf[1024]; // 读取数据容器
// 非阻塞 read 最重要的是提供了我们在一个线程内管理多个文件描述符的能力
int n = read(connfd, buf); // 检测 connfd 是否可读
if(n != -1){
newThreadDeal(buf);