第五章 线程(操作系统)

第五章 线程(操作系统)
第五章 线程(操作系统)

操作系统概念(第六版) 

第五章线程

更新日期:2004-9-9

第四章介绍了进程模型,进程模型假设一个进程是一个正在执行的程序,是一个单独的控制执行序列。现在有许多现代操作系统提供了进程包含多个控制执行序列的特性。本章将介绍多线程计算机系统中的一些概念,包括了对Pthread API和Java线程的讨论。我们将会看到许多与多线程程序设计有关的问题,并且将了解它(多线程程序设计)是如何影响操作系统的设计的。最后,我们将探讨几个现代操作系统是如何在内核级(kernel level)支持线程的。

5.1 综述

线程,有时也被称为轻量级进程(LWP),是一个基本的CPU执行单元;它包含了一个线程ID、一个程序计数器、一个寄存器组和一个堆栈。它与属于同一个进程的其它的线程共享代码段、数据段,以及其它的操作系统资源(比如:打开的文件和信号)。一个传统的(或者说重量级)进程有一个单独的控制执行序列。如果一个进程有多个控制执行序列,那么它就能够同时进行多个任务。图5.1说明了传统的单线程进程和多线程进程的区别。

5.1.1 线程的起源

运行在现代桌面PC上的一些软件包是多线程的。应用程序通常被实现为一个拥有多个控制执行序列的独立的进程。一个网页浏览器可能会有一个线程来显示图片或文本,而同时有其它的线程从网络上获取数据。一个字处理器可能有一个线程显示图像,另一个线程读取用户按键,并且有第三个线程在后台执行拼写和语法检查。

Figure 5.1 Single- and multithreaded processes.

在某些情况下,一个单独的应用程序可能需要执行几个相似的任务。例如,一个Web服务器接收到对网页、图片、声音等的客户端请求。流量比较高的服务器(busy web server)可能同时有很多(可能有数百个)客户端访问它。如果Web服务器作为一个传统的单线程进程运行,那么它同时只能够响应一个客户端。客户端等待服务的时间可能会非常长。

一种解决方案是使服务器作为一个单独的接收请求的进程来运行。当服务器接收到一个请求时,它就

创建一个独立的进程来处理这个请求。事实上,这种创建进程的方法在线程广泛应用之前非常普遍。如上一章所述,进程的创建是重量级的。如果一个新进程要执行的任务与已存在进程的任务相同,那么何必要耗费所有这些开销呢?通常更高效的方法是创建一个包含了多个线程的进程来服务同样的请求。这种方法将使web服务进程多线程化。(This approach would multithread the web-server process.)服务器将创建一个独立的线程来监听客户端请求;当一个请求产生时,它就创建另一个线程来服务这个请求,而不是创建一个进程。

线程在远程过程调用(RPC)系统中也是一个很重要的角色。第四章讲到RPC建立了一个类似于普通的函数或过程调用的通信机制,从而允许进程间通信。典型的RPC服务器是多线程的。当一个服务器接收到一个消息时,它使用一个独立的线程处理该消息。这允许服务器服务多个并发的请求。

5.1.2 线程的优点

多线程程序设计的优点可以如下四类:

1.提高了响应速度:多线程交互式应用程序可以允许程序在它的一部分被阻塞或正在执行一个冗长的操作时持续运行,从而提高了了对用户的响应速度。例如,一个多线程网页浏览器可以在一个线程下载图片时利用另外一个线程与用户交互。

2.资源共享:缺省情况下,线程共享它们所属进程的存储器和资源。代码共享的优点在于它允许应用程序在同样的地址空间内拥有多个不同的活动线程。

3.经济实惠:为进程创建分配存储器和资源代价高昂。因为线程共享它们所属进程的资源,所以线程的创建和上下文转换更为划算。创建和维护进程与创建和维护线程的开销孰大孰小难以测量(一般根据经验判断),但是通常创建和管理进程比创建和管理线程需要更多的时间。在Solaris 2中,创建线程的速度比创建进程快30倍,上下文转换速度快5倍。

