【OS】5种网络IO模型

本文详细介绍了五种主要的IO模型:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO及异步IO,并对比了它们在处理IO操作时的不同特性与优劣。

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

 参考地址:http://www.cnblogs.com/findumars/p/6361627.html

https://time.geekbang.org/column/article/9293

目录

阻塞IO(Blocking IO)

非阻塞IO(Non-Blocking IO)-轮询

多路复用IO(IO Multiplexing)- 事件驱动IO

信号驱动IO

异步IO(Asynchronous IO)- 真正的非阻塞

总结



什么是IO

Java中I/O操作主要是指使用Java进行输入,输出操作。 Java所有的I/O机制都是基于数据流进行输入输出,这些数据流表示了字符或者字节数据的流动序列

 

IO要经历的阶段

以接收(read)IO流为例,会涉及到两个对象:调用IO的进程(线程)、系统内核(kernel)

一个read操作:

    1)数据准备阶段(Socket --> 内核)
    2)将数据从内核拷贝到进程中(内核 --> 进程)

 

本文讨论的背景是Linux环境下的network IO。本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”,Stevens在这节中详细说明了各种IO的特点和区别,如果英文够好的话,推荐直接阅读。Stevens的文风是有名的深入浅出,所以不用担心看不懂。本文中的流程图也是截取自参考文献。

 

阻塞IO(Blocking IO)


 *特点:IO执行的两个阶段(等待+拷贝)都被阻塞了

 

角色:

  • 客户端(进程(应用application)+内核kernel+Socket)
  • 服务端(Socket)

 

服务端内的IO过程

datagram ready 就是从Socket获取数据的结果

当用户调用了recvfrom(),如果此时内核没有准备好数据(no datagram ready),整个用户进程就①阻塞

当内核准备好数据(datagram ready),内核就将数据复制到用户进程的内存,这个过程用户进程②阻塞直到kernel返回结果(copy complete --> return OK)

简而言之:等待数据和拷贝数据都阻塞了

 

 客户端与服务端之间的IO过程UDP(recvfrom() + sendto())

刚才分析过,recvfrom()是阻塞的,而Socket的其他方法sendto()也是阻塞的,这种时候就是“一问一答”,也就是服务器专心只解决你一个客户端的问题,此时服务器的线程无法执行任何运算或响应任何的网络请求,影响其他进程/线程。这时就可以用多线程/多进程来让服务器分身出来解决其他线程/进程的请求。

解决服务器IO阻塞(服务器端-多进程/多线程)

*开启多进程/多线程不代表 异步,因为还是自己(自己是进程/线程)来处理

要交给不同的对象(比如内核)来处理,然后自己向下执行才算是异步

多进程/多线程的目的就是让每一个连接都拥有独立的进程/线程,这样任何一个连接的阻塞都不会影响其他进程/线程

UDP的多线程服务器模型

 

TCP的多线程服务器模型

*一个Socket可以accpet()多次原因: accpet()返回的是一个新socket

 

 而通过多线程和多进程解决的区别,其实就是进程和线程的区别:最主要的是资源问题

  • 多进程就相当于,每有一个项目,就开一个公司来做这个项目(在父进程的基础上完全拷贝一个子进程fork(),在 Linux 内核中,会复制文件描述符的列表,也会复制内存空间,还会复制一条记录当前执行到了哪一行程序的进程)
  •  多线程就相当于,在同一个公司,开多一个项目组来做这个项目(很多资源,例如文件描述符列表、进程空间,还是共享的,只不过多了一个引用而已)

多线程和多进程解决服务器IO阻塞的不足之处

  上述多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求,但其实并不尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率(太多线程/进程),而线程与进程本身也更容易进入假死状态
    很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

走到这里,对于BIO(阻塞IO)的优化已经差不多了,而还是有局限性,也就是面对大规模的服务请求,会遇到瓶颈(线程池/连接池请求超过上限),所以可以考虑用NIO(非阻塞IO)

 

 

非阻塞IO(Non-Blocking IO)-轮询


*特点:非阻塞的接口相比于阻塞型接口,前者在被调用之后立即返回

*缺点:循环调用recvfrom()大幅度占用CPU资源

 

这里的非阻塞指的是,recvfrom()执行时发现内核还没准备好数据就直接返回

而在内核准备好数据时,复制内核数据到线程内存还是会阻塞线程的。

也就是等待非阻塞(等待Socket --> 内核),而复制阻塞(内核复制 --> 线程内存)了,而对于异步就是两者都非阻塞

 

Linux下,可以通过设置socket来使其变为非阻塞IO

服务端内的IO过程 

 可以看到recvfrom(),如果内核还没有数据会立刻返回结果(EWOULDBLOCK)

也就是说,对于用户发起一个recvfrom()操作可以立刻得到结果。如果得到是error,就知道内核还没准备好数据;反之..

所以,在NIO中,用户进程需要轮询内核是否准备好数据

 

客户端与服务端之间的IO

使用如下的函数可以将某句柄fd设为非阻塞状态(也就是将Socket设置成NIO?)
    fcntl( fd, F_SETFL, O_NONBLOCK ); 

可以看到recv(fd1)没有阻塞,因为有返回错误了就继续往下执行

 

 

多路复用IO(IO Multiplexing)- 事件驱动IO


