Windows编程_Lesson007_内核对象

来源:互联网 发布:店铺数据查看 编辑:程序博客网 时间:2024/05/16 06:19

内核对象概述

在前面课程中,我们学习了很多关于内核对象、句柄等各种各样的使用,并且在使用过程中,我们好像拥有了当前这个句柄,并且操作当前内核对象,但是我们从未深入的解析过,在Windows中这个内核对象到底意味着什么!句柄的本质又是怎么回事!我们接下来会使用一些工具来查看我们系统中内核对象的意思是什么!我们系统中的句柄本质是什么!
我们首先使用的第一个工具是WinObj,它可以用来查看我们系统中的内核对象,因为内核对象是属于操作系统的,而进程仅仅是拥有内核对象的使用权,而且这个使用权也是在受监管之下的。
我们把WinObj软件下载下来之后,双击运行,程序默认运行的权限是普通用户权限,这时需要我们在File中以管理员身份运行,这样既可以得到我们下面的界面。
这里写图片描述
如果不是以管理员运行的话,我们选择ObjectsTypes后显示的是一片空白。
总之,我们通过这个软件,我们知道我们的操作系统中是有一系列的内核对象的,这些内核对象将会对应到当前的所有操作,那么Windows为什么要来设计这一系列的内核对象呢?主要是因为考虑到安全问题。
请看下图:
这里写图片描述

内核对象的共性

在我们操作系统中的很多种的内核对象是有很多共性的。
1. 使用计数;
2. 安全性;

1.设置使用计数的目的

使用计数就是我们要说的第一个共性,使用计数我们不会感到很陌生,因为我们之前在讲进程的时候,提到过这个进程的内核对象,这个内核对象的使用计数会变成2(进程加1,线程加1,所以总共加2),所以R3层和R0层进行沟通的一个具体点,因为R0层和R3层中间是完全隔离的,所以R3层不能直接发送一个命令来清理它对应的内核对象,因为从微软的角度来考虑的话,他为了提高操作系统的稳定性,它并不希望把系统的内核对象暴露在用户面前,而是提供了一些相关的函数来进行内核对象允许的操作,但是并不希望把所有内核对象的生死权全部交给用户,所以微软设计出了一个使用计数的东西,这个使用计数是非常有意义的事情,并且我们也能够看出来Windows操作系统是如何来架构的。
首先,任何一个内核对象都是属于操作系统的,它不属于某一个进程的。但是任何一个进程可以使用任意一个内核对象,但是它不能决定任何一个内核对象的生或者死。但是内核对象的生或者死是由进程来影响,因为内核对象变得没有意义的时候,操作系统为了节约内存空间,应该把这个内核对象干掉。但是R3层和R0层之间又没有一个直接交互的接口,我们只能使用使用计数。使用计数会随着使用个数的增加而增加,随着不使用而减小,当最后使用计数变为0的内核对象,这个内核对象就会被我们的操作系统所回收,然而这种回收并非是实时的,而是等到系统空闲时,它会优先的来清理使用计数为0的内核对象。
这种设计导致了内核对象和进程是物理分割的,为什么这么说呢?比如在R3层使用CreateFile打开了两个文件,但是对于内核来说,它只生成一个File内核对象,那么如果其中一个CreateFile完成后(假如能够直接关闭File内核对象,那么对于另一个CreateFile来说,将没有File内核对象来供它使用了,这时就会出现问题)。所以内核对象的生命周期大于某一个进程的生命周期,它可以供多个进程来使用。

2.安全性设置及作用

我们知道使用计数拥有了让多个进程共同使用同一个内核对象,并且能将这个内核对象正确释放的能力。那么,如果我们让所有的进程有所有的权限来访问我们的内核对象,很明显这样会出现问题的。比如我们的File内核对象,里面所装的内容是一样的,如果有两个进程在不停的更改File内核对象的内容,就和导致File内核对象发生不可避免的错误,这就和我们的线程安全一样了。所以每一个内核对象里面都有一个安全性的设置,但是这个属性的设置,我们一般不会去做特殊的设置,一般会去设置为NULL,就是让它继承当前进程里面的安全性。
安全性设置可以有以下几种:
1. 可以决定有哪些用户拥有访问当前内核对象的访问权限;
在前面讲UAC的时候提到过,在一个操作系统中有各种各样不同的用户级别,特别是我们在做服务器编程的时候,服务器中肯定有各种各样的用户级,安全性将会决定哪些用户可以访问当前的内核对象。
2. 可以设置内核对象的安全性(比如使用CreateFile生成的内核对象);
在设置了安全性的内核对象后,其它的进程想要打开这个内核对象,那么就需要按照我们设置好的安全性的要求来进行打开,否则是打开不成功的。比如我们在打开一个文件并进行写操作的时候,使用CreateFile函数设置只共享读模式,其它的进程可以打开这个内核对象进行读操作,但是不能进行写操作,这就避免了同一个内核对象被多个进程进行访问造成的错误。我们在做一般性操作的时候,对安全性要求并不是很高。但是如果我们以后在进行多个进程进行操作同一个内核对象的时候,碰到的问题有百分之七八十都是由于安全性设置导致的(我们就可以找一下安全性的相关知识就基本上能把问题解决了),比如一个文件内核对象就不应该有两个进程同时来写,应该只有一个写,其它的可以读。实际上安全性还涉及到继承等各方面的知识。

句柄表及句柄的本质

前面我们一直在说内核对象,但是我们是一直没有办法来直接使用内核对象的,必须通过句柄才能间接的使用内核对象。我们就先来看一下,在进程中的句柄到底是怎么回事!并且,前面我们一直在强调,R3层和R0层之间是完全隔离的,那么我们如何在R3层通过句柄来和R0层的内核对象进行关联呢?下面通过一个例子先来看一下:

#include <Windows.h>#include <tchar.h>int  main(){    HANDLE hFileOne = CreateFile(TEXT("one.txt"),   // open one.txt        GENERIC_READ,                               // open for reading        0,                                          // do not share        nullptr,                                    // no security        CREATE_ALWAYS,                              // existing file only        FILE_ATTRIBUTE_NORMAL,                      // normal file        nullptr);    HANDLE hFileTwo = CreateFile(TEXT("two.txt"),   // open two.txt        GENERIC_READ,                               // open for reading        0,                                          // do not share        nullptr,                                    // no security        CREATE_ALWAYS,                              // existing file only        FILE_ATTRIBUTE_NORMAL,                      // normal file        nullptr);    _tprintf(TEXT("hFileOne:%p\r\nhFileTwo:%p\r\n"), hFileOne, hFileTwo);    CloseHandle(hFileOne);    CloseHandle(hFileTwo);    system("pause");    return 0;

这里写图片描述
结果上来看,内核对象的句柄好像和我们想象的不太一样,并且句柄的数值是以4在递增,那么为什么一个数值能够代表一个内核对象呢?
实际上操作系统在R0层有一个进程内核对象,进程内核对象才是真正的指示了CreateFile的一些操作。在R3层所有的操作指示发出一个请求到R0层,然后R0层的内核对象根据情况作出相应的操作。在进程内核对象中有一个句柄表,并且在任何API文档中都查不到这个句柄表,因为微软不希望用户接触R0层的任何东西,微软只希望用户通过R3层的特定的一些API来间接的操作R0层。
在进程内核对象的句柄表中,一般会包括索引、指向内核对象的指针、以及一些标识。这里的索引其实就是我们在R3层使用的句柄,但是指向内核对象的指针却不会暴露给R3层任何进程。
我们在启动了一个CreateFile函数之后,这个函数会发送一个指令,然后在内核里面会启动一个File Obj,那么这个File Obj毫无疑问会有自己的地址,然后拿着自己的地址填充到进程内核对象中,再生成一个索引,这个索引是从0开始,以4为间隔,这样一直往下递增。我们在R3层拿到的内核对象的句柄就是内核进程对象中的索引,所以R3层得到的句柄是和进程相关的,这个句柄给到任何一个其它进程将无法使用,因为我们在操作这些句柄的时候,它会根据索引去找实际的File Obj,然后再进行实际的操作,File Obj的这些操作都是在R0层做的。举个例子:假如我们有两个进程A、B,在A进程中有自己的一张索引表,在R3层里面只能拿到索引,并且这个索引只对当前的进程有效,假如把A进程的句柄给了B进程(当然,B进程和A进程一样,也有自己的一张索引表),这个句柄是对B进程不起作用的,所以我们也得到一个结论:句柄是不能跨进程存在,但是句柄里面的内容,也就是真正的指向内核对象的指针是可以跨进程存在的。既然在R3层没有办法进行句柄传递,那么我们就不能把A进程通过CreateFile创建的一个句柄传递给B进程来直接使用,如果在B进程中想使用A进程的那个句柄,就必须使用同样的方法(即CreateFile函数得到的句柄)来重新获得句柄。在同样一个内核对象,在不同的进程中,它的索引值可能不同。所以句柄实际上并不是唯一的,也并非是整个系统所通用的,这里推翻了前面很多概念。总的来说句柄就是一个索引值,只是在当前进程中有效,出了当前的进程,在其它进程中,这个句柄值将变得毫无意义。

深入理解句柄表及索引

前面我们提到过,当启动一个进程的时候,这个进程会创建一个进程内核对象并分配空间。现在在学习了内核对象以及内核对象句柄表后,我们会发现,当一个线程创建之后,它会指定自己的空间,同时会在自己的内核对象中开辟一段空间,这段空间主要用于存放句柄表。当内核对象完成了初始化,以及整个进程完成了初始化,此时操作系统会自动的帮我们启动一个主线程,并创建一个主线程内核对象,就会在句柄表中找空白的区域,然后将主线程句柄的地址放到这个空白区域,并生成与之对应的索引。
如果我们想将句柄索引从当前的句柄表中删除的话,我们就可以使用CloseHandle将这个句柄关闭即可,下面我们通过一个小小的实验来进行说明。

#include <Windows.h>#include <tchar.h>int  main(){    HANDLE hFileOne = CreateFile(TEXT("one.txt"),   // open one.txt        GENERIC_READ,                               // open for reading        0,                                          // do not share        nullptr,                                    // no security        CREATE_ALWAYS,                              // existing file only        FILE_ATTRIBUTE_NORMAL,                      // normal file        nullptr);    _tprintf(TEXT("hFileOne:%p\r\n"), hFileOne);    CloseHandle(hFileOne);    HANDLE hFileTwo = CreateFile(TEXT("two.txt"),   // open two.txt        GENERIC_READ,                               // open for reading        0,                                          // do not share        nullptr,                                    // no security        CREATE_ALWAYS,                              // existing file only        FILE_ATTRIBUTE_NORMAL,                      // normal file        nullptr);    _tprintf(TEXT("hFileTwo:%p\r\n"), hFileTwo);    CloseHandle(hFileTwo);    system("pause");    return 0;}

这里写图片描述

从结果可以看出,句柄表中会去找空白区域,然后去填充这个空白区域。在R3层看来,所填充的数值仅仅就是一个索引值。CloseHandle可以将句柄关闭,删除这个句柄在句柄表中的索引。当然了,CloseHandle还可以改变其它内核对象的使用计数,在这里我们就先不讲了。

句柄表存在的意义