4.提高了多处理机体系结构的利用率:在多处理机体系结构中,多线程的优点就更加显著了。在这种系统中,多个线程可以在不同的处理器上并行运行。一个单线程进程只能够在一个CPU上运行,而不论有多少CPU可供使用。在多CPU机器中,多线程提高了并发性。在单处理机体系结构中,CPU通常快速的在每个线程之间移动,如此以至于用户感觉到这是在并行运行(这是个假象),但是实际上同时只有一个线程在运行。

5.1.3 用户线程和内核线程

至此,我们依然以一种普通的认识来对待线程。然而,通常在用户级(用户线程)或内核级(内核线程)来提供对线程的支持。

l对用户线程的支持通常处于内核之上,通过一个用户级线程库(thread library)实现。线程库提供了对线程的创建、调度和管理的支持,这无需来自内核的支持。因为内核并不知道用户级线程

的存在,所有的线程创建和调度工作都在用户空间完成,而且整个过程不受内核的干涉。所以,用户级线程的创建和管理通常很快;然而,它们也有一些缺点。例如:如果内核是单线程的,那么任何用户级线程执行一个导致阻塞的系统调用时就会导致整个进程阻塞,即使程序内部的其它线程可以运行。用户线程库包括了POSIX Pthreads、Mach C-threads和Solaris 2 UI-threads。

l内核线程由操作系统直接支持:内核在内核空间内实现了线程的创建、调度和管理。因为线程管理由操作系统完成,所以内核线程的创建和管理要比用户线程慢。然而,由于线程由内核管理,如果一个线程执行一个导致阻塞的系统调用,那么内核可以调度程序中的其它线程执行。同样,在多处理机环境中,内核能够在不同的处理器中调度线程。大多数现代操作系统都支持内核线程,包括:Windows NT、Windows 2000、Solaris 2、BeOS和Tru64 UNIX (原先的Digital UNIX)。

我们将在5.4节讨论Pthread,把它作为一个用户级线程库的例子。也将讨论Windows 2000(5.6节)和Solaris 2(5.5节),把它们作为支持内核线程的操作系统的例子。我们还将在5.7节讨论Linux是如何支持线程的(虽然Linux并不完全区分线程和进程)。

Java也提供了对线程的支持。然而,由于Java线程的创建和管理通过Java虚拟机(JVM)完成,所以就不能简单的把它归为用户线程或内核线程。我们将在5.8节讨论Java线程。

5.2 多线程模型

有些系统同时支持用户线程和内核线程,由此产生了不同的多线程模型。我们看一下通常的三种线程

实现类型。

5.2.1 多对一模型

多对一模型(many-to-one model)(图5.2)将多个用户级线程映射到一个内核线程。线程管理在用户空间完成,所以它的效率比较高。但是如果一个线程调用了导致阻塞的系统调用的话,那么将阻塞整个进程。而且,因为一次只有一个线程可以访问内核,所以在多处理机环境中多个线程不能够并发执行。Green thread(一个Solaris 2的线程库)采用了这种模型。另外,用户级线程库在那些采用了多对一模型不支持内核线程的操作系统上实现。

Figure 5.2 Many-to-one model.

5.2.2 一对一模型

一对一模型(one-to-one model)(图5.3)将每个用户线程映射到一个内核线程。它允许在一个线程调用导致阻塞的系统调用的情况下持续运行其它的线程,从而提供了比多对一模型更好的并发性;它也允许多个线程在多处理机环境中并行执行。这种模型的唯一缺点在于创建一个用户线程就需要创建一个相应的内核线程。因为创建内核线程的开销会加重应用程序的负担,所以这种模型的大多数实现都要限制系统支持的线程数量。Windows NT、Windows 2000和OS/2实现了一对一模型。

5.2.3 多对多模型

多对多模型(many-to-many model )(图5.4)将用户级线程多路复用到与之数量相等或少一点的内核线程。(The many-to-many model multiplexes many user-level threads to a smaller or equal number of kernel threads.)内核线程的数量由具体的应用程序或具体的机器确定(分配给应用程序的内核线程数量在多处理机环境中可能比单处理机环境中多)。然而,多对一模型允许开发者随心所欲的创建用户线程。但是,因为内核一次只能调度一个线程,所以并不能获得真正的并行性。一对一模型允许更大的并行性,但是开发者必须小心,以免在一个程序中创建过多的线程(而且在某些情况下可能会限制开发者能够创建的线程的

数目)。多对多模型则免却了所有这些缺点:开发者能够创建所需的用户线程,而且相应的内核线程能够在多处理机环境中并行运行。而且当一个线程执行导致阻塞的系统调用时,内核能够调度其它的线程执行。Solaris 2、IRIX、HP-UX和Tru64 UNIX支持这种模型。

