nachos实验记录
- 实验一
- 分析threads文件夹内容
- thread
- scheduler
- 初始化函数Initialize()的工作
- main()函数的工作
- gdb基本使用
- 实验三
- 分析synch文件
- 实验内容
实验一
分析threads文件夹内容
思路:通过thread类中的方法,来调用schedule类中的调度函数,实现线程的创建,就绪,运行,阻塞和结束五种状态的转换。
thread
- 创建线程:fork()。首先为线程分配栈资源(StackAllocate()),然后将线程状态设为就绪态(setStatus()),并放入就绪队列中(Append()),等待被调度即可。
- 线程阻塞:sleep()。首先将状态设置为阻塞态,然后从就绪队列中寻找新的线程来调度(FindNextToRun()),如果就绪队列中有线程就可以执行该线程(run()),如果就绪队列中没有线程则阻塞等待时钟中断到来(idle())。
- 线程切换:yield()。 从就绪队列中寻找新的线程来调度(FindNextToRun()),如果就绪队列中有线程就可以执行该线程(run()),同时将当前运行的线程放入就绪队列中(ReadyToRun());如果就绪队列中没有线程,则修改状态后直接退出即可。(区别于sleep(),yield()不会进入阻塞等待状态)
- 线程结束:finish()。先将线程标记为待删除的线程(Run()中进行删除),然后直接调用sleep()即可。为什么不调用yield():因为yield()虽然也释放cpu资源,但却将线程再次放入就绪队列中,而线程结束则意味着线程不需要再使用cpu,因此不应该再放入就绪队列中,调用sleep()则正好满足要求。
scheduler
- 寻找下一个运行的线程:FindNextToRun()。查看就绪队列中是否还有线程,如果有就从就绪队列中删除这个线程,并将其返回;如果没有线程则返回NULL。
准备执行线程:ReadyToRun()。本质上就是创建到就绪态的转换,先将线程状态设为就绪态(setStatus()),然后将其放入就绪队列中即可(Append())。 - 执行线程:Run()。本质上就是就绪态到运行态的转换,先将线程状态设为运行态(setStatus()),然后进行上下文的切换(SWITCH()),最后判断被调度下去的线程是否需要销毁(finish()中进行的设置),如果要销毁那么就进行destory。
初始化函数Initialize()的工作
这个属于system类,即当执行一个main()函数时,操作系统自己进行的初始化工作。首先进行硬件资源如磁盘和网络设备的初始化工作,这里频繁使用ifdef,endif的搭配组合,工程项目中须要掌握这点。之后进行中断,调度队列,时钟的初始化。然后便可以创建main()主线程,并将其设为运行状态。
main()函数的工作
注意,这个main()函数并不是我们写的main()函数,而是nachos操作系统运行的内核程序,因为操作系统本质上也是一个程序,而nachos作为一个模拟的操作系统也自然需要去运行。main()函数首先调用Initialize()函数进行初始化工作,需要关注的是之后调用了ThreadTest()函数做测试,其中又调用了fork()创建了新的线程,并用新的线程和主线程均调用了SimpleThread()函数,但是传递的参数却不同,因此执行结果自然不同。
gdb基本使用
- 查看函数地址:首先list,然后找到要查看的函数位置,利用break设置断点即可。
- 查看线程对象地址:首先list,找到创建线程的代码位置,并利用break在创建线程返回值的位置设置断点,然后用run命令运行线程,到达这个位置时会暂停,此时利用p查看线程地址即可。
- 查看汇编代码的返回地址:可以用break在函数入口打断点,然后利用disass查看汇编代码,由于函数返回时一定会将返回地址放入寄存器中,则可以在返回的汇编代码的下一步中,利用info r查看寄存器内容,从而得到返回地址。需要注意的是,s和n命令都是c语言级的,想要单步执行汇编代码,需要利用si和ni,这些命令是汇编级的。
实验三
分析synch文件
首先需要了解的是,list文件中自己实现了链表节点,并基于此实现了链表类,然后在synch文件中利用这个链表实现了信号量,互斥锁和条件变量。其中信号量主要是维护了一个等待当前信号量的线程队列和信号量的值,而互斥锁则是维护了一个大小为1的信号量和锁的拥有者,由于本次实验我们使用信号量和互斥锁来实现生产者消费者模型,因此条件变量不在此详述,只需知道条件变量和互斥锁配合也完全可以实现生产者消费者模型,这其实可以作为线程池的实现方式。
- P()操作:为了实现线程安全,信号量的操作必须是原子性的,而保证原子性的方法就是关掉中断,即禁止了线程切换。然后判断当前信号量的值是否为0,若为0则需要加入队列,并进入阻塞状态;若不为0则可以将信号量值减一,重新打开中断即可。
- V()操作:依然需要先关闭中断,然后判断当前队列中是否有等待的线程,若有则将其从等待队列中删除,并准备执行(ReadyToRun())。最后将信号量值加一,并打开中断。
- Acquire()操作:首先依然是关闭中断,然后修改锁的持有者,并直接调用P()操作即可,最后记得重新打开中断。
- Release()操作:首先关闭中断,然后锁的持有者设为NULL,并直接调用V()操作即可,最后记得重新打开中断。
实验内容
缓冲区是在ring文件中实现的,就是一个简单的数组,并用in和out标记当前插入和取出的位置。还需要注意的是由于是环形缓冲区,因此需要取余。
需要填充的只有prodcons文件,首先明确的是由于缓冲区是临界区,因此需要互斥锁;并且缓冲区的大小有限,不可能无限取,也不可能无限填充,因此需要生产者和消费者进行同步的信号量。因此完成的工作主要是信号量和互斥锁的初始化,以及生产者的填入数据和消费者的取出数据工作。由于初始时默认缓冲区大小为空,因此对应的取出信号量为0,而填入信号量则为缓冲区大小。此时消费者会进入阻塞状态,等待生产者填入数据。
当生产者填入数据时首先遵循先同步后互斥的原则,先调用填入信号量的P()操作,然后调用加互斥锁。然后将数据填入缓冲区中,最后再解锁和调用取出信号量的V()操作即可。
消费者取出数据道理也类似,先调用取出信号量的P()操作,再加锁。将数据取出后,再解锁和调用填入信号量的V()操作即可。