Windows Internals 笔记——线程

1.进程有两个组成部分,一个进程内核对象和一个地址空间。线程也有两个组成部分:

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

2.线程要在其进程的地址空间内执行代码和处理数据,假如一个进程上下文中有两个以上的线程运行,这些线程将共享一个地址空间。这些线程可以执行同样的代码,可以处理相同的数据。此外,这些线程还共享内核对象句柄,因为句柄表是针对每一个进程的。

3.为一个进程创建一个虚拟的地址空间需要大量系统资源,系统会发生大量的记录活动,这需要用到大量内存,而且由于.exe和.dll文件要加载到一个地址空间,所有还需要用到文件资源。而线程只有一个内核对象和一个栈,几乎不涉及记录活动,所有不需要占用多少内存。

4.线程函数终止返回时,用于线程栈的内存也会被释放,线程内核对象的使用计数会递减,变为0时销毁。

5.系统从进程的地址空间中分配内存给线程栈使用,新线程在与负责创建的那个线程在相同的进程上下文中运行。因此,新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其他所有线程的栈。

6.调用CreateThread时,预定的地址空间容量设定了栈空间的上限,这样才能捕获代码中的无穷递归bug。否则系统会将进程的所有地址空间分配殆尽,并为线程栈调拨大量物理存储。

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

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

8.让线程函数返回,可以确保以下正确的应用程序清理工作都得以执行。

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

9.ExitThread函数将终止线程的运行,并导致操作系统清理该线程使用的所有操作系统资源。但是C/C++资源(如C++类对象)不会被销毁。

10.ExitThread是杀死主调线程,而TerminateThread能杀死任何线程。TerminateThread是异步的,返回时并不能保证线程以及被终止。而且使用这个函数时,除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈。

11.动态链接库DLL通常会在线程终止运行时收到通知,但是如果调用TerminateThread杀死线程,则DLL不会收到这个通知。

12.线程终止运行时,会发生下面这些事情

  • 线程拥有的所有用户对象句柄会被释放。在Windows中,大多数对象都是由包含了“创建这些对象的线程”的进程拥有。但一个线程有两个用户对象:窗口和挂钩。一个线程终止运行时,系统会自动销毁由线程创建或安装的任何窗口,并卸载由线程创建或安装的任何挂钩,其他对象只有在拥有线程的进程终止时才会被销毁。
  • 线程的退出码从STILL_ACTIVE变成传给iExitThread或TerminateThread的代码。
  • 线程内核对象的状态变为触发状态。
  • 如果线程时进程中的最后一个活动线程,系统认为进程也终止了。
  • 线程内核对象的使用计数递减1。

13.线程终止运行时,其关联的线程对象不会自动释放,除非对这个对象的所有未结束的引用都被关闭了。

14.一旦线程不再运行,系统中就没有别的线程再用该线程的句柄了,但是其他线程可以调用GetExitCodeThread来检查线程是否终止运行,以及其退出代码。

15.对CreateThread函数的一个调用导致系统创建了一个线程内核对象。该对象最初的使用计数为2.其他属性也被初始化。一旦创建了内核对象,系统就分配内存,供线程的堆栈使用。此内存是从进程的地址空间内存分配的,因为线程没有自己的地址空间。然后系统将pvParam和pfnStartAddr参数写入线程栈的第一个和第二个值。

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

17.指令寄存器和栈指针寄存器时线程上下文中最重要的两个寄存器。记住,线程始终在进程的上下文中运行,所以,这两个地址标识的内存都位于线程所在进程的地址空间中。当线程内核对象被初始化的时候,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
}

18.RtlUserThreadStart实际就是线程开始执行的地方。两个参数是由操作系统将值显示地写入线程堆栈,而不是从另一个函数调用的。新线程执行RtlUserThreadStart函数的时候,将会发生以下事情:

  • 围绕线程函数,会设置一个结构化异常处理帧。这样一来,线程执行期间所产生的任何异常都能得到系统的默认处理。
  • 系统调用线程函数,把传给CreateThread函数的pvParam参数传给它。
  • 线程函数返回时,RtlUserThreadStart调用ExitThread,将你的线程函数的返回值传给它。线程内核对象的使用计数递减,而后线程停止执行。
  • 如果线程产生一个未被处理的异常,RtlUserThreadStart函数所设置的SEH帧会处理这个异常。通常,这意味者系统会向用户显示一个消息框,而且当用户关闭此消息框时,RtlUserThreadStart会调用ExitProcess来终止整个进程,而不只是终止有问题的线程。

19. RtlUserThreadStart内,线程会调用ExitThread或者ExitProcess。这意味着线程永远不能退出此函数。

20.当RtlUserThreadStart调用你的线程函数时,它会将线程函数的返回地址压入堆栈,使线程函数知道在何处返回。但是,RtlUserThreadStart函数是不允许返回的。如果它没有在强行“杀死”线程的前提下尝试返回,几乎肯定会引起访问违规,因为线程堆栈上没有函数返回地址,RtlUserThreadStart将尝试返回某个随机的内存位置。

