📄
字号:
双方都打开了一个共享内存区对象以后,就可以各自通过NtMapViewOfSection()将其映射到自己的用户空间。
[code]NTSTATUS STDCALL
NtMapViewOfSection(IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress OPTIONAL,
IN ULONG ZeroBits OPTIONAL,
IN ULONG CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PULONG ViewSize,
IN SECTION_INHERIT InheritDisposition,
IN ULONG AllocationType OPTIONAL,
IN ULONG Protect)
{
. . . . . .
PreviousMode = ExGetPreviousMode();
if(PreviousMode != KernelMode)
{
SafeBaseAddress = NULL;
SafeSectionOffset.QuadPart = 0;
SafeViewSize = 0;
_SEH_TRY. . . . . . _SEH_END;
. . . . . .
}
else
{
SafeBaseAddress = (BaseAddress != NULL ? *BaseAddress : NULL);
SafeSectionOffset.QuadPart = (SectionOffset != NULL ? SectionOffset->QuadPart : 0);
SafeViewSize = (ViewSize != NULL ? *ViewSize : 0);
}
Status = ObReferenceObjectByHandle(ProcessHandle,
PROCESS_VM_OPERATION,
PsProcessType,
PreviousMode,
(PVOID*)(PVOID)&Process,
NULL);
. . . . . .
AddressSpace = &Process->AddressSpace;
Status = ObReferenceObjectByHandle(SectionHandle,
SECTION_MAP_READ,
MmSectionObjectType,
PreviousMode,
(PVOID*)(PVOID)&Section,
NULL);
. . . . . .
Status = MmMapViewOfSection(Section, Process,
(BaseAddress != NULL ? &SafeBaseAddress : NULL),
ZeroBits, CommitSize,
(SectionOffset != NULL ? &SafeSectionOffset : NULL),
(ViewSize != NULL ? &SafeViewSize : NULL),
InheritDisposition, AllocationType, Protect);
ObDereferenceObject(Section);
ObDereferenceObject(Process);
. . . . . .
return(Status);
}[/code]
以前讲过,NtMapViewOfSection()并不是专为当前进程的,而是可以用于任何已打开的进程,所以参数中既有共享内存区对象的Handle,又有进程对象的Handle。从这个意义上说,两个进程之间通过共享内存区的通信完全可以由第三方撮合、包办而成,但是一般还是由通信双方自己加以映射的。显然,这里实质性的操作是MmMapViewOfSection(),那是存储管理底层的事了,我们就不再追究下去了。
一旦把同一个共享内存区映射到了通信双方的用户空间(可能在不同的地址上),出现在这双方用户空间的特定地址区间就落实到了同一组物理页面。这样,一方往里面写的内容就可以为另一方所见,于是就可以通过普通的内存访问(例如通过指针)实现进程间通信了。
但是,如前所述,通过共享内存区实现的只是原始的、低效的进程间通信,因为共享内存区只满足了前述三个条件中的两个,只是提供了信息的(实时)传输,但是却缺乏进程间的同步。所以实际使用时需要或者结合别的进程间通信(同步)手段,或者由应用程序自己设法实现同步(例如定时查询)。
2. 信号量(Semaphore)
学过操作系统原理的读者想必知道“临界区”和“信号量”、以及二者之间的关系。如果没有学过,那也不要紧,不妨把临界区想像成一个凭通行证入内的工作场所,作为对象存在的“信号量”则是发放通行证的“票务处”。但是,通行证的数量是有限的,一般只有寥寥几张。一旦发完,想要领票的进程(线程)就只好睡眠等待,直到已经在里面干活的进程(线程)完成了操作以后退出临界区并交还通行证,才会被唤醒并领到通行证进入临界区。之所以称之为(翻译为)“信号量”,是因为“票务处”必须维持一个数值,表明当前手头还有几张通行证,这就是作为数值的“信号量”。所以,“信号量”是一个对象,对象中有个数值,而信号量这个名称就是因这个数值而来。那么,为什么要到临界区里面去进行某些操作呢?一般是因为这些操作不允许受到干扰,必须排它地进行,或者说有互斥要求。
在操作系统理论中,“领票”操作称为P操作,具体的操作如下:
l 递减信号量的数值。
l 如果递减后大于或等于0就说明领到了通行证,因而就进入了临界区,可以接着进行想要做的操作了。
l 反之如果递减后小于0,则说明通行证已经发完,当前线程只好(主动)在临界区的大门口睡眠,直至有通行证可发时才被唤醒。
l 如果信号量的数值小于0,那么其绝对值表明正在睡眠等待的线程个数。
领到票进入了临界区的线程,在完成了操作以后应从临界区退出、并交还通行证,这个操作称为V操作,具体的操作如下:
l 递增信号量的数值。
l 如果递增以后的数值小于等于0,就说明有进程正在等待,因而需要加以唤醒。
这里,执行了P操作的进程稍后就会执行V操作,但是也可以把这两种操作分开来,让有的进程光执行V操作,另一些进程则光执行P操作。于是,这两种进程就成了供应者/消费者的关系,前者是供应者,后者是消费者,而信号量的P/V操作正好可以被用作进程间的睡眠/唤醒机制。例如,假定最初时“通行证”的数量为0,并把它理解为筹码。每当写入方在共享内存区中写入数据以后就对信号量执行一次V操作,而每当读出方想要读出新的数据时就先执行一次P操作。所以,许多别的进程间同步机制其实只是“信号量”的变种,是由“信号量”派生、演变而来的。
注意在条件不具备时进入睡眠、以及在条件具备时加以唤醒,是P操作和V操作固有的一部分,否则就退化成对于标志位或数值的测试和设置了。当然,“测试和设置”也是进程间同步的手段之一,但是那只相当于轮询,一般而言效率是很低的。所以,离开(睡眠)等待和唤醒,就不足于构成高效的进程间通信机制。但是也有例外,那就是如果能肯定所等待的条件在一个很短的时间内一定会得到满足,那就不妨不断地反复测试直至成功,这就是“空转锁(SpinLock)”的来历。不过空转锁一般只是在内核中使用,而不提供给用户空间。
信号量对象的创建和打开是由NtCreateSemaphore()和NtOpenSemaphore()实现的,我们看一下NtCreateSemaphore():
[code]NTSTATUS
STDCALL
NtCreateSemaphore(OUT PHANDLE SemaphoreHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN LONG InitialCount,
IN LONG MaximumCount)
{
. . . . . .
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
. . . . . .
if(PreviousMode != KernelMode) {
_SEH_TRY . . . . . ._SEH_END;
if(!NT_SUCCESS(Status)) return Status;
}
. . . . . .
/* Create the Semaphore Object */
Status = ObCreateObject(PreviousMode, ExSemaphoreObjectType,
ObjectAttributes, PreviousMode, NULL,
sizeof(KSEMAPHORE), 0, 0, (PVOID*)&Semaphore);
/* Check for Success */
if (NT_SUCCESS(Status)) {
/* Initialize it */
KeInitializeSemaphore(Semaphore, InitialCount, MaximumCount);
/* Insert it into the Object Tree */
Status = ObInsertObject((PVOID)Semaphore, NULL,
DesiredAccess, 0, NULL, &hSemaphore);
ObDereferenceObject(Semaphore);
/* Check for success and return handle */
if(NT_SUCCESS(Status)) {
_SEH_TRY . . . . . ._SEH_END;
}
}
/* Return Status */
return Status;
}[/code]
这个函数可以说是对象创建函数的样板(Template)。一般而言,创建对象的过程总是涉及三步主要操作:
1. 通过ObCreateObject()创建对象,对象的类型代码决定了具体的对象种类。对于信号量,这个类型代码是ExSemaphoreObjectType。所创建的对象被挂入内核中该种类型的对象队列(类似于文件系统的目录),以备别的进程打开。这个函数返回一个指向具体对象数据结构的指针,数据结构的类型取决于对象的类型代码。
2. 类似于KeInitializeSemaphore()那样的初始化函数,对所创建对象的数据结构进行初始化。
3. 通过ObInsertObject()将所创建对象的数据结构指针填入当前进程的打开对象表,并返回相应的Handle。所以,创建对象实际上是创建并打开一个对象。
对于信号量,ObCreateObject()返回的是KSEMAPHORE数据结构指针:
[code]typedef struct _KSEMAPHORE {
DISPATCHER_HEADER Header;
LONG Limit;
} KSEMAPHORE, *PKSEMAPHORE, *RESTRICTED_POINTER PRKSEMAPHORE;[/code]
这里的Limit是信号量数值的上限,来自前面的调用参数MaximumCount。而参数InitialCount的数值、即信号量的初值,则记录在DISPATCHER_HEADER内的SignalState字段中。所以,信号量对象将其头部的SignalState字段用作了“信号量”。
NtOpenSemaphore()的代码就不看了,所有的打开对象操作都是一样的,基本上就是通过内核函数ObOpenObjectByName()根据对象名(路径名)找到目标对象,然后将它的数据结构指针填入本进程的打开对象表,并返回相应的Handle。
读者可能已经在急切想要知道信号量的P/V操作是怎么实现的。也许会使读者感到意外,Windows并没有专为信号量的P操作而设的系统调用,信号量的P操作就是通过NtWaitForSingleObject()或NtWaitForMultipleObjects()实现的。事实上,所有与P操作类似、会使调用者阻塞的操作都是由这两个函数实现的。读者已经在上一篇漫谈中看过NtWaitForSingleObject()的代码,这里就不重复了。
信号量的V操作倒是有专门的系统调用,那就是NtReleaseSemaphore()。
[code]NTSTATUS
STDCALL
NtReleaseSemaphore(IN HANDLE SemaphoreHandle,
IN LONG ReleaseCount,
OUT PLONG PreviousCount OPTIONAL)
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
. . . . . .
if(PreviousCount != NULL && PreviousMode == UserMode) {
_SEH_TRY . . . . . ._SEH_END;
if(!NT_SUCCESS(Status)) return Status;
}
. . . . . .
/* Get the Object */
Status = ObReferenceObjectByHandle(SemaphoreHandle,
SEMAPHORE_MODIFY_STATE,
ExSemaphoreObjectType, PreviousMode,
(PVOID*)&Semaphore, NULL);
/* Check for success */
if (NT_SUCCESS(Status)) {
/* Release the semaphore */
LONG PrevCount = KeReleaseSemaphore(Semaphore, IO_NO_INCREMENT,
ReleaseCount, FALSE);
ObDereferenceObject(Semaphore);
/* Return it */
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -