IO多路复用模块selectors

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协程:

根据事件驱动写的异步框架;

个人总结:

  1. 将不阻塞(原阻塞)的方法,注册进IO事件循环里—手动写;
  2. 外部loop,调用select()阻塞loop,等待被调用(然后执行具体逻辑操作)—手动写;
  3. 内部kernel不停地轮询注册进IO事件的文件对象,一旦有文件对象调用,则触发2里的select(),执行下一步(本质就是往select阻塞的那个队列里塞了个数据)—内部自动,看不见;

Selectors主要对象

类的层次结构

1
2
3
4
5
6
BaseSelector
+-- SelectSelector
+-- PollSelector
+-- EpollSelector
+-- DevpollSelector
+-- KqueueSelector

在下文中,事件 是指那些等待I/O 事件的给定的文件对象的位掩码(可读/可写)。它可以是下面的模块常量的组合︰

1
2
EVENT_READ	 可读
EVENT_WRITE 可写

SelectorKey —文件对象+文件描述符fd,可读/可写事件,文件对象相关联的回调函数

是一个用来将文件对象关联到其底层文件描述符,选定的事件掩码和附加的数据的 namedtuple。它由 BaseSelector 的几种方法返回。

1
2
3
4
5
6
7
8
9
10
11
fileobj
注册的文件对象。

fd
底层的文件描述符。

events
该文件对象必须等待的事件。

data
可选的与此文件对象相关联的不透明数据︰ 例如,这可以用来存储每个客户端的会话 id

BaseSelector —多个文件对象的IO事件 的操作选择器类,返回类型是SelectorKey实例

  • register(fileobj, events, data=None) —注册文件对象、可读/可写、回调函数,data就是回调函数

    注册一个文件对象到选择器来监视它的 I/O 事件

1
2
3
fileobj 是要监视的文件对象。它可能是一个整型文件描述符或有 fileno() 方法的对象。events 是要监视的事件的位掩码。data 是不透明的对象。

这返回一个新的 SelectorKey 实例,或者因无效的事件掩码或文件描述符抛出 ValueError 错误,或者如果文件对象已注册则抛出 KeyError 错误。
  • unregister(fileobj) —注销文件对象

    从选择器注销一个文件对象并移除对它的监视。文件对象被注销前应先关闭

1
2
fileobj 必须是先前注册过的文件对象。
这返回关联的 SelectorKey 实例,或者如果 fileobj未注册则抛出 KeyError错误。如果 fileobj 无效则抛出ValueError错误(例如,它没有 fileno() 方法或其 fileno() 方法有无效的返回值)。
  • select(timeout=None) —阻塞,等待触发事件列表里 文件对象被触发,返回文件对象列表

    等到一些已注册的文件对象准备好或者超时。
    如果 timeout > 0,会指定最长的等待时间,以秒为单位。如果 timeout < = 0,调用不会阻塞并会报告目前准备好的文件对象。如果 timeout 是 None,调用将会阻塞直到一个监视的文件对象准备好。

1
2
3
这将返回一个以 (key, events) 元组为元素的列表,每一个元组代表一个准备好的文件对象。

key 是对应于一个准备好的文件对象的SelectorKey 实例。events 是该文件对象上准备好的事件位掩码。

代码实现

TCP Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import selectors
import socket

# 默认选择器类型select/epoll,返回一个选择器;用来操作IO事件
sel = selectors.DefaultSelector()
sock = socket.socket()
sock.bind(('127.0.0.1', 8800))
sock.listen(5)
sock.setblocking(False) # 设置socket为不阻塞


##############################################################
# 子socket的handle处理方法--回调方法
# conn--文件对象子socket,mask--文件对象的事件(可读/可写)
##############################################################
def read(conn, mask):
try:
data = conn.recv(1024)
print(data.decode('utf-8'))
print("接收到:{}".format(data))
data2 = "xxxxxxx"
print("发送数据:{}".format(data2))
conn.send(data2.encode("utf-8"))
except Exception as e:
# 注销掉事件:client断开时,出发异常将该子socket文件对象从IO事件列表移除(注销)
sel.unregister(conn)


##############################################################
# 主socket的handle方法--回调方法,主要aceept接收socket客户端,
# so--文件对象主socket,mask--文件对象的事件(可读/可写)
##############################################################

def accept(so, mask):
conn, addr = so.accept()
print("-----------", conn)
# 注册子socket文件对象,事件,回调函数(上面的read方法)
sel.register(conn, selectors.EVENT_READ, read)

# 注册主socket事件,事件(可读/可写),回调函数
sel.register(sock, selectors.EVENT_READ, accept)
while 1:
print("waiting.....")
# 阻塞,等待任意一个文件对象被触发,返回文件对象列表
events = sel.select()

for key, mask in events:
'''
遍历获得 被触发文件对象,并通过获取文件对象关联的回调方法,执行回调;
在这里可以进行过滤,判断不同的文件对象,是否进行不同的操作;
'''
print(key.fileobj) # 文件对象
print(key.fd) # 文件对象 关联的 文件描述符fd
func = key.data # 文件对象关联的回调函数
obj = key.fileobj # 文件对象
func(obj, mask) # 执行回调方法

IO模型(课外阅读)

IO 多路复用是5种I/O模型中的第3种,对各种模型讲个故事,描述下区别:
故事情节为:老李去买火车票,三天后买到一张退票。参演人员(老李,黄牛,售票员,快递员),往返车站耗费1小时。

  1. 阻塞I/O模型
    老李去火车站买票,排队三天买到一张退票。
    耗费:在车站吃喝拉撒睡 3天,其他事一件没干。

  2. 非阻塞I/O模型
    老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。
    耗费:往返车站6次,路上6小时,其他时间做了好多事。

  3. 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进程。

  4. 信号驱动I/O模型
    老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
    耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话

  5. 异步I/O模型
    老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
    耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话

1同2的区别是:自己轮询
2同3的区别是:委托黄牛
3同4的区别是:电话代替黄牛
4同5的区别是:电话通知是自取还是送票上门

事件驱动模型(课外阅读)

在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢?
方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:

  1. CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?
  2. 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
  3. 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
    所以,该方式是非常不好的。

方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:

  1. 有一个事件(消息)队列;
  2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
  3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
  4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。

本文标题:IO多路复用模块selectors

文章作者:HT

发布时间:2018年03月26日 - 15:03

最后更新:2018年03月27日 - 09:03

原始链接:http://7ht.gitee.io/2018/03/26/IO多路复用模块selectors/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。