windows核心编程 - 线程基础

时间:2023-10-23 16:57:14

一、基本概念:

一个进程至少需要一个线程。

组成:一个线程包括仅包括一个线程堆栈一个线程内核对象

线程堆栈:用于维护线程在执行代码时需要的所有函数参数和局部变量

线程内核对象:操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息。(此处内核对象专指线程内核对象)

生命周期:线程总是创建于某个进程环境中,其生命周期仅存在于创建它的进程生命周期内。

执行位置:线程在进程的地址空间中执行代码并进行数据操作。从属于同一进程的线程共享进程的地址空间,可以执行一样的代码并对一样的数据进行操作,共享该进程的内核句柄。

线程状态

二、创建线程

2.1 线程创建函数CreateThread

 WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ __drv_aliasesMem LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);

通常关住的一个参数是lpStartAddress,传入线程函数入口地址。

其类型定义如下:

 typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(
LPVOID lpThreadParameter
);
typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE;

通常入口函数格式定义如下:

 DWORD WINAPI  ThreadFunc(PVOID pvParam)
{
DWORD dwRet = ; .
.
.
return dwRet;
}

线程创建函数_beginthreadex

 _CRTIMP uintptr_t __cdecl _beginthreadex(_In_opt_ void * _Security, _In_ unsigned _StackSize,
_In_ unsigned (__stdcall * _StartAddress) (void *), _In_opt_ void * _ArgList,
_In_ unsigned _InitFlag, _Out_opt_ unsigned * _ThrdAddr);

_beginthreadex函数内部调用CreateThread

  当CreateThread函数被调用且调用成功时,系统创建一个线程内核对象,该内核对象并不是线程本身,而是一个系统内核对该线程进行管理的数据结构,对象类型为HANDLE。

  如果后续代码对该数据结构内容不感兴趣,可以调用CloseHandle直接将该内核对象关闭,而不用等到线程结束时关闭或等进程关闭时被动销毁。

2.1.1 lpThreadAttributes

  类型为LPSECURITY_ATTRIBUTES,安全性描述符结构体指针。表示创建的线程安全性,可以设置其对后续子线程的安全性的继承特性。

2.1.2 dwStackSize

  用于设定启动的线程可以将多少地址空间用于它的堆栈,每个线程维护自己的堆栈空间。

  传0时,默认值为1M。链接器的/STACK参数可以修改此默认值。后续传入值与该值进行比较,取较大值。

2.1.3 lpStartAddress & lpParameter

  lpStartAddress 用于指明新线程执行的线程函数的入口地址。

  lpParameter 用于创建者为给线程传递的外部参数,空指针类型可以很好的兼容转换。

  多个线程使用同一个线程函数是合乎逻辑的,而且非常有用。例如服务程序在处理相同的请求时候为不同客户线程执行相同的处理函数。

2.1.4 dwCreationFlags

  该标记有2个值,0是创建后立即执行,CREATE_SUSPENDED为创建后挂起状态。通常CREATE_SUSPENDED用于创建线程后进行一些属性的修改工作,不是很常用。

2.1.5 lpThreadId

  必需是一个有效的DWORD变量的地址,新创建的线程ID会存放在该变量地址中作为输出参数。

三、终止线程

3.1 线程函数返回

  始终都应该将线程设计成这样的形式,即当线程需要终止运行时,它们就能够返回,这是确保线程所有资源都安全释放的唯一方法。

  1.所有C++的对象都可以正确被析构

  2.操作系统正确释放线程堆栈的内存资源

  3.操作系统将线程的退出代码设置为线程函数的返回值

  4.系统将递减线程内核对象的计数

3.2 ExitThread函数

 WINBASEAPI
DECLSPEC_NORETURN
VOID
WINAPI
ExitThread(
_In_ DWORD dwExitCode
);

  在线程执行函数内部调用该函数以在当前位置直接结束线程。操作系统负责释放当前线程的所有系统资源。

  但是,C++对象不会被析构。

  该行函数调用之后的代码都不会执行。

3.3 TerminateThread函数

 WINBASEAPI
BOOL
WINAPI
TerminateThread(
_In_ HANDLE hThread,
_In_ DWORD dwExitCode
);

  在线程外部,需要取得该线程内核对象句柄才可以使用这个函数进行外部结束,第二个参数dwExitCode设置线程的退出代码。

  同时包含CloseHanle的功能,该句柄的线程内核对象计数减一。

  缺点:线程所属进程结束之前,线程堆栈不会撤销;微软这种实现是为了防止其他线程使用该堆栈,保证强制执行TerminateThread之后的相对稳定; 不会正确析构C++对象;(调用的DLL收不到通知?)

3.4 进程结束时线程结束。

   。。。

3.5 线程终止运行时发生的操作

  • 线程所拥有的用户对象被释放
  • 线程的退出代码从STILL_ACTIVE改为?*** 传递给ExitThread或TerminateThread的代码
  • 线程内核对象状态变为已通知
  • 如果该线程为进程中最后一个线程,操作系统将视为进程结束
  • 线程内核对象计数减一

  当线程终止运行时,在与它相关联的线程内核对象的所有未结束的引用关闭之前,该内核对象不会自动被释放。

  一旦线程不再运行,系统中没有其他线程可以处理该线程的句柄,然而别的线程可以调用GetExitCodeThread函数来检查由hThread标识的线程是否已经终止运行,如果该线程已经终止运行,则确认其退出代码,由lpExitCode指向的DWORD存储。

  若调用GetExitCodeThread时线程尚未终止运行,则返回代码为STILL_ACTIVE,否则函数返回TRUE。

 WINBASEAPI