Figure 5.3 One-to-one model.

Figure 5.4 Many-to-many model.

5.3 与线程相关的问题

在这一节我们讨论一些与多线程程序相关的问题。

5.3.1 fork和exec系统调用

在第四章我们描述了怎样使用fork系统调用创建一个独立的复制的进程。在一个多线程程序中,fork 和exec系统调用的语义有所改变。如果一个程序中的线程调用fork系统调用,那么新进程是复制所有的线程呢还是新进程是单线程的呢?有些UNIX系统拥有两种fork,一种复制所有的线程,另一种仅仅复制调用了fork系统调用的那个线程。exec系统调用与第四章所描述的工作方式相同。就是说,如果一个线程调用了exec系统调用,那么传给exec的参数中指定的程序将取代整个进程——包括所有的线程和LWP。

两种fork方案的使用要根据应用程序的情况。如果在调用fork之后立即调用exec,那么就无需复制所有的线程,因为在传给exec的参数中指定的程序将取代该进程。在这种情况下,只需复制调用fork的线程就可以了。然而,如果在调用fork后并不调用exec,那就要复制所有的线程。

5.3.2 取消线程

取消线程是指在线程完成之前终止它。例如,如果多个线程并行搜索一个数据库,而一个线程返回了结果,那么就可能需要取消仍在搜索的线程。当用户点击网页浏览器上的按钮来停止下载一个网页时,也会发生这种情况(取消线程)。通常是有一个独立的线程下载网页。当用户点击停止按钮时,下载网页的线程就被取消。

即将被取消的线程通常被称为目标线程(target thread)。目标线程的取消有两种不同的情况:

1.异步取消(asynchronous cancellation):一个线程立即终止目标线程。

2.延迟取消(deferred cancellation):目标线程定时检测是否需要终止,这种方式允许目标线程以有

序的方式选择机会终止自身。(The target thread can periodically check if it should terminate, allowing the target thread an opportunity to terminate itself in an orderly fashion.)

如果被取消的线程被分配有资源或一个线程被取消时正在更新与其它线程共享的数据,那么在这种情况下取消线程会面临一些问题。这对异步取消来说尤其麻烦。操作系统通常从一个被取消的线程中收回系统资源,但是往往不回收所有的资源。所以,异步取消线程可能不会释放必须的系统资源(a necessary system-wide resource)。

换句话说,延迟取消线程要由一个线程指明一个要被取消的目标线程。然而,只有当目标线程检查决定自己应该被取消时才会取消。这允许当一个线程能够被安全取消时,该线程检查在某一点是否应该被取消。Pthread称这样的点为取消点(cancellation point)。

大多数操作系统允许异步取消进程或线程。而Pthread API提供了延迟取消。这意味着实现了Pthread API的操作系统允许延迟取消。

5.3.3 信号处理

UNIX系统使用信号通知进程发生了某个特定的事件。基于信号的来源和发出信号的原因,信号的接收可以是同步的或异步的。不论信号是同步的还是异步的,所有的信号都遵从同样的模式:

1.特定事件的发生产生一个信号。

2.产生的信号被传送给进程。

3.信号传送完毕后要得到处理。

同步信号的例子包括非法内存访问和除以零错误。在这种情况下,如果一个正在运行的程序执行了这样的任意一个操作,就产生一个信号。同步信号被传送给执行了产生该信号的操作的进程(它们因此被认为是同步的)。

当一个信号在运行的程序外部产生时,进程就要异步接收信号。这种信号的例子有通过指定的按键(如)来终止一个进程或利用计时器期限。异步信号通常被发送给另外一个进程。

每个信号可能会由如下的某个可能的处理程序处理:

1.一个缺省的信号处理程序。

2.一个用户定义的信号处理程序。

