操作系统-进程与线程
在进程模型中,计算机上所有可运行的软件,通常也包括操作系统,被组织成若干个顺序进程,简称进程。一个进程就是一个正在执行程序的实例,包括程序计算器,寄存器和变量的当前值。从概念上说,每个进程拥有他自己的虚拟CPU。
守护进程
停留在后台处理诸如电子邮件,web页面,新闻等之类的活动的进程称为守护进程。
进程的创建
1)系统初始化
2正在运行的程序执行了创建进程的系统调用
3)用户请求创建一个新进程
4)一个批处理作业的初始化
进程的终止
1)正常退出(自愿)
2)出错退出(自愿)
3)严重错误(非自愿)
4)被其他进程杀死(非自愿)
进程的层次结构
某些系统中,当进程创建了另一个进程后,父进程和子进程就以某种形式继续保持关联。子进程自身可以创建更多的进程,组成一个进程的层次结构。
进程的状态
尽管每个进程是一个独立的实体,有其自己的程序计数器和内部状态,但是,进程之间经常需要相互作用,一个进程的输出结果可能作为另一个进程的输入。
当一个进程在逻辑上不能继续运行时,它就会被阻塞,典型的例子是它在等待可以使用的输入,还有这样的情况,一个概念上能够运行的进程被迫停止,因为操作系统调度另一个进程占用了CPU,第一种情况下是进程挂起是程序自身固有的原因,第二种情况则是由系统技术上的原因引起的,(在键入用户命令行之前,无法执行命令)。这种情况则是由技术上。
进程的状态:
1.运行态(该时刻)
2.就绪态
3.阻塞态
进程的实现
为了实现进程模型,操作系统维护着一张表格(一个结构数组),即进程表。每个进程占用一个进程表项(有些作者称这些表项为进程控制块),该表项包含了进程状态的重要信息,包括程序计数器,堆栈指针,内存分配状况,所打开文件状态、账号和调度信息,以及其他在进程由运行状态转换到就绪态或阻塞态必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过。
多道程序设计模式
好的模型是从概率的角度来看CPU的利用率,假设一个进程等待i/o操作的事件与其停留在内存的时间比为p。当内存中同时有n个进程是,则所有n个进程都在等待i/o的概率是p的n次方。CPU的利用率 = 1-p的n次方。
从完全精确的角度考虑,应该指出此概率模型只是描述了一个大致情况,它假设所有n个进程是独立的,即内存中的5个进程中,3个运行,2个等待,是完全可以接受的,但在单个CPU中,不能同时运行3个进程,所以当cpu忙时,以及就绪的进程也必须等待cpu,因而,进程不是独立的,更精确的模型应该用排队论构建。但我们的模型(当进程就绪时,给进程分配cpu,否则让cpu空转)仍然是有效的。
线程
在传统操作系统中,每个进程有一个地址空间和一个控制线程,事实上,这几乎就是进程的定义。不过,经常存在在同一个地址空间中准并行运行多个控制线程的情形,这些线程就像(差不多)分离的进程(共享地址空间除外)。
线程的使用
为什么人们需要在一个进程中再有一类线程?人们需要多线程的主要原因是,在许多应用中同时发生着多种活动,其中某些活动随着时间的推移会被阻塞。通过将这些应用程序分解成可以准并行运行的多个顺序线程,程序设计模型会变得更简单。
有了这样的抽象,我们才不考虑中断,定时器,上下文切换,而只需考察并行进程,类似地,只有在有了多线程概念后,我们才加入了一种新元素;并行实体拥有共享同一个地址空间和所有课用数据的能力,对于某些应用而言,这种能力是必须的,而这盛世多进程模型多无法表达的。
第二个关于需要多线程的理由是,由于线程比进程更轻量级,所以他们比进程更容易(更快)创建,也更容易撤销,在许多系统中,创建一个线程较创建一个进程要快10-100倍。在有大量线程需要动态和快速修改时,具有这一特性是很有用的。
需要多线程的第三个原因设计性能方面的讨论,若多个线程是CPU密集型的,那么并不能获得性能上的增强,但是如果存在着大量的计算和大量的i/o处理,拥有多个线程允许这些活动彼此重叠进行。
从而会加快应用程序执行的速度。
经典的线程模型
理解进程的一个角度是,用某种方法把相关的资源集中在一起,进程与存放程序正文和数据以及其他资源的地址空间,这些资源中包括打开的文件,子进程,即将发生的定时器,信号处理程序,账号信息。把它们都放到进程中可以更容易理解。
另一个概念是,进程拥有一个可执行线程,通常简写为线程,在线程中有一个程序计数器,用来记录接着要执行哪一条指令,线程拥有寄存器,用来保存线程当前的工作变量,线程还拥有一个堆栈,用来记录执行历史。其中每一帧保存了一个已经调用的但是还没有返回的过程,尽管线程必须在某个进程中执行,但是线程和它的进程是不同的概念,并且可以分别处理,进程用于把资源集中到一起,而线程则是在CPU上被调度执行的实体。
线程给进程模型增加了一项内容,即在同一个进程环境中,允许彼此之间有较大的独立性的多个线程执行。在同一个进程中并行运行多个线程,是对在同一台计算机上并行运行多个进程的模拟,在前一种情形下,多个线程共享同一个地址空间和其他资源。而在后一种情形中,多个进程共享物理内存,磁盘,打印机和其他资源,由于线程具有进程的某些性质,所以有时被称为轻量级进程。多线程这个术语,也用来描述在同一个进程中允许多个线程的情形,正如在第一章中看到的,一些CPU已经有直接硬件支持多线程,并允许线程切换在纳秒级完成。
进程中的不同线程不像不同进程之间那样存在很大的独立性,所有的线程都有完全一样的地址空间,这意味着他们也共享同样的全局变量。由于各个线程都可以访问进程地址空间中的每一个内存地址,这意味着他们也共享同样的全局变量,由于各个线程都可以访问进程地址空间中的每一个内存地址,所以一个线程可以读,写或设置清除另一个线程的堆栈。线程之间是没有保护的,原因是:1)不可能,2)也没有必要。这与不用进程是有差别的,不同的进程会来自不同的用户,它们彼此之间可能有敌意,一个进程总是由某个用户所拥有,该用户创建多个线程应该是为了它们之间的合作,而不是彼此间争斗,除了共享地址空间之外,所有线程还共享同一个打开文件集,子进程,定时器,以及相关信号等。
和传统进程一样,线程可以处于若干种状态的任何一个,运行,阻塞,就绪或终止。正在运行的线程拥有CPU并且是活跃的,被阻塞的线程正在等待某个释放他的事件,例如,当一个线程执行从键盘读入数据的系统调用时,该线程就被阻塞直到键入了输入为止。线程可以被阻塞,以等待某个外部事件的发生或者等待其他线程来释放他,就绪线程可以被调度运行,并且只有论道它就很快可以运行,线程状态之间的转换和进程状态之间的转换是一样的。
每个线程都有其自己的堆栈。每个线程的堆栈有一帧,供各个被调用但是还没有从中返回的过程使用,在该栈帧中存放了相应过程的局部变量以及过程调用完成之后使用的返回地址,例如,如果过程x调用过程y,y又调用了z,那么当z执行时,供x,y和z使用的栈帧会全部存在堆栈中,通常,每个线程会调用不同的过程,从而有一个各自不同的执行历史,这就是为什么每个线程都需要有自己堆栈的原因。
在多线程的情况下,进程通常会从当前的单个线程开始,这个线程有能力通过调用一个库函数(thread_creat)创建新的线程,thread_creat的参数专门指定了新线程要运行的过程名。这里,没有必要对新线程的地址空间加以规定,因为新线程会自动在创建线程的地址空间中运行。有时,线程是有层次的,它们具有一种父子关系,但是,通常不存在这样一种关系,所有线程都是平等的,不论有无层次关系,创建线程通常都返回一个线程标识符,该标识符就是新线程的名字。
当一个线程完成工作之后,可以通过调用一个库的过程(如thread_exit)退出,该线程接着小时,不再可调度,在某些线程中,通过调用一个过程,例如thread_join,一个线程可以等待一个(特定)线程退出。这个过程阻塞调用线程,直到那个(特性)线程退出。在这种情况下,线程的创建和终止非常类似于进程的创建和终止,并且也有着相同的选项。
另一个常见的线程调用时thread_yeild,它允许线程自动放弃CPU从而让另一个线程允许,这样一个调用是很重要的,因为不同于进程,(线程库)无法利用始终中断强制让线程让出CPU,所以设法使线程行为“高尚起来”,并且随着时间的推移自动交出cpu,以便让其他线程有机会允许,就变得非常重要。有的调用允许某个线程等待另一个线程完成某些任务,或等待一个线程宣传它已经完成了有关的工作。
2.2.4在用户空间中实现线程
有两种主要的方法实现线程包,在用户空间中和在内核中,这两种方法忽互有利弊,不过混合实现方式也是可能的,我们现在介绍这些方法,并去分析它们的优点和缺点。
第一种方法是把整个线程包放在用户空间中,内核对线程包一无所知,从内核角度考虑,就是按正常的方式管理,即单线程进程,这种方法第一个也是最明显的优点是,用户级线程包可以不在支持线程的操作系统上实现,过去所有的操作系统都属于这个范围,即使现在也有一些操作系统还是不支持线程。
问题:程序猿通常在经常发生线程阻塞的应用中才希望使用多线程。例如,在多线程Web服务器里,这些线程持续的进行系统调用,而一旦发生内核陷阱进行系统调用,如果原有的线程已经阻塞,就很难让内核进行线程的切换,如果要让内核消除这种情况,就要持续进行select系统调用,以便检查read系统调用是否安全,对于那些基本上是CPU密集型而且极少有阻塞的应用程序而言,使用多线程的目的又何在呢?由于这样的做法并不能得到任何益处,所以没有人真正会提出使用多线程来计算前n个素数这样的工作。
2.2.5在内核中实现线程
现在考虑内核支持和管理线程的情形,此时不在需要运行时系统了,另外,每个进程中也没有线程表,相反,在内核中有用来记录系统中所有的线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新完成线程创建或者撤销工作。
内核的线程表保存了每个线程的寄存器,状态和其他信息,这些信息和在用户空间中,(在运行时系统中)的线程是一样的,但是现在保存在内核中,这些信息时传统内核所维护的每个单线程进程信息(即进程状态)的子集。另外,内核还维护了传统的进程表,以便跟踪进程的状态。
所有能够阻塞线程的调用都以系统调用的形式实现,这与运行时系统过程相比,代价是相当可观的,当一个线程阻塞时,内核根据其选择,可以运行同一个进程中的另一个线程(若有一个就绪线程)或者另一个进程中的线程,而在用户级线程中,运行时系统始终运行自己进程中的线程,直到内核剥夺他的CPU(或者没有可运行的线程存在了)为止
由于在内核中创建或撤销线程的代价比较大,某些系统采取“环保”的处理方式,回收其线程,当某个线程被撤销时,就把它标志为不可运行的,但是其内核数据结构没有收到影响,稍后,在必须创建一个新线程时,就重新启动某个就线程,从而节省了一些开心,在用户级线程中线程回收也是可能的,但是由于其线程管理的代价较小,所以没有必要进行这项工作。
内核线程不需要任何新的,非阻塞系统调用,另外,如果某个进程中的线程引起了页面故障,内核可以很方便的检查该进程是否有任何其他可运行的线程。如果有,在等待所需要的页面从磁盘度入市,就选择一个可以运行的线程运行,这样做的主要缺点是系统调用的代价比较大,所以如果线程的操作(创建,终止等)比较多,就会带来很大的开销
2.3进程间通信
进程经常需要与其他进程通信,例如,在一个shell管道中第一个进程的输出必须传送给第二个进程,这样沿着管道传递下去,因此在进程之间需要通信,而且最好使用一种结构良好的方式而不要使用中断,
2.3.1竞争条件
在一些操作系统中,协作的进程可能共享一些彼此都能读写的公共存储区,这个公用存储区可能在内存中,也可能是一个共享文件,这里共享存储区的位置并不影响通信的本质及其带来的问题,为了理解实际中进程间通信如何工作,我们考虑一个简单但很普遍的例子:一个假脱机打印程序,当一个进程需要打印一个文件时,它将文件名放在一个特殊的假脱机目录。另一个进程(打印机守护进程)则周期性地检查是否有文件需要打印,若有就打印并将该文件名从目录下删掉。