第六章:线程基础

来源:互联网 发布:ps破解软件下载 编辑:程序博客网 时间:2024/06/05 16:50

1:线程基础

进程有进程内核对象和地址空间,而线程则是线程内核对象和线程栈

之所以线程比进程高效,是因为进程地址空间要占用很多资源用于记录,并且.exe和.dll文件要加载到地址空间,还要用到文件资源

2:CreateThread

HANDLE CreateThread(  LPSECURITY_ATTRIBUTES lpThreadAttributes,  SIZE_T dwStackSize,  LPTHREAD_START_ROUTINE lpStartAddress,  LPVOID lpParameter,  DWORD dwCreationFlags,  LPDWORD lpThreadId);

2.1:LPSECURITY_ATTRIBUTES lpThreadAttributes参数一般为NULL,如果要让此线程内核对象句柄可以被继承,则应创建一个安全描述符并将其bInheritHandle设为true

2.2:dwStackSize为堆栈大小,默认是预留1M,调拨一个页面存储空间,如果调拨的用完,则会产生一个异常,这样会继续调拨一个页面,最多达到1M,这是为了检测死循环BUG

2.3:lpStartAddress是线程函数,LPVOID lpParameter是传入参数,线程函数一般如下

DWORD WINAPI MyThread(PVOID pLparam){return dwResult;//返回线程退出代码,这个代码会被设置到线程内核对象中}

2.4:DWORD dwCreationFlags如果为0,则线程创建好后可以立即被调度,以可以设置为CREATE_SUSPENDED

2.5:LPDWORD lpThreadId可以设置为NULL,表示我们不需要线程ID

3:线程退出
3.1:线程退出的几种方式:

线程函数返回

ExitThread(DWORD dwExitCode);

TerminateThread(HANDLE hd,DWORD dwExitCode);

进程终止

3.2:线程函数返回会执行如下操作

C++对象被析构

线程栈被释放

线程退出代码被设置进线程内核对象

递减线程内核对象使用计数

3.3:线程自己不能阻止自己被TerminateThread()

ExitThread()会销毁线程栈,但TerminateThread()不能销毁线程栈,微软是故意这样设计的,防止一个线程被Terminate后别的线程还需要用它的数据

DLL在一个线程终止时会受到通知,但被TerminateThread()的线程,DLL不会受到通知

3.4:线程终止时,退出代码从STILL_ACTIVE变为函数返回或者调用终止函数时设置的退出代码

线程内核对象变为触发状态,其引用计数-1

如果此线程是进程最后一个线程,则进程被终止

3.5:可以通过GetExitCodeThread(HANDLE hd,DWORD dwExitCode)获得线程退出代码

4:线程内幕

4.1:线程内核对象包括:

使用计数:初始值2

挂起计数:初始值1,如果CreateThread没有设置CREAT_SUSPENDED,则挂起计数-1,线程可以被调度

退出代码:初始值STILL_ACTIVE

触发状态:初始值未触发

一个CONTEXT(上下文)结构,这个结构保存了线程用到的所有寄存器,IP指向NTDLL.DLL中的RtlUserThreadStart();SP指向线程函数;初始化线程栈的时候,会以此压入线程入口参数(pvParam)和函数地址(pfnStartAddr),当程序运行时,变会执行IP所指向的地方的函数(RtlUserThreadStart()),并且由于这两个参数已经被压入线程栈,这等于是给RtlUserThreadStart()传入了这两个参数,RtlUserThreadStart()执行的基本操作如下:

VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr,PVOID pvParam){_try{ExitThread((pfnStartAddr)(pvParam));}_except(UnhandledExceptionFilter(GetExceptionInformation())){ExitProcess(GetExceptionCode());}}

4.2:RtlUserThreadStart永远也不会返回

4.3:当创建的线程是主线程时,RtlUserThreadStart运行时会调用C/C++运行时代码,后者会调用入口函数,入口函数返回后会调用C/C++运行库ExitThread()或者ExitProcess();所以对于主线程来说,永远不会返回RtlUserThreadStart函数

4.4:再次说明,对操作系统而言,线程没有主次之分,主线程是针对C/C++运行库而言的

5:C/C++运行库

5.1:C/C++运行库只有多线程版本

5.2:当我们创建一个线程时,应该调用_beginthreadex(),而不应该调用CreateThread(),原因是:C/C++运行库有很多全局变量,多线程访问会出问题,所以C/C++运行库把这些变量(不是全局变量,5.6解释)设置为线程相关的(线程局部存储),这就要为创建的线程分配内存以存放这些变量,但调用操作系统CreateThread()不会分配多余内存,所以必须调用_beginthreadex()

5.3:_beginthreadex()的调用在Threadex.c中,5.2创建的内存块叫做_tiddata,线程函数地址也被保存在这个内存块中,_beginthreadex()会调用CreateThread(),并且传递给CreateThread()的函数入口地址是_threadstartex而非pfnStartAddr函数地址,传递给CreateThread()的参数地址也不是pvParam而是_tiddata内存块

