`
mmdev
  • 浏览: 12960644 次
  • 性别: Icon_minigender_1
  • 来自: 大连
文章分类
社区版块
存档分类
最新评论

【Windows核心编程】Windows核心编程 -- 线程基础

 
阅读更多

一,线程

与前面介绍的进程一样,线程也有两部分组成:

1)一个线程内核对象,操作系统用它来管理线程。内核对象中还存储了线程的各种统计信息,包括挂起计数、退出代码等,以便于系统对线程的管理。内核对象中有一个CONTEXT结构,这个结构中存储了线程上一次执行的时候CPU寄存器的状态。

2)一个线程栈,用于维护线程执行时所需的所有函数参量和局部变量。

位于同一个进程的线程共享进程的地址空间且它们共享进程句柄表。因为句柄表是针对进程的。进程需要很多的系统资源,而线程仅仅需要一个线程内核对象和线程栈就可以了,因此线程比进程的开销要小得多。采用多线程来处理问题也是理所当然的了。

采用多线程可以提高程序的执行效率,但是多线程也存在很多问题。在尝试使用多线程时如果处理不当还可能会引入新的问题。如同步问题。

二,编写第一个线程函数

每个线程都需要一个入口点函数。这是线程执行的起点。主线程的入口点函数是_tmain或_tWinmain。如果在进程中创建新线程必须提供自己的入口点函数。

形如:

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

线程函数可以是任何我们希望它执行的任务,最终线程函数会终止并返回。类似于进程内核对象,如果线程内核对象使用计数变为0,则会被销毁。

1) 默认情况下主线程的入口点函数必须命名为main,wmain,WinMain或wWinMain。我们可以通过设置/ENTRY:链接器选项来指定另一个函数作为入口点函数。

2)主线程入口点函数有字符串参数,所以它提供了ANSI/Unicode版本。相反,线程函数只有一个参数,其意义可由我们定义。可以为其传递一个值,也可以将其作为某个数据结构的指针。这需要在线程函数内部做类型转换。

3)线程函数必须返回一个值,它的值传递给ExitThread,作为线程的退出代码。

4)线程函数尽可能地使用局部变量函数参数,它们是在线程栈上创建的。不太可能被其他线程破坏。使用静态变量或全局变量时其他线程可以访问这些变量,这会导致同步和互斥问题。


三,CreateThread 函数

如果想创建一个或多个辅助线程,只需让一个正在运行的线程调用CreateThread :

HANDLE CreateThread(

PSECURITY_ATTRIBUTES psa, //指向SECURITY_ATTRIBUTES结构的指针;

DWORD cbStackSize, //制定线程可以为其线程栈使用多少地址空间; 栈空间大小 ,传入0则默认使用内部值 链接器/STACK 可以改变大小 默认1M

PTHREAD_START_ROUTINE pfnStartAddr, //线程函数的地址;

PVOID pvParam, //线程函数参数

DWORD dwCreateFlags, //指定额外的标志来控制线程的创建; CREATE_SUSPENDED 暂停执行

PDWORD pdwThreadID); //存储系统分配给新线程的ID;

例:

DWORD WINAPI FirstThread(PVOID pvParam)

{

int x=0; //这里的函数,为SecoundThread 的返回值,最好声明为static,防止FirstThread 执行完,SecoundThread 返回时,访问栈越界。

DWORD dwThreadID;

HANDLE hThread = CreatThread( NULL, 0 , SecondThread , (PVOID)&x , 0, &dwThread);

}

DWORD WINAPI SecondThread(PVOID pvParam)

{


*(int *)pvParam =5;

}

调用 CreateThread 时,系统会创建一个线程内核对象。系统从进程地址空间中分配内存给线程栈使用。新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其它所有线程的栈

CreateThread 函数时用于创建线程的Windows函数。不过如果写的是C/C++代码,就绝对不要调用 CreateThread。 正确地选择是使用Microsoft C++运行库函数_beginthreadex 。如果使用的不是Microsoft C++编译器,你的编译器的提供商应该提供类似的函数来替代 CreateThread 。不管这个替代函数时什么,都必须使用它。

四,终止运行线程

线程可以通过以下4种方法来终止运行:

线程函数返回(这是强烈推荐的);

线程通过ExitThread 函数“杀死”自己(应避免);

同一个进程或另一个进程中的线程调用TerminateThread 函数(应避免);

包含线程的进程终止运行(应避免);

五,线程函数返回

设计线程函数时,应该确保在我们希望线程终止运行时,就让它们返回。这是保证线程的所有资源被正确清理的唯一方式。让线程函数返回,可以确保一下正确地应用程序权利工作都得以执行:

线程函数中创建的所有C++对象都通过其析构函数被正确销毁;

操作系统正确释放线程栈使用的内存;

操作系统把线程的退出代码(在线程的内核对象中维护)设为线程函数的返回值;

系统减少线程的内核对象的使用计数;

六,ExitThread函数

VOID ExitThread(DWORD dwExitCode);

该函数将终止线程的执行,并导致操作系统清理该线程使用的所有系统资源。但是你的C/C++资源(如C++类对象)不会被销毁。所以更好的做法是直接从线程函数返回不要自己调用 ExitThread 。

ExitThread 是Windows用于“杀死”线程的函数,如果要写C/C++代码,就绝对不要调用 ExitThread 。相反,应该使用C++运行库函数_endthreadex 。 如果使用的不是Microsoft的C++编译器,那么编译器供应商应该提供它们自己的 ExitThread 替代函数。 不管这个替代函数是什么,都必须使用它。

七,TerminateThread函数

BOOL TerminateThread(

HANDLE hThread,

DWORD dwExitCode);

TerminateThread 是异步函数 ,ExitThread 函数来终止线程,线程的堆栈会被销毁,而 TerminateThread ,除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈。

八,线程终止运行时

一个线程终止时,系统会一次执行以下操作:

线程拥有的所有用户对象句柄会被释放;

线程的退出代码从STILE_ACTIVE变为传给ExitThread或TerminateThread函数的代码;

线程内核对象的状态变为已触发状态;

如果线程是进程中的最后一个活动线程,系统认为进程也终止了;

线程内核对象的使用计数递减1;
九,线程内幕

CreateThread 函数的一个调用 导致 系统创建一个线程内核对象,该对象最初的使用计数为2。( 创建线程内核对象加1,返回线程内核对象句柄加1 ),所以除非线程终止,而且 CreateThread 返回的句柄关闭,否则线程内核对象不会被销毁。该线程对象的其它属性也被初始化:暂停计数被设为1,退出代码被设备STILE_ACTIVE(0x103),而且对象被设为未触发状态。
创建了内核对象,系统就分配内存,供线程的堆栈使用。此内存是从进程的地址空间分配的,因为线程没有自己的地址空间。系统将来个值写入新线程堆栈的最上端,如图1所示,即调用的线程函数及其参数。

每个线程都有自己的一组CPU寄存器,称为线程的上下文(context)。上下文反映了当线程上一次执行时,线程CPU寄存器的状态。CONTEXT结构保存在线程的内核对象中。

当线程内核对象被初始化的时候,CONTEXT结构的堆栈指针寄存器被设为pfnStartAddr在线程堆栈中的地址。而指令指针寄存器被设为RtlUserThreadStart函数的地址。

VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) {
__try {
ExitThread((pfnStartAddr)(pvParam));
}
__except(UnhandledExceptionFilter(GetExceptionInformation())) {
ExitProcess(GetExceptionCode());
}
// NOTE: We never get here.
}

线程完全初始化之后,系统检查CREATE_SUSPENDED标志是否已被传给CreateThread函数。如果此标记没有传递,系统将线程的挂起计数递减至0;随后,线程就可以调度给一个处理器去执行。然后,系统在实际的 CPU寄存器中加载上一次在线程上下文中保存的值。现在,线程可以在其进程的地址空间中执行代码并处理数据了。

新线程执行RtlUserThreadStart函数的时候,将发生以下事情:

围绕线程函数,会设置一个结构化异常处理(SEH)帧。这样一来,线程执行期间所产生的任何异常都能得到系统的默认处理。

系统调用线程函数,把传给CreateThread函数的pvParam参数传给它。

线程函数返回时,RtlUserThreadStart调用ExitThread,将你的线程函数的返回值传给它。线程内核对象的使用计数递减,而后线程停止执行。

如果线程产生了一个未被处理的异常,RtlUserThreadStart函数所设置的SEH帧会处理这个异常。通常,这意味着系统会向用户显示一个消息框,而且当用户关闭此消息框时,RtlUserThreadStart会调用ExitProcess来终止真个进程,而不是终止有问题的线程。

当一个进程的主线程初始化时,其指令指针指向RtlUserThreadStart,当RtlUserThreadStart开始执行时,它会调用C/C++运行库的启动代码,后者初始化继而调用你的_tmain或_tWinMain函数。

十,C/C++运行库注意事项

为了保证C和C++多线程应用程序正常运行,必须创建一个数据结构,并使之与使用了C/C++运行库函数的每个线程关联。然后,在调用C/C++运行库函数时,那些函数必须知道去查找主调线程的数据块,从而避免影响到其它线程

编写C/C++应用程序,一定不要调用操作系统的CreateThread函数,相反,应该调用C/C++运行库函数_beginthreadex

 uintptr_t __cdecl _beginthreadex (  void *psa, unsigned cbStackSize,  unsigned (__stdcall * pfnStartAddr) (void *), 
                 void * pvParam,  unsigned dwCreateFlags,  unsigned *pdwThreadID)
    { 
                 _ptiddata     ptd;         // Pointer to thread's data block 
                 uintptr_t      thdl;         // Thread's handle 
                 // Allocate data block for the new thread. 
               if ((ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL) 
                       goto   error_return; // Initialize the data block. 
              initptd(ptd);             // Save the desired thread function and the parameter 
              // we want it to get in the data block. 
              ptd->_initaddr = (void *) pfnStartAddr; 
              ptd->_initarg = pvParam; 
              ptd->_thandle = (uintptr_t)(-1); // Create the new thread. 
              thdl = (uintptr_t) CreateThread ((LPSECURITY_ATTRIBUTES)psa, cbStackSize, _threadstartex , (PVOID)         ptd, dwCreateFlags, pdwThreadID); 
              if (thdl == 0) { 
                        // Thread couldn't be created, cleanup and return failure. 
                       goto error_return; 
             } // Thread created OK, return the handle as unsigned long. 
       return(thdl); 
      error_return: 
         // Error: data block or thread couldn't be created. 
        // GetLastError() is mapped into errno corresponding values 
        // if something wrong happened in CreateThread. 
        _free_crt(ptd); 
       return((uintptr_t)0L); 
    } 

对于_beginthreadex函数有以下重点

1) 每个线程都有自己的专用_tiddata内存块,它们是从C/C++运行库的堆(heap)上分配的。

2)传给_beginthreadex的线程函数的地址保存在_tiddata内存块中。

3)_beginthreadex确实会在内部调用CreateThread,因为操作系统只知道用这种方式来创建一个新线程。

4)CreateThread函数被调用时,传给它的函数地址是_threadstartex(而非pfnStartAddr)。另外,参数地址是_tiddata结构的地址,而非pvParam。

5)如果一切顺利,会返回线程的句柄,就像CreateThread那样。任何操作失败,会返回0。

为新线程初始化_tiddata结构之后,接着来看看这个结构如何与线程关联的:

static unsigned long WINAPI _threadstartex (void* ptd) {

// Note: ptd is the address of this thread's tiddata block.
// Associate the tiddata block with this thread so

// _getptd() will be able to find it in _callthreadstartex.

TlsSetValue(__tlsindex, ptd);
// Save this thread ID in the _tiddata block.
((_ptiddata) ptd)->_tid = GetCurrentThreadId();

// Initialize floating-point support (code not shown).

// call helper function.
_callthreadstartex ();
// We never get here; the thread dies in _callthreadstartex.
return(0L);
}


static void _callthreadstartex(void) {
_ptiddata ptd; /* pointer to thread's _tiddata struct */

// get the pointer to thread data from TLS
ptd = _getptd();


// Wrap desired thread function in SEH frame to
// handle run-time errors and signal support.
__try {
// Call desired thread function, passing it the desired parameter.
// Pass thread's exit code value to _endthreadex.
_endthreadex (
((unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr))
(((_ptiddata)ptd)->_initarg)) ;
}
__except(_XcptFilter(GetExceptionCode(), GetExceptionInformation())){

// The C run-time's exception handler deals with run-time errors

// and signal support; we should never get it here.

_exit(GetExceptionCode());
}
}
关于_threadstartex函数,要注意一下几大重点:

1)新的线程首先执行RtlUserThreadStart(在NTDLL.dll文件中),然后再跳转到_threadstartex;

2)_threadstartex唯一的参数就是新线程的_tiddata内存块的地址;

3)TlsSetValue是一个操作系统函数,它将一个值与主调函数关联起来。这就是所谓的线程局部存储(Thread Local Storage,TLS)。_threadstartex函数将_tiddata内存块与新建线程关联起来;
4)在无参数的辅助函数_callthreadstartex中,有一个SEH帧,它将预期要执行的线程函数包围起来。这个帧处理着与运行库有关的许多事情—比如运行时错误;

5)预期要执行的线程函数会被调用,并向其传递预期的参数。函数的地址和参数会被保存在TLS的_tiddata数据块中,并会在_callthreadstartex中从TLS中获取;
6)线程函数的返回值被认为是线程的退出代码;


再来看看_endthreadex:

void __cdecl _endthreadex (unsigned retcode) { 
    _ptiddata ptd; // Pointer to thread's data block 

    // Clean up floating-point support (code not shown). 
    // Get the address of this thread's tiddata block. 
    ptd = _getptd_noexit (); 


    // Free the tiddata block. 
    if (ptd != NULL) 
        _freeptd(ptd); 
    // Terminate the thread. 
    ExitThread(retcode); 
    } 

对于_endthreadex函数,要注意一下几点:

1)C运行库的_getptd_noexit函数在内部调用操作系统的TlsGetValue函数,后者获取主调函数的tiddata内存块的地址;
2)然后_endthreadex将此数据块释放,并调用操作系统的ExitThread函数来实际地销毁线程。它会传递并正确设置退出代码;


我们应该避免使用ExitThread函数,因为此函数会“杀死”主调线程,而且不允许它从当前执行的函数返回。由于函数没有返回,所以构造的任何C++对象都不会被析构;它还会阻止线程的_tiddata内存块被释放,使应用程序出现内存泄露(直到整个进程终止)。
我们应该尽量用C/C++运行库函数(_beginthreadex,_endthreadex)而尽量避免使用操作系统提供的函数(CreateThread,ExitThread)。

十一,担心伪句柄

HANDLE GetCurrentProcess();

HANDLE GetCurrentThread();

这两个函数返回到主调函数的进程内核对象或线程内核对象的一个伪句柄。它们不会再主调进程的句柄表中新建句柄。即返回的句柄并不在进程句柄表中有实际的表项,也不会影响进程内核对象或线程内核对象的使用计数。如果嗲用CloseHandle函数,并传入一个“伪句柄”,CloseHandle只是简单的忽略此调用。
将伪句柄转换为真正的句柄,DuplicateHandle函数可以执行这个转换,如在进程句柄表中创建线程内核句柄:

    DuplicateHandle( 
           GetCurrentProcess(),     // Handle of process that thread 
                                                // pseudohandle is relative to 
           GetCurrentThread(),    // Parent thread's pseudohandle 
           GetCurrentProcess(),  // Handle of process that the new, real, 
                                                // thread handle is relative to 
            &hThreadParent,        // Will receive the new, real, handle 
                                             // identifying the parent thread 
            0, // Ignored due to DUPLICATE_SAME_ACCESS 
            FALSE, // New thread handle is not inheritable 
            DUPLICATE_SAME_ACCESS); // New thread handle has same 

分享到:
评论

相关推荐

    Windows核心编程.zip

    Windows核心编程.zip dll 进程 内存体系结构 栈\堆 内存映射文件 线程 字符和字符串处理

    《Windows核心编程系列》 — 线程基础

    一、线程  与前面介绍的进程一样,线程也有两部分组成:  1)一个线程内核对象,操作系统用它来管理线程。内核对象中还存储了线程的各种统计信息,包括挂起计数、退出代码等,以便于系统对线程的管理。内核...

    lindexi#lindexi#2020-04-21-Windows-核心编程笔记-Direct2D-比-GDI-快的一部分1

    但这不是说明 Direct2D 一定比 GDI 快,而是在应用程序消费在用户模式和内核模式切换的耗时这一部分是 Direct2D 更少对于一个用户线程来说,有一

    Windows核心编程培训 .pptx

    Windows核心编程培训相关知识,包括内核对象、进程、线程及线程同步、内存映射文件 、动态链接库等知识点。

    多线程资料基础资料

    MFC下的多线程编程.ppt Windows多线程编程.pdf WINDOWS核心编程.chm 多线程编程简明教程(附代码和文档) 这些是windows下多线程编程的基础资料,可以说是入门必备资料!相当的不错!可以应付一般的多线程问题,至于...

    C#多线程编程实战 源代码

    本书是一本通俗易懂的C#多线程编程指南,通过70多个容易理解的示例,循序渐进地讲解C#5.0中的异步及并发编程,引导读者了解Windows下C#多线程编程的多样性。 通过阅读本书,你将学到: 使用原始线程、异步线程,...

    windows高级编程指南(中文版+25个c源码案例)

    之后,他又推出了经典著作《Windows 高级编程指南》和《Windows核心编程》。如今这两本书早已成为Windows程序设计领域的颠峰之作,培育了几代软件开发设计人员。他的每一本新作问世,我们都有理由相信这是一本巨著,...

    window 核心编程part1

    之后,他又推出了经典著作《Windows 高级编程指南》和《Windows核心编程》。如今这两本书早已成为Windows程序设计领域的颠峰之作,培育了几代软件开发设计人员。他的每一本新作问世,我们都有理由相信这是一本巨著,...

    寒江独钓-Windows内核安全编程(高清完整版).part6

    全面系统地介绍了串口、键盘、磁盘、文件系统、网络等相关的Windows内核模块的编程技术,以及基于这些技术实现的密码保护、防毒引擎、文件加密、网络嗅探、网络防火墙等信息安全软件的核心组件的具体编程。...

    精通Windows.API-函数、接口、编程实例.pdf

    2.3 Windows API核心DLL 21 2.3.1 Kernel32.dll 21 2.3.2 User32.dll 21 2.3.3 Gdi32.dll 22 2.3.4 标准C函数 22 2.3.5 其他Dll 22 2.4 Unicode和多字节 22 2.4.1 W版本和A版本的API 24 2.4.2 ...

    寒江独钓-Windows内核安全编程(高清完整版).part7

    全面系统地介绍了串口、键盘、磁盘、文件系统、网络等相关的Windows内核模块的编程技术,以及基于这些技术实现的密码保护、防毒引擎、文件加密、网络嗅探、网络防火墙等信息安全软件的核心组件的具体编程。...

    Windows应用程序捆绑核心编程光盘代码

    4.4.2 流式套接字编程流程 91 4.4.3 套接字调用基本函数 92 4.4.4 Winsock程序设计 95 4.4.5 一个通用套接字类 96 4.4.6 套接字服务器端与客户端间通信的实例 101 4.5 本章小结 106 参考文献 106 第5章 使用...

    《Windows核心编程系列》谈谈用户模式下的线程同步

    系统中的线程必须访问系统资源,如堆、...  线程同步包括许多方面,windows提供了许多基础设施使线程同步变得容易。  用户模式下的线程同步:方法一,原子访问  线程同步的一大部分与原子访问有关。所谓原子访

    寒江独钓-Windows内核安全编程(高清完整版).part4

    全面系统地介绍了串口、键盘、磁盘、文件系统、网络等相关的Windows内核模块的编程技术,以及基于这些技术实现的密码保护、防毒引擎、文件加密、网络嗅探、网络防火墙等信息安全软件的核心组件的具体编程。...

    寒江独钓-Windows内核安全编程(高清完整版).part5

    全面系统地介绍了串口、键盘、磁盘、文件系统、网络等相关的Windows内核模块的编程技术,以及基于这些技术实现的密码保护、防毒引擎、文件加密、网络嗅探、网络防火墙等信息安全软件的核心组件的具体编程。...

    寒江独钓-Windows内核安全编程(高清完整版).part1

    全面系统地介绍了串口、键盘、磁盘、文件系统、网络等相关的Windows内核模块的编程技术,以及基于这些技术实现的密码保护、防毒引擎、文件加密、网络嗅探、网络防火墙等信息安全软件的核心组件的具体编程。...

    windows 核心指南-2

    作者从基本概念入手,全面系统地介绍了Windows的各种基本构件,如进程、线程、DLL和内存管理等,并列举了大量应用程序,精辟地分析了构件的使用方法,为掌握Windows 编程技巧提供了一条有效的捷径。对于不同水平的...

    C#多线程编程实战

    为了创建这样的应用程序,你需要学习...本书的目的是给你提供C#中多线程以及并行编程的详尽指导。我们将从基本概念开始,每章主题比前一章都有所拔高,最后展示了现实世界中的并行编程模式以及Windows商店应用示例。

    游戏编程--大师技巧

    第一部分 Windows编程基础  第一章 无尽之旅  历史一瞥  设计游戏  游戏类型  集思广益  设计文档和情节图板  使游戏具有趣味性  游戏的构成  常规游戏编程指导  使用工具  从准备到完成一使用编译器  ...

    Windows系统编程 原书第4版pdf

    该书主要是利用windows API进行应用程序开发,有文件系统、进程和线程管理、进程间通信、网络编程以及同步等核心系统服务。

Global site tag (gtag.js) - Google Analytics