现在的位置: 首页 > 综合 > 正文

《Windows核心编程》学习——线程基础

2019年03月10日 ⁄ 综合 ⁄ 共 2937字 ⁄ 字号 评论关闭

线程的组成:

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

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

 

 

线程的运行:

在解释线程的运行机制之前,首先回顾一下过去单线程程序的运行机制:

1.程序是一条指令接着一条指令顺序执行的,回忆下之前单片机课上学习的汇编语言。

2.CPU现在正在执行的指令地址被存储在IP寄存器里面,这条指令执行完之后,IP就会自动增加,这样CPU下次就会接着执行下一条指令了。这个可以通过DEBUG指令在命令行里查看一下,具体还是要回忆下之前《汇编语言》这本书里面学到的东西。

3.为了便于管理,在汇编代码里经常把程序分成几个"",包括"代码段""栈段""数据段"。这样把所有代码指令都放到同一个地方,临时变量之类的放到另外一个地方,管理起来容易不少,后来栈的设计被CPU广泛支持,有一个寄存器SP专门就用来存储栈顶指针。具体的还是要回忆下《汇编语言》里学的东西。

 

下面解释Windows下多线程程序的运行机制:

1.如果有两个CPU的话,运行两个线程,事情当然好办许多,两个CPU各有自己的IPSP寄存器,代码段应该是共享的、只需要分别建立两个"栈段",两个CPU各自访问自己的临时数据即可,还是原来的指令顺序执行,互相并不干扰。

2.而对于一个CPU运行两个线程的情况,事情就比较麻烦,毕竟需要用一个CPU模拟出两个CPU的效果。想要达到这种效果,最直观的办法当然是有一个变量把每个线程当前执行的状况——也就是当前CPU的寄存器值存储下来。每次从当前线程切换到另一个线程的时候,都先把当前线程的寄存器状况存下来,然后把另一个线程之前已经存好的寄存器状况载入到CPU里去执行。为了两个线程之间互不干扰,它们也应该各自有一个"栈段",来存储自己执行过程中的临时变量什么的。

3.Windows系统里当前CPU寄存器的值以CONTEXT结构的形式被存储在线程内核对象里面。《Windows核心编程》一书中有CONTEXT结构定义,可以看到其中确实有ESP,EIP寄存器。

4.线程内核对象之中还记录了挂起计数、使用计数等信息。Windows用这些信息来管理线程。

5.Windows中多线程程序的运行机制简述如下:

每隔大概20msWindows都会查看所有当前存在的线程内核对象,在这些对象中,只有一部分被认为是可调度的(可根据线程内核对象中记录的信息来判断)Windows根据某种算法在可调度的线程内核对象中选一个,并将上次保存在线程上下文(CONTEXT)中的值载入CPU寄存器。这一操作被称为上下文切换。线程上下文中的值载入CPU寄存器之后,也就把上一次线程的运行状态装载到了CPU里面,线程所执行的指令也就可以被运行了。

 

现在,应该可以理解为什么线程的组成只是线程内核对象和线程栈这么简单,也理解了线程之间切换时为什么线程中的指令能够接着上次没执行完的地方继续执行,另外,还理解了通过线程内核对象,Windows才能对线程进行有效的管理——内核对象里记录了线程那么多的信息,而线程栈仅仅是为线程执行提供临时空间而已。

 

 

线程的创建:

Windows中负责线程创建的APICreateThread,虽然_beginthreadex等也可以创建线程,但只不过是调用了CreateThread而已。下面介绍CreateThread是如何创建一个线程的:

1.一个线程内核对象被创建出来,需要注意的是它的挂起计数被设定为1,这个时候线程还没创建完毕,但是线程内核对象已经创建出来了,为了使系统轮询的时候线程不被执行到,挂起计数不能被设定为0。另外这个时候内核对象的CONTEXT结构里的值还没被设定。

2.线程栈被创建出来,其中被传进了两个变量,一个是线程函数的地址,一个是线程函数的参数。这两个变量在之后函数调用的时候会用到。

3.内核对象的CONTEXT结构里,IP寄存器的值被设定为RtlUserThreadStart函数的地址,而SP寄存器的值指向了之前创建好的线程栈,通过SP就可以找到之前传到栈里的两个变量了。

4.经过以上操作,线程就被初始化好了,接着启动便是,把挂起计数递减到0。系统轮询线程内核对象的时候,发现此线程可以执行了,就执行CONTEXT寄存器里存储的指令,在这里也就是执行RtlUserThreadStart函数,RtlUserThreadStart函数有两个参数,这两个参数就是由SP来指定的那两个变量——线程函数地址和线程函数参数。

5.RtlUserThreadStart会在内部调用线程函数,并把参数传到线程函数里,这样线程函数就被执行起来了。

6.当线程函数返回的时候,RtlUserThreadStart会为它调用ExitThread来结束线程。

 

 

线程的销毁:

销毁一个线程有多种方式,方式之间各有区别。下面逐一介绍:

1.线程函数退出:

这种方式是最正常和最正确的方式。因为是从函数里正常退出的,在线程执行过程中创建的临时对象都能够调用它们的析构函数而死去。退出之后会执行ExitThread函数来销毁线程所占有的系统资源——正如之前的介绍,线程函数其实是退出到了RtlUserThreadStart函数那里,有RtlUserThreadStart函数来负责调用ExitThread

2.调用ExitThread

这种方式不大好的地方在于线程执行过程中创建的临时对象没办法调用它们自己的析构函数,根据MSDN的说法,ExitThread将会直接销毁本线程所拥有的线程栈——当然也包括线程栈中存储的临时对象。在实际情况中我们经常在构造函数里new出一块内存来使用,析构的时候再delete它,如果对这种情况调用了ExitThread,析构函数无法被调用,new出来的那块内存也就没办法释放了,造成了内存泄漏。所以不推荐这种线程退出的方式。

ExitThread还会执行一些系统上的操作,比如内核对象使用计数递减,关闭线程中使用过的任何I/O端口,与使用过的DLL脱离关系等等。

3.调用TerminateThread

TerminateThread相对与ExitThread来说更不推荐使用,除非在极其了解要终结的线程代码如何构建的情况下,TerminateThread才有可能被安全地使用,它所造成的危害在MSDN上已经有比较清楚的说明。比如堆没法释放,关键代码段没机会关闭等等。

并且TerminateThread不会销毁线程的线程栈,但他会把线程内核对象的使用计数递减——这意味着线程内核对象有可能已经被销毁了,但是线程栈还存在着。Microsoft故意以这种方式来实现TerminateThread。否则,加入其他还在运行的线程要引用被杀死的那个线程的堆栈上的值,就会引起访问违规。让被杀死的线程的堆栈保留在内存中,其他的线程就可以正常运行。

4.包含线程的进程终止时:

进程终止的时候会杀死所有的线程,不知道具体杀死的方法是什么。

抱歉!评论已关闭.