5.4:调用过程为:RtlUserThreadStart()调用_threadstartex(),_threadstartex()从线程局部存储中获得_tiddata内存块,从中获得真正的线程入口函数,调用它,等此函数返回后,调用_endthreadex析构_tiddata内存块,然后调用ExitThread()

5.5:如果手动调用了ExitThread(),则必须手动调用_endthread()析构_tiddata内存块,但不支持这样做

5.6:对于全局变量的处理:全局变量被一个宏重定义,如全局变量error

#define error (*_error)())

调用error实际上是在调用C/C++运行库一个函数,该函数会获取线程相关的数据并返回(这个数据被保存在_tiddata中)

5.7:C/C++运行库会为一些函数同步,如malloc函数

6:如果调用CreateThread()而不是_beginthreadex()
CreateThread()的调用不会分配_tiddata内存块,当要使用内存块中的内容时,C/C++运行库会自动分配_tiddata内存块,并与相应线程关联,但线程终止时,应该显式调用_endthread()析构这个内存块,但一般都会忘记析构这个内存块,所有这种调用方式是不被提倡的

如果有一个DLL连接到C/C++运行库的DLL版本时,这个DLL会在线程析构的时候收到一个DLL_THREAD_DETACH通知,并释放_tiddata内存块,但任然强烈建议不要使用CreateThread()函数

不要调用_beginthread()和_endthread()而应该调用_beginthreadex和_endthreadex()

7:一些有价值的函数

7.1:获得当前进程内核对象句柄和当前线程内核对象句柄:GetCurrentProcess()和GetCurrentThread(),这回返回一个伪句柄,所谓伪句柄是指不会影响其使用计数的句柄,所以不需要调用CloseHandle()递减其引用计数,如果向CloseHanlde()传递了一个伪句柄,CloseHanled()也不会递减其引用计数,这种情况下GetLastError()会返回ERROR_INVALID_HANDLE

7.2:查询进程用时:

FILETIME ftCreationTime,ftExitTime,ftKernelTime,ftUserTiem;GetProcessTimes(GetCurrentProcess(),&ftCreationTime,&ftExitTime,&ftKernelTime,&ftUserTiem);

8:伪句柄转换为真正的句柄

伪句柄指向的是当前进程或线程的句柄,如果写这样一段代码,父进程创建子进程,并将自己的伪句柄传递给子进程,子进程调用GetProcessTimes()想获得父进程执行时间,由于传递的是一个伪句柄,则会得到自己的执行时间

解决办法是用DumplicateHandle()得到一个真正的句柄

8:TLS

慎用TLS,一般在线程栈上分配数据能达到同样的效果,TLS主要解决有大量全局变量或者静态变量的情况

8.1:动态TLS

一个进程创建时,会分配全局使用标志,微软保证至少有64个标志可被使用,但这是动态扩充的,最多能扩充到1000多个

一个线程创建时,会分配同全局使用标志一样多的数组,这是用来存储数据的

使用方法如下

PVOID pElement;//需要存储的数据DWORD dwIndex=TlsAlloc();//存储的位置TlsSetValue(dwIndex,pElement);//存储进线程对应数组PVOID p=TlsGetValue(dwIndex);//获取存储的数据TlsFree(dwIndex);//释放存储位置

TlsSetValue(dwIndex,pElement)中的dwIndex不是TlsAlloc分配的也没有问题,因为没有进行错误检查,这是为了保证速度

TlsAlloc()的一个附加作用是在分配这个存储位置之前,会把每个线程这个位置的数据清零,然后再把这个位置分配出去,这样做的原因是因为:如果加载了一个DLL,设置了该位置,然后释放,如果没有清理过程,下一个使用该位置的DLL也许会有这样的逻辑,检查这里有没有值,如果有值,干什么什么事

8.2:静态TLS

静态TLS不需要调用任何函数,TLS变量定义方法例子如下:

_declspec(thread) DWORD gt_dwSrartTime=0;

_declspec(thread)告诉编译器应该把变量放入自己的段中,gt_dwSrartTime必须是全局或者静态变量

编译器编译时,会将所有上述TLS变量放到他们自己的段中,段名为.tls

编译器链接时,会将上述所有.tls段合并成一个更大的.tls段,并保存在exe或者dll中

当操作系统加载exe或者dll时,会查看.tls段,并为其分配内存块,当访问TLS变量时,操作系统会将其定位到此内存块,这里操作系统会生成额外的代码(3条机器指令),所以静态TLS会让应用程序更大而且更慢

如果新创建了一个线程,系统会再分配一个内存块,新线程会访问新内存块中的TLS变量

这里没有讨论书上DLL和TLS之间的情况