• 《Windows核心编程》学习笔记(6)– 线程的创建、与进程的关系、伪句柄转换


    线程与进程的关系

    一般将进程定义成一个正在运行的程序的一个实例,它由以下两部分构成。

    一个内核对象,操作系统用它来管理进程。内核对象也是系统保存进程统计信息的地方。
    一个地址空间,其中包含所有可执行文件或DLL模块的代码和数据。此外,它还包含动态内存分配,比如线程堆栈和堆的分配。

      进程要做任何事情,都必须让一个线程在它的上下文中运行。该线程负责执行进程地址空间包含的代码。事实上,一个进程可以有多个线程,所有线程都在进程的地 址空间中“同时”执行代码。为此,每个线程都有它自己的一组CPU寄存器和它自己的堆栈。每个进程至少要有一个线程来执行进程地址空间包含的代码。当系统 创建一个进程的时候,会自动为进程创建第一个线程,这称为主线程。然后,这个线程再创建更多的线程,后者再创建更多的线程。。。如果没有线程要执行进程地 址空间包含的代码,进程就失去了继续存在的理由。这时,系统会自动销毁进程及其地址空间。


    线程也有两个组成部分:

    一个是线程的内核对象,操作系统用它管理线程。系统还用内核对象来存放线程统计信息的地方。
    一个线程栈,用于维护线程执行时所需的所有函数参数和局部变量。

    进程从来不执行任何东西,它只是一个线程的容器。线程必然是在某个进程的上下文中创建的,而且会在这个进程内部“终其一生”。这意味着线程要在其进程的地址 空间内执行代码和处理数据。所以,假如一个进程上下文中有两个以上的线程运行,这些线程将共享同一个地址空间。这些线程可以执行同样的代码,可以处理相同 的数据。此外,这些线程还共享内核对象句柄,因为句柄表是针对每一个进程的,而不是针对每一个线程。

    对于所有要运行的线程,操作系统会轮流为每个线程调度一些CPU时间。它会采取循环(轮询或轮流)方式,为每个线程都分配时间片(称为“量程”),从而营造出所有线程都在“并发”运行的假象。  

    每个线程都有一个上下文,后者保存在线程的内核对象中。这个上下文反映了线程上一次执行时CPU寄存器的状态。大约每隔20ms,Windows都会查看 所有当前存在的线程内核对象。在这些对象中,只有一些被认为是可调度的。Windows在可调度的线程内核对象中选择一个,并将上次保存在线程上下文中的 值载入CPU寄存器。这一操作被称为上下文切换。线程执行代码,并在进程的地址空间中操作数据。又过了大约20ms,Windows将CPU寄存器存回线 程的上下文,线程不再运行。系统再次检查剩下的可调度线程内核对象,选择另一个线程的内核对象,将该线程的上下文载入CPU寄存器,然后继续。载入线程上 下文、让线程运行、保存上下文并重复的操作在系统启动的时候就开始,然后这样的操作会不断重复,直至系统关闭。

    线程的创建

    CreateThread(

      LPSECURITY_ATTRIBUTES lpsa,

      DWORD cbStack,

      LPTHREAD_START_ROUTINE lpStartAddr,

      LPVOID lpvThreadParam,

      DWORD fdwCreate,

      LPDWORD );

    其中参数lpStartAddr 是指定希望新线程执行线程函数的地址。

    lpvThreadParam 参数是线程函数的参数。

    线程函数可以执行我们希望他执行的任何任务,函数原型类似于:

    DWORD WINAPI ThreadFunc(PVOID pvParam) {

    DWORD dwResult;

    return (dwResult);

    }


    调用CreateThread 时,系统会创建一个线程内核对象。这个线程内核对象不是线程本身,而是一个较小的数据结构,操作系统用这个结构来管理线程。

    系统将进程地址空间的内存分配给线程堆栈使用。新线程在与负责创建的那个线程相同的进程上下文中运行。因此,新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其他所有线程的堆栈。这样一来,同一个进程中的多个进程可以很容易地互相通信。 


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

    1.线程函数返回(这是强烈推荐的)。

    2.线程通过调用ExitThread函数“杀死”自己(要避免使用这种方法)。

    3.同一个进程或另一个进程中的线程调用TerminateThread函数(要避免使用这种方法)。

    4.包含线程的进程终止运行(这种方法避免使用)。

     

    PsTerminateThread函数是异步的。也就是说,它告诉系统你想终止线程,但在函数返回时,并不保证线程已经终止了。如果需要确定线程已终止运行,还需要调用WaitForSingleObject或类似的函数,并向其传递线程的句柄。


      线程的初始化

     《Windows核心编程》学习笔记(11)– 线程的创建 - fly - 天嗎荇箜


     1.CreateThread函数的一个调用导致系统创建一个线程内核对象。该对象最初的使用计数为2

           (除非线程终止,而且从CreateThread返回的句柄关闭, 否则线程内核对象不会被销毁。)

     

    2.暂停计数被设为1

    (因为线程的初始化需要时间,我们当然不希望在线程准备好之前就执行它。)

     

    3.退出代码被设为STILL_ACTIVE (0x103)

    (线程终止运行的时候,线程退出代码从STILL_ACTIVE (0x103)变成传给ExitThread TerminateThread 的代码);

     

    4.对象被设为nonsignaled(未触发)状态。 

     

    5.系统分配内存,供线程堆栈使用。然后系统将两个值写入新线程堆栈的最上端。写入线程堆栈的第一个值是传给 

    CreateThread函数的pvParam参数的值。紧接在它下方的是传给CreateThread函数的pfnStartAddr

      栈:windows中栈的大小是固定的,栈底在高地址,数据入栈从高地址开始放。

     

    6. 每个线程都有其自己的一组CPU寄存器,称为线程的上下文(context)。上下文反映了当线程上一 次执行时,线程的

    CPU寄存器的状态。线程的CPU寄存器全部保存在一个CONTEXT结构(在 WinNT.h头文件中定义)。CONTEXT结构

    本身保存在线程内核对象中。 

     

    7.指令指针栈指针寄存器是线程上下文中最重要的两个寄存器。当线程的内核对象被初始化的时候,CONTEXT结构的堆栈指针寄存器被设为pfnStartAddr(线程执行函数的地址在线程堆栈中的地址。而指令指针寄存器被设为RtlUserThreadStart函数(线程真正从这里开始执行)的 地址,此函数是NTDLL.dll模块导出的。

     

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

     

    伪句柄的转换

    HANDLE GetCurrentProcess(); 

    HANDLE GetCurrentThread(); 

     

    这两个函数都返回到主调线程的进程或线程内核对象的一个伪句柄(pseudohandle )。它们不会在主调进程的句柄表中新建句柄。而且,调用这两个函数,不会影响进程或线程内核对象的使用计数。如果调用CloseHandle,将一个伪句柄作为参数传入,CloseHandle只是简单地忽略此调 用,并返回FALSE。在这种情况下,GetLastError将返回ERROR_INVALID_HANDLE 

    将伪句柄转换为真正的句柄

        

    有时或许需要一个真正的线程句柄,而不是一个伪句柄。所谓真正的句柄,指的是能明确、无歧义地标识一个线程的句柄。来仔细分析下面的代码: 

     

    DWORD WINAPI ParentThread(PVOID pvParam) { 

      HANDLE hThreadParent = GetCurrentThread(); 

      CreateThread(NULL, 0, ChildThread, (PVOID) hThreadParent, 0, NULL); 

      // Function continues... 

    DWORD WINAPI ChildThread(PVOID pvParam) { 

      HANDLE hThreadParent = (HANDLE) pvParam; 

      FILETIME ftCreationTime, ftExitTime, ftKernelTime, ftUserTime; 

      GetThreadTimes(hThreadParent, 

      &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime); 

      // Function continues... 

     

      能看出这个代码段的问题吗?其意图是让父线程向子线程传递一个可以标识父线程的句柄。但是,父线程传递的是一个伪句柄,

    而不是一个真正的句柄。子线程开始执行时,它把这个伪句柄传给GetThreadTimes函数,这将导致子线程得到的是它自己的CPU

    计时数据,而不是父线程的。之所以会发生这种情况,是因为线程的伪句柄是一个指向当前线程的句柄;换言之, 

    指向的是发出函数调用的那个线程。 

     

         为了修正这段代码,必须将伪句柄转换为一个真正的句柄。DuplicateHandle函数可以执行这个转换: 

    BOOL DuplicateHandle( 

    HANDLE hSourceProcess, 

    HANDLE hSource, 

    HANDLE hTargetProcess, 

    PHANDLE phTarget, 

    DWORD dwDesiredAccess, 

    BOOL bInheritHandle, 

    DWORD dwOptions); 

     

    正常情况下,利用这个函数,你可以根据与进程A相关的一个内核对象句柄来创建一个新句柄,并让它同进程B相关。但是,我们可以采取一种特殊的方式来使用它,以纠正前面的那个代码段的错误。纠正过后的代码如下: 

     

    DWORD WINAPI ParentThread(PVOID pvParam) { 

    HANDLE hThreadParent; 

    DuplicateHandle( 

    GetCurrentProcess(), // Handle of process that thread pseudohandle is relative to 

    GetCurrentThread(), // 父伪句柄

    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 access as pseudohandle 

     

    CreateThread(NULL, 0, ChildThread, (PVOID) hThreadParent, 0, NULL); 

    // Function continues... 

     

    DWORD WINAPI ChildThread(PVOID pvParam) { 

    HANDLE hThreadParent = (HANDLE) pvParam; 

    FILETIME ftCreationTime, ftExitTime, ftKernelTime, ftUserTime; 

    GetThreadTimes(hThreadParent, &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime); 

    CloseHandle(hThreadParent); 

    // Function continues... 

     

    现在,当父线程执行时,它会把标识父线程的有歧义的伪句柄转换为一个新的、真正的句柄,后者明确、无歧义地标识了父线程。然后,它将这个真正的句柄传给CreateThread。当子线程开始执行时,其pvParam参数就会包含这个真正的线程句柄。在调用任何函数时,只要传入这个句 柄,影响的就将是父线程,而非子线程。 因为DuplicateHandle递增了指定内核对象的使用计数,所以在用完复制的对象句柄后,有必要 把目标句柄传给CloseHandle,以递减对象的使用计数。前面的代码体现了这一点。调用 GetThreadTimes之后,子线程紧接着调用CloseHandle来递减父线程对象的使用计数。在这段代 码中,我假设子线程不会用这个句柄调用其他任何函数。如果还要在调用其他函数时传入父线程的句柄,那么只有在子线程完全不需要此句柄的时候,才能调用CloseHandle 

     

    还要强调一点,DuplicateHandle函数同样可用于把进程的伪句柄转换为真正的进程句柄,如下所示: 

    HANDLE hProcess; 

    DuplicateHandle( 

    GetCurrentProcess(), // Handle of process that the process pseudohandle is relative to 

    GetCurrentProcess(), // Process' pseudohandle 

    GetCurrentProcess(), 

    &hProcess

    0

    FALSE,

    DUPLICATE_SAME_ACCESS

    )


  • 相关阅读:
    C++类中的函数重载
    C++中的友元
    bzoj 2820
    莫比乌斯函数
    bzoj 2440: [中山市选2011]完全平方数
    莫比乌斯反演1
    [转]C++ 指针和引用
    P2756 飞行员配对方案问题
    P2055 [ZJOI2009]假期的宿舍
    P2654 原核生物培养
  • 原文地址:https://www.cnblogs.com/forlina/p/2132271.html
Copyright © 2020-2023  润新知