0.写在前面(重点)
由于一些突发事件,导致目前大家手里或多或少都有了完整版的答案了。甚至很多学长学姐们写的代码远比我写的要好很多。
但是这个系列我觉得还是稍微坚持下去一点,或许某些地方可以帮到未来的同学们。
还是那句话,有需要可以随时向我反馈你遇到的问题,你的指点就是我最大的动力
1.实验代码解析
注意,这个实验比较特殊,不是想以前一样,直接从nachos源码文件中复制文件到本目录下,然后做拓展。而是重新创建一个新的文件,实现一些功能。
这个文件叫什么,local文件中新增的条目已经向我们指明了,threadsbar.cc()函数。
而且这个函数需要重写什么东西,也很容易了解了,就在这个文件中
至于实验大纲中提到的,关于那个n屏障,我酱在下一个小目录中实现。
2.手把手攻略
这里要说一下n屏障机制
N线程屏障(N-thread barrier)是一种同步机制,用于确保多个线程在达到某个点之前都会被阻塞,直到所有线程都到达该点后才能继续执行。它提供了一种线程同步的方式,以便在多线程环境中协调并发操作。
N线程屏障中的N表示参与屏障同步的线程数量。当N个线程都到达屏障位置时,屏障将打开,所有线程将被释放并可以继续执行后续的操作。如果有任何一个线程到达屏障位置之前发生阻塞(未到达屏障位置),则其他线程也会被阻塞,直到所有线程都准备好后才会继续执行。
而对于这个代码段,我估计写过go语言的同学都不陌生
rendezvousmutex . wait ()
count = count + 1
mutex . signal ()if count == n: barrier . signal ()barrier . wait ()
barrier . signal ()critical point
这是一个比较典型的创建n屏蔽的过程
首先mutex在go语言中,我们经常称之为互斥锁,互斥锁内的资源,每次之允许一个线程进行访问,其他的线程将会存入队列中等待被执行。在nachos中,使用Semaphore实现了mutex这个东西。我们可以用图上的方式完成这些操作
而barrier其实也是通过Semaphore来实现的,在这里我们先不关心是如何实现的,具体的实现和定义我们会放在下面的代码中解释。barrier理想中的状态,是在代码的某处拦住所有的线程,当某个线程发送了signal这个指令的时候,其他被阻塞的线程就”跨过“自己当前所在的wait语句。
因此这一整段代码实现的就是,在第n个线程到达之前,前面n-1个线程都处在被阻塞的状态,第n个线程到达if语句以后,经过判断释放signal信号,让其他线程可以脱离。
而自身则可能会被wait洽住,但是此时已经”逃脱“的线程,重新发送了信号。这样让第n个县城也可以顺利脱困
(至于这个东西需要不需要时间机制。。。我想说的事barrier只是一个逻辑上的概念,尤其是在nachos上的模拟实现,所以底层是存在信号量这一资源的,所谓的”信号“也是通过资源数目来确定能否通行,比如barrier本质上是一个初始资源数目为0的semaphore对象,而mutex为初始资源数目为1的semaphore对象)
3.实验过程分析
首先说明,具体的实验流程和名称在这里先简写了,所以题目可能对不上,不过内容是大致一致的
3.1:分析说明nachos信号量如何实现
信号量主要依靠如下semaphore个类的实现
该类中使用P V两个函数来完成信号量增加和减少的原语操作
另外这个类中还使用了value来模拟”可支配资源“的数目,以及信号等待队列List的实现,这个队列中用来阻塞线程。
3.2:说明并发进程如何创建以及运行
在nachos中,并发进程的创建是通过for循环进行多重创建,生成数个可以并行的thread对象,如图所示
在nachos中,默认每个时刻只有一个线程在运行,因此如果需要调度的话需要Threads类中的yield,sleep,以及finish方法进行辅助。并且还需要scheduler中的这几个方法作为调度逻辑
并发的执行运行则可以在scheduler.cc中的方法看到
ReadyToRun负责进行从就绪转化为运行态
FindNextToRun方法负责将运行态转化为结束态,并且将下一个线程移入
Run方法负责直接执行线程
3.3,修改代码以及实现n屏障机制
在文件夹lab3下面,创建一个名为threadsbar.cc的cpp文件,如图所展示
在lab3文件夹下存在一个readme的文件,里面展示了一个代码框架,我们根据这个代码框架实现了如下的代码,在threadsbar.cc中
//事先说明,需要改动的其实就是这些东西
//我们根据信号量Semaphore这个类来实现互斥锁mutex,以及n线程屏障barrier//首先要导入一些新的包
#include <unistd.h>
#include <stdio.h>#include "copyright.h"
#include "system.h"
#include "synch.h"#define N_THREADS 20 // the number of threads
#define MAX_NAME 16 //设置最大长度“名称”
#define N_TICKS 1000 // the number of ticks to advance simulated time//创建线程队列
Thread *threads[N_THREADS];
char thread_names[N_THREADS][MAX_NAME]; //给每个线程队列都加上一个名字//提前设置好指针
Semaphore *barrier;
Semaphore *mutex;int count=0;void MakeTicks(int n) // advance n ticks of simulated time n个模拟时间推进
{ //说实话至少这个函数哥们是没看懂一点的。。。。int i; //首先是我不会操作系统,其次我不写cppIntStatus oldLevel; //根据代码框架的提示,在最开始就写好了这个开关来增加时间的方法oldLevel=interrupt->SetLevel(IntOff);for(i=0;i<n;i++){interrupt->SetLevel(IntOff);interrupt->SetLevel(IntOn);}interrupt->SetLevel(oldLevel);
}void BarThread(_int which) //下面这个其实就是基本实现了
{MakeTicks(N_TICKS);printf("Thread %d rendezvous\n", which);//这个东西被称作rendezvous循环什么的??乱七八糟的//总之就是首先由互斥锁保证,计数器是合理运行的mutex->P();count++; mutex->V();/*if(count==N_THREADS){printf("Thread %d is the last===================\n",which);barrier->V(); //}barrier->P(); //前面n-1个线程会被阻塞在这里barrier->V(); //会去解救原本的第n个线程*///好像出现的问题之一是有的线程会觉得自己是最后一个。。。。???没发现这种问题???//好的,加大力度,现在是线程数目为20的时候,随机种子为114514,会发生两个线程都认为自己是最后一个的情况//原因在于临界资源的访问,无论是读写,都应该加上一个互斥锁//在最开始的代码中,对于count的判断并没有使用互斥,导致了两个线程的同时访问//导致的错误:最后一个线程需要处理内存相关的东西,但是当有多个判断自己为最后一个线程的时候,可能会导致内存等其他临界区域发生问题//解决方法:对临界资源的读写仍然是加上互斥锁,如下图代码所示,即可解决这个问题//最后是关于随机种子的情况://rz并非无效,根据实验代码提供的提示,应该是为每个进程分配的时间片不足导致的//使用空循环耗时并没有解决实际问题:首先较少的空循环并不能起到拖延很多时间的作用,其次空循环是在用户状态下执行的任务,对整体的时钟没有什么改变效果//使用linux下的sleep则根本无法解决问题:因为sleep的作用是linux上的进程,而不是nachos上的“线程”,nachos在linux的视角下本身就是一个进程而已//makeTick的开关中断处理了这个问题,由于给的代码框架比较完善,所以在第一次就完善了这个方法//导致后面就没有出现这些问题//所以正确的解决方法就是在makeTick中,增加(根据题目给出的框架,我们默认使用1000来进行实验)//的开关次数,使用nachos内部的“进程”的开关中断进行模拟mutex->P();if(count==N_THREADS){printf("Thread %d is the last===================\n",which);barrier->V(); }mutex->V();barrier->P(); //前面n-1个线程会被阻塞在这里barrier->V(); //会去解救原本的第n个线程printf("Thread %d critical point\n", which);
}void ThreadsBarrier() //这个函数用来设置县城之间的同步/并发控制
{int i;DEBUG('t',"ThreadsBarrier"); //DEBUG函数使用来干啥的来着??//创建互斥锁以及屏障barrier=new Semaphore("barrier",0);//障碍的信号量设置为0,带波奥这是个屏障mutex=new Semaphore("mutex",1);//互斥锁的信号量设置为1,代表统一时刻只能有一个线程访问这个东西// Create and fork N_THREADS threads //在这里创造多个线程,也就是县城初始化的部分for(int i = 0; i < N_THREADS; i++) {//...............sprintf(thread_names[i],"thread_%d",i);//...............threads[i]=new Thread(thread_names[i]);threads[i]->Fork(BarThread, i);};
}
其中对于n屏蔽机制,如图所示(代码加上了一些注释,因为这段代码是需要后期改进的,这里是完成了第三步要求的展示。)
使用make指令进行编译,并且根据-rz指令设置随机种子
在这里我们直接使用随机种子114514,并且为了让结果更加明显,我们创建了而是个并发的县城,结果如下
使用指令
./nachos -rz 114514
结果如下
出现了明显的问题,进程0和进程1都认为自己是最后一个进程
这个现象的原因是因为,对于最后的count的读取判断,并没有是互斥枷锁,倒是了0和1都能读取到count=n的情况,导致二者均认为自己可以处理这个问题。
处理方法为:对于if的count判断,也要加上互斥锁
3.4:使用随机种子进行测试,可能会发生-rz失效的情况,如何处理,以及怎么处理
rz确实会发生”无效“的情况,根据实验代码提供的提示,应该是为每个进程分配的时间片不足导致的
1。使用空循环耗时并没有解决实际问题:首先较少的空循环并不能起到拖延很多时间的作用,其次空循环是在用户状态下执行的任务,对整体的时钟没有什么改变效果
2。使用linux下的sleep则根本无法解决问题:因为sleep的作用是linux上的进程,而不是nachos上的“线程”,nachos在linux的视角下本身就是一个进程而已
makeTick的开关中断处理了这个问题,由于给的代码框架比较完善,所以在第一次就完善了这个方法,导致在最开始的测试中就没有出现这些问题,所以正确的解决方法就是在makeTick中,增加(根据题目给出的框架,我们默认使用1000来进行实验)
如图所示,我们进一步填充了maketick函数的内容
void MakeTicks(int n) // advance n ticks of simulated time n个模拟时间推进
{ //说实话至少这个函数哥们是没看懂一点的。。。。int i; //首先是我不会操作系统,其次我不写cppIntStatus oldLevel; //根据代码框架的提示,在最开始就写好了这个开关来增加时间的方法oldLevel=interrupt->SetLevel(IntOff);for(i=0;i<n;i++){interrupt->SetLevel(IntOff);interrupt->SetLevel(IntOn);}interrupt->SetLevel(oldLevel);
}