一、为什么要有句柄

句柄是一个8字节的结构体,用途是指向内核对象。3环程序无法通过地址直接访问内核对象,所以需要用句柄来间接访问。

本文重点介绍句柄表,句柄本身则留到下一篇博客介绍。但因为接下来介绍句柄表时也会涉及到句柄表项的成员,所以不妨先简单铺垫一下。

首先要区分开句柄和 HANDLE_TABLE_ENTRY ,句柄是3环使用的一个4字节整数,它是句柄表的索引,且一定是4的整数倍;而 HANDLE_TABLE_ENTRY 是句柄表里的项,大小是8字节,称为句柄表项。

HANDLE_TABLE_ENTRY 由两个32位的联合体(union) 组成:

typedef struct _HANDLE_TABLE_ENTRY {union {PVOID Object;                          // 指向句柄所代表的对象ULONG ObAttributes;                    // 最低三位有特别含义,参见// OBJ_HANDLE_ATTRIBUTES 宏定义PHANDLE_TABLE_ENTRY_INFO InfoTable;    // 各个句柄表页面的第一个表项// 使用此成员指向一张表ULONG_PTR Value;};union {union {ACCESS_MASK GrantedAccess;         // 访问掩码struct {                           // 当NtGlobalFlag 中包含// FLG_KERNEL_STACK_TRACE_DB 标记时使用USHORT GrantedAccessIndex;USHORT CreatorBackTraceIndex;};};LONG NextFreeTableEntry;               // 空闲时表示下一个空闲句柄索引};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

多数情况下,使用联合体的目的是节省内存,隐含的意思是联合体内部的成员使用的时机不同,不会发生冲突。Object 指向一个内核对象,而 NextFreeTableEntry 是下一个空闲句柄表项的索引,下文会详细介绍这个属性的用途。

一个进程创建或打开了一个内核对象,如 CreateProcess ,3环会得到一个句柄,而句柄是存储在0环的 EPROCESS.ObjectTable 里,这个 ObjectTable 称为进程句柄表。

二、句柄表结构

typedef struct _HANDLE_TABLE {////  A pointer to the top level handle table tree node.//ULONG_PTR TableCode;             // 指向句柄表的存储结构////  The process who is being charged quota for this handle table and a//  unique process id to use in our callbacks//struct _EPROCESS *QuotaProcess;     // 句柄表的内存资源记录在此进程中HANDLE UniqueProcessId;               // 创建进程的ID,用于回调函数//// These locks are used for table expansion and preventing the A-B-A problem// on handle allocate.//#define HANDLE_TABLE_LOCKS 4EX_PUSH_LOCK HandleTableLock[HANDLE_TABLE_LOCKS]; // 句柄表锁,扩展句柄表时使用////  The list of global handle tables.  This field is protected by a global//  lock.//LIST_ENTRY HandleTableList;           // 所有句柄表形成一个链表// 链表头为全局变量 HandleTableListHead//// Define a field to block on if a handle is found locked.//EX_PUSH_LOCK HandleContentionEvent;  // 若在访问句柄时发生竞争,则在此推锁上等待//// Debug info. Only allocated if we are debugging handles//PHANDLE_TRACE_DEBUG_INFO DebugInfo; // 调试信息,仅当调试句柄表时有意义////  The number of pages for additional info.//  This counter is used to improve the performance//  in ExGetHandleInfo//LONG ExtraInfoPages;              // 审计信息所占用的页面数量////  This is a singly linked list of free table entries.  We don't actually//  use pointers, but have each store the index of the next free entry//  in the list.  The list is managed as a lifo list.  We also keep track//  of the next index that we have to allocate pool to hold.//ULONG FirstFree;                   // 空闲链表表头的句柄索引//// We free handles to this list when handle debugging is on or if we see// that a thread has this handles bucket lock held. The allows us to delay reuse// of handles to get a better chance of catching offenders//ULONG LastFree;                     // 最近被释放的句柄索引,用于FIFO类型空闲链表//// This is the next handle index needing a pool allocation. Its also used as a bound// for good handles.//ULONG NextHandleNeedingPool;       // 下一次句柄表扩展的起始句柄索引////  The number of handle table entries in use.//LONG HandleCount;                   // 正在使用的句柄表项的数量//// Define a flags field//union {ULONG Flags;                   // 标志域//// For optimization we reuse handle values quickly. This can be a problem for// some usages of handles and makes debugging a little harder. If this// bit is set then we always use FIFO handle allocation.//BOOLEAN StrictFIFO : 1;            // 是否使用FIFO风格的重用,即先释放先重用};} HANDLE_TABLE, *PHANDLE_TABLE;

进程句柄表 HANDLE_TABLE 存储了进程拥有的所有句柄,其结构随着句柄数量的增加变化,它是一个多级结构,有三种形态:

HANDLE_TABLE 的 TableCode 指针指向了句柄表存储的位置,TableCode 的低2位表示句柄表的结构。考虑三层句柄表的情况,理论上能存储的句柄数量是 1024 x 1024 x 512 ,但实际上远小于这个值,规定进程拥有的句柄数最多为 MAX_HANDLE 个,即 2^24 个。另一个事实是,句柄表按页分配,每页是4KB,最低层句柄表能存储 4KB / 8B = 512 个句柄表项,但实际数量要减一,即511个,因为第一项有特殊用途。

三、句柄表的创建和初始化

ExCreateHandleTable

创建进程时,调用 ExCreateHandleTable 函数分配并初始化句柄表,这个函数本身代码比较简短,创建句柄表的工作交给了更底层的 ExpAllocateHandleTable 函数。

NTKERNELAPI
PHANDLE_TABLE
ExCreateHandleTable (__in_opt struct _EPROCESS *Process)/*++Routine Description:This function allocate and initialize a new new handle tableArguments:Process - Supplies an optional pointer to the process against which quotawill be charged.Return Value:If a handle table is successfully created, then the address of thehandle table is returned as the function value. Otherwise, a valueNULL is returned.--*/{PKTHREAD CurrentThread;PHANDLE_TABLE HandleTable;PAGED_CODE();// 从KPCR中获取当前线程CurrentThread = KeGetCurrentThread ();////  Allocate and initialize a handle table descriptor//// 句柄表的内存分配和初始化HandleTable = ExpAllocateHandleTable( Process, TRUE );if (HandleTable == NULL) {return NULL;}////  Insert the handle table in the handle table list.//KeEnterCriticalRegionThread (CurrentThread); // 暂时停用普通内核APCExAcquirePushLockExclusive( &HandleTableListLock );// HandleTableListHead 是全局句柄表链表头,将当前进程的句柄表插入到其尾部InsertTailList( &HandleTableListHead, &HandleTable->HandleTableList );ExReleasePushLockExclusive( &HandleTableListLock );KeLeaveCriticalRegionThread (CurrentThread); // 恢复普通内核APC////  And return to our caller//return HandleTable;
}

ExpAllocateHandleTable

ExpAllocateHandleTable 的工作是创建并初始化句柄表结构(HANDLE_TABLE)和一个最低层句柄表(TableCode)。如果 DoInit 参数为 TRUE,则需要初始化句柄表的空闲链表(FirstFree),空闲链表是用来指示下一个空闲句柄表项的位置的。大家可以先阅读源码,对这个空闲链表的初始化有一个大致的印象,待会我会详细分析句柄的插入和删除过程。

PHANDLE_TABLE
ExpAllocateHandleTable (IN PEPROCESS Process OPTIONAL,IN BOOLEAN DoInit)/*++Routine Description:This worker routine will allocate and initialize a new handle tablestructure.  The new structure consists of the basic handle tablestruct plus the first allocation needed to store handles.  This isreally one page divided up into the top level node, the first midlevel node, and one bottom level node.Arguments:Process - Optionally supplies the process to charge quota for thehandle tableDoInit - If FALSE then we are being called by duplicate and we don't needthe free list built for the callerReturn Value:A pointer to the new handle table or NULL if unsuccessful at gettingpool.--*/{PHANDLE_TABLE HandleTable;PHANDLE_TABLE_ENTRY HandleTableTable, HandleEntry;ULONG i, Idx;PAGED_CODE();////  If any allocation or quota failures happen we will catch it in the//  following try-except clause and cleanup after outselves before//  we return null//////  First allocate the handle table, make sure we got one, charge quota//  for it and then zero it out//// HANDLE_TABLE 申请分页内存HandleTable = (PHANDLE_TABLE)ExAllocatePoolWithTag (PagedPool,sizeof(HANDLE_TABLE),'btbO');if (HandleTable == NULL) {return NULL;}if (ARGUMENT_PRESENT(Process)) {if (!NT_SUCCESS (PsChargeProcessPagedPoolQuota( Process,sizeof(HANDLE_TABLE)))) {ExFreePool( HandleTable );return NULL;}}// HANDLE_TABLE 内存清零RtlZeroMemory( HandleTable, sizeof(HANDLE_TABLE) );////  Now allocate space of the top level, one mid level and one bottom//  level table structure.  This will all fit on a page, maybe two.//// 申请最高层句柄表内存,大小是一个页 4KB,创建进程时初始化,应该是只有一层句柄表的HandleTableTable = ExpAllocateTablePagedPoolNoZero ( Process,TABLE_PAGE_SIZE);if ( HandleTableTable == NULL ) {ExFreePool( HandleTable );if (ARGUMENT_PRESENT(Process)) {PsReturnProcessPagedPoolQuota (Process,sizeof(HANDLE_TABLE));}return NULL;}// TableCode 指向最高层句柄表,此时只有一层,所以也是最低层句柄表HandleTable->TableCode = (ULONG_PTR)HandleTableTable;////  We stamp with EX_ADDITIONAL_INFO_SIGNATURE to recognize in the future this//  is a special information entry//// 最低层句柄表的第0项有特殊用途// 高32位用 EX_ADDITIONAL_INFO_SIGNATURE 标记起来// 低32位初始化为NULLHandleEntry = &HandleTableTable[0];    HandleEntry->NextFreeTableEntry = EX_ADDITIONAL_INFO_SIGNATURE;    HandleEntry->Value = 0;//// For duplicate calls we skip building the free list as we rebuild it manually as// we traverse the old table we are duplicating// 对于重复调用,我们跳过构建空闲列表,因为我们在遍历要复制的旧表时手动重新构建它// if (DoInit) {// HandleEntry 指向了最低层句柄表下标为1的项HandleEntry++;////  Now setup the free list.  We do this by chaining together the free//  entries such that each free entry give the next free index (i.e.,//  like a fat chain).  The chain is terminated with a 0.  Note that//  we'll skip handle zero because our callers will get that value//  confused with null.// 通过遍历的方式初始化空闲句柄链表。每个链表项的 NextFreeTableEntry 成员存储了下一个空闲// 句柄的下标,空闲链表以0结尾。只遍历到下标510,因为最后一项要特殊处理。// 遍历第1-510项for (i = 1; i < LOWLEVEL_COUNT - 1; i += 1) {HandleEntry->Value = 0; // 不指向任何内核对象// NextFreeTableEntry 等于下一个空闲句柄的句柄索引值HandleEntry->NextFreeTableEntry = (i+1)*HANDLE_VALUE_INC;HandleEntry++;}// 第511项 NextFreeTableEntry 是0,表示链表结束HandleEntry->Value = 0;HandleEntry->NextFreeTableEntry = 0;// FirstFree 存储了当前第一个空闲句柄表项的索引// 因为第0项是特殊用途,所以第一个空闲句柄索引是4HandleTable->FirstFree = HANDLE_VALUE_INC;}// 下一次扩展句柄表时,起始句柄下标是 512 * 4HandleTable->NextHandleNeedingPool = LOWLEVEL_COUNT * HANDLE_VALUE_INC;//// Setup the necessary process information// 设置必须的进程信息HandleTable->QuotaProcess = Process;HandleTable->UniqueProcessId = PsGetCurrentProcess()->UniqueProcessId;HandleTable->Flags = 0;#if DBG && !EXHANDLE_EXTRA_CHECKSif (Process != NULL) {HandleTable->StrictFIFO = TRUE;}
#endif////  Initialize the handle table lock. This is only used by table expansion.// 初始化句柄表锁,用于扩展句柄表for (Idx = 0; Idx < HANDLE_TABLE_LOCKS; Idx++) {ExInitializePushLock (&HandleTable->HandleTableLock[Idx]);}////  Initialize the blocker for handle entry lock contention.// 初始化句柄表入口锁,用于互斥访问ExInitializePushLock (&HandleTable->HandleContentionEvent);if (TRACE_ALL_TABLES) {ExEnableHandleTracing (HandleTable, 0);    }////  And return to our caller//return HandleTable;
}

四、句柄表的扩展

ExpAllocateHandleTableEntrySlow

扩展句柄表是由 ExpAllocateHandleTableEntrySlow 函数实现的。分析函数之前,我先把示意图给出大家。

首先,考虑从一层扩展到二层的情况,当出现这种情况,说明现在进程拥有了511个句柄,一个页面已经存不下新的句柄了,所以需要扩展成二层结构。做法是调用 ExpAllocateMidLevelTable 函数创建中间层句柄表,ExpAllocateMidLevelTable 函数调用了 ExpAllocateLowLevelTable 函数来创建一个新的最低层句柄表。

接下来,考虑二层扩展到三层的情况:

最后,是三层结构情况下,新增中间层句柄表的情况:

扩展完之后还有一些操作,需要先搞明白句柄如何插入,FirstFree 如何使用,才能分析。

BOOLEAN
ExpAllocateHandleTableEntrySlow (IN PHANDLE_TABLE HandleTable,IN BOOLEAN DoInit)/*++Routine Description:This worker routine allocates a new handle table entry for the specifiedhandle table.这个函数为指定的句柄表分配一个新的句柄表项Note: The caller must have already locked the handle table注意:调用者必须先锁住句柄表Arguments:HandleTable - Supplies the handle table being usedDoInit - If FALSE then the caller (duplicate) doesn't need the free list built如果 FALSE 就不需要构建空闲链表Return Value:BOOLEAN - TRUE, Retry the fast allocation path, FALSE, We failed to allocate memory--*/{ULONG i,j;PHANDLE_TABLE_ENTRY NewLowLevel;PHANDLE_TABLE_ENTRY *NewMidLevel;PHANDLE_TABLE_ENTRY **NewHighLevel;ULONG NewFree, OldFree;ULONG OldIndex;PVOID OldValue;ULONG_PTR CapturedTable = HandleTable->TableCode;ULONG TableLevel = (ULONG)(CapturedTable & LEVEL_CODE_MASK); // 当前句柄表层级结构,取 0, 1, 2PAGED_CODE();//// Initializing NewLowLevel is not needed for// correctness but without it the compiler cannot compile this code// W4 to check for use of uninitialized variables.//NewLowLevel = NULL; // 为了编译通过必须初始化// CapturedTable 存储了 TableCode 第2位清零的值,即旧的最低层句柄表的地址CapturedTable = CapturedTable & ~LEVEL_CODE_MASK;if ( TableLevel == 0 ) {////  We have a single level. We need to ad a mid-layer//  to the process handle table// 现在是单层句柄表结构,需要扩展为二层句柄表// 创建一个中间层句柄表,同时也创建了一个最低层句柄表,存储在中间层句柄表的第0项NewMidLevel = ExpAllocateMidLevelTable( HandleTable, DoInit, &NewLowLevel );if (NewMidLevel == NULL) {return FALSE;}////  Since ExpAllocateMidLevelTable initialize the //  first position with a new table, we need to move it in //  the second position, and store in the first position the current one// 因为 ExpAllocateMidLevelTable 的第0项存储了新的最低层句柄表,现在我们// 把它移动到第1项,然后将旧的最低层句柄表存到第0项。NewMidLevel[1] = NewMidLevel[0];NewMidLevel[0] = (PHANDLE_TABLE_ENTRY)CapturedTable;////  Encode the current level and set it to the handle table process// 修改 TableCode 的第2位,改成1,表示现在是二层句柄表结构// CapturedTable = ((ULONG_PTR)NewMidLevel) | 1;OldValue = InterlockedExchangePointer( (PVOID *)&HandleTable->TableCode, (PVOID)CapturedTable );} else if (TableLevel == 1) {////  We have a 2 levels handle table// 现在是二层句柄表结构//PHANDLE_TABLE_ENTRY *TableLevel2 = (PHANDLE_TABLE_ENTRY *)CapturedTable;////  Test whether the index we need to create is still in the //  range for a 2 layers table// 检查当前的句柄表下标是否真的需要扩展到三层结构// 解释:当已经存了 1024 x 512 个句柄时,即 NextHandleNeedingPool = 1024 x 512 x 4 ,// 这时需要扩展。换言之,如果需要扩展,下面算出的 i 不应小于 1024// // i 表示当前需要多少张中层句柄表,如果小于 1024 就不需要扩展到三层结构i = HandleTable->NextHandleNeedingPool / (LOWLEVEL_COUNT * HANDLE_VALUE_INC);if (i < MIDLEVEL_COUNT) {////  We just need to allocate a new low-level//  table// 二层结构还够用,我们只需要申请一张新的最低层句柄表// NewLowLevel = ExpAllocateLowLevelTable( HandleTable, DoInit );if (NewLowLevel == NULL) {return FALSE;}////  Set the new one to the table, at appropriate position//OldValue = InterlockedExchangePointer( (PVOID *) (&TableLevel2[i]), NewLowLevel );EXASSERT (OldValue == NULL);} else {////  We exhausted the 2 level domain. We need to insert a new one// 我们已经用完了二层结构的空间,真的需要扩展到三层结构了// 最高层句柄表只需 HIGHLEVEL_SIZE = 32 x 4 = 128 个字节// 因为一个进程只能存 1<<24 个句柄,所以最高层句柄表最多只能存32张中间层句柄表// 32 x 1024 x 512 = 1 << 24// NewHighLevel = ExpAllocateTablePagedPool( HandleTable->QuotaProcess,HIGHLEVEL_SIZE);if (NewHighLevel == NULL) {return FALSE;}// 既然需要扩展到三层结构,说明一张中间层句柄表已经不够用// 自然需要新创建一张中间层句柄表NewMidLevel = ExpAllocateMidLevelTable( HandleTable, DoInit, &NewLowLevel );if (NewMidLevel == NULL) {ExpFreeTablePagedPool( HandleTable->QuotaProcess,NewHighLevel,HIGHLEVEL_SIZE);return FALSE;}////  Initialize the first index with the previous mid-level layer// 把原来的中间层句柄表放在第0项,新创建的放在第1项//NewHighLevel[0] = (PHANDLE_TABLE_ENTRY*)CapturedTable;NewHighLevel[1] = NewMidLevel;////  Encode the level into the table pointer// 标记低2位为2,表示现在是三层结构//CapturedTable = ((ULONG_PTR)NewHighLevel) | 2;////  Change the handle table pointer with this one// 更新 TableCode 的值,指向最高层句柄表OldValue = InterlockedExchangePointer( (PVOID *)&HandleTable->TableCode, (PVOID)CapturedTable );}} else if (TableLevel == 2) {////  we have already a table with 3 levels// 当前已经是三级句柄表结构//ULONG RemainingIndex;PHANDLE_TABLE_ENTRY **TableLevel3 = (PHANDLE_TABLE_ENTRY **)CapturedTable;// i 算出来表示新的中间层句柄表在最高层句柄表中的下标,不应大于31i = HandleTable->NextHandleNeedingPool / (MIDLEVEL_THRESHOLD * HANDLE_VALUE_INC);////  Check whether we exhausted all possible indexes.// 检查是否已经用完了 HIGHLEVEL_COUNT(32)个下标,如果是,返回 FALSE//// 不能大于31if (i >= HIGHLEVEL_COUNT) {return FALSE;}if (TableLevel3[i] == NULL) {////  The new available handle points to a free mid-level entry//  We need then to allocate a new one and save it in that position// 这个下标还没有指向中间层句柄表,则创建一个新的//// 创建一个新的中间层句柄表NewMidLevel = ExpAllocateMidLevelTable( HandleTable, DoInit, &NewLowLevel );if (NewMidLevel == NULL) {return FALSE;}             // 存储到最高层句柄表OldValue = InterlockedExchangePointer( (PVOID *) &(TableLevel3[i]), NewMidLevel );EXASSERT (OldValue == NULL);} else {////  We have already a mid-level table. We just need to add a new low-level one//  at the end// 这个下标已经有一个中间层句柄表了,只需往里面新增一个最低层句柄表//// j 表示在当前中间层句柄表的第 j 项插入一个新的最低层句柄表RemainingIndex = (HandleTable->NextHandleNeedingPool / HANDLE_VALUE_INC) -i * MIDLEVEL_THRESHOLD;j = RemainingIndex / LOWLEVEL_COUNT;NewLowLevel = ExpAllocateLowLevelTable( HandleTable, DoInit );if (NewLowLevel == NULL) {return FALSE;}OldValue = InterlockedExchangePointer( (PVOID *)(&TableLevel3[i][j]) , NewLowLevel );EXASSERT (OldValue == NULL);}}//// This must be done after the table pointers so that new created handles// are valid before being freed.// 更新 NextHandleNeedingPool 的值,NextHandleNeedingPool 表示下一次扩展时新句柄的值。// 就是简单地原子加 512 * 4 ,因为不管怎么扩展,最终都是新增了一张最低层句柄表。// OldIndex 存储了 NextHandleNeedingPool 的旧值,即新的最低层句柄表的第0项//OldIndex = InterlockedExchangeAdd ((PLONG) &HandleTable->NextHandleNeedingPool,LOWLEVEL_COUNT * HANDLE_VALUE_INC);if (DoInit) {//// Generate a new sequence number since this is a push// 跳过第0项,此时 OldIndex 等于新的最低层句柄表的第1项//OldIndex += HANDLE_VALUE_INC + GetNextSeq(); // GetNextSeq = 0//// Now free the handles. These are all ready to be accepted by the lookup logic now.//// FirstFree 的用途不太清楚,所以看不懂下面的代码// FirstFree 的用途不太清楚,所以看不懂下面的代码// FirstFree 的用途不太清楚,所以看不懂下面的代码// FirstFree 的用途不太清楚,所以看不懂下面的代码// FirstFree 的用途不太清楚,所以看不懂下面的代码// FirstFree 的用途不太清楚,所以看不懂下面的代码// FirstFree 的用途不太清楚,所以看不懂下面的代码// FirstFree 的用途不太清楚,所以看不懂下面的代码// FirstFree 的用途不太清楚,所以看不懂下面的代码// FirstFree 的用途不太清楚,所以看不懂下面的代码while (1) {// 新的最低层句柄表的最后一项的 NextFreeTableEntry 等于旧的 FirstFree// 创建新句柄时,新句柄直接存到 FirstFree 指向的位置,然后把新句柄的// NextFreeTableEntry 赋给 FirstFree。OldFree = ReadForWriteAccess (&HandleTable->FirstFree);NewLowLevel[LOWLEVEL_COUNT - 1].NextFreeTableEntry = OldFree;//// These are new entries that have never existed before. We can't have an A-B-A problem// with these so we don't need to take any locks//// 比较 HandleTable->FirstFree 和 OldFree,// 如果相等,则 OldIndex 赋给 HandleTable->FirstFree// 返回值 NewFree 是 HandleTable->FirstFree 的旧值,// 所以如果相等,循环也就结束了NewFree = InterlockedCompareExchange ((PLONG)&HandleTable->FirstFree,OldIndex,OldFree);if (NewFree == OldFree) {break;}}}return TRUE;
}

ExpAllocateMidLevelTable

这个函数创建一个中间层句柄表,是一个存储最低层句柄表指针的数组。

PHANDLE_TABLE_ENTRY *
ExpAllocateMidLevelTable (IN PHANDLE_TABLE HandleTable,IN BOOLEAN DoInit,OUT PHANDLE_TABLE_ENTRY *pNewLowLevel)/*++Routine Description:This worker routine allocates a mid-level table. This is an array withpointers to low-level tables.这个函数创建一个中间层句柄表,是一个存储最低层句柄表指针的数组。It will allocate also a low-level table and will save it in the first index同时会申请一个新的最低层句柄表,并存储到中间层句柄表的第0项Note: The caller must have already locked the handle table调用者必须先锁住句柄表Arguments:HandleTable - Supplies the handle table being used当前的单层句柄表DoInit - If FALSE the caller (duplicate) does not want the free list build是否构造空闲链表pNewLowLevel - Returns the new low level table for later free list chaining返回新的最低层句柄表Return Value:Returns a pointer to the new mid-level table allocated返回新创建的中间层句柄表--*/{PHANDLE_TABLE_ENTRY *NewMidLevel; // 中间层句柄表,是一个数组,存储的是最低层句柄表PHANDLE_TABLE_ENTRY NewLowLevel; // 新增的最低层句柄表,存到下标0的位置// 创建中间层句柄表NewMidLevel = ExpAllocateTablePagedPool( HandleTable->QuotaProcess,PAGE_SIZE);if (NewMidLevel == NULL) {return NULL;}////  If we need a new mid-level, we'll need a low-level too.//  We'll create one and if success we'll save it at the first position// 创建中间层句柄表意味着一张最低层句柄表不够用了,所以需要一张新的最低层句柄表// NewLowLevel = ExpAllocateLowLevelTable( HandleTable, DoInit );if (NewLowLevel == NULL) {ExpFreeTablePagedPool( HandleTable->QuotaProcess,NewMidLevel,PAGE_SIZE);return NULL;}////  Set the low-level table at the first index//// 新创建的最低层句柄表存储到中间层句柄表的第0项NewMidLevel[0] = NewLowLevel;// 输出参数返回新创建的最低层句柄表*pNewLowLevel = NewLowLevel;// 返回新创建的中间层句柄表return NewMidLevel;
}

ExpAllocateLowLevelTable

这个函数创建并返回一个新的最低层句柄表。如果 DoInit 为真,就会构造空闲句柄表项链表,它会根据句柄表的 NextHandleNeedingPool 的值,和旧的空闲链表连接起来。

PHANDLE_TABLE_ENTRY
ExpAllocateLowLevelTable (IN PHANDLE_TABLE HandleTable,IN BOOLEAN DoInit)/*++Routine Description:This worker routine allocates a new low level table创建一个新的最低层句柄表。Note: The caller must have already locked the handle table调用者须先锁住句柄表Arguments:HandleTable - Supplies the handle table being usedDoInit - If FALSE the caller (duplicate) doesn't need the free list maintainedReturn Value:Returns - a pointer to a low-level table if allocation issuccessful otherwise the return value is null.如果成功,返回新的最低层句柄表--*/{ULONG k;PHANDLE_TABLE_ENTRY NewLowLevel = NULL, HandleEntry;ULONG BaseHandle;////  Allocate the pool for lower level//NewLowLevel = ExpAllocateTablePagedPoolNoZero( HandleTable->QuotaProcess,TABLE_PAGE_SIZE);if (NewLowLevel == NULL) {return NULL;}////  We stamp with EX_ADDITIONAL_INFO_SIGNATURE to recognize in the future this//  is a special information entry// 第0项特殊标记HandleEntry = &NewLowLevel[0];HandleEntry->NextFreeTableEntry = EX_ADDITIONAL_INFO_SIGNATURE;HandleEntry->Value = 0;//// Initialize the free list within this page if the caller wants this//if (DoInit) {HandleEntry++;////  Now add the new entries to the free list.  To do this we//  chain the new free entries together.  We are guaranteed to//  have at least one new buffer.  The second buffer we need//  to check for.////  We reserve the first entry in the table to the structure with//  additional info//////  Do the guaranteed first buffer//// 这里是初始化空闲链表,NextHandleNeedingPool 是新的最低层句柄表的// 第一项的下标,例如 512 * 4。这里 BaseHandle 存储的是第一个空闲项的下标,// 最低层句柄表的下标0是特殊用途,第一项是新插入的句柄,所以第一个空闲// 位置应该是第三项,所以要加上 2 * HANDLE_VALUE_INCBaseHandle = HandleTable->NextHandleNeedingPool + 2 * HANDLE_VALUE_INC;// 遍历到这张表的倒数第二项(下标510),最后一项特殊处理for (k = BaseHandle; k < BaseHandle + (LOWLEVEL_COUNT - 2) * HANDLE_VALUE_INC; k += HANDLE_VALUE_INC) {HandleEntry->NextFreeTableEntry = k;HandleEntry->Value = 0;HandleEntry++;}// 空闲链表结束标记HandleEntry->NextFreeTableEntry = 0;HandleEntry->Value = 0;}return NewLowLevel;
}

五、句柄的插入和删除

模拟句柄表插入删除算法

HANDLE_TABLE 结构的 FirstFree 成员记录了空闲句柄链,需要创建新的句柄时,调用 ExpAllocateHandleTableEntry 函数直接从 FirstFree 得到句柄表中下一个空闲句柄的位置,把新句柄存进去,然后把原 FirstFree 位置的那个空闲句柄表项的 NextFreeTableEntry 赋给 FirstFree。

这实际上是用数组(最低层句柄表),FirstFree 和 NextFreeTableEntry 模拟了一个空闲句柄链表,不过与其说是链表,我觉得它更像一个栈,因为它是遵循 LIFO 规则的。但是这也有一个例外,据我了解,PspCidTable 句柄表由于设置了 StrictFIFO 属性,所以这个句柄表是遵循 FIFO 规则的。

我们姑且认为,除了 PspCidTable 以外, 所有进程的句柄表 StrictFIFO 都是0,这就是说,它们的句柄表都是后释放,先重用的。如果您对 LIFO 不太理解,我这里给出一个程序,是我模拟句柄表增删数据写的一个例程,通过这个程序可以很直观的感受句柄表的增删规律。

// test.cpp : 定义控制台应用程序的入口点。
//#include "stdafx.h"struct Node
{int used;int Next;
};// 数组模拟链表
// 值表示下一个空闲下标
Node List[10];
int NextFree = 1; // 当前空闲下标,0用作特殊用途void AllocHandle()
{if (NextFree == 0){printf("句柄表空间耗尽,请先释放句柄.\n");return;}printf("获取新句柄:%d\n", NextFree);List[NextFree].used = 1;NextFree = List[NextFree].Next;printf("当前空闲句柄:%d\n", NextFree);
}void FreeHandle(int x)
{if (List[x].used == 0){printf("不能释放一个未使用的句柄.\n");return;}List[x].used = 0;printf("释放了一个句柄:%d\n", x);int temp = NextFree;NextFree = x;printf("当前空闲句柄:%d\n", NextFree);List[x].Next = temp;}void PrintList()
{for (int i = 0; i < 10; i++){printf("%d ", i);}printf("\n");for (int i = 0; i < 10; i++){if (List[i].used)printf("* ");elseprintf("  ");}printf("\n");for (int i = 0; i < 10; i++){printf("%d ", List[i].Next);}printf("\n");printf("----------------------------------------------\n");printf("----------------------------------------------\n");
}int _tmain(int argc, _TCHAR* argv[])
{for (int i = 1; i < 9; i++){List[i].Next = i + 1;List[i].used = 0;}PrintList();while (1){int n;printf("申请句柄请输入-1,释放句柄请输入句柄号码:");scanf("%d", &n);getchar();if (n == -1){AllocHandle();}else{FreeHandle(n);}PrintList();}return 0;
}

ExpAllocateHandleTableEntry

这个函数的功能是从句柄表里找一个空闲位置,然后把新句柄插入进去。它调用了 ExpMoveFreeHandles 函数,我分析过这个 ExpMoveFreeHandles 函数,不敢说完全弄懂了,不过大概用途应该是检查 LastFree 队列是否有句柄,LastFree 存的是使用过但已经释放的句柄,据我观察,绝大多数进程的句柄表是不使用这个值的,所以在分析进程句柄表的时候,可以忽略这个函数,它啥也没做。不过正如我所说,我不敢保证我的分析是对的,所以我也会贴出我对这个函数的部分分析,仅供参考。

此外,如果当前最低层句柄表空间耗尽,还会调用 ExpAllocateHandleTableEntrySlow 函数来扩展句柄表,这个已经分析过了,此处不表。函数返回值是句柄表项的地址,这个地址是通过 ExpLookupHandleTableEntry 函数获取的。

插入成功后,句柄计数加一,这些细节在代码中能看到。

下面给出我的分析结果,即 ExpAllocateHandleTableEntry,ExpMoveFreeHandles,ExpLookupHandleTableEntry 的注释。

再次强调,ExpMoveFreeHandles 的分析结果可能并不准确。

PHANDLE_TABLE_ENTRY
ExpAllocateHandleTableEntry (IN PHANDLE_TABLE HandleTable,OUT PEXHANDLE pHandle)
/*++Routine Description:This routine does a fast allocate of a free handle. It's lock free ifpossible.快速分配一个句柄表项,Only the rare case of handle table expansion is covered by the handletable lock.Arguments:HandleTable - Supplies the handle table being allocated from.pHandle - Handle returnedReturn Value:PHANDLE_TABLE_ENTRY - The allocated handle table entry pointer or NULLon failure.--*/
{PKTHREAD CurrentThread;ULONG OldValue, NewValue, NewValue1;PHANDLE_TABLE_ENTRY Entry;EXHANDLE Handle;BOOLEAN RetVal;ULONG Idx;CurrentThread = KeGetCurrentThread ();while (1) {// FirstFree 是下一个空闲句柄的索引OldValue = HandleTable->FirstFree;// 如果没有空位了,就扩展句柄表while (OldValue == 0) {////  Lock the handle table for exclusive access as we will be//  allocating a new table level.//// 锁住句柄表,为了互斥访问,因为现在要扩展句柄表了ExpLockHandleTableExclusive (HandleTable, CurrentThread);//// If we have multiple threads trying to expand the table at// the same time then by just acquiring the table lock we// force those threads to complete their allocations and// populate the free list. We must check the free list here// so we don't expand the list twice without needing to.//// 如果有多个线程同时尝试扩展句柄表,只需要把句柄表锁住,// 等待其他线程完成操作,当我们获得操作权限时,必须先检查// 其他线程是否已经扩展了句柄表,避免重复扩展。//// FirstFree 有空位了,因为其他线程已经扩展了句柄表OldValue = HandleTable->FirstFree;if (OldValue != 0) {ExpUnlockHandleTableExclusive (HandleTable, CurrentThread);break;}//// See if we have any handles on the alternate free list// These handles need some locking to move them over.//// 检查 LastFree 队列是否有空闲句柄位置,如果没有(LastFree == 0)就啥也不干//// FirstFree 是当前空闲队列,LastFree 是已使用但被释放了的空闲队列// 如果 StrictFIFO == 0 并且 FirstFree 已经为空,就把 LastFree 给到 FirstFree//OldValue = ExpMoveFreeHandles (HandleTable);if (OldValue != 0) {ExpUnlockHandleTableExclusive (HandleTable, CurrentThread);break;}//// This must be the first thread attempting expansion or all the// free handles allocated by another thread got used up in the gap.// 必须是第一个要求扩展句柄表的线程,或者是别的线程扩展的空间已经用完//// 尝试扩展句柄表RetVal = ExpAllocateHandleTableEntrySlow (HandleTable, TRUE);// 句柄表解锁ExpUnlockHandleTableExclusive (HandleTable, CurrentThread);OldValue = HandleTable->FirstFree;//// If ExpAllocateHandleTableEntrySlow had a failed allocation// then we want to fail the call.  We check for free entries// before we exit just in case they got allocated or freed by// somebody else in the gap.//             //if (!RetVal) {if (OldValue == 0) {// 扩展失败的原因是句柄表空间用尽pHandle->GenericHandleOverlay = NULL;return NULL;}            }}// 设置句柄值,如果失败,就是0,否则就是 FirstFreeHandle.Value = (OldValue & FREE_HANDLE_MASK);// 找到新句柄在句柄表的位置Entry = ExpLookupHandleTableEntry (HandleTable, Handle);Idx = ((OldValue & FREE_HANDLE_MASK)>>2) % HANDLE_TABLE_LOCKS;ExpLockHandleTableShared (HandleTable, CurrentThread, Idx);if (OldValue != *(volatile ULONG *) &HandleTable->FirstFree) {ExpUnlockHandleTableShared (HandleTable, CurrentThread, Idx);continue;}KeMemoryBarrier ();// 获取新句柄里存储的 NextFreeTableEntry ,这个值的含义是下一个空闲句柄的位置NewValue = *(volatile ULONG *) &Entry->NextFreeTableEntry;// 更新 FirstFree ,指向下一个空闲句柄位置NewValue1 = InterlockedCompareExchange ((PLONG)&HandleTable->FirstFree,NewValue,OldValue);ExpUnlockHandleTableShared (HandleTable, CurrentThread, Idx);if (NewValue1 == OldValue) {EXASSERT ((NewValue & FREE_HANDLE_MASK) < HandleTable->NextHandleNeedingPool);break;} else {//// We should have eliminated the A-B-A problem so if only the sequence number has// changed we are broken.//EXASSERT ((NewValue1 & FREE_HANDLE_MASK) != (OldValue & FREE_HANDLE_MASK));}}// 句柄计数加一InterlockedIncrement (&HandleTable->HandleCount);// 输出参数返回句柄值*pHandle = Handle;// 返回句柄表项的地址return Entry;
}

ExpMoveFreeHandles

ULONG
ExpMoveFreeHandles (IN PHANDLE_TABLE HandleTable)
{ULONG OldValue, NewValue;ULONG Index, OldIndex, NewIndex, FreeSize;PHANDLE_TABLE_ENTRY Entry, FirstEntry;EXHANDLE Handle;ULONG Idx;BOOLEAN StrictFIFO;//// First remove all the handles from the free list so we can add them to the// list we use for allocates.//// 如果 LastFree == 0,就啥也不干,直接返回OldValue = InterlockedExchange ((PLONG)&HandleTable->LastFree,0);Index = OldValue;if (Index == 0) {//// There are no free handles.  Nothing to do.// 如果 LastFree 本来就是0,说明没有空闲句柄,啥也不做返回//return OldValue;}//// We are pushing old entries onto the free list.// We have the A-B-A problem here as these items may have been moved here because// another thread was using them in the pop code.//for (Idx = 1; Idx < HANDLE_TABLE_LOCKS; Idx++) {ExAcquireReleasePushLockExclusive (&HandleTable->HandleTableLock[Idx]);}StrictFIFO = HandleTable->StrictFIFO;//// If we are strict FIFO then reverse the list to make handle reuse rare.//// 系统中唯一一个StrictFIFO=1的句柄表是PspCidTable,普通进程 StrictFIFO == 0if (!StrictFIFO) {//// We have a complete chain here. If there is no existing chain we// can just push this one without any hassles. If we can't then// we can just fall into the reversing code anyway as we need// to find the end of the chain to continue it.////// This is a push so create a new sequence number//// 如果不要求FIFO,且 FirstFree 链表为空,则 FirstFree = LastFreeif (InterlockedCompareExchange ((PLONG)&HandleTable->FirstFree,OldValue + GetNextSeq(),0) == 0) {return OldValue;}}//// Loop over all the entries and reverse the chain.// 反转链表,保证 FIFOFreeSize = OldIndex = 0;FirstEntry = NULL;while (1) {FreeSize++;Handle.Value = Index;Entry = ExpLookupHandleTableEntry (HandleTable, Handle);EXASSERT (Entry->Object == NULL);NewIndex = Entry->NextFreeTableEntry;Entry->NextFreeTableEntry = OldIndex;if (OldIndex == 0) {FirstEntry = Entry;}OldIndex = Index;if (NewIndex == 0) {break;}Index = NewIndex;}NewValue = ExpInterlockedExchange (&HandleTable->FirstFree,OldIndex,FirstEntry);//// If we haven't got a pool of a few handles then force// table expansion to keep the free handle size high//if (FreeSize < 100 && StrictFIFO) {OldValue = 0;}return OldValue;
}

ExpLookupHandleTableEntry

这个比较简单,看一遍源码应该就懂了。

PHANDLE_TABLE_ENTRY
ExpLookupHandleTableEntry (IN PHANDLE_TABLE HandleTable,IN EXHANDLE tHandle)/*++Routine Description:This routine looks up and returns the table entry for thespecified handle value.查找句柄在句柄表里的地址Arguments:HandleTable - Supplies the handle table being queriedtHandle - Supplies the handle value being queriedReturn Value:Returns a pointer to the corresponding table entry for the inputhandle.  Or NULL if the handle value is invalid (i.e., too largefor the tables current allocation.--*/{ULONG_PTR i,j,k;ULONG_PTR CapturedTable;ULONG TableLevel;PHANDLE_TABLE_ENTRY Entry = NULL;EXHANDLE Handle;PUCHAR TableLevel1;PUCHAR TableLevel2;PUCHAR TableLevel3;ULONG_PTR MaxHandle;PAGED_CODE();//// Extract the handle index//Handle = tHandle;Handle.TagBits = 0;// 句柄的范围小于 NextHandleNeedingPoolMaxHandle = *(volatile ULONG *) &HandleTable->NextHandleNeedingPool;//// See if this can be a valid handle given the table levels.// 检查句柄是否超过范围,是就返回NULL//if (Handle.Value >= MaxHandle) {return NULL;        }//// Now fetch the table address and level bits. We must preserve the// ordering here.//CapturedTable = *(volatile ULONG_PTR *) &HandleTable->TableCode;////  we need to capture the current table. This routine is lock free//  so another thread may change the table at HandleTable->TableCode//// 获取句柄表层级结构TableLevel = (ULONG)(CapturedTable & LEVEL_CODE_MASK);// 计算最高层句柄表地址CapturedTable = CapturedTable - TableLevel;////  The lookup code depends on number of levels we have//switch (TableLevel) {case 0:////  We have a simple index into the array, for a single level//  handle table//TableLevel1 = (PUCHAR) CapturedTable;//// The index for this level is already scaled by a factor of 4. Take advantage of this//Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[Handle.Value *(sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)];break;case 1:////  we have a 2 level handle table. We need to get the upper index//  and lower index into the array//TableLevel2 = (PUCHAR) CapturedTable;i = Handle.Value % (LOWLEVEL_COUNT * HANDLE_VALUE_INC);Handle.Value -= i;j = Handle.Value / ((LOWLEVEL_COUNT * HANDLE_VALUE_INC) / sizeof (PHANDLE_TABLE_ENTRY));TableLevel1 =  (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel2[j];Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[i * (sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)];break;case 2:////  We have here a three level handle table.//TableLevel3 = (PUCHAR) CapturedTable;i = Handle.Value  % (LOWLEVEL_COUNT * HANDLE_VALUE_INC);Handle.Value -= i;k = Handle.Value / ((LOWLEVEL_COUNT * HANDLE_VALUE_INC) / sizeof (PHANDLE_TABLE_ENTRY));j = k % (MIDLEVEL_COUNT * sizeof (PHANDLE_TABLE_ENTRY));k -= j;k /= MIDLEVEL_COUNT;TableLevel2 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel3[k];TableLevel1 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel2[j];Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[i * (sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)];break;default :_assume (0);}return Entry;
}

ExpFreeHandleTableEntry

和插入比较类似,删除操作调用的函数是 ExpFreeHandleTableEntry。如果您阅读并运行了我给出的模拟程序,这部分应该是很好理解的,直接给出源码注释。

VOID
ExpFreeHandleTableEntry (IN PHANDLE_TABLE HandleTable,IN EXHANDLE Handle,IN PHANDLE_TABLE_ENTRY HandleTableEntry)/*++Routine Description:This worker routine returns the specified handle table entry to the freelist for the handle table.Note: The caller must have already locked the handle tableArguments:HandleTable - Supplies the parent handle table being modifiedHandle - Supplies the handle of the entry being freedHandleTableEntry - Supplies the table entry being freedReturn Value:None.--*/{PHANDLE_TABLE_ENTRY_INFO EntryInfo;ULONG OldFree, NewFree, *Free;PKTHREAD CurrentThread;ULONG Idx;ULONG SeqInc;PAGED_CODE();EXASSERT (HandleTableEntry->Object == NULL);EXASSERT (HandleTableEntry == ExpLookupHandleTableEntry (HandleTable, Handle));////  Clear the AuditMask flags if these are present into the table//EntryInfo = ExGetHandleInfo(HandleTable, Handle.GenericHandleOverlay, TRUE);if (EntryInfo) {EntryInfo->AuditMask = 0;}////  A free is simply a push onto the free table entry stack, or in the//  debug case we'll sometimes just float the entry to catch apps who//  reuse a recycled handle value.//// 句柄计数减一InterlockedDecrement (&HandleTable->HandleCount);CurrentThread = KeGetCurrentThread ();// 句柄值第2位清零存到 NewFree ,正常来说句柄值是4的倍数,低2位肯定是00// 待会要用 NewFree 来设置 FirstFreeNewFree = (ULONG) Handle.Value & ~(HANDLE_VALUE_INC - 1);#if DBGif (ExReuseHandles) {#endif //DBGif (!HandleTable->StrictFIFO) {//// We are pushing potentially old entries onto the free list.// Prevent the A-B-A problem by shifting to an alternate list// read this element has the list head out of the loop.//Idx = (NewFree>>2) % HANDLE_TABLE_LOCKS;if (ExTryAcquireReleasePushLockExclusive (&HandleTable->HandleTableLock[Idx])) {SeqInc = GetNextSeq();Free = &HandleTable->FirstFree; // 走这} else {SeqInc = 0;Free = &HandleTable->LastFree;}} else {SeqInc = 0;Free = &HandleTable->LastFree;}while (1) {OldFree = ReadForWriteAccess (Free);// 释放的句柄的 NextFreeTableEntry 等于原来的 FirstFreeHandleTableEntry->NextFreeTableEntry = OldFree;// 更新 FirstFreeif ((ULONG)InterlockedCompareExchange ((PLONG)Free,NewFree + SeqInc,OldFree) == OldFree) {EXASSERT ((OldFree & FREE_HANDLE_MASK) < HandleTable->NextHandleNeedingPool);break;}}#if DBG} else {HandleTableEntry->NextFreeTableEntry = 0;}
#endif //DBGreturn;
}

进程句柄表初始化,扩展,插入删除句柄源码分析相关推荐

  1. hadoop作业初始化过程详解(源码分析第三篇)

    (一)概述 我们在上一篇blog已经详细的分析了一个作业从用户输入提交命令到到达JobTracker之前的各个过程.在作业到达JobTracker之后初始化之前,JobTracker会通过submit ...

  2. PHP扩展编写、PHP扩展调试、VLD源码分析、基于嵌入式Embed SAPI实现opcode查看

    catalogue 1. 编译PHP源码 2. 扩展结构.优缺点 3. 使用PHP原生扩展框架wizard ext_skel编写扩展 4. 编译安装VLD 5. Debug调试VLD 6. VLD源码 ...

  3. LinkedList中查询(contains)和删除(remove)源码分析

    一.contains源码分析 本文分析双向链表LinkedList的查询操作源码实现.jdk中源程序中,LinkedList的查询操作,通过contains(Object o)函数实现.具体见下面两部 ...

  4. linux源码分析之cpu初始化 kernel/head.s,linux源码分析之cpu初始化

    linux源码分析之cpu初始化 kernel/head.s 收藏 来自:http://blog.csdn.net/BoySKung/archive/2008/12/09/3486026.aspx l ...

  5. django之:网页伪静态 JsonResponse form表单携带文件数据 CBV源码分析 模板语法传值 模板语法之过滤器 标签 自定义标签函数 过滤器、inclusion_tag模板的继承导入

    目录标题 一:网页伪静态 1.定义 2.如何实现 二:视图层 1.视图函数返回值问题 2.视图层返回json格式的数据 3.form表单携带文件数据 4.CBV源码分析 1.CBV和FBV: 2.CB ...

  6. StringBuilder初始化容量以及扩容机制(源码分析)

    我们从源码来分析一下StringBuilder的底层原理: /*** Constructs a string builder with no characters in it and an* init ...

  7. rm删除命令源码分析

    为什么看? 想要在删除文件前,先覆盖文件内容,防止他人恢复文件,从而得到文件原内容:并且需要支持rm命令原本的参数选项: NAME rm - remove files or directories S ...

  8. 1、Ribbon相关组件初始化 - Ribbon 核心原理与源码分析

    我们举例说明Ribbon的作用,微服务架构中,服务B的一个功能依赖服务A,而服务A部署了多台机器,如下图 当一个请求来请求服务B时,服务B依赖Ribbon采用某种负载均衡的策略来调用服务A,Ribbo ...

  9. rocketmq 消息删除_RocketMQ源码分析之文件过期删除机制

    1.由于RocketMQ操作CommitLog.ConsumeQueue文件,都是基于内存映射方法并在启动的时候,会加载commitlog.ConsumeQueue目录下的所有文件,为了避免内存与磁盘 ...

最新文章

  1. 黄金时代:这个地区17所新大学建设,提速!
  2. 存储引擎:MySQL系列之七
  3. 【风控系统】风控中心—京东基于Spark的风控系统架构实践和技术细节
  4. 如何在 Windows XP 的事件查看器中查看和管理事件日志
  5. PyQt4编程之如何让状态栏显示信息
  6. [iOS] 引用外部静态库时,(类别)目录方法无法加载问题
  7. S3C2440 汇编指令
  8. 程序员下班回家,路上被拦…
  9. 《我在谷歌大脑见习机器学习的一年:Node.js创始人的尝试笔记》阅读笔记
  10. 翻译:YOLOv5 新版本——改进与评估
  11. CSS动画效果(animation属性)解析
  12. docker: Error response from daemon: Conflict. The container name “/mysql“ is already in use by conta
  13. 写给小白的云计算入门科普
  14. elementui 文件转ts_[ElementUI] 修改默认语言为英文 el-table filter 筛选
  15. 罗马数字背后的秘密——LeetCode XII XIII 题记
  16. 华科世界第六,北邮碾压伯克利:USNews世界大学CS榜发布
  17. Watcher--数据变更的通知
  18. Python计算图像纹理-灰度共生矩阵
  19. 深入理解指针数组、数组指针、函数指针、函数指针数组、指向函数指针数组的指针
  20. 国产芯片替代ST很容易:记航顺HK32F103RET6替代STM32F103RET6

热门文章

  1. python collection counter_python collection模块中几种数据结构(Counter、OrderedDict、namedtup)详解...
  2. 自带的数据集_机器学习练习数据哪里找?两行代码搞定!
  3. mysql 数据类型 int_MySQL数据类型 int(M) 表示什么意思?
  4. TF之DD:利用Inception模型+GD算法生成带背景的大尺寸、高质量的Deep Dream图片——五个架构设计思维导图
  5. ML之xgboost:利用xgboost算法对breast_cancer数据集实现二分类预测并进行graphviz二叉树节点图可视化
  6. Paper:《Adam: A Method for Stochastic Optimization》的翻译与解读
  7. Interview:算法岗位面试—上海某公司算法岗位(偏机器学习,互联网金融行业)技术面试考点之数据结构相关考察点—斐波那契数列、八皇后问题、两种LCS问题
  8. pyhanlp 文本分类与情感分析
  9. Web应用开发技术(3)-html
  10. Error: Visual Inheritance is currently disabled because the base … (NET CF)