1 进程
程序,即一个指令序列
一个程序被加载到内存后可分为程序段与数据段。程序段存放程序的代码(或指令),程序运行过程处理的数据存放在数据段。
为了方便操作系统的管理,完成程序并发的执行,引入了进程(or 进程实体的概念)。
当内存中存在多道程序,各程序的代码、数据存放的位置不同,为了找到各程序的存放位置,系统为每个运行的程序配置一个了数据结构,即PCB(进程控制块),PCB用来描述进程的各种信息,如进程代码存放的位置,进程描述信息(PID)、用户标识符(UID)、进程控制和管理信息、资源分配清单、处理及相关信息等。
程序段、数据段、PCB构成了一个进程实体,简称进程。
进程的状态
3种基本状态:运行态,就绪态,阻塞态,此外还有创建态、终止态。
- 运行态:占有CPU,并在CPU上运行
- 就绪态: 已具备运行条件,但没有空闲CPU
- 阻塞态:因等待某一事件而暂时不能运行
进程若想由阻塞态转换成运行态,需要先转换为就绪态,进入就绪队列后才能转换为运行态.
进程通信–管道
各进程拥有独立的内存地址空间,一个进程不能直接访问另一个进程的地址空间
管道通信:
- 是一种半双工通信,如果要实现双向同时通信,需要设置两个管道
- 各进程要互斥地访问管道。
- 数据以字符流地方式写入管道。当管道被写满时,写进程地write()将阻塞,当读进程read()将数据全部取走后,管道变为空,这时候read()就会阻塞
- 管道如果没有写满地话,是不能读的。同样,如果没有读空,也不能写
消息
进程间的数据交换以格式化的消息为单位。
消息的传递包括直接传递和间接传递两种。
直接传递即消息直接挂到接收进程的缓冲队列,间接传递会将消息先发到某中间实体种,如电子邮件系统。
消息可分为消息头与消息体,消息头中包含有进程ID,接收进程ID,消息的类型、长度等
2 线程
引入线程后,线程成为程序执行流的最小单位。
线程的属性:
- 处理机调度的单位
- 各线程可占用不同的CPU
- 每个线程都有一个线程ID,线程控制块(TCB)
- 也有就绪、运行、阻塞三态
- 同一进程的不同线程共享进程资源
- 切换线程的系统开销要比进程小
线程在操作系统中可以分为用户级线程以及内核级线程。
用户级线程由程序通过线程库实现,所有线程的管理工作由应用程序负责(包括线程切换)。
用户级线程在用户态下完成。
内核级线程的管理工作由内核完成。 核心态。
多线程模型:
操作系统只看得到内核级线程,因此,只有内核级线程才是处理机分配的单位
为每一个用户级线程都分配一个处理机可能会造成系统资源浪费,因此为了实现高并发且最大化利用系统资源,可以将n个用户级线程映射到m个内核级线程。
1 多对一模型:
即将多个用户级线程映射到一个内核级线程,一个用户级进程对应一个内核级线程
优点:用户及线程的切换在用户态下即可完成,开销小,效率高
缺点: 当一个用户级线程阻塞后,整个进程都会阻塞,并发度不高;多个线程不能在多核处理机上运行。
2 一对一模型
每个用户级线程都会映射到一个内核级线程
优点:并发高;一个线程阻塞也不会影响到其他线程
缺点:系统开销大,占用了太多内核线程
3 多对多模型
m个用户级线程映射到n个内核级线程:
3 处理机的调度
即:高级调度(作业调度)、中级调度,低级调度。
- 高级调度:按照一定的原则从外存上处于后背队列的作业中挑选一个或多个作业,为它们分配内存等必要资源。
作业调度是会建立相应的PCB,作业调出时才会撤销PCB - 中级调度:引入虚拟内存后,可将暂时不能运行的进程调至外存等待,等它具备运行条件且内存空闲时重新调入内存(PCB不会被调到外存)。
暂时调到外存等待的进程被称为挂起状态,被挂的进程的PCB会被放到挂起队列。 - 低级调度:操作系统中最基本的调度,按照某种方法和策略从就绪队列中选取一个进程分配处理机。
何时需要进程调度?
当前进程主动/被动放弃处理机时
主动放弃:进程异常终止或主动请求阻塞;
被动放弃:进程的时间片用完,或有更紧急的事件,或有更高优先级的进程进入就绪队列
不能进行进程调度与切换的情况:
- 处理终端时;
- 进程在操作系统的内核程序临界区中;
- 在原子操作的过程中。
临界资源:一个时间段只允许一个进程使用的资源;各进程要互斥地访问临界资源
临界区:访问临界资源的那段代码
内核临界区一般时用来访问某种内核数据结构的,如进程的就绪队列(就绪队列由各就绪进程的PCB组成)
内核程序临界区访问的临界资源要尽快释放,否则可能会影响到操作系统内核的其他管理工作。所以在访问内核程序临界区时不能进行调度与切换。不过普通临界区访问的资源不会直接影响操作系统内核的工作,所以在访问普通临界区时可以进行调度、切换。
进程调度方式
非剥夺调度方式与剥夺调度方式
- 非剥夺调度方式(非抢占方式):只允许进程主动放弃,如果由更紧急的任务到达,当前进程并不会让出处理机。
优点:实现简单,系统开销小
缺点:无法处理紧急的任务 - 剥夺调度方式(抢占方式):当一个进程正在处理机上运行时,若有一个更重要的进程需要处理机,则立即暂停正在执行的进程,将处理机让给更重要的进程
相较于非抢占式调度,这样可以优先处理更紧急的进程,适用于分时操作系统
进程切换的过程:
- 对原来的运行进程的各种数据进行保存
- 对新的进程的各种数据的修复
如程序计数器、程序状态字、各种寄存器数据,这些信息一般保存在进程控制块(PCB)中
调度算法:
- 先来先服务(FCFS)
- 短作业优先(SSF)→服务时间最短
- 高相应比优先(HRRN)
相应比 = 等待时间 + 要求服务时间 要求服务时间 相应比=\frac{等待时间+要求服务时间}{要求服务时间} 相应比=要求服务时间等待时间+要求服务时间 - 时间片轮转调度
- 优先级调度
- 多级反馈队列调度
调度算法的评价指标
-
系统吞吐量:单位时间内完成作业的数量
系统吞吐量 = 完成作业数 所用时间 系统吞吐量 = \frac{完成作业数}{所用时间} 系统吞吐量=所用时间完成作业数
如10道作业用了100秒,则吞吐量=10/100=0.1道/秒 -
周转时间:从作业提交给系统开始到作业完成所用的时间
周转时间 = 作业完成时间 − 作业提交时间 周转时间=作业完成时间-作业提交时间 周转时间=作业完成时间−作业提交时间 -
平均周转时间:作业周转总时间/作业个数
-
带权周转时间:周转时间/服务时间
-
平均带权周转时间:带权周转总时间/作业个数
-
等待时间:指进程处于等待处理机的时间之和
-
响应时间:从用户提交请求到首次产生响应多用的时间
4 进程同步
保证各进程的推进顺序以我们想要的顺序来。
因为进程具有异步性,即各并发执行的进程以各自独立的、不可预知的速度向前推进
各并发执行的进程不可避免地需要共享一些系统资源(如内存、打印机、摄像头)。
两种资源共享方式:互斥共享、同时共享
对临界资源的互斥访问可分为如下四个部分:
- 进入区
- 临界区
- 退出区
- 剩余区
进入区和退出区时负责实现互斥的代码段
临界区时进程中访问临界资源的代码段,也叫临界段
剩余区做其他处理
进程互斥的软件实现方法:
- 单标志法
- 双标志先检查
- 双标志后检查
- Peterson算法:遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待原则
进程互斥的硬件实现方法:
- 中断屏蔽方法
- TestAndSet(TS指令/TSL指令)
- swap指令
信号量机制
信号量就是一个变量,可以用来表示系统中某种资源的数量.如系统中只有一台打印机,就可以设置一个初值未1的信号量。
用户进程可以使用操作系统提供的一对原语来对信号量进行操,即wait(s)
和signals(s)
。这对源于简称P.V操作
wait(s)
和signals(s)
这两个操作可写为P(s) V(s).(s表示信号量)
信号量可分为整型信号量 和记录型信号量
整型信号量
表示系统中某种资源的数量
与普通整型变量的区别: 对信号量的操作只有三种:初始化、P操作、V操作
int s = 1;
void wait(int s){ // wait原语,相当于进入区
while(s <= 0); //检查当前系统中资源是否足够,如果不够则一直阻塞
s = s - 1;
}
void signal(int s){ //signal原语,相当于退出区
s = s + 1; //使用完资源后,在退出区释放资源
}
记录型信号量
整型信号量的缺陷是存在忙等的问题
即当系统资源不够时,会一直while等待,占用处理机
记录型信号量的定义:
typedef struct{
int val; //剩余资源数
struct process *L; //等待队列
}semaphore;
某进程需要使用资源时,通过wait原语申请
void wait(semaphore s){
s.value--;
if(s.value < 0){ //如果剩余资源不够,使用block原语使进程从运行态进入阻塞态,
block(s.L); //并将进程挂到信号量s的等待队列中
}
}
//进程使用外资源后使用signal原语释放
void signal(semaphore s){
s.value++;
if(s.value <=0){ //释放信号量后,若还有别的进程等待这资源,则使用wakeup()原语唤醒等待队列中的一个进程,
wakeup(s.L); //使该进程从阻塞态变为就绪态
}
}
信号量实现进程互斥
① 分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应该放在临界区)
② 设置互斥信号量mutex,初值为1
//信号量机制实现互斥
semaphore mutex = 1; //初始化信号量
P1(){
...
P(mutex) //使用临界资源前要加锁
临界区代码段,...
V(mutex) //使用临界资源后要解锁
...
}
P2(){
...
P(mutex)
临界区代码段,...
V(mutex)
...
}
对不同的临界资源要设置不同的互斥信号量
P.V操作必须成对出现
缺少P(mutex)不能保证临界资源的互斥访问
缺少V(mutex)会导致资源不被释放,等待进程永远不会呗唤醒
信号量机制实现进程同步
进程同步:让各并发进程按照要求有序地推进
比如:P1、P2两个进程并发地执行,由于存在异步性,二者交替推进地次序是不确定的。比如下面一段程序:
P1(){
代码1;
代码2;
代码3;
}
P2(){
代码4;
代码5;
代码6;
}
当进程P1、P2两个进程并发地执行,它们执行地顺序可能为:代码1→代码4、5→代码2、3→代码6.
如果代码4的执行是基于代码1或代码2的结果,那么就必须让代码1或代码2执行完后执行代码4.
这就是进程同步要解决的问题
用信号量实现进程同步:
①分析同步关系,如代码4要在代码1、2之后执行
②设置同步信号量为s,初始为0
③在前操作之后执行V(s)
④在后操作之后执行P(s)
semaphore s = 0;
P1(){
代码1;
代码2; //
V(s);
代码3;
}
P2(){
P(s);
代码4;
代码5;
代码6;
}
若先执行V(s),则s++后s=1,之后当执行P(s)时,由于s=1,表示有可用资源,会执行s–,s变为0,P2不会执行block原语,而是继续执行代码4;
若先执行P(s),由于s=0,s–后s=-1,表示无可用资源,此时会执行block()原语,主动阻塞,之后当推行完代码2,继而执行V(s),s++,使s=0,由于此时有进程阻塞在该信号量对应的阻塞队列中,因此会在V操作中执行wakeup原语,唤醒P2,这样就可以继续执行代码4了。
信号量机制实现前驱关系(即更复杂的同步问题)
如果P1进程有句代码S1,P2进程有句代码S2,P3进程有句代码S3,… ,P6进程有句代码S6,需要按照如下前驱图所示顺序执行:
方法:
①要为每一对前驱关系各设置一个同步变量
②在前操作之后要对相应的同步变量执行一个V操作
③在后操作之前要对相应的同步变量执行一个P操作
具体如下:
待续。。。