ioloop 分析
首先要看的是关于 epoll 操作的方法,还记得前文说过的 epoll 只需要四个 api 就能完全操作嘛? 我们来看 PollIOLoop 的实现:
epoll 操作
1 def add_handler(self, fd, handler, events): 2 fd, obj = self.split_fd(fd) 3 self._handlers[fd] = (obj, stack_context.wrap(handler)) 4 self._impl.register(fd, events | self.ERROR) 5 def update_handler(self, fd, events): 6 fd, obj = self.split_fd(fd) 7 self._impl.modify(fd, events | self.ERROR) 8 def remove_handler(self, fd): 9 fd, obj = self.split_fd(fd) 10 self._handlers.pop(fd, None) 11 self._events.pop(fd, None) 12 try: 13 self._impl.unregister(fd) 14 except Exception: 15 gen_log.debug("Error deleting fd from IOLoop", exc_info=True)
epoll_ctl:这个三个方法分别对应 epoll_ctl 中的 add 、 modify 、 del 参数。 所以这三个方法实现了 epoll 的 epoll_ctl 。
epoll_create:然后 epoll 的生成在前文 EPollIOLoop 的初始化中就已经完成了:super(EPollIOLoop, self).initialize(impl=select.epoll(), **kwargs)
。 这个相当于 epoll_create 。
epoll_wait:epoll_wait 操作则在 start()
中:event_pairs = self._impl.poll(poll_timeout)
epoll_close:而 epoll 的 close 则在 PollIOLoop 中的 close
方法内调用: self._impl.close()
完成。
initialize
接下来看 PollIOLoop 的初始化方法中作了什么:
1 def initialize(self, impl, time_func=None, **kwargs): 2 super(PollIOLoop, self).initialize(**kwargs) 3 self._impl = impl # 指定 epoll 4 if hasattr(self._impl, 'fileno'): 5 set_close_exec(self._impl.fileno()) # fork 后关闭无用文件描述符 6 self.time_func = time_func or time.time # 指定获取当前时间的函数 7 self._handlers = {} # handler 的字典,储存被 epoll 监听的 handler,与打开它的文件描述符 ( file descriptor 简称 fd ) 一一对应 8 self._events = {} # event 的字典,储存 epoll 返回的活跃的 fd event pairs 9 self._callbacks = [] # 储存各个 fd 回调函数的列表 10 self._callback_lock = threading.Lock() # 指定进程锁 11 self._timeouts = [] # 将是一个最小堆结构,按照超时时间从小到大排列的 fd 的任务堆( 通常这个任务都会包含一个 callback ) 12 self._cancellations = 0 # 关于 timeout 的计数器 13 self._running = False # ioloop 是否在运行 14 self._stopped = False # ioloop 是否停止 15 self._closing = False # ioloop 是否关闭 16 self._thread_ident = None # 当前线程堆标识符 ( thread identify ) 17 self._blocking_signal_threshold = None # 系统信号, 主要用来在 epoll_wait 时判断是否会有 signal alarm 打断 epoll 18 self._timeout_counter = itertools.count() # 超时计数器 ( 暂时不是很明白具体作用,好像和前面的 _cancellations 有关系? 请大神讲讲) 19 self._waker = Waker() # 一个 waker 类,主要是对于管道 pipe 的操作,因为 ioloop 属于底层的数据操作,这里 epoll 监听的是 pipe 20 self.add_handler(self._waker.fileno(), 21 lambda fd, events: self._waker.consume(), 22 self.READ) # 将管道加入 epoll 监听,对于 web server 初始化时只需要关心 READ 事件
除了注释中的解释,还有几点补充:
- close_exec 的作用: 子进程在fork出来的时候,使用了写时复制(COW,Copy-On-Write)方式获得父进程的数据空间、 堆和栈副本,这其中也包括文件描述符。刚刚fork成功时,父子进程中相同的文件描述符指向系统文件表中的同一项,接着,一般我们会调用exec执行另一个程序,此时会用全新的程序替换子进程的正文,数据,堆和栈等。此时保存文件描述符的变量当然也不存在了,我们就无法关闭无用的文件描述符了。所以通常我们会fork子进程后在子进程中直接执行close关掉无用的文件描述符,然后再执行exec。 所以 close_exec 执行的其实就是 关闭 + 执行的作用。 详情可以查看: 关于linux进程间的close-on-exec机制
2.Waker(): Waker 封装了对于管道 pipe 的操作:
1 def set_close_exec(fd): 2 flags = fcntl.fcntl(fd, fcntl.F_GETFD) 3 fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) 4 def _set_nonblocking(fd): 5 flags = fcntl.fcntl(fd, fcntl.F_GETFL) 6 fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) 7 class Waker(interface.Waker): 8 def __init__(self): 9 r, w = os.pipe() 10 _set_nonblocking(r) 11 _set_nonblocking(w) 12 set_close_exec(r) 13 set_close_exec(w) 14 self.reader = os.fdopen(r, "rb", 0) 15 self.writer = os.fdopen(w, "wb", 0) 16 def fileno(self): 17 return self.reader.fileno() 18 def write_fileno(self): 19 return self.writer.fileno() 20 def wake(self): 21 try: 22 self.writer.write(b"x") 23 except IOError: 24 pass 25 def consume(self): 26 try: 27 while True: 28 result = self.reader.read() 29 if not result: 30 break 31 except IOError: 32 pass 33 def close(self): 34 self.reader.close() 35 self.writer.close() 36 ```
可以看到 waker 把 pipe 分为读、 写两个管道并都设置了非阻塞和 close_exec
。 注意wake(self)
方法中:self.writer.write(b"x")
直接向管道中写入随意字符从而释放管道。
start
ioloop 最核心的部分:
1 def start(self): 2 if self._running: # 判断是否已经运行 3 raise RuntimeError("IOLoop is already running") 4 self._setup_logging() 5 if self._stopped: 6 self._stopped = False # 设置停止为假 7 return 8 old_current = getattr(IOLoop._current, "instance", None) 9 IOLoop._current.instance = self 10 self._thread_ident = thread.get_ident() # 获得当前线程标识符 11 self._running = True # 设置运行 12 old_wakeup_fd = None 13 if hasattr(signal, 'set_wakeup_fd') and os.name == 'posix': 14 try: 15 old_wakeup_fd = signal.set_wakeup_fd(self._waker.write_fileno()) 16 if old_wakeup_fd != -1: 17 signal.set_wakeup_fd(old_wakeup_fd) 18 old_wakeup_fd = None 19 except ValueError: 20 old_wakeup_fd = None 21 try: 22 while True: # 服务器进程正式开始,类似于其他服务器的 serve_forever 23 with self._callback_lock: # 加锁,_callbacks 做为临界区不加锁进行读写会产生脏数据 24 callbacks = self._callbacks # 读取 _callbacks 25 self._callbacks = []. # 清空 _callbacks 26 due_timeouts = [] # 用于存放这个周期内已过期( 已超时 )的任务 27 if self._timeouts: # 判断 _timeouts 里是否有数据 28 now = self.time() # 获取当前时间,用来判断 _timeouts 里的任务有没有超时 29 while self._timeouts: # _timeouts 有数据时一直循环, _timeouts 是个最小堆,第一个数据永远是最小的, 这里第一个数据永远是最接近超时或已超时的 30 if self._timeouts[0].callback is None: # 超时任务无回调 31 heapq.heappop(self._timeouts) # 直接弹出 32 self._cancellations -= 1 # 超时计数器 -1 33 elif self._timeouts[0].deadline <= now: # 判断最小的数据是否超时 34 due_timeouts.append(heapq.heappop(self._timeouts)) # 超时就加到已超时列表里。 35 else: 36 break # 因为最小堆,如果没超时就直接退出循环( 后面的数据必定未超时 ) 37 if (self._cancellations > 512 38 and self._cancellations > (len(self._timeouts) >> 1)): # 当超时计数器大于 512 并且 大于 _timeouts 长度一半( >> 为右移运算, 相当于十进制数据被除 2 )时,清零计数器,并剔除 _timeouts 中无 callbacks 的任务 39 self._cancellations = 0 40 self._timeouts = [x for x in self._timeouts 41 if x.callback is not None] 42 heapq.heapify(self._timeouts) # 进行 _timeouts 最小堆化 43 for callback in callbacks: 44 self._run_callback(callback) # 运行 callbacks 里所有的 calllback 45 for timeout in due_timeouts: 46 if timeout.callback is not None: 47 self._run_callback(timeout.callback) # 运行所有已过期任务的 callback 48 callbacks = callback = due_timeouts = timeout = None # 释放内存 49 if self._callbacks: # _callbacks 里有数据时 50 poll_timeout = 0.0 # 设置 epoll_wait 时间为0( 立即返回 ) 51 elif self._timeouts: # _timeouts 里有数据时 52 poll_timeout = self._timeouts[0].deadline - self.time() 53 # 取最小过期时间当 epoll_wait 等待时间,这样当第一个任务过期时立即返回 54 poll_timeout = max(0, min(poll_timeout, _POLL_TIMEOUT)) 55 # 如果最小过期时间大于默认等待时间 _POLL_TIMEOUT = 3600,则用 3600,如果最小过期时间小于0 就设置为0 立即返回。 56 else: 57 poll_timeout = _POLL_TIMEOUT # 默认 3600 s 等待时间 58 if not self._running: # 检查是否有系统信号中断运行,有则中断,无则继续 59 break 60 if self._blocking_signal_threshold is not None: 61 signal.setitimer(signal.ITIMER_REAL, 0, 0) # 开始 epoll_wait 之前确保 signal alarm 都被清空( 这样在 epoll_wait 过程中不会被 signal alarm 打断 ) 62 try: 63 event_pairs = self._impl.poll(poll_timeout) # 获取返回的活跃事件队 64 except Exception as e: 65 if errno_from_exception(e) == errno.EINTR: 66 continue 67 else: 68 raise 69 if self._blocking_signal_threshold is not None: 70 signal.setitimer(signal.ITIMER_REAL, 71 self._blocking_signal_threshold, 0) # epoll_wait 结束, 再设置 signal alarm 72 self._events.update(event_pairs) # 将活跃事件加入 _events 73 while self._events: 74 fd, events = self._events.popitem() # 循环弹出事件 75 try: 76 fd_obj, handler_func = self._handlers[fd] # 处理事件 77 handler_func(fd_obj, events) 78 except (OSError, IOError) as e: 79 if errno_from_exception(e) == errno.EPIPE: 80 pass 81 else: 82 self.handle_callback_exception(self._handlers.get(fd)) 83 except Exception: 84 self.handle_callback_exception(self._handlers.get(fd)) 85 fd_obj = handler_func = None 86 finally: 87 self._stopped = False # 确保发生异常也继续运行 88 if self._blocking_signal_threshold is not None: 89 signal.setitimer(signal.ITIMER_REAL, 0, 0) # 清空 signal alarm 90 IOLoop._current.instance = old_current 91 if old_wakeup_fd is not None: 92 signal.set_wakeup_fd(old_wakeup_fd) # 和 start 开头部分对应,但是不是很清楚作用,求老司机带带路
最后来看 stop
:
stop
1 def stop(self): 2 self._running = False 3 self._stopped = True 4 self._waker.wake()
这个很简单,设置判断条件,然后调用 self._waker.wake()
向 pipe 写入随意字符唤醒 ioloop 事件循环(感谢 mlcyng 指正这里的错误)。 over!
总结
噗,写了这么长,终于写完了。 经过分析,我们可以看到, ioloop 实际上是对 epoll 的封装,并加入了一些对上层事件的处理和 server 相关的底层处理。
最后,感谢大家不辞辛苦看到这,文中理解有误的地方还请多多指教!