21.一个进程的主线程初始化时,其指令指针会被设为同一个未文档化的函数RtlUserThreadStart,当RtlUserThreadStart开始执行时,它会调用C/C++运行库的启动代码,后者初始化继而调用你的_tmain或_tWinMain函数。你的入口点函数返回时,C/C++运行时启动代码会调用ExitProcess。所以对于C/C++应用程序来说,主线程永远不会返回到RtlUserThreadStart函数。

22.VS附带了4个C/C++运行库用于本机代码的开发,还有两个库向,Microsoft.NET的托管环境。注意,所有这些库都支持对线程开发,不再有单独的一个C/C++库专门针对单线程开发。

23.由于标准C运行库是在1970年左右发明的,很久以后才在操作系统上出现线程的概念,所以多线程应用程序使用C运行库会有问题,如设置errno等全局变量。

24.创建新线程时,一定不要调用操作系统的CreateThread函数。相反,必须调用C/C++运行库函数_beginthreadex。

25.对于_beginthreadex函数,需要重点关注以下几点:

  • 每个线程都有自己的专用_tiddata内存块,它们是从C/C++运行库的堆上分配的。
  • 传给_beginthreadex的线程函数的地址保存在_tiddata内存块中。
  • _beginthreadex确实会在内部调用CreateThread,因为操作系统只知道用这种方式来创建一个新线程。
  • CreateThread函数被调用时,传给它的函数地址是_threadstartex(而非pfnStartAddr)。另外,参数地址是_tiddata结构的地址,而非pvParam。
  • 如果一切顺利,会返回线程的句柄,就像CreateThread那样。任何操作失败,会返回0。

26.关于threadstartex函数,以及其重点:

  • 新的线程首先执行RtlUserThreadStart,然后再跳转到_threadstartex。
  • threadstartex唯一的参数就是新线程的_tiddata内存块的地址。
  • TlsSetValue是一个操作系统函数,它将一个值与主调线程关联起来,这就是所谓的线程局部存储。threadstartex函数将_tiddata内存块与新建线程关联起来。
  • 在无参数的辅助函数_callthreadstartex中,有一个SEH帧,它将预期要执行的线程函数包围起来。这个帧处理着与运行库有关的许多事情,比如运行时错误(如抛出未被捕捉的C++异常),和C/C++运行库的signal函数。这一点相当重要,如果用CreateThread函数新建了一个线程,然后调用C/C++运行库的signal函数,那么signal函数不能正常工作。
  • 预期要执行的线程函数会被调用,并向其传递预期的参数。
  • 线程函数的返回值被认为时线程的退出代码。但是注意callthreadstartex不是简单的返回到_threadstartex,继而到RtlUserThreadStart,如果时那样的话,线程会终止运行,其退出代码也会被正确设置,但线程的_tiddata内存块不会被销毁。这会导致应用程序出现内存泄漏。为了防止这个问题,_threadstartex调用了_endthreadex,并向其传递退出代码。

27.对于_endthreadex函数,需要注意以下几点:

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

28.当一个线程调用一个需要_tiddata结构地C/C++运行库函数时:

  • 首先,C/C++运行库函数尝试取得线程数据块的地址(通过调用TlsGetValue)。
  • 如果NULL被作为_tiddata块的地址返回,表明主调线程没有与之关联的_tiddata块。在这个时候,C/C++运行库函数会为主调线程分配并初始化一个_tiddata块。
  • 然后,这个块会与线程关联(通过TlsSetValue),而且只要线程还在运行,这个块就会一直存在并与线程关联。
  • 现在C/C++运行库函数可以使用线程的_tiddata块,以后调用的任何C/C++运行库函数也都可以使用。

29.对于上述过程事实上,问题还是有的,假如线程使用了C/C++运行库的signal函数,则整个进程都会终止,因为结构化异常处理(SEH)帧没有就绪,从而导致内存泄漏。第二个问题是,假如线程不是通过调用_endthreadex来终止的,数据库就不能被销毁,从而导致内存泄漏。

30.当模块连接到C/C++运行库的DLL版本时,这个库会在线程终止时收到一个DLL_THREAD_DETACH通知,并会释放_tiddata块,可以防止_tiddata块的泄漏,但是还是尽量避免使用。

31._endthread函数是无参的,意味者线程的退出代码被硬编码为0,而且它在调用ExitThread前,会调用CloseHandle,向其传入新线程的句柄。

32.Windows提供了一些函数来方便线程引用它的进程内核对象或者它自己的线程内核对象:GetCurrentProcess() GetCurrentThread()。这两个函数都返回到主调线程的进程内核对象或线程内核对象的一个伪句柄。调用这两个函数,不会影响进程内核对象或线程内核对象的使用计数。调用CloseHandle会忽略此调用,并返回FALSE。注意,伪句柄是一个指向当前线程的句柄,即发出函数调用的那个线程。可以用DuplicateHandle转换成真正的句柄。

33.

原文地址:https://www.cnblogs.com/zoneofmine/p/8607777.html

时间: 2024-10-27 07:13:43

Windows Internals 笔记——线程的相关文章

Windows Internals 笔记——终止进程

