操作系统基础(一)(进程和线程)

进程

进程是对正在运行程序的一个抽象,CPU一个时间只能运行一个进程,但在1秒内,它可能运行多个进程,从而产生一种并行的错觉。

进程的模型

说到进程的模型,我们可以通过将进程与程序相比较来得出进程的模型。

进程与程序的区别很微妙,但非常重要,用个比喻:一个人为他女儿做蛋糕,他有食谱,有原料。在这个例子中,这个人代表CPU,做蛋糕的食谱代表程序(即为适当形式描述的算法),其中原料代表输入数据。进程则是这个人阅读食谱,取原料来制作蛋糕等一系列动作的总和。

为了实现进程模型,操作系统维护着一张表格,即进程表,该表包括了进程状态的重要信息:程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息等等,从而保证了进程的正常运行。

进程的创建和终止

4种主要事件会导致进程的创建
1)系统的初始化
2)正在运行的程序执行了创建进程的系统调用
3)用户请求创建一个新进程
4)一批处理作业的初始化

进程的终止主要由4种情况引起
1)正常退出(自愿的)
2)出错退出(自愿的)
3)严重错误(非自愿)
4)被其他进程杀死(非自愿)

进程的状态

一个进程主要有3种状态:
1)运行态(该时刻进程实际占用CPU)
2)阻塞态(除非某种外部事件发生,否则进程不能运行)
3)就绪态(可运行,但因为其他进程正在运行而暂时停止)

这三种转态之间有四种转换关系
1)运行变阻塞:进程因等待输入而被阻塞
2)阻塞变就绪:出现有效输入
3)就绪变运行:调度程序选择这个进程
4)运行变就绪:调度程序选择另一个进程

线程

再用之前进程和程序的那个例子,一个人为他女儿做蛋糕这个例子中,进程代表这个人阅读食谱,取原料来制作蛋糕等一系列动作的总和。而线程则是进程中的每个步骤,即阅读食谱、去原材料等等。

线程使用的意义

1)在许多应用中同时发生着多种活动,将这些程序分解成可以准并行运行的多个顺序线程,程序设计模型会变得更简单。
2)线程比进程更轻量级,所以他们比进程更容易创建,也更容易撤销。
3)线程之间的切换比进程之间的切换要容易的多,所以线程的使用可以提高CPU的效率。

线程模型

线程主要包括:程序计数器、寄存器、堆栈、状态。

程序计数器用来记录接着要执行哪一条指令;寄存器用来保存线程当前的工作变量;堆栈,用来记录执行历史,其中每一帧保存了一个已调用的但是还没有从中返回的过程。

进程中的线程,他们共享同一个地址空间、打开文件集、子进程、定时器以及相关信号等,他们密切合作,共同完成一个任务。

线程的实现

在用户空间中实现

将整个线程包放在用户空间中,即使用函数库来实现线程,内核对线程包一无所知。从内核的角度考虑,就是按正常的方式管理,即单线程进程。
在用户空间管理线程时,每个进程需要有其专用的线程表,用来跟踪进程中的线程。

在内核中实现线程

即在内核中增加线程的功能,即在内核中有记录系统中所有线程的线程表。
内核的线程表保存了每个线程的寄存器、状态和其他信息,这些信息是传统内核所维护的每个单线程进程信息的子集。

混合实现

用户实现的优缺点:
优:
1)用户线程包可以在不支持现线程的操作系统上实现。
2)用户线程允许每个进程都有自己制定的调度算法。
缺:
1)无法实现阻塞系统的调用,假设一个在还没有任何击键之前,一个线程读取键盘,由于内核不考虑线程,如果让该线程进入阻塞状态,即让整个进程处于阻塞,这样会停止进程中所有的线程。
2)缺页中断的影响,当一个线程引起缺页中断时,由于内核不知道线程的存在,则会把整个进程阻塞直到磁盘I/O完成为止,尽管其他线程是可以运行的,也会因此而停止。

