进程
- 进程是操作系统结构的基础;是一个正在执行的程序;计算机中正在运行的程序实例;可以分配给处理器执行的一个实体
- 一个进程包含一个运行中的程序所用到的所有资源:一个虚拟的内存空间。内存空间中有很多模块,被分为两大部分:用户层和内核层。普通程序只能访问用户层空间(00000000
7FFFFFFF),大部分对象(结构体变量)都是放在内核空间(80000000FFFFFFFF),故不能直接访问到,只能通过句柄。所有的进程内核层是共享的,不同的进程用户层空间不一样。在进程的虚拟内存中一般会加载一个 exe 和很多的 dll,这些都称之为模块,进程本身是一个综合了各种资源的东西,不能执行代码,能执行代码的是归属于进程的线程 - 进程是惰性的,它仅是一个包含有线程的容器,其本身没有执行代码的能力,因此每个进程在创建之初都会创建一个主线程用于执行代码,若此主线程结束,系统就会销毁这个进程内核对象
- 进程是程序在一个数据集合上运行的过程(一个程序可能同时属于多个进程),是操作系统进行资源分配和调度的一个独立单位,进程可分为系统进程和用户进程
- 在 Windows 下,进程又被细化为线程,即一个进程下有多个能独立运行的更小的单位。程序是静止的,而进程是动态的
进程的创建
1 | BOOL WINAPI CreateProcess(_In_opt_ LPCSTR lpApplicationName,//可执行文件名称 |
进程创建实例
1 | bool CreateChildProcess(LPTSTR lpPath, BOOL bWait) { |
线程
一个线程就是操作系统的一个内核对象,在 Windows 操作系统内核中没有进程的概念,只有线程的概念,进程只不过是在逻辑上对一组线程及其相关的资源进行的一种封装
线程是进程中某个单一顺序的控制流,也被称为轻量进程,指运行中的程序的调度单位,有自己的一块堆栈和执行环境
一个线程可以再次创建多个线程,由其创建的线程仍然可以分别创建多个线程
作为一个独立的执行单元来讲,线程占用的系统资源远不及进程,但其本质却未大打折扣
CPU 执行代码主要依靠一套寄存器:
通用寄存器:eax,ebx,ecx,edx,esi,esp,ebp
指令寄存器:eip 存储下一条要执行的指令
段寄存器:cs,ss,ds,es,fd,gs
所有线程都是操作系统统一管理的,每一个线程都有自己的优先级,根据优先级决定先后调用顺序,线程发生切换,实际就是切换线程的执行环境
退出线程的方式:
- 通过返回方式正常退出(最理想)
- 通过
ExitThread
强制结束本线程 - 通过
TernamiteThread
来结束指定线程 - 通过结束进程来结束所有线程
线程的创建
1 | HANDLE WINAPI CreateThread(_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, |
线程实例
1 | //线程函数 |
内核对象
Windows 系统中对象被保护起来,需要通过他们的句柄,调用相应的 API 函数去操作这些对象,所有的 API 函数都属于不同的动态链接库(DLL 文件)。根据对象的不同使用情况,可以分为 user对象,GDI对象和内核对象,其中user对象和窗口、控件等有关,GDI对象和绘图有关,内核对象和系统全局性的一些功能有关
常见的内核对象:
进程 线程 访问令牌 文件 文件映射 I/O 完成端口 邮槽 管道 互斥体 信号量 事件 计时器 线程池 所有内核对象都遵循统一的使用模式:
- 创建对象
- 打开对象,得到句柄(可与第一步合并,表示创建时就打开)
- 通过 API 访问对象
- 关闭句柄
- 句柄完全关闭,对象自动销毁
所有内核对象都属于操作系统内核,可在不同的进程间访问到,俗称:内核对象是跨进程的,通过引用计数和句柄表管理
每一个内核对象结构体都有一个字段叫做引用计数,当有一个进程创建或打开此内核对象,则引用计数自增1,进程终止,或关闭句柄,引用计数自减1,当引用计数为0,内核对象自动销毁
每个内核对象创建时都有一个安全属性,这个安全属性标识了怎么去使用这个对象,比如可读可写,权限等,一般创建一个内核对象时都需要指定一个安全属性
SECURITY_ATTRIBUTES
,若传NULL
的话,就会指定一个默认的属性内核对象的句柄和进程有关,对于同一个对象,在不同的进程中,其句柄值不同,这点和 GDI对象不同,GDI对象的句柄值是全局有效的。由此可见,不同类型的对象,其管理方式也是不同的
每个进程对象中都有一个句柄表,用于记录本进程所打开的所有内核对象,可以简单的将句柄表理解为一个一维的结构体数组,句柄值可看做句柄表中的索引,故而内核对象的句柄值仅仅对本进程有效。句柄表中的每一项描述了使用此句柄访问内核对象的权限,以及此句柄是否可以被子进程继承
获取内核对象句柄:
- 自己创建
- 自己打开(此对象是已创建好的)
- 从父进程继承过来
- 别的进程复制过来,
DuplicateHandle()
函数
基本操作
进程的基本操作:
创建:
CreateProcess
终止:ExitProcess
打开已存在的进程:OpenProcess
强制结束:
TerminateProcess
进程间的通讯:COPY_DATA
消息,邮槽线程的基本操作:
创建:
CreateThread
打开:OpenThread
挂起:SuspendThread
恢复:ResumeThread
终止:
ExitThread
强制结束:TerminateThread
遍历
方法很多,这里使用创建快照的方式:CreateToolHelp32Snapshot
- 进程快照:得到系统上所有的进程
- 模块快照:以进程为单位
需要知道:
进程是操作系统管理的,遍历时能够遍历出系统所有进程的信息(进程名,路径,进程 ID),通常都是知道进程名再去找 ID,ID 在每一次程序运行时都不一样,若要操作进程,就要使用
OpenProcess
函数得到其句柄,OpenProcess
函数的作用就是根据进程 ID 得到句柄的模块是属于某个进程的,故遍历模块时需指定遍历哪一个进程的模块,能够遍历出的模块信息为:模块名,模块的起始地址(加载基址)
遍历模块的用处:
- 可以知道一个程序都加载了哪些 DLL,监测 DLL 注入
- 分析 DLL 中的 PE 文件信息,可以为分析一个程序提供依据
线程虽属于一个进程,但其是操作系统统一管理的,故需遍历操作系统中的所有线程,然后自己过滤得到某个进程的线程,线程遍历得到的信息有:线程 ID,所属进程的 ID
遍历线程的用处:得到进程中每个线程的信息,操作这些线程,比如挂起,终止等
进程的遍历,使用 Release
1 |
|
模块的遍历
1 | HANDLE hModuleSnap = INVALID_HANDLE_VALUE; |
线程的遍历
1 |
|
文件的遍历
1 |
|
用树型控件实现文件目录的浏览
1 | void CDlgFile::ShowFile(CString str_Dir, HTREEITEM tree_Root) |
进程间的通讯
WM_COPYDATA
WM_COPYDATA
是一个特殊的、专门用于传递数据的消息,此消息可以携带一个大体积的消息参数,不同于其他只能携带两个固定参数的消息。发送 WM_COPYDATA
消息时,wParam
应保存发送此消息的窗口句柄,lParam
应指向一个名为 COPYDATASTRUCT
的结构体
1 | typedef struct tagCOPYDATASTRUCT { |
需注意:WM_COPYDATA
的数据会被发送到目标进程的栈空间保存,因此单次发送的数据量不宜过大
发送方:为一个窗口名为“WM_COPYDATA接收方”的窗口发送消息
1 |
|
接收方:首先创建一个对话框窗口,名为“WM_COPYDATA接收方”
1 |
|
邮槽
- 邮槽是 Windows 系统中最简单的一种进程间通信方式,一个进程可以创建一个邮槽,其他进程可以通过打开此邮槽与创建邮槽的进程通讯
- 邮槽的通讯是单向的,消息被写入邮槽后以队列的方式保存
- 邮槽除可在本机内进行进程间通讯外,还可在主机间通讯
- 创建邮槽对象:
CreateMailslot()
- 打开邮槽对象:
CreateFile()
- 读写邮槽:
ReadFile/WriteFile
- 获取邮槽信息:
GetMailslotInfo()
- 创建邮槽对象:
服务端代码:
1 |
|
客户端代码:
1 |
|
线程同步
若编写多线程程序,那么多个线程是并发执行,可以认为它们是在同时执行代码。但线程之间并非完全没有关系,很多时候会有以下两种关系:
- 线程 A 的继续执行,要以线程 B 完成某一操作之后为前提,这种需求称为同步
- 多个线程在争抢同一个资源,如全局变量,文件,数据结构,对象等,这种需求称为同步互斥
解决互斥问题
原子操作:适合去解决共享资源是全局变量的互斥问题,作用就是对于一个变量的基本算术运算保证是原子性的
函数 作用 备注 InterlockedIncrement 自增 InterlockedIncrement(&g_count) InterlockedDecrement 自减 InterlockedDecrement(&g_count); InterlockedExchangeAdd 加法/减法 InterlockedExchangeAdd(&g_count, 256L); InterlockedExchange 赋值 InterlockedExchange(&g_count, 256L); 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24long g_n = 0;
DWORD WINAPI ThreadPro1(LPVOID lparam) {
for (int i = 0; i < 100000; i++)
//g_n++;
InterlockedIncrement(&g_n);
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lparam) {
for (int i = 0; i < 100000; i++)
//g_n--;
InterlockedDecrement(&g_n);
return 0;
}
int main()
{
HANDLE hthread1, hthread2;
hthread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hthread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
WaitForSingleObject(hthread1, -1);
WaitForSingleObject(hthread2, -1);
printf("%d\n", g_n);
getchar();
return 0;
}
原子操作仅仅能解决某一个变量问题,只能使得一个整型数据做简单算数运算时是原子的,但大部分时候其实是希望保护一段代码,使得这一段代码是原子操作,而非是某一个变量的操作,使用临界区恰能解决这个问题
被保护的代码(代码访问了共享资源)放置在
函數 | 作用 |
---|---|
InitializeCriticalSection | 初始化 |
DeleteCriticalSection | 销毁 |
EnterCriticalSection | 进入临界区 |
LeaveCriticalSection | 离开临界区 |
1 |
|
异步 IO
将同步 IO 模式改为异步 IO 的模式
打开文件时加上重叠 IO 的标志:
FILE_FLAG_OVERLAPPED
普通的文件读取:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21int main()
{
//打开文件
HANDLE hFile = CreateFile(
L"异步IO.cpp",
GENERIC_READ,//读写方式,只读
FILE_SHARE_READ,//共享方式,共享读
NULL,//安全描述符
OPEN_EXISTING,//创建标志,存在时才打开
FILE_ATTRIBUTE_NORMAL,//文件属性和标志,正常属性
NULL
);
//获取一个文件的大小
DWORD dwSize = GetFileSize(hFile, NULL);
//读取文件
char *p = new char[dwSize];
DWORD dwRealSize = 0;
ReadFile(hFile, p, dwSize, &dwRealSize, NULL);
//关闭句柄
CloseHandle(hFile);
}异步 IO 方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//打开文件
HANDLE hFile = INVALID_HANDLE_VALUE;
hFile = CreateFile(
L"异步IO.cpp",
GENERIC_READ,//读写方式,只读
FILE_SHARE_READ,//共享方式,共享读
NULL,//安全描述符
OPEN_EXISTING,//创建标志,存在时才打开
//文件属性和标志,正常属性|重叠IO标志
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
//获取一个文件的大小
DWORD dwSize = GetFileSize(hFile, NULL);
//读取文件
char *p = new char[dwSize];
DWORD dwRealSize = 0;
//异步IO函数返回时IO没有结束,所以原来的一些参数无效,如读取或写入多少个字节,文件指针也没用,因为同一时刻可能会有多个IO在进行
OVERLAPPED al = {0};
ReadFile(hFile, p, dwSize, NULL, &al);//调用ReadFile函数后,IO并未结束,还可以继续做其他事
//.......
WaitForSingleObject(hFile, -1);
//关闭句柄
CloseHandle(hFile);以重叠方式打开的句柄拥有以下特性:
句柄变成了可等待的状态
- 有信号:当有一个 IO 任务被完成,就变成有信号
- 无信号:默认无信号
句柄不能使用文件读写位置
- 不能使用
SetFilePointer
来设置文件的读写位置 - 调用
ReadFile
或WriteFile
时,也不会使用文件读写位置来定义读写的文件内容 - 只能使用
OVERLAPPED
的结构体来指定文件的读写位置
- 不能使用
1
2
3
4
5
6
7
8
9
10
11
12typedef struct _OVERLAPPED {
ULONG_PTR Internal;// [输出]保存IO任务的错误码
ULONG_PTR InternalHigh;//[输出]保存IO任务的完成的字节数
union {
struct {
DWORD Offset;//[输入]用于指定文件读写位置的低32位
DWORD OffsetHigh;//[输入]用于指定文件读写位置的高32位
} DUMMYSTRUCTNAME;
PVOID Pointer;
} DUMMYUNIONNAME;
HANDLE hEvent;//[输入]用于提供任务完成通知事件对象
} OVERLAPPED, *LPOVERLAPPED;
- 投递 IO 任务
- 通过
ReadFile
来投递一个读取的 IO 任务 - 通过
WriteFile
来投递一个写入的 IO 任务
- 通过
获取 IO 完成通知
根据文件句柄的信号来判断
缺点:若一个文件同时存在多个 IO 任务,只要其中一个任务完成了,文件句柄就变成有信号状态,无法判断是哪个 IO 被完成了
使用事件对象信号状态来判断
- 每个 IO 任务都有一个重叠 IO 结构体来配置信息
- 每个 IO 任务都会配有一个事件对象,哪个事件对象被设置成有信号了,就说明哪个 IO 任务完成了
- 要等待事件对象的信号时,需要创建线程等待
缺点:若 IO 任务很多,就说明要等待的事件也很多,就需要创建大量线程等待,造成效率低下
1 |
|
使用扩展版的 API:
ReadFileEX
,WriteFileEx
- 扩展版的函数能接收一个完成函数的回调函数
- 当 IO 任务被系统底层处理完毕后,这个回调函数就会被系统插入到线程的 APC 队列中
- 当线程被挂起(睡眠)时,挂起前,根据可警醒状态是否为真,来决定调用 APC 队列中的函数
缺点:若在程序中没调用
Sleep
或其他等待函数,或调用了但没设置可警醒状态,那么即使 IO 任务完成了,回调函数也不会被调用
1 |
|
- 通过完成端口来等待 IO 任务
- 创建异步 IO 方式的文件对象
- 创建一个完成端口对象
- 将完成端口和文件对象进行绑定
- 创建指定个数线程,在线程监视完成端口和完成列表的状态,通过 API
GetQueuedCompletionStatus
来获取完成列表的状态 - 发起异步 IO 任务
1 |
|
事件对象
内核对象
通过
CreateEvent
来创建- 可配置手动使用或自动使用
- 可配置初始化是否有信号
- 通过
SetEvent
将事件设置为有信号 - 通过
ResetEvent
重置事件对象的信号(设置为无信号)
通过
WaitForSingleObject
来等待信号WaitForSingleObject
还会进一步修改等待事件对象的信号,从无信号开始阻塞等待,直到事件对象有信号后就从阻塞状态返回,返回前会顺手将事件对象设置为无信号