我们大概了解了在句柄表创建的时候,句柄表发生的一系列变化。那么在CloseHandle的时候会发生一些什么变化呢?这还用说嘛,应该是和创建的过程正好相反呀!对,确实是相反的。
在CloseHandle的时候传递的是一个索引,根据这个索引值,会去当前进程内核对象表中找这个索引值所对应的对象,然后给这个对象里面的使用计数递减的指令,然后返回一个值。这个返回值如果是TRUE,说明找到了这个索引值,如果返回FALSE,说明没有找到。这就是CloseHandle函数所做的事情。所以我们发现,在对于进程里面的句柄进行一系列操作的时候,实际上都是拿着这个句柄(进程内核对象表中的索引值)来进行的操作。
之前我们一直在回避内核对象的产生,现在我们还得直面内核对象的泄漏。
我们现在知道了什么是索引表,但是微软为什么要多加一个索引表呢?我们直接拿着地址来进行操作不是更简单吗?这样看起来好像徒劳的增加操作,不符合一切从简的常理啊!我们下面来看一下Windows到底是为什么要这样设计。
当我们创建了一个内核对象之后,我们有时候会忘记使用CloseHandle来将这个句柄关闭掉(尤其对于初学者来说更是这样),这样就会导致内核对象无法被正确的释放,这样就会导致操作系统会被浪费掉很多的内存。然而,索引表就很好的解决了这个问题,使得操作系统变得更加稳定可靠。
因为句柄表是属于当前进程内核对象的,当进程内核对象中知道使用了哪些内核对象的句柄,那么在进程消亡的时候,就可以按照句柄表来一一进行关闭(相当于调用了CloseHandle函数),如果没有其它进程使用这个内核对象,那么操作系统就会将这个内核对象关闭,回收内存。所以索引表主要是方便进程知道使用了哪些内核对象句柄,在进程结束的时候能够正确的将它们自动关闭掉!如果我们手动关闭掉之后,就不会被再次关闭了,因为手动关闭后,它就会从索引表中移出。所以索引表的存在,主要是为了维护操作系统的稳定性和可靠性。那么我们有没有办法来看到这些索引表呢?答案是肯定的,不过我们需要ProcessExplorer工具进行查看。显示界面如下:

这里写图片描述
这个工具以后详细讲解如何使用,因为在R3层中很多功能无法实现,这个工具非常强大,对于逆向也是有很大帮助的,大家可以自己研究一下!

内核对象的Signal状态

在以下内核对象中都会有一个signal的对象,并且在初始化时为FALSE,它将决定内核对象的授信状态,它相当于一个标志比如说在进程内核对象中,在初始化时或者在运行过程中signal会一直为FALSE,直到进程运行完成,它的signal对象会变为TRUE,此时我们就可以认为当前进程内核对象已经变成为可授信状态,下面其它的内核对象和进程内核对象是一模一样的。
1. 进程;
2. 线程;
3. 标准输入输出;
4. 事件;
5. 互斥体;
6. 信号;
7. 可等待计时器;
8. 作业。
这些内核对象将会一直标注着signal对象这个标志,让我们来进行一些判断,判断的函数我们之前也接触过。
WaitForSingleObject函数会使得当前正在执行的线程变成不可调度的状态,它就会一直等待我们传递给它的内核对象 里面的signal对象变为TRUE为止,此时WaitForSingleObject函数就会返回,它的返回值就会变得有意思起来,我们可以根据返回值来做很多的事情,比如判断一个线程是否还在运行等。
WaitForMultipleObjects是用来等待多个内核对象里面的signal变为有信号,就我们现在已经了解的进程和线程来说,有可能有一个父进程,在父进程里面启动了一个子进程和一个子线程,并且让子线程和子进程产生交互,在交互完成以后,需要等待这两个内核对象同时变得有信号的状态,此时我们所说的有信号状态是在子进程和子线程内核对象里面自动变得有信号,WaitForMultipleObjects的具体参数和返回值这里就不详细介绍了,请参考msdn的详细说明。
当我们参考了msdn后会发现WaitForSingleObject和WaitForMultipleObjects这两个函数真的是太好用了,我们平时应该多使用,但是在使用这两个函数的时候,我们还要有些地方需要特别注意,WaitForSingleObject根据内核对象进行整体的操作,这个操作会导致一系列的问题,对于现在我们了解的线程和进程还好,因为它们里面的signal的状态是由操作系统来维护的,只有它们消亡之后才会变得有信号状态,但是其它的拥有signal的内核对象可以以两种状态来改变signal的值,一种是由系统来进行改变,还有一种是由手动来改变的,WaitForSingleObject有一个副作用,如果这个内核对象是委托给系统来进行signal状态更改的,WaitForSingleObject函数会干涉signal的状态,因为当某一个内核对象变为有信号状态后,WaitForSingleObject函数会把这个有信号的状态拿过来,并且会将这个内核对象的signal设置为无信号状态,这一点我们一定要注意!!!

