查看原文
其他

Windows 网络编程:进程与线程

计算机与网络安全 计算机与网络安全 2022-06-01

一次性进群,长期免费索取教程,没有付费教程。

教程列表见微信公众号底部菜单

进微信群回复公众号:微信群;QQ群:460500587



微信公众号:计算机与网络安全

ID:Computer-network

进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象。


进程,是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。每一个进程都有一个自己的地址空间,即进程空间或(虚空间)。进程空间的大小只与处理机的位数有关,一个16位长处理机的进程空间大小为216 ,而32位处理机的进程空间大小为232 。进程至少有5种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。


线程,在网络或多用户环境下,一个服务器通常需要接收大量且不确定数量用户的并发请求,为每一个请求都创建一个进程显然是行不通的,——无论是从系统资源开销方面或是响应用户请求的效率方面来看。因此,操作系统中线程的概念便被引进了。线程,是进程的一部分,一个没有线程的进程可以被看作是单线程的。线程有时又被称为轻权进程或轻量级进程,也是CPU调度的一个基本单位。


一、进程


当按下Ctrl+Shift+Esc组合键时,就打开了Windows任务管理器对话框,里面有很多进程列表,如图1所示。

图1  任务管理器

进程是运行当中的程序,是向操作系统申请资源的基本单位。我们运行一个记事本程序,那么就相应会创建一个记事本的进程。当关闭记事本时,进程也随即结束了。对于这个任务管理器中,我们对进程比较关心的是“映像名称”、“PID”两项。对于进程相关的,我们主要学习进程的启动、结束、枚举等编程。


二、进程的创建


任何一个计算机文件都是一个二进制文件,对于可执行程序来说,它的二进制数据是可以被CPU执行的。程序的概念是一个静态的概念,程序本身只是存在于硬盘上的一个二进制文件。当用鼠标双击了某个可执行程序以后,这个程序就被加载,如内存,这时就产生了一个进程。进程会向系统申请各种所需的资源,并且会生成一个主线程,线程会拥有CPU执行时间,占用进程申请的内存……那么在编程的时候用API函数启动一个进程也是我们要学习的内容。可以创建进程的API函数有WinExec()、ShellExecute()和CreateProcess()等。这里主要介绍WinExec()和CreateProcess()两个函数。


三、“下载者”的简单演示


参数说明如下。


(1)lpCmdLine:指向一个欲执行的可执行文件。

(2)uCmdShow:程序运行后的窗口状态。


对于第一个参数比较好理解,比如要执行“记事本”程序,那么这个参数就可以是“c:\windows\system32\notepad.exe”。对于第二个参数是指程序运行窗口的状态,常用的有两个,一个是SW_SHOW,另一个是SW_HIDE。第一个参数是程序运行后窗口显示,另一个是程序运行后窗口不显示。大家可以试着创建一个不显示窗口的“记事本”程序。代码很简单,如下:

这样创建的“记事本”进程,在任务管理器中可以看到“notepad.exe”这个进程,但是无法看到其窗口界面。


WinExec()这个函数多用在“下载者”中,“下载者”的英文名字叫“Downloader”,也就是下载器的意思,它是一种恶意程序,该恶意程序的功能较为单一(相对木马来说功能单一)。该恶意程序的功能是让受害计算机到黑客指定的URL地址去下载更多的病毒文件或木马文件并运行。下载者的体积较小,容易传播。当“下载者”下载到病毒木马后,通常都会使用WinExec()来运行病毒


我们简单地来做一个“下载者”的演示,记住,这只是一个演示。不要企图拿来做坏事,因为我们的演示代码很轻易地会被杀毒软件杀掉,我们的目的是学习编程知识。


我们要完成一个模拟的“下载者”,既然是“下载者”,重要的当然是文件的下载了。文件的下载方式比较多,但是相对简单而又比较常用的函数是URLDownloadToFile(),这个函数也是经常出现在“下载者”中的函数,该函数的定义如下:

参数说明如下。


(1)szURL:指向URL地址的字符串。

(2)szFileName:指向要保存地址的字符串。


对于使用URLDownloadToFile()函数,需要包含Urlmon.h头文件及Urlmon.lib库文件,否则当编译和链接时会无法通过。


既然了解了该函数的使用,那么我们就来完成一个模拟的“下载者”吧。代码如下:

我们的模拟是把C盘系统目录下的记事本程序下载到D盘下并保存为Virus.exe,然后运行它。这里只是一个简单模拟,如果要真正完成一个“下载者”的话,其代码要复杂很多。如果要在源代码上对其进行“免杀”,那么要考虑的问题也会很多。


四、CreateProcess()函数介绍与程序创建


通常情况下,我们创建一个进程都会选择使用CreateProcess(),该函数的参数非常多,功能更强大,使用也更为灵活。WinExec()函数的使用相对简单,只能完成简单的进程创建工作。如果要对创建的进程有控制的能力,那么必须使用CreateProcess( )函数。

在介绍CreateProcess()函数之前,先来考虑一个问题。在我们编写C程序时,如果是控制台下的程序,那么编写程序的入口函数是main()函数,也就是我们通常所说的主函数。如果是一个Windows程序,那么入口函数是WinMain()函数,即使使用MFC进行开发,也是有WinMain()函数的,只不过它被庞大的MFC框架给隐藏了。那么我们的函数真的是从main()函数或者是WinMain()开始执行的吗?我们在写控制台程序时,如果需要给程序提供参数,那么这个参数是从哪里来的呢?


我们使用VC6来写一个简单的程序,并调试一下,看程序是否真的是由main()函数开始的。我们选择输出“Hello World”的程序来演示。


在控制台下打印“Hello World”字样的程序如图2所示。

图2  输出“Hello World”的程序

接下来要怎么做呢?按下F10键,这时,我们的程序在VC6下处于调试状态,打开“CallStack”窗口,如图3所示。

图3  CallStack窗口内容

双击“mainCRTStartup() line 206 + 25 bytes”这一行,查看代码编辑窗口的内容,如图4所示。

图4  当前代码编辑窗口内容

可以看到,绿色三角指向那行代码是对main()函数的调用,并且main()函数还有返回值。滚动代码,查看一下mainret变量的类型。该变量的定义如下:

该变量的类型为int型。那么,当定义main()函数时,main()函数的返回值是什么呢?通常情况下,定义main()函数的返回值是int型。但是,也有一部分人喜欢把main()函数的返回值定义为void型,也就是空型。那么,如果定义为void型,mainret接收的返回值是什么呢?在Windows下约定,函数的返回值保存在eax寄存器中。如果对main()函数的返回值定义为void型,那么当main()函数返回后,mainret中保存的是退出main()函数后eax寄存器中的值。


除了这个以外,大家看一下VC6的标题栏,标题栏上给出的这个“.C”文件,我们叫做启动代码,用于在启动一个进程后对全局变量等内容的初始化。再看一下图3,CallStack还有一行,双击它,观察代码编辑器中的内容。


下面介绍CreateProcess()函数的使用。

参数说明如下。


(1)lpApplicationName:指定可执行文件的文件名。


(2)lpCommandLine:指定可执行文件的运行参数。


(3)lpProcessAttributes:进程安全属性,该值通常为NULL,表示为默认安全属性。


(4)lpThreadAttributes:线程安全属性,该值通常为NULL,表示为默认安全属性。


(5)bInheritHandles:指定当前进程中的可继承句柄是否可被新进程继承。


(6)dwCreationFlags:指定新进程的优先级以及其他创建标志。


该参数一般情况下可以为0。


如果要创建一个被调试进程的话,需要把该参数设置为DEBUG_PROCESS。创建进程的进程为父进程,被创建的进程为子进程。也就是说,父进程要对子进程进行调试的话,需要指定DEBUG_PROCESS。在指定了DEBUG_PROCESS后,子进程创建的子进程,同样也处在被调试状态中。


如果不希望子进程创建的子进程处在调试状态,那么需要同时指定DEBUG_ONLY_THIS_PROCESS。


在有些情况下,如果希望被创建的子进程暂时不要运行,那么可以指定CREATE_SUSPENDED参数。事后希望该子进程运行的话,那么可以使用ResumeThread()函数使子进程恢复运行。


(7)lpEnvironment:指定新进程的环境变量。


(8)lpCurrentDirectory:指定新进程使用的当前目录。


(9)lpStartupInfo:指定新进程的启动信息。


该参数是一个结构体,该结构体决定进程启动的状态,该结构体的定义如下:

该结构体在使用前,需要对cb这个成员变量赋值,该成员变量用于保存结构体的大小。如果要对新进程的输入输出重定向的话,会用到该结构体。


(10)lpProcessInformation:返回新进程、进程线程等相关信息。


该参数同样是一个结构体,该结构体的定义如下:

该结构体中保存着新创建进程的句柄、进程主线程的句柄,还有进程ID和主线程的ID。


下面进行一个简单的演示,用CreateProcess()创建一个记事本进程代码如下:

对于进程创建后PROCESS_INFORMATION接收的两个句柄,需要进行关闭。


五、进程的结束


介绍完进程的创建,对于进程的结束,通常情况下希望程序可以自己进行结束,也就是进程正常退出运行状态。在进程正常进行退出时,会调用ExitProcess()函数,该函数使用简单。这里主要介绍一下如何实现类似任务管理器那样结束其他的进程。


结束其他进程需要使用到TerminateProcess()这个函数,该函数的定义如下:

该函数的参数比较少,只有两个,第一个参数是要结束进程的进程句柄,第二个参数通常给0。那么这里遇到一个问题,我们如何获得要结束进程的进程句柄呢?方法有两种,第一种方法是枚举进程列表,找到要结束的进程。第二种方法是根据要结束进程的窗口标题获得该窗口的句柄,然后通过窗口句柄得到进程的PID,有了进程的PID后,我们可以打开该进程得到进程的句柄,然后将其结束。我们这里主要介绍第二种方法。


下面通过介绍如何结束“记事本”进程来学习第二种方法。先来看一下代码。

我们打开一个“记事本”,然后编译运行这段代码,会发现“记事本”被关闭了。我们逐个介绍我们使用到的这些API函数。


首先来介绍FindWindow()这个函数,FindWindow()函数的原型如下:

该函数有两个参数,第一个参数是类名,第二个参数是窗口名。该函数的返回值就是窗口的句柄。这两个参数只要指定一个就可以了。对于我们来说,希望通过进程的窗口名来获得窗口的句柄,因此这里只要给出窗口名就可以了。在我们的代码中,“记事本”的窗口名为“无标题 - 记事本”,如果窗口名取得不准确的话,那么将无法获得该窗口的窗口句柄。怎样才能准确无误地获取窗口的名称呢?这里推荐大家使用一款VC6中自带的工具——SPY++。只要安装了VC6后,就会安装该工具,大家可以在开始菜单中找到,其具体位置为:“开始”->“程序”->“Microsoft Visual Studio 6.0”->“Microsoft Visual Studio 6.0 Tools”->“Spy++”。打开“Spy++”这个工具,如图5所示。

图5  Spy++工具界面

单击工具栏中的按钮,该按钮为“Find”按钮,出现如图6所示的“Windows Search”界面。

图6“Window Search”界面

用鼠标拖动“Finder Tool”后面的那个图标到“记事本”进程的标题栏上,该窗口会显示出“记事本”的窗口名,如图7所示。

图7“Window Search”显示“记事本”的窗口名称

在“Caption”中的内容就是“记事本”程序的窗口名称,我们把它作为FindWindow()函数的第二参数,这样,可以获取该窗口的窗口句柄,再通过窗口句柄获得该窗口所属进程的ID。代码如下:

该函数第一个参数是获取到的窗口句柄,第二个参数是一个输出参数,会返回该进程的ID。


在获得了进程ID后,通常通过使用OpenProcess()来获得进程的句柄。该函数的原型如下:

参数说明如下。


(1)dwDesiredAccess:进程欲获得的访问权限,该参数为了方便可以始终为PROCESS_ALL_ACCESS。


(2)bInheritHandle:指定获取的句柄是否可以继承,一般情况下为FALSE。


(3)dwProcessId:指定欲打开的进程ID号。


该函数的返回值为进程的句柄,通过这个句柄可以结束进程。


既然已经获得了进程的句柄,那么就可以结束指定的进程了。在结束完进程后,也要记得用CloseHandle()关闭用OpenProcess()打开的进程句柄。

六、进程的枚举


进程的枚举就是把所有的进程都显示出来。当然了,有一些特意隐藏的进程是无法通过常规的枚举方式枚举到的。这里只介绍应用层的进程枚举。在应用层枚举进程有多种方法,这里只介绍相对常见的枚举进程的方法。在学习进程的枚举的过程中,我们会完成一个自己的进程管理器,如图8所示。

图8  进程管理器

在进程管理器中,除了对进程的枚举,还会对线程进行枚举,还有对进程中加载的DLL进行枚举。要枚举的内容非常多,但枚举的API函数都类似。下面介绍以下几个API函数。


枚举进程需要的API函数分别有CreateToolhelp32Snapshot(),该函数的作用是对当前系统中的进程进行一个快照,在创建快照以后进行逐个进程的枚举。枚举进程的函数是Process32First()/Process32Next()。如果是枚举线程的话那么枚举函数是Thread32First()/Thread32Next()。如果是枚举进程中的DLL的话,那么枚举函数是Module32First()/Module32Next()。在使用以上这些函数的时候,需要包含Tlhelp32.h头文件,否则在编译的时候会提示使用了未定义的函数。针对以上函数,分别进行一个简单的介绍。

参数说明如下。


(1)dwFlags:该参数指明要建立系统快照的类型,对于要枚举的内容,该参数可以指定如下值。


① TH32CS_SNAPMODULE:在枚举进程中的DLL时指定。

② TH32CS_SNAPPROCESS:在枚举系统中的进程时指定。

③ TH32CS_SNAPTHREAD:在枚举系统中的线程时指定。


(2)Th32ProcessID:该参数根据dwFlags的不同而不同。如果枚举的是系统中的进程,或系统中的线程时,该参数为NULL;如果枚举的是进程中加载的DLL的话,那么该参数为进程ID号。


该函数返回一个快照的句柄,在进行枚举时都会用到该句柄。

参数说明如下。


(1)hSnapshot:该参数为CreateToolhelp32Snapshot()返回的句柄;


(2)lppe:为指向一个PROCESSENTRY32结构体的指针,该结构的定义如下。

在使用该结构体时,需要对该结构体中的成员变量dwSize进行赋值,该变量保存PROCESSENTRY32结构体的大小。

该函数的使用与Process32Next()类似。对于枚举进程中加载DLL的枚举,对于系统中线程的枚举,都与此函数类似,只是枚举DLL与线程时,XXX32First()与XXX32Next()的第二个参数指向的结构体不同。


下面看一下枚举进程、枚举进程中加载DLL的代码。


枚举系统进程的代码:

枚举指定进程中加载DLL的代码:

七、调整当前进程的权限


我们枚举的代码基本上是完成了,到VC6下直接按Ctrl+F5组合键运行程序,可以看到我们枚举的进程都出来了。然后选中“svchost.exe”进程,单击“查看DLL”按钮,“svchost.exe”进程中加载的DLL也都枚举出来了,这样运行是没有问题的。接下来找到编译好的任务管理器运行(不要直接在VC6下运行),可以看到,我们枚举的进程也都显示出来了。仍然选中“svchost.exe”,然后单击“查看DLL”,是不是没有查看到“svchost.exe”进程加载的DLL文件,这是什么原因?换一个其他的进程试试,比如选择自己编写的任务管理器试试,可以查看其DLL文件。通过试验发现,系统文件的DLL我们都无法枚举到,可是在VC6下直接运行是可以枚举到的。不单单是这方面的问题,而且在使用OpenProcess()函数打开如smss.exe、winlogon.exe等系统进程的时候,也同样会导致函数的调用失败,其实这个问题是当前进程权限级别不够所导致的。解决这个问题很容易,只要当前进程具有“SeDebugPrivilege”权限就可以了,接下来就来说明这个调整当前进程的权限。


调整权限其实并不复杂,主要有3个步骤。


(1)使用OpenProcessToken()函数打开当前进程的访问令牌。

(2)使用LookupPrivilegeValue()函数取得描述权限的LUID。

(3)使用AdjustTokenPrivileges()函数调整访问令牌的权限。


调整权限使当前进程拥有“SeDebugPrivilege”权限,拥有这个权限后,当前进程可以访问一些受限的系统资源。在远程线程注入的时候,同样需要调整当前进程的访问令牌权限,否则是无法对系统进程进行注入的。因为在进行注入的时候,同样要用到OpenProcess()这个函数。下面给出调整权限的代码,不过在这个代码里面没有做返回值的判断,通常情况下是没有问题的。代码如下:

八、进程的暂停与恢复


在有些时候,我们不得不让进程暂停运行。比如,病毒有两个运行的进程,它们在不断的“互相帮助”,当一个病毒进程发现另一个病毒进程被结束了,那么它会再次把那个病毒运行起来。由于两个进程一直在做这样的事情,而且频度很高,因此很难把两个进程都结束掉。这样,就不得不让病毒的进程暂停了,当两个进程都暂停掉以后,就可以把病毒结束掉了。


让进程暂停,通常使用的是SuspendThread()函数。该函数的定义如下:

该函数就一个参数,是一个线程的句柄。获得线程的句柄需要使用OpenThread()来打开一个指定的线程。对于得到线程ID,我们可以对线程进行枚举,然后就可以打开线程并将其暂停了。


注:OpenThread()函数在VC6提供的PSDK中是不存在的,必须更新PSDK才可以使用。如果没有更新PSDK的话,需要使用LoadLibrary()和GetProcAddress()来使用该函数。对于LoadLibrary()和GetProcAddress()函数的使用在DLL编程中将会进行介绍。


枚举线程的函数是Thread32First()和Thread32Next()这两个,对于枚举线程前,我们用CreateToolhelp32Snapshot()只能创建系统的线程快照,不能创建指定进程中的线程的快照。这样在暂停线程时,必须对枚举到的线程进行判断,看其是否为指定进程中的线程。如何判断一个线程是属于哪个进程的呢?我们看一下THREADENTRY32这个结构体。

该结构体中,th32ThreadID标识了当前枚举到的线程的线程ID,th32OwnerProcessID标识了该线程归属的进程ID。因此,只要进行一次简单的判断就可以了。看一下暂停的代码:

与进程暂停相对应的是恢复暂停的进程。恢复暂停的进程的函数是使用ResumeThread()函数,该函数原型如下:

该函数的使用方法与SuspendThread()一样。恢复暂停的进程的代码大家可根据暂停进程的代码自行修改,这里就不给出完整的代码了。


给大家介绍一个不错的工具,该工具的界面如图9所示。

图9  Process Explorer的界面

该软件的功能非常强大,当启动一个进程或者结束一个进程的时候,该软件会高亮显示被启动或结束的进程。当然了,它的功能非常多,还是大家自己研究挖掘一下。在这里重点介绍该工具中的一个小功能,单击菜单“Options”->“ReplaceTaskManager”命令,该功能是用来替换系统的任务管理的,也就是将Process Explorer设为默认的任务管理器。大家替换一下任务管理器,然后按下Ctrl+Shift+Esc组合键试试看,是不是Process Explorer被打开了,原来的任务管理器不见了。如果想要还原到原来的任务管理器,只要再次单击“Replace Task Manager”菜单项就可以了,我们单击一下该菜单还原到原来的任务管理器。


该功能是如何实现的呢?原理其实是对注册表做了手脚,对注册表的哪些地方做了手脚呢?我们介绍另外一个值得推荐的工具,叫做Regmon,它是用来监控注册表的。该软件如图10所示。

图10  RegMon界面

按Ctrl+L组合键,弹出“Regmon Filter”界面,我们在“Include”文本框中输入“procexp.exe”,如图11所示。

图11  RegMon Filter界面

输入完后单击“OK”按钮,再单击Process Explorer的“Replace Task Manager”菜单项,看RegMon捕获到的注册表的信息,如图12和图13所示。

图12  修改的注册表项

图13  修改的注册表键的值

打开注册表编辑器看一下被修改的内容,如图14所示。

图14  注册表中被修改的值

将该值删掉,再按下Ctrl+ Shift+Esc组合键看一下,默认的任务管理器出现了,这就是注册表中有名的映像劫持。大家可以自己在我们编写的任务管理器中添加这样一个替换系统任务管理器的功能以做练习。


九、多线程


线程是进程中的一个执行单位(每个进程都必须有一个主线程),一个进程中可以有多个线程,而一个线程只存在于一个进程中。在数据关系上,这是一对多的关系。线程不拥有系统资源,线程所使用的资源全部由进程向系统申请。


在多处理器中,不同的线程可以同时运行在不同的CPU上,这样可以提高程序运行的效率。除此而外,在有些方面必须要使用多线程。比如,如果扫描磁盘并同时在程序界面上显示当前扫描的位置,这样就必须使用多线程。因为在程序界面上显示和磁盘的扫描工作在同一个线程中,而且界面也不停地进行重新显示,这样就会导致软件看起来像是卡死一样。如果分为两个线程就可以解决该问题。界面的显示由主线程完成,而扫描磁盘的工作由另外一个线程工作,两个线程协同工作,这样就可以达到我们想要的效果了。


首先了解一下线程的创建,线程的创建使用CreateThread()函数,该函数的原型如下:

参数说明如下。


(1)lpThreadAttributes:该函数指向一个安全属性,该参数一般设置为NULL。


(2)dwStackSize:该参数指定线程的栈大小,该参数一般设置为0,表示默认栈大小。


(3)lpStartAddress:该参数指向一个线程函数地址,该函数属于一个回调函数。所谓回调函数,就是由系统去调用该函数,并不是由我们直接调用该函数。


线程函数的定义如下:

线程函数的返回值为DWORD类型,该函数只有一个参数,该参数由CreateThread()函数给定。该函数的函数名称可以任意给定。这里介绍一下WINAPI,WINAPI是一个宏,该宏的定义如下:

这是一种函数的调用约定,在这里只需要了解就可以了。


(1)lpParameter:该参数是传递给线程函数的一个参数。


(2)dwCreationFlags:该参数指明创建线程后的线程状态,在创建线程后可以让线程立刻执行,也可以让线程处于暂停状态。如果需要立刻执行,将该参数设置为0;如果要让线程处于暂停状态,那么该参数值设为CREATE_SUSPENDED;需要线程执行时调用ResumeThread()函数。


(3)lpThreadId:该参数用于返回新创建线程的线程ID。


该函数返回新创建线程的句柄,在线程结束后需要使用CloseHandle()函数关闭该句柄以便释放资源。


每个线程都有自己的CPU时间片,当主线程创建了新线程后,它的CPU时间片并没有完,它还可以继续执行。由于主线程的代码非常少,在CPU指定的CPU时间片中主线程执行完后就退出了。主线程结束,那么意味着程序也就结束了,所以我们自己创建的线程根本就没有被执行到。如果要等我们自己创建的线程结束后才可以输出“main”的话,主线程要如何等待我们创建的线程呢?答案是使用WaitForSingleObject()函数,该函数的原型如下:

参数说明如下。


(1)hHandle:该参数指向要等待的对象句柄。


(2)dwMilliseconds:该参数指定等待超时的毫秒数,设置为INFINITE则表示一直等待到线程函数返回。INFINITE是系统给出的一个宏,该定义如下。

该函数如果返回为WAIT_OBJECT_0,表明制定的对象已经是处于完成状态;如果返回值为WAIT_TIMEOUT,则表明在指定的时间内没有完成,处于潮湿状态。若该函数调用失败,则返回WAIT_FAILED。


修改上面的代码,在CreateThread()函数的后面加入以下代码:

添加WaitForSingleObject()以后,主线程会等待我们的创建的线程结束后再执行主线程后续的代码。这样,在控制台上就会分别打印“ThreadProc”和“main”了。

在使用多线程时常常需要注意很多问题。比如,多个线程同时对某一个共享资源进行操作,那么可能就会出现问题。来看一个简单的例子:

每个线程都有一个CPU时间片,当自己的时间片运行完成后,CPU会停止该线程的运行,并切换到其他线程去运行。当多线程同时操作一个共享资源时,这样的切换会带来隐形的问题。我们的代码比较短,在一个CPU时间片内会完成,因此可能看不出错误,为了达到出错的效果,在代码中加入Sleep(1),主动让出CPU,让CPU进行线程的切换。这里的共享资源就是那个全局变量g_Num_One。调用该线程函数的代码如下:

我们创建10个线程,每个线程都让g_Num_One进行10次自增1的操作,那么g_Num_One的结果应该是100。我们实际运行一下看看结果是多少,如图15所示。

图15  运行结果

这个输出结果并不是我们想要的输出结果。在多线程环境中,对共享资源的操作要进行保护,在这里,我们可以使用临界区对象对该全局变量进行保护。


临界区对象是一个CRITICAL_SECTION的数据结构Windows操作系统使用该数据结构来进行对关键代码的保护,以确保多线程下的共享资源。


对于临界区的函数有4个,分别是初始化临界区对象(InitializeCriticalSection())、进入临界区(EnterCriticalSection())、离开临界区(LeaveCriticalSection())和删除临界区对象(DeleteCriticalSection())。这4个函数的定义分别如下:

这4个函数的参数都是指向CRITICAL_SECTION结构体的指针。我们修改的代码如下:

再次编译运行该代码,输出的结果为正确结果,即g_Num_One的值为100。除了临界区对象以外,对于线程的同步还有其他的方法,这里就不进行一一的介绍了。希望大家在今后开发多线程编程时,切记要注意多线程的同步问题。

微信公众号:计算机与网络安全

ID:Computer-network

【推荐书籍】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存