在处理信号的时候,每个信号都有由操作系统运行的一个缺省的信号处理程序。这个缺省的行为可能为用户定义的信号处理程序所重载(override)。对同步和异步信号的处理可能会有所不同。有些信号只是被忽视了(如改变一个窗口的形状);其它的信号可能会终止一个程序(如非法内存访问)。

在一个单线程程序中处理信号是非常容易的;信号总是传送给一个进程。然而,在多线程程序中传送信号就复杂多了,因为一个进程可能会有多个线程。那么把信号传送给哪个线程呢?

通常会有如下的四种方式:

1.将信号传送给信号请求的线程。

2.将信号传送给进程中的每一个线程。

3.将信号传送给进程中特定的线程。

4.分派一个指定的线程接收发给进程的所有信号。

传送信号的方法基于信号产生的类型。例如,同步信号需要被发送给产生信号的线程,而不是进程中的其它线程。然而,异步信号的情况就不这么明朗了。如终止一个进程(比如)等一些异步信号应该被发送给所有的线程。有些UNIX的多线程版本允许一个线程指明它将接收哪些信号阻塞哪些信号。所以,有些异步信号只能传送给那些不阻塞这些信号的线程。然而有些异步信号只需要处理一次,一个信号通常只传送给从进程中找到的第一个不阻塞该信号的线程。Solaris 2实现了第四种方式:它在每个进程中单独创建一个特殊的线程来处理信号。当一个异步信号发送给进程时,它被传送给这个特殊的线程,然后传送给第一个不阻塞该信号的线程。

Windows 2000不直接支持信号机制,而是使用异步过程调用(APC)。(Although Windows 2000 does not explicitly provide support for signals, they can be emulated using asynchronous procedure calls (APCs).)APC允许一个用户线程指定一个函数,该函数在这个用户线程接收到一个特定事件的通知时被调用。因为通过名

称来识别,所以APC与UNIX中的异步信号很相似。然而在多线程环境下UNIX必须要解决怎样处理信号的问题,而APC是传送给一个特定的线程而不是进程,也更加简单。

5.3.4 线程池

我们在5.1节讨论了多线程网页服务器的情况。在这种情况下,不论服务器何时接收到一个请求,它都会创建一个独立的线程来处理这个请求。而创建一个独立的线程明显要优于创建一个独立的进程,虽然如此,多线程服务器依然面临一些潜在的问题。第一个问题是在处理请求之前创建线程的时间需求,还要考虑到该线程一旦完成工作就要被丢弃。第二个问题就更加严重了:如果我们允许为每一个请求都创建一个新线程来处理,那么我们难以在系统中实现足够数量的线程。不受限制的创建线程可能耗尽系统资源,比如:CPU时间或存储器。一种解决方案是线程池。

线程池的思想是在进程开始时创建一定数量的线程并将它们置入一个池(pool)中,线程在这个池中等待工作。当服务器接收到一个请求时,它就从池中唤醒一个线程(如果有可用的线程),由它来处理请求。一旦线程服务完毕,它就返回线程池等待后面的工作。如果池中没有可用的线程,那么服务器就等待,直到某个线程被释放。

线程池有如下优点:

1.利用已存在的线程服务请求要比等待创建一个线程要快。

2.线程池限制了线程的数量。(A thread pool limits the number of threads that exist at any one point.)在

不能够支持大量的并发线程的系统中这一点特别重要。

线程池中的线程数量的设定要根据一些因素,比如:系统中的CPU数量、物理内存的容量和所期望的并发的客户端请求的数量。更加完善的线程池体系结构能够根据应用情况动态调节线程池中的线程数量。这样的体系结构优点更多,系统负载较低时拥有一个较小的线程池,因此所需的存储器更少。

5.3.5 Thread-Specific Data

进程中的线程共享属于进程的数据。这种共享的确是多线程程序设计的优点之一。然而,在有些环境下每个线程可能需要拥有自己的数据。我们把这种数据称为thread-specific date。例如,在一个事务处理系统(transaction-processing system)中,我们可能要使用独立的线程处理每个事务。更进一步讲,为事务赋予一个唯一的标识符。为了使每个线程联系到它的标识符,我们可能要使用thread-specific date。大多数线程库(包括Win32和Pthread)提供了对thread-specific date的某些形式的支持。Java也提供了支持。

