看一遍就理解:IO模型详解

本文详细讲解了IO模型的基本概念,包括阻塞与非阻塞IO、同步与异步的区别,以及select、poll、epoll和AIO的运作机制。通过生活实例和专业术语,对比了BIO、NIO和AIO的性能优劣,帮助读者全面理解IO模型的演变和优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

大家好,我是狼王。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~

什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll跟IO模型有什么关系?有几种经典IO模型呢?BIO、NIO、AIO到底有什么区别的?

如果这些问题,你都能很好答上的话,那恭喜你,你对IO的掌握已经很棒啦!那你跟田螺哥一起看完这篇文章,再复习一下,加深印象吧~如果你对这些问题模棱两可的话,那也没关系,看完这篇文章,就理解啦!

da43c302e71eefb8c2a4edec8e30d242.png

什么是IO呢?

IO,英文全称是Input/Output,翻译过来就是输入/输出。平时我们听得挺多,就是什么磁盘IO,网络IO。那IO到底是什么呢?是不是有种懵懵懂懂的感觉呀,好像大概知道它是什么,又好像说不清楚。

IO,即输入/输出,到底谁是输入?谁是输出呢?IO如果脱离了主体,就会让人疑惑。

计算机角度的IO

我们常说的输入输出,比较直观的意思就是计算机的输入输出计算机就是主体。大家是否还记得,大学学计算机组成原理的时候,有个冯.诺依曼结构,它将计算机分成分为5个部分:运算器、控制器、存储器、输入设备、输出设备。

a7a2a96b8579816caa29f53e7182e4ca.png

输入设备是向计算机输入数据和信息的设备,键盘,鼠标都属于输入设备;输出设备是计算机硬件系统的终端设备,用于接收计算机数据的输出显示,一般显示器、打印机属于输出设备。

例如你在鼠标键盘敲几下,它就会把你的指令数据,传给主机,主机通过运算后,把返回的数据信息,输出到显示器。

鼠标、显示器这只是直观表面的输入输出,回到计算机架构来说,涉及计算机核心与其他设备间数据迁移的过程,就是IO。如磁盘IO,就是从磁盘读取数据到内存,这算一次输入,对应的,将内存中的数据写入磁盘,就算输出。这就是IO的本质。

操作系统的IO

我们要将内存中的数据写入到磁盘的话,主体会是什么呢?主体可能是一个应用程序,比如一个Java进程(假设网络传来二进制流,一个Java进程可以把它写入到磁盘)。

操作系统负责计算机的资源管理和进程的调度。我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等等。因为这些都是比较危险的操作,不可以由应用程序乱来,只能交给底层操作系统来。也就是说,你的应用程序要把数据写入磁盘,只能通过调用操作系统开放出来的API来操作。

  • 什么是用户空间?什么是内核空间?

  • 以32位操作系统为例,它为每一个进程都分配了4G(2的32次方)的内存空间。这4G可访问的内存空间分为二部分,一部分是用户空间,一部分是内核空间。内核空间是操作系统内核访问的区域,是受保护的内存空间,而用户空间是用户应用程序访问的内存区域。

我们应用程序是跑在用户空间的,它不存在实质的IO过程,真正的IO是在操作系统执行的。即应用程序的IO操作分为两种动作:IO调用和IO执行。IO调用是由进程(应用程序的运行态)发起,而IO执行是操作系统内核的工作。此时所说的IO是应用程序对操作系统IO功能的一次触发,即IO调用。

操作系统的一次IO过程

应用程序发起的一次IO操作包含两个阶段:

  • IO调用:应用程序进程向操作系统内核发起调用。

  • IO执行:操作系统内核完成IO操作。

操作系统内核完成IO操作还包括两个过程:

  • 准备数据阶段:内核等待I/O设备准备好数据

  • 拷贝数据阶段:将数据从内核缓冲区拷贝到用户进程缓冲区

1e28bb9f8489050cdf5e6d6c59deac4a.png

其实IO就是把进程的内部数据转移到外部设备,或者把外部设备的数据迁移到进程内部。外部设备一般指硬盘、socket通讯的网卡。一个完整的IO过程包括以下几个步骤:

  • 应用程序进程向操作系统发起IO调用请求

  • 操作系统准备数据,把IO外部设备的数据,加载到内核缓冲区

  • 操作系统拷贝数据,即将内核缓冲区的数据,拷贝到用户进程缓冲区