_Success_(return != )
BOOL
WINAPI
GetExitCodeThread(
_In_ HANDLE hThread,
_Out_ LPDWORD lpExitCode
);

3.6 线程的一些性质

  调用CreateThread函数成功后,操作系统创建线程内核对象,其结构大致如下:

windows核心编程 - 线程基础

  包含一个CONTEXT结构,主要保存线程执行状态下的CPU寄存器内容;其他的线程内核对象计数、信号状态、退出代码、使用计数等其他对象属性信息。

  指令指针堆栈指针寄存器是线程上下文中两个最重要的寄存器。记住,线程总是在进程的上下文中运行的。因此,这些地址都用于标识拥有线程的进程地址空间中的内存。

3.7 C/C++运行期库的考虑

Visual C++配有6个C/C++运行期库:

  • LibCMt.lib    用于多线程应用程序的静态链接库的发布版本
  • LibCMtD.lib    用于多线程应用程序的静态链接库的调试版本
  • MSVCRt.lib  导入库,用于动态链接库MSVCR80.dll的发行版输入库(视vs版本而定)
  • MSVCRtD.lib  导入库,用于动态链接库MSVCR80D,dll的调试版本输入库
  • MSVCMRt.lib 导入库,用于托管/原生代码混合
  • MSVCURt    导入库,编译成百分之百纯MSIL代码

  早期1970年的时候,标准C运行库发布时候并没有多线程概念,很多的全局静态变量及函数都不适用于新的多线程环境,在多线程下就需要为各个线程创建这些线程自己使用的“全局结构”。

  作为一个windows开发人员,必需知道CreateThread函数与ExitThread函数存在的一些可能的隐患。

  在<Program Files>\Microsoft Visual Studio 8\VC\crt\src\Threadex.c下可以找到相关_beginthreadex函数的源码,很明显的可以发现它在内部调用了CreateThread函数,因为windows操作系统只知道用这个方式创建线程。so,_beginthreadex在创建线程之前会比CreateThread做更多的事情。

关于_createthreadex函数,注意以下:

  1. 每个线程会从C/C++运行库的(HEAP)上申请独立的_tiddata内存块
  2. 传递给_beginthread的线程函数入口地址及部分其他参数保存在_tiddata结构中
  3. _beginthread会在内部调用CreateThread,因为windows操作系统只知道这么创建新线程
  4. 在它调用CreateThread函数时,传递的执行函数入口不是前面提到的pfnStartAddr,而是一个统一入口地址_threadstartex,参数pvParam的位置传入的是_tiddata,其本身的值存放在_tiddata中
  5. 一切顺利的话,返回线程句柄,就像CreateThread那样。操作失败则返回0

关于_threadstartex函数,要注意以下重点:

  1. 新线程首先执行RtlUserThreadStart(在NTDLL.dll文件中),然后再跳转到_threadstartex函数
  2. _threadstartex函数唯一参数就是新线程的_tiddata内存块地址
  3. TlsGetValue是一个操作系统函数,它将一个值与主调线程关联起来。这就是所谓的线程本地存储(TLS,Thread Local Storage),_threadstartex函数将_tiddata内存块与新线程联系起来
  4. 在无参的helper函数_callthreadstartex函数中,一个SEH帧将预期要执行的线程函数包围起来,它处理很多与运行库有关的事情。
  5. 预期要执行的线程函数将被调用,并向其传递预期的参数
  6. 线程函数的返回值被认为是线程的退出代码  

关于_endthreadex函数,要注意以下重点:

  1. C运行库的_getptd_noexit函数在内部调用操作系统的TlsGetValue函数,后者获取主调线程的_tiddata内存块地址
  2. 释放该内存块,调用操作系统的ExitThread函数实际销毁线程。退出代码会被传递,并正确设置。

3.8 引用进程或线程内核对象

HANDLE GetCurrentProcess();

HANDLE GetCurrentThread();

这两个函数获取主调进程或线程的内核对象伪句柄,它们并不会在进程的句柄表中创建新句柄或者改变现有内核对象使用计数。

DWORD GetCurrentProcessId();

DWORD GetCurrentThreadId();

这两个函数用于获取当前进程或线程的唯一ID。

 WINBASEAPI
BOOL
WINAPI
DuplicateHandle(
__in HANDLE hSourceProcessHandle,
__in HANDLE hSourceHandle,
__in HANDLE hTargetProcessHandle,
__deref_out LPHANDLE lpTargetHandle,
__in DWORD dwDesiredAccess,
__in BOOL bInheritHandle,
__in DWORD dwOptions
);
DuplicateHandle函数通常用与另一个进程相关的内核对象来创建一个与进程相关的新句柄,也可以实现把上述伪句柄转换为真实句柄。因此它会将内核对象的使用计数加1,在该实句柄使用完毕后需要进行CloseHandle操作。