IO多路复用
IO多路复用就是我们经常说的select epoll.select和epoll,好处是单个process就可以同时处理多个网络IO。基本原理是select\epoll会不断的轮询所负责的所有socket,当有某个socket数据到达了,就通知用户进程。
在同一个线程里面,接收多个连接,select/epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
文件描述符fd:
为打开文件的文件描述符,而每个进程都有一张文件描述符表,fd文件描述符就是这张表的索引,同样这张表中有一表项,该表项又是指向前面提到打开文件的file结构体,file结构体才是内核中用来描述文件属性的结构体。
我们都知道在Linux下一切皆文件。当然设备也不例外,如果要对某个设备进行操作,就不得不打开此设备文件,打开文件就会获得该文件的文件描述符fd( file discriptor), 它就是一个很小的整数。
IO多路复用:
事件循环不断地调用select获取被激光的socket,然后根据获取socket对应的EventHandler,执行Handle_event函数即可。
因为select()会阻塞,因此只能被称为异步阻塞IO,而非真正的异步IO;
asyncio协程:
根据事件驱动写的异步框架;
个人总结:
- 将不阻塞(原阻塞)的方法,注册进IO事件循环里—手动写;
- 外部loop,调用select()阻塞loop,等待被调用(然后执行具体逻辑操作)—手动写;
- 内部kernel不停地轮询注册进IO事件的文件对象,一旦有文件对象调用,则触发2里的select(),执行下一步(本质就是往select阻塞的那个队列里塞了个数据)—内部自动,看不见;
Selectors主要对象
类的层次结构1
2
3
4
5
6BaseSelector
+-- SelectSelector
+-- PollSelector
+-- EpollSelector
+-- DevpollSelector
+-- KqueueSelector
在下文中,事件 是指那些等待I/O 事件的给定的文件对象的位掩码(可读/可写)。它可以是下面的模块常量的组合︰1
2EVENT_READ 可读
EVENT_WRITE 可写
SelectorKey —文件对象+文件描述符fd,可读/可写事件,文件对象相关联的回调函数
是一个用来将文件对象关联到其底层文件描述符,选定的事件掩码和附加的数据的 namedtuple。它由 BaseSelector 的几种方法返回。
1 | fileobj |
BaseSelector —多个文件对象的IO事件 的操作选择器类,返回类型是SelectorKey实例
- register(fileobj, events, data=None) —注册文件对象、可读/可写、回调函数,data就是回调函数
注册一个文件对象到选择器来监视它的 I/O 事件
1 | fileobj 是要监视的文件对象。它可能是一个整型文件描述符或有 fileno() 方法的对象。events 是要监视的事件的位掩码。data 是不透明的对象。 |
- unregister(fileobj) —注销文件对象
从选择器注销一个文件对象并移除对它的监视。文件对象被注销前应先关闭
1 | fileobj 必须是先前注册过的文件对象。 |
- select(timeout=None) —阻塞,等待触发事件列表里 文件对象被触发,返回文件对象列表
等到一些已注册的文件对象准备好或者超时。
如果 timeout > 0,会指定最长的等待时间,以秒为单位。如果 timeout < = 0,调用不会阻塞并会报告目前准备好的文件对象。如果 timeout 是 None,调用将会阻塞直到一个监视的文件对象准备好。
1 | 这将返回一个以 (key, events) 元组为元素的列表,每一个元组代表一个准备好的文件对象。 |
代码实现
TCP Server
1 | import selectors |
IO模型(课外阅读)
IO 多路复用是5种I/O模型中的第3种,对各种模型讲个故事,描述下区别:
故事情节为:老李去买火车票,三天后买到一张退票。参演人员(老李,黄牛,售票员,快递员),往返车站耗费1小时。
阻塞I/O模型
老李去火车站买票,排队三天买到一张退票。
耗费:在车站吃喝拉撒睡 3天,其他事一件没干。非阻塞I/O模型
老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。
耗费:往返车站6次,路上6小时,其他时间做了好多事。I/O复用模型
- select/poll
老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次 - epoll
老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话 区别
1、epoll内部使用了mmap共享了用户和内核的部分空间,避免了数据的来回拷贝;
2、epoll基于事件驱动,epoll_ctl注册事件并注册callback回调函数,epoll_wait只返回发生的事件避免了像select和poll对事件的整个轮寻操作。nginx中使用了epoll,是基于事件驱动模型的,由一个或多个事件收集器来收集或者分发事件,epoll就属于事件驱动模型的事件收集器,将注册过的事件中发生的事件收集起来,master进程负责管理worker进程。
- select/poll
信号驱动I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话异步I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话
1同2的区别是:自己轮询
2同3的区别是:委托黄牛
3同4的区别是:电话代替黄牛
4同5的区别是:电话通知是自取还是送票上门
事件驱动模型(课外阅读)
在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢?
方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:
- CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?
- 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
- 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
所以,该方式是非常不好的。
方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
- 有一个事件(消息)队列;
- 鼠标按下时,往这个队列中增加一个点击事件(消息);
- 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
- 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。