很多时候,内核对象的设计是用于同步的,这个同步并不单单指的是线程的同步,也包括进程和进程之间的同步。比如说之前我们的电脑不是很稳定,会导致某一些程序发生一些异常,有的时候我们可能会设计一个叫做守护进程的东西,此时就可以使用内核对象进行设计,守护进程做的事情非常简单,当检测到当前的进程已经消亡,那么就赶紧再启动一个全新的进程出来,方便之后的程序能够使用。但是如果我们想要获取其它进程的状态时,那么中间需要做很多事情,所以我们就可以这样设计:把守护进程设计成守护进程,然后使用WaitForSingleObject函数进行等待,当发现这个进程变为有信号状态时,我们就知道这个进程已经消亡了,我们就可以立刻重新启动这个进程,这样做的好处在于:
如果我们普通的进行进程守护的话,要么我们会间隔性的对系统中所有的进程进行遍历,看看我们需要的进程是否还存在,毫无疑问,这种做法效率低并且消耗资源多。使用WaitForSingleObject就会节省很多资源,提高效率。这就是我们内核最强大的一点,就是它能够进行进程与进程之间的同步。QQ、暴风、爱奇艺等都会使用多进程来组成一个软件。

Wait的3种结果

  1. 等待超时的情况;
#include <Windows.h>#include <process.h>#include <cstdio>unsigned int __stdcall ThreadFunc(void *lParam){    Sleep(5000);    return 0;}int main(){    HANDLE hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, nullptr, 0, nullptr);    DWORD dwRet = WaitForSingleObject(hThread, 1000);   // 等待1000毫秒    switch (dwRet)    {    case WAIT_TIMEOUT:        printf("等待超时!\r\n");        break;    case WAIT_OBJECT_0:        printf("成功等待!\r\n");        break;    case WAIT_FAILED:    printf("执行失败!\r\n");        break;    default:        break;    }    CloseHandle(hThread);    system("pause");    return 0;}

这里写图片描述

  1. 成功等待的情况;
    这里写图片描述

  2. 执行失败的情况。
    这里写图片描述

对于上面的三种返回结果,我们很容易理解等待超时和执行失败两种情况,但是我们不太理解执行成功的返回值,实际上,这个返回值主要是用于WaitForMultipleObjects函数的。我们再来看下面的情况:

#include <Windows.h>#include <process.h>#include <cstdio>unsigned int __stdcall ThreadFunc(void *lParam){    Sleep(int(lParam));    return 0;}int main(){    const int THREADCOUNT = 2;    HANDLE hThreads[THREADCOUNT] = { INVALID_HANDLE_VALUE };    hThreads[0] = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, (void *)3000, 0, nullptr);    hThreads[1] = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, (void *)1000, 0, nullptr);    DWORD dwRet = WaitForMultipleObjects(THREADCOUNT, hThreads, FALSE, 2000);   // 等待2000毫秒    switch (dwRet)    {    case WAIT_TIMEOUT:        printf("等待超时!\r\n");        break;    case WAIT_OBJECT_0:        printf("线程 1 成功等待!\r\n");        break;    case WAIT_OBJECT_0 + 1:        printf("线程 2 成功等待!\r\n");        break;    case WAIT_FAILED:        printf("执行失败!\r\n");        break;    default:        break;    }    for (int i = 0; i < THREADCOUNT; ++i)    {        CloseHandle(hThreads[i]);    }    system("pause");    return 0;}

