如何利用小游戏开发框架提升企业小程序的用户体验与运营效率
839
2022-09-20
Linux IO多路复用之Select简史
内容目录
前言早期的UnixTCP/IP诞生后终端复用套接字章节回顾结论引用
前言
最近我一直在思考 Linux 中的多路复用,即 epoll(7)[1]系统调用。我很好奇 epoll与Windows等操作系统的iocp 或 macOS等操作系统的kqueue相比,是更好还是更差呢?我想知道基于批处理 epoll_ctl 调用是否性能更佳。在我们开始认真讨论之前,我们不妨往前追溯下,先了解一些背景信息。首要问题是----文件描述符多路复用是 Unix 设计理念的偏差还是温和扩展?
要回答这些问题,我们必须首先讨论 epoll 的前身:select(2)[2]系统调用。正好趁此机会对Unix系统做一次考古!
在 1960 年代中期,分时系统仍是最近的发明。与以前的批处理系统相比,时间共享确实是革命性的。它大大减少了程序编写和结果输出之间的时间浪费。批处理意味着经常要等待数小时才能看到并判断程序的运行结果。
早期的Unix
1970 年开发出了 Unix 的第一个版本。需要强调的是:Unix 不是凭空创造出来的----它试图解决批处理系统存在的问题。其目的是创造一个更好的、多用户、分时的环境,以加快最常见的任务处理。“常见任务”主要是:执行需要大量 CPU 计算和大量磁盘访问的程序。
在当时,一个程序被执行时,它会在以下几种事件上“停止”(阻塞):
等待 CPU等待磁盘 I/O等待用户输入(等待 shell 命令)或控制台(打印数据太快)
在Linux 进程状态中,上述“停止”表示为:R、D、S 三种进程状态码。
1进程状态代码2 R 运行或可运行状态(在运行队列上)3 D 不可中断的睡眠状态(通常是IO)4 S 可中断的睡眠状态(等待事件完成)5 Z 已失效/僵尸状态,进程结束但尚未消亡(未被父进程回收)6 T 停止,要么是由于作业控制信号,要么是因为7 它正在被调试追踪8 [...]
一个进程的生命周期以 R“运行”状态开始,以其父进程从 Z“僵尸”状态回收它后结束。下图是其状态流转
让我们仔细看看 pipe(2)[3]。论文“UNIX 分时系统:回顾”[4]的作者Ritchie于 1978 年在其论文中写到:
在没有通用的进程间消息工具,甚至没有信号量等有限通信方案的情况下。事实证明,上述管道机制足以实现密切相关的协作进程之间所需的任何通信。[…] 但是管道在与守护进程(用来为多个用户服务)进行通信时没有任何用处。
在这里,Ritchie 似乎已经确认同步管道足以作为基本的进程间通信设施。
可能已经足够了!在操作系统BSD中,进程被限制最多有 20 个文件描述符。每个用户被限制为 20 个并发进程。这些系统真的很简陋。它们也不需要 IPC(进程间通信) 或复杂的 I/O。
例如,在早期的 Unix 中,没有文件描述符多路复用的概念。一个很好的例子是cu命令[5],全称是Call Unix 。其手册页讲到:
当与远程系统建立连接时,cu命令 forks成两个进程。一个从端口读取并写入终端,而另一个从终端读取并写入端口。
这是有道理的,因为所有的 I/O 都被阻塞了,让操作系统同时使用read方法来读取、write方法来写入的唯一途径是使用两个进程。
附带说明一下,如果您是 Golang 程序员,这可能听起来很熟悉。在 Golang 中,读写调用通常是阻塞的,所以当你想同时读写时,你不得不使用两个协程。
TCP/IP诞生后
这一切都在 1983 年随着 4.2BSD 的发布而改变。此版本引入了 TCP/IP 堆栈的早期实现,最重要的---- BSD 套接字 API。
尽管今天我们认为 BSD 套接字 API 存在是理所当然的,但其API的设计是正确的吗?STREAMS 框架是 System V Revision 3 上的比较完善的API 设计。
1在计算机网络中,STREAMS 是 Unix System V 中用于实现字符设备驱动程序、网络协议和进程间通信的本地框架。 在这个框架中,流是在程序和设备驱动程序之间(或在一对程序之间)传递消息的协程链。 2STREAMS 的设计是一种模块化架构,用于在内核和设备驱动程序之间实现全双工 I/O。 它最常用于开发终端 I/O(线路规程)和网络子系统。
随后 BSD 套接字 API 出现 select() 这个系统调用。为什么它的出现是有必要的呢?
我一直认为编写网络服务器的“正确”Unix 方法是为每个连接创建一个工作进程。在 TCP/IP 服务器的情况下,这代表着 accept-and-fork 模型:
1// 端口绑定 2sd = bind(); 3while (1) { 4 // 接受连接请求 5 cd = accept(sd); 6 // fork函数返回两个值,对于子进程,返回0; 父进程,返回子进程ID. 7 if (fork() == 0) { 8 close(sd); 9 // TODO:worker进程处理套接字“cd“相关逻辑.10 exit(0);11 }12 // TODO:回到“accept”循环,避免泄漏套接字“cd”相关逻辑。13 close(cd);14}
虽然这个模型可能足以编写基本的网络服务 ,但对于复杂程序来说还不够。
终端复用
1983 年左右,贝尔实验室的Rob Pike 正在开发 Blit----第八代Unix实验版(Research Unix 8th Edition)的一个可编程位图图形终端。
Blit 显然做了终端多路复用。它允许用户通过单个串行链路与多个终端连接后进行交互。
我向 Pike 先生询问了 select 的历史:
正如你所说的那样,Accept-and-fork 使得多个客户端无法在服务器上共享状态。这不仅仅是关于网络, Blit也受其影响。
虽然运行两个同步进程来为 cu (早期的Unix一章中有说到)供电就足够了,但不足以为 Blit 供电。Blit 确实需要某种套接字多路复用工具才能顺利工作。
有人可能会尝试扩展 cu 模型并将文件描述符和多路复用器组合在一起,方法是生成多个阻塞 I/O 的进程并让它们在某种 IPC 上同步。
不幸的是,在BSD上没有适合的 IPC 机制。System V(Unix操作系统的一个版本)的 IPC 于 1983 年 1 月发布,但在与 BSD 上实现相比没有任何的可比性。4.2BSD的手册页上也找不到任何真正的 IPC。
由于缺乏任何严格的 IPC 机制,Blit 似乎只需要 select 就可以进行控制台多路复用。
套接字
Kirk McKusick(美国著名计算机科学家,致力于BSD UNIX相关工作)解释了为什么会出现select:
引入 select 是为了允许应用程序进行IO多路复用。
你可以思考下:有一个简单的应用程序,例如远程登录。它的descriptor(描述符)可以对终端设备和双向套接字进行读写。这个程序可以读取终端键盘的字符并将它们写入套接字,也可以从套接字读取字符并写入终端。读取没有数据的空描述符导致应用程序阻塞,直到数据到达。应用程序不知道是从终端读取还是从套接字读取,如果猜错将也导致错误地阻塞。所以 select 登场了,它可以帮助程序找出哪个描述符已经准备好读取数据。如果两者都没有,程序会被 select 堵塞住,直到数据到达。然后select会去唤醒程序并告诉它哪个描述符有数据要读取。
[…] 非阻塞是在 select 的同时添加的。但是在读取描述符时使用非阻塞IO并不好。因为你需要写一个for循环来读取每个描述符的数据,要决定什么时候暂停对每个描述符的读取,还要考虑多久读一次描述符,这个过程很复杂,相比而言Select 的效率要高得多。
Select 还允许你创建一个单独的 inetd 守护程序,而不必为每个服务都有一个单独的守护程序。(inetd守护程序通过仅在需要时调用其他守护程序以及通过在内部提供几个简单的 Internet 服务而不调用其他守护程序来减少系统负载)
McKusick 先生确认非阻塞 I/O 在 select 之前根本不存在。此外,他引用了 cu 终端用例----如果没有 I/O 多路复用,很难编写 telnet 客户端。最后,他提到了 inetd守护进程,虽然后来在 4.3BSD 中引入了它,但如果没有 select,它是不可能实现的。
章节回顾
必须运行两个进程才能让 cu 工作是一种 hack。由于缺乏任何严格的 IPC,所以如果没有select,就不可能在 Blit 中模拟套接字多路复用。
此外,还需要 select 来实现 inetd。在架构级别 select 需要实现有状态的服务器,允许客户端连接之间的一些状态共享。
这是 1966 年“UNIX 分时系统:回顾”页面的另一个片段:
[在 UNIX 中] 输入和输出通常看起来是同步的;程序得先等 I/O 完成。[…] 仍然有一些特殊的应用程序希望在多个流上启动 I/O 并延迟直到仅在其中一个流上完成操作。当流的数量很少时,可以用几个进程来模拟这种用法。然而,Arpanet UNIX ncp(网络控制程序)接口的作者认为真正的异步 I/O 会显著改进他们的实现。
早期的 Unix 系统非常基础,根本不需要 select。并不是说C语言中的阻塞 I/O 模型被认为是每个人的最佳编程范式。这个模型很有意义,因为你所能做的只是对文件进行简单的操作。
这一切都随着网络的出现而改变。网络应用程序需要诸如 inetd、有状态服务器和终端仿真器(如 telnet)之类的东西。如果操作系统不允许套接字多路复用,这些事情将很难实现。
结论
在这次讨论中,我害怕说出核心问题。Unix 进程是否打算成为 CSP 风格的进程?文件描述符是 CSP 派生的“channels"(通道)吗?select 是否等同于 ALT 语句?
1来自维基百科:23通信顺序进程 (CSP) 是一种用于描述并发系统中交互模式的正式语言。 它是基于通过通道传递消息的并发数学理论家族的成员,称为进程代数或进程演算。CSP 在指令式过程式编程语言的设计中具有很大的影响力,也影响了 Limbo、RaftLib、Erlang、Go、Crystal等编程语言的设计。
我想不是的。即使有设计相似之处,它们也是偶然的。因为文件描述符出现的时间比 CSP 论文早。
似乎套接字 API 的发展完全脱离了普通的程序员,它不像CSP编程范式那样让普通程序员简单快速地编程并发程序。虽然很可惜,但看到一个与用户空间程序的编程范式一致的操作系统会很有趣。
引用
[1]https://man7.org/linux/man-pages/man7/epoll.7.html
[2]https://man7.org/linux/man-pages/man2/select.2.html
[3]https://linux.die-/man/2/pipe
[4]https://ia601600.us.archive.org/30/items/bstj57-6-1947/bstj57-6-1947_text.pdf
[5]https://computerhope.com/unix/ucu.htm
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~