阻塞IO模型

我们已经知道IO是什么啦,那什么是阻塞IO呢?

假设应用程序的进程发起IO调用,但是如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次IO操作,称之为阻塞IO

c5350b301f46ea8193a504db1157ceed.png
  • 阻塞IO比较经典的应用就是阻塞socket、Java BIO

  • 阻塞IO的缺点就是:如果内核数据一直没准备好,那用户进程将一直阻塞,浪费性能,可以使用非阻塞IO优化。

非阻塞IO模型

如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求。这就是非阻塞IO,流程图如下:

91edfe832830a04c101f2d0266050d17.png

非阻塞IO的流程如下:

  • 应用进程向操作系统内核,发起recvfrom读取数据。

  • 操作系统内核数据没有准备好,立即返回EWOULDBLOCK错误码。

  • 应用程序进程轮询调用,继续向操作系统内核发起recvfrom读取数据。

  • 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。

  • 完成调用,返回成功提示。

非阻塞IO模型,简称NIONon-Blocking IO。它相对于阻塞IO,虽然大幅提升了性能,但是它依然存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU资源。可以考虑IO复用模型,去解决这个问题。

IO多路复用模型

既然NIO无效的轮询会导致CPU资源消耗,我们等到内核数据准备好了,主动通知应用进程再去进行系统调用,那不就好了嘛?

在这之前,我们先来复习下,什么是文件描述符fd(File Descriptor),它是计算机科学中的一个术语,形式上是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

IO复用模型核心思路:系统给我们提供一类函数(如我们耳濡目染的select、poll、epoll函数),它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。

IO多路复用之select

应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起recvfrom请求去读取数据。

6765982b130c3eb79a986da33ed1c455.png

非阻塞IO模型(NIO)中,需要N(N>=1)次轮询系统调用,然而借助select的IO多路复用模型,只需要发起一次询问就够了,大大优化了性能。