这里写图片描述

我们可以根据这个返回值就可以做很多事情,比如说我们自己可以做一个线程池,根据这个返回值就可以知道哪个线程已经执行完成,说明这个位置的线程已经空闲下来了,我们就可以重新开启其它的线程来执行任务。

事件内核对象

我们现在有一个需求,就是让线程1执行完成以后,才让线程2执行,我们设计的代码如下所示:

#include <Windows.h>#include <process.h>#include <tchar.h>int *g_pNum;BOOL g_bUsing = FALSE;void Entry(){    while (InterlockedExchange((long *)&g_bUsing, TRUE) == TRUE)    {        Sleep(0);    }}void Leave(){    InterlockedExchange((long *)&g_bUsing, FALSE);}void ThreadPrint(TCHAR *strText){    Entry();    _tprintf(strText);    Leave();}unsigned int __stdcall StartThread(void *lParam){    ThreadPrint(TEXT("StartThread Begin...\r\n"));    g_pNum = new int(0);    Sleep(1000);    ThreadPrint(TEXT("StartThread End...\r\n"));    return 0;}unsigned int __stdcall EndThread(void *lParam){    ThreadPrint(TEXT("EndThread Begin...\r\n"));    HANDLE hStart = lParam;    ThreadPrint(TEXT("EndThread Waiting...\r\n"));    WaitForSingleObject(hStart, INFINITE);              // 这样的同步有两个好处:                                                        // 1.代码简短;                                                        // 2.使线程2变得不可调度,可以将CPU让给别的线程执行    ThreadPrint(TEXT("EndThread Waited...\r\n"));    delete g_pNum;    ThreadPrint(TEXT("EndThread End...\r\n"));    return 0;}int main(){    HANDLE hStart = (HANDLE)_beginthreadex(nullptr, 0, StartThread, nullptr, 0, nullptr);    HANDLE hEnd = (HANDLE)_beginthreadex(nullptr, 0, EndThread, hStart, 0, nullptr);    WaitForSingleObject(hEnd, INFINITE);    system("pause");    return 0;}

执行结果如下所示:
这里写图片描述
上面的结果好像跟我们预想的结果一样,不过我们的执行结果有时还有以下情况:
这里写图片描述
这个结果就不是我们想要的结果了。因为我们没有办法确保EndThread线程是在SatartThread线程运行之后才开始运行。
由于线程内核对象和进程内核对象并不是用来设置signal的内核对象,它并不是用来给我们做同步使用的,只能说这两个内核对象中有signal的成员,我们可以对这两个内核对象进行wait的操作,但是它们的作用还是非常的有限的,因为我们没有办法手动的修改这些内核对象中的signal的状态。
那么如何才能确保EndThread线程是在SatartThread线程运行之后才开始运行呢?最好的方法是同步,事件内核对象就可以用于线程或者进程之间的同步。
事件内核对象是一个较为特殊的内核对象,它并没有代表操作系统中的任何一个实体,它是为了编程人员的方便而产生的。在事件内核对象中也有使用计数等,但是对于事件内核对象较为重要的是signal,因为我们可以自动或者手动的设置事件内核对象的signal状态。我们可以使用CreateEvent函数来创建一个事件内核对象,其原型如下:

HANDLE WINAPI CreateEvent(  _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,  _In_     BOOL                  bManualReset,  _In_     BOOL                  bInitialState,  _In_opt_ LPCTSTR               lpName);

