IO多路复用(multiplexing)
IO多路复用,又称事件驱动,是操作系统提供的一项服务:由操作系统监控一个或多个IO,一旦可读或可写就通知对应的进程。相关的系统调用有大多数系统都支持的select, poll,linux上的epoll, BSD(包括mac os)上的kqueue等。
举个例子,你在等一个快递(等待一个IO事件),必须收到这个快递才能做后续的工作,你的选择可以是反复打电话问快递员到了没(非阻塞忙轮询),或者休息(阻塞)等着被快递员叫醒。显然前者在浪费大家的时间,一般人都会选择后者,即阻塞IO。
单线程阻塞IO只能监控一个流,引入多线程多进程徒增开销。非阻塞忙轮询IO倒是可以监控很多个流,但是效率低。为此,我们找了一个代理(select, poll, epoll或kqueue),同时监控多个流。我们的程序闲时阻塞,有事发生时醒来。epoll,kqueue比select高效。
python封装的系统调用
语言环境 python 2.7.14,相关系统调用在select
包里。
import select
dir(select)
# linux下有epoll, poll, select
# mac下有kqueue, select
epoll
# 返回一个epoll对象
select.epoll([sizehint=-1]) # 正整数或-1, 用于优化内部数据结构,并不据此限制事件数量,-1为默认大小
API | 签名 | 描述 |
---|---|---|
poll | poll([timeout=-1[, maxevents=-1]]) -> [(fd, events), (…)] | 等待事件发生,timeout为秒数,-1为等待无限长,maxevent返回事件最大数量 |
register | register(fd[, eventmask]) -> None | 为一个文件描述符注册监听事件,如果注册过会引发异常 |
unregister | unregister(fd) -> None | |
modify | modify(fd, eventmask) -> None | |
close | close() -> None | 关闭epoll对象,对其的后续操作会引发异常 |
select
select
系统调用会等待,直到指定的IO(一个或多个)可读rlist、可写wlist、或者有其他预定义的事件待处理xlist。如果只有一种情况发生,其他list返回空列表。
select(rlist, wlist, xlist[, timeout]) -> (rlist, wlist, xlist)
由于select
只知道有事件发生了,不知道发生在哪个IO上,故后续还需遍历检查才知道该调哪个回调函数处理。select
只有一个函数,epoll
是一个对象,ssloop对select
进行了封装,使其接口与epoll
接口一致。
# ssloop对select系统调用的封装
class SelectLoop(object):
def __init__(self):
self._r_list = set()
self._w_list = set()
self._x_list = set()
def poll(self, timeout):
# 封装后的select系统调用
# 返回[(fd, events), (...)]
r, w, x = select.select(self._r_list, self._w_list, self._x_list,
timeout)
# POLL_NULL = 0x00
results = defaultdict(lambda: POLL_NULL)
# POLL_IN = 0x01,POLL_OUT = 0x04,POLL_ERR = 0x08
for p in [(r, POLL_IN), (w, POLL_OUT), (x, POLL_ERR)]:
for fd in p[0]:
results[fd] |= p[1]
return results.items()
def register(self, fd, mode):
if mode & POLL_IN:
self._r_list.add(fd)
if mode & POLL_OUT:
self._w_list.add(fd)
if mode & POLL_ERR:
self._x_list.add(fd)
def unregister(self, fd):
if fd in self._r_list:
self._r_list.remove(fd)
if fd in self._w_list:
self._w_list.remove(fd)
if fd in self._x_list:
self._x_list.remove(fd)
def modify(self, fd, mode):
self.unregister(fd)
self.register(fd, mode)
def close(self):
# select系统调用只是一个函数,不是对象,不提供close
pass
kqueue
API | 签名 | 描述 |
---|---|---|
control | control(changelist, max_events[, timeout=None]) -> eventlist | changelist是kevent的列表,timeout为None时等待时间无穷 |
close | close() -> None |
官方样例如下
# To start watching a socket for input:
>>> kq = kqueue()
>>> sock = socket()
>>> sock.connect((host, port))
>>> kq.control([kevent(sock, KQ_FILTER_WRITE, KQ_EV_ADD)], 0)
# To wait one second for it to become writeable:
>>> kq.control(None, 1, 1000)
# To stop listening:
>>> kq.control([kevent(sock, KQ_FILTER_WRITE, KQ_EV_DELETE)], 0)
kqueue
定义kevent
再使用control
函数进行注册。ssloop对kqueue
进行了封装,使其接口与epoll
接口一致。
# ssloop对BSD上kqueue系统调用的封装
class KqueueLoop(object):
MAX_EVENTS = 1024
def __init__(self):
self._kqueue = select.kqueue()
self._fds = {}
def _control(self, fd, mode, flags):
# 对kqueue.control的封装,将kqueue定义的事件转本地定义的事件
events = []
if mode & POLL_IN:
events.append(select.kevent(fd, select.KQ_FILTER_READ, flags))
if mode & POLL_OUT:
events.append(select.kevent(fd, select.KQ_FILTER_WRITE, flags))
for e in events:
self._kqueue.control([e], 0)
def poll(self, timeout):
if timeout < 0:
timeout = None # kqueue behaviour
events = self._kqueue.control(None, KqueueLoop.MAX_EVENTS, timeout)
results = defaultdict(lambda: POLL_NULL)
for e in events:
fd = e.ident
if e.filter == select.KQ_FILTER_READ:
results[fd] |= POLL_IN
elif e.filter == select.KQ_FILTER_WRITE:
results[fd] |= POLL_OUT
return results.items()
def register(self, fd, mode):
self._fds[fd] = mode
self._control(fd, mode, select.KQ_EV_ADD)
def unregister(self, fd):
self._control(fd, self._fds[fd], select.KQ_EV_DELETE)
del self._fds[fd]
def modify(self, fd, mode):
self.unregister(fd)
self.register(fd, mode)
def close(self):
self._kqueue.close()