Bootstrap

HNU-2024操作系统实验-Lab7-信号量与同步

一、 实验目的

  1. 理解信号量的基本概念,以及其在进程同步中的作用

  2. 学习使用信号量来实现进程之间的同步

  3. 理解并解决多种并发时可能出现的问题

二、 实验过程

1.实现信息量结构初始化

① 在lab7/src/include目录下新建prt_sem_external.h 头文件

在这里插入图片描述
在这里插入图片描述

下面对该板块进行分析:

这个代码板块主要是实现了一个信号量管理模块的接口,包含了信号量的状态、类型、模式的宏定义、控制块结构体、全局变量和操作信号量的函数。

首先对宏定义进行分析:

  • OS_SEM_UNUSED 和 OS_SEM_USED 用来标识信号量是否在使用。

  • SEM_PROTOCOL_PRIO_INHERIT 表示优先级继承协议。

  • SEM_TYPE_BIT_WIDTH 和 SEM_PROTOCOL_BIT_WIDTH 定义了信号量类型和协议的位宽。

  • MAX_POSIX_SEMAPHORE_NAME_LEN 定义了POSIX信号量名称的最大长度。

  • GET_SEM_LIST 等宏用来操作信号量列表和获取信号量相关信息。

最后定义了一些变量,同时声明了一些外部定义的函数。

② 在lab7/src/kernel/sem目录下新建prt_sem_init.c 文件

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这段代码主要是定义了信号量的初始化函数,分别分析这三个函数的功能:

  • OsSemInit:这个函数是对信号量管理模块的初始化,它首先调用 OsMemAllocAlign函数分配对齐的内存,用于存储信号量控制块,然后计算最大信号量数量 g_maxSem,同时将分配的内存清零,确保信号量控制块的初始状态为0,最后初始化空闲信号量列表g_unusedSemList、遍历所有信号量控制块,初始化其ID,并将其添加到空闲信号量列表中。

  • OsSemCreate:这个函数是信号量创建函数,它首先检查参数sem_handle信号量句柄(响应的信号)是否不为空,若为空则返回错误,然后关闭中断,防止创建信号量时引发中断,接着从空闲信号量表中取出第一个空闲块,并对其进行初始化,设置信号量的初始值、状态、模式、类型和拥有者,然后根据信号量的类别semType来进行相应操作,若为二进制量,则初始化相关字段,最后将信号量ID以地址间接访问的方式赋值给semHandle,打开中断,完成信号量的初始化。

  • PRT_SemCreate:这个函数是一个封装函数,首先排除不合理的count数,然后调用OsSemCreat函数进行信号量创建。

③ 在src/bsp目录中的os_cpu_armv8_external.h文件加添加定义

在这里插入图片描述

④ 在src/kernel/sem目录下新建prt_sem.c 文件。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这一部分主要是定义了一系列检查等辅助函数,其中OsSemPostErrorCheck函数和OsSemPendParaCheck函数用于检查post操作和pend操作是否有效,接着对其中较为复杂的函数进行分析:

  • OsSemPendListPut函数:该函数将当前运行任务挂接到信号量的等待链表上,首先,此函数将当前运行的任务从就绪列表中移除,然后根据信号量的等待模式(优先级或FIFO),将任务添加到信号量的等待列表中。如果是优先级模式,并且当前任务的优先级高于等待列表中的某些任务,则将其插入到对应合适的位置,否则以FIFO方式添加到列表末尾。

  • OsSemPendListGet函数:此函数首先从信号量的等待列表中取出第一个任务,并将其放入就绪队列,如果取出的任务之前设置了定时等待标志(OS_TSK_TIMEOUT),则清除该标志并从定时等待列表中移除。

接下来分析本实验的两个重点核心函数:PRT_SemPend与PRT_SemPost:

在这里插入图片描述

PRT_SemPend函数实现等待信号相应操作,其两个参数分别代表需要响应的信号以及等待时间,进入函数,本函数首先判断semHandle是否有效,若无效则直接返回错误,接着利用GET_SEM宏获取该信号对应的信号控制块,然后禁用中断,防止在执行Pend操作时引发中断,保证该操作的原子性,然后根据信号控制块对信号量状态、中断活动、任务调度情况以及超时参数进行检查,检查完毕后将该任务挂起,置于信号量等待链表中,直到信号量被释放,即收到该信号的Post操作,若等待时间不为永久,则判断是否超时,超时则返回错误,若等待时间为永久,则调用快速调度函数OsTskScheduleFastPs进行调度,顺利完成后恢复中断并正常返回。

