IO模型介绍(select、poll、epoll)
wptr33 2024-12-14 15:35 21 浏览
什么是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流程如下:
??
一个完整的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流程如下:
??
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); // 创建新线程处理
close(connfd); // 关闭连接
arr.remove(connfd); // 移除已处理的连接
}
}
}
newTheadDeal(buf){
doSomeThing(buf); // 处理数据
}
所谓非阻塞 IO 只是将第一阶段的等待读就绪改为非阻塞,但是第二阶段的数据读取还是阻塞的,非阻塞 read 最重要的是提供了我们在一个线程内管理多个文件描述符的能力
非阻塞具体流程如下:
??
3. IO多路复用(select、poll、epoll):
上面的实现看着很不错,但是却存在一个很大的问题,我们需要不断的调用 read() 进行系统调用,这里的系统调用我们可以理解为分布式系统的 RPC 调用,性能损耗十分严重,因为这依然是用户层的一些小把戏。
多路复用就是系统提供了一种函数可以同时监控多个文件描述符的操作,这个函数就是我们常说到的select、poll、epoll函数,可以通过它们同时监控多个文件描述符,只要有任何一个数据状态准备就绪了,就返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起read()请求去读取数据。实际上最核心之处在于IO多路转接能够同时等待多个文件描述符的就绪状态,来达到不必为每个文件描述符创建一个对应的监控线程,从而减少线程资源创建的目的。
select:
select 是操作系统提供的系统函数,通过它我们可以将文件描述符发送给系统,让系统内核帮我们遍历检测是否可读,并告诉我们进行读取数据。
伪代码如下:
arr = new Arr[];
listenfd = socket(); // 打开一个网络通信套接字
bind(listenfd); // 绑定
listen(listenfd); // 监听
while(true) {
connfd = accept(listenfd); // 阻塞 等待建立连接
arr.add(connfd);
}
// 异步线程检测 通过 select 判断是否有连接可读
new Tread(){
while(select(arr) > 0){
for(connfd : arr){
if(connfd can read){
// 如果套接字可读 创建新线程处理
newTheadDeal(connfd);
arr.remove(connfd); // 移除已处理的连接
}
}
}
}
newTheadDeal(connfd){
buf = new buf[1024]; // 读取数据容器
int n = read(connfd, buf); // 阻塞读取数据
doSomeThing(buf); // 处理数据
close(connfd); // 关闭连接
}
流程简图:
??
优点:
1.减少大量系统调用。
2.系统内核帮我们遍历检测是否可读。
存在一些问题:
? 每次调用需要在用户态和内核态之间拷贝文件描述符数组,但高并发场景下这个拷贝的消耗是很大的。
? 内核检测文件描述符可读还是通过遍历实现,当文件描述符数组很长时,遍历操作耗时也很长。
? 内核检测完文件描述符数组后,当存在可读的文件描述符数组时,用户态需要再遍历检测一遍。
poll:
? poll 和 select 原理基本一致,最大的区别是去掉了最大 1024 个文件描述符的限制。
? select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。
? poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
epoll:
epoll 主要优化了上面三个问题实现:
1.每次调用需要在用户态和内核态之间拷贝文件描述符数组,但高并发场景下这个拷贝的消耗是很大的。
方案:内核中保存一份文件描述符,无需用户每次传入,而是仅同步修改部分。
2.内核检测文件描述符可读还是通过遍历实现,当文件描述符数组很长时,遍历操作耗时也很长。
方案:通过事件唤醒机制唤醒替代遍历。
3.内核检测完文件描述符数组后,当存在可读的文件描述符数组时,用户态需要再遍历检测一遍。
方案:仅将可读部分文件描述符同步给用户态,不需要用户态再次遍历。
epoll 基于高效的红黑树结构,提供了三个核心操作:epoll_create、epoll_ctl、epoll_wait。
epoll_create:
用于创建epoll文件描述符,该文件描述符用于后续的epoll操作,参数size目前还没有实际用处,我们只要填一个大于0的数就行。
??
epoll_ctl:
epoll_ctl函数用于增加,删除,修改epoll事件,epoll事件会存储于内核epoll结构体红黑树中.
??
epoll_wait函数:
epoll_wait用于监听套接字事件,可以通过设置超时时间timeout来控制监听的行为为阻塞模式还是超时模式。
??
整体运转如下:
??
伪代码如下:
listenfd = socket(); // 打开一个网络通信套接字
bind(listenfd); // 绑定
listen(listenfd); // 监听
int epfd = epoll_create(...); // 创建 epoll 对象
while(1) {
connfd = accept(listenfd); // 阻塞 等待建立连接
epoll_ctl(connfd, ...); // 将新连接加入到 epoll 对象
}
// 异步线程检测 通过 epoll_wait 阻塞获取可读的套接字
new Tread(){
while(arr = epoll_wait()){
for(connfd : arr){
// 仅返回可读套接字
newTheadDeal(connfd);
}
}
}
newTheadDeal(connfd){
buf = new buf[1024]; // 读取数据容器
int n = read(connfd, buf); // 阻塞读取数据
doSomeThing(buf); // 处理数据
close(connfd); // 关闭连接
}
LT模式和ET模式:
LT模式:水平触发:
1.socket读触发:socket接收缓冲区有数据,会一直触发epoll_wait EPOLLIN事件,直到数据被用户读取完。
2.socket写触发:socket可写,会一直触发epoll_wait EPOLLOUT事件。
ET模式:边缘触发:
1.socket读触发:当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完。
2.socket写触发:socket可写,会触发一次epoll_wait EPOLLOUT事件。
epoll为什么高效:
1.红黑树红黑树提高epoll事件增删查改效率。
2.回调通知机制:当epoll监听套接字有数据读或者写时,会通过注册到socket的回调函数通知epoll,epoll检测到事件后,将事件存储在就绪队列(rdllist)。
3.就绪队列:epoll_wait返回成功后,会将所有就绪事件存储在事件数组,用户不需要进行无效的轮询,从而提高了效率。
信号驱动IO:
多路转接解决了一个线程可以监控多个fd的问题,但是select采用无脑的轮询就显得有点暴力,因为大部分情况下的轮询都是无效的,所以有人就想,别让我总去问数据是否准备就绪,而是等你准备就绪后主动通知我,这边是信号驱动IO。
信号驱动IO是在调用sigaction时候建立一个SIGIO的信号联系,当内核准备好数据之后再通过SIGIO信号通知线程,此fd准备就绪,当线程收到可读信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下,应用线程在发出信号监控后即可返回,不会阻塞,所以一个应用线程也可以同时监控多个fd。
异步 IO:
应用只需要向内核发送一个读取请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,我们称这种模式为异步IO模型。
异步IO的优化思路是解决应用程序需要先后发送询问请求、接收数据请求两个阶段的模式,在异步IO的模式下,只需要向内核发送一次请求就可以完成状态询问和数拷贝的所有操作。
同步和异步区别:
同步和异步关注的是消息通信机制.
同步:就是在发出一个调用时,自己需要参与等待结果的过程,则为同步,前面四个IO都自己参与了,所以也称为同步IO.
异步:则指出发出调用以后,到数据准备完成,自己都未参与,则为异步IO。
相关推荐
- redis的八种使用场景
-
前言:redis是我们工作开发中,经常要打交道的,下面对redis的使用场景做总结介绍也是对redis举报的功能做梳理。缓存Redis最常见的用途是作为缓存,用于加速应用程序的响应速度。...
- 基于Redis的3种分布式ID生成策略
-
在分布式系统设计中,全局唯一ID是一个基础而关键的组件。随着业务规模扩大和系统架构向微服务演进,传统的单机自增ID已无法满足需求。高并发、高可用的分布式ID生成方案成为构建可靠分布式系统的必要条件。R...
- 基于OpenWrt系统路由器的模式切换与网页设计
-
摘要:目前商用WiFi路由器已应用到多个领域,商家通过给用户提供一个稳定免费WiFi热点达到吸引客户、提升服务的目标。传统路由器自带的Luci界面提供了工厂模式的Web界面,用户可通过该界面配置路...
- 这篇文章教你看明白 nginx-ingress 控制器
-
主机nginx一般nginx做主机反向代理(网关)有以下配置...
- 如何用redis实现注册中心
-
一句话总结使用Redis实现注册中心:服务注册...
- 爱可可老师24小时热门分享(2020.5.10)
-
No1.看自己以前写的代码是种什么体验?No2.DooM-chip!国外网友SylvainLefebvre自制的无CPU、无操作码、无指令计数器...No3.我认为CS学位可以更好,如...
- Apportable:拯救程序员,IOS一秒变安卓
-
摘要:还在为了跨平台使用cocos2d-x吗,拯救objc程序员的奇葩来了,ApportableSDK:FreeAndroidsupportforcocos2d-iPhone。App...
- JAVA实现超买超卖方案汇总,那个最适合你,一篇文章彻底讲透
-
以下是几种Java实现超买超卖问题的核心解决方案及代码示例,针对高并发场景下的库存扣减问题:方案一:Redis原子操作+Lua脚本(推荐)//使用Redis+Lua保证原子性publicbo...
- 3月26日更新 快速施法自动施法可独立设置
-
2016年3月26日DOTA2有一个79.6MB的更新主要是针对自动施法和快速施法的调整本来内容不多不少朋友都有自动施法和快速施法的困扰英文更新日志一些视觉BUG修复就不翻译了主要翻译自动施...
- Redis 是如何提供服务的
-
在刚刚接触Redis的时候,最想要知道的是一个’setnameJhon’命令到达Redis服务器的时候,它是如何返回’OK’的?里面命令处理的流程如何,具体细节怎么样?你一定有问过自己...
- lua _G、_VERSION使用
-
到这里我们已经把lua基础库中的函数介绍完了,除了函数外基础库中还有两个常量,一个是_G,另一个是_VERSION。_G是基础库本身,指向自己,这个变量很有意思,可以无限引用自己,最后得到的还是自己,...
- China's top diplomat to chair third China-Pacific Island countries foreign ministers' meeting
-
BEIJING,May21(Xinhua)--ChineseForeignMinisterWangYi,alsoamemberofthePoliticalBureau...
- 移动工作交流工具Lua推出Insights数据分析产品
-
Lua是一个适用于各种职业人士的移动交流平台,它在今天推出了一项叫做Insights的全新功能。Insights是一个数据平台,客户可以在上面实时看到员工之间的交流情况,并分析这些情况对公司发展的影响...
- Redis 7新武器:用Redis Stack实现向量搜索的极限压测
-
当传统关系型数据库还在为向量相似度搜索的性能挣扎时,Redis7的RedisStack...
- Nginx/OpenResty详解,Nginx Lua编程,重定向与内部子请求
-
重定向与内部子请求Nginx的rewrite指令不仅可以在Nginx内部的server、location之间进行跳转,还可以进行外部链接的重定向。通过ngx_lua模块的Lua函数除了能实现Nginx...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)