文章目录
资源分配图(RAG)
前言
之前的博文中我总结了图在程序分析中常用的一些算法,如果对图还不熟悉的可以通过这个链接去了解https://blog.csdn.net/qq_41252520/article/details/138393200
这篇博文将正式地走进图在程序分析中的应用场景——资源分配图检测死锁
什么是资源分配图?
资源分配图是有向图,图中的顶点分两种类型:
- 资源
- 进程
资源顶点
对于资源类型的顶点,它会有独特的一个属性就是资源的数目。资源顶点到进程顶点的一条有向边代表了资源已被分配给进程,有多少条这样的有向边就表示分配了多少个这样的资源给进程。
进程顶点
进程顶点到资源顶点的一条有向边代表了进程请求资源,有多少条这样的有向边就表示进程请求了多少个这样的资源。
资源分配图的特点
根据资源分配图的概念,得出以下3个特点:
- 每个不同类型的顶点之间,同方向的边可能会有多条。
- 同类型顶点之间不存在边。
- 资源类型的顶点到进程顶点的有向边的条数小于等于该资源的数目,换句话说资源类型顶点的出度小于等于其资源的数目。
习惯上用字母R表示资源顶点,字母P表示进程顶点。
下面是RAG示例:
RAG-1
RAG-2
RAG-3
图中的大圆圈表示进程顶点,矩形表示资源顶点,矩形里边的小圆圈数目表示资源的数量。
资源分配图的作用
主要作用就是检测死锁的。我们回顾一下发生死锁的四个必要条件:
- 互斥:同一资源同一时间只能被一个进程占有。
- 不可剥夺:进程获得的资源在未主动释放前,不能被其他进程抢占。
- 请求与保持:进程在获得至少一个资源的同时,继续请求其他的资源,并且不会释放已占有的资源。
- 循环等待:存在由进程和资源组成的环形等待链。
以上 4 个必要条件同时存在才会发生死锁,任何一个条件被打破,死锁就会消除。 \textcolor{BrickRed}{以上4个必要条件同时存在才会发生死锁,任何一个条件被打破,死锁就会消除。} 以上4个必要条件同时存在才会发生死锁,任何一个条件被打破,死锁就会消除。
那么基于死锁产生的必要条件和RAG,如何避免死锁呢?
基于RAG检测死锁
基本思想就是化简RAG,如果最后化简得到的RAG中没有环则说明可以避免死锁,否则就是必然死锁。而化简的一般思路步骤如下:
- 看资源顶点,统计资源顶点的出度计算出资源的空闲数目。
- 看进程顶点,主要看进程顶点到资源顶点的有向边。如果所有的请求都能得到满足,则可以删除进程顶点与所有资源的所有有向边。
- 重复步骤1和2,直到无法删除进程顶点时停止。此时得到的就是化简后的RAG。
- 如果化简后的RAG存在环就说明死锁,否则无死锁。
注:以上步骤顺序不能乱。 \textcolor{BrickRed}{注:以上步骤顺序不能乱。} 注:以上步骤顺序不能乱。
分别拿上面三张RAG的图来详细解释化简的过程。
图RAG-1化简过程
按部就班,先看资源顶点。
-
资源顶点分别有R1、R2。按顺序看,R1分配两个资源给P1,分配一个资源给P2,所以R1已经没有空闲资源了。R2分配一个资源给P2,所以R2还有一个空闲资源。
-
进程顶点分别有P2、P2。按顺序看,P1还需请求一个R2资源,R2此时空余一个资源可以满足P1的请求,所以P1可以顺利执行,并且后续会释放掉所有的资源。因此可以删除P1与资源顶点的所有有向边,这一轮的化简结果为:
开始第二轮化简:
-
资源顶点R1分配了一个资源给P2,还剩2个空闲资源;R1分配一个资源给P2,,还剩1个空闲资源。
-
进程顶点P2还需请求一个R1资源,R1有2个空闲资源,可以满足P2的请求,所以P2可以顺利执行,并且后续会释放掉所有资源。因此P2顶点也可以化简成孤立的顶点。最终所有进程顶点都变成了孤立状态:
这就是最终的化简结果,所有的进程节点都变成了孤立状态,不存在环,也就没有死锁。
图RAG-2化简过程
-
资源顶点R1分配了一个资源给P2,没有空闲资源;R2分配一个资源给P1,另一个资源给P3,无空闲资源;R3分配一个资源给P2,还剩1个空闲资源;R4分配一个资源给P3,无空闲资源。
-
进程顶点P1还需要请求1一个R1资源,但是R1没有空闲资源,所以P1处于阻塞状态,保持请求,P1无法孤立;P2还需请求一个R4资源,因为R4无空余资源,所以P2也被阻塞,无法孤立;P3需要请求一个R3资源,一个R2资源。R3有1个空闲资源可以满足P3请求,但是R2无空闲资源,无法满足P3请求,所以P3也被阻塞。
-
至此所有的进程顶点都无法孤立,RAG无法化简,且原RAG中存在环( P 2 − > R 4 − > P 3 − > R 3 − > P 2 \textcolor{orange}{P2->R4->P3->R3->P2} P2−>R4−>P3−>R3−>P2),所以必然死锁。
图RG-3化简过程
-
所有资源顶点都没有被分配,并且每个资源数量都是1个,属于互斥资源。
-
进程顶点P1申请1个R2资源,一个R1资源。R1和R2都有空余资源,所以P1的申请得到满足,P1不会被阻塞,并在执行完毕后释放掉所占用的资源。P1顶点可以被孤立,所以简化后得到:
开始第二轮化简:
进程顶点P2申请1个R2,资源,1个R3资源。因为R2和R3都有空闲资源,所以P2的申请得到满足,P2也可以孤立出去,化简得到:
第三轮化简:
进程顶点P3申请1个R3资源,1个R1资源。因为R3和R1都有空闲资源,所以P3也可以孤立出去,同理P4也可以被孤立出去。最后经过4轮化简,得到最终化简结果:
因此无死锁。
算法实现
数据结构
定义节点类Vertrix
enum class VertexType
{
VT_Process = 0, // 进程顶点
VT_Resource // 资源顶点
};
class Vertex
{
private:
int FreeCountBackup_;
protected:
VertexType m_Type; // 表示进程顶点还是资源顶点
int m_Id; // 表示该节点在邻接矩阵中的位置
int m_FreeCount; // 空闲资源数
public:
// 进程节点所拥有的资源
std::unordered_set<int> m_OwnedRv;
// 进程请求的资源
std::unordered_set<int> m_RequestRv;
Vertex(int Id, int Count, VertexType Type) :
FreeCountBackup_(0),
m_Type(Type),
m_Id(Id),
m_FreeCount(Count)
{}
Vertex(const Vertex& v)
{
FreeCountBackup_ = v.FreeCountBackup_;
m_Type = v.m_Type;
m_FreeCount = v.m_FreeCount;
m_Id = v.m_Id;
m_OwnedRv = v.m_OwnedRv;
}
~Vertex() {}
auto stash()->void { FreeCountBackup_ = m_FreeCount; }
auto revert()->void { m_FreeCount = FreeCountBackup_; }
auto setId(int Id)->void { m_Id = Id; }
auto getId()const->int { return m_Id; }
auto getType()const->VertexType { return m_Type; }
auto setFreeCount(int Count)->void { m_FreeCount = Count; }
auto getFreeCount()const->int { return m_FreeCount; }
};
引入
m_OwnedRv
和m_RequestRv
这两个成员其实是为了优化访问邻接矩阵时的速度。m_OwnedRv和m_RequestRv中保存的是资源顶点在邻接矩阵中的索引,不同的是m_OwnedRv代表着r->p的有向边,m_RequestRv代表着p->r的有向边。有了这两个字段,其实邻接矩阵的作用已经很小了,我这里把邻接矩阵用来记录进程顶点申请资源顶点的次数和资源顶点分配给进程顶点的个数。
定义资源图类RAG
class RAG
{
private:
// 用于hashCycl函数中,作为临时容器,存储进程顶点在邻接矩阵中的索引
std::unordered_set<int> Explorer_;
protected:
// 顶点个数
int m_VertextCount;
// 邻接矩阵,这里的作用主要是记录进程顶点申请资源顶点的次数和资源顶点分配给进程顶点的空闲资源个数
unsigned char* m_AdjMatrix;
// 资源型顶点集合
std::unordered_map<int, Vertex> m_RVertexs;
// 消耗资源型的顶点集合
std::unordered_map<int, Vertex> m_PVertexs;
public:
RAG(int vertext_num);
// 拷贝构造
RAG(const RAG& g);
// 移动构造
RAG(RAG&& g) noexcept;
~RAG();
//添加顶点
auto addEdge(const Vertex& v1, const Vertex& v2)->void;
// 移除顶点
auto removeEdge(const Vertex& v1, const Vertex& v2)->void;
// 检测是否存在环
auto hasCycl(bool backup = true)->bool;
// 获取化简后的资源分配图
auto simplify()->RAG;
};
检测死锁算法描述
基本思想:尝试根据规则孤立RAG中的进程顶点,并删除孤立的进程顶点与所有资源顶点的边,将孤立的进程顶点从Explorer_ 中移除,最后判断Explorer_为空则表示无环无死锁。下面是算法步骤:
- 遍历Explorer_集合,获取对应进程顶点的m_RequestRv字段。
- 遍历m_RequestRv字段数据,进程顶点在申请哪些资源顶点。如果被申请的资源如果还有空余,则可以满足进程顶点的申请,于是我们用一个临时变量iSatisfy表示申请被满足的个数,在步骤1开始时初始为0,并在每次满足申请的时候递增1。
- 遍历完m_RequestRv的数据后,判断iSatisfy是否与m_RequestRv中的元数个数相等,相等则表示进程申请的所有资源都得到了满足,进程顶点就可以被孤立。然后将其移出Explorer_。
- 如果每一轮到步骤3时,Explorer_中的元素都在减少,则说明还可以进行下一轮的化简,于是重复步骤1~3;否则转步骤5。
- 判断Explorer_是否为空,如果为空则说明无死锁,否则有死锁。
完整代码实现见我的github:https://github.com/singlefreshBird/Algorithm/tree/main/rag
测试
RAG-1
void TestRAG1()
{
Vertex p1(0, 1, VertexType::VT_Process);
Vertex p2(1, 1, VertexType::VT_Process);
Vertex r1(2, 3, VertexType::VT_Resource);
Vertex r2(3, 2, VertexType::VT_Resource);
RAG rag(4);
rag.addEdge(p1, r2);
rag.addEdge(r2, p2);
rag.addEdge(p2, r1);
rag.addEdge(r1, p1);
rag.addEdge(r1, p1);
rag.addEdge(r1, p2);
if (rag.hasCycl()) std::cout << "TestRAG1(): Death lock has been detected!" << std::endl;
else std::cout << "TestRAG1(): Pass!" << std::endl;
}
TestRAG1(): Pass!
RAG-2
void TestRAG2()
{
Vertex p1(0, 1, VertexType::VT_Process);
Vertex p2(1, 1, VertexType::VT_Process);
Vertex p3(2, 1, VertexType::VT_Process);
Vertex r1(3, 1, VertexType::VT_Resource);
Vertex r2(4, 2, VertexType::VT_Resource);
Vertex r3(5, 2, VertexType::VT_Resource);
Vertex r4(6, 1, VertexType::VT_Resource);
RAG rag(7);
rag.addEdge(p1, r1);
rag.addEdge(p2, r4);
rag.addEdge(p3, r3);
rag.addEdge(p3, r2);
rag.addEdge(r1, p2);
rag.addEdge(r2, p1);
rag.addEdge(r2, p3);
rag.addEdge(r3, p2);
rag.addEdge(r4, p3);
if (rag.hasCycl()) std::cout << "TestRAG3(): Death lock has been detected!" << std::endl;
else std::cout << "TestRAG3(): Pass!" << std::endl;
}
TestRAG2(): Death lock has been detected!
RAG-3
void TestRAG3()
{
Vertex p1(0, 1, VertexType::VT_Process);
Vertex p2(1, 1, VertexType::VT_Process);
Vertex p3(2, 1, VertexType::VT_Process);
Vertex p4(3, 1, VertexType::VT_Process);
Vertex r1(4, 1, VertexType::VT_Resource);
Vertex r2(5, 1, VertexType::VT_Resource);
Vertex r3(6, 1, VertexType::VT_Resource);
RAG rag(7);
rag.addEdge(p1, r2);
rag.addEdge(p1, r1);
rag.addEdge(p2, r2);
rag.addEdge(p2, r3);
rag.addEdge(p3, r1);
rag.addEdge(p3, r3);
rag.addEdge(p4, r2);
if (rag.hasCycl()) std::cout << "TestRAG4(): Death lock has been detected!" << std::endl;
else std::cout << "TestRAG4(): Pass!" << std::endl;
}
TestRAG3(): Pass!