操作系统中线程的实现模型

1、为什么需要线程?

首先需要回答一个问题,为什么操作系统需要线程。如果非要说是为什么需要线程,还不如说为什么需要进程中还有其它进程。这些进程中包含的其它迷你进程就是线程。进程有以下缺陷:

1、进程只能在一个时间内干一件事(执行一个程序执行流),而如果想同时干两件或多件事情,进程就不够用了。
2、进程在执行过程中如果阻塞,例如等待输入,整个进程就将挂起,而无法继续执行。这样,即使进程里面有部分工作不依赖于输入数据,也无法推进。
为了解决这个问题,人们发明了线程。线程之所以说是迷你进程,是因为线程和进程有很多相似之处,比如线程和进程的状态都有运行,就绪,阻塞状态。这几种状态理解起来非常简单,当进程所需的资源没有到位时会是阻塞状态,当进程所需的资源到位但CPU没有到位时是就绪状态,当进程既有所需的资源,又有CPU时,就为运行状态。使用多线程,每个线程仅仅需要处理自己那一部分应该完成的任务,而不用去关心和其它线程的冲突。因此简化了编程模型。
更具体的说,线程的好处如下:

1、在很多程序中,需要多个线程互相同步或互斥的并行完成工作,而将这些工作分解到不同的线程中去无疑简化了编程模型。
2、因为线程相比进程来说,更加的轻量,故线程的创建和销毁的代价变得更小。
2、线程提高了性能,虽然线程宏观上是并行的,但微观上却是串行(单CPU或单核时)。从CPU角度线程并无法提升性能,但如果某些线程涉及到等待资源(比如IO,等待输入)时,多线程允许进程中的其它线程继续执行而不是整个进程被阻塞,因此提高了CPU的利用率,从这个角度会提升性能。
3、在多CPU或多核的情况下,使用线程不仅仅在宏观上并行,在微观上也是并行的。
进程有自己独立的内存地址空间,而线程共享进程的内存地址空间。

2、经典的线程模型

另一个看进程和线程的角度是进程模型基于两类不同的概念:资源的组织和执行。在过去没有线程的操作系统中,资源的组织和执行都是由进程完成的。但这两者很多时候需要加以区分,这也是为什么需要引入线程。进程是用于组织资源的单位,进程将相关的资源组织在一起,这些资源包括:内存地址空间,程序,数据等,将这些以进程的形式组织起来可以使得操作系统管理这些资源更为容易。而线程,是每一个进程中执行的一个条线。线程虽然共享进程中的大多数资源,但线程也需要自己的一些资源,比如:用于标识下一条执行指令的程序计数器,一些容纳局部变量的寄存器,以及用于表示执行的历史的栈。

总而言之:进程是组织资源的最小单位,而线程是安排CPU执行的最小单位。其实在一个进程中多个线程并行和在操作系统中多个进程并行非常类似,只是线程共享的是地址空间,而进程共享的是物理内存,打印机,键盘等资源。每一个进程和线程所独自占有的资源如表1所示。

表1

 

进程占有的资源 线程占有的资源
地址空间
全局变量 寄存器
打开的文件 程序计数器
信号量 状态
账户信息  


其中,线程可以共享进程独占的资源。我们常用的术语“多线程”一般指的是在同一个进程中多个线程的并发执行。线程是进程里面的一个执行上下文,或者执行序列。同一个地址空间里面的所有线程就构成了进程。线程是CPU切换的最小单位,进程是资源分配的最小单位

 

例如:当我们使用Word时,实际上是打开了多个线程。这些线程一个负责显示,一个接受输入,一个定时存盘。这些线程一起运转(同时参与竞争CPU),让我们感觉到输入和屏幕显示同时发生,而不用键入一些字符,等待一会儿才看到屏幕显示。

 

3、线程管理与实现方式

