Nachos实习——Lab1线程机制实习报告
文章目录
内容一:总体概述
本次Lab针对的内容是实现线程最基本的数据结构——进程控制块(PCB)和线程的调度机制,同时扩展实现时间片轮转调度算法。
内容二:任务完成情况
Exercise 1 | Exercise 2 | Exercise 3 | Exercise 4 | Exercise 5 | Exercise 6 | Challenge | |
---|---|---|---|---|---|---|---|
完成情况 | Y | Y | Y | Y | Y | Y | Y |
内容三:具体完成Exercise情况
Exercise 1 调研
调研Linux或Windows中进程控制块(PCB)的基本实现方式,理解与Nachos的异同。调研Linux或Windows中采用的进程/线程调度算法。
我的调研选择了Linux-2.6.0版本阅读
1、Linux进程控制块(PCB)
通过百度了解到Linux的进程控制块由一个task_struct的结构体定义。存放在文件/linux-2.6.0/include/linux/sched.h中,里面包含了众多信息,其中比较重要的信息:
-
标识符:主要包括进程标识符、用户标识符、组标识符、备份用户标识符、文件系统用户标识符等。
-
进程状态:进程状态是进城调度和交换的依据。
-
进程调度信息:这些包含进程优先级以及进程的类别等
-
进程通信相关的信息:Linux支持多种不同形式的通信机制。它支持典型的Unix通信机制:信号、管道,也支持System V通信机制:共享内存、信号量和消息队列
-
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
-
上下文环境:进程执行时处理器的寄存器中的数据
-
其他:还有其他一些必要信息
2、Linux进程控制块与Nachos的异同
Linux的进程控制块包含了比较详细的进程信息,而Nachos相对于Linux系统的PCB实现就要简单很多。
Nachos在Thread 类中实现了操作系统的线程控制块。Thread 线程控制块较Linux PCB 为简单的多,它没有线程标识 (pid)、实际用户标识 (uid)等和线程操作不是非常有联系的部分。Nachos仅仅定义了四个线程控制块的变量stackTop and stack
(表示当前进程所占的栈顶和栈底)、machineState
( 保留未在CPU上运行的进程的寄存器状态)、status
(表示当前进程的状态)以及一些最基本的对线程操作的函数,如Fork()、Sleep()等。
除此之外,Nachos线程的总数目没有限制,线程的调度比较简单,而且没有实现线程的父子关系等。
3、Linux采用的进程/线程调度算法
Linux内核的三种调度策略 :
-
SCHED_OTHER 分时调度策略,(默认的)
-
SCHED_FIFO实时调度策略,先到先服务
-
SCHED_RR实时调度策略,时间片轮转
Linux内核将进程分成两个级别:普通进程和实时进程。实时进程将得到优先调用,实时进程根据实时优先级决定调度权值,分时进程则通过nice和counter值决定权值,nice越小,counter越大,被调度的概率越大,也就是曾经使用了cpu最少的进程将会得到优先调度。
SHCED_RR和SCHED_FIFO的不同:
当采用SHCED_RR策略的进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。
SCHED_FIFO一旦占用cpu则一直运行。一直运行直到有 更高优先级任务到达或自己放弃 。
Exercise 2 源代码阅读
仔细阅读下列源代码,理解Nachos现有的线程机制。
-
code/threads/main.cc和code/threads/threadtest.cc
-
code/threads/thread.h和code/threads/thread.cc
Nachos现有的线程机制与Linux的进程机制比较相似,只是相对比较简单。
Nachos的线程有四种状态:
JUST_CREATED
RUNNING
READY
BLOCKED
和四个主要的方法Thread()
、Fork()
、Finish()
、Yield()
、Sleep()
。
Thread()
:构造函数,初始化一个新的Thread。Fork(VoidFunctionPtr func,int arg):func
,新线程运行的函数;分配一块固定大小的内存作为线程的堆栈,在栈顶放入 ThreadRoot 的地址。Finish()
:并不是直接收回线程的数据结构和堆栈,因为我们仍在这个堆栈上运行这个线程。做法是将threadToBeDestroyed的值设为当前线程,使得Scheduler的Run()可以调用销毁程序,当我们这个程序退出上下文时,将其销毁。Yield()
:用于本线程放弃处理机。Sleep()
:可以使当前线程转入阻塞态,并放弃 CPU, 直到被另一个线程唤醒,把它放回就绪线程队列
下面对每个文件进行了简单的说明:
1、code/threads/main.cc
该模块是整个 Nachos 系统的入口,它分析了 Nachos 的命令行参数,根据不同的选项进行不同功能的初始化设置。
2、code/threads/threadtest.cc
这是一个简单的线程实验的测试用例。用于指导我们如何对线程的修改进行测试的。
- testnum:测试号,对应相应的测试函数。
- SimpleThread():一个5次循环的程序,每次循环中都让出CPU,让其他就绪的线程执行。
- ThreadTest1():一个测试方法,创建两个线程,让他们都执行SimpleThread()方法,使这两个线程可以交替执行。
- ThreadTest():可以看做一个总控程序,根据main函数传过来testnum参数值来执行不同的测试程序。例如,当testnum==1时,就执行ThreadTest1()。
3、code/threads/thread.h
用于管理线程的数据结构。如线程控制块、线程的基本方法都在这个文件中被定义。
4、code/threads/thread.cc
实现了用于管理线程事务的具体方法。主要有四种操作:Fork 、Finish、Yield、Seelp。
Exercise 3 扩展线程的数据结构
增加“用户ID、线程ID”两个数据成员,并在Nachos现有的线程管理机制中增加对这两个数据成员的维护机制。
设计思路:
- 对于用户ID:现有的Nachos代码不支持多用户,所以我直接将用户ID设置为固定值1000。
- 对于线程ID:结合Exercise 4,我声明了一个容量为128的数组,初始值全0。每个元素的取值为0或1。0表示该数组下标没有作为线程的ID分配出去,1表示已分配。每次创建线程的时候都遍历一次数组,把第一个不为0的下标分配给线程作为ID。如果全为1,则返回线程ID为-1,表示不创建线程
1、threads/threads.h
在threads/threads.h中添加变量和方法
class Thread {
private:
int tid; //添加线程ID
int uid; //添加用户ID
public:
int getTid(); //获取线程ID
int getUid(); //获取用户ID
void setUid(); //设置用户ID
}
2、threads/system.h
由于系统的全局变量都声明和定义在system.h或system.cc中,我们也将数组定义在这里
#define MAX_THREAD_NUM 128 //根据Exercise 4将系统可容纳的线程数量设置为128
extern int tid_flag[MAX_THREAD_NUM]; //用来标识该ID是否被分配
注意:上面使用了extern定义tid_flag数组,是因为我们事先需要在system.cc文件中定义和初始化
3、threads/system.cc
在Initialize()函数中初始化数组
int tid_flag[MAX_THREAD_NUM];
void
Initialize(int argc, char **argv)
{
for (int i