本章讨论的是相对抽象的概念,不涉及任何具体的内核对象的细节而是讨论所有内核对象的共有特性。
首先让我们来了解一下什么是内核对象。内核对象通过API来创建,每个内核对象是一个数据结构,它对应一块内存,由操作系统内核分配,并且只能由操作系统内核访问。在此数据结构中少数成员如安全描述符和使用计数是所有对象都有的,但其他大多数成员都是不同类型的对象特有的。内核对象的数据结构只能由操作系统提供的API访问,应用程序在内存中不能访问。调用创建内核对象的函数后,该函数会返回一个句柄,它标识了所创建的对象。它可以由进程的任何线程使用。在32位系统中,句柄是一个32位值。64位系统中则是64位值。
很多人对句柄到底是什么东西很疑惑。有人说是指针有人说是索引。其实句柄仅仅是独立于每个进程的句柄表的一个索引。在每个进程中都存在一个句柄表,列出了所有本进程内可以使用的句柄
。它只是一个有数据结构组成的数组,每个结构都包含一个指向内核对象的指针、访问掩码、继承标识等,而句柄仅仅是句柄表数组的下标。由于每个进程都存在句柄表,因此句柄是独立于进程的,虽然将一个进程的句柄传给另一个进程不一定会失败,但是它引用的是另一个进程完全不同的内核对象。后面的跨进程边界共享内核对象将介绍如何跨界成共享内核对象。
内核对象的所有者是操作系统内核,而非进程。也就是说多个进程可以共享一个内核对象。内核对象数据结构内有一个使用计数成员,它是所有对象都有的一个成员,标识该内核对象被引用的次数。刚创建时使用计数被初始化为1,如果有另一个进程获得对此内核对象的访问后,使用计数就会递增。一个使用此内核对象的进程终止后或是对此内核对象调用CloseHandle,操作系统内核会自动递减内核对象的使用计数。一旦计数变为0,操作系统内核就会销毁对象。
安全描述符用以描述内核对象的安全性。它描述了内核对象的拥有者,那组用户可以访问此对象,那组用户无访问权限。安全描述符对应一个数据结构:SECURITY_ATTRIBUTES结构,几乎内核对象在创建时都需要传入此结构,但是大部分情况下都是传入NULL,表示使用默认的安全性。
除了使用内核对象,应用程序还需要使用其他类型的对象,如菜单、窗口、鼠标光标等,这些属于用户对象或GDI对象,而非内核对象。要判断一个对象是不是内核对象,最简单的方法就是查看创建这个对象的函数,几乎所有的创建内核对象的函数都需要指定安全属性信息的参数,而用于创建用户对象的函数都不需要使用安全描述符。
一个进程在刚被创建时,它的句柄表是空的。当进程内的一个线程调用创建内核对象的函数时,内核将为这个对象分配并初始化一个内存块,然后扫描进程的句柄表,查找一个空白的记录项,并对其进行初始化。指针成员将会被初始化为内核对象的地址,继承标志也会被设置。
用于创建内核对象的函数都会返回一个与进程相关的句柄,此句柄可由属于该进程的所有线程引用。调用一个函数,如果它需要一个内核对象句柄的参数,就必须为它传递一个句柄。在内部,这个函数会查找进程的句柄表,获得目标内核对象的地址然后对此数据结构进行操作。如果我们直接使用其他进程的的句柄,那么实际引用的只是那个进程句柄表中位于同一索引的内核对象,它们仅仅是索引值相同而已。创建内核对象的函数在失败时会返回NULL。但有时也有的函数会返回-1,如CreateFile,它返回的是INVALID_HANDLE_VALUE而不是NULL。失败的原因可能是内存不足或是没有权限。这在检查内核对象是否创建成功时要特别注意。
当进程不再使用某内核对象时应该调用CloseHandle来向系统表明我们已经结束使用此对象。在内部该函数会扫描进程的句柄表,如果句柄是有效的,系统就获得此内核对象的数据结构的地址,并将此结构的使用计数成员递减1。如果使用计数变为0,句柄表对应的记录项将会被清除,内核对象将被销毁,所占内存将会被释放。此后再在此进程内使用此句柄将会发生未知错误。因为调用CloseHandle后此内核对象不知是否已经被销毁,如果没有销毁那么此次对此句柄的使用将没有问题。如果此内核对象已被销毁,且句柄表对应项已经被其他项占据,此时操作的将是另一个内核对象,可能发生无法预知的错误。因此在调用CloseHandle后要最好将原来的变量赋值为NULL。
即使在进程结束了对内核对象的访问后,没有调用CloseHandle,进程终止时,操作系统也会确保进程所使用的所有资源都被释放。系统自动扫描进程句柄表,将所有内核对象的使用计数都减1。同样如存在使用计数为0的内核对象,它就会被释放。但是这毕竟不是个好习惯,如果我们开发的程序是长时间运行的程序,由于没有主动调用CloseHandle,进程已经不再使用的内核对象仍然得不到释放,越往后运行系统所占内存就越大。这跟内存泄露很类似,自己开辟的堆空间不再使用时要自己主动释放。对于内核对象这也同样适用。在任务管理器的Handle列可以查看每个进程占用的内核对象数。
前面说到另一个进程不能直接使用一个进程的内核对象的句柄。注意这里使用直接二字,这并不意味着其他进程不能使用此内核对象,虽然句柄是独立于进程的,但是内核对象是归系统内核所有,各个进程都可以使用,只是要使用还需要费一番周折。接下来将介绍跨进程边界共享内核对象。
跨进程共享内核对象是必要的,1:是利用文件映射对象可以在两个进程间共享数据块。2:互斥量、信号量和事件允许不同进程的线程同步执行。
跨进程共享内核对象方法之一:使用对象句柄继承
只有进程之间属于父子关系时才可以使用对象句柄继承。当父进程创建一个内核对象时,父进程必须向系统指出它希望这个对象的句柄是可继承的。为了创建可继承句柄父进程必须分配并初始化一个SECURITY_ATTRIBUTES结构,并将这个结构的地址传递给Create*函数。如:
SECURITY_ATTRIBUTES sa;
sa.nLength=sizeof(sa);
sa.lpSecurityDescriptor=NULL;//使用默认安全性。
sa.bInheritHandle=TRUE;//是此句柄可以继承。
HANDLE mutex=CeattMutex(&sa,FALSE,NULL);
以上代码初始化了一个SECURITY_ATTRIBUTES结构,表明使用默认安全性来创建此对象,且返回的对象时可继承的。
句柄表的每个记录中还有一个指明该句柄是否可继承的标志位,如果在创建内核对象的时候将NULL作为PSECURITY_ATTRIBUTES的参数传入,则返回的句柄是不可继承的,标志位为0。
下一步是由父进程创建子进程,这是通过CreateProcess实现的,此函数第四章会详细介绍,此处仅仅注意bInheritHandles参数。如果在创建进程时,此参数被设为false,则表明不希望子进程继承父进程句柄表中的可继承句柄。如为true,则表明希望子进程继承父进程句柄表中的可继承句柄。注意只有可继承句柄才可以被继承。
新创建的进程句柄表为空,由于我们希望它继承父进程句柄表,此时系统会遍历父进程句柄表,对它的每一个项进行检查,将所有的可继承的句柄的项全部复制到子进程的句柄表中。在子进程的句柄表中,复制项的位置与它在父进程句柄表中的位置是完全一样的,这是非常重要的。它意味着在父进程和子进程中,对一个内核对象进行标识的句柄是完全一样的。除了复制句柄表,系统还会递增每个可继承句柄的使用计数。为了销毁内核对象,父进程和子进程必须都不再使用才可以。这可以通过CloseHandle和进程终止来实现。注意:句柄进程仅仅发生在进程刚被创建时,如果此后父进程又创建了新的内核对象,那么此时子进程不会继承这些新创建的内核对象句柄。
如果父进程创建了一个内核对象,得到一个不可继承的句柄,但是后来父进程又希望后来创建的子进程继承它,这怎么办呢?这可以通过使用SetHandleInformation修改内核对象句柄的继承标志 。它需三个参数,第一个标识了一个有效句柄,第二个标识想更改哪些标识。第三个标识指出想把它设成什么。这个标识可以是
HANDLE_FLAG_INHERI,//打开句柄继承标识。
HANDLE_FLAG_PROTECT_FROM_CLOSE//不允许关闭句柄。
GetHandleInformation可以用来返回指定句柄的当前标识。
跨进程共享内核对象方法之二:命名对象
许多对象都可以进行命名,但并不是全部。因此该方法有一定局限性。有些创建内核对象的函数都有一个指定内核对象名称的参数,如果传入NULL,则会创建一个匿名的内核对象。如果不为NULL,则应该传入一个一'\0'结尾的字符串。所有这些命名对象共享一个名字空间。即使它们类型不同,如果已存在同名对象,创建就会失败。
一旦一个命名的内核对象被创建,其他进程(不仅仅是子进程)可以通过调用Open*或是Create*函数来访问它。当使用Create*函数时,系统会检查是否存在一个传给此函数的名字,如果确实存在一个这样的对象,内核执行安全检查,验证调用者是否有足够的安全权限。如果是,系统就会在此进程的句柄表中查找空白记录项,并将其初始化为指向已存在的命名的内核对象。两个进程的句柄不一定相同,这没有任何影响。由于内核对象被再一次引用,所以其引用计数会被递增。
为了防止在创建一个命名对象时,仅仅打开了一个现有的而不是新建的,可以在创建后调用GetLastError获得详细信息。
使用Open*函数可以打开已存在的命名内核对象,如果没有找到这个名称的内核对象将返回NULL。如果找到这个名称的内核对象,但类型不同,函数仍返回NULL。只有当名称相同且类型相同的情况下系统才会进一步检查访问权限。如果有权访问,系统就会更新此进程的句柄表,并递增内核对象的引用计数。在Open*函数中也可以指定此句柄的继承性。
Open*和Create*的区别:如果对象不存在,Create*会创建它,Open*将会调用失败。
我们经常使用命名的内核对象来防止运行一个程序的多个实例。可以在main函数中建立一个命名对象,返回后调用GetLastError如果GetLastError返回ERROR_ALREADY_EXISTS表明此程序的另一个实例在运行。
关于终端服务命名空间不再介绍,只需知道它是为了防止命名内核对象命名冲突而设计的。以后有需要的可以仔细研究下。
跨进程共享内核对象方法之三:复制对象句柄
实现该方法使用的是Duplicatehandle函数。
bool DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargethandle
DWORD ddwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions
);
这个函数的功能就是获得进程句柄表的一个记录项,然后在另一个进程中创建这个记录项的副本。第一个和第三个参数分别标识源进程和目标进程内核对象句柄。第二个参数标识要复制的内核对象句柄,它可以指向任何类型的内核对象。第四个参数是一个目标句柄的地址,用来接收复制到的HANDLE值。
函数将源进程中的句柄信息复制到目标进程所标识的句柄表中。第五第六个参数用以指定此内核对象句柄在目标进程句柄表中应该使用何种访问掩码和继承标志。
dwOption参数可以是DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE任一个。如果是DUPLICATE_SAME_ACCESS标志,将向DuplicateHandle函数表明我们希望目标句柄拥有与源进程句柄一样的访问掩码,此时会忽略dwDesiredAccess。如果是DUPLICATE_CLOSE_SOURCE标志,会关闭源进程的句柄,此时将一个内核对象从一个进程复制到另一个进程,但是内核对象的使用计数不受影响。
GetCurrentProcess可以返回当前进程的句柄,但是它是一个伪句柄。其值为-1,GetCurrentThread返回的也是伪句柄其值为-2,它们并不在句柄表中而仅仅代表当前进程和当前线程。
这一章很抽象,原来学习的时候读了很多遍也不是很明白,后来干脆跳过去了,一段时间的学习之后再回来看看,发现竟然非常简单。所以有时候学习不能钻牛角尖该跳过就跳过。随着学习的深入,你所站的高度、看问题的角度都会不一样。理解起来也会更容易!!!
- Windows核心编程 第三章 内核对象
第3章内核对象 在介绍Windows API的时候,首先要讲述内核对象以及它们的句柄.本章将要介绍一些比较抽象的概念,在此并不讨论某个特定内核对象的特性,相反只是介绍适用于所有内核对象的特性. 首先介 ...
- windows核心编程---第八章 使用内核对象进行线程同步
使用内核对象进行线程同步. 前面我们介绍了用户模式下线程同步的几种方式.在用户模式下进行线程同步的最大好处就是速度非常快.因此当需要使用线程同步时用户模式下的线程同步是首选. 但是用户模式下的线程同步 ...
- windows核心编程 第5章job lab示例程序 解决小技巧
看到windows核心编程 第5章的最后一节,发现job lab例子程序不能在我的系统(win8下)正常运行,总是提示“进程在一个作业里” 用process explorer程序查看 ...
- windows核心编程 第8章201页旋转锁的代码在新版Visual Studio运行问题
// 全局变量,用于指示共享的资源是否在使用 BOOL g_fResourceInUse = FALSE; void Func1() { //等待访问资源 while(InterlockedExcha ...
- 《windows核心编程系列》三谈谈内核对象及句柄的本质
内核对象 本章讨论的是相对抽象的概念,不涉及任何具体的内核对象的细节而是讨论所有内核对象的共有特性. 首先让我们来了解一下什么是内核对象.内核对象通过API来创建,每个内核对象是一个数据结构,它对应一 ...
- Windows核心编程 第十七章 -内存映射文件(下)
17.3 使用内存映射文件 若要使用内存映射文件,必须执行下列操作步骤: 1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件. 2) 创建一个文件映射内核对象,告诉系统该 ...
- windows核心编程---第四章 进程
上一章介绍了内核对象,这一节开始就要不断接触各种内核对象了.首先要给大家介绍的是进程内核对象.进程大家都不陌生,它是资源和分配的基本单位,而进程内核对象就是与进程相关联的一个数据结构.操作系统内核通过 ...
- Windows核心编程 第十一章 线程池的使用
第11章 线程池的使用 第8章讲述了如何使用让线程保持用户方式的机制来实现线程同步的方法.用户方式的同步机制的出色之处在于它的同步速度很快.如果关心线程的运行速度,那么应该了解一下用户方式的同步机制是 ...
- Windows核心编程 第四章 进程(下)
4.3 终止进程的运行 若要终止进程的运行,可以使用下面四种方法: • 主线程的进入点函数返回(最好使用这个方法) . • 进程中的一个线程调用E x i t P r o c e s s函数(应该避免 ...
随机推荐
- map函数
概述 map() 方法返回一个由原数组中的每个元素调用一个指定方法后的返回值组成的新数组. 语法 array.map(callback[, thisArg]) 参数 callback 原数组中的元素经 ...
- nwjs如何打包文件为exe文件并修改exe图标
1.下载nw.js,如果是SDK版的可以调试页面,打包后可不可以调试还没有试,不是SDK的话没有调试选项,试了一下,打包后的文件也一样调试不了. 2.把要打包的文件和package.json都放在nw ...
- Python学习笔记(0)
Python 是什么类型的语言 Python是脚本语言 Python下载地址:https://www.python.org/downloads/ Python版本:Python 3.4.2 - 64b ...
- Zookeeper第一课 安装和配置
简介: Zookeeper,是Google的Chubby一个开源的实现,是Hadoop的分布式协调服务,它包含一个简单的原语集,来实现同步.配置维护.分集群.命名的服务. zookeeper是一个由多 ...
- Hibernate 中出现 XXXX is not mapped 问题
1.查询的不是数据库名是实体名…Entity 2.也许是你where条件后的字段名称写错了
- Linux系统下Apache2.4.17的安装过程
Linux系统下安装Apache Server2.4.17.还是先声明一下,Linux命令我不进行讲解,因为我不是讲Linux命令的.有需要注意的地方,我会上图,没什么值得的注意的地方,我就不上图了. ...
- [maven] 使用Nexus创建maven私有仓库
1.为什么需要maven私有仓库? 从Maven中央仓库下载所需的jar包,需要外网的支持.如果公司不能上外网的话则不能从中央仓库下载所需jar包,公司网速慢的时候也会影响项目构建的速度.用户可以用n ...
- PHP 扩展开发小结
1. 变量操作(常量) 设置变量 ZVAL_*系列函数; 例: zval t; ZVAL_STRING(t,"10",2); 获取变量 Z_* 系列函数 获取变量指针 Z_*_P ...
- linux查看及改变运行级别
Linux运行级别从0-6,共7个. 0:关机.不能将系统缺省运行级别设置为0,否则无法启动. 1:单用户模式,只允许root用户对系统进行维护. 2:多用户模式,但不能使用NFS(相当于Window ...
- 点击a标签,跳转到iframe中,并在iframe中显示指定的页面
点击a标签,跳转到iframe中,并在iframe中显示指定的页面 1.用a标签的target属性 <iframe id="myFrameId" name="myF ...