事件驱动

Event-Driven(事件驱动)这个词这几年随着 Node.js 的大热也成了一个热词,似乎已经成了“高性能”的代名词,殊不知事件驱动其实是通用计算机的胎记,是一种与生俱来的能力。本文我们就要一起了解一下事件驱动的价值和本质。

通用计算机中的时间驱动

首先我们定义当下最火的 x86 PC 机为典型的通用电子计算机:可以写文章,可以打游戏,可以上网聊天,可以读U盘,可以打印,可以设计三维模型,可以编辑渲染视频,可以作路由器,还可以控制巨大的工业机器。那么,这种计算机的事件驱动能力就很容易理解了:

  1. 假设 Chrome 正在播放 Youtube 视频,你按下了键盘上的空格键,视频暂停了。这个操作就是事件驱动:计算机获得了你单机空格的事件,于是把视频暂停了。
  2. 假设你正在跟人聊 QQ,别人发了一段文字给你,计算机获得了网络传输事件,于是将信息提取出来显示到了屏幕上,这也是事件驱动。

事件驱动的实现方式

事件驱动本质是由 CPU 提供的,因为 CPU 作为控制器+运算器,它需要随时响应意外事件,如上面例子中的键盘和网络。

CPU 对于意外事件的响应是依靠 Execption Control Flow(异常控制流)来实现的。

强大的异常控制流

异常控制流是 CPU 的核心功能,它是以下听起来高大上的功能的基础。

时间片

CPU 时间片的分配也是利用异常控制流来实现的,它让多个进程在宏观上位于同一个 CPU 核心上同时运行,而我们知道在微观上任一个时刻,每个 CPU 核心都只能运行一条指令。

虚拟内存

这里的虚拟内存不是 Windows 的虚拟内存,是 Linux 虚拟内存,即逻辑内存。

逻辑内存是用一段内存和一段磁盘上的存储空间放在一起组成的一个逻辑内存空间,对外依然表现为“线性数组内存空间”。逻辑内存引出了现代计算机的一个重要性能观念:

内存局部性天然的让相邻指令需要读写的内存空间也相邻,于是可以把一个进程的内存放到磁盘上,再把一小部分的“热数据”放到内存中,让其作为磁盘的缓存,这样可以在降低很少性能的情况下,大幅提升计算机能够同时运行的进程数量,从而大幅提升性能。

虚拟内存的本质其实是使用缓存+乐观的手段来提升计算机的性能。

系统调用

系统调用是进程向操作系统索取资源的通道,这也是利用异常控制流来实现的。

硬件中断

键盘点击、鼠标移动、网络接收到数据、麦克风有声音输入、插入 U 盘这些操作全部需要 CPU 暂时停下手头的工作,来做出响应。

进程、线程

进程的创建、管理和销毁全部都是基于异常控制流实现的,其生命周期的钩子函数也是操作系统依赖异常控制流实现的。在 Linux 上线程和进程没有功能上的区别。

编程语言中的 try-catch

C++ 编译成二进制程序,其异常控制语句是直接基于异常控制流实现的。Java 这种硬虚拟机语言,PHP 这种软虚拟机语言,其异常控制流的一部分也是基于最底层的异常控制流来实现的,另一部分可以用逻辑判断来实现。

基于异常控制流的事件驱动

其实现在人们讨论的事件驱动,是由 Linux 内核提供的 epoll,是 2002年10月18日伴随着 kernel 2.5.44 发布的,是 Linux 首次将操作系统中的 IO 事件和异常控制流暴露给了进程,实现了本文开头提的事件驱动。

Kqueue

FreeBSD 4.1 版本于 2000 年发布,一起携带的 Kqueue 是 BSD 系统中事件驱动的 API 提供者。BSD 系统如今已遍地开花,从 macOS 到 iOS,从 watchOS 到 PS4 游戏机,都受到了 Kqueue 的蒙荫。

epoll 是什么

操作系统本身就是事件驱动的,所以 epoll 并不是什么新发明,而只是把本来不给用户空间的 API 暴露给了用户空间而已。

epoll 做了什么

网络 IO 是一种纯异步的 IO 模型,所以 Nginx 和 Node.js 都基于 epoll 实现了完全的事件驱动,活得好了相比于 select/epoll 巨量的性能提升。而磁盘 IO 就没有这么幸运的,因此磁盘本身也是单体阻塞资源:即,有进程在写磁盘的时候,其他写入请求只能等待,就是天王老子来了也不行,磁盘做不到呀。所以磁盘 IO 是基于 epoll 实现的非阻塞 IO,但是其底层依旧是异步阻塞,即便这样,性能也已经爆棚了。Node 的磁盘 IO 性能远超其他解释型语言,过去几年在 web 后端霸占了一些对磁盘 IO 要求较高的领域。