但是呢,select有几个缺点:

  • 监听的IO最大连接数有限,在Linux系统上一般为1024。

  • select函数返回后,是通过遍历fdset,找到就绪的描述符fd。(仅知道有I/O事件发生,却不知是哪几个流,所以遍历所有流

因为存在连接数限制,所以后来又提出了poll。与select相比,poll解决了连接数限制问题。但是呢,select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降

因此经典的多路复用模型epoll诞生。

IO多路复用之epoll

为了解决select/poll存在的问题,多路复用模型epoll诞生,它采用事件驱动来实现,流程图如下:

cc83c2715e550c9b2ba6108a7e23d3b1.png

epoll先通过epoll_ctl()来注册一个fd(文件描述符),一旦基于某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。这就是epoll的亮点。

我们一起来总结一下select、poll、epoll的区别


selectpollepoll
底层数据结构数组链表红黑树和双链表
获取就绪的fd遍历遍历事件回调
事件复杂度O(n)O(n)O(1)
最大连接数1024无限制无限制
fd数据拷贝每次调用select,需要将fd数据从用户空间拷贝到内核空间每次调用poll,需要将fd数据从用户空间拷贝到内核空间使用内存映射(mmap),不需要从用户空间频繁拷贝fd数据到内核空间

epoll明显优化了IO的执行效率,但在进程调用epoll_wait()时,仍然可能被阻塞。能不能酱紫:不用我老是去问你数据是否准备就绪,等我发出请求后,你数据准备好了通知我就行了,这就诞生了信号驱动IO模型

IO模型之信号驱动模型

信号驱动IO不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号(调用sigaction的时候建立一个SIGIO的信号),然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过SIGIO信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用recvfrom,去读取数据。

ef9d131dee525acb2e72d5c561868dad.png

信号驱动IO模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。它已经有异步操作的感觉了。但是你细看上面的流程图,发现数据复制到应用缓冲的时候,应用进程还是阻塞的。回过头来看下,不管是BIO,还是NIO,还是信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的。还有没有优化方案呢?AIO(真正的异步IO)!

IO 模型之异步IO(AIO)

前面讲的BIO,NIO和信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的,因此都不算是真正的异步。AIO实现了IO全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程IO操作执行完毕。

流程如下:1db0a35b65c45d36b1c5933e4b0a6e4d.png

异步IO的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。日常开发中,有类似思想的业务场景:

比如发起一笔批量转账,但是批量转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。

阻塞、非阻塞、同步、异步IO划分

2f48bae831136d3f79f32ba08eb6ffc5.png
IO模型
阻塞I/O模型同步阻塞
非阻塞I/O模型同步非阻塞
I/O多路复用模型同步阻塞
信号驱动I/O模型同步非阻塞
异步IO(AIO)模型异步非阻塞

一个通俗例子读懂BIO、NIO、AIO

  • 同步阻塞(blocking-IO)简称BIO

  • 同步非阻塞(non-blocking-IO)简称NIO

  • 异步非阻塞(asynchronous-non-blocking-IO)简称AIO

一个经典生活的例子:

  • 小明去吃同仁四季的椰子鸡,就这样在那里排队,等了一小时,然后才开始吃火锅。(BIO)

  • 小红也去同仁四季的椰子鸡,她一看要等挺久的,于是去逛会商场,每次逛一下,就跑回来看看,是不是轮到她了。于是最后她既购了物,又吃上椰子鸡了。(NIO

  • 小华一样,去吃椰子鸡,由于他是高级会员,所以店长说,你去商场随便逛会吧,等下有位置,我立马打电话给你。于是小华不用干巴巴坐着等,也不用每过一会儿就跑回来看有没有等到,最后也吃上了美味的椰子鸡(AIO

最后

希望本文对你有帮助哦,如果文章有哪些地方你觉得不对的,可以提出来哈,一起学习进步。我是程序员田螺,我们下期再见! 求一键三连:点赞、转发、在看~

参考与感谢

  • 程序员应该这样理解IO[1]

  • Linux IO模式及 select、poll、epoll详解[2]

  • IO 模型知多少 | 理论篇[3]

  • 100%弄明白5种IO模型[4]

参考资料

[1]

程序员应该这样理解IO: https://www.jianshu.com/p/fa7bdc4f3de7

[2]

Linux IO模式及 select、poll、epoll详解: https://segmentfault.com/a/1190000003063859

[3]

IO 模型知多少 | 理论篇: https://cloud.tencent.com/developer/article/1648650

[4]

100%弄明白5种IO模型: https://zhuanlan.zhihu.com/p/115912936

import h5py import scipy.io as io import PIL.Image as Image import numpy as np import os import glob from matplotlib import pyplot as plt import matplotlib.image as mpimg from scipy.ndimage.filters import gaussian_filter import scipy import json import torchvision.transforms.functional as F from matplotlib import cm as CM from image import * #from model_soft import SoftCSRNet from model_origin import CSRNet import torch #%matplotlib inline from torchvision import datasets, transforms transform=transforms.Compose([ transforms.ToTensor(),transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) root = './dataset/' #now generate the ShanghaiA's ground truth part_A_train = os.path.join(root,'part_A_final/train_data','images') part_A_test = os.path.join(root,'part_A_final/test_data','images') part_B_train = os.path.join(root,'part_B_final/train_data','images') part_B_test = os.path.join(root,'part_B_final/test_data','images') path_sets = [part_A_test] img_paths = [] for path in path_sets: for img_path in glob.glob(os.path.join(path, '*.jpg')): img_paths.append(img_path) model = CSRNet() print(model.state_dict().keys()) model = model.cuda() checkpoint = torch.load('./PartBmodel_best.pth.tar') print(checkpoint['state_dict'].keys()) model.load_state_dict(checkpoint['state_dict']) mae = 0 mse = 0 for i in range(len(img_paths)): img = 255.0 * F.to_tensor(Image.open(img_paths[i]).convert('RGB')) img[0,:,:]=img[0,:,:]-92.8207477031 img[1,:,:]=img[1,:,:]-95.2757037428 img[2,:,:]=img[2,:,:]-104.877445883 img = img.cuda() # 计算模型输出 output = model(img.unsqueeze(0)) # 将输出的密度图转为 NumPy 数组 output_density = output.squeeze().detach().cpu().numpy() # 读取 ground truth 密度图 gt_file = h5py.File(img_paths[i].replace('.jpg','.h5').replace('images','ground_truth'),'r') groundtruth = np.asarray(gt_file['density']) # 计算 MAE mae += np.abs(output_density.sum() - np.sum(groundtruth)) # 计算平方差并累加到 MSE 中 squared_diff = np.square(output_density.sum() - np.sum(groundtruth)) mse += squared_diff print(i, "MAE:", mae, "MSE:", mse) # 计算 MAE 和 MSE mae /= len(img_paths) mse /= len(img_paths) print("Mean Absolute Error (MAE):", mae) print("Mean Squared Error (MSE):", mse)
03-14
<think>好的,我现在需要分析用户提供的这段代码,并解释其中涉及的库和主要流程。首先,用户希望了解关于导入h5py等库的信息,以及整个代码的功能。让我先通读一遍代码,理解各个部分的作用。 首先,代码开头导入了多个库:h5py、scipy.io、PIL.Image、numpy、os、glob、matplotlib等。这些库主要用于数据处理、图像处理、文件操作和可视化。接着,代码中还有一些自定义模块的导入,比如从image导入所有内容,以及从model_origin导入CSRNet模型。不过,部分导入被注释掉了,比如model_soft的SoftCSRNet,这可能意味着在开发过程中尝试过不同的模型结构。 接下来,定义了一个transform,使用torchvision.transforms中的Compose组合了ToTensor和Normalize操作。ToTensor将图像转换为张量,并调整通道顺序,Normalize则对图像进行标准化处理,使用的均值和标准差应该是ImageNet的标准参数。 然后,设置了一些路径变量,指向数据集的不同部分,比如part_A_train、part_A_test等。这些路径可能对应ShanghaiTech数据集的两个部分A和B。通过遍历这些路径,收集所有.jpg图像的路径到img_paths列表中。 之后,初始化了CSRNet模型,并将其转移到CUDA设备上。加载预训练的权重文件PartBmodel_best.pth.tar,并将参数加载到模型中。这里需要注意,加载模型时使用了checkpoint['state_dict'],这可能是因为保存的模型包含其他信息如优化器状态,而模型参数存储在state_dict中。 然后,初始化了MAE和MSE的变量,用于评估模型性能。接下来是一个循环,遍历所有测试图像路径。对于每张图像,使用PIL.Image打开并转换为RGB格式,然后通过F.to_tensor处理,乘以255可能是在调整像素范围?不过通常ToTensor会将像素值归一化到0-1,所以这里乘以255可能是在还原原始范围。然后对每个通道进行减去均值的操作,这里的均值可能与之前提到的标准化参数有关,但需要确认是否正确应用了预处理步骤。 处理后的图像被转移到CUDA,然后通过模型得到输出密度图。将输出的密度图转换为numpy数组,并读取对应的ground truth密度图(存储在.h5文件中)。计算MAE和MSE时,分别累加绝对差和平方差。 最后,计算平均MAE和MSE,并输出结果。用户可能在运行人群计数模型的测试过程,使用CSRNet对ShanghaiTech数据集进行测试,并评估模型的性能指标。 现在需要检查代码中可能存在的问题。例如,在图像预处理部分,用户使用了F.to_tensor然后乘以255,但通常ToTensor会将图像转换为[0,1]范围,乘以255后可能恢复到0-255,但之后又对每个通道减去了特定的均值(如92.82等),这些均值看起来像是针对原始图像的,而不是标准化后的。但通常在使用预训练模型时,标准化参数应该是基于ImageNet的均值[0.485, 0.456, 0.406]和标准差[0.229, 0.224, 0.225]。但在这段代码中,用户直接对每个通道减去了不同的数值,这可能存在错误,因为正确的预处理应该是在归一化到0-1后,再进行标准化,即减去均值并除以标准差。但代码中的处理方式可能不正确,导致输入数据分布不符合模型预期,影响预测结果。 另一个需要注意的地方是,加载模型参数时是否结构匹配。用户打印了model.state_dict().keys()和checkpoint['state_dict'].keys(),可能是为了检查参数名称是否一致,避免加载错误。如果两者键名不一致,可能导致加载失败,但根据用户代码中的输出,可能已经确认了这一点。 此外,在计算MAE和MSE时,代码将密度图的总和作为预测人数,与ground truth的总和进行比较。这是人群计数的标准做法,因为密度图的总和即为估计的人数。 总结来说,这段代码的主要流程是:加载预训练的人群计数模型CSRNet,对测试集中的每张图像进行预处理,通过模型预测密度图,计算与真实密度图的总人数差异,最终输出平均MAE和MSE作为评估指标。可能存在的问题在于图像预处理的步骤是否正确应用了标准化参数,需要用户检查是否符合模型训练时的预处理流程。</think>以下是代码的逐步解释及相关库的介绍: --- ### **1. 主要库介绍** - **`h5py`**: 用于读写HDF5格式文件,常用于存储大型科学数据。此处用于读取标注的密度图(`.h5`文件)。 - **`scipy.io`**: SciPy的输入输出模块,用于处理.mat等格式数据(但代码中未直接使用)。 - **`PIL.Image`** (Pillow): 图像处理库,用于加载、转换图像。 - **`numpy`**: 数值计算库,处理数组和矩阵运算。 - **`os` + `glob`**: 文件路径操作,遍历目录中的图像文件(如`glob.glob(os.path.join(path, '*.jpg'))`)。 - **`matplotlib`**: 绘图库,用于显示图像和密度图(但代码中未直接调用显示函数)。 - **`torch` + `torchvision`**: PyTorch深度学习框架及视觉工具库,用于模型定义、数据预处理(如`transforms`)。 --- ### **2. 代码功能概述** 此代码实现了一个**人群计数模型(CSRNet)的测试流程**,目标是计算模型在ShanghaiTech数据集上的MAE(平均绝对误差)和MSE(均方误差)。 --- ### **3. 关键步骤详解** #### **(1) 数据预处理** ```python transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) ``` - **`ToTensor()`**: 将图像转换为PyTorch张量,并归一化像素值到$[0,1]$。 - **`Normalize()`**: 标准化处理,使用ImageNet的均值和标准差。 #### **(2) 加载模型** ```python model = CSRNet().cuda() checkpoint = torch.load('./PartBmodel_best.pth.tar') model.load_state_dict(checkpoint['state_dict']) ``` - **`CSRNet()`**: 定义人群计数模型(基于论文[CSRNet](https://arxiv.org/abs/1802.10062))。 - **`.cuda()`**:模型转移到GPU。 - **加载预训练权重**: 从检查点文件加载模型参数。 #### **(3) 遍历测试图像** ```python for img_path in img_paths: img = 255.0 * F.to_tensor(Image.open(img_path).convert('RGB')) img[0,:,:] -= 92.82 # 手动调整通道均值(可能存在问题) img[1,:,:] -= 95.27 img[2,:,:] -= 104.87 img = img.cuda() ``` - **图像加载**: 使用PIL读取图像并转换为RGB格式。 - **预处理问题**: 直接对张量通道减去固定值,可能与标准化步骤冲突。正确做法应在`transform`中统一处理。 #### **(4) 模型推理与评估** ```python output = model(img.unsqueeze(0)) # 添加批次维度 output_density = output.squeeze().detach().cpu().numpy() # 读取真实密度图 gt_file = h5py.File(img_path.replace('.jpg','.h5'), 'r') groundtruth = np.asarray(gt_file['density']) # 计算指标 mae += abs(output_density.sum() - groundtruth.sum()) mse += (output_density.sum() - groundtruth.sum())**2 ``` - **密度图求和**: 密度图所有元素的和即为预测人数。 - **MAE/MSE**: 统计模型预测人数与真实人数的误差。 --- ### **4. 潜在问题与改进** - **预处理不一致**: 代码中手动调整通道值,可能与`transforms.Normalize`冲突。建议统一使用`transform`处理。 - **均值参数疑问**: 代码中减去的均值(92.82, 95.27, 104.87)可能与实际训练时的预处理不一致,需检查是否符合模型要求。 - **数据集路径**: 需确保`root = './dataset/'`路径正确指向ShanghaiTech数据集。 --- ### **5. 输出结果** 最终输出测试集的平均MAE和MSE,例如: ``` Mean Absolute Error (MAE): 12.5 Mean Squared Error (MSE): 225.0 ``` - **MAE**:平均绝对误差越小,模型越准确。 - **MSE**:对大误差更敏感,常用于衡量稳定性。 --- 如需进一步优化或调试,可检查预处理逻辑,并确保模型输入与训练时一致。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值