- 大多数操作系统都支持select和poll
- Linux2.5+支持epoll
- BSD、Mac支持kqueue
- Solaris支持/dev/poll
- Windows支持IOCP
目前几乎支持所有的平台。
默认单个进程能够监视的文件描述符的数量存在最大限制,在linux上默认只支持1024个socket。
可以通过修改宏定义或重新编译内核(修改系统最大支持的端口数)的方式提升这一限制。
内核准备好数据后通知用户有数据了,但不告诉用户是哪个连接有数据,用户只能通过轮询的方式来获取数据。
假定select让内核监视100个socket连接,当有1个连接有数据后,内核就通知用户100个连接中有数据了,但是不告诉用户是哪个连接有数据了,此时用户只能通过轮询的方式一个个去检查然后获取数据。
这里是假定有100个socket连接,那么如果有上万个,上十万个呢?那你就得轮询上万次,上十万次,而你所取的结果仅仅就那么1个。这样就会浪费很多没用的开销。
只支持水平触发
每次调用select,都需要把fd(文件描述符)集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
同时每次调用select都需要在内核遍历所传递过来的所有fd,这个开销在fd很大时也会很大。
与select没有本质上的差别,仅仅是没有了最大文件描述符数量的限制
同样只支持水平触发
只是一个过渡版本,很少用。
Linux2.6开始才出现的epoll,具备了select和poll的一切优先,是公认性能最好的多路I/O就绪通知方法
没有最大文件描述符数量的限制
同时支持水平触发和边缘触发
不支持windows平台
内核准备好数据以后会通知用户哪个连接有数据了。
I/O效率不随着fd数目的增加而线性下降。
使用mmap加速内核与用户空间的消息传递。
将就绪的文件描述符告诉进程后,如果进程没有对其进行I/O操作,那么下次epoll时将再次报告这些文件描述符,称为水平触发。
只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果没有采取行动,那么它将不会再次告知,这种方式称为边缘触发。
1.select通过select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述从而进行后续的读写操作。
由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,这也浪费了一定的开销。
2.epoll只告知那些就绪的文件描述符,而且当我们调用epoll_wait获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中一次取得相应数量的文件描述符即可,这里也是用了内存映射技术(mmap),这样便彻底省掉了这些描述符在系统调用时复制的开销。
另一个本质的改进在意epoll采用基于时间的就绪通知方式。在select/poll中,进程只有在调用一定的方法之后,内核才对所有监视的文件描述符进行扫描,而epoll事件事先通过epoll_ctl来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait时变得到通知。
它封装了I/O对路复用中的select和epoll,能够更快,更方便的实现多并发效果。
它定义了一个BaseSelector抽象基类,以及几个具体实现(KqueueSelector, EpollSelector…),可以等待多核文件I/O的就绪通知。
类的层次结构:
BaseSelector
+-- SelectSelector
+-- PollSelector
+-- EpollSelector
+-- DevpollSelector
+-- KqueueSelector
1.1 创建selector对象
class selectors.DefaultSelector是当前平台上可用的系统调用最有效的实现。
if 'KqueueSelector' in globals():
DefaultSelector = KqueueSelector
elif 'EpollSelector' in globals():
DefaultSelector = EpollSelector
elif 'DevpollSelector' in globals():
DefaultSelector = DevpollSelector
elif 'PollSelector' in globals():
DefaultSelector = PollSelector
else:
DefaultSelector = SelectSelector
注意,由于这个库没有实现windows下的IOCP, 所以windows下只能退化为select
创建一个SelectSelector实例对象
In [1]: import selectors
In [2]: selector = selectors.DefaultSelector()
...:
In [3]: selector
Out[3]: <selectors.SelectSelector at 0x9cd5d30>
1.2 register对I/O事件的注册
源码
class SelectSelector(_BaseSelectorImpl):
def __init__(self):
super().__init__()
self._readers = set()
self._writers = set()
def register(self, fileobj, events, data=None): -> selectorKey对象
key = super().register(fileobj, events, data)
if events & EVENT_READ:
self._readers.add(key.fd)
if events & EVENT_WRITE:
self._writers.add(key.fd)
return key
register为selector实例对象注册一个事件,目标为fileobj,监视它的I/O
register的三个参数
参数 | 含义 |
---|---|
fileobj | 被监视的文件对象,例如socket对象 |
events | 该文件对象必须等待的I/O事件。 |
data | 与此文件对象相关联的可选的opaque数据。就是这个参数在关注I/O事件产生后让操作系统select来干什么。例如关联用来存储每个客户端的会话ID,关联方法等操作。 |
参数events的几个常数形式解释
常数 | 意义 |
---|---|
EVENT_READ | 可读的 |
EVENT_WRITE | 可写的 |
register的返回值
源码
SelectorKey = namedtuple('SelectorKey', ['fileobj', 'fd', 'events', 'data'])
key = SelectorKey(fileobj, self._fileobj_lookup(fileobj), events, data)
返回值这个key是一个工厂方法:具名元组,有下面四个属性
属性 | 含义 |
---|---|
fileobj | 注册的文件对象 |
fd | 文件描述符 |
events | 等待上面的文件描述符的文件对象的I/O事件,为读、写、读和写 |
data | 注册时关联的一些操作对象 |
代码示范
In [4]: import socket
In [5]: sock = socket.socket()
In [6]: sock.bind(("", 9999)) # 绑定localhost和端口9999
In [7]: sock.listen()
In [8]: def accept():
...: client, addr = sock.accept()
In [9]: selector.register(sock, selectors.EVENT_READ, accept)
Out[9]: SelectorKey(fileobj=<socket.socket fd=1376, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>, fd=1376, events=1, data=<function accept at 0x0000000009CC3BF8>)
注意看上面的返回值SelectKey
1.3 select进行监视
events = selector.select()
events是一个二元组列表,里面保存着(SelectorKey, mask)这样的二元组,都是就绪状态的。
因为默认是水平触发,所以key处于就绪状态,但是如果没有对其进行I/O操作的话,他会一直报告这些文件描述符。所以一般需要采用callback函数对已经就绪的事件进行处理
for key, mask in events:
callback = key.data
callback(*args, **kwargs)
1.构建一个selector实例对象直接使用selectors.DefaultSelector(),实例对象selector的get_map的返回值是一个文件对象到选择键的映射类型对象(fd:key)
2.执行register方法都会向_key_from_fd中添加一对(key,fd),执行unregister则相反。
3.将关注的(register)的I/O操作告诉select并系统调用,进程阻塞,内核监视select关注的文件描述符,被关注的任何一个fd对应的I/O准备好了数据,select返回,在使用callback调用,避免水平触发。
4.内核监视select,用户空间并不会开辟其他线程,也就是I/O多路复用是操作系统来监管的,交给它好了。
5.千万注意I/O操作的就绪态和线程的就绪态区分开。。。
import threading
import selectors # 封装了select和epoll
import socket
import multiprocessing
class ChatServer:
def __init__(self, ip="127.0.0.1", port=9997):
self.addr = ip, port
self.sock = socket.socket() # 创建socket对象
self.sock.setblocking(False) # 设置为非阻塞
self.event = threading.Event()
self.s = selectors.DefaultSelector() # 默认选取当前平台最优selector, 返回值为SelectorKey
def start(self):
self.sock.bind(self.addr)
self.sock.listen()
key = self.s.register(self.sock, selectors.EVENT_READ, self.accept)
threading.Thread(target=self.select, name="select", daemon=True).start() # 设置为daemon线程,主线程退出,这个线程也会退出
self.acceptkey = key
def select(self):
while not self.event.is_set(): # WDNMD!!! 这里你在做什么????!!!注意是event.is_set(),不是直接event!!!
events = self.s.select() # windows下默认启用select, linux下默认启用epoll,,关注的是刚才已经被注册了的IO操作,如果就绪,也就是datagram已经来到,
for key, mask in events:
callback = key.data
callback(key.fileobj)
def accept(self, sock:socket.socket):
client, addr = sock.accept()
print("已连接", addr)
client.setblocking(False) # 设置为非阻塞
self.s.register(client, selectors.EVENT_READ, self.recv)
def recv(self, client:socket.socket):
data = client.recv(1024)
if data == b"quit" or data == b"":
print("有人退出连接")
self.s.unregister(client)
client.close()
return
for key in self.s.get_map().values():
# if key.data == self.recv:
# key.fileobj.send(data)
if self.acceptkey != key:
key.fileobj.send(data)
def stop(self): #退出意味着全面关闭与unregister刚才添加到
self.event.set()
l = []
for i in self.s.get_map().values(): # get_map方法得到实际上是_fd_to_key的一种mapping映射类型数据结构
l.append(i.fileobj)
for fj in l:
self.s.unregister(fj) # unregister里面涉及到discard方法
self.s.close()
self.sock.close()
def main():
cs = ChatServer()
cs.start()
while True:
cmd = input(">>").strip()
if cmd == "quit":
cs.stop()
break
print(threading.enumerate())
print(multiprocessing.active_children())
if __name__ == "__main__":
main()
以上代码仅实现主要功能,细节并未处理。
对于IO的其他理解请参考python之多路复用
Python官方文档selectors—高级I/O复用库