操作系统中最核心的概念就是进程:这是对正在运行程序的一个抽象。
操作系统的其他所有内容都是围绕着进程的概念展开的。
进程是操作系统提供的最古老的也是最重要的抽象概念之一。
即使可以利用的CPU只有一个,但它们也支持(伪)并发操作的能力。
它们将一个单独的CPU变换成多个虚拟的CPU。
没有进程的抽象,现代计算将不复存在。
进程
所有现代计算机经常会在同一时间做许多件事。
在任何多道程序设计系统中,CPU由一个进程快速切换至另一个进程,使每个进程各运行几十或几百个毫秒。
严格的说,在某个一瞬间,CPU只能运行一个进程。
但在1秒钟期间,它可能运行多个进程,这样就产生并行的错觉。
人们很难对多个并行活动进行跟踪。
因此,经过多年的努力,操作系统的设计者发展了用于描述并行的一种概念模型(顺序进程),使得并行更容易处理。
进程模型
在进程模型中,计算机上所有可运行的软件,通常也包括操作系统,被组织成若干顺序进程,简称进程。
一个进程就是一个正在执行程序的实例,包括程序计数器,寄存器和变量的当前值。
从概念上说,每个进程拥有它自己的虚拟CPU。
当然,实际上真正的CPU在各进程之间来回切换。
但为了理解这种系统,考虑在(伪)并行情况下运行的进程集,要比我们试图跟踪CPU如何在程序间来回切换简单得多。
一个进程是某种类型的一个活动,它有程序,输入,输出以及状态。
单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另一个进程提供服务。
值得注意的是,如果一个程序运行了两遍,则算作两个进程。
线程
在传统的操作系统中,每个进程有一个地址空间和一个控制线程。
不过,经常存在在同一个地址空间中准并行运行多个控制线程的情形,这些线程就像(差不多)分离的进程(共享地址空间除外)。
人们需要多线程的主要原因是,在许多应用中同时发生着多种活动。
其中某些活动随着时间的推移会被阻塞。
通过将这些应用程序分解成可以准并行运行的多个顺序线程,程序设计模型会变得更简单。
准确的说,正是之前关于进程模型的讨论,有了这样的抽象,我们才不必要考虑中断,定时器和上下文切换,而只需考虑并行进程。
类似的,只是在有了多线程概念之后,我们才加入了一种新的元素:并行实体共享同一个地址空间和所有可用数据的能力。
对于某些应用而言,这种能力是必需的,而这正是多进程模型(它们具有不同地址空间)所无法表达的。
第二个关于需要多线程的理由是,由于线程比进程更轻量级,所以它们比进程更容易(即更快)创建,也更容易撤销。
在许多系统中,创建一个线程较创建一个进程要快10-100倍。
在有大量线程需要动态和快速修改时,具有这一特性是很有用的。
需要多线程的第三个原因涉及性能方面的讨论。
若多个线程都是CPU密集型的,那么并不能获得性能上的增强,但是如果存在着大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠进行,从而会加快应用程序执行的速度。
经典的线程模型
线程给进程模型增加了一项内容,即在同一个进程环境中,允许彼此之间有较大独立性的多个线程执行。
在同一个进程中并行运行多个线程,是对在同一台计算机上并行运行多个进程的模拟。
在前一种情形下,多个线程共享同一个地址空间和其他资源。
在后一种情形下,多个进程共享物理内存,磁盘,打印机和其他资源。
由于线程具有进程的某些性质,所以有时被称为轻量级进程。
多线程这个术语,也用来描述在同一个进程中允许多个线程的情形。
一些CPU已经有直接硬件支持多线程,并允许线程切换在纳秒级完成。
进程用于把资源集中到一起,而线程则是在CPU上被调度执行的实体。
当多线程进程在单CPU系统中运行时,线程轮流运行。
进程中的不同线程不像不同进程之间那样存在很大的独立性。
同一个进程中的所有的线程都有完全一样的地址空间,这意味着它们也共享同样的全局变量。
由于各个线程都可以访问进程地址空间中的每一个内存地址,所以一个线程可以读,写或甚至清除另一个线程的堆栈。
线程之间是没有保护的,这与不同进程是有差别的。
不同进程会来自不同的用户,它们彼此之间可能有敌意,一个进程总是由某个用户所拥有,该用户创建多个线程应该是为了它们之间的合作而不是彼此间的争斗。
除了共享地址空间之外,同一个进程中的所有线程还共享同一个打开文件集,子进程,报警以及相关信号等。
对于三个没有关系的线程,应该放到不同的进程中。
认识到每个线程都有其自己的堆栈很重要,每个线程的堆栈有一帧,供各个被调用但是还没有从中返回的过程使用。
在该帧中存放了相应过程的局部变量以及过程调用完成之后使用的返回地址。
通常每个线程会调用不同的过程,从而有一个各自不同的执行历史。
这就是为什么每个线程需要有自己的堆栈的原因。
在多线程情况下,进程通常会从当前的单个线程开始。
这个线程有能力通过调用一个库函数(如thread_create)创建新的线程。
thread_create的参数专门指定了新线程要运行的过程名。
这里没有必要对新线程的地址空间加以规定,因为新线程会自动在创建线程的地址空间中运行。
当一个线程完成工作后,可以通过调用一个库过程(thread_exit)退出。
该线程接着消失,不再可调度。
在某些线程系统中,通过调用一个过程,例如thread_join,一个线程可以等待一个(特定)线程退出。
这个过程阻塞调用线程知道那个(特定)线程退出。
另一个常见的线程调用是thread_yield,它允许线程自动放弃CPU从而让另一个线程运行。
这样一个调用是很重要的,因为不同于进程,(线程库)无法利用时钟中断强制线程让出CPU。
所以设法使线程行为“高尚”起来,并且随着时间的推移自动交出CPU,以便让其他线程有机会运行,就变得非常重要。
用户级线程和内核级线程
为实现可移植的线程程序,IEEE在IEEE标准1003.1c中定义了线程的标准。
它定义的线程包叫做Pthread,大部分UNIX系统都支持该标准。
有两种主要的方法实现线程包:在用户空间中和在内核中。
这两种方法互有利弊,不过混合实现方式也是可能的。
第一种方法是把整个线程包放在用户空间中,内核对线程包一无所知。
从内核角度考虑,就是按正常的方式管理,即单线程进程。
用户级线程包可以在不支持线程的操作系统上实现。
线程在一个运行时系统的顶部运行,这个运行时系统是一个管理线程的过程的集合。
这时,保存线程状态的过程和调度程度都只是本地过程,所以启动它们比进行内核调用效率更高。
另一方面,不需要陷阱,不需要上下文切换,也不需要对内存高速缓存进行刷新,这就使得线程调度非常便捷。
用户级线程还有另一个优点,它允许每个进程有自己定制的调度算法。
尽管用户级线程包有更好的性能,但它也存在一些明显的问题。
其中第一个问题是如何实现阻塞系统调用。
另一个问题是,如果一个线程开始运行,那么在该进程中的其他线程就不能运行,除非第一个线程自动放弃CPU。
人们已经研究了各种试图将用户级线程的优点和内核级线程的优点结合起来的方法。
一种方法是使用内核级线程,然后讲用户级线程与某些或者全部内核线程多路复用起来。
采用这种方法,内核只识别内核级线程,并对其进行调度。
其中一些内核级线程会被多个用户级线程多路复用。