要管理线程就要维持线程的各种信息,存放这些信息的数据结构称为线程控制块。线程共享一个进程空间,因此许多资源是共享的(如地址空间、全局变量、文件等),这些共享资源存放在进程控制块即可。还有一些不被共享的资源和信息(如程序计数器、寄存器等),需要存放在线程控制块里。 由谁来管理线程有两种选择:一是让进程自己来管理线程;二是让操作系统来管理线程。由进程自己管理就是用户态线程实现,由操作系统管理就是内核态线程实现。

 

3.1 线程实现在用户空间下

当线程在用户空间下实现时,操作系统对线程的存在一无所知,操作系统只能看到进程,而不能看到线程。所有的线程都是在用户空间下实现。在操作系统看来,每一个进程只有一个线程。过去的操作系统大部分是这种实现方式,这种方式的好处之一就是即使操作系统不支持线程,也可以通过库函数来支持线程。在这种模式下,每一个进程中都维护着一个线程表来追踪本进程中的线程,这个表中包含表1中每个线程独占的资源,比如栈,寄存器,状态等,如图1所示。

操作系统中线程的实现模型_第1张图片
图1 线程实现在用户空间下
 

这种模式当一个线程完成了其工作或等待需要被阻塞时,其调用系统过程阻塞自身,然后将CPU交由其它线程。用户线程多见于一些历史悠久的操作系统,如UNIX操作系统。这种的模式的好处,首先,是在用户空间下进行进程切换的速度要远快于在操作系统内核中实现。其次,在用户空间下实现线程使得程序员可以实现自己的线程调度算法。比如进程可以实现垃圾回收器来回收线程。还有,当线程数量过多时,由于在用户空间维护线程表,不会占用大量的操作系统空间。最后是灵活性,由于操作系统不需要知道线程的存在,所以在任何操作系统上都能应用。

这种模式最致命的缺点也是由于操作系统不知道线程的存在,因此当一个进程中的某一个线程进行系统调用时,比如缺页中断而导致线程阻塞,此时操作系统会阻塞整个进程,即使这个进程中其它线程还在工作。此外会造成编程困难,我们在写程序的时候必须仔细斟酌在什么时候应该让出CPU给别的线程使用。还有一个问题是假如进程中一个线程长时间不释放CPU,因为用户空间并没有时钟中断机制,会导致此进程中的其它线程得不到CPU而持续等待。

 

3.2 线程实现在操作系统内核中

在这种模式下,操作系统知道线程的存在。此时线程表存在操作系统内核中,如图2所示。线程应该是CPU调度的基本单位,操作系统要管理线程,就要保持维护线程的各种资料,即将线程控制块存放在操作系统内核空间。这样操作系统内核就同时保有进程控制块和线程控制块。

操作系统中线程的实现模型_第2张图片
图2 线程实现在操作系统内核空间中


在这种模式下,所有可能阻塞线程的调用都以系统调用(System Call)的方式实现,相比在用户空间下实现线程造成阻塞的运行时调用(System runtime call)成本会高出很多。当一个线程阻塞时,操作系统可以选择将CPU交给同一进程中的其它线程,或是其它进程中的线程,而在用户空间下实现线程时,调度只能在本进程中执行,直到操作系统剥夺了当前进程的CPU。

因为在内核模式下实现线程的成本更高,一个比较好的做法是将线程回收利用,当一个线程需要被销毁时,仅仅是修改标记位,而不是直接销毁其内容,当一个新的线程需要被创建时,也同样修改被“销毁”的线程其标记位即可。名为内核态线程实现的优点是用户编程简单,用户程序员在编程的时候无需关心线程的调度,即无需担心线程什么时候会执行,什么时候会挂起。如果一个线程执行阻塞操作,操作系统可以从容地调度另外一个线程执行。因为操作系统能监控所有线程。

这种模式下同样还是有一些弊端,例如效率较低。因为线程在内核态实现,每次线程切换都需要陷入到内核,由操作系统来进行调度。其次,占用内核稀缺的内存资源。操作系统需要维护线程表,操作系统所占的内存空间一旦装载结束后就已经固定,无法动态改变。由于线程的数量大大高于进程的数量,那么随着线程数量的增加,操作系统内核空间将迅速耗尽。

 

