0%

Windows核心编程 - 线程

最近在学习 Win32编程,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习Win32开发的童鞋们提供一份参考。

本系列博文均根据学习《Windows核心编程》一书总结而来;

运行环境:

  • 操作系统: Windows 10家庭版
  • 编译器:Visual Studio 2019

线程

每个进程至少拥有一个线程,与进程内核对象一样,线程内核对象也拥有属性;

线程由两个部分组成:

  • 一个是线程的内核对象,操作系统用它对线程实施管理;
  • 另一个是线程堆栈,它用于维护线程在执行代码时所需要的所有函数参数和局部变量;

进程是不活泼的,进程从不执行任何执行,它只是线程的容器。线程总是在某个进程环境中创建的,而它的整个寿命期都在这个进程中。这意味着线程在它的进程地址中执行代码,并且在进程的地址空间中对数据进行操作;

线程只有一个内核对象和堆栈,所以线程的开销比进程少;

何时创建线程

线程用于描述进程中的运行路径。每当进程被初始化,系统就要创建一个主线程。

每个进程至少拥有一个线程;

第一个线程函数

每个线程必须拥有一个进入点函数,线程从这个进入点开始运行。

1
DWORD WINAPI ThreadProc(  LPVOID lpParameter   // thread data);

MSDN给我们提供的模板,你的线程函数可以执行你想要它做的任务任务。最终,线程函数到达它的结尾处并返回。这时,线程终止运行,该堆栈的内存被释放,同时,线程的内核对象的使用计数被递减。如果为0,线程则会被撤销;

需要注意的点:

  • 主线程的入口函数必须是main之类的函数,线程函数可以是任意名字;
  • 线程函数必须返回一个值,它将成为该线程的退出代码;
  • 线程函数应尽量使用函数参数和局部变量。当使用静态变量和全局变量时,多个线程可以访问,这可能会破坏变量内容;

CreateThread

1
2
3
4
5
6
HANDLE CreateThread(  LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
SIZE_T dwStackSize, // initial stack size
LPTHREAD_START_ROUTINE lpStartAddress, // thread function
LPVOID lpParameter, // thread argument
DWORD dwCreationFlags, // creation option
LPDWORD lpThreadId // thread identifier);

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

系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中所有其他线程的堆栈。

psa

psa指向LPSECURITY_ATTRIBUTES结构的指针。如果想要该线程内核对象的默认安全属性,通常传递NULL。如果我们希望子进程可以继承该线程对象的句柄,必须设置这个结构值中的bInheritHandle为TRUE。

1
2
3
4
5
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; / /结构体的大小,可用SIZEOF取得
LPVOID lpSecurityDescriptor; / /安全描述符
BOOL bInheritHandle ;/ /安全描述的对象能否被新创建的进程继承
} SECURITY_ATTRIBUTES,* PSECURITY_ATTRIBUTES;

dwStackSize

dwStackSize参数用于设定线程可以将多少地址空间用于自己的堆栈。每个线程都拥有自己的堆栈。

lpStartAddress和lpParameter

lpStartAddress参数指明想要新线程执行的线程函数的地址。lpParameter指明线程的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
DWORD WINAPI FirstThreadProc(LPVOID lpParameter);
DWORD WINAPI SecondThreadProc(LPVOID lpParameter);
DWORD WINAPI FirstThreadProc(LPVOID lpParameter)
{
static int x = 0; //如果不声明x为静态变量,线程2将在修改参数时访问错误,因为线程1已经被关闭了
DWORD dwThreadID;
HANDLE hThread1;
hThread1 = CreateThread(NULL, 0, SecondThreadProc, &x, 0, &dwThreadID);
CloseHandle(hThread1);
return 0;
}
DWORD WINAPI SecondThreadProc(LPVOID lpParameter)
{
*((int*)lpParameter) = 5;
printf("%d\n", *((int*)lpParameter));
return 0;
}

int main()
{
int nCount = 10;
HANDLE hThread1;
hThread1 = CreateThread(NULL, 0, FirstThreadProc, &nCount, 0, NULL);
CloseHandle(hThread1);
getchar();
return 0;
}

如果我们需要写自己的函数,并且不需要参数的时候,我们可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
DWORD WINAPI FirstThreadProc(LPVOID lpParameter);
DWORD WINAPI SecondThreadProc(LPVOID lpParameter);
DWORD WINAPI FirstThreadProc(LPVOID lpParameter)
{
static int x = 0; //如果不声明x为静态变量,线程2将在修改参数时访问错误,因为线程1已经被关闭了
DWORD dwThreadID;
HANDLE hThread1;
hThread1 = CreateThread(NULL, 0, SecondThreadProc, &x, 0, &dwThreadID);
CloseHandle(hThread1);
return 0;
}
DWORD WINAPI SecondThreadProc(LPVOID lpParameter)
{
*((int*)lpParameter) = 5;
printf("%d\n", *((int*)lpParameter));
return 0;
}

int WINAPI TestThreadProc(int x)
{
printf("TestThreadProc\n");
printf("x = %d\n", x);
return 0;
}

int main()
{
int nCount = 10;
HANDLE hThread1;
HANDLE hThread2;
hThread1 = CreateThread(NULL, 0, FirstThreadProc, &nCount, 0, NULL);
hThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)TestThreadProc(10), &nCount, 0, NULL); //自己的函数一定要进行类型强制转换
CloseHandle(hThread1);
getchar();
return 0;
}

