操作系统-并发控制原理及其实现
首先我们要明白“皮之不存,毛将焉附”的道理,计算机系统是硬件与系统软件完美结合的一个有机整体。因此在学习这一部分时,特别是学习中断控制原理和系统凋用等内容时,要联系计算机组成原理的知识,这样才能对整个系统了解。
一、程序和进程
进程(process)这一术语 ,最初是在麻省理工学院(MIT)开发的MULTICS;系统以及IBM公司开发的CTSS/360系统中提出,时间是60年代初期。自那时起,进程已成为现代操作系统中最核心的概念之一。因为,操作系统的某本控制原理都是围绕进程展开的。但是,进程控制的复杂性是由操作系统的并发机制引起的。 而目前经常使用的几乎所有操作系统都支持并行或并发机制,我们先介绍并发控制,并以多道程序作为问题的切人点来引出进程的定义。
注:这里读者可能还不太理解进程到底是怎么回事?我们先把它记在心里,回过头再来看遍。
1.1 并发控制
在支持多道程序环境的通用操作系统中 ,允许一个或若十个进程在系统中并发执行, 但由于系统硬件资源的有限性, 使得并发执行的若干进程之间会出现竞争系统有限软硬件资源的现象。 这些资源包括处理器、 内存、I/0设备以及数据库等, 这就需要操作系统来协调和优化分配系统共享资源。 特别是在单处理器系统中,任一时刻 CPU 只能运行一个进程, 而其他进程只能是等待 CPU 或其他资源。为了公平合理地对待所有进程, 内核为每个进程分配一小段时间一小段时间被称为时间片。 一旦正在被运行进程的时间片用完, CPU 就立即被切换去执行另一个等待运行的就绪进程。由于 CPU 的运行速度很快, 造成若干个进程同时在运行的一 种虚拟假像。 这就是多道程序的并发控制。
1.1.1 多道程序设计与分时共享
如上所述 ,操作系统的功能是管理系统资源,以便为用户程序(应用程序)提供一个可执行环境。然而,对于初学者来说。操作系统却是以一种复杂的令人难以琢磨的方式运行。 追根溯底 ,还要从计算机系统的硬件资源讲起。
在操作系统管理的硬件资源中,根据其工作速度可分为两大类。一类是组成计算机主要功能部件的处理器CPU、高速缓存、内存以及I/O接口;而另一类则是外围设备,又叫I/O设备。包括:键盘、显示器、打印机、鼠标、软驱、硬驱、光驱和网络通信设备等。 在这两类硬件资源中, 由于构成的原材料及工作性质的不同,其运行速度有着天壤之别,处理器 CPU 、高速缓存以及内存等是由晶体半导体材料所组成;因此工作时, 其电信号的传播速度几乎为光速。而外围设备大都含有机械装置,显然依赖按键、齿轮以及杠杆传递信息,其运行速度无法与接近光速的电子器件相比。
多道程序设计就是将内存分成若干个部分,每—部分存放不同程序的执行代码。当其中—个进程需要等待外设操作完成时,CPU可以保存当前进程的所有信息,选择另—个进程来运行,但是如果多道程序中的一道是大型科学计算,在运行的数小时里不需要外设操作,那么内存中的其他若干道程序将只能等待,而拥有这些程序的用户却希望能及时得到响应。 这种需求导致了分时系统的出现。在分时系统中,系统为内存中的每一个进程分配一个时间片,当正在执行程序的时间片用完后,操作系统将把处理机分配给另一个就绪进程。对于单机系统(一个 CPU), 在某一时刻只能运行一个进程的一条指令,但是由于CPU的工作速度比人的反应快几百万倍甚至几亿倍。因此,虽然CPU在进程之间快速切换,而人的感觉却是机器在同时执行多个程序。 这就是所谓的多道程序的分时共享,这种工作方式叫做并发机制。 当然,并发控制并非不用付出代价。当操作系统从一个进程切换到另一个进程时,也要使用CPU。因此并发机制首先要保证并发设计所带来的效率要抵过由于进程切换所带来的额外开销。
并发是指在同一时间间隔内对资源的共享。即内存中的多个进程分时共享CPU、内存以及 I/O设备。显然,并发机制可以高效地使用CPU,协调高速CPU与慢速外设的矛盾。但是处理并发并不容易。在内存中同时驻留多个进程需要特殊的硬件以及软件的配合对其进行保护,以免各个进程的信息被窃取并遭到攻击。因此,现代操作系统的一个重要内容就是管理计算机的并发操作。
操作系统中的许多迫切需要解决与研究的问题都是由并发机制而引起的。例如:围绕若 “竞争条件”而引入了临界区、原子操作、同步与互斥、锁变量,等等。因此,并发控制向程序设计人员提出了新的重要的学习目标。因为并发程序并不总是按照预期的结果运行。因此调试并发程序是一件棘手的事,而且并发程序典型的不易解决的毛病是,一个并发程序编译运行后,大多数情况下运行结果都很好,但是极少数的情况下它会莫名其妙地失败,而且无法找到原因。
1.1.2 并发控制的硬件支持
操作系统要实现并发控制的设计目标,离不开硬件平台的支持。因为内核必须使用中断机制和硬件上下文切换机制,才能实现多道程序的分时共享,否则CPU无法知道某个进程的时间片已经耗尽,需要调用另一个进程运行。另外,还要考虑CPU执行进程切换的时间开销要尽可能的小,如果CPU执行进程切换的时间开销太大,则多道程序设计将失去意义。
现代计算机都有一个叫做定时器和计数器的硬时钟设备,而用于定时中断的时间测量设备,叫做可编程间隔定时器。在IBM代机上一般使用8254CMOS芯片作为硬时钟设备,操作系统可对其进行编程,编程后它可以按照人的意愿以一定的时间间隔向CPU发出定时中断。也就是当间隔定时器到时时,它便产生一个中断请求,请求CPU去执行定时器中断服务例程。该中断服务例程将控制器交给操作系统,由操作系统确认当前进程在CPU上已运行的时间,如果分配给它的时间片已经用完,操作系统将剥夺该进程的执行权,并按照一定的算法把它放置到就绪进程队列中,等待下一个运行的机会。
为了实现分时共享,内核必须将时间片已经耗尽的当前进程挂起,然后从就绪队列中选择,一个具有较高优先权的进程投入运行这个过程称为进程切换,或叫任务切换、上下文切换。
在多道程序环境中虽然每个进程可以拥有属于自己的地址空间,但所有进程必须共享使用,CPU的各种寄存器。因此在恢复一个进程执行之前.内核必须将该进程在挂起时的寄存器数据再装人CPU的各个寄存器中才能继续运行。我们把进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。这样可以节省进程切换为系统带来的额外时间开销,当然在具体实现进程切换时如果系统指标允许,也可以使用软件的方法进行上下文切换。Linux2.2以上版本就是如此。
1.2 进程的定义和特征
在并发系统中,进程(process)是在由内核定义的数据结构上进行操作的一个计算活动。它是系统进行资源分配和调度的一个独立单位。进程是动态的、有生命周期的。内核可以创建一个进程,并由调度程序“调度”而运行。在请求I/O操作时被阻塞,当它完成自己的历史使命后,将由内核终止该进程使其消亡。
1.2.1 程序与进程
程序这个术语是非常形象,非常具体的;它就是人们常说的“源程序”和“源代码”。例如C语言经典程序:
1 #include <stdio.h> 2 3 int main(void) 4 { 5 printf("Hello World!\n"); 6 return 0; 7 }
用汇编语言编制的程序就是完成某一特定任务的一组指令的集合。 而C语言程序是面向过程的语言 , 组成该程序的语句比汇编指令容易理解 , 因为它和人类所习惯的表达方式比较接近。 只是运算符号以及语法有所不同。但无论用什么语言编制的程序, 都是为让计算机完成某一特定功能而编写的文本文件。 这些文本文件是不能直接在机器上运行的, 它们必须经过系统软件(包括编辑器和编译器)的输入并编译或汇编后,转换成二进制的可执行代码, 才是计算机可以识别的机器语言。 此时。程序就是一个包含二进制可执行代码文件的模块。 当内核把二进制的可执行代码装入内存后, 它由三部分组成:代码段、 数据段和堆栈段,线性地址的低地址字段是代码段 , 存放程序经编译后的可执行代码(程序文本)。
在操作系统中,代码段是只读的、不能修改,所以,代码段的长度是不会改变的。 在程序文本的上方是数据段,用来存放程序的变量 、字符串和其他数据。它分为初始化静态数据和未初始化静态数据 (BSS)。数据段的长度是可以改变的。程序可以修改其中的变量。许多程序在运行时需要动态分配内存。在UNIX中可以使用系统调用 malloc ()函数来获得。在数据区上方是堆栈段。 一般情况下, 堆栈段起始于虚拟地址空间的高端,栈向下方增长。当系统启动程序运行时,堆栈中存放有环境变量和命令行参数。程序的可执行代码调人内存后,这三部分统称为程序映像。 在虚拟地址空间 ,代码段 、数据段和堆栈段的地址空间是连续的。而在物理内存中,代码段、数据段和堆栈段的地址空间不一定是连续的。
在内存中的程序映像运行之前,操作系统还要为其在内核的进程表项中分配一个被称为进程控制块的内核数据结构。同时要在该内核数据结构中加入程序运行时所需要的相应信息,其中最为重要的信息有:
• 指令指针:用以指示该进程正在执行的那条程序代码指令的地址。
• 进程的用户标识符。
• 进程的程序文本和数据区的存储器位置。
• 进程的状态。
由此可以看出,进程映像使程序完全具备了在多道程序环境中运行的基本条件。因此,内核为程序映像分配了进程控制块后 ,程序映像便转化成为进程映像。当调度程序调度该进程映像执行时,就是所谓的进程。进程控制块是形成进程的最为重要的内核数据结构, 它是内核控制进程的数据结构。因为系统内核是根据进程控制块中的信息对正在运行的程序实施控制,以便完成程序所要求的操作可以用类比的方法来说明程序与进程之间的区别:
程序是一个项目在行动之前的计划书(或叫行动方案),而进程就是对该计划书的实施过程。 显然,计划书和计划书的实施过程存在着很大的差别。 因为,虽然计划书是计划实施过程中一个不可缺少的组成部分,但不是全部,在计划书的实施过程中, 除了计划书这个文本文件(程序)外 。还需要人力资源和物力资源以及相应的信息资源。比方说,盖一幢大楼。 光有人楼的设计方案(图纸)是不行的, 还必须要有施工办公室(或称为指挥控制部门,相当于CPU和操作系统)、负责调配人力资源和物力资源以及保证施工方案顺利进行的各种措施(信息资源)。 如果没有人力资源 、物力资源以及运行机制去实施设计方案,那么计划书上的大楼将永远是纸上的大楼。 但是计划书的实施过程也永远离不开计划书。 另外,在计划书实施过程的不同阶段,工程可能处于不同的状态,例如:由于等待建筑材料或者是由于缺乏劳动力而小得不停工,此时工程将处于等待状态;当各项条件具备时再继续施工,此时工程处于运行态,下面给出进程的抽象定义:
定义 2.1 (1) 进程是一个多元组,P = (p, h, f, e, s) ,其中: P 代表进程; p是可执行程序代码、程序数据与堆栈;h和f分别是执行程序代码时所需要的硬件和软件资源;硬件资源包括CPU、特殊功能寄存器、内存和磁盘等;软件资源包括内核数据结构以及相关的所有信息资源; s代表进程运行期间的各种状态;e是程序运行期间需要的控制信息。
(2) 进程能够动态产生、运行,也有生命周期,能够被内核终止。
1.2.2 从并发机制理解进程
以上是从进程的数据结构描述了进程与程序的区别。 程序映像变成进程映像的时刻也就意味着程序将被调度执行,这样一个新的进程使产生了。下面从并发机制来理解进程和程序的区别,在一个并发处理系统中,当代码装入内存之后,内核允许多个或多个进程来执行它。也就是说, 允许多个进程并发执行同一代码段。 这就相当于,在同时启动的全国各地的香格里拉大酒店的建筑下地上,可以使用完全相同的酒店建筑设计图纸 ,这个建筑设计图纸就是可以共享的程序代码。
如果多个进程并发执行同一代码段,那么要允许各个进程按照其各自的情况在任何时间开始执行和结束执行。因此,在同一时刻, 毋个进程处在同一代码段的不同机器指令处执行(相当于几个建筑工地的进度不同)。为了使执行同一代码的多个进程之间的数据和进度相互独立,必须为每个进程设置一个指令指针,指示该进程下一步将执行哪条指令。同时,每个进程要有自己的数据段和堆栈段用来保存程序运行过程中用到的变量值。
下面我们分析一个简短的循环打印的C语言程序,就可以进一步得明白程序与进程的差别:
1 int i; 2 for(i = 5; i > 0; i--) 3 { 4 printf("Value of i is %d:\n",i); 5 }
这是C语言的for循环语句,其中变量 i 的取值根据循环次数的不同而不同。假设有一个进程执行这个代码段的第三次循环(i= 3)时,另有一个进程开始执行这段代码的第一次循环 (i = 1),虽然两个进程执行的是同一段代码,但是它们的变量值却不相同。因此内核要为每个进程分配数据段和堆栈段,保留自己的变量值。否则就会产生矛盾,系统将不能正常工作。
由此可以看到,程序代码一旦被加载到内存,内核可以允许一个或多个进程来执行它,因此进程只代表一个计算活动的执行过程。而不是代码本身,这就是程序与进程之间的区别。
1.2.2 进程的特征
进程的基本特征是动态性和并发性。同时进程还具有顺序性、独立性和异步性等特征。
在进程的定义中,已强调了进程的动态性。即进程是有生命期的,具体表现在:它是由 “创建”而产生,因“调度程序” 调度而运行,由于“I/O操作”而阻塞,最终,因“退出”而 消亡。
进程的并发性前面已经描述,这里不再赘述。
进程的顺序性是指在单个顺序处理机上,机器指令的执行以及硬件操作是严格按照程序指令的顺序进行的。例如,i386处理器工作时,必须等—条指令执行结束后,硬件才检查是否有硬中断诮求;如果没有请求,再接着执行下一条指令;而如果有请求,CPU允许中断,并且该中断请求排队在前面(优先级高),CPU将响应这个中断请求,从而转去执行中断服务例程。 也就是说,进程的顺序性是指,系统中正在运行的进程不管是发生同步事件还是异步事件,只有当其中的一个操作结束后,才开始其后续的操作。
进程的独立性是指每一个进程是系统中一个独立的实体,它有自己的程序计数器和内部状态。因此进程是系统进行资源分配和调度的独立单位。进程的异步性是指系统中的进程是按照各自独立的、不可确定的时间发生的。
在讲解线程前,我们先深入理解下进程的内存分配机制是怎样的。
1.2.3 进程地址空间和虚拟地址空间
(1)、早期的内存分配机制
在早期的计算机中,要运行一个程序,会把这些程序全都装入内存,程序都是直接运行在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。那当程序同时运行多个程序时,操作系统是如何为这些程序分配内存 的呢?下面通过实例来说明当时的内存分配方法:
某台计算机总的内存大小是 128M ,现在同时运行两个程序 A 和 B , A 需占用内存 10M , B 需占用内存 110 。计算机在给程序分配内存时会采取这样的方法:先将内存中的前 10M 分配给程序 A ,接着再从内存中剩余的 118M 中划分出 110M 分配给程序 B 。这种分配方法可以保证程序 A 和程序 B 都能运行,但是这种简单的内存分配策略问题很多。
问题 1 :进程地址空间不隔离。由于程序都是直接访问物理内存,所以恶意程序可以随意修改别的进程的内存数据,以达到破坏的目的。有些非恶意的,但是有 bug 的程序也可能不小心修改了其它程序的内存数据,就会导致其它程序的运行出现异常。这种情况对用户来说是无法容忍的,因为用户希望使用计算机的时候,其中一个任务失败了,至少不能影响其它的任务。
问题 2 :内存使用效率低。在 A 和 B 都运行的情况下,如果用户又运行了程序 C ,而程序 C 需要 20M 大小的内存才能运行,而此时系统只剩下 8M 的空间可供使用,所以此时系统必须在已运行的程序中选择一个将该程序的数据暂时拷贝到硬盘上,释放出部分空间来供程序 C 使用,然后再将程序 C 的数据全部装入内存中运行。可以想象得到,在这个过程中,有大量的数据在装入装出,导致效率十分低下。
问题 3 :程序运行的地址不确定。当内存中的剩余空间可以满足程序 C 的要求后,操作系统会在剩余空间中随机分配一段连续的 20M 大小的空间给程序 C 使用,因为是随机分配的,所以程序运行的地址是不确定的。
(2)、分段
为了解决上述问题,人们想到了一种变通的方法,就是增加一个中间层,利用一种间接的地址访问方法访问物理内存。按照这种方法,程序中访问的内存地址不再是实 际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。这样,只要操作系统处理好虚拟地址到物理内存地址的映 射,就可以保证不同的程序最终访问的内存地址位于不同的区域,彼此没有重叠,就可以达到内存地址空间隔离的效果。
当创建一个进程时,操作系统会为该进程分配一个 4GB 大小的虚拟 进程地址空间。之所以是 4GB ,是因为在 32 位的操作系统中,一个指针长度是 4 字节,而 4 字节指针的寻址能力是从 0x00000000~0xFFFFFFFF ,最大值 0xFFFFFFFF 表示的即为 4GB 大小的容量。与虚拟地址空间相对的,还有一个物理地址空间,这个地址空间对应的是真实的物理内存。如果你的计算机上安装了 512M 大小的内存,那么这个物理地址空间表示的范围是 0x00000000~0x1FFFFFFF 。当操作系统做虚拟地址到物理地址映射时,只能映射到这一范围,操作系统也只会映射到这一范围。当进程创建时,每个进程都会有一个自己的 4GB 虚拟地址空间。要注意的是这个 4GB 的地址空间是“虚拟”的,并不是真实存在的,而且每个进程只能访问自己虚拟地址空间中的数据,无法访问别的进程中的数据,通过这种方法实现了进程间的地址隔离。那是不是这 4GB 的虚拟地址空间应用程序可以随意使用呢?很遗憾,在 Windows 系统下,这个虚拟地址空间被分成了 4 部分: NULL 指针区、用户区、 64KB 禁入区、内核区。应用程序能使用的只是用户区而已,大约 2GB 左右 ( 最大可以调整到 3GB) 。内核区为 2GB ,内核区保存的是系统线程调度、内存管理、设备驱动等数据,这部分数据供所有的进程共享,但应用程序是不能直接访问的。
人们之所以要创建一个虚拟地址空间,目的是为了解决进程地址空间隔离的问题。但程序要想执行,必须运行在真实的内存上,所以,必须在虚拟地址与物理地址间建 立一种映射关系。这样,通过映射机制,当程序访问虚拟地址空间上的某个地址值时,就相当于访问了物理地址空间中的另一个值。人们想到了一种分段 (Sagmentation) 的方法,它的思想是在虚拟地址空间和物理地址空间之间做一一映射。比如说虚拟地址空间中某个 10M 大小的空间映射到物理地址空间中某个 10M 大小的空间。这种思想理解起来并不难,操作系统保证不同进程的地址空间被映射到物理地址空间中不同的区域上,这样每个进程最终访问到的物理地址空间都是彼此分开的。通过这种方式,就实现了进程间的地址隔离。
还是以实例说明,假设有两个进程 A 和 B ,进程 A 所需内存大小为 10M ,其虚拟地址空间分布在 0x00000000 到 0x00A00000 ,进程 B 所需内存为 100M ,其虚拟地址空间分布为 0x00000000 到 0x06400000 。那么按照分段的映射方法,进程 A 在物理内存上映射区域为 0x00100000 到 0x00B00000 ,进程 B 在物理内存上映射区域为 0x00C00000 到 0x07000000 。于是进程 A 和进程 B 分别被映射到了不同的内存区间,彼此互不重叠,实现了地址隔离。从应用程序的角度看来,进程 A 的地址空间就是分布在 0x00000000 到 0x00A00000 ,在做开发时,开发人员只需访问这段区间上的地址即可。应用程序并不关心进程 A 究竟被映射到物理内存的那块区域上了,所以程序的运行地址也就是相当于说是确定的了。 图二显示的是分段方式的内存映射方法。
这种分段的映射方法虽然解决了上述中的问题一和问题三,但并没能解决问题二,即内存的使用效率问题。在分段的映射方法中,每次换入换出内存的都是整个程序, 这样会造成大量的磁盘访问操作,导致效率低下。所以这种映射方法还是稍显粗糙,粒度比较大。实际上,程序的运行有局部性特点,在某个时间段内,程序只是访 问程序的一小部分数据,也就是说,程序的大部分数据在一个时间段内都不会被用到。基于这种情况,人们想到了粒度更小的内存分割和映射方法,这种方法就是分页 (Paging) 。
(3)、分页
分页的基本方法是,将地址空间分成许多的页。每页的大小由 CPU 决定,然后由操作系统选择页的大小。目前 Inter 系列的 CPU 支持 4KB 或 4MB 的页大小,而 PC 上目前都选择使用 4KB 。按这种选择, 4GB 虚拟地址空间共可以分成 1048576 个页, 512M 的物理内存可以分为 131072 个页。显然虚拟空间的页数要比物理空间的页数多得多。
在分段的方法中,每次程序运行时总是把程序全部装入内存,而分页的方法则有所不同。分页的思想是程序运行时用到哪页就为哪页分配内存,没用到的页暂时保留在 硬盘上。当用到这些页时再在物理地址空间中为这些页分配内存,然后建立虚拟地址空间中的页和刚分配的物理内存页间的映射。
下面通过介绍一个可执行文件的装载过程来说明分页机制的实现方法。一个可执行文件 (PE 文件 ) 其实就是一些编译链接好的数据和指令的集合,它也会被分成很多页,在 PE 文件执行的过程中,它往内存中装载的单位就是页。当一个 PE 文件被执行时,操作系统会先为该程序创建一个 4GB 的进程虚拟地址空间。前面介绍过,虚拟地址空间只是一个中间层而已,它的功能是利用一种映射机制将虚拟地址空间映射到物理地址空间,所以,创建 4GB 虚拟地址空间其实并不是要真的创建空间,只是要创建那种映射机制所需要的数据结构而已,这种数据结构就是页目和页表。
当创建完虚拟地址空间所需要的数据结构后,进程开始读取 PE 文件的第一页。在 PE 文件的第一页包含了 PE 文件头和段表等信息,进程根据文件头和段表等信息,将 PE 文件中所有的段一一映射到虚拟地址空间中相应的页 (PE 文件中的段的长度都是页长的整数倍 ) 。这时 PE 文件的真正指令和数据还没有被装入内存中,操作系统只是根据 PE 文件的头部等信息建立了 PE 文件和进程虚拟地址空间中页的映射关系而已。当 CPU 要访问程序中用到的某个虚拟地址时,当 CPU 发现该地址并没有相相关联的物理地址时, CPU 认为该虚拟地址所在的页面是个空页面, CPU 会认为这是个页错误 (Page Fault) , CPU 也就知道了操作系统还未给该 PE 页面分配内存, CPU 会将控制权交还给操作系统。操作系统于是为该 PE 页面在物理空间中分配一个页面,然后再将这个物理页面与虚拟空间中的虚拟页面映射起来,然后将控制权再还给进程,进程从刚才发生页错误的位置重新开始执行。由于此时已为 PE 文件的那个页面分配了内存,所以就不会发生页错误了。随着程序的执行,页错误会不断地产生,操作系统也会为进程分配相应的物理页面来满足进程执行的需求。
分页方法的核心思想就是当可执行文件执行到第 x 页时,就为第 x 页分配一个内存页 y ,然后再将这个内存页添加到进程虚拟地址空间的映射表中,这个映射表就相当于一个 y=f(x) 函数。应用程序通过这个映射表就可以访问到 x 页关联的 y 页了。
32 位的CPU 的寻址空间是4G,所以虚拟内存的最大值为4G ,而windows 操作系统把这4G 分成2 部分,即2G 的用户空间和2G 的系统空间,系统空间是各个进程所共享的, 他存放的是操作系统及一些内核对象等, 而用户空间是分配给各个进程使用的,用户空间包括用: 程序代码和数据,堆,共享库,栈。
通过上面的讲解,我们了解了并发机制的实现,了解了程序与进程的区别,了解了进程空间的内存分配机制,了解了进程的特征,下面我们看看线程是如何的呢?
二、线程
实际上,一个进程是由一个PCB数据结构和一个可执行代码的指令序列所组成。如果单纯从CPU执行程序所必须的硬件上下文去考虑,PCB中的绝大部分成员与CPU执行程序代码是没有关系的。因为CPU执行程序所必须的、并且对程序员可见的硬件资源只包含:程序计数器、执行堆栈、通用寄存器组和状态标志。CPU执行程序是根据程序计数器中的地址来取下一条指令代码,因此在程序代码运行期间,CPU的执行流程由“经过“程序计数器的指令地址序列来描述,这个地址序列就是指令的执行轨迹,叫做执行线程,简称线程。它是进程的控制流程。
虽然线程的概念出现得比较晚,但是客观上它早已存在,只是后来随着并行编程的研发和进步才逐渐明确起来,传统的用户进程中只有一个执行流程,所以传统进程都是单线程的。
有了线程的概念以后,进程模型得到了有效的扩展,因为在一个进程中完全可以设置多个执行流程,对应多个执行线程。即在—个进程中可以同时运行多个不同的线程来执行不同的任务, 例如: 浏览器就是一个很好的多线程的例子, 使用浏览器你即可以在下载图片的同时滚动页面;在访问新页面时、播放动画和声音、 打印文件等。
2.1 多线程
在一个进程中允许有多个线程时 , 它们都共享该进程的状态和资源, 也就是说它们驻留在同一个用户地址空间中 、可以访问相同的数据。 当一个线程改变了所属进程的变量时,其他线程在下次访问该变量时就会看到这种改变。
由千每个线程具有自己的执行堆栈 、 程序计数器 、 寄存器集和状态标志, 所以同样要为每 个线程定义一个抽象数据类型来包含这些数据成员, 以实现线程之间的切换。 由于同一进程的多个线程共享同一地址空间, 因此引入多线程后为系统管理带来了许多好处:
• 线程之间的切换减轻了内存管理的负担, 另外线程之间的切换所花时间也比进程间切换少。
• 系统创建或终止一个线程的开销要比创建或终止—个进程的开销少得多。
• 线程之间通信的效率要高于进程之间的通信效率。 进程间通信需要内核介入, 而同一进程中的多线程由于共享同一地址空间, 所以通信时无须内核介入。
但是, 引入线程后也需要解决一些问题:
• 需要增加CPU开销, 以便跟踪线程。
• 线程之间也存在争用共享资源的问题。
所以,当编制程序时,如果要求完成一组相关任务的应用程序或函数,应该首选使用多线程的组织方式。
由于传统的原因,线程可分为内核级线程和用户级线程,也简称为内核线程和用户线程。
2.2 内核线程和用户线程
内核中的线程是一个调度的实体。 它的创建与撤销和用户进程没有联系, 而是根据核心的内部管理需求来确定的,例如为了执行一个指定的函数,它们没有虚拟地址空间,可共享内核正文段和全局数据,并有自己的内核堆栈,它可以被单独地调度执行,也能使用内核同步机制。
Linux操作系统支持内核线程, 它的页交换进程kswapd就是一个内核线程。
用户线程运行在操作系统核心之上, 但进程中的线程对内核是透明的, 或者说内核无须知道它们的存在。 这些线程将竞争分配给进程的资源。 当内核不支持用户级线程时, 可以通过使用 POSIX. 1C 提供的标准线程库来实现用户级线程, 其线程包括线程的创建、 删除、 互斥和条件变量的同步橾作以及调度和管理线程的标准函数,而无须内核的帮助。
在Linux中,当使用系统调用Clonde()时,它创建的新进程与被调用者共享同一个用 户地址空间,从原理讲,这个新创建的进程是调用者进程中的一个线程。但是Linux内核没有专门定义供线程使用的数据结构,所以它的线程和进程在结构上没有任何区别。
参考文献:
操作系统原理技术与编程-蒋静(04)