作者:五阳
1996 年,linux系统的创始人 linus(林纳斯)在邮件中表达了自己对于进程和线程的深刻理解!以下是翻译的邮件内容。
翻译内容
传统认为“线程”和“进程”是独立的实体,但我个人认为这样想是一个重大错误,唯一这样想的理由是历史包袱。
线程和进程实际上都是一回事:即“执行上下文”,试图人为地区分两者的不同只是自我设限。
所谓“执行上下文”(以下简称COE),只不过是该COE的所有状态的集合。这些状态包括诸如CPU状态(寄存器等)、MMU状态(页映射)、权限状态(uid、gid)和各种“通信状态”(打开的文件、信号处理程序等)。
传统认为,“线程”和“进程”之间的区别主要在于线程具有CPU状态(加上可能的一些其他最小状态),而所有其他上下文来自进程。然而,这仅仅是一种划分计算环境(COE)整体状态的方法,并没有什么规定这种方法是正确的。局限于这种思维方式简直是愚蠢的。
Linux对此的理解(以及我希望事情运作的方式)是,没有所谓的“进程”或“线程”。只有整个计算环境的总和(在Linux中称为“任务”)。不同的计算环境可以共享它们上下文的部分内容,而这种共享能力所能实现的能力之一是传统的“线程/进程” 能力,但是这应该被看作一个 “子集“(这是一个重要的子集,这不是来自于设计,而是来自标准:显然,我们也希望在Linux上运行符合标准的线程程序)。
简而言之:不要围绕线程/进程的思维方式来设计。内核应该围绕COE的思维方式进行设计,然后pthreads库可以向希望以这种方式看待COE的用户导出有限的pthreads接口。
举个例子,当你用COE的思维方式而不是线程/进程的思维方式进行思考时,会变得可能的事情:
你可以做一个外部的 "cd" 程序,这是在UNIX、其他传统的进程线程操作系统上不可能做到的事(虽然例子很简单,但意思是你可以拥有这些不受传统UNIX/线程设置限制的“模块”)。执行:
父进程:clone(CLONE_VM|CLONE_FS);
子进程:execve("external-cd");
/* 由于 "execve()" 会解除 VM 关联,所以我们使用 CLONE_VM 的唯一理由是使克隆操作更快捷 */
你可以自然地使用 "vfork()"(它需要最少的内核支持,这种支持非常符合 CUA 的思路):
父进程:clone(CLONE_VM);
子进程:继续运行,最后调用 execve()
父进程:等待 execve
你可以创建外部 "IO 守护进程":
父进程:clone(CLONE_FILES);
子进程:打开文件描述符等
父进程:使用子进程打开的文件描述符,反之亦然。
插播解释 clone 和 execve
clone
在Linux系统中,clone()系统调用的原理是通过创建一个新的用户空间线程来实现进程的复制。这个新的线程可以与原线程共享内存空间、文件描述符等资源,从而可以实现资源的高效共享和协作。clone()系统调用非常灵活,可以通过参数来控制新进程和原进程之间的共享资源,比如可以选择是否共享文件描述符、共享内存空间等等,从而可以实现不同程度的资源共享和隔离。
execve
linux execve的定义如下,execve()执行由pathname引用的程序。这会导致当前由调用进程运行的程序被一个新程序替换,该新程序具有新初始化的堆栈、堆和(已初始化和未初始化)数据段。
int execve(const char pathname, char const argv[], char *const envp[]);
继续翻译原文~~~
上述所有工作之所以可行,是因为你没有被线程/进程的思维方式束缚住。举个例子,想象一个Web服务器,其中CGI脚本作为“执行线程”。如果使用传统线程的话,你无法做到这一点,因为传统线程总是需要共享整个地址空间,所以你必须将所有你想在Web服务器中进行的操作(即脚本)全部链接到服务器本身(传统做法,一个“线程”不能运行另一个可执行文件)。
如果将其视为一个“执行上下文”的问题,那么你的任务现在可以选择执行外部程序(即从父进程分离地址空间)等,或者例如可以与父进程共享除了文件描述符之外的一切(这样子线程可以打开很多文件,而父进程无需担心这些文件:当子线程退出时,这些文件描述符会自动关闭,而且不会占用父进程的文件描述符)。
linux 中可以使用 clone 轻量复制一个子线程,然后execve 执行cgi脚本,将新建的线程替换为另一个进程。
想象一下一个线程化的 "inetd",比如说。你希望有低开销的 fork+exec,所以按照 Linux 的方式,你可以不用 "fork()",而是编写一个多线程的 inetd,每个线程只使用 CLONE_VM 创建(共享地址空间,但不共享文件描述符等)。然后子线程可以执行 execve,如果它是一个外部服务(例如 rlogind),或者它可能是 inetd 的内部服务之一(比如 echo、timeofday),在这种情况下,它只执行它的功能然后退出。
你无法使用 "线程"/"进程" 来做到这一点。
插播解释 inetd
在嵌入式场景受限于硬件资源,使用inetd较多。
inetd 是一个服务器守护进程,负责管理其他网络服务程序。它通常分配给特定的端口,当有网络请求到达这个端口时,它会负责启动相应的服务程序,将请求转交给它们处理,并在完成请求处理之后将控制权还给inetd进程。
inetd的优势之一就是能够实现多种协议的服务共享,不同类型的服务共享一个端口,这样可以避免系统资源的浪费和端口的冲突。在嵌入式场景受限于硬件资源,使用inetd较多。
<顺便插播一句,如果你在看新的机会→前、后端 or 测试 ,技术大厂核心团队,薪酬一线标准,base 武汉、深圳、苏州等多地;语言:Java、Js、测试、python、ios、安卓、C++ 等 >
我的思考
林纳斯想要表达 什么内容呢?
在应用开发中存在进程和线程的概念,比如单进程多线程应用程序、多进程应用程序等模式。此外,还有诸如 pthreads 等线程标准库,它们在 Linux 系统中依靠系统调用 API 接口来实现相关功能。
然而,林纳斯认为系统调用API层 区分进程和线程的需要,不应该干扰 linux内核的设计。他坚持认为应该保持内核的抽象性和简洁性。这是因为操作系统的核心任务是管理软硬件资源,例如进程管理、内存管理等,如果额外增加另一个模型,会不可避免地增加系统设计的复杂性,并可能降低未来的扩展性。
因此综合考虑下,纳斯决定在 Linux 内核中,不对进程和线程进行严格区分,而是将其统一抽象为 "执行上下文"(Context of Execution,COE)。这种设计确保了内核结构的简洁性、可维护性、可扩展性。
即便是 30 年前的想法,依然不过时,依然值得我们所有人借鉴!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。