在这里插入图片描述

PRT_SemPost函数与PRT_SemPend函数的检查准备操作非常类似,只是最后一步操作是执行OsSemPostSchePre函数从阻塞队列移除对应信号,并将其加入到信号就绪队列,准备进行任务调度,然后通过OsTskScheduleFastPs函数进行快速任务调度。

⑤ 在src/include目录下的prt_task_external.h文件中加入 OsTskReadyAddBgd()

在这里插入图片描述

⑥ 在src/kernel/task目录中的prt_task.c文件加入 OsTskScheduleFastPs()

在这里插入图片描述

⑦ src/bsp/os_cpu_armv8_external.h 加入 OsTaskTrapFastPs()

在这里插入图片描述

⑧ 在src/include目录中加入prt_sem.h文件

⑨ 最后将所有的新文件加入构建系统

在这里插入图片描述

在这里插入图片描述

至此,信号系统已构建完毕

三、 测试及分析

在main函数中执行任务1与任务2:

正常运行,符合预期情况

四、 Lab7作业

各种并发问题模拟,至少3种。

1.违反原子性缺陷

在这里插入图片描述

观察此段代码,可以发现线程1、线程2都要访问公共指针变量ptr,第一个线程检查指针不为空,然后打印出指针指向的值,第二个线程则是将指针设置为空,对于线程一的操作违反了原子性,就有可能当检查完ptr非空之后,在PRT_Printf函数调用之前引发时钟中断,第二个线程将指针设置为空,当第一个线程恢复执行时,引用空指针导致程序崩溃。(此处随机访问地址空间并打印出其中的值)

在这里插入图片描述

解决方法:给共享变量ptr加锁,确保每个线程访问该变量时都持有锁

在这里插入图片描述

在这里插入图片描述

正常访问执行

2.违反顺序缺陷

在这里插入图片描述

这段代码中线程二的执行已经假定指针ptr已经被初始化,即默认线程一在线程二之前执行,但是一旦线程二优先执行,就会因为引用空指针而产生崩溃

在这里插入图片描述

解决方法:利用信号量,让线程二使用本实验定义的函数Pend,线程一使用函数Post,这样即使线程二优先执行,也会等待线程一初始化指针ptr之后,释放信号量才能继续执行

在这里插入图片描述

在这里插入图片描述

保证了线程一在线程二前运行

3.死锁

在这里插入图片描述

第一个线程持有锁1,正在等待锁2,第二个线程持有锁2,正在等待锁1,死锁就产生了,两个线程互相等待,无法正常继续运行

在这里插入图片描述

4.哲学家进餐问题

在这里插入图片描述

此处模拟的是教材《操作系统导论》中的一个非常有趣的问题–哲学家进餐问题,书中的例子是每个哲学家要进食,必须先拿起左手的筷子,再拿起右手的筷子,同时这些哲学家围成一圈,拿筷子的动作是并发的,这时当所有哲学家拿起左手边的筷子之后,由于每个哲学家的右手筷子是其他哲学家的左手筷子,因此所有哲学家无法再拿起右手的筷子,陷入死锁,此处给出的是解决方案:即让最后一个哲学家先拿右手的筷子,破坏死锁产生的必要条件循环等待,这样就成功让所有哲学家顺利进食:

在这里插入图片描述

5.生产者消费者问题

在这里插入图片描述

这里模拟的是教材中的另一个经典问题:生产者消费者问题,在本程序中,消费者会先进入缓冲区,但由于此时缓冲区为空,消费者阻塞等待生产者的sem_full信号,这时生产者进入缓冲区,往缓冲区中写入内容,此时缓冲区已满,故生产者向消费者发送sem_full信号,自己阻塞,等待消费者将缓冲区内容读完后发送sem_empty信号,如此就实现了对缓冲区资源的互斥访问,生产者写,消费者用,二者互不干扰,有秩序地执行:

在这里插入图片描述

五、 心得体会

  1. 通过这个实验我更为深入地理解了信号量的相关原理

  2. 我理解了信号量及信号量相关函数实现的底层原理

  3. 通过自己模拟教材中的并发问题,我对并发有了更深刻的理解,也使得我将来能够更好地解决这些问题

;