*优势:能同时处理更多的连接,而不是对于单个连接能够处理的更快

解决NIO中循环调用recvfrom()占用高CPU的问题 --> 调用一个select()

 

服务器内的IO过程(select-1024个socket)

线程/进程执行select(阻塞)通过不断的轮询所有的socket,如果某个socket有数据到达了(return readable),就通知用户进程

 

与BIO的区别:应用(application)有了两个系统调用select + recvfrom,所以对于处理的连接数不是很多时,多路复用IO不一定就比BIO+多线程

poll

 轮询的是链表(准备就绪的socket)

epoll

轮询的是链表(准备就绪的socket),还有红黑树(存放未准备就绪的socket),共享内存(加快线程间传输速度)

将红黑树放到链表的过程中会执行回调函数callback,通知线程,这样就不用轮询了

 

信号驱动IO


*特点:等待(不阻塞)+ 拷贝(阻塞)服务端内IO

通过传SIGIO给signal handler来提醒线程数据准备好了

类似于NIO,但是不用循环执行recvfrom(),但是多了一个系统调用establish SIGIO,适用于长时间Socket没有准备好数据的情况

 

 

异步IO(Asynchronous IO)- 真正的非阻塞


*特点:IO执行的两个阶段(等待+拷贝)都没有阻塞

*特点:事情交给内核来处理(异步)

 

服务端的IO

从用户(application)的角度:发起read操作后就可以做其他事了(所谓异步:就好像所有事情交给内核来处理了)

从内核(kernel)的角度:收到一个asynchronous read(aio_read)后,首先会立刻返回(read),然后等数据准备完成,将数据拷贝到用户内存,然后给用户进程发送一个信号(deliver signal)表示read操作完成

 

 

 

总结


这个图是对于要执行IO操作的进程所执行的方法

☆☆☆☆BIO/NIO/多路复用/信号驱动IO 都是同步IO(还得自己负责等待和复制)☆☆☆☆

异步IO就像进程将整个IO操作交给内核处理

对于BIO和NIO,BIO的两个阶段(等待和复制)都会阻塞,而NIO只有在复制的时候才会阻塞

### Python 实现基于 CNN 的车牌识别 为了实现车牌识别功能,可以采用卷积神经网络(CNN),这是一种特别适合于图像处理任务的深度学习模型。下面是一个完整的Python代码示例,展示了如何构建一个能够识别车牌上字符的CNN模型。 #### 加载依赖库 首先安装必要的库文件: ```bash pip install tensorflow numpy opencv-python pillow matplotlib scikit-image ``` 接着导入所需的模块: ```python import cv2 from skimage import io, transform import numpy as np import os from PIL import ImageFont, ImageDraw, Image import matplotlib.pyplot as plt import string import random from sklearn.model_selection import train_test_split from keras.preprocessing.image import img_to_array from keras.utils.np_utils import to_categorical from keras.models import Sequential from keras.layers.convolutional import Conv2D, MaxPooling2D from keras.layers.core import Dense, Dropout, Activation, Flatten ``` #### 数据准备与预处理 创建函数`preprocess_plate_image()`来进行图片预处理操作,这一步骤对于提高最终分类器性能至关重要[^4]。 ```python def preprocess_plate_image(image_path): image = cv2.imread(image_path) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 将彩色图转为灰度图 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # 应用高斯滤波去除噪声 binary = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2) # 自适应阈值二值化 return binary ``` #### 构建CNN模型架构 定义一个简单的CNN框架用于训练: ```python model = Sequential() # 添加第一个卷积层+池化层 model.add(Conv2D(filters=32, kernel_size=(3, 3), padding='same', input_shape=(height, width, depth))) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(rate=0.2)) ... # 完整版应继续堆叠更多层直至完成整个网络搭建 ``` 注意这里的输入尺寸`(height,width,depth)`需根据实际使用的图片大小调整;同样地,在真实应用场景下通常会加入更多的卷积/全连接层以增强表达能力。 #### 训练模型 收集足够的样本数据集之后就可以调用`.fit()`方法开始训练过程了。这里省略具体细节因为涉及到的数据量较大且需要较长时间才能收敛到满意的结果。 #### 预测新图片中的文字 当有了已经训练完毕的模型后,可以通过以下方式读取待测图片并获取预测结果: ```python def predict_character_image(model, char_img_path): processed_char_img = preprocess_plate_image(char_img_path) ... prediction = model.predict(processed_char_img.reshape((1,) + processed_char_img.shape))[0] predicted_label_index = int(np.argmax(prediction)) # 获取最大概率对应的索引作为类别标签 label_map = {i: c for i, c in enumerate(string.ascii_uppercase)} return label_map[predicted_label_index] def recognize_license_plate(model, plate_img_path): segmented_chars_imgs_paths = segment_characters_from_plate(plate_img_path) # 假设有这样一个分割函数存在 recognized_text = ''.join([predict_character_image(model, p) for p in segmented_chars_imgs_paths]) return recognized_text ``` 上述代码片段假设有一个名为`segment_characters_from_plate()`的功能可以从给定的车牌照片中提取出各个独立字符的小幅面截图路径列表,并依次传递给`predict_character_image()`做逐字辨识工作,最后拼接起来形成完整的字符串形式输出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值