从操作系统"出发","品"技术演化,"深扎"知识树根。 为您收录的操作系统系列 - 进程管理(上篇)对进程概念有了初步了解,下面的内容将围绕进程管理中的“进程控制”,“进程同步”两点进行。
第二篇:进程管理(中)
进程控制
进程的创建
新程创建的条件通常包括:
用户登陆,作业调度,提供服务,应用请求 。
linux 系统中除 0 号进程 (swapper 进程 )外的其他进程都是由其父进程创建的。
新进程被创建时,有两种执行可能:
父进程与子进程并发执行,
父进程等待,直到某个或全部子进程执行完毕。
新进程的地址空间也有两种可能:
子进程共享父进程的地址空间。
进程拥有独立的地址空间。
创建进程的一般步骤:
申请空白 PCB (进程控制块)。
为新进程分配资源 。
初始化进程控制块(PCB) 。
将新进程插入就绪队列。
linux 的创建流程 :
为新进程分配唯一的进程 pid 。
为新进程获取自己的进程描述符 。
为新进程指定内存地址空间 。
完成进程一些初始化的工作 。
返回进程的 pid。
进程的阻塞
可能出现进程阻塞的条件:
请求系统服务。
请求打印服务。
启动某种操作。
启动 I/O 。
新数据尚未到达。
进程没有输入数据。
无新工作可做 。
发生消息后的等待完成。
进程阻塞的简化过程:
将进程的状态改为阻塞态。
将进程插入相应的阻挡队列。
转进程调度程序。
从就绪程序中选择进程为其分配 CPU。
进程的唤醒
唤醒过程
将进程从阻塞队列中移除。
将进程状态有阻塞态改为就绪态。
将进程加入就绪队列。
进程的终止
进程终止也被成为进程撤销。
进程终止的情况:
终止原因 :
子进程使用了超过它所分配到的一些资源 。
分配给子进程的任务已不在需要。
父进程推出。
终止过程:
从进程的 PCB 中读取进程状态。
若正在执行,则终止程序执行。
若进程有子进程,在大多数情况下需要终止子进程。
释放资源。
将终止进程的 PCB 移除。
进程同步
多任务操作系统支持多个进程并发执行,并发执行的进程共享系统的软件和硬件资源。进程访问资源通过执行指令实现,在这种情况下,操作系统可以也必须对进程访问共享资源的过程进行控制与管理。
操作系统的同步机制的主要任务就是保证在多任务共享系统资源的情况下,程序执行能得到正确的结果。此外,有些进程直接具有相互合作的关系,同步机制需要解决这些进程执行的协调问题。
进程同步的基本概念
在多道批程序环境下,进程之间可能存在资源共享关系和相互合作关系。
进程同步有两个任务:
临界资源说明:
P1 和 P2 两个进程并发执行,counter 是全局变量,初始化值为 0,P1 和 P2 两个进程分别对 counter 进行 + 1 操作。counter 就是临界资源。(所有代码部分采用 java 线程代替进程概念,有点绕!)感谢 程序员架构进阶 作者的提醒.
下面代码用 java 代码模拟一下 counter 的累计操作。P1 和 P2 两个线程对 counter 先取值,后加 1。
/**
* @author Arvin https://www.infoq.cn/u/shenghuoheike
* @data 2021-01-30
*/
public class IntegerDataDemo {
//公共资源计数器
static int counter = 0;
//执行测试
public static void main(String[] args) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
10,
10,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100)) ;
//线程P1
Runnable P1 = new Runnable() {
public void run() {
int p1v = IntegerDataDemo.counter;
System.out.println("P1拿到的值 = "+p1v);
IntegerDataDemo.counter ++;
System.out.println("P1++后的值 = "+ IntegerDataDemo.counter);
}
};
//线程P2
Runnable P2 = new Runnable() {
public void run() {
int p2v = IntegerDataDemo.counter;
System.out.println("P2拿到的值 = "+p2v);
IntegerDataDemo.counter ++;
System.out.println("P2++后的值 = "+ IntegerDataDemo.counter);
}
};
executor.execute(P1);
executor.execute(P2);
try {
//获得资源 do something here
Thread.sleep(500);
}catch (Exception e){
}
executor.shutdown();
}
}
复制代码
执行输出:对于累加后值的正确是因为 Java 基本类型保证了原子性(原语)。
输出结果中可以看出,对 counter 的访问并没有实现互斥,所以 P1 和 P2 的取值都为 0。
临界区说明:
临界区是进程访问临界资源的那段代码。访问临界资源是通过访问临界区的代码来实现的。
如果使程序以互斥的方式进入临界区,就能实现临界资源互斥访问。这一点可以通过在临界区前面加入区代码,在临界区后面加入区代码来实现。
同步机制应该遵循的准则
没有进程处于临界区时,表明临界区资源处于空闲状态,应允许一个请求进入临界区的进程立即进入临界区,以有效的利用临界资源。
当已有进程进入临界区,表明临界区正在被访问,因而其他企图进入临界区的进程必须等待,以保证临界区的互斥访问。
对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,以免进程无限等待。
当进程申请不到共享资源的访问权限不清时,应立即释放处理机。以免陷入“忙等”状态,做出 cpu 资源浪费。
信号量机制
在信号量机制中,用某种类型的变量,及信号量的取值来表示资源的使用状况,或者某种时间释放发生,以此为继承实现进程同步。
整型信号量机制
整型信号量是表示共享资源状态且只能有特殊的原则操作改变的整形量。
原理:
定义一个整型变量,用变量来标记资源的使用情况。如果整型量 > 0 , 说明有可用资源。 如果整型量 <= 0 ,说明资源忙,进程必须等待, 对于一次只允许一个进程访问的临界资源,可以定义一个用于互斥访问的整型信号量,并将初始化为 1.
下面代码用 java 代码模拟一下 counter 的累计操作。P1 和 P2 两个线程对 counter 先取值,后加 1。并实现了互斥访问。(所有代码部分采用 java 线程代替进程概念,有点绕!)感谢 程序员架构进阶 作者的提醒.
import java.applet.Applet;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 整型信号量
* @author Arvin https://www.infoq.cn/u/shenghuoheike
* @data 2021-01-30
*/
public class IntegerDataMechanism{
//(整型信号量的wait和signal 操作)
//定义一个整型变量,用变量来标记资源的使用情况。如果整型量 > 0 , 说明有可用资源
static int mutex = 1;
//公共资源计数器
static int counter = 0;
//用户申请资源
/**
* 使用 synchronized 关键字模拟 不可中断效果。
* synchronized 用于 static 时候,相当于将锁加到了当前 Class 对象上,因此所有对该方法的调用
* 必须获取 Class 对象的锁。
*
* 普通方法声明 synchronized 被调用时,掉用的线程首先必须获得当前对象的锁,
* 若对象锁被其他线程持有,则调用线程会等待,方法结束后,对象锁会被释放。
* @param pid
* @return
*/
synchronized static int waitMethod(String pid){
//如果整型量 <= 0 ,说明资源忙,进程必须等待,
while (mutex <= 0){
//打印机等待
System.out.println(pid +"在等待 ......");
}
System.out.println(pid +"获取资源 " + mutex);
mutex = mutex - 1;
return mutex;
}
//用于释放资源
static void signalMethod(String pid){
System.out.println(pid +"释放资源");
mutex = mutex + 1;
}
//执行测试
public static void main(String[] args) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
10,
10,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100)) ;
//线程P1
Runnable P1 = new Runnable() {
public void run() {
//申请资源
int p1v = IntegerDataMechanism.waitMethod("P1");
System.out.println("P1 获取资源 = "+p1v);
IntegerDataMechanism.counter ++;
//释放资源
IntegerDataMechanism.signalMethod("P1");
System.out.println("资源计数器----->"+IntegerDataMechanism.counter);
}
};
//线程P2
Runnable P2 = new Runnable() {
public void run() {
//申请资源
int p2v = IntegerDataMechanism.waitMethod("P2");
System.out.println("P2 获取资源 = "+p2v);
IntegerDataMechanism.counter ++;
//释放资源
IntegerDataMechanism.signalMethod("P2");
System.out.println("资源计数器----->"+IntegerDataMechanism.counter);
}
};
executor.execute(P1);
executor.execute(P2);
try {
//获得资源 do something here
Thread.sleep(500);
}catch (Exception e){
}
executor.shutdown();
}
}
复制代码
执行输出:
在输出结果中可以看出 P1 首先获得资源,P2 回一直等待 P1 释放资源后才可以对资源进行操作。达到了互斥效果。
总结:
整型信号量的值只能由 wait 和 signal 操作改变。 wait 和 signal 操作都是原子操作,即在者这两个操作中对信号量的访问是不能被中断的。 在单 CPU 的系统中,wait 和 signal 都是原子性,只能关中断。原子操作可以通过中断来实现。
整型信号量机制的实例:liunx 中的子旋锁 SpinLock 。不同的资源对应不同的信号量,并不是系统中所有的资源都用同一个信号量表示。
记录型信号量机制
记录型信号量的数据类型:资源数量 ,阻塞队列。
现有资源数量为 2,分别 P1,P2,P3 ,3 个线程对资源进行使用,利用记录型信号量机制实现的 java 代码如下:(所有代码部分采用 java 线程代替进程概念,有点绕!)感谢 程序员架构进阶 作者的提醒.
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 记录型信号量
* @author Arvin https://www.infoq.cn/u/shenghuoheike
* @data 2021-01-30
*/
public class RecordMechanism {
//初始化资源数量
static int source = 2;
//阻塞队列
static Queue<String> process = new ArrayBlockingQueue<>(5);
//
/**
* 1)当 source >= 0 表示资源数量。当 source <0 的时候 source的绝对值等于资源等待队列中阻塞进程的数量。
* 2)每次 waitMethod 操作意味着请求一个资源,表示为 source = source - 1; 当 source < 0 表示资源分配完毕。
* 因而进入 process 阻塞队列中 通过 block 进行自我阻塞。
* @param pid
*/
synchronized static void waitMethod(String pid){
System.out.println(pid + "申请资源前,阻塞队列长度:" + process.size()+",资源数量:"+source);
//使用资源
source = source - 1;
//阻塞队列数据新增
if(source < 0){
block(pid);
}
}
//用于释放资源
/**
* 3)每次的 signalMethod 操作就意味着释放一个资源,故 source = source + 1; 并通过 wakeup 唤醒
* 4)如果 source 的初始化值为1,表示只允许一个进程访问临界资源。此时型号量转化为互斥型号量。
* 5)记录信号量机制的优点是不存在 "忙等",采用"让权等待"的策略。
* @param pid
*/
static void signalMethod(String pid){
System.out.println(pid +"释放资源");
//判断释放需要释放资源
boolean release = Math.abs(source) == process.size();
//新增资源
source = source + 1;
if(release){
wakeup(pid);
}
}
static void block(String pid){
process.add(pid);
System.out.println(pid + "调用是无资源,添加阻塞队队列中:" + process.size()+",资源数量:"+source);
//判断是否存在阻塞
while (!process.isEmpty()){
System.out.println(pid +",调用时候,模拟阻塞等待 ......");
try {
//获得资源 do something here
Thread.sleep(1000);
}catch (Exception e){
}
}
}
static void wakeup(String pid){
String v = process.poll();
System.out.println(pid + ",阻塞队列释放元素:"+ v);
}
//执行测试
public static void main(String[] args) {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
10,
10,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10)) ;
//线程P1
Runnable P1 = new Runnable() {
public void run() {
//申请资源
RecordMechanism.waitMethod("P1");
try {
System.out.println("P1,拿到资源了");
//获得资源 do something here
Thread.sleep(100);
}catch (Exception e){
}
//释放资源
RecordMechanism.signalMethod("P1");
}
};
//线程P2
Runnable P2 = new Runnable() {
public void run() {
//申请资源
RecordMechanism.waitMethod("P2");
try {
System.out.println("P2,拿到资源了");
//获得资源 do something here
Thread.sleep(100);
}catch (Exception e){
}
//释放资源
RecordMechanism.signalMethod("P2");
}
};
//线程P3
Runnable P3 = new Runnable() {
public void run() {
//申请资源
RecordMechanism.waitMethod("P3");
try {
System.out.println("P3,拿到资源了");
//获得资源 do something here
Thread.sleep(100);
}catch (Exception e){
}
//释放资源
RecordMechanism.signalMethod("P3");
}
};
executor.execute(P1);
executor.execute(P2);
executor.execute(P3);
try {
Thread.sleep(3000);
}catch (Exception e){
}
executor.shutdown();
}
}
复制代码
执行输出:
测试结果:程序 P1 和 P3 分别拿到了现有的 2 个资源,P2 在获取资源时,又有没有资源而进行了阻塞。后面 P1 或 P3 释放资源后,P2 顺利获取了资源。
AND 信号量机制
AND 型信号量机制的引入
一个进程在运行过程中往往申请多个共享资源,如果使用整型或记录型信号量机制,可能会出现申请资源顺序不当导致进程死锁。AND 信号量的基本思想是将进程在整个运行过程中说需要的所有资源一次性的全部分给进程,带进程用完后一起释放,只要已有一个资源不能分配给该进程,其他所有可能为之分配的资源也不分配给它。可以沿用记录型信号量的实现方式,将多个资源的 waitMethod 和 signalMethod 原子性操作即可。
以上为 《重拾操作系统》系列的第二篇"进程管理(中篇)",欢迎大家的留言讨论。按惯例最后分享一首诗给大家。
我们醉了,不是应为酒。
我们聚会,如此欢快,不是因为竖琴或鲁巴。
没人斟酒,没人作伴,没人演奏,甚至没有酒。
我们却烂醉如泥,凌乱,晕眩。
相关收藏
为您收录的操作系统系列 - 进程管理(上篇) (操作系统-进程概述)
技术根儿扎得深,不怕“首都”狂风吹! (操作系统-发展简介)
评论 (4 条评论)