人, 既无虎狼之爪牙,亦无狮象之力量,却能擒狼缚虎,驯狮猎象,无他,唯智慧耳。
全部博文(167)
分类: WINDOWS
2012-03-22 15:46:51
线程基础
一、概述
由于编程中遇到的诸多问题,这周终于开始下决心学习进程线程编程部分。首先看的是这部分的基础知识,主要是线程的创建和终止以及线程优先级与调度。下面对现在自己所理解的东西简单做一个笔记,以资日后复习回顾。这篇笔记主要简单谈谈以下几个问题:
1. 线程内幕
l 线程的创建和终止
l 线程内核对象
2. 线程的调度与优先级
l 线程调度
l 查看修改上下文Context
l 线程优先级
二、线程内幕
虽说是“线程内幕”,也不是很深入的内容,只是相对于自己之前对线程的理解而言又深入了一步。
Ø 线程的创建和终止
尽管Windows所提供的C/C++运行库中也有_beginthreadex()这样的创建线程的函数,但是其实质也只是在调用Windows API的基础上又做了一些优化工作。在Windows下创建线程的实质还是调用CreateThread()函数来实现的。
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa, //线程的安全属性结构,NULL为使用默认值
DWORD cbStackSize, //线程堆栈的大小,0为默认大小
PTHREAD_START_ROUTINE pfnStartAddress, //线程函数的地址
PVOID pvParam, //线程函数的参数
DWORD dwCreateFlags, //创建线程时标志,0表示立即执行,Create_Suspend表示//挂起
PDWORD pdwThreadID //新线程的ID,NULL时表示不需要返回该值
);
函数执行成功会返回新线程句柄,这同CreateProcess()不同,它没有PROCESS_INFORMATION结构,不能在参数中返回新进程相关的进程句柄和线程句柄,因而需要作为函数的返回值返回。
终止一个线程一般有以下几种方式:
F 线程函数返回
这是我们最希望发生的情况,因为可以保证所有资源都被正确清理。比如:
1. 线程函数中创建的所有C++对象都通过其析构函数得以执行
2. 操作系统正确释放线程栈使用的内存
3. 操作系统把线程退出代码设为线程函数的返回值
4. 系统递减线程内核对象的使用计数
F 使用ExitThread()结束一个线程
VOID ExitThread(DWORD dwExitCode);
本函数可以终止一个线程,并且导致系统清理该线程使用的所有操作系统资源,如线程栈。但是不会销毁C/C++对象。退出代码可以在参数中进行设置。因此,本函数只能实现上述的2,3,4项。
F 使用TerminateThread()结束线程
可以利用本函数结束另一个线程。参数需要指定线程句柄和退出代码。这个函数要注意两点:
1. 该函数是一个异步函数,即便函数本身已经返回也不代表目标线程已经被成功结束
2. 该函数直接“杀死”了目标线程,目标线程提前没有任何“觉察”,因而所有的线程资源都没有得以释放。即只是实现了上述的3和4项。线程堆栈依旧可以使用,当然这是Microsoft为了使相关的其他线程可以继续使用该线程栈而故意为之的。
F 进程终止时,所有线程都会终止执行。随着进程清理,所有的线程资源也会被系统干净的彻底清理。
Ø 线程内核对象
系统创建一个进程时,会同时创建一个主线程;系统对进程和线程的管理都是通过相应的内核对象实现的。如同进程有进程内核对象和地址空间两个组成部分一样,创建线程时也会有两个工作:
1. 创建一个线程内核对象,操作系统用它来管理线程。系统还用内核对象来存放线程统计信息
2. 在进程地址空间中分配一个线程栈,用户维护线程执行所需的所有函数参数和局部变量
一个线程内核对象的结构大致如下:
线程内核对象主要由两个部分组成。上下文(Context)用来存储线程执行状态的一系列CPU寄存器的值,这里面最重要的是堆栈指针SP和指令指针IP,分别指向了线程执行的数据和指令的地址。统计信息部分中需要特别注意的是使用计数一开始就被初始化为了2,如果要完全释放该线程内核对象,除了线程函数正常退出之外,主调线程还需要CloseHandle()一次。
Suspend count计数初始化为1,创建线程时会检查dwCreateFlags参数,只有当参数是CREATE_SUSPEND时才会真正挂起此线程。
三、线程的调度与优先级
了解了线程内核对象的大致结构,我们现在来趁热打铁来了解下线程的调度问题。
Ø 线程的调度
Windows系统是一个抢占式多线程操作系统,什么意思呢?我的理解有以下几点:
1. 系统可以在任何时刻停止一个线程而另行调度另一个线程;
2. 调度的依据是各线程的优先级,0~31,依次从高到低优先调用。
我们知道,真正的执行流程是由线程实现的,因而,实质上不存在调度进程的问题,调度的实质对象都是各个线程。一般来说,如果各条件相同,CPU先运行一个线程大约20ms,之后将该线程的Context存入内核对象,从剩余的其他线程中选择一个新线程,载入其Context,再运行大约20ms,之后再次重复轮询线程。这里注意的是各个线程上下文的来回切换。
当然,系统每次只会调度可以调度的线程,有些线程本身是不可调度的。比如被挂起的线程,比如等待某种事件发生的线程。很简单的例子,打开一个记事本程序,不输入任何数据,也不拖动窗口,该线程就会陷入睡眠,等待点击触发。
一般来说,挂起一个线程有两种方法,一种是在CreateThread()时指定CREATE_SUSPEND参数,新线程一旦创建即被挂起;另一种是利用函数
DWORD SuspendThread(HANDLE hThread); //挂起线程
DWORD ResumeThread(HANDLE hThread); //恢复线程
两个函数都会返回调用之前线程的挂起计数,只有当计数为0,线程才会执行。多次挂起的线程需要多次恢复。
Windows没有直接提供挂起进程的函数,只是提供了调试器使用的方法来挂起进程。
Ø 查看修改上下文(Context)
Conetext结构在线程的调度切换中起着至关重要的作用,其实质是当时CPU一系列寄存器的值。Windows定义了CONTEXT结构体,主要包含:
CONTEXT_CONTROL:CPU控制寄存器
CONTEXT_INTEGER:CPU整数寄存器
CONTEXT_SEGMENTS:CPU段寄存器
CONTEXT_DEBUG_REGISTERS:CPU调式寄存器
各个部分分别由变量来存放寄存器中相应的数值。Windows同样提供了查看和修改CONETXT的函数
BOOL GetThreadContext(
HANDLE hThread,
PCONTEXT pContext );
BOOL SetThreadContext(
HANDLE hThread,
CONST CONTEXT *pContext );
当然,使用这些函数之前,我们需要先定义一个CONTEXT结构,具体使用例子如下:
//定义CONTEXT结构体
CONTEXT Context;
SuspendThread(hThread); //需要先挂起目标线程
//获取上下文中控制寄存器的值
Context.ContextFlags = CONTEXT_CONTROL; //指定CONETXT中包含的寄存器类型
GetThreadContext(hThread, &Context);
//修改寄存器的值
Context.Eip = 0x00010000;
SetThreadContext(hThread, &Context);
//继续线程
ResumeThread(hThread);
Ø 线程优先级
线程的优先级由进程优先级类和线程相对于进程的优先级决定,具体的对应关系在不同的版本中会有所变化。一般来说normal类的进程中的normal线程优先级值为8。系统调度时当然从可调度的线程中从高到低来选择线程。这里需要具体说明几个问题:
1. 优先级的值范围为0~31,但是应用程序最大值是16,至于17~21,27~30是只有内核模式下的设备驱动程序才可以获得的优先级。后面我们会看到360木马防火墙中就存在着优先级为16的最高级应用程序线程。
2. 虽然系统调度依据优先级来选择线程,但是如果优先级较低的线程长期不能得到执行,比如(2~4秒),系统就会认为该线程处于“饥饿”状态,就会临时提高该线程的优先级使得其可以执行一到两次。
3. 实际应用中,我们应当确保优先级高的程序能够快速执行,然后恢复到睡眠或者挂起状态,以保证最大的实时性;而另优先级低的程序在大多时候可以调度执行。
最后,看下360中的线程优先级,说不定会为如何免杀提供些思路