内核实现的优缺点:
优:可以对线程进行阻塞,从而让其他线程接着运行。
缺:在内核中创建或撤销线程的代价比较大,如果线程的创建和终止等操作比较多,则会造成巨大的开销。

所以基于上述的优缺点,人们开始将上述两种方法结合起来,即使用内核级线程,同时将用户线程与某些或者全部内核线程多路复用起来。

进程间通信

进程经常需要与其他进程通信,例如在一个shell管道中,第一个进程的输出必须传递给第二个进程,这样沿着管道传递下去。因此在进程之间需要通信,而且最好使用一种结构良好的方式而不要使用中断。

简要来说有三个问题:
1)一个进程如何把信息传给另一个进程。
2)确保两个或者更多的进程在关键活动中不出现交叉,例如,在飞机订票系统中的两个进程为不同的客户争夺飞机的最后一个座位。
3)与顺序正确有关,例如,进程A产生数据而进程B打印数据,那么B就需要在A完成前处于等待状态。

竞争条件

两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,成为竞争条件。
例如,当进程A读到in的值为7时,将7存入进程中的局部变量next_free_slot(下一个空闲区),若进程A未完成向空闲区写入文件就发生了中断,运行了进程B,此时进程B读到in的值也为7(因为A还没向7里面写入文件),则进程B会向7里面写入文件,当进程B运行结束后,运行进程A,A中的局部变量next_free_slot为7,则会向7中写入文件,从而覆盖进程B写入的文件。上述例子中进程A和B的关系则为竞争关系。

临界区

要避免竞争关系,关键是要指出某种途径来阻止多个进程同时读写共享的数据,他们需要的是互斥,即以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。于是我们将对共享内存访问的程序片段称作临界区域,即临界区。

为了提高进程的高效和正确性,需满足以下4个条件:
1)任何两个进程不能同时进入临界区。
2)不应对CPU的速度和数量做任何假设。
3)临界区外运行的进程不得阻塞其他进程。
4)不得使进程无限期等待进入临界区。

互斥的实现方法

屏蔽中断

在单处理器系统中,最简单的方法是使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在就要离开之前再打开中断。屏蔽中断后,时钟中断也被屏蔽,CPU只有发生时钟中断或者其他中断时才会进行进程切换,这样就可以保证该进程运行时,不会担心其他进程的介入。

锁变量

设一个共享变量(锁),其初始值为0,。当一个进程进入其临界区时,它首先测试这把锁,如果该锁的值为0,则该进程将其设置为1并进入临界区,若这把锁的值已经为1,则该进程将等待直到其值为0。

可能会发生的错误:假设一个进程读出锁的值并发现它为0,而恰好在它将其值设置为1前,另一个进程被调度运行,将其值设置为1 ,由于第一个进程以及判定锁为0了,所以这两个进程还会发生竞争关系。

严格轮换法

//进程0
while(TRUE){
	while(turn!=0);//循环
	critical_region();
	turn=1;
	noncritical_region();
};
//进程1
while(TRUE){
	while(turn!=1);//循环
	critical_region();
	turn=0;
	noncritical_region();
};

整形变量turn用来记录哪个进程进入了临界区。这种连续测试一个变量直到某个值出现为止,成为忙等待,浪费CPU时间,这种方法不是个好方法。

Peterson解法

#define FALSE 0
#define TRUE 1
#define N 2 					/*进程数量*/

int turn   						/*现在轮到谁了*/
int interested[N] 				/*所有值初始化为0*/

void enter_region(int process) /*进程为0或者1*/
{
	int other;					/*另一进程*/
	other=1-process;
	interested[process]=TRUE;	/*表示感兴趣*/
	turn=process;				/*设置标志*/
	while(turn==process&&interested[other]==TRUE);
}
void leave_region(int process)
{
	interested[process]=FLASE;	/*表示离开临界区*/
}

一开始,没有任何进程处于临界区,现在进程0调用enter_region。它通过设置其数组元素和将turn置为0来标识它希望进入临界区。由于进程1并不想进入临界区,所以enter_region很快便返回了。如果进程1现在调用enter_region,进程将在此处挂起知道interested[0]变成FALSE,改事件只有在进程0调用leave_region退出临界区时才会发生。

