windows下进程与线程剖析
Posted Curo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了windows下进程与线程剖析相关的知识,希望对你有一定的参考价值。
进程与线程的解析
进程:一个正在运行的程序的实例,由两部分组成:
1.一个内核对象,操作系统用它来管理进程。内核对象也是系统保存进程统计信息的地方。
2.一个地址空间,其中包含所有可执行文件或DLL模块的代码和数据。此外,它还包含动态内存分配,比如线程堆栈和堆的分配。
进程要做任何事情,都必须让一个线程在它的上下文中运行。该线程负责执行进程地址空间包含的代码。事实上,一个进程可以有多个线程,所有线程都在进程的地 址空间中“同时”执行代码。为此,每个线程都有它自己的一组CPU寄存器和它自己的堆栈。每个进程至少要有一个线程来执行进程地址空间包含的代码。当系统 创建一个进程的时候,会自动为进程创建第一个线程,这称为主线程。然后,这个线程再创建更多的线程,后者再创建更多的线程。。。如果没有线程要执行进程地 址空间包含的代码,进程就失去了继续存在的理由。这时,系统会自动销毁进程及其地址空间。
线程也有两个部分组成:
一个是线程的内核对象,操作系统用它管理线程。系统还用内核对象来存放线程统计信息的地方。
一个线程栈,线程栈默认大小为1M,线程内申请的资源都放在线程栈中,用于维护线程执行时所需的所有函数参数和局部变量
内核对象又包括:1.计数器(起始值为2,线程退出减一,句柄关闭减一,该计数器为0内核对象才会消失)
2.挂起计数器(初始值为0,计数器为0的时候该线程开始运行,每挂起一个进程,该计数器加一,每恢复一个进程,该计数器减一,且它的值只可以是非负整数)
3.信号
进程从来不执行任何东西,它只是一个线程的容器。线程必然是在某个进程的上下文中创建的,而且会在这个进程内部“终其一生”。这意味着线程要在其进程的地址 空间内执行代码和处理数据。所以,假如一个进程上下文中有两个以上的线程运行,这些线程将共享同一个地址空间。这些线程可以执行同样的代码,可以处理相同 的数据。此外,这些线程还共享内核对象句柄,因为句柄表是针对每一个进程的,而不是针对每一个线程。
对于所有要运行的线程,操作系统会轮流为每个线程调度一些CPU时间。它会采取循环(轮询或轮流)方式,为每个线程都分配时间片(称为“量程”),从而营造出所有线程都在“并发”运行的假象。
每个线程都有一个上下文,后者保存在线程的内核对象中。这个上下文反映了线程上一次执行时CPU寄存器的状态。大约每隔20ms,Windows都会查看 所有当前存在的线程内核对象。在这些对象中,只有一些被认为是可调度的。Windows在可调度的线程内核对象中选择一个,并将上次保存在线程上下文中的 值载入CPU寄存器。这一操作被称为上下文切换。线程执行代码,并在进程的地址空间中操作数据。又过了大约20ms,Windows将CPU寄存器存回线 程的上下文,线程不再运行。系统再次检查剩下的可调度线程内核对象,选择另一个线程的内核对象,将该线程的上下文载入CPU寄存器,然后继续。载入线程上 下文、让线程运行、保存上下文并重复的操作在系统启动的时候就开始,然后这样的操作会不断重复,直至系统关闭。
创建进程是用来占空间的,真正干活的是线程
线程的创建:
用MFC写一个简单的小例子来介绍一下工作者线程的创建:
首先我们先让进度条跑一下
1 while(1)
2 {
3 m_process.StepIt();
4 Sleep(100);//为了让进度条更明显,可以睡一会儿,作用是让出时间片
5 //因为cpu是轮换时间片的,代表cpu到该线程时它放弃本次时间片,让cpu先给别人分配任务
6 //注意windows中的sleep的单位是毫秒,而linux中的是秒
7 }
我们会发现在进度条跑的时候窗口是不可以移动的,因为现在进程里干活的只有这一个线程,它在一段时间内只能干一件事,为了让跑进度条的同时也可以移动窗口,这就需要我们再创建一根线程
接下来我们用CreateThread来创建线程,参数如下
1 _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,//安全属性
2 _In_ SIZE_T dwStackSize,//栈大小,若设置为0 ,新线程的大小默认为1M,
3 _In_ LPTHREAD_START_ROUTINE lpStartAddress,//线程函数,该指针代表线程函数的起始地址
4 _In_opt_ LPVOID lpParameter,//线程函数参数
5 _In_ DWORD dwCreationFlags,//创建线程的标志,0位创建后线程就跑起来,CREATE_SUSPENDED为创建后状态为挂起状态
6 _Out_opt_ LPDWORD lpThreadId
线程函数如下:
1 DWORD WINAPI ThreadProc(
2 _In_ LPVOID lpParameter//LPVOID代表void *
3 );
WINAP代表的是调用约定,转到定义为
1 #define WINAPI __stdcall//上面的这个是c++的默认的调用约定,参数调用从右向左,函数本身去清理空间
2 #define WINAPIV __cdecl//下面的是c的默认调用约定,参数调用从右向左,调用者清理空间
整体代码实现为:
1 DWORD WINAPI ThreadProc(LPVOID lpParameter)//当前函数时全局函数,没有this指针,类成员m_process无法直接使用,所以江this指针作为参数传进来
2 {
3 CtestThreadDlg *pthis=(CtestThreadDlg*)lpParameter;//传进来的this指针是作为void *类型传进来的,此处要强转
4 while(1)
5 {
6 pthis->m_process.StepIt();
7 Sleep(100);
8 }
return 0;
9 }
10 void CtestThreadDlg::OnBnClickedButton1()
11 {
12 HANDLE h_thread= CreateThread( NULL,//安全属性
13 0,//栈大小为1M
14 &ThreadProc,
15 this,
16 0,//创建起来就跑
17 NULL//线程id
18 );
19 }
那么如果上面的线程函数中的第五个参数我们设置为CREATE_SUSPENDED让它的初始态是挂起的呢,为了解除挂起状态,我们可以用到函数ResumeThread(h_thread);
ResumeThread(h_thread);//解除挂起状态
既然有恢复函数,当然也有挂起函数
SuspendThread(h_thread);//让线程变成挂起状态
那么下面的这个小例子我们来判断一下线程是否可以运行?
1 SuspendThread(h_thread);
2 SuspendThread(h_thread);
3 ResumeThread(h_thread);
答案是不能,我们在前面已经说了在线程里面有一个挂起计数器,当挂起一次,计数器加一,恢复一次计数器减一,当挂起计数器为0的时候线程开始运行。要保证挂几次就恢复几次。
那么看看下面的这个小例子呢,线程是否可以运行?答案是不能,挂起计数器只能是非负数,即使你先恢复两次线程,挂起计数器仍然是0.
1 ResumeThread(h_thread);
2 ResumeThread(h_thread);
3 SuspendThread(h_thread);
4 SuspendThread(h_thread);
接下来我们要为我们的进度条加一些功能,加上暂停和停止的功能,暂停部分代码为:
11 void CtestThreadDlg::OnBnClickedButton1()
12 {
13 if(!h_thread)//如果没有线程,则创建线程,否则恢复线程
14 {
15 h_thread=CreateThread( NULL,//安全属性
16 0,//栈大小为1M
17 &ThreadProc,
18 this,
19 0,//创建起来就跑
20 NULL//线程id
21 );
22 if(NULL==h_thread)
23 {
24 MessageBox("failed!\\n");
25 }
26 }
27 ResumeThread(h_thread);
28 }
29 void CtestThreadDlg::OnBnClickedButton2()
30 {
31 // TODO: 在此添加控件通知处理程序代码
32 SuspendThread(h_thread);
33 }//这段代码其实有个小问题就是你要是按多次暂停的话,要按多次开始才可以继续运行线程,可以加一个判断让线程只挂起一次,我懒得写。。。。。
接下来我们要重点实现的是停止部分的功能。
当线程退出时,线程栈也不在了,但是内核对象不一定在不在,为什么说不一定呢,这个要看句柄是否已经关闭,若已经关闭,则内核对象不在,否则还在。
内核对象不在了,线程一定不在了。
停止的代码如下:
1 DWORD WINAPI ThreadProc(LPVOID lpParameter)
2 {
3 CtestThreadDlg *pthis=(CtestThreadDlg*)lpParameter;
4 while(pthis->m_flag)//设置一个标志,初始化为false,在线程创建时将其置为true,再次置为false,时候,线程退出
5 {
6 pthis->m_process.StepIt();
7 Sleep(100);
8 }
9 return 0;
10 }
11 void CtestThreadDlg::OnBnClickedButton1()
12 {
13 if(!h_thread)//如果没有线程,则创建线程,否则恢复线程
14 {
15 m_flag=true;
16 h_thread=CreateThread( NULL,//安全属性
17 0,//栈大小为1M
18 &ThreadProc,
19 this,
20 0,//创建起来就跑
21 NULL//线程id
22 );
23 if(NULL==h_thread)
24 MessageBox("failed\\n");
25 }
26 ResumeThread(h_thread);
27 }
35 void CtestThreadDlg::OnBnClickedButton3()
36 {
37 //1.正常退出
38 m_flag=false;//这个情况是适用于正常情况下的退出,在这种情况下若是按暂停后再按下停止无法正常杀死进程(异常退出),于是要采取强制退出
39 //2.能正常退出的正常退出,实在不行再强制杀死
40 if(WaitForSingleObject(h_thread,100)==WAIT_TIMEOUT)//代表没收到线程退出的信号
41 { //WAIT_OBJECT_0代表收到了信号
42 TerminateThread(h_thread,-1);//可以杀死任何线程
43
44 }
45 m_process.SetPos(0);//让停止后的进度条归零
46 if(h_thread)
47 {
48 CloseHandle(h_thread);
49 h_thread=NULL;
50 }
51
52 }
线程可以通过以下4种方法来终止运行。
1.线程函数返回(这是强烈推荐的)。
始终都应该将线程设计成这样的形式,即当想要线程终止运行时,它们就能够返回。这是
确保所有线程资源被正确地清除的唯一办法。
如果线程能够返回,就可以确保下列事项的实现:
a) 在线程函数中创建的所有C + +对象均将通过它们的撤消函数正确地撤消。
b)操作系统将正确地释放线程堆栈使用的内存。
c)系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
d)系统将递减线程内核对象的使用计数。
2.TerminateThread(h_thread,-1);//可以杀死任何线程(避免使用)
注意TerminateThread 函数是异步运行的函数,也就是说,它告诉系统你想要线程终止运行,但是,当函数返回时,不能保证线程被撤消。
如果需要确切地知道该线程已经终止运行,必须调用WaitForSingleObject或者类似的函数,传递线程的句柄。
设计良好的应用程序从来不使用这个函数,因为被终止运行的线程收不到它被撤消的通知。线程不能正确地清除,并且不能防止自己被撤消。
当线程终止运行时, DLL通常接收通知。如果使用Terminate Thread 强迫线程终止,DLL就不接收通知,这能阻止适当的清除
3.ExitThread(-1);//杀死调用它的线程(避免使用)
可以让线程调用ExitThread 函数,以便强制线程终止运行:
该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C++资源(如C++类对象)将不被撤消。由于这个原因,
最好从线程函数返回,而不是通过调用ExitThread 来返回。
4.包含线程的进程终止运行(避免使用)
由于整个进程已经被关闭,进程使用的所有资源肯定已被清除。这当然包括所有线程的堆栈。这两个函数会导致进程中的剩余线程被强制撤消,就像从每个剩余的线程调用
TerminateThread 一样。显然,这意味着正确的应用程序清除没有发生,即C++对象撤消函数没有被调用,数据没有转至磁盘等等。
以上是关于windows下进程与线程剖析的主要内容,如果未能解决你的问题,请参考以下文章