需要注意的是:CreateEventEx函数只能在Vista之后才能使用。
lpEventAttributes权限访问,一般设置为nullptr,让它继承进程的安全属性,这也是CreateEventEx函数产生的原因。
bManualReset是否手动重置,TRUE为手动设置signal状态,若果是FALSE,那么系统会来设置signal状态。
bInitialState事件内核对象初始化时的signal状态。为TRUE时有信号状态,那么WaitForSingleObject将不会阻塞当前的线程,否则会阻塞当前的线程,WaitForSingleObject会一直等待这个内核对象直到有信号状态。
lpName这个参数最为重要,因为事件内核对象属于操作系统,我们平时在同一个进程里面操作的时候一般只是使用一个句柄来操作这个事件内核对象,但是这个句柄并没有代表这个事件内核对象。因为我们这个句柄在另外一个进程里面是无法操作它所对应的内核事件对象的,如果想操作这个内核事件对象怎么办呢?这个参数就可以帮我们做到这一点。在跨进程的时候。就可以使用OpenEvent函数来打开这个事件内核对象。如果lpName为nullptr,说明只能在本进程中使用这个事件内核对象(即匿名事件内核对象)。

下面我们再来看一下CreateEventEx函数,它的函数原型如下所示:

HANDLE WINAPI CreateEventEx(  _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,  _In_opt_ LPCTSTR               lpName,  _In_     DWORD                 dwFlags,  _In_     DWORD                 dwDesiredAccess);

它同样也是有4个参数,但是参数含义就变得完全不同了。
lpEventAttributes安全属性结构体指针,一般设置为nullptr。
lpName事件内核对象名字,一般用于跨进程。
dwFlags它有两种方式CREATE_EVENT_INITIAL_SET和CREATE_EVENT_MANUAL_RESET,具体细节请看msdn吧。
dwDesiredAccess权限设置。比如说我们设置了读权限,那么只能使用WaitForSingleObject来进行等待,如果设置为写权限,就可以使用SetEvent或者ResetEvent来设置signal的状态。
小测试:
使用事件内核对象控制多个线程函数的启动顺序:

#include <Windows.h>#include <process.h>#include <tchar.h>int *g_pNum;BOOL g_bUsing = FALSE;void Entry(){    while (InterlockedExchange((long *)&g_bUsing, TRUE) == TRUE)    {        Sleep(0);    }}void Leave(){    InterlockedExchange((long *)&g_bUsing, FALSE);}void ThreadPrint(TCHAR *strText){    Entry();    _tprintf(strText);    Leave();}unsigned int __stdcall StartThread(void *lParam){    ThreadPrint(TEXT("StartThread Begin...\r\n"));    ResetEvent(lParam);    g_pNum = new int(0);    Sleep(1000);    ThreadPrint(TEXT("StartThread End...\r\n"));    return 0;}unsigned int __stdcall EndThread(void *lParam){    ThreadPrint(TEXT("EndThread Begin...\r\n"));    HANDLE hStart = lParam;    ThreadPrint(TEXT("EndThread Waiting...\r\n"));    WaitForSingleObject(hStart, INFINITE);              // 这样的同步有两个好处:                                                        // 1.代码简短;                                                        // 2.使线程2变得不可调度,可以将CPU让给别的线程执行    ThreadPrint(TEXT("EndThread Waited...\r\n"));    delete g_pNum;    ThreadPrint(TEXT("EndThread End...\r\n"));    return 0;}int main(){    // 创建一个事件内核对象,并将它初始化为有信号状态,并且是手动设置signal状态    //HANDLE hEvent = CreateEvent(nullptr, TRUE, TRUE, TEXT("My Event"));   // 和下面的一行代码效果一样    HANDLE hEvent = CreateEventEx(nullptr, TEXT("My Event"), CREATE_EVENT_INITIAL_SET | CREATE_EVENT_MANUAL_RESET, EVENT_MODIFY_STATE);    HANDLE hStart = (HANDLE)_beginthreadex(nullptr, 0, StartThread, hEvent, 0, nullptr);    WaitForSingleObject(hEvent, INFINITE);    HANDLE hEnd = (HANDLE)_beginthreadex(nullptr, 0, EndThread, hStart, 0, nullptr);    WaitForSingleObject(hEnd, INFINITE);    system("pause");    return 0;}

这里写图片描述
这样就可以保证每次启动StartThread函数了。