TSL指令

TSL RX,LOOK
该指令称为测试并加锁,它将一个内存字LOCK读到寄存器RX中,然后在该内存地址上有一个非零值。读字和写字操作保证是不可分割的,及该指令结束之前其他处理器均不允许访问该内存字。执行TSL指令的CPU将锁住内存总线,以禁止其他CPU在本指令结束之前访问内存。

 enter_region:
 	TSL REGISTER,LOCK |复制锁到寄存器并将锁设为1
 	CMP REGISTER,#0	  
 	JNE enter_region  |若锁不为0,说明锁已被设置,所以循环
 	RET				  |进入临界区
 leave_region:
 	MOVE LOCK,#0	  |在锁中存入0
 	RET

第一条指令将lock原来的值复制到寄存器中,并将锁设置为1(表示有进程想进入临界区),将寄存器的值(锁原来的值)与0进行比较,若为0,说明当前无进程处于临界区,反之则进行循环等待临界区中的进程出临界区。

睡眠与唤醒

互斥方法的本质上都是这样的:当一个进程想进入临界区,先检查是否允许进入,若不允许,则进程将原地等待,直到允许为止。这种方法不仅浪费了CPU的时间,而且还可能会引起预想不到的结果。

现在考察几条进程间通信原语,他们无法进入临界区时将阻塞,而不是忙等待。最简单的方法是sleep和wakeup。sleep是将一个将引起调用进程阻塞的系统调用,即挂起,直到另一个进程将其唤醒。下面举个生产者-消费者问题的例子。

#define N 100
int count=0;

void producer(void)
{
	int  item;
	while(TRUE){
		item=produce_item();
		if(count==N)slepp();		 /*若缓冲区已满则休眠*/
		insert_item(item);
		count=count+1;
		if(count==1)wakeup(consumer);/*缓冲区为1时推断消费者在休眠,则唤醒消费者*/
	}
}

void consumer(coid)
{
	int item;
	while(TRUE){
		if(count==0)sleep();			/*若缓冲区为空则休眠*/
		item=remove_item();
		count=count-1;
		if(count==N-1)wakeup(producer);/*缓冲区为N-1时推断生产者在休眠,则唤醒生成者*/
		cconsumer_item(item);
	}
}

上面的生产者和消费者问题可能会产生竞争关系,其原因是对count的访问未加限制。有可能出现以下情况:缓冲区为空,消费者刚刚读取count的值发现它为0,还没进入休眠。此时调度程序决定暂停消费者并启动运行生产者。生产者想缓冲区加入一个数据项,count加1,并唤醒消费者。由于消费者还没进入休眠,则wakeup信号丢失,当消费者运行时,由于之前已经判断count为0,则会休眠。生产者则会一直生产直到缓冲区填满,然后休眠。这样一来,两个进程都将永远休眠下去。

信号量

信号量是一个整形变量来累计唤醒次数,供以后使用。一个信号量的取值可以为0(表示没有保存下来的唤醒操作)或者正值(表示有一个或者多个唤醒操作)。
设立两种操作down和up(分别为一般化后的sleep和wakeup)。

对一信号量进行down操作,则是检查其值是否大于0,若该值大于0,则将其减1(即用掉一个保存的唤醒信号)并继续;若该值为0,则进程将睡眠。检查数值、修改变量值以及可能发生的睡眠操作均作为一个单一的、不可分割的原子操作完成的(即通过屏蔽中断来实现,由于这些动作只需要几条指令,所以屏蔽中断不会带来什么副作用)。

up操作对信号量的值增1,如果一个或多个进程在该信号量上睡眠,无法完成一个先前的down操作,则有系统选择其中一个并允许该进程完成他的down操作。

