I/O多路复用器之隐秘的角落
上一篇文章讲到了Unix的I/O模型,以及在java中的具体实现,其中在java中我们最为关注的就是 I/O 复用了,这篇主要总结下I/O多路复用器。
概念:文件描述符fd
Linux的内核将所有外部设备都可以看做一个文件来操作。那么我们对与外部设备的操作都可以看做对文件进行操作。我们对一个文件的读写,都通过调用内核提供的系统调用;内核给我们返回一个filede scriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符)。描述符就是一个数字,指向内核中一个结构体(文件路径,数据区,等一些属性)。那么我们的应用程序对文件的读写就通过对描述符的读写完成。
复用器
1. select
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
参数解析
- readset、writeset、exceptset 指定我们要让内核测试读、写和异常条件的描述符。它们的类型fd_set是一个BitMap的设计,32位操作系统内核设置它1024位,64位操作系统则设置为2048位,拿readset为例,如果描述符为3的读时间被关注,那么readset的第三位会被置为1。
- maxfdp1 指定待测试对的描述符个数,它的值是待测试的最大描述符加1(如其名),它就是在readset、writeset、exceptset三个描述符集中找出最大描述符编号值,然后加1。表示0~(maxfgp1 - 1)位均将被内核关注。
- timeout 就是超时时间,它的结构精确到了微秒,相对更为精准;
- 返回 若有就绪描述符则返回其数目,若超时则返回0,若出错则返回-1;
操作过程
- select执行,CPU由用户态转为内核态,同时这些参数也拷贝到内核态,由内核进行判断所关注的0~(maxfgp1 - 1)描述符是否有事件,如果没有,则阻塞,如果有,则对有事件发生的FD,进行置位(其实是对没有发生事件的置位为0),select返回;
- 程序接下来还需要重新遍历0~(maxfgp1 - 1)的FD,对已经被置位的FD进行处理;
- readset、writeset、exceptset 3个数据对关注的FD置位,因为已经被内核修改了,所以需要每次都要重新设置一下;
缺点
- FD的数目有上限,意味着最大连接数:x86机器为1024,x64为2048;
- 每次调用,都会发生上下文切换,而且都需要将3个fd_set数据结构传入内核;
- fd_set不可重用,每次都需重新置位;
- 2次对关注FD进行遍历,时间复杂度为O(n);
2. poll
int poll(struct pollfd *fdarray, unsigned nfds,int timeout);
参数解析
- fdarray
fdarray是一个链表的结构,数据结构如下:
struct pollfd{
int fd; // 需要关注的文件描述符
short events;// 关注的事件类型
short revents;// 发生的事件
}
poll相对于select的改进主要是在这个结构体上,由数组到链表,解决了描述符有上限的问题,并且将结果和参数分离,只需要重置revents就可以了,而不需要重新申请整个结构;
- nfds 代表元素的个数
- timeout 超时时间
- 返回 若有就绪描述符则返回其数目,若超时则返回0,若出错则返回-1;
缺点
经过select->poll的改进,还剩下select的2个缺点、
- 每次调用,都会发生上下文切换,而且都需要将3个fd_set数据结构传入内核;
- 2次对关注FD进行遍历,时间复杂度为O(n);
3. epoll
epoll在poll和select的缺点之上做了重大改进,但是逻辑也更为复杂。它有三个函数:
int epoll_create(int size)
直接在内核创建保存文件描述符的空间。epoll的结构采用红黑树保存,epoll_create可视为初始化root节点。调用epoll_create所创建的文件描述符保存空间称为“epoll例程”。size值可视为建议值,并非用来决定例程大小;
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
向控件注册并注销文件描述符,可视为向红黑树注册描述符,并注册相关事件的相关回调;
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout):
与select函数类似,等待文件描述符发生变化;
参数解析
- epfd:表示事件发生监视范围的epoll例程的文件描述符
- events:用于保存发生事件的文件描述符集合的链表
- maxevents:告诉内核第二个参数events的大小
- timeout:超时时间
操作过程
存储FD的数据结构直接改由在内核的维护,我们便不再需要重复多次从用户态copy到内核态,只需要实时维护发生变化的FD到内核就可以了;用events来存储发生事件的fd,则无需再遍历整个被关注的描述符集合。
操作模式
epoll对文件描述符的操作有2种模式,默认模式是水平触发。
- 水平触发(level trigger) 当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。水平触发同时支持block和none-blocking
- 边缘触发(edge trigger) 当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。边缘触发是高速工作方式,只支持none-blocking
4. 总结
select |
poll |
epoll |
|
---|---|---|---|
操作方式 |
遍历 |
遍历 |
回调 |
底层实现 |
数组 |
链表 |
红黑树 |
IO效率 |
每次调用都进行线性遍历,时间复杂度为O(n) |
每次调用都进行线性遍历,时间复杂度为O(n) |
事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1) |
最大连接数 |
1024(x86)或2048(x64) |
无上限 |
无上限 |
fd拷贝 |
每次调用select,都需要把fd集合从用户态拷贝到内核态 |
每次调用poll,都需要把fd集合从用户态拷贝到内核态 |
调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
- golang的HTTP基本认证机制实例详解
- hdu----(3065)病毒侵袭持续中(AC自动机)
- Spark机器学习库(MLlib)指南之简介及基础统计
- 监控利器之 Prometheus
- C线程同步/异步
- Spring-Security-入门(一):登录与退出
- Shell系列-Shell概述
- sqoop2系统入门之2汇总:用户指南shell命令
- 深入解析golang编程中函数的用法
- MySQL数据库(一):安装MySQL数据库
- Spring Security入门(二):基于数据库验证
- flume搜集日志:如何解决实时不断追加的日志文件及不断增加的文件个数问题
- hduoj-----(2896)病毒侵袭(ac自动机)
- MySQL数据库(二):基本管理
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- linux中使用boost.python调用c++动态库的办法
- 在Linux系统下上传项目到码云的办法
- PHP global全局变量经典应用与注意事项分析【附$GLOBALS用法对比】 原创
- Linux(Ubuntu 18.04)上安装Anaconda步骤详解
- php web环境和命令行环境下查找php.ini的位置
- PHP大文件分块上传功能实例详解
- Linux 命令行通配符及转义符的实现
- Python爬虫抓取指定网页图片代码实例
- PHP变量作用域(全局变量&局部变量)&global&static关键字用法实例分析
- CentOS 7 安装Chrome浏览器的方法
- PHP高级编程之消息队列原理与实现方法详解
- thinkphp5.1框架模板布局与模板继承用法分析
- Linux内核设备驱动之内存管理笔记整理
- Matplotlib 绘制饼图解决文字重叠的方法
- Yii 实现数据加密和解密的示例代码