朴素的UNIX之-进程/线程模型
因此就出现了线程。
线程事实上就是共享资源的不同的运行绪。
线程的语义和朴素的UNIX进程是不同的。
0.原始进程模型-著名的fork调用
朴素的UNIX进程依托于著名的fork调用。就是这个fork调用让UNIX进程和Windows进程截然不同。也正是由于这个fork调用,使二者没有兼容的余地。这个fork调用的根源有久远的历史。早在UNIX之前的大型操作系统中,它就存在了。UNIX刚出现的1969年,事实上并未引入fork调用。当时之有两个固定的进程连接两个终端。当fork调用引入后,进程的数量便高速添加了,注意,此时暂且还没有exec调用!在理解fork背后的哲学之前,先看一下什么是fork。fork就是叉子,由同一个叉子柄逐渐分叉,变成一把叉子,也相似那种道生一,一生二,二生三。三生万物。我们看到。有了fork。理论上能够生成无数的进程。它们都能够向上回溯到同样的根!为何UNIX会採用这个模型?我们首先要理解,在还没有“可运行文件”概念的时候。进程意味着什么。
试想程序最初是怎么录入到计算机的。
今天它们理所当然地存在于磁盘上,作为“可运行文件”已经深入人心。可是在1950-1960年代初,程序都是现场录入的。通过原始的纸带或者携带非常重的磁带,文件系统还没有概念。整个纸带。磁带上的内容就是计算机要运行的程序,运行完了,想运行还有一个程序,就要换介质...人们写一个程序当然是为了做一件不止做一次的事,因此假设能够有多个“进程”同一时候运行纸带/磁带上的程序,系统的吞吐率将大大提高,注意,多个进程运行的是同一个程序!
这是最朴素的分时系统进程模型。
fork在伯克利分时系统应运而生!
fork提供了复制当前运行流的手段。fork出来的全部子进程能够方便地运行同样的代码。
这个著名的fork调用深深影响了人们怎样解释分时系统!
自然而然在1970年代初引入了朴素的UNIX,说fork调用著名,就是由于它尾随UNIX(以及类UNIX。比方Linux)至今。直接影响了UNIX的进程模型。如今总结UNIX为何採用fork调用来生成进程。我们知道从0到1非常难,从1到2相对easy。也比較难,从2到3...就非常easy了。这就是道生一,...三生万物。1969年的UNIX中已经有了两个进程。使用fork能够超级简单地实现二生三,三生万物。于是,或许是一种巧合,早先的伯克利分时系统的fork正好就在那里,便被托马斯引入了UNIX。
我想说一下为何是三生万物而不是二生万物。道生一这个是最难的,我们都知道。
0和1是两个极其特殊的数字,0更加特殊。
2也比較特殊。可是3就非常一般了,为何2特殊呢?我不想用博弈理论来描写叙述。仅仅是举一个样例。2个人在一起,闻到一股屁味,每一个人都肯定能百分百确定是谁放的,假设是我,那我肯定知道,假设我没有放,那肯定是对方。当然两人一起放的几率也是有的。
可是3个人在一起的时候,除了真正放屁的那个人之外的2个人根本无法推断这个屁究竟是谁放的。这就是3和0,1。2的本质差别。所以三生万物。
1.UNIX进程模型
在UNIX伊始,进程的概念和其史前前辈是一致的,那个时候文件系统相当不成熟。程序猿关注的是运行好不easy写好的任务而不是编写任务本身(首先是没有那么大的需求,其次是信息存储是一个问题,没有互联网。能够对照一下如今的AppStore...)。fork调用便直接将UNIX的进程组织成了tree,于是:1.0号swap/sched进程和1号init进程便有了特殊地位。
2.形成了谁fork谁wait并回收的模型,在tree组织中这个非常重要,便于资源回收;
3.假设父进程先退出,将全部子进程过继给init,这导致init必须存在且不容退出。总之。不论什么进程不能脱离整个进程tree。
总之,朴素的UNIX进程就是处在一棵树的某个节点的可运行对象。
注意。它是可运行对象。
UNIX进程模型就是在上述基本原则上构建的。除此之外,在外围,UNIX延续了歇菜的Multics项目的shell思想,为每一个终端开放了一个shell。shell是UNIX系统的第二个重要特征(假设先不说文件抽象的话!)。它须要fork出来的进程exec出一个新的不同的运行流。
从以上fork/exec的历史上看。它们从一開始就是分离的,这就构建了完整的UNIX进程模型:fork+exec。
我们看一下UNIX的进程模型能够构建哪些东西。早期的UNIX将进程进行了组织。伙同终端的概念。UNIX给出了进程组,会话的概念。
进程组是相关联的一组进程的集合,比方管道符连接的各个命令。很多其它的是它们之间的关联由用户来解释。会话则是进程组的集合。会话的意义在于用户能够方便地让多个进程组以某种形式共享终端訪问权。由于坐在一个终端前的是一个人,他每次运行一个操作,这个操作作用给谁就是一个问题。
他能够创建一个会话。该会话内创建多个进程组,他以自己的方式让不同的进程组轮流成为前台进程组从而操作它。
会话和进程组的概念能够理解成由操作员控制的分时系统,仅仅是调度者不再是操作系统,而成了终端前的操作员。和每一个CPU同一时候仅仅能有一个进程运行相似,每一个终端会话同一时候仅仅能有一个前台进程组。
我们能够看到。UNIX进程模型构建的进程组织自然而然形成了一个分级的分时调度层次。最底层是进程,由操作系统内核调度。然后是进程组,协作完毕一个任务,组织多个进程,由创建所属会话的操作员调度。在这个分级的层次底层,全部的进程组织成一棵tree。这就是完整的UNIX进程模型构建的图景。
之所以能够构建如此漂亮的图景,fork+exec是基本原则。fork和exec之间。给了进程很多其它的控制自己的空间。怎样控制自己属于哪一个组或者会话,由进程自己决定而不是调用者决定,相反的样例请看一下Win32 API的CreateProcess。
如今麻烦来了,线程出现了,该怎么办?假设你想知道Linux是怎么创造历史的,请直接跳到最后。
我之所以没有提及不论什么UNIX版本号对上述构建的实现,是由于思想远比实现更重要,实现反而会拖累你构建新的模型。本文的最后。我会说明Linux是怎样调和不同的进程模型之间的语义的,同一时候印证了UNIX进程模型的先进性。
2.提供资源环境的进程模型
Windows NT尽管在非常多方面都借鉴了UNIX的思想。可是在进程模型上却採用了一种截然不同的思路。Windows NT出生的1990年代。应用已经開始遍地开花。文件系统也已经非常成熟,可运行文件的概念延续自MS-DOS时代(事实上UNIXv6版本号就有可运行文件的概念,在UNIX引入exec调用之后。可运行文件仅仅是进程的后备资源。仅此而已)。人们能够基于Win32 API开发大量不同的程序,然后让它们分别运行,假设你想让一个程序运行多次,多点击它几次便是了。在这样的时代,正如本文最初所说的。运行的粒度细化到了一个程序的内部。
一个应用程序要完毕一项任务,须要做不同的几件事,可能须要同一时候进行这几件事,相似数学中的统筹方法。进程。在WinNT中也能够等同于从可运行文件里抽取出来的命名资源集合,已经不再适合作为可运行的对象,真正可运行的对象成了线程。
此时的进程仅仅是提供了一个资源环境,线程使用这些能够共享的资源共同完毕详细的事情。
这样的提供资源环境的进程模型我称为资源模型。
在本小节。我尽管以WinNT作为样例来描写叙述第二种进程模型。仅仅是由于它作为这样的模型的代表比較纯粹。实际上,非常多的UNIX版本号也在努力融合fork模型和资源模型这两者。企图既能继承UNIX的语义,又能实现多线程调度。
3.两种模型的调和
首先,fork模型和资源模型的冲突是明显的,典型体现于下面两个方面:1.信号问题:究竟哪个线程运行信号处理;
2.fork语义:假设已经运行了一个线程,在当中运行了fork,怎样来解释fork的是哪个运行流。
当中第一个问题比較好解决。规定假设不是线程自身引发的异常导致的信号,就由随意线程来处理,反之由引发异常的线程来处理。第二个问题比較棘手,棘手之处在于某个UNIX是怎么实现进程模型的。
在进程结构体或者u区中维护一个链表。保存线程控制块指针!Oh,NO。这是怎么回事啊。UNIX怎么会忘了可运行的对象是进程啊。如此一来,进程岂不成了线程的容器?直接倒向了资源模型,然而自己确实是纯正的UNIX!设计LWP是一个好方案吗?可能是,可是它引入非常多的高层抽象,显得复杂了,假设几年后再引入一个新的什么什么程呢?总之。不论什么改动朴素UNIX进程模型的方法都不是好方法。那么用户库级别的线程呢?这不属于内核的范畴,但表现了内核的无能为力。
抛开实现,回到思想。
我们再来看看进程,进程组。会话之间的关系。最主要的可运行对象是进程,上面的进程组。会话都是以某种组织形式对进程集合的封装。每一个集合都有一系列的资源可供这个集合中的进程共享。比方会话的环境变量。进程组的命令行变量等,线程是什么呢,线程不就是一组运行流的集合共享内存地址空间吗?明确了些什么吗?假设不明确,我们能够把UNIX进程模型图景中的进程改成调度实体,仅仅须要在这个图景的基础上往下走一层,线程自然而然就被支持了:
线程,线程集合。进程组,会话...
换成调度实体的说法,就是:
调度实体,调度实体组,进程组。会话...
就像进程组里面能够仅仅有一个进程,组ID等于进程ID一样,进程里面也能够仅仅有一个线程,线程ID就是进程ID。一切都统一到这个UNIX进程模型的图景中了,假设一个线程集合仅仅有一个线程。那么我们就称其为进程,假设拥有不止一个线程,我们就称这个集合为进程,而集合的元素为线程。事实上,此时此刻,怎么称呼已经无所谓了。
如今还缺什么?缺的是怎样实现线程集合共享内存地址空间。传统的UNIX fork模型无疑无法做到这一点,由于它没有不论什么參数用来指示实现这样的行为。
于是须要略微改动一下fork语义。引入一个clone调用,含实用户能够控制的參数:
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
用户不但能够控制用户栈的位置,还能够有诸多的flags可供选择。假设要共享调用者的内存,CLONE_VM这个标志无疑是须要的。当然想clone线程不仅仅须要这一个标志。这里就不细说了。详细能够參考NPTL最新规范。
4.Linux的对UNIX进程模型的实现
Linux实现的线程支持非常帅,它差点儿没有触动不论什么已经有的task_struct结构体。也没有改变不论什么既有的fork语义。它仅仅是引入了一个PID类型,叫做TGID,即进程组ID。Linux中的可运行对象就是task_struct,并且仅仅有task_struct。每一个task_struct拥有不止一个ID,按照这些ID的不同的解释方式即不同的类型。将task_struct定位到一个进程或者是一个进程的某个线程。ID类型例如以下所看到的:enum pid_type
{
PIDTYPE_PID,
PIDTYPE_TGID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX
};
当中:
PIDTYPE_PID:调度实体ID。
假设该task_struct是一个进程的线程,那么它就是线程ID,假设该进程仅仅有唯一的线程。那么它同一时候也是进程ID。
PIDTYPE_TGID,:线程集合ID。假设该task_struct所属的进程拥有多个线程。它就是进程ID,假设仅仅有一个线程,它等同于PIDTYPE_PID。
PIDTYPE_PGID:进程组ID。不解释;
PIDTYPE_SID:会话ID。
不解释。
依据上述解释。无论一个进程拥有一个线程还是拥有多个线程。其进程ID即PID均等于PIDTYPE_TGID标识的ID。
而PIDTYPE_PID标识的ID则依据详细情况给予不同的解释。详细实施例如以下:
1.每一个task_struct均有一个本PID命名空间内唯一的ID标识符,初始化时将其同一时候赋给进程ID和线程ID。
2.假设该task_struct是一个进程的第一个线程,即由标准的fork调用创建,那么保持1的初始化数值不变。
3.假设该task_struct不是一个进程的第一个线程。即由带有CLONE_VM等的clone调用创建,那么将当前调用者的PIDTYPE_TGID标识的ID覆盖新task_struct的PIDTYPE_TGID标识的ID。
4.关于进程组ID以及会话ID的设置。有专门的setpgid, setpgrp,setsid等系统调用来完毕,实现非常相似上述进程和线程。
5.每一个task_struct中有4个pid结构体。将这些pid结构体而不是task_struct本身用链表连接起来,指示谁是进程。谁是哪个进程的线程,谁是哪个进程组当头的组成员...
总之。在Linux中。无论是线程,还是进程,都是使用task_struct这个结构体。由其PID type的值的连接方式指示怎样构建UNIX进程模型的图景,这真的是太帅了。个人觉得还是用一张图表示连接方式比較直观,文字表达在这方面弱爆了:
5.一段富有诗意的话
丹尼斯.里奇在回想UNIX的发展史时,在最后说了一段话。这段话简直出自诗人之口,此诗意仅仅有真情实感真性情方可抒发,可见丹尼斯.里奇对UNIX的感情是多么特殊:One of the comforting things about old memories is their tendencyto take on a rosy glow.The programming environment provided by the early versions of Unix seems,when described here, to be extremely harsh and primitive.I am sure that if forced back to the PDP-7 I would find it intolerably limiting andlacking in conveniences.Nevertheless, it did not seem so at the time;the memory fixes on what was good and what lasted, and on the joy of helpingto create the improvements that made life better.In ten years, I hope we can look back with the same mixed impressionof progress combined with continuity.

浙公网安备 33010602011771号