基于WRK平台的IPC实验.docx
《基于WRK平台的IPC实验.docx》由会员分享,可在线阅读,更多相关《基于WRK平台的IPC实验.docx(18页珍藏版)》请在冰豆网上搜索。
基于WRK平台的IPC实验
基于WRK平台的IPC实验
实验背景:
Inter-ProcessCommunication(进程间通信)在现在通用的时分操作系统中的进程管理中扮演着重要的角色,可以说没有同步/互斥机制,就不会实现系统的多线程。
在Windows中,内核提供了多种机制防止多个线程对同一个数据结构进行修改。
通过对WRK平台的IPC实验,我们可以更加深入地了解到Windows内部是如何实现线程的同步/互斥的。
第一部分:
阅读代码——了解WRK同步对象管理
1、阅读WRK代码,理解WRK中同步/互斥的对象管理
1.1WRK中的同步对象
Windows提供了一组内核同步对象(KernelDispatcherObject),或者就称为同步对象(DispatcherObject)。
在任何时刻,同步对象都处于两种状态中的一种:
信号态(signaledstate)或者非信号态(nonsignaledstate)。
这些对象包括定时器对象事件对象、信号量对象、临界区对象等。
表1WRK中同步对象数据结构
同步对象
定义位置
数据结构
Event
WRK\base\ntos\inc\Ntosdef.h
(注:
WRK是存放WRK源代码的文件夹,下同)
typedefstruct_KEVENT{
DISPATCHER_HEADERHeader;
}KEVENT,*PKEVENT,*PRKEVENT;
Mutex
WRK\base\ntos\inc\Ke.h
typedefstruct_KMUTANT{
DISPATCHER_HEADERHeader;
LIST_ENTRYMutantListEntry;
struct_KTHREAD*OwnerThread;
BOOLEANAbandoned;
UCHARApcDisable;
}KMUTANT,*PKMUTANT,
*PRKMUTANT,KMUTEX,
*PKMUTEX,*PRKMUTEX;
Semaphore
WRK\base\ntos\inc\Ke.h
typedefstruct_KSEMAPHORE{
DISPATCHER_HEADERHeader;
LONGLimit;
}KSEMAPHORE,
*PKSEMAPHORE,*PRKSEMAPHORE;
WaitableTimer
WRK\base\ntos\inc\Ntosdef.h
typedefstruct_KTIMER{
DISPATCHER_HEADERHeader;
ULARGE_INTEGERDueTime;
LIST_ENTRYTimerListEntry;
struct_KDPC*Dpc;
LONGPeriod;
}KTIMER,*PKTIMER,*PRKTIMER;
观察这些同步对象的数据结构,就会发现第一个成员都是DISPATHER_HEADER,这就是同步对象可以被等待的原因,也是同步对象的英文是DispatcherObject的原因。
查看下DISPATHER_HEADER的数据结构(WRK\base\ntos\inc\Ntosdef.h)。
typedefstruct_DISPATCHER_HEADER{
//对象类型
UCHARType;
UCHARAbsolute;
//对象体的大小
UCHARSize;
UCHARInserted;
//信号状态
LONGSignalState;
//等待该对象的线程列表
LIST_ENTRYWaitListHead;
}DISPATCHER_HEADER;
1.2线程中同步数据结构
在Windows中,调度的最小单位是线程,进行同步的也是线程,所以,在线程的数据结构中,必然包含有与同步有关的数据结构(WRK\base\ntos\inc\Ke.h)。
typedefstruct_KTHREAD{
DISPATCHER_HEADERHeader;
…
LONG_PTRWaitStatus;
//stateinformation,reflectingthereasonforendingwhenawaitingisended
union{
PKWAIT_BLOCKWaitBlockList;
//waitblockqueuepointedtothisthread
PKGATEGateObject;
};
BOOLEANAlertable;
BOOLEANWaitNext;
UCHARWaitReason;
…
KWAIT_BLOCKWaitBlock[THREAD_WAIT_OBJECTS+1];
//THREAD_WAIT_OBJECTS=3,thebuilt-insynchronizationobjectarray
//ifnowaitblockarrayisspecified,usethebuilt-insynchronizationobjectarray
…
};
其中,KTHREAD中的WaitBlockList是KWAIT_BLOCK对象的列表。
1.3联系线程和同步对象的数据结构
在WRK平台中,线程的同步/互斥是基于同步对象实现的。
那么必然有一个数据结构将二者联系起来,这个数据对象就是KWAIT_BLOCK(WRK\base\ntos\inc\Ke.h)。
typedefstruct_KWAIT_BLOCK{
LIST_ENTRYWaitListEntry;
struct_KTHREAD*Thread;
PVOIDObject;
struct_KWAIT_BLOCK*NextWaitBlock;
USHORTWaitKey;
UCHARWaitType;
UCHARSpareByte;
#ifdefined(_AMD64_)
LONGSpareLong;
#endif
}KWAIT_BLOCK,*PKWAIT_BLOCK,*PRKWAIT_BLOCK;
KWAIT_BLOCK中包含三个重要的指针:
一个是指向等待同步对象的线程指针Thread,一个是指向同步对象的指针(Object),另一个指向它自己NextWaitBlock。
通过这三个指针,线程、同步对象与KWAIT_BLOCK三者有机地联系起来。
下图显示了这三者之间的关系。
图1同步数据之间的关系
在这三个指针的帮助下,整个同步过程中始终保持两个队列。
一个是一个线程等待的同步对象队列,KTHREAD中的WaitBlockList指向这个队列,队列中的节点KWAIT_BLOCK通过自身的NextWaitBlock指针连接起来,通过这个队列,内核可以轻而易举地查看这个线程等待的同步对象。
另一个是等待同一个同步对象的线程队列,同步对象DISPATHER_HEADER中的WaitListEntry指向这个队列,KWAIT_BLOCK对象通过WaitListEntry指针加入到这个队列中。
很明显,同一个KWAIT_BLOCK对象会同时出现在两个队列中。
下图是实验一的情景图,我们很容易看出对象A的队列中只有一个等待对象,线程2,而对象B的队列中有两个等待对象,线程1和2。
线程1的队列里只有一个节点,线程1只需要等待对象B释放就可以进入就绪队列,而线程2的等待队列里面有两个节点,它必须在对象A和B都被释放了才能进入就绪队列。
图2实例
2、分析同步对象的释放和WRK的调度机制
2.1同步API函数
Windows提供的同步API采用一种分层的结构,包含面向用户的API,执行API,内核API。
上层的API通过调用下层对应的API完成其功能。
图3Windows的三层同步API结构
2.2同步对象的释放和WRK的线程调度
等待同步对象是线程的一个操作,线程可以等待一个同步对象或者多个同步对象,分别调用WaitForSingleObject和WaitForMultipleObjects。
2.2.1WaitForSingleObject等待一个同步对象
用户调用WaitForSingleObject函数会间接调用执行层的NtWaitForSingleObject。
在NtWaitForSingleObject中,最核心的操作是ObReferenceObjectByHandle和KeWaitForSingleObject。
(这些函数的代码放在WRK\base\ntos\ke\wait.c文件中,有兴趣的话可以阅读)
占用同步对象的线程调用ObReferenceObjectByHandle函数将用户传来的句柄转化成指针,传递给内核。
这个函数保证了指针的安全。
而KeWaitForSingleObject函数是实现同步功能的核心函数。
KeWaitForSingleObject函数通过调用InsertTailList(&Objectx->Header.WaitListHead,&WaitBlock->WaitListEntry),将等待块KWAIT_BLOCK插入该同步对象的等待线程队列中。
从而当占据的线程释放该线程时,就会遍历等待队列通知等待的线程。
最后,等待的线程调用WaitStatus=(NTSTATUS)KiSwapThread(Thread,CurrentPrcb)阻塞自己。
当然,这个函数调用会放在一个循环里面,因为进入阻塞循环的线程可能会被APC调用唤醒。
但是当APC函数执行完之后,线程的状态被改成STATUS_KERNEL_APC,然后线程进入下一个循环继续阻塞自己。
这个循环有三个出口。
首先是当同步对象变成“信号态”,这时已经满足同步条件,则正常跳出循环。
其次是超时,当用户设定了等待时间,则根据等待时间进行跳转。
最后一个是执行KiSwapThread之后的一个判断语句。
当阻塞被警醒后,判断是否是由于APC调用,如果不是则返回状态。
2.2.2WaitForMultipleObjects等待多个同步对象
WaitForMultipleObjects函数调用的过程如下:
WaitForMultiObjects->NtWaitForMultipleObjects->ObpWaitForMultipleObjects->KeWaitForMultipleObjects.
WaitForMultipleObjects函数与WaitForSingleObject类似,只是线程在等待多个同步对象变成“信号态”,因此WaitForMultipleObjects将等待的同步对象放入数组中,并且依次地将这些同步对象放入等待循环中。
线程等待每一个同步对象的过程类似于WaitForSingleObject,这里就不再赘述。
根据等待类型可以将线程分成两个分支。
分支1是WaitAny。
只要有一个等待的同步对象变成“信号态”,这个分支的线程就会结束阻塞,跳出等待的循环。
分支2是WaitAll,这个分支的线程会在等待的所有同步对象都变成“信号态”之后才会结束阻塞。
每次等待的同步对象变成“信号态”,WaitAll线程都会检查Index是否等于等待的同步对象综述,如果小于,线程会继续等待。
2.2.3同步对象的释放和线程调度
ReleaseXxx释放同步对象,ReleaseXxx间接调用NtReleaseXxx。
在NtReleaseXxx中,主要的操作为ObReferenceObjectByHandle和KeReleaseXxx。
这里类似于NtWaitForSingleObject,将用户传进来的句柄转换成内核中使用的指针,起到了保护指针的作用。
最核心的操作就是KeReleaseXxx了,在这里当内核将一个同步对象设置为信号状态时,就会调用KiWaitTest来进行检查,遍历同步对象的等待队列中正在等待的线程。
如果等待线程的等待类型为WaitAny,则直接满足该线程的等待要求,将该线程放入就绪队列中;若等待类型为WaitAll,只有等待的同步对象都被释放后才会进入就绪队列,否则不作任何处理。
最后,调用KiUnwaitThread将等待线程加入到就绪队列中等待调度。
第二部分:
联机调试——查看WRK等待队列
1、了解WinDbg的一些常用命令
1.1在winDbg中常用到的WinDbg命令:
Kd>!
process
Kd>dtnt!
_kthread
Kd>dtnt!
_kwait_block
Kd>dtnt!
_dispatcher_header
Kd>!
process00
Kd>!
process#thread
1.2!
process命令
主要用于查看进程状态(如图)。
1.3dt命令
显示类型,主要用dt来解释线程、等待块(如图)。
2、使用WinDbg联机查看一个线程等待的所有同步对象
(1)在系统初始化时,按下debug的break,进入断点。
并通过!
process查看当前所有线程的状态。
(2)选择其中一个线程查看,比如817a0020,可以看到它正在等待两个事件。
(3)用dtnt!
_kthread查看这个线程的全部信息。
可以看到等待的列表头为0x80899b28_KWAIT_BLOCK
(4)查看下一个等待块信息。
0x80899b28的具体信息如下:
3、使用WinDbg联机查看等待一个同步对象的所有线程
(1)用!
process命令,得到当前的线程信息,如图,看到8089eb3c被7个线程同时等待。
(2)用dt命令来解释该对象(8089eb3c)的分发器头:
可以看出有多个线程在等待这个对象。
(3)解释等待块0x817a70c8.
(4)解释等待块0x817a7e58。
(5)解释等待块0x817a7be8。
(6)解释等待块0x817a7978
(7)解释等待块0x817a7708。
发现NextWaitBlock就是自己,所以等待该对象的队列遍历完成。
(8)测试发现8x817a7798已经不是等待块了。
第三部分:
生产者-消费者问题在WRK平台上的实现与调试
1、充分理解“生产者-消费者”算法,编写“生产者-消费者”程序
1.1“生产者-消费者”问题描述
如上图所示,生产者把产品生产出来,送入仓库队列中。
给消费者发信号,消费者得到信号后,到仓库取产品,取走产品后给生产者发信号。
某时刻,只允许有一个生产者或消费者访问仓库队列,同时在仓库队列中进行操作将导致不可预知的错误。
1.2“生产者-消费者”算法。
1.2.1“生产者-消费者”问题分析
生产者-消费者问题是相互合作进程关系的一种抽象,其中,生产者作为系统中释放资源的进程,而消费者作为系统中使用同类资源的进程。
通过对该问题的分析,可知,消费者想接收数据时,有界缓冲区中至少有一个单元是满的,即对于“消费者”而言,缓冲区空则应等待。
而生产者想发送数据时,有界缓冲区中至少有一个单元是空的,即对于“生产者”而言,缓冲区满则应等待。
这便表现为一个同步问题。
而由于缓冲区是临界资源,所以生产者和消费者之间必须互斥的访问临界资源。
即任何时刻,只能有一个进程在临界区中操作。
这便表现为一个互斥问题。
为此,对于同步问题,引入同步信号量“empty”,为0表示缓冲区满及同步信号量“full”,为0表示缓冲区空,对于互斥问题,引入互斥信号量(二元信号量),信号量为0,表明已有进程进入临界区。
1.2.2“生产者-消费者”算法
设:
互斥信号量(Mutex)m_S_Queue表示缓冲区的个数初值为1。
(消费者)同步信号量(Semaphore)m_S_Consumer表示有界缓冲区中非空单元数初值为0。
(生产者)同步信号量(Semaphore)m_S_Producer表示有界缓冲区中空的单元数初值为n。
生产者:
Product(data):
begin
P(m_S_Producer)检查缓冲区中是否有空单元执行后n-1
P(m_S_Queue)检查缓冲区是否可用执行后mutex=0
送数据入缓冲区
V(m_S_Queue)释放缓冲区中的资源
V(m_S_Consumer)执行后,非空单元数加10+1=1
end
消费者:
Consume(data):
begin
P(m_S_Consumer)
P(m_S_Queue)
取缓冲区中某单元数据
V(m_S_Queue)
V(m_S_Producer)
end
3)在宿主机上编写“生产者-消费者”程序,并运行生成PC.exe,放入主机共享的工作目录c:
\wrk下。
2、“生产者-消费者”问题在WRK平台下调试
1)将生成的“生产者-消费者”可执行程序PC.exe放入虚拟机
在虚拟机中的Windows2003中,运行cmd命令,打开命令行窗口,输入xcopyz:
\PC.exec:
\,将PC.exe程序拷到虚拟机c盘下。
2)运行PC.exe后进入调试状态,输入!
process00
找到PC.exe进程的进程号811ef020
3)查看PC.exe进程的全部信息,输入!
process811ef020
4)看到与之相关的有三个线程,可以看到线程811ef518在等待线程811ee478,而811ee478在等待两个分发器对象,811ea020线程已经就绪,等待调度
注:
不同的时间点观察到的三个线程状态不一样,可另取时间点
5)查看线程811ee478的全部信息
6)查看线程811ee478等待队列中等待块信息
Object指向地址0x811f4e08的Semaphore对象。
Object指向地址0x811ee4f0的NotificationTimer对象。
NextWaitBlock指回上一等待块,结束遍历线程等待队列。
3、修改WRK代码,增加同步/互斥数据结构的输出
这里只是描述一下解决问题的思路:
在KeWaitForSingleObject中的InitializeWaitSingle及KeWaitForMultipleObject中的InitializeWaitMultiple中实现了对等待块的赋值操作,而在同步的过程中,始终涉及到两个对象,线程和同步对象,等待块负责将二者联系起来。
知道了等待块的信息,两个队列中的信息均可以知道,所以,不妨在这两个宏定义之后增加调用向文件输出相关数据结构的函数。
但是,这个函数的实现是相当复杂的,首先,它要找到在当前状态下由线程维持的队列中所有同步对象,及由同步对象维持的队列中的所有线程。
然后,它要调用相关的I/O设备向文件中输入这些对象及线程的数据结构及其值。
这样,我们就能在不使用调试的情况下,通过用户调用WaitforSingalObject或WaitforMultipleObject便可在相关文件中看到调试中出现的数据结构,从而方便的观察到该机制的实现原理。
.