1.进程可以通过以下四种方式终止: 主线程的入口点函数返回(强烈推荐的方式) 进程中的一个线程调用ExitProcess函数(避免这种方式) 另一个进程中的线程调用TerminateProcess函数(避免这种方式) 进程中的所有线程都“自然死亡”(这种情况几乎从来都不会发生) 2.应该保证只有在主线程的入口点函数返回之后,这个应用程序的进程才终止,只有这样才能保证主线程的所有资源都被正确清理.让主线程的入口点函数返回,可以保证以下操作会被执行: 该线程创建的任何C++对象都将由这些对象的析构函

深入解析Windows操作系统笔记——CH1概念和术语

1.概念和工具 本章主要介绍Windows操作系统的关键概念和术语 1.概念和工具... 1 1.1操作系统版本... 1 1.2基础概念和术语... 2 1.2.1Windows API2 1.2.2 服务.函数和例程... 3 1.2.3 进程.线程和作业... 4 1.2.3.1 进程... 4 1.2.3.2 线程... 4 1.2.3.3 虚拟地址描述符... 4 1.2.3.4 作业... 4 1.2.4 虚拟内存... 5 1.2.5 内核模式和用户模式... 5 1.2.6 终端

《coredump问题原理探究》Windows版 笔记

<coredump问题原理探究>Windows版 笔记 Debug 一.环境搭建 1.Win7捕获程序dump 2.Windbg符号表设置(Symbols Search Path) 二.WinDbg命令 三.函数栈帧 1.栈内存布局 2.栈溢出 3.栈的规律 4.定位栈溢出问题的经验方法 四.函数逆向 五.C内存布局 1.基本类型 2.数组类型 3.结构体 六.C++内存布局 1.类的内存布局 2.this指针 3.虚函数表及虚表指针 4.单继承 5.多继承(无公共基类) 七.STL容器内存布

多线程编程学习笔记——线程同步(三)

接上文 多线程编程学习笔记——线程同步(一) 接上文 多线程编程学习笔记——线程同步(二) 七.使用Barrier类 Barrier类用于组织多个线程及时在某个时刻会面,其提供一个回调函数,每次线程调用了SignalAndWait方法后该回调函数就会被执行. 1.代码如下: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; //

Windows Server 笔记(六):Active Directory域服务:额外域控制器

额外域控制器: 额外域控制器是指除了第一台安装的域控制器(主域控制器)意外的所有域控制器: 那么额外域控制器有什么好处呢? 1.可以提供容错.即一台DC出问题后,另一台仍可以可以继续工作,提供服务: 2.提高用户登录效率.多台域控可以分担用户审核,加快用户登录速度: 3.备份.域控制器之间会相互复制,就等于多了一份备份: 1.首先设置好IP配置:这里首选DNS指向自己,备用DNS指向主域:将服务器加入域: 2.选择"添加角色和功能": 3.选择"下一步": 4.选择

windows程序设计笔记

2014.05.06 新建一个visual C++ -- 常规 -- 空白 的项目,用.c后缀名指定这是一个用C语言来写的windows项目.和C语言的hellworld程序做了一个比较,按照windows程序设计规定的入口函数名称.函数参数.参数传递方式等写个入口函数,并弹出一个MessageBox. windows程序设计笔记,布布扣,bubuko.com

Windows内核之线程的调度,优先级,亲缘性

1 调度 Windows不是实时操作系统,它是抢占式多线程操作系统.在假设所有优先级相同的情况下,CPU对线程的调度原则是每隔20m就会切换到下一个线程,根据Context中的IP和SP来接着执行上次的东西.Windows永远不会让1个线程去独占一段时间. 2 可调度性 系统只调用可以调度的线程,其实系统的大部分线程都是处于不可调度的状态,要么处于暂停的状态,要么处于休眠的状态. 3 线程的暂停和恢复 <1>在CreateThread的时候通过制定CREATE_SUSPENDED来让线程暂停执

Windows Server 笔记(五):DHCP(1)

手动配置TCP/IP客户端除了是一个费时的苦差事,设置错误还会导致网络通信故障.DHCP就避免了这些错误,并提供了很多其他优势,包括计算机从一个子网移到另一个子网新地址的自动分配,不用时自动回收. DHCP配置的过程: 1.DHCP客户端以广播的方式向网络中的DHCP服务器发送出DHCP Discover数据包: 2.DHCP服务器在收到DHCP Discover数据包后,回应一个DHCP Offer数据包,并给予IP地址.TTL租期等相关参数: 3.DHCP客户端会选取第一个收到的IP地址(多

windows TLS (线程本地存储)

windows TLS (线程本地存储) 一.TLS简述和分类 我们知道在一个进程中,所有线程是共享同一个地址空间的.所以,如果一个变量是全局的或者是静态的,那么所有线程访问的是同一份,如果某一个线程对其进行了修改,也就会影响到其他所有的线程.不过我们可能并不希望这样,所以更多的推荐用基于堆栈的自动变量或函数参数来访问数据,因为基于堆栈的变量总是和特定的线程相联系的. 不过如果某些时候(比如可能是特定设计的dll),我们就是需要依赖全局变量或者静态变量,那有没有办法保证在多线程程序中能访问而不互