3.3 现代操作系统的线程实现模型—混合模式

鉴于用户态和内核态都存在缺陷,现代操作系统使用的是将二者结合起来。用户态的执行系统负责进程内部线程在非阻塞时的切换;内核态的操作系统负责阻塞线程的切换,即我们同时实现内核态和用户态线程管理。其中内核态线程数量较少,而用户态线程数量多。每个内核态线程可以服务一个或多个用户态线程。换句话说,用户态线程被多路复用到内核态线程上。混合模式下,用户空间中的进程管理自己的线程,操作系统内核中有一部分内核级别的线程,如图3所示。

例如,某个进程有5个线程,我们可以将5个线程分成两组,一组3个线程,另一组2个线程。每一组线程使用一个内核线程。这样,该进程将使用两个内核线程。如果一个线程阻塞,则与其同属于一组的线程皆阻塞,但另外一组线程却可以继续执行。在这种模式下,在分配线程时,我们可将需要执行阻塞操作的线程设为内核态线程,而不会执行阻塞操作的线程设为用户态线程。这样我们就可以获得两种态势实现下的优点,而避免其缺点。

操作系统中线程的实现模型_第3张图片
图3 线程实现的混合模型

在这种模式下,操作系统只能看到内核线程。用户空间线程基于操作系统线程运行。因此,程序员可以决定使用多少用户空间线程以及操作系统线程,这无疑具有更大的灵活性。而用户空间线程的调度和前面所说的在用户空间下执行实现线程是一样的,同样可以自定义实现。

4、从用户态进入内核态

Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核)。内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

内核态:CPU可以访问内存所有数据,包括外围设备,例如硬盘、 网卡,CPU也可以将自己从一个程序切换到另一个程序。

用户态:只能受限的访问内存,且不允许访问外围设备。占用CPU的能力被剥夺,CPU资源可以被其他程序获取。

那么计算机是如何知道现在正在运转的程序是内核态程序呢?这个标志就是处理器的一个状态位。这个状态位是CPU状态字里面的一个字位。这就是说,所谓的用户态、内核态实际上是处理器的一种状态,而不是程序的状态。我们通过设置该状态字,可以将CPU设置为内核态,或者用户态,或者其他的子态。一个程序运行时,CPU是什么态,这个程序就运行在什么态

因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。所以为了减少有限资源的访问和使用冲突,Unix/Linux的设计哲学之一就是:对不同的操作赋予不同的执行等级,就是所谓特权的概念。简单说就是有多大能力做多大的事,与系统相关的一些特别关键的操作必须由最高特权的程序来完成。Intel的X86架构的CPU提供了0到3四个特权级,数字越小,特权越高,Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态和用户态。运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如C函数库中的内存分配函数malloc(),它具体是使用sbrk()系统调用来分配内存,当malloc调用sbrk()的时候就涉及一次从用户态到内核态的切换,类似的函数还有printf(),调用的是wirte()系统调用来输出字符串等等。

例如:一个C++程序调用函数cin,cin是一个标准库函数,它将调用read函数。而read则是由操作系统提供的一个系统调用。其执行过程如下:
1、执行汇编语言里面的系统调用指令(如syscall);
2、将调用的参数存放在指定的寄存器或栈上(事先约定好);
3、当处理器执行到“syscall”指令时,察觉到这是一个系统调用指令,将进行如下操作:
设置处理器为内核状态
②保存当前寄存器(栈指针、程序计数器、通用寄存器)
③将栈指针设置指向内核栈地址
④将程序计数器设置为一个事先约定的地址上。该地址上存放的是系统调用处理程序的起始地址(所有的系统调用都由系统调用处理程序来处理);
4、系统调用处理程序执行系统调用,并调用内核里面的read函数。
这样,就实现了从用户态到内核态的转换,并完成系统调用所要求的功能。

 

你可能感兴趣的:(操作系统及运维,操作系统,线程模型)