一、 实验目的
-
理解信号量的基本概念,以及其在进程同步中的作用
-
学习使用信号量来实现进程之间的同步
-
理解并解决多种并发时可能出现的问题
二、 实验过程
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信号,如此就实现了对缓冲区资源的互斥访问,生产者写,消费者用,二者互不干扰,有秩序地执行:
五、 心得体会
-
通过这个实验我更为深入地理解了信号量的相关原理
-
我理解了信号量及信号量相关函数实现的底层原理
-
通过自己模拟教材中的并发问题,我对并发有了更深刻的理解,也使得我将来能够更好地解决这些问题