dwCreationFlags

dwCreationFlags参数用于设定创建线程的其他标志,它可以是两个值中的一个,如果是0,线程创建后立刻调度。如果为CREATE_SUSPENDED,线程创建后则会挂起;

lpThreadId

CreateThread的最后一个参数是lpThreadId,它必须是一个DWORD的有效地址,CreateThread使用这个地址来存放系统分配给新线程的ID;

终止线程

若要终止线程的运行,可以使用以下方法:

  • 线程函数返回
  • 通过调用ExitProcess函数,线程将自行撤销
  • 同一个进程或另一个进程中的线程调用TerminateThread函数
  • 包含线程的进程终止运行

线程函数返回

ExitThread函数

可以让线程调用ExitThread函数,以强制终止线程运行;

1
VOID ExitThread( DWORD dwExitCode); 

TerminateThread函数

调用TerminateThread函数也可以终止线程的运行,但是与ExitThread函数不同,TerminateThread可以撤销任意线程;

线程性质

一旦内核对象创建完成,系统就分配用于线程的堆栈的内存。该内存是从进程的地址空间分配而来,因为线程并不拥有自己的地址空间。

每一个线程都有它自己一组CPU寄存器,称为线程的上下文,该上下文反映上次运行时该线程的CPU寄存器状态。线程的这组CPU寄存器保存在CONTEXT结构中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>
DWORD WINAPI ThreadProc1(LPVOID pR)
{
int* p = (int*)pR;
printf("%d\n", *p);
return 0;
}

DWORD WINAPI ThreadProc2(LPVOID pR)
{
int* p = (int*)pR;
printf("%d\n", *p);
return 0;
}


int main()
{
int x = 10;
int y = 20;
DWORD dwThreadID1;
DWORD dwThreadID2;
HANDLE hThread1;
HANDLE hThread2;
hThread1 = CreateThread(NULL, 0, ThreadProc1, &x, 0, &dwThreadID1);
hThread2 = CreateThread(NULL, 0, ThreadProc2, &y, 0, &dwThreadID2);
SuspendThread(hThread1); //挂起线程
CONTEXT context; //创建线程上下文的结构体
context.ContextFlags = CONTEXT_INTEGER; //设置线程上下文的标志
GetThreadContext(hThread1, &context); //获取线程上下文
printf("%x %x %x %x\n", context.Eax, context.Ecx, context.Ebx, context.Edx); //打印四个寄存器
context.Eax = 0x100; //修改EAX寄存器
printf("%x %x %x %x\n", context.Eax, context.Ecx, context.Ebx, context.Edx); //重新打印四个寄存器
ResumeThread(hThread1); //恢复线程执行
getchar();
CloseHandle(hThread1);
CloseHandle(hThread2);
return 0;
}

对线程ID的了解

当线程运行时,他们想要调用Windows函数来改变运行环境。想要引用自己的进程内核对象,活着引用自身线程的内核对象函数:

1
2
HANDLE GetCurrentProcess(void);
HANDLE GetCurrentThread(void);

上面这两个函数都能返回调用线程的进程的伪句柄或线程内核对象的伪句柄;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>
DWORD WINAPI ThreadProc1(LPVOID pR)
{
int* p = (int*)pR;
printf("%d\n", *p);
return 0;
}

DWORD WINAPI ThreadProc2(LPVOID pR)
{
int* p = (int*)pR;
printf("%d\n", *p);
return 0;
}


int main()
{


int x = 10;
int y = 20;
DWORD dwThreadID1;
DWORD dwThreadID2;
HANDLE hThread1;
HANDLE hThread2;
hThread1 = CreateThread(NULL, 0, ThreadProc1, &x, 0, &dwThreadID1);
hThread2 = CreateThread(NULL, 0, ThreadProc2, &y, 0, &dwThreadID2);
DWORD dProcessID = GetCurrentProcessId();
DWORD dThreadID = GetCurrentProcessId();
printf("%x\n", dProcessID);
printf("%x\n", dThreadID);
printf("%x\n", dwThreadID1);
getchar();
CloseHandle(hThread1);
CloseHandle(hThread2);
return 0;
}