5.4 Pthread

PThread遵照POSIX标准,该标准规定了线程创建和同步的API。(Pthread refers to the POSIX standard (IEEE 1003.1c) defining an API for thread creation and synchronization.)这是对线程行为制定的一个规范,而不是一个实现。操作系统的设计者可以以自己的方式来实现该规范。通常,实现了Pthread标准的线程库被认为是基于UNIX的系统,如:Solaris 2。虽然在公共领域内可以获取Windows共享版,但是Windows 操作系统通常不支持Pthread。

作为一个用户级线程库的例子,我们将在本节介绍Pthread API中的部分函数。用Pthread API创建的线程和内核线程之间没有任何显著的关系,因此我们说它是一个用户级线程库。(We refer to it as a user-level library because no distinct relationship exists between a thread created using the Pthread API and any associated kernel threads.)图5.5中的C程序示范了创建一个多线程程序的基本的Pthread API。如果你有兴趣了解Pthread API更详细的内容,请参考一下本章后的文献注记。

图5.5中的程序创建了一个独立的线程,该线程计算一个非负整数的和。在Pthread程序中,独立的线程在一个指定的函数中开始执行。在图5.5中,该函数是runner。当这个程序开始运行时,一个单独的控制执行序列在主程序中开始运行。一些初始化工作完成后,主程序创建第二个线程,然后第二个线程运行runner函数。

现在我们进一步了解这个程序。所有的Pthread程序必须要包含pthread.h头文件。语句pthread_t tid 声明了我们将要创建的线程的标识符。每个线程有一组属性,这包括堆栈的大小和调度信息。声明pthread_attr_t attr表示了线程的属性。pthread_attr_init (&attr)函数设置属性值。因为这儿并不直接设置任何属性,所以使用缺省的属性。pthread_create函数创建一个独立的线程。除了要传送线程标识符和线程的属性值之外,还要传送新线程将开始执行的函数的名称,在此是runner函数。最后,我们传送在命令行中提

供的整型参数,argv [1]。

(At this point, 此刻,程序中就有两个线程:一个初始化线程在主函数中运行,一个运行runner函数求和。

the program has two threads: the initial thread in main and the thread performing the summation in the runner function.)创建第二个线程之后,主线程通过调用pthread_join函数等待runner线程执行完毕。在调用pthread_exit函数后,runner线程完成工作。

Figure 5.5 Multithreaded C program using the Pthread API.

5.5 Solaris 2线程

Solaris 2是一个UNIX版本,它在内核和用户级、SMP和实时调度中支持线程。(Solaris 2 is a version of UNIX with support for threads at the kernel and user levels, SMP, and real-time scheduling.)Solar is 2支持5.4节讨论的Pthread API,除此之外还利用一个包含了线程的创建和管理的库来支持用户级线程(被称为UI线程)。虽然大多数开发者现在选择Pthread库,但是这两个库之间的不同点是无关紧要的。Solaris 2也定义了一个线程的媒介层。在用户级线程和内核级线承之间是轻量级进程(LWP)。每个进程至少包含一个LWP。线程库在进程的LWP池上多路复用用户级线程,只有当前连接到一个LWP的线程才可以完成工作。(The thread library multiplexes user-level t hreads on the pool of LWPs for the process, and only user-level threads currently connected to an LWP accomplish work.)其它的用户级线程阻塞或等待LWP。

标准的内核级线程在内核内部执行所有的操作。每个LWP有一个内核级线程,并且有些内核级线程以内核的名义运行,没有相关的LWP(例如,服务磁盘请求的线程)。系统中,内核级线程是唯一的调度对象(第六章)。(Kernel-level threads are the only objects scheduled within the system (Chapter 6).)Solaris 2实现了多对多模型;图5.6描述了它的整个线程系统。

