Windows核心编程:线程基础

作者:shenzi

链接:http://blog.csdn.net/shenzi

Windows核心编程:线程基础

2.线程
    2.1线程概念
     线程包括下面的必要组件:

  • CPU寄存器的内容,它表示寄存器的状态;
  • 两个栈:一个是以内核模式执行时被线程使用,另一个则是以用户模式执行时被线程使用;
  • 被称为“本机线程存储区”(TLS)的专用存储区,供子系统、运行时库和DLL使用;
  • 称为“线程ID”的唯一标示符(内部也称为“客户ID”,进程ID和线程ID产生于不同的名字空间,因此它们不会重叠);
  • 线程有时有自己的被多线程服务器应用程序使用的安全环境,多线程服务器应用程序模拟它们服务的客户的安全环境;   

     易失寄存器、栈和专用存储区被称为线程的环境,这些信息对于每个机器体系结构来说是不同的,这种结构取决于特定的体系结构。尽管线程有它们自己的执行环境,但进程中的每个线程都共享进程的虚拟地址空间和资源。
    2.2何时创建线程
    每次初始化进程时,系统都会创建一个主线程。对于用Microsoft C/C++编译器生成的应用程序,这个线程首先会执行C/C++运行库的启动代码,后者调用入口点函数(_tmain_tWinMain ),并继续执行,直至入口点函数返回C/C++运行库的启动代码,后者最终将调用ExitProcess
    每个线程都必须有一个入口点函数,这是线程执行的起点。主线程的入口点函数:_tmain或_tWinMain。如果想在进程中常见辅助线程,它必须有自己的入口点函数,形式如下:
DWORD WINAPI ThreadFunc(PVOID pvParam){
    DWORD dwResult = 0;
     ...
    return(dwResult);
}

    线程函数可以执行我们希望它执行的任何任务。最终线程函数将终止运行并返回。此时,线程将终止运行,用于线程栈的内存也会被释放,线程内核对象的使用计数也会递减。如果使用计数变为0,线程内核对象会被销毁。

  • 默认情况下,主线程的入口点函数必须命名为main,wmain,WinMain或wWinMain(除非用/ENTRY: 链接器选项来制定另一个函数作为入口点函数)。与此不同的是,线程函数可以任意命名。
  • 主线程入口点函数有字符串参数,所以它提供了ANSI/Unicode版本。相反,线程函数只有一个参数,而且意义由我们(而非操作系统)来定义。
  • 线程函数必须返回一个值,它会成为该线程的退出代码。C/C++的策略:令主线程的退出代码成为进程的退出代码。
  • 线程函数应尽可能使用函数参数和局部变量。使用静态变量和全局变量时,多个线程可以同时访问这些变量,这样可能会破坏变量中保存的内容。由于函数的参数和局部变量在线程栈上创建,一次不太可能被其它线程破坏。   

      2.3 CreateThread函数
      如果想创建一个或多个辅助线程,只需让一个正在运行的线程调用CreateThread
    HANDLE CreateThread(
        PSECURITY_ATTRIBUTES psa,//指向SECURITY_ATTRIBUTES结构的指针;
        DWORD cbStackSize,//制定线程可以为其线程栈使用多少地址空间;
        PTHREAD_START_ROUTINE pfnStartAddr,//线程函数的地址;
        PVOID pvParam,//线程函数参数
        DWORD dwCreateFlags,指定额外的标志来控制线程的创建;
        PDWORD pdwThreadID);//存储系统分配给新线程的ID;

     调用 CreateThread 时,系统会创建一个县城内核对象。系统从进程地址空间中分配内存给线程栈使用。新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其它所有线程的栈。
      CreateThread 函数时用于创建线程的Windows函数。不过如果写的是C/C++代码,就绝对不要调用 CreateThread。 正确地选择是使用Microsoft C++运行库函数_beginthreadex 。如果使用的不是Microsoft C++编译器,你的编译器的提供商应该提供类似的函数来替代 CreateThread 。不管这个替代函数时什么,都必须使用它。
    2.4终止运行线程
    线程可以通过以下4种方法来终止运行:

  • 线程函数返回(这是强烈推荐的);
  • 线程通过ExitThread 函数“杀死”自己(应避免);
  • 同一个进程或另一个进程中的线程调用TerminateThread 函数(应避免);
  • 包含线程的进程终止运行(应避免);

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

  • 线程函数中创建的所有C++对象都通过其析构函数被正确销毁;
  • 操作系统正确释放线程栈使用的内存;
  • 操作系统把线程的退出代码(在线程的内核对象中维护)设为线程函数的返回值;
  • 系统减少线程的内核对象的使用计数;

    2.4.2ExitThread函数
    VOID ExitThread(DWORD dwExitCode);
    该函数将终止线程的执行,并导致操作系统清理该线程使用的所有系统资源。但是你的C/C++资源(如C++类对象)不会被销毁。所以更好的做法是直接从线程函数返回,不要自己调用 ExitThread
    ExitThread 是Windows用于“杀死”线程的函数,如果要写C/C++代码,就绝对不要调用 ExitThread 。相反,应该使用C++运行库函数_endthreadex 如果使用的不是Microsoft的C++编译器,那么编译器供应商应该提供它们自己的 ExitThread 替代函数。 不管这个替代函数是什么,都必须使用它。
    2.4.3TerminateThread函数
     BOOL TerminateThread(
    HANDLE hThread,
    DWORD dwExitCode);

    TerminateThread 是异步函数 ExitThread 函数来终止线程,线程的对战会被销毁,而 TerminateThread ,除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈。
    2.5线程终止运行时
一个线程终止时,系统会一次执行以下操作:

  • 线程拥有的所有用户对象句柄会被释放;
  • 线程的退出代码从STILE_ACTIVE变为传给ExitThread或TerminateThread函数的代码;
  • 线程内核对象的状态变为已触发状态;
  • 如果线程是进程中的最后一个活动线程,系统认为进程也终止了;
  • 线程内核对象的使用计数递减1;

    2.6线程内幕
    系统如何创建和初始化一个线程:

图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函数。
    2.7C/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);
    }

  • 每个线程都有自己的专用_tiddata内存块,它们是从C/C++运行库的堆(heap)上分配的。
  • 传给_beginthreadex的线程函数的地址保存在_tiddata内存块中。
  • _beginthreadex确实会在内部调用CreateThread,因为操作系统只知道用这种方式来创建一个新线程。
  • CreateThread函数被调用时,传给它的函数地址是_threadstartex(而非pfnStartAddr)。另外,参数地址是_tiddata结构的地址,而非pvParam。
  • 如果一切顺利,会返回线程的句柄,就像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函数,要注意一下几大重点:

  • 新的线程首先执行RtlUserThreadStart(在NTDLL.dll文件中),然后再跳转到_threadstartex;
  • _threadstartex唯一的参数就是新线程的_tiddata内存块的地址;
  • TlsSetValue是一个操作系统函数,它将一个值与主调函数关联起来。这就是所谓的线程局部存储(Thread Local Storage,TLS)。_threadstartex函数将_tiddata内存块与新建线程关联起来;
  • 在无参数的辅助函数_callthreadstartex中,有一个SEH帧,它将预期要执行的线程函数包围起来。这个帧处理着与运行库有关的许多事情—比如运行时错误;
  • 预期要执行的线程函数会被调用,并向其传递预期的参数。函数的地址和参数会被保存在TLS的_tiddata数据块中,并会在_callthreadstartex中从TLS中获取;
  • 线程函数的返回值被认为是线程的退出代码;

    再来看看_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函数,要注意一下几点:

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

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