内核对象只是操作系统内核分配的一个内存块,并且只能由操作系统内核访问。该内存块是一种数据结构,它的成员负责维护该对象的各种信息。Windows提供一组函数创建和操作内核对象。调用一个创建内核对象的函数,函数会返回一个句柄,该句柄标识了这个内核对象,这个句柄可由当前进程中的所有线程调用。也可以通过跨进程边界共享内核对象,让其他的进程调用。
使用计数
内核对象有个使用计数数据成员,标识内核对象被多少个进程所使用。大部分情况是内核对象只被创建它的进程所有使用,当这个进程退出时,内核对象的使用计数就会减一,如果内核对象的使用计数为0时,内核对象就会自动销毁,如果内核对象被多个进程使用时,它的生命周期就可能比创建它的进程要长。只要内核对象的使用计数不为0,它就不会销毁,当当前进程退出时,只要有其他进程使用这个内核对象,它就不会销毁。但是我们也不用担心内核对象导致的内存泄露,就算进程没有手动关闭内核对象,进程在退出的时候,会检查自己的句柄表,如果句柄表中有使用的内核对象,操作系统会为我们关闭这些句柄,被这些句柄引用的内核对象的使用计数就会减一,如果使用计数为0,内核对象就会自动销毁。从上面我们可以看出,内核对象并没有和创建它的进程所绑定,创建它的进程退出了,内核对象可能还存活,内核对象的操作所有者是操作系统,而不是进程。
内核对象的安全性
内核对象可以用一个安全描述符来保护,安全描述符描述了谁是该对象的拥有者,哪些组和用户可以访问或使用这些对象,这个对象是否可以被继承等。SECURITY_ATTRIBUTES这个结构体就是用于安全描述的。其结构如下:
typedef struct _SECURITY_ATTRIBUTES { DWORD nLength;//表示结构体的长度 LPVOID lpSecurityDescriptor;//指向一个安全描述符 BOOL bInheritHandle;//表示内核对象被子进程继承 } SECURITY_ATTRIBUTES;
区分一个对象是否是内核对象就可以看创建对象函数的参数中有没有SECURITY_ATTRIBUTES这个参数,有的就是内核对象,没有的就不是内核对象。有了这个结构就将内核对象保护起来了,其他对象就不能随便访问它,也就不能破坏它的内存结构。
进程内核对象句柄表
一个进程在初始化时,系统将为它分配一个句柄表。这个句柄表仅供内核对象使用,不适用于用户对象和GDI对象,句柄表的结构大致包括:
1:索引 2:指向内核对象内存块的指针 3:访问掩码 4:标志
一个进程在首次初始化的时候,其句柄表为空,即进程没有引用任何内核对象,当进程中的一个线程调用一个创建一个内核对象的函数时,内核将会为这个内核对象分配并初始化一个内存块,然后内核会扫描句柄表,找到一个空白的记录项,指针成员会指向刚创建的内核对象的内存地址,访问掩码会设置成拥有完全访问的权限,如果这个内核对象可以被继承,标志成员将设为1,如果不能被继承就是0。当调用关闭内核对象的函数Closehandle,引用该内核对象的进程中的句柄表中相应的记录项就会被清除,当进程退出的使用句柄表中的所有记录都会被清除,所有被使用的内核对象的使用计数都会减一,使用计数为0的内核对象就会自动销毁。就算没有手动关闭内核对象,进程退出了也不会出现内核对象泄露,当一个内核对象没有被任何进程引用时会自动销毁的。
跨进程边界共享内核对象
在很多时候需要再不同的进程中共享内核对象,如信号量,互斥量和事件允许不同的进程中的线程同步执行,这时就需要共享内核对象。共享内核对象的方式有三种:
1:使用内核对象句柄继承
2:为对象命名
3:复制对象句柄
1:使用内核对象句柄继承。只有进程之间有父子关系时才能使用内核对象句柄继承。若一个对象进程允许其子进程继承它的句柄,那么它在创建可被继承的内核对象时,创建内核对象的函数的参数SECURITY_ATTRIBUTES的成员bInheritHandle要设为true,这样当父进程创建一个子进程时,子进程就会复制父进程的句柄表中可以被继承的句柄到自己的句柄表中。每个进程都有自己的句柄表,父进程与子进程之间并不是共享句柄表,所以当子进程创建好了后,父进程在创建一个可以被继承的内核对象时,只进程并不能访问这个内核对象,因为它根本不知道这个内核对象的存在,当然子进程创建自己的内核对象时,父进程也是不知道的。因为微软没有写多于的代码来复制这些可被继承的句柄到相应的句柄表中。所以说内核对象并不是真的继承,而是继承了内核对象的句柄。也就是说句柄只是内核对象的一个指针,而不是内核对象的一个成员。
我们可以通过函数SetHandleInformation来标志内核对象是否可一个被子进程继承。如有一个可以被子进程继承的内核对象,但你不想让某个进程继承,在创建这个子进程之前将该内核对象标记为不可继承,在创建子进程,然后在调用函数SetHandleInformation将内核对象标记为可以继承,这个其他即将要创建的子进程就可以共享这个内核对象。
SetHandleInformation( __in HANDLE hObject,//内核对象的句柄 __in DWORD dwMask,//告诉函数想更改哪个或哪些标志 __in DWORD dwFlags//是否可以被继承 );
2:为对象命名。很多创建内核对象的函数都有一个参数lpName指定内核对象的名字,如以下两个函数:
CreateMutexA( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCSTR lpName ); CreateEventW( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCWSTR lpName );
最后一个参数lpName的值就是内核对象的名称,如果为这个参数传入NULL,表示不想为该内核对象命名。如下面的代码创建了一个名为CTH的互斥量内核对象。
HANDLE mutexProcessA=CreateMutex(NULL,FALSE,TEXT("CTH"));
另一个进程B也想创建一个互斥量内核对象,代码如下:
HANDLE mutexProcessB=CreateMutex(NULL,FALSE,TEXT("CTH"));
内核并不会马上创建一个互斥量内核对象,而是会先检查是否存在一个名为CTH的互斥两内核对象,若有,判断是否有权限,若有,会将已经存在的互斥量的句柄复制到进程B的句柄表中,将互斥量的句柄返回给mutexProcessB,若不存在名为CTH的对象当然是创建一个,若是存在一个名为CTH的其他类型的内核对象那就会创建失败,函数返回NULL。还是那句话内核对象属于操作系统,跟是哪个进程创建它关系并不大,这样也就很容易实现内核对象的共享了。
3:复制对象句柄。使用函数DuplicateHandle。
DuplicateHandle( HANDLE hSourceProcessHandle, HANDLE hSourceHandle, HANDLE hTargetProcessHandle, LPHANDLE lpTargetHandle, DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions );
这个函数获取一个进程句柄表中的一个记录项,然后在另一个进程的句柄表中创建该记录项的一个副本,然后另一个进程也就能访问这个内核对象了。