02_实验二_线程状态及转换
实验二 线程状态及转换
实验目的
- 调试线程在各种状态间的转换过程,熟悉线程的状态和转换。
- 通过为线程增加挂起状态,加深对线程状态的理解。
预备知识
线程都有哪些状态
- 就绪
- 运行
- 阻塞
- 结束
EOS 是如何定义这些状态
线程在其整个生命周期中(从创建到终止)会在多个不同的状态间进行转换。EOS 线程的状态由线程控制块 TCB 中的 State 域保存,在文件 ps/psp.h 中定义的线程状态如下:
typedef enum _THREAD_STATE {
Zero, // 0线程状态转换过程中的中间状态
Ready, // 1就绪
Running, // 2运行
Waiting, // 3等待(阻塞)
Terminated // 4结束
} THREAD_STATE;
线程之间的转换函数
在 EOS 中,线程在不同的状态间相互转换时,最终都是通过调用 ps/sched.c 文件中的下面几个函数来完成的:
PspReadyThread:将指定线程插入其优先级对应的就绪队列的队尾,并修改其状态码为 Ready。除了当前运行线程因被抢先而进入就绪状态的情况外,其它任何情况下,都是通过调用此函数使线程进入就绪状态的。
PspUnreadyThread:将指定线程从就绪队列中移除,并修改其状态码为 Zero。无论是要将就绪线程转入运行状态,还是要结束处于就绪状态的线程,都必须先调用这个函数使线程脱离就绪状态。
PspWait:将当前运行线程插入指定等待队列的队尾,并修改状态码为 Waiting,然后执行线程调度,让出处理器。通过 PspWait 的第二个参数可以指定等待的时限,如果等待超时则会被系统自动唤醒进入就绪状态。当前运行的线程都是调用这个函数而进入阻塞状态的
PspUnwaitThread:将阻塞线程从其所在的等待队列中移除,并修改其状态码为 Zero。不管是因事件条件满足或等待超时而使阻塞线程进入就绪状态,还是要结束处于阻塞状态的线程,都必须先调用这个函数使线程脱离阻塞状态。
PspWakeThread:该函数会先调用 PspUnwaitThread 函数使线程脱离阻塞状态,然后再调用PspReadyThread 函数使线程进入就绪状态,从而唤醒被阻塞的线程。线程被唤醒后将从函数 PspWait 返回并继续运行。PspWakeThread 的第二个参数将作为被唤醒线程从 PspWait 返回时的返回值。
PspSelectNextThread:线程调度程序函数,使被抢先的线程从运行状态进入就绪状态,并决定哪个就绪线程应该进入运行状态。任何线程进入运行状态都是这个函数执行的结果。
挂起状态,以及 Suspend 和 Resume 原语
为线程添加挂起状态,这就不可避免地涉及到了活动状态与挂起状态的转化,因此引入了两个原语:Resume 与 Suspend.
- Resume 负责将挂起状态的线程重新恢复到活动状态
- Suspend 负责将活动状态的线程转入挂起状态
ps/psspnd.c 文件中(实验一的文件夹里面),定义了一个线程队列用于保存处于静止就绪状态的线程控制块(TCB),并已经将队列初始化为空:
LIST_ENTRY SuspendListHead = {&SuspendListHead, &SuspendListHead};
在该文件中,已经为原语 Suspend 实现了对应的函数 PsSuspendThread:
STATUS
PsSuspendThread(
IN HANDLE hThread
)
/*++
功能描述:
挂起指定的线程。目前只能将处于就绪状态的线程挂起。
参数:
hThread - 需要被挂起的线程的句柄。
返回值:
如果成功则返回 STATUS_SUCCESS。
--*/
{
STATUS Status;
BOOL IntState;
PTHREAD Thread;
//
// 根据线程句柄获得线程对象的指针
//
Status = ObRefObjectByHandle(hThread, PspThreadType, (PVOID*)&Thread);
if (EOS_SUCCESS(Status)) {
IntState = KeEnableInterrupts(FALSE); // 关中断
if (Ready == Thread->State) {
//
// 将处于就绪状态(Ready)的线程从就绪队列中移除,
// 从而使该线程进入游离状态(Zero)。
//
PspUnreadyThread(Thread);
//
// 将处于游离状态的线程插入挂起线程队列的末尾,
// 挂起线程的操作就完成了。线程由活动就绪状态(Active Ready)进入静止就绪状态(Static Ready)。
//
ListInsertTail(&SuspendListHead, &Thread->StateListEntry);
Status = STATUS_SUCCESS;
} else {
Status = STATUS_NOT_SUPPORTED;
}
KeEnableInterrupts(IntState); // 开中断
ObDerefObject(Thread);
}
return Status;
}
在此函数中通过关闭中断实现了原语操作,在原语操作中确定要挂起的线程是处于活动就绪状态(Ready)后:
- 调用 PspUnreadyThread 函数将线程从就绪队列中移除,此时线程处于游离状态(Zero)
- 调用 ListInsertTail 函数将处于游离状态的线程插入挂起线程队列的末尾,从而完成对线程的挂起操作。
PsSuspendThread 函数只能挂起处于活动就绪状态的线程,对于挂起处于运行或者阻塞状态的线程的工作留给读者来完成。
原语 Resume 对应的函数 PsResumThread 也已经在此文件中实现,但是,将处于静止就绪状态的线程恢复为活动就绪状态的代码还没有编写完毕,此处也留给读者来实现。
STATUS
PsResumThread(
IN HANDLE hThread
)
/*++
功能描述:
恢复指定的线程。
参数:
hThread - 需要被恢复的线程的句柄。
返回值:
如果成功则返回 STATUS_SUCCESS。
--*/
{
STATUS Status;
BOOL IntState;
PTHREAD Thread;
//
// 根据线程句柄获得线程对象的指针
//
Status = ObRefObjectByHandle(hThread, PspThreadType, (PVOID*)&Thread);
if (EOS_SUCCESS(Status)) {
IntState = KeEnableInterrupts(FALSE); // 关中断
if (Zero == Thread->State) { //如果处于游离态
//
// 在此添加代码将线程恢复为就绪状态
ListRemoveEntry(&)
//
Status = STATUS_SUCCESS;
} else {
Status = STATUS_NOT_SUPPORTED;
}
KeEnableInterrupts(IntState); // 开中断
ObDerefObject(Thread);
}
return Status;
}
进程的同步和通信
基本概念
临界资源 指多个并发进程可以同时访问的硬件或软件资源
临界区(Critical Section) 访问临界资源的代码
互斥体(Mutex) 为保证各进程互斥进入临界区而引入的信号量
**进程同步 ** 的主要任务是使并发执行的各进程之间能有效的共享资源和相互合作。可以使用互斥体(Mutex)、信号量(Semophore)和事件(Event)等同步对象来解决一系列经典的进程同步问题,例如“生产者-消费者问题”、“读者-写者问题”、“哲学家进餐问题”等。
EOS 内核同步
EOS 中的线程属于内核级,所有线程并发执行,不区分所属进程
EOS 提供了三种同步对象:
- 互斥对象(Mutex)
- 信号量对象(Semaphore)
- 事件对象(Event)
这三种同步对象都涉及到两个状态 signaled
和 nosignaled
,进程间同步本质上是对这两个状态进行切换,不同对象有不同的切换逻辑.EOS 内核为同步对象提供了统一的 Wait 操作接口函数:WaitForSingleObject,其 C 语言函数在文件 inc/eos.h 中声明如下:
EOSAPI
ULONG
WaitForSingleObject(
IN HANDLE Handle,//Wait 操作对象的句柄,可以是互斥体、信号量、事件、线程、进程等任意支持同步功能的内核对象
IN ULONG Milliseconds//等待的最长时间。时间终了,即使等待的对象未变为 signaled 状态,此函数也要返回。如果此值为 0,函数测试对象的状态并立刻返回。如果此值为 INFINITE,函数永远阻塞等待直到对象变为 signaled 状态。设定合理的超时值,可以有效避免线程死锁。
);
Mutex 对象(互斥型信号量结构体)
Mutex 对象用于同步多个线程对临界资源的互斥访问。用于定义 Mutex 对象的结构体在文件 inc/ps.h 文件中定义如下:
//
// 互斥信号量结构体
//
typedef struct _MUTEX {
PVOID OwnerThread; // 当前拥有 Mutex 的线程指针
ULONG RecursionCount; // 递归拥有 Mutex 的计数器
LIST_ENTRY WaitListHead; // 等待队列
}MUTEX, *PMUTEX;
当持有线程指针为 NULL 时,Mutex 不被任何线程持有,此时 Mutex 处于 signaled 状态;当指针指向持有 Mutex 的线程时,Mutex 处于 nonsignaled 状态。当一个线程对 Mutex 成功调用了 WaitForSingleObject 后,Mutex 的线程指针就会指向此线程,此线程就持有了 Mutex,Mutex 变为 nonsignaled 状态。
线程再对同一个 Mutex 调用 WaitForSingleObject,将会被阻塞在 Mutex 的等待队列中,直到持有该 Mutex 的线程调用 ReleaseMutex 释放 Mutex 使之变为 signaled 状态。Mutex 还支持递归,持有该 Mutex 的线程可以对该 Mutex 多次调用 WaitForSingleObject 而不被阻塞,递归计数器记录了持有线程调用 WaitForSingleObject 的次数。只有该 Mutex 的持有线程可以对该 Mutex 调用 ReleaseMutex,持有线程每调用一次 ReleaseMutex,该 Mutex 的递归计数器减小 1,当计数器变为 0 时,该 Mutex 的持有线程指针被设置为 NULL,变为 signaled 状态。
不知所言
Mutex 对象的 Create 函数在文件 inc/eos.h 中声明:
EOSAPI
HANDLE
CreateMutex(
IN BOOL InitialOwner,//为 TRUE 则初始化新建 Mutex 对象的持有线程为当前调用线程,为FALSE 则初始化新建 Mutex 对象的持有线程为 NULL。如果CreateMutex 执行的是打开已存在的命名 Mutex 对象则忽略此值。
IN PCSTR Name
);
EOSAPI
BOOL
ReleaseMutex(
IN HANDLE Handle
);
Semaphore(记录型信号量对象)
EOS 内核提供的 Semaphore 对象是典型的记录型信号量。当 Semaphore 对象中的整形变量大于 0 时,其处于 signaled 状态,当整型变量小于等于 0 时,其处于 nonsignaled 状态。不同于标准记录信号量,EOS 内核提供的 Semaphore 对象还记录了其整型变量的最大取值范围。用于定义 Semaphore 对象的结构体在文件 inc/ps.h 文件中定义如下:
//
// 记录型信号量结构体
//
typedef struct _SEMAPHORE {
LONG Count; // 信号量的整形值
LONG MaximumCount; // 允许最大值
LIST_ENTRY WaitListHead; // 等待队列
}SEMAPHORE, *PSEMAPHORE;
//创建信号量
EOSAPI
HANDLE
CreateSemaphore(
IN LONG InitialCount,
IN LONG MaximumCount,
IN PSTR Name
);
记录型信号量大于 0,代表可使用的临界资源的数量。
Event 对象
Event 对象是 EOS 中最具弹性的同步对象,Event 的两种状态 signaled 和 nonsignaled 可完全由程序控制,使用非常灵活。用于定义 Event 对象的结构体在文件 inc/ps.h 文件中定义如下:
//
// 事件结构体
//
typedef struct _EVENT {
BOOL IsManual; // 是否手动类型事件
BOOL IsSignaled; // 是否处于 Signaled 状态
LIST_ENTRY WaitListHead; // 等待队列
}EVENT, *PEVENT;
Event 调用 SetEvent 可使之变为 signaled 状态,调用 ResetEvent 可使之复位为 nonsignaled 状态。Event 分为手动和自动两种类型。线程对手动类型 Event 调用 WaitForSingleObject 并成功返回会时,Event 保持 signaled 不改变,所有线程再对 Event 调用 WaitForSingleObject 都将立即成功返回,除非手动调用 ResetEvent 使之复位为 nonsignaled 状态。线程对自动类型 Event 调用 WaitForSingleObject 并成功返回时,Event 将自动复位为 nonsignaled 状态,所有线程再对 Event 调用 WaitForSingleObject 都将会被阻塞。
实验内容
在本练习中,会在与线程状态转换相关的函数中添加若干个断点,并引导读者单步调试这些函数,使读者对 EOS 中的下列线程状态转换过程有一个全面的认识:
- 线程由阻塞状态进入就绪状态。
- 线程由运行状态进入就绪状态。
- 线程由就绪状态进入运行状态。
- 线程由运行状态进入阻塞状态。
loop
控制台命令:这个命令的命令函数是 ke/sysproc.c 文件中的 ConsoleCmdLoop 函数(第 796 行),在此函数中使用 LoopThreadFunction 函数(第 754 行)作为线程函数创建了一个优先级为 8 的线程(后面简称为“loop 线程”),该线程会在控制台中不停的(死循环)输出该线程的 ID 和执行计数,执行计数会不停的增长以表示该线程在不停的运行。
调试线程状态的转换过程
在系统中执行 loop 命令输出如上图所示。
查看当 loop 线程正在运行时,系统中各个线程的状态
- 在 ke/sysproc.c 文件的 LoopThreadFunction 函数中,开始死循环的代码行(第 786 行)添加一个断点。进行调试。
EOS 会在断点处中断执行,表明 loop 线程已经开始死循环了。选择“调试”菜单“窗口”中的“进程线程”,打开“进程线程”窗口,在该窗口工具栏上点击“刷新”按钮,可以查看当前系统中所有的线程信息,如图 12-2 所示。其中,系统空闲线程处于就绪状态,其优先级为 0;控制台派遣线程和所有控制台线程处于阻塞状态,其优先级为 24;只有优先级为 8 的 loop 线程处于运行状态,能够在处理器上执行。
- 在“项目管理器”窗口中双击打开 ps/sched.c 文件,在与线程状态转换相关的函数中添加断点,这样,一旦有线程的状态发生改变,EOS 会中断执行,就可以观察线程状态转换的详细过程了。需要添加的断点有
开始调试。
- 按 F5 继续执行,然后激活虚拟机窗口。
- 此时在虚拟机窗口中会看到 loop 线程在不停执行,而之前添加的断点都没有被命中,说明此时还没有任何线程的状态发生改变。(注意,如果命中了断点,可能是由于之前存在未处理的键盘事件导致的,此时只需继续按 F5,直到不再命中断点为止。)
在 loop 线程执行的过程中按一次空格键,这会导致 EOS 依次执行下面的操作:
- 控制台派遣线程被唤醒,由阻塞状态进入就绪状态。
- loop 线程由运行状态进入就绪状态。
- 控制台派遣线程由就绪状态进入运行状态。
- 待控制台派遣线程处理完毕由于空格键被按下而产生的键盘事件后,控制台派遣线程会由运行状态重新进入阻塞状态,开始等待下一个键盘事件到来。
- loop 线程由就绪状态进入运行状态,继续执行死循环。
控制台派遣程序由运行状态进入阻塞态
实现 Resume 原语
首先需要判断线程是否为 NULL,随后按照参考步骤调用 ListRemoveEntry 函数将线程从挂起线程队列中移除、调用 PspReadyThread 函数将线程恢复为就绪状态,最后调用 PspThreadSchedule 宏函数执行线程调度,让刚刚恢复的线程有机会执行。
在 kernel/psspnd.c
文件第 119
行的 PsResumThread
函数中加入如下代码
ASSERT(NULL != Thread)
// 1. 首先调用ListRemoveEntry函数将线程从挂起线程队列中移除。
ListRemoveEntry(&Thread->StateListEntry);
// 2. 然后调用PspReadyThread函数将线程恢复为就绪状态。
PspReadyThread(Thread);
// 3. 最后调用PspThreadSchedule宏函数执行线程调度,让刚刚恢复的线程有机会执行。
PspThreadSchedule();
启用 Bochs 虚拟机,首先执行 loop
运行线程号为 24
的线程,随后切换至另一个 Console 先执行 Suspend 24
挂起线程,随后 Resume 24
,重复两次以确保 Resume 功能正常。观察到对于 24 号线程的控制成功了。
思考题
1、思考一下,在本实验中,当 loop 线程处于运行状态时,EOS 中还有哪些线程,它们分别处于什么状态。
1 个优先级为 0 的空闲进程,处于就绪状态。
8 个优先级为 24 的控制台线程,处于阻塞状态。
1 个优先级为 24 的控制台派遣线程,处于阻塞状态。
2、当 loop 线程在控制台 1 中执行,并且在控制台 2 中执行 suspend 命令时,为什么控制台 1 中的 loop 线程处于就绪状态而不是运行状态?
控制台 2 执行 suspend 命令实际上是优先级为 24 的控制台 2 线程抢占了处理器,即控制台 2 处于运行状态,所以 loop 就处于了就绪状态。
2、总结一下哪些需要使用线程控制块中的上下文(将线程控制块中的上下文恢复到处理器中,或者将处理器的状态复制到线程控制块的上下文中),哪些不需要使用,并说明原因。
(1)新建线程转入就绪状态时,需要初始化线程控制块中的上下文。
(2)线程由就绪转入运行状态时,需要将线程控制块中的上下文恢复到处理器中,这是在中断返回前完成的。
(3)线程由运行状态进入就绪状态或阻塞状态时,需要将处理器的状态复制到线程控制块的上下文中,这是在进入中断时完成的。
(4)线程由阻塞状态进入就绪状态时,不需要使用线程控制块的上下文。
(5)线程由任意状态进入结束状态时,如果是运行状态被结束,则还需要在中断时保存上下文,然后再结束,如果是其他状态就不需要使用。
本文来自彬彬zhidao的博客,作者:彬彬zhidao,转载请注明原文链接:https://www.cnblogs.com/binbinzhidao/p/17833628.html