转载请注明: TheViper http://www.cnblogs.com/TheViper
- 进程
在任何多道程序设计系统中,cpu由一个进程快速切换至另一个进程,使的每个进程运行各运行几十或几百毫秒。严格的说,在某一瞬间,cpu只能运行一个进程,但在一秒期间,它可能运行了多个进程,这样就产生了并行的错觉。这就是所谓的“伪并行”,以此来区分多处理器系统(有两个或更多cpu共享同一物理内存)的真正硬件并行。
进程模型
在进程模型中,所有可运行的软件,通常也包括操作系统,被主组织成若干顺序进程。一个进程就是一个正在执行程序的实例。
从概念上说,每个进程都拥有自己的虚拟cpu.当然,实际上真正的cpu在各进程间来回切换。由于进程在cpu间来回快速切换,所以,每个进程执行其运算的速度是不确定的。而且当同一进程再次运行时,其运算速度通常也不可再现。所以在对进程编程时对时序做任何确定的假设。
进程和程序的区别
一个进程是某种类型的一个活动,它有程序,输入,输出,以及状态。单个处理器可以被若干进程共享。它使用某种调度算法决定何时停止一个进程的工作,并转而为另一个进程提供服务。
值得注意的是,如果一个程序运行了两遍,则算做两个进程。
创建进程
有4中主要事件会导致进程的创建
1.系统初始化
2.执行了正在运行的进程所调用的进程,创建系统调用
3.用户请求创建一个新进程
4.一个批处理作业的初始化
进程的终止
通常由下列条件引起
1.正常退出(自愿)
2.出错退出(自愿)
3.严重错误(非自愿)
4.被其他进程杀死(非自愿)
进程的状态
1.运行态(该时刻进程实际占用cpu)
2.就绪态(可运行,但因为其他进程正在运行而暂时停止)
3.阻塞态(除非某种外部事件发生,否则进程不能运行)
前两种状态在逻辑上是类似的。处于这两种状态的进程都可以运行,只是对于第二种状态暂时没有cpu分配给它。
第三种状态则不一样,处于该状态的进程不能运行,即使cpu空闲也不行。
对于1,4过程,典型的例子是进程在等待可以使用的输入。
从上面的图可以看到,进程的三种状态之间有4种可能的转换关系。具体的,
在操作系统发现进程不能继续运行下去时,发生转换1.
转换2,3是由进程调度程序引起的,进程调度程序是操作系统的一部分,进程甚至感觉不到调度程序的存在,系统会认为一个运行进程占用处理器的时间已经过长,决定让其他进程使用cpu时,会发生转换2.在系统已经让其他所有进程享有了它们应有的公平待遇而重新轮到第一个进程再次占用cpu运行时,会发生转换3.调度程序的主要工作就是决定应当运行哪个进程,何时运行以及它应该运行多长时间。
当进程等待到一个外部事件发生时(如一些输入到达),则发生转换4.如果此时恰巧没有其他进程运行,则立即触发转换3,该进程便开始运行,否则该进程就处于就绪状态,等待cpu空闲并且轮到它运行时才运行。
进程的实现
为了实现进程模型,操作系统维护着一张表格,即进程表(进程控制块)。每个进程占用一个进程表项,该表项包含了进程状态的重要信息。
典型的进程表表项中的一些字段
- 线程
在传统操作系统中,每个进程有一个地址空间和一个控制线程。事实上,这几乎就是进程的定义。不过,经常存在在同一个地址空间中准并行运行多个控制线程的情形。这些线程就像分离的进程(共享地址空间除外)。
需要多线程的原因
1.在许多应用(可以看成任务管理器中的进程)中同时发生着多种活动,其中某些活动随着时间的推移会被阻塞。通过将这些应用程序分解成可以准并行运行的多个顺序线程,程序设计模型会变得更简单。正是有了这样的抽象,我们才不必考虑中断,定时器和上下午切换,而只需考虑并行进程。
2.由于线程比进程更轻量,所以它们比进程更容易创建,也更容易撤销。
3.若多个线程都是cpu密集型(即一个进程只有一个线程),那么并不能获得性能上的增强,但是如果存在着大量的计算和io处理,拥有多个线程允许这些活动彼此重叠进行,从而加快应用程序的执行速度。
进一步的理解,进程用于将资源集中到一起,而线程则是在cpu上被调度执行的实体。一个进程里的多个线程全部在相同的地址空间中运行。
线程给进程模型增加了一项内容,那就是在同一个进程环境中,允许彼此之间有较大独立性的多个线程执行。这是对在同一台计算机上并行运行多个进程的模拟。
和传统进程一样(即只有一个线程的进程),线程可以处于若干状态的任何一个:运行,阻塞,就绪或终止。而且线程间的状态转换和进程是一样的。
- 进程间通信
竞争条件:两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的进程时序。
要避免竞争条件这种错误,关键是要找出某种途径来阻止多个进程同时读写共享的数据。换言之,需要的就是互斥。即以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样操作。
临界区:对共享内存进行访问的程序片段。如果能够适当的安排,使得两个进程不可能同时处于临界区中,就能够避免竞争状态。
实现互斥的方案
标准:
1.任何两个进程不能同时处于其临界区
2.不应对cpu的速度,数量做任何假设
3.临界区外的进程不得阻塞其他进程
4.不得是进程无限期等待进入临界区
忙等待的互斥方案
1.屏蔽中断
在单处理器系统中,最简单的方法是使每个进程在进入临界区后立即屏蔽所有中断,并在就要离开之前再打开中断。屏蔽中断后,时钟中断也被屏蔽。而cpu只有发生时钟中断或其他中断时,才会进行进程切换。只有cpu就不会被切换到其他进程。这样就不必担心其他进程的介入。
2.锁变量
设想有一个锁变量,初始值为0.当一个进程进入临界区,先看锁变量是不是为0,后面很简单就不说了。
3.严格轮换法
turn初始值为0,用于记录轮到哪个进程进入临界区,并检查或更新共享内存。具体的,
开始时,进程a检查turn,发现其值为0,于是进入临界区。这时,进程b也发现其值为0,所以,在一个等待循环中不停的测试turn,看其值何时变为1.当进程a离开临界区时,它将turn的值设为1,以便允许进程b进入其临界区。
这种方式叫忙等待,由于这种方式浪费cpu时间,所以通常应该避免。
只有在有理由认为等待时间是非常短的情况下,才使用忙等待。用于忙等待的锁称为自旋锁。
这种方法存在的问题是,如果一个进程比另一个进程慢很多的话,执行完非临界区操作的快进程会被迫等待慢进程。
4.Peterson解法
代码比较简单,下面考虑有两个进程几乎同时调用enter_region的情况。
它们都将自己的进程号存入turn,但只有后被保存的进程号才有效。假设进程1是后存入,turn为1,当两个进程都运行到while时,进程0将循环0次并进入临界区,而进程1则不停的循环,且不能进入临界区,直到进程0退出临界区为止。
5.TSL指令
睡眠与唤醒
忙等待这种方法不仅浪费cpu时间,还可能引起意想不到的效果。比如,
一个计算机有两个进程,H进程优 先级较高,L进程优先级较低。调度规则规定,只要H处于就绪态它就可以运行。在某一时刻,L处于临界区中,此时H在就绪态,准备运行。现在H开始忙等待, 但由于当H就绪时L不会被调度,也就无法离开临时区。所以H将永远忙等待下去。这种情况被称作优先级反转问题。
下面用进程间通信原语。它们在无法进入临界区时将阻塞,而不是忙等待。
sleep是一个将引起调用进程阻塞的系统调用,即被挂起,直到另外一个进程将其唤醒。
weakup的调用有一个参数,即被唤醒的进程。
生产者---消费者问题
也称作有界缓冲区问题。两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,将信息放入缓冲区,另一个是消费者,从缓冲区中取出信息。这里只讨论一个消费者,一个生产者的情况。
不过,这样仍然可能产生竞争条件,因为对count的访问未加限制。例如,
缓 冲区为空,生产者向缓冲区加入一个数据项,count加1,变为1.它推断认为由于count刚才为0,所以消费者此时一定在睡眠,于是生产者调用 wakeup唤醒消费者。但是,消费者此时在逻辑上并没有睡眠,这个是要点,所以weakup信号丢失。当消费者下次运行时,它将测试先前读到的 count值,发现值为0,于是睡眠。生产者迟早会填满缓冲区,也会睡眠。这样,两个进程都将永久睡眠。
信号量
使用一个整型变量来累计唤醒次数,供以后使用,这个变量称为信号量。
设立两种操作,down和up(分别为一般化后的sleep和weakup).
执行down操作,检查其值是否大于0,。若大于0,其值减1(即用掉一个保存的唤醒信号)并继续;若等于0,进程将休眠。此时down操作并未结束。
up 操作,对信号量的值加1,如果一个或多个进程在该信号量上睡眠,无法完成先前的down操作,则由系统选择其中的一个,并允许该进程完成它的down操 作。于是,对一个有进程在其上睡眠的信号量执行一次up操作后,该信号量的值仍然为0.但在其上睡眠的进程却少了一个。
下面用信号量解决生产者-消费者问题。
使用3个信号量。
1.full,用来记录充满的缓冲槽数目
2.empty,记录空的缓冲槽数目
3.mutex,用来确保生产者和消费者不会同时访问缓冲区。初值为1,
信号量另一个用途是实现同步,信号量full,empty保证某种事件的顺序发生或不发生。在上面例子中,它们保证当缓冲区满的时候生产者停止运行,当缓冲区空时消费者停止运行。
互斥量
如果不需要信号量的计数功能,可以使用信号量的一个简化版本,称为互斥量。
互斥量是一个可以处于两态之一的变量,解锁和加锁。这样,只需要一个二进制表示它就可以了。
由于互斥量非常简单,所以如果有可用的TSL或XCHG指令,就可以很容易的在用户空间中实现他们。
在用户级进程包中,多个线程访问同一个互斥量是没有问题的。因为所有的线程都在一个公共地址空间中操作。但这些多个进程同样也会访问一些共享内存。这时,有两种方法。
1.有些共享数据结构,如信号量,可以存放在内核中,并且只能通过系统调用来访问。
2.现代操作系统提供一种方法,让进程与其他进程共享其部分地址空间。