用信号量来解决生产者和消费者问题。通过使用三个信号量:一个成为full,用来记录充满的缓冲槽数目;一个称为empty,记录空的缓冲槽数目;一个称为mutex,用来确保生产者和消费者不会同时访问缓冲区。full的初值为0,empty的初值为缓冲区的槽数,mutex的初值为1,。供两个或多个进程使用的信号量,其初值为1,保证同时只有一个进程可以进入临界区,称为二元信号量。如果每个进程在进入临界区前执行一个down操作,并在刚刚推出时执行一个up操作,就能够实现互斥。

#define N 100
typedef int semaphore;
semaphore mutex 1;
semaphore empty N;
semaphore full=0;

void producer(void)
{
	int item;
	while(TRUE){
		item=produce_item();	/*生产数据*/
		down(&empty);
		down(&mutex);			/*进入临界区*/
		insert_item(item);		/*将数据放入缓冲区*/
		up(&mutex);				/*出临界区*/
		up(&full);
	}
}
void consumer(void)
{
	int item;
	while(TRUE){
		down(&full);
		down(&mutex);
		item=remove_item(item);	/*取出数据*/
		up(&mutex);
		up(&empty);
		consume_item(item);
	}
}

互斥量

互斥量是信号量的一个简化版本,不具备计数功能,仅仅适用于管理共享资源或一小段代码。互斥量是一个可以处于两态之一的变量:解锁和加锁。

进程的调度

当只有一个CPU可以使用时,那么就必须选择下一个需要执行的进程。在操作系统中完成选择工作的这一部分成为调度程序,该程序使用的算法成为调度算法。调度算法主要分为以下3种情况:
1)批处理。
2)交互式。
3)实时
调度算法主要有以下几个目标:
1)公平:给每个进程公平的CPU份额
2)策略强制执行:保证规定的策略被执行
3)平衡:保持系统的所有部分都忙碌

批处理:
1)吞吐量:每小时最大作业数
2)周转时间:从提交到终止间的最小时间
3)CPU利用率:保持CPU始终忙碌

交互式系统:
1)响应时间:快速响应请求
2)均衡性:满足用户期待

实时:
1)满足截止时间:避免丢失数据
2)可预测性:在多媒体系统中避免品质的降低

批处理系统中的调度

批处理情况下主要有以下3中调度算法:
1)先来先服务:进程按照他们请求CPU的顺序使用CPU
2)最短作业优先:当输入队列中有若干个同等重要作业启动时,需要CPU时间最短的先运行
3)最短剩余时间优先:调度程序总是选择剩余运行时间最短的那个进程运行

交互式系统中的调度

交互式系统主要有以下7种调度算法:
1)轮转调度:每个进程被分配一个时间段,成为时间片。则允许进程在该时间段中运行,如果时间片结束后进程还在运行,则剥夺CPU并分配给另一个进程。如果该进程在时间片结束前阻塞,则CPU立即进行切换。
2)优先级调度:给进程f赋予优先级,优先级高的先运行。
3)多级队列
4)最短进程优先
5)保证调度:保证每个进程的使用时间
6)彩票调度:给每个进程发彩票,每次使用调度算法的时候宣布中奖彩票,则中奖的进程获得CPU使用权,通过概率来分配CPU,还可以通过回收彩票来改变概率。(比较有用)
7)公平分享调度

实时操作系统

实时操作系统分为硬实时和软实时,前者含义是必须满足绝对的截止时间,后者的含义是虽然不希望偶尔错失截止时间,但是可以容忍。

小结

进程可以动态的创建和终止,每个进程都有自己的地址空间。

对于某些应用而言,在一个进程中使用多个线程是有益的,这些线程被独立调度并且有独立的栈,但是在一个进程中所有线程共享一个地址空间。线程可以在用户态实现,也可以在内核态实现。

进程之间通过进程间通信原语来交换信息,这些原语用来确保不会有两个进程同时在临界区中,以避免出现混乱。

目前已经有大量成熟的调度算法,如最短作业优先调度算法、轮转算法、优先级算法、多级队列调度、彩票调度以及公平分享调度等等。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值