操作系统
大佬们博客:https://blog.csdn.net/gatieme/article/details/50994533
请不要把它与之前所说的信号混淆起来,信号与信号量是不同的两种事物
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。
在学习信号量之前,我们必须先知道——Linux提供两种信号量:
- 内核信号量,由内核控制路径使用
- 用户态进程使用的信号量,这种信号量又分为POSIX信号量和SYSTEM V信号量。
POSIX 信号量与SYSTEM V信号量的比较
对POSIX来说,信号量是个非负整数。常用于线程间同步。
而SYSTEM V信号量则是一个或多个信号量的集合,它对应的是一个信号量结构体,这个结构体是为SYSTEM V IPC服务的,信号量只不过是它的一部分。常用于进程间同步。
POSIX信号量的引用头文件是<semaphore.h>,而SYSTEM V信号量的引用头文件是<sys/sem.h>
从使用的角度,System V信号量是复杂的,而Posix信号量是简单。比如,POSIX信号量的创建和初始化或PV操作就很非常方便。
System V 信号量,其在内核中维护
信号量一般有两种,二值信号量(binary semaphore)和计数信号量(counting semaphore)。
二值信号量: 顾名思义,其值只有两种0或1,相当于互斥量,当值为1时资源可用;而当值为0时,资源被锁住,进程阻塞无法继续执行。
计数信号量: 其值是在0到某个限制值之间的信号量。
System V信号量是指的计数信号量集,是一个或多个信号量的集合,其中每个都是计数信号量。(注:System V 信号量是计数信号量集,Posix 信号量是单个计数信号量。)
函数:
- int semget(key_t key, int nsems, int oflag);创建/访问一个信号量集
- int semctl(int semid, int semnum, int cmd, ... /*union semun arg */);信号量控制操作
- int semop(int semid, struct sembuf *opstr, size_t nops);操作信号量,P,V 操作
int semget(key_t key, int nsems, int oflag); 用来创建或打开一个信号量集;
key表示信号集的名字,一般由ftok函数产生;
nsems表示信号集中信号量的个数;
semflg用来标识信号量集合的权限,和创建文件类似,由九个权限标志为构成如0644,他还可以和以下参数一起使用:
——IPC_CREAT表示如果key不存在就创建;
——IPC_EXCL表示如果key存在就返回失败;
——IPC_NOWAIT表示如果需要等待,则直接返回错误;
key是信号量的键值,多个进程可以通过这个键值访问同一个信号量;nsems参数指定信号量集合中的信号量数,一般设为1,如果不创建新的信号量集,只是访问一个已经存在的集合,可以把该参数设为0,一旦创建完一个信号量集,就不能改变其中的信号量数;oflag同open()权限位,IPC_CREAT表示创建新的信号量,如果或上(|)IPC_EXCL,若信号量已存在则出错,如果没有或上IPC_EXCL,若信号量存在也不会出错。
返回值:成功返回一个非负整数即该信号量的标识码,semop和semctrl函数将使用它;失败返回-1;
int semctl(int semid, int semnum, int cmd, ... /*union semun arg */); 这个函数是对标识为semid的信号量集中的第semnum个信号量进行cmd操作;
semid是由semget()函数返回的信号量集标识符;
semnum信号集中信号量的序号;指定信号量集内的某个成员,仅在cmd为GETVAL、SETVAL、GETNCNT、GETZCNT和GETPID时使用
cmd表示将要采取的操作 :可采取的操作有:IPC_RMID、IPC_SET、IPC_STAT和IPC_INFO
——IPC_RMID表示删除信号集。
——IPC_STAT表示获取ipc_perm的参数
——IPC_INFO表示获取系统信息
——IPC_SET表示设置ipc_prem的参数,对于这个参数,semctl有单独的规定参数:
- GETPID获取信号量拥有者的pid的值;
- GETVAL获取信号量的值;
- GETALL获取所有信号量的值;
- GETNCNT获取等待信号量的值递增的进程数;
- GETZCNT获取等待信号量的值递减的进程数;
- SETVAL设置信号量的值;
- SETALL设置所有信号的值
最后一个参数是可选的,取决于cmd的值。是一个类型为senum的联合union:
- semval:信号量的当前值
- sempid:上一次成功调用semop,或以SETVAL、SETALL调用semctl的进程ID
- semncnt:等待semval变为大于当前值的线程数
- semzcnt:等待semval变为0的线程数
返回值:若成功,根据cmd不同返回不同的值,IPC_STAT,IPC_SETVAL,IPC_RMID返回0,IPC_GETVAL返回信号量当前值;出错返回-1.
eg:
当semctl()函数cmd为GETVAL时,semnum为信号量编号,若执行成功,semctl返回当前信号量的值,失败返回-1;
当cmd为SETVAL时,semnum为信号量编号,第四个参数为要设置的val(一个set一个get)
int semop(int semid, struct sembuf *opstr, size_t nops);使用semget打开一个信号量集后,对其中一个或多个信号量值的操作
返回值:成功返回信号量标识符,出错返回-1
semid指定待操作的信号量集
nops为集合中的信号量个数
ops指向一个struct sembuf类型的结构数组,该数组中的每个元素给目标信号量集中 某个特定的信号量 指定sem_op操作,这个特定的信号量由sem_num指定
sembuf结构体定义如下:
——sem_num:表示信号量编号;
——sem_op:信号量一次pv操作时加减的数值,一般会用到两个值:
- -1,p操作,等待信号量变得可用
- +1,v操作,发出的信号量变得可用
——sem_flg操作标识,有以下值: - IPC_NOWAIT对某一信号量操作,即使其中一个操作失败,也不会导致修改其他信号量的值;
- SEM_UNDO当进程退出后,该进程对sem进行的操作将被撤销;
P,V原语中P是荷兰语的Passeren,相当于英文的pass, 意为通过,V是荷兰语的Verhoog,相当于英文中的incremnet,意为释放。
上面有提到的:
信号量集合数据结构:struct semid_ds,此数据结构中定义了整个信号量集的基本属性。
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct semid_ds
{
struct ipc_perm sem_perm; /* permissions .. see ipc.h 权限 */
__kernel_time_t sem_otime; /* last semop time 最近semop时间 */
__kernel_time_t sem_ctime; /* last change time 最近修改时间 */
struct sem *sem_base; /* ptr to first semaphore in array 第一个信号量 */
struct sem_queue *sem_pending; /* pending operations to be processed 阻塞信号量 */
struct sem_queue **sem_pending_last; /* last pending operation 最后一个阻塞信号量 */
struct sem_undo *undo; /* undo requests on this array undo队列 */
unsigned short sem_nsems; /* no. of semaphores in array 信号量数 */
};
信号量数据结构:struct sem,此数据结构中定义了信号量的基本属性。
struct sem {
ushort semval; /* 信号量的当前值 */
short sempid; /* 最后一次返回该信号量的进程ID 号 */
ushort semncnt; /* 等待semval大于当前值的进程个数 */
ushort semzcnt; /* 等待semval变成0的进程个数 */
};
根据这两个结构体,在内核中某个特定信号量集可以图解为:
一个c语言示例 https://www.cnblogs.com/52php/p/5851570.html
下面使用一个例子来说明进程间如何使用信号量来进行通信,这个例子是两个相同的程序同时向屏幕输出数据,我们可以看到如何使用信号量来使两个进程协调工作,使同一时间只有一个进程可以向屏幕输出数据。注意,如果程序是第一次被调用(为了区分,第一次调用程序时带一个要输出到屏幕中的字符作为一个参数),则需要调用set_semvalue()函数初始化信号并将message字符设置为传递给程序的参数的第一个字符,同时第一个启动的进程还负责信号量的删除工作。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
在main函数中调用semget()来创建一个信号量,该函数将返回一个信号量标识符,保存于全局变量sem_id中,然后以后的函数就使用这个标识符来访问信号量。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
//union semun
//{
// int val;
// struct semid_ds *buf;
// unsigned short *arry;
//};
static int sem_id = 0;
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
int main(int argc, char *argv[])
{
char message = 'X';
int i = 0;
// 创建信号量
sem_id = semget((key_t) 1234, 1, 0666 | IPC_CREAT);
if (argc > 1)
{
// 程序第一次被调用,初始化信号量
if (!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
// 设置要输出到屏幕中的信息,即其参数的第一个字符
message = argv[1][0];
sleep(2);
}
for (i = 0; i < 10; ++i)
{
// 进入临界区
if (!semaphore_p())
{
exit(EXIT_FAILURE);
}
// 向屏幕中输出数据
printf("%c", message);
// 清理缓冲区,然后休眠随机时间
fflush(stdout);
sleep(rand() % 3);
// 离开临界区前再一次向屏幕输出数据
printf("%c", message);
fflush(stdout);
// 离开临界区,休眠随机时间后继续循环
if (!semaphore_v())
{
exit(EXIT_FAILURE);
}
sleep(rand() % 2);
}
sleep(10);
printf("\n%d - finished\n", getpid());
if (argc > 1)
{
// 如果程序是第一次被调用,则在退出前删除信号量
sleep(3);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue()
{
// 用于初始化信号量,在使用信号量前必须这样做
union semun sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
{
return 0;
}
return 1;
}
static void del_semvalue()
{
// 删除信号量
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
{
fprintf(stderr, "Failed to delete semaphore\n");
}
}
static int semaphore_p()
{
// 对信号量做减1操作,即等待P(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;//P()
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}
static int semaphore_v()
{
// 这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1; // V()
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}
这个程序的临界区为main函数for循环不的semaphore_p()和semaphore_v()函数中间的代码。
迷糊后:
对于system V信号量来说,其创建和赋初值是分开的
- Posix 信号量一般只有0和1两个值。但是system V信号量可以的数值范围可以变化一般默认的范围为0~2^15 - 1,同时对于system V信号量来说其申请的不是以个而是以组的概念进行申请。
- 创建system V信号量:
system V信号量使用semget函数进行申请。semget函数在成功申请到信号量之后,会返回一个信号量的标记值,用于之后对信号量的控制和访问。
该函数调用如下:int semget(key_t key, int nsems, int semflg);
其中key为键值需要具有唯一性,如果这个值相同那么对应的信号量相同。
该值一般由ftok函数产生,ftok函数负责产生一个在系统中具有唯一性的值。ftok函数如下: key_t ftok(const char *pathname, int proj_id); 其中的pathname需要为一个文件路径,该路径必须是具体存在的,因为ftok在生成key值时需要使用到文件的inode值,因为inode值在文件系统中具有唯一性。第二个参数proj_id为偏移值,这个值的末尾8位也同时参与到key的计算中,防止通过inode值进行生成时由于文件相同出现重复。 semget的第二个参数为nsem,即所需申请的system V信号量组中信号量的数目。 第三个参数semflg负责指定这个system V信号量的的读写权限,该权限和system V的作用范围相关,如果希望信号量的适用范围尽可能广,那么可以设置为0666。- 对system V信号量赋初值:
赋初值需要调用如下函数 semctl(data_req_p_id, 0, SETVAL, sem_union);
需要注意的是上面赋初值的使用方式是semctl较为简单的使用方式,semctl本质上有很多调用方式。
其中data_rep_p_id是信号量的标记值。
第二个参数0指明操作信号量组中的第几个信号量,(system V信号量以组的方式进行分配)
第三个参数SETVAL是对应的命令,该参数指明semctl函数应该进行的操作为赋值操作
第四个参数sem_union是一个联合体,负责传递具体的初值
较为周全的一份示例:https://blog.51cto.com/lingdandan/1763987 看的时候觉得不方便直接合并就行
//mysem_h
#ifndef _MYSEM_H_
#define _MYSEM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h> // ftok
#include <sys/sem.h>
#include <sys//wait.h>
#define PATHNAME "." // ftok 中生成key值 . 表示当前路径
#define PROJ_ID 56 // ftok 中配合PATHNAME 生成唯一key值
int create_sems(int nums); // 创建含有nums个信号量的集合
int get_sems(); // 获取信号量
// 初始化semid对应的信号量集中编号为which的信号量值为value
int init_sems(int semid , int which, int value);
int destroy_sems(int semid); // 释放该信号量集
int P(int semid, int which); // 表示分配 信号量值-1
int V(int semid, int which); // 表示释放 信号量值+1
#endif /* _MYSEM_H_ */
//mysem_c
#include "mysem.h"
// 创建信号量和获取信号量公用函数
static int comm_sem ( int nums , int semflag)
{
// 获取key
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int semid = semget(key,nums, semflag );
if( semid < 0)
{
perror("semget");
return -1;
}
return semid;
}
int create_sems(int nums) // 创建含有nums个信号量的集合
{
return comm_sem(nums, IPC_CREAT|IPC_EXCL|0666);
}
int get_sems() // 获取信号量
{
return comm_sem(0, IPC_CREAT);
}
union semun
{
int val; // value for SETVAL
struct semid_ds *buf; // buffer for IPC_STAT & IPC_SET
unsigned short *array; // buffer for GETALL & SELALL
struct seminfo * __buf; // buffer for IPC_INFO
};
// 初始化semid对应的信号量集中编号为which的信号量值为value
int init_sems(int semid , int which, int value)
{
union semun _semun;
_semun.val = value;
int ret = semctl(semid, which, SETVAL,_semun);
if(ret < 0)
{
perror("inin_sem");
return -1;
}
return 0;
}
int destroy_sems(int semid) // 释放该信号量集
{
int ret = semctl(semid, 0, IPC_RMID, NULL);
if(ret < 0)
{
perror("rm_sem");
return -1;
}
return 0;
}
static int comm_sem_op(int semid, int which, int op)
{
struct sembuf _sembuf;
_sembuf.sem_num = which;
_sembuf.sem_op = op;
_sembuf.sem_flg = 0; // IPC_NOWAIT SEM_UNDO
return semop(semid, &_sembuf, 1);
}
int P(int semid, int which) // 表示通过 信号量值-1
{
return comm_sem_op(semid, which , -1);
}
int V(int semid, int which) // 表示释放 信号量值+1
{
return comm_sem_op(semid, which, 1);
}
//test_mysem_c
// 加入信号量操作后的程序
#include "mysem.h"
#include <stdio.h>
#include <unistd.h>
int main()
{
int semid = create_sems(10); // 创建一个包含10个信号量的信号集
init_sems(semid, 0, 1); // 初始化编号为 0 的信号量值为1
pid_t id = fork(); // 创建子进程
if( id < 0)
{
perror("fork");
return -1;
}
else if (0 == id)
{// child
int sem_id = get_sems();
while(1)
{
P(sem_id, 0); // 对该信号量集中的0号信号 做P操作
printf("你");
fflush(stdout);
sleep(1);
printf("好");
printf(":");
fflush(stdout);
sleep(1);
V(sem_id, 0);
}
}
else
{// father
while(1)
{
P(semid,0);
printf("在");
sleep(1);
printf("吗");
printf("?");
fflush(stdout);
V(semid, 0);
}
wait(NULL);
}
destroy_sems(semid);
return 0;
}
竟然是markdown 烦死的一天
关于使用进程+信号量编写的三个同步互斥实验
https://www.bilibili.com/video/BV1YE411D7nH?from=search&seid=11439484532605724987 王道考研里有讲
生产者消费者【作业要求多生产者多消费者 我估计是写错了】
就是这个 啊啊啊啊啊啊烦死了理解错了我还以为。。。唉😔
那就看b站好啦
--------------------------waitwaitwait:吸烟者问题 一个生产者三个消费者
每个吸烟者连续不断做烟卷并抽他做好的烟卷,做一支烟卷需要烟草、纸、火柴三种原料,这3个吸烟者分别掌握有烟草、纸和火柴;
经销商源源不断地提供上述三种原料,但他只将其中的两种原料放在桌上,具有另一种原料的吸烟者就可做烟卷并抽烟,且在做完后给经销商发信号,然后经销商再拿出两种原料放在桌上,如此反复。
[问题的精华是如何实现“轮流让各个吸烟者吸烟”,解决方案是使用一个整形变量i来实现这个“轮流”过程的。 https://www.cnblogs.com/wkfvawl/p/11534452.html ]
读者写者问题
分为三类问题:读者优先,写者优先 ,读写公平【实验报告里写的是读者写者公平】
Ø
读优先:要求指一个读者试图进行读操作时,如果这时正有其他读者在进行操作,他可直接开始读操作,而不需要等待。
读者优先的附加限制:如果一个读者申请进行读操作时已有另一读者正在进行读操作,则该读者可直接开始读操作。
Ø
写优先:一个读者试图进行读操作时,如果有其他写者在等待进行写操作或正在进行写操作,他要等待该写者完成写操作后才开始读操作。
写者优先的附加限制:如果一个读者申请进行读操作时已有另一写者在等待访问共享资源,则该读者必须等到没有写者处于等待状态后才能开始读操作。
写者优先的话说白了就是提升写进程的优先级【读写进程都申请执行时写进程优先 读进程正读的时候写进程申请 等到读进程处理完共享资源立马让给写进程】
https://blog.csdn.net/weixin_41413441/article/details/80551237 大佬讲的非常不错
https://blog.csdn.net/william_munch/article/details/84256690 另一个大佬的 明了
【总结:增加两个信号量和一个计数器】
哲学家进餐问题
这个问题可以用来解释死锁和资源耗尽。
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)
例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(互相等着一个人放下刀叉 系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。
问题解法
服务生解法 [是因为哲学家们从不交谈 那就找一个人来]
一个简单的解法是引入一个餐厅服务生,哲学家必须经过他的允许才能拿起餐叉。因为服务生知道哪只餐叉正在使用,所以他能够作出判断避免死锁。
为了演示这种解法,假设哲学家依次标号为A至E。如果A和C在吃东西,则有四只餐叉在使用中。B坐在A和C之间,所以两只餐叉都无法使用,而D和E之间有一只空余的餐叉。假设这时D想要吃东西。如果他拿起了第五只餐叉,就有可能发生死锁。相反,如果他征求服务生同意,服务生会让他等待。这样,我们就能保证下次当两把餐叉空余出来时,一定有一位哲学家可以成功的得到一对餐叉,从而避免了死锁。
资源分级解法
另一个简单的解法是为资源(这里是餐叉)分配一个偏序或者分级的关系,并约定所有资源都按照这种顺序获取,按相反顺序释放,而且保证不会有两个无关资源同时被同一项工作所需要。在哲学家就餐问题中,资源(餐叉)按照某种规则编号为1至5,每一个工作单元(哲学家)总是先拿起左右两边编号较低的餐叉,再拿编号较高的。用完餐叉后,他总是先放下编号较高的餐叉,再放下编号较低的。在这种情况下,当四位哲学家同时拿起他们手边编号较低的餐叉时,只有编号最高的餐叉留在桌上,从而第五位哲学家就不能使用任何一只餐叉了。而且,只有一位哲学家能使用最高编号的餐叉,所以他能使用两只餐叉用餐。当他吃完后,他会先放下编号最高的餐叉,再放下编号较低的餐叉,从而让另一位哲学家拿起后边的这只开始吃东西。
尽管资源分级能避免死锁,但这种策略并不总是实用的,特别是当所需资源的列表并不是事先知道的时候。例如,假设一个工作单元拿着资源3和5,并决定需要资源2,则必须先要释放5,之后释放3,才能得到2,之后必须重新按顺序获取3和5。对需要访问大量数据库记录的计算机程序来说,如果需要先释放高编号的记录才能访问新的记录,那么运行效率就不会高,因此这种方法在这里并不实用。
这种方法经常是实际计算机科学问题中最实用的解法,通过为分级锁指定常量,强制获得锁的顺序,就可以解决这个问题。
Chandy/Misra解法
1984年,K. Mani Chandy和J. Misra提出了哲学家就餐问题的另一个解法,允许任意的用户(编号P1, ..., Pn)争用任意数量的资源。与迪科斯彻的解法不同的是,这里编号可以是任意的。
1.对每一对竞争一个资源的哲学家,新拿一个餐叉,给编号较低的哲学家。每只餐叉都是“干净的”或者“脏的”。最初,所有的餐叉都是脏的。
2.当一位哲学家要使用资源(也就是要吃东西)时,他必须从与他竞争的邻居那里得到。对每只他当前没有的餐叉,他都发送一个请求。
3.当拥有餐叉的哲学家收到请求时,如果餐叉是干净的,那么他继续留着,否则就擦干净并交出餐叉。
4.当某个哲学家吃东西后,他的餐叉就变脏了。如果另一个哲学家之前请求过其中的餐叉,那他就擦干净并交出餐叉。
这个解法允许很大的并行性,适用于任意大多问题。
一些正常的在实验报告里
想说的是忽略的几个延伸拓展点:
活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。
期中考试题目里哲学家吃不上饭是有两种情况:死锁(都拿着一个筷子等待另一个人放下筷子) 活锁(拿了 发现另一个拿不到 一起又放下这只筷子 过一会再一起拿)
--------------------------waitwaitwait:理发师问题
2.理发师问题:一个理发店有一个入口和一个出口。理发店内有一个可站5 位顾客的站席
区、4 个单人沙发、3 个理发师及其专用理发工具、一个收银台。新来的顾客坐在沙发上等
待;没有空沙发时,可在站席区等待;站席区满时,只能在入口外等待。理发师可从事理
发、收银和休息三种活动。理发店的活动满足下列条件:
1)休息的理发师是坐地自己专用的理发椅上,不会占用顾客的沙发;
2)处理休息状态的理发师可为在沙发上等待时间最长的顾客理发;
3)理发时间长短由理发师决定;
4)在站席区等待时间最长的顾客可坐到空闲的理发上;
5)任何时刻最多只能有一个理发师在收银。
试用信号量机制或管程机制实现理发师进程和顾客进程。 https://blog.csdn.net/duanzhengbing/article/details/52141699
semget:https://blog.csdn.net/ta893115871/article/details/7505560
Segmentation fault (core dumped): 我还是算了吧 https://www.cnblogs.com/kuliuheng/p/11698378.html
讲真 说实话 我感觉我这三个作业写得emmm 是信号量的特有原因嘛?感觉我就是在套模板 信号量的一些基础配置先写上【union/sem_p/sem_v/init/del/main里初始化删除】--再创建几个进程--主要函数里写几个【注意点】的pv先后操作TT就完了 啊啊啊啊啊啊啊啊啊啊啊
作业四要求 银行家算法 Dijkstra
哪都有你
要求延伸 为什么输入法延伸都打不对啊 还是看那个b站视频