用户级线程可能是绑定的或非绑定的。一个绑定的(bound)用户级线程永远附属于一个LWP。只有该线程运行在这个LWP上,而且如果请求的话,这个LWP可被分配给一个单独的处理器(见图5.6最右侧的线程)。(Only that thread runs on the LWP, and by request the LWP can be dedicated to a single processor (see the rightmost thread in Figure 5.6).)绑定线程用于须要快速响应速度的情形,不如实时应用程序。一个非绑定(unbound)的线程并不总是依附于任何LWP。一个应用程序中所有的非绑定线程在该应用程序的LWP池上多路复用。在缺省的情况下线程是非绑定的。Solaris 8也支持一个备用的线程库,该线程库在缺省的情况下将所有的线程绑定到相关的LWP。

Figure 5.6 Solaris 2 threads.

考虑一下系统的操作:任何进程都可能有多个用户级线程。这些用户级线程通过线程库调度和在LWP 间转换,而不需要内核的干涉。用户级线程库的效率非常的高,这是因为线程的创建和销毁不需要内核的支持,或者是因为线程库从一个用户级线程到另一个进行上下文转换。

每个LWP仅仅与一个内核级线程连接,然而用户级线程独立于内核。一个进程中可能有多个LWP,但是只有当线程要与内核通信时才需要LWP。例如,在系统调用时可能会同时被阻塞的多个线程可能需要

争用一个LWP。(For instance, one LWP is needed for every thread that may block concurrently in system calls.)考虑一下同时有五个不同的文件读请求的情况。它们都要在内核中等待I/O完成,这就需要五个LWP。如果一个任务只有四个LWP,那么第五个请求就要等待一个LWP从内核中返回。如果五个LWP就足以,那么添加第六个LWP将一无所获。

内核调度程序调度内核线程,内核线程在系统(单处理机系统或多处理机系统)的CPU上执行。如果一个内核线程阻塞(如等待一个I/O操作完成),那么处理器就被释放来运行另一个内核线程。如果代表LWP的线程阻塞,那么LWP也阻塞。向上,当前附属于该LWP的用户级线程也阻塞。如果一个进程有多个LWP,那么内核可以调度其它的LWP。

线程库动态的调节池中的LWP数量以确保应用程序的性能。例如,如果一个进程中的所有LWP被阻塞而有其它的线程可以运行,那么线程库就自动的创建其它的LWP并将其赋予等待线程。这样,一个程序就不会因为缺少LWP而停止运行。还有,LWP在不被使用时是一种维护代价高昂的内核资源。(Also, LWPs are expensive kernel resources to maintain if they are not being used.)线程库记录LWP的“年龄”并在不再使用的一段时间后删除它们,典型的时间为5分钟。

在Solaris 2中,开发者可以使用如下的数据结构实现线程:

l用户级线程包含一个线程ID;寄存器组(包括一个程序计数器和栈指针);堆栈;优先权(供线程库调度时使用)。这些数据结构都不是内核资源;全部在用户空间中存在。

l一个LWP有一个寄存器组用于它正在运行的用户级线程,也有些内存和记账信息。一个LWP是一个内核数据结构,它驻留在内核空间中。

l一个内核线程只有小的数据结构和一个堆栈。这个数据结构包含一个内核寄存器的拷贝、一个指向它所附属的LWP的指针和优先权及调度信息。

Figure 5.7 Solaris 2 process.

Solaris 2中的每个进程包含了进程控制块(PCB)中所描述的诸多信息,PCB在4.1.3节中讨论。特别的,一个Solaris 2进程包含了一个进程ID(PID)、内存映象、打开的文件的列表、优先权信息和指向与进程关联的内核线程列表的指针(图5.7)。

5.6 Window 2000线程

Windows 2000实现了Win32 API。Win32 API是Microsoft操作系统的基本的(primary)API(Windows 95/98/NT和Windows 2000。本节提到的很多内容的确都适用于这一操作系统家族。

Windows应用程序作为独立的进程运行,进程中可以包含一个或多个线程。(A Windows application runs as a separate process where each process may contain one or more threads.)Windows 2000采用在5.2.2节所描述的一对一映射方式,每个用户级线程映射到一个关联的内核线程。然而Windows提供了对fiber库的支持,它提供了多对多模型的功能(5.2.3节)。属于一个进程的每个线程都可以访问该进程的虚拟地址空间。

一个线程通常包含如下几个组成部分:

l标识线程的一个唯一的线程ID。

l描述处理器状态的寄存器组。

l一个用户堆栈,当线程在用户摸式下运行时使用。相似的,每个线程也有一个内核堆栈,当线程在内核模式下运行时使用。

l一个私有的存储空间,以供各种运行时库和动态链接库(DLL)使用。

寄存器组、堆栈和私有存储空间被称为线程的上下文(context),它们构建在指定的硬件中,而操作系统运行在这些硬件之上。线程的基本数据结构包括:

l ETHREAD(执行线程块)。

l KTHREAD(内核线程块)。

l TEB(线程环境块)。

ETHREAD的关键组成部分包括一个指向线程所属进程的指针和线程开始控制的函数的地址。ETHREAD也包含了指向相应的KTHREAD的指针。

KTHREA D包括线程的调度和同步信息。另外,KTHREAD也包括了内核堆栈(当线程运行在内核摸式时使用)和一个指向TEB的指针。

ETHREAD和KTHREAD完全存在于内核空间之中;这意味着只有内核能够访问它们。TEB是一个用户空间数据结构,当线程在用户摸式下运行时访问它。TEB在其它的段中包含了一个用户模式堆栈和一个-thread-specific data数组(它被Windows称为thread-local storage)。

5.7 Linux线程

Linux内核在2.2版中引入了线程。Linux提供了一个fork系统调用,它像传统的fork功能那样复制一个进程。Linux也提供了clone系统调用,它与创建一个线程类似。clone创建一个与调用clone的进程共享地址空间的独立的进程,而不像fork那样创建一个调用fork的进程的拷贝,除此之外clone的行为与fork 非常相似。通过与父进程共享地址空间,克隆的作业的行为与独立的线程非常相似。

因为在Linux内核中表示进程,所以就允许共享地址空间。(The sharing of the address space is allowed because of the representation of a process in the Linux kernel.)系统中每个进程都有一个唯一的内核数据结构。然而,进程的数据并不存储在这个数据结构里面,而是在数据结构中包含一个指向存储这些数据的地址的指针。例如,每个进程数据结构中包含了指向其它的数据结构的指针,这些数据结构表示打开的文件列表、信号处理信息和虚拟内存。当调用fork时,新进程的创建伴随着父进程的所有相关的数据结构的拷贝。(When fork is invoked, a new process is created along with a copy of all the associated data struc tures of the parent process.)当调用clone系统调用时,就创建一个新进程。然而,并不是拷贝所有的数据结构,而是新进程指向父进程的数据结构,因此就允许子进程共享父进程的内存和其它的进程资源。一组标识(flag)作为参数被传送给clone系统调用。这组标识用于指示子进程共享父进程的资源数量。如果没有设定标识,那么就没有共享,clone的行为就像fork那样。如果设定了所有五个标识,那么子进程就共享父进程的一切。其它标识组合允许在这两个极端之间的各种共享级别。(Linux提供了五个共享标识:CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND (共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效))。

有趣的是,Linux并不区分进程和线程。事实上,当提及程序控制流时,Linux通常使用术语任务(task),而不是进程或线程。除了克隆的进程之外,Linux不支持多线程、独立数据结构(separate data structure)或内核程序(kernel routine)。然而,有多种Pthread实现了用户级多线程。

5.8 Java线程

如前所述,可以通过在用户级提供一个库(如Pthread)来实现对线程的支持。而且,大多数操作系统也在内核级提供对线程的支持。Java是少数在语言级提供了线程的创建和管理的语言之一。然而,因为Java 虚拟机(JVM)管理线程,不是由用户级库或内核管理,所以就难以把Java线程归类为用户级或内核级。

(In this section we present 这一节我们介绍Java线程,它既不是严格的用户级模型也不是严格的内核级模型。

Java threads as an alternative to the strict user- or kernel-level models.)稍后,我们将讨论Java线程是如何被映射到底层内核线程(underlying kernel thread)的。

所有的Java程序都至少包含一个控制执行序列。即使是仅仅由一个主函数(main method)构成的简单的Java程序也要把该主函数作为一个单独的线程在JVM中运行。另外,Java提供了允许开发者创建和维护程序中另外的线程的命令。

5.8.1线程的创建

创建线程的一个方法是创建一个继承自Thread类的新类,并重载Thread类的run方法。图5.8描述了这种方法,一个多线程程序的Java版本计算一个非负整数的和(the summation of a non-negative integer)。

这个继承类的一个对象将作为一个独立的控制执行序列在JVM中运行。然而,创建一个继承自Thread 类的对象并不显明的创建一个新线程;实际上是start方法创建了一个新线程。为新对象调用start方法要做两件事:

1.在JVM中为新线程分配内存并初始化该线程。

2.调用run方法,使该线程能够在JVM中运行。(注意:永远不要直接调用run方法。而是调用start

方法,然后由它调用run方法。)

Figure 5.8 Java program for the summation of a non-negative integer.

当Summation程序运行时,JVM创建两个线程。第一个是与应用程序关联的线程——该线程在主函数中开始执行。第二个线程是由start方法直接创建的Summation线程。Summation线程在它的run方法中开始执行。当该线程从run方法中退出时就终止运行。

5.8.2 JVM和主机操作系统

JVM通常在一个主机操作系统之上实现。这种设置允许JVM隐藏底层操作系统的实现细节,提供一致的抽象环境来允许Java程序在任何支持JVM的平台上运行。JVM规范并不指明怎样把Java线程映射到

底层操作系统中,而把它留给了具体的JVM实现。Windows 95/98/NT和Windows 2000采用了一对一模型;所以,运行在这些系统上的每个Java线程都要映射到一个内核线程。Solaris 2最初使用多对一模型来实现JVM(被称为Green线程)。然而,为Solaris 2.6提供的JVM 1.1采用了多对多模型实现。

5.9 摘要

线程是程序中的一个控制流。一个多线程程序在同样的地址空间内包含了多个不同的控制流。多线程技术的优点包括:提高了对用户的响应速度、进程内的资源共享、经济实惠和能够充分发挥多处理机体系结构的优势。

用户级线程对程序员可视,而内核却不知道它的存在。(User-level threads are threads that are visible to the programmer and are unknown to the kernel.)典型的,在用户空间内的线程库管理用户级线程。操作系统内核支持和管理内核级线程。通常,用户级线程的创建和管理速度比内核线程要快。由三种不同的用户线程和内核线程关联的方法:多对一模型将多个用户模型映射到一个单一的内核模型。一对一模型将每个用户线程映射到一个相应的内核线程。多对多模型(many-to-many model )将用户级线程多路复用到与之数量相等或少一点的内核线程。(The many-to-many model multiplexes many user threads to a smaller or equal number of kernel threads.)

多线程程序的引入使程序员面临一些挑战,包括fork和exec系统调用的语义。其它的问题涉及到线程的取消、信号处理和thread-specific data。一些现代操作系统提供了对线程的内核支持;其中包括了Windows NT和Windows 2000、Solaris 2和Linux。Pthread API在用户级提供了一系列用于创建和管理线程的函数。Java提供了一个类似的API用于支持线程。然而,因为由JVM管理Java线程,而不是由用户级或内核级线程库管理,所以它们并不属于用户级线程或内核级线程。

词汇

轻量级进程:lightweight process, LWP

多处理机体系结构:multiprocessor architecture

用户线程:user thread

内核线程:kernel thread

用户级:user level

内核级:kernel level

异步过程调用:asynchronous procedure call, APC

线程池:thread pool

运行时库:run-time library

动态链接库(DLL):dynamic link library, DLL

主机操作系统:host operating system

栈指针:stack pointer

tulipsys@https://www.360docs.net/doc/8e1257039.html,

戈尔巴乔夫开车

一次,苏联领袖戈尔巴乔夫耽心赶不上会议,告诉他的司机开快车。司机因怕违章拒绝了他。戈尔巴乔夫便命令司机坐在后座位上,亲自开车。

车行不到几英里,就被巡逻队警察拦住,警官派他的警士将违章者拘留起来。

几分钟后,那位警士回来报告说,坐车的人是一位显要人物,不好究办。

“那是谁?”警官询问警士。

“我说不准,警官同志,”警士回答说:“不过戈尔巴乔夫是他的司机。”

相关主题
相关文档
最新文档