启动一个线程
HANDLE hThread = CreateThread(
nullptr, // 默认安全属性
0, // 默认栈大小
ThreadProc, // 线程函数
pParam, // 传给线程的参数
0, // 创建选项(0 = 立即运行)
&dwThreadId // 返回线程ID
);
在使用 C 运行库(CRT,如 printf、malloc 等)的应用程序中,更推荐使用 _beginthreadex,因为它会正确初始化线程的 CRT 状态。
uintptr_t hThread = _beginthreadex(
nullptr, 0,
ThreadProc, pParam,
0, &dwThreadId
);
创建方式 | 接口 | 特点 & 适用场景 |
---|---|---|
Win32 API | CreateThread |
最底层、没有初始化 CRT |
CRT 封装 | _beginthread / _beginthreadex |
初始化 C 运行时(重要) |
C++ 标准库 | std::thread |
跨平台、类型安全,自动初始化CRT |
什么是CRT状态
在使用 Visual C++ 编写的程序中,CRT(C Runtime Library,C运行库) 会为每个线程维护一些线程本地的上下文信息,比如:
- 当前线程的 errno、_doserrno(线程本地错误号)
- locale 设置
- stdio 缓冲
- C++ 异常支持状态
- _beginthread 和 _beginthreadex 创建线程时的清理机制
- TLS(Thread Local Storage)索引和对应的内容
这些信息都保存在 CRT 自己分配的一段线程本地结构里,初始化过程由_beginthread/_beginthreadex 来处理。
CreateThread 启动是可以运行的,但如果在线程中使用 malloc/free、strtok、errno、C++ 异常等,就可能出现内存泄漏、崩溃或者行为异常。尤其是在线程退出时不会自动清理 TLS 和堆内存。
详见: https://www.cnblogs.com/liangzige/p/12915879.html
CreateThread()和_beginthreadex()在Jeffrey的《Windows核心编程》中讲的很清楚,应当尽量避免使用CreateThread()。 事实上,_beginthreadex()在内部先为线程创建一个线程特有的tiddata结构,然后调用CreateThread()。在某些非线程安全的CRT函数中会请求这个结构。如果直接使用CreateThread()的话,那些函数发现请求的tiddata为NULL,就会在现场为该线程创建该结构,此后调用EndThread()时会引起内存泄漏。_endthreadex()可以释放由CreateThread()创建的线程,实际上,在它的内部会先释放由_beginthreadex()创建的tiddata结构,然后调用EndThread()。 因此,应当使用_beginthreadex()和_endthreadex(),而避免使用CreateThread()和EndThread()。当然,_beginthread()和_endthread()也是应当避免使用的。
TEB
TEB 是线程级别的环境信息块,每个线程都有一份,位于用户模式内存空间。它包含了:
- TLS(Thread Local Storage)数据
- 当前堆栈信息
- 异常处理信息
- 线程 ID
- 对应的 PEB(Process Environment Block)指针
- Windows 异常机制结构(SEH)
- 语言区域设置等
// windbg: dt ntdll!_TEB typedef struct _TEB { NT_TIB NtTib; // 包括堆栈和 SEH 链 PVOID EnvironmentPointer; // 一般为 NULL CLIENT_ID ClientId; // 包含进程ID和线程ID PVOID ActiveRpcHandle; PVOID ThreadLocalStoragePointer; // TLS数据的指针 PPEB ProcessEnvironmentBlock; // 指向 PEB ULONG LastErrorValue; // 对应 GetLastError() ULONG CountOfOwnedCriticalSections; PVOID CsrClientThread; // CSR 线程信息(用于子系统通信) PVOID Win32ThreadInfo; ULONG User32Reserved[26]; ULONG UserReserved[5]; PVOID WOW32Reserved; // 用于 WOW64 LCID CurrentLocale; ULONG FpSoftwareStatusRegister; PVOID SystemReserved1[54]; LONG ExceptionCode; // 还有更多字段... } TEB, *PTEB;
windbg查看每个线程的TEB结构:
# ~ ; 打印所有线程
. 0 Id: c514.e4f8 Suspend: 1 Teb: 00000051`3d612000 Unfrozen
1 Id: c514.cba8 Suspend: 1 Teb: 00000051`3d614000 Unfrozen
2 Id: c514.c538 Suspend: 1 Teb: 00000051`3d616000 Unfrozen
3 Id: c514.dc58 Suspend: 1 Teb: 00000051`3d618000 Unfrozen
# !teb 513d612000 (用户模式下只能用小写)
TEB at 000000513d612000
ExceptionList: 0000000000000000
StackBase: 000000513d880000
StackLimit: 000000513d86f000
SubSystemTib: 0000000000000000
FiberData: 0000000000001e00
ArbitraryUserPointer: 0000000000000000
Self: 000000513d612000
EnvironmentPointer: 0000000000000000
ClientId: 000000000000c514 . 000000000000e4f8
RpcHandle: 0000000000000000
Tls Storage: 0000016c7ad88a80
PEB Address: 000000513d611000
LastErrorValue: 0
LastStatusValue: 0
Count Owned Locks: 0
HardErrorMode: 0
线程局部变量
API | 说明 |
---|---|
TlsAlloc() |
分配一个 TLS 索引(DWORD) |
TlsSetValue(index, lpVoid) |
设置当前线程对应索引的值 |
TlsGetValue(index) |
获取当前线程对应索引的值 |
TlsFree(index) |
释放索引 |
- 系统最多支持约 1088 个 TLS 索引(0~0xFFF)
- TLS 索引是全局的,但索引值与线程私有数据一一映射 TLS 数据存储在每个线程自己的 TEB(Thread Environment Block) 中。
机制 | 线程隔离 | 自动释放 | 可用于类成员 | 常见使用场景 |
---|---|---|---|---|
thread_local |
✅ 是 | ✅ 是 | ❌ 否 | 普通变量 |
__declspec(thread) |
✅ 是 | ❌ 否 | ❌ 否 | 静态链接 DLL / EXE |
TlsAlloc + TlsSetValue |
✅ 是 | ❌ 否 | ✅ 是 | 灵活控制资源 |
C++中的 thread_local:
thread_local std::string name = "thread name";
编译器会:
- 为变量分配 .tls 段中的偏移
- 为该线程在首次访问变量时生成构造代码
- 在线程退出时自动调用析构函数(仅 C++ 支持)
在动态加载的dll中使用TLS(LoadLibrary):
- __declspec(thread) / thread_local 变量 不会自动初始化;
- 如果在DLL中使用这种TLS变量,线程在调用它之前,必须显式调用 TlsAlloc 等 API 管理 TLS 数据,否则行为未定义,甚至 crash。
为什么会有这个限制?
TLS 的原理是操作系统在创建线程的时候,会查看该模块(EXE/DLL)的 TLS Directory(.tls 段中的结构),并为每个 TLS slot 分配内存并初始化。 但是这个机制仅在模块加载时由系统 PE 加载器(Loader)识别和初始化。
- 对于静态链接的 DLL,系统在主模块加载和线程创建时会一并处理。
- 对于 LoadLibrary 动态加载的 DLL,如果此时已有线程存在,这些线程不会重新初始化 TLS 相关结构,因为操作系统没法“回过头”给所有现存线程更新 TLS 块。
应尽量避免在动态库中使用thread_local关键字,改成使用windows api自己管理。
线程优先级
宏常量 | 数值 | 含义说明 |
---|---|---|
THREAD_PRIORITY_IDLE |
-15 | 几乎不运行 |
THREAD_PRIORITY_LOWEST |
-2 | 非常低 |
THREAD_PRIORITY_BELOW_NORMAL |
-1 | 比正常低一点 |
THREAD_PRIORITY_NORMAL |
0 | 默认值 |
THREAD_PRIORITY_ABOVE_NORMAL |
+1 | 比正常高一点 |
THREAD_PRIORITY_HIGHEST |
+2 | 非常高 |
THREAD_PRIORITY_TIME_CRITICAL |
+15 | 接近实时级别,慎用!⚠️ |
光设置线程优先级还不顶用,还得把进程的优先级也提高:
宏定义 | 调度优先级类 |
---|---|
IDLE_PRIORITY_CLASS |
最低 |
BELOW_NORMAL_PRIORITY_CLASS |
较低 |
NORMAL_PRIORITY_CLASS |
默认 |
ABOVE_NORMAL_PRIORITY_CLASS |
稍高 |
HIGH_PRIORITY_CLASS |
高 |
REALTIME_PRIORITY_CLASS |
实时 |
线程的实际优先级 = 基于类的基准 + SetThreadPriority 设置的偏移值
调度机制
- Windows 使用 基于优先级的抢占式调度,每个 CPU 都有自己的就绪线程队列。
- 高优线程会立即抢占低优线程。
- 时间片(量子)随优先级调整:高优线程时间片更长。
- Windows 会根据线程是否频繁 I/O,是否长期阻塞等特征,临时提升优先级。
- 比如 GUI 线程收到输入事件后,其优先级会被短时间提高以提升响应性。
- 优先级提升后,系统会逐步衰减回原始值(优先级衰减)。
线程上下文切换
- 保存当前线程的执行状态(CPU 寄存器、栈指针、程序计数器等)
- 加载另一个线程的执行状态
- 修改调度队列和线程状态
好复杂,驱动开发才需要详细了解吧?
线程栈
默认大小1MB, 可以修改。线程栈地址可以通过!teb打印:
0:000> !teb
...
Stack Base: 0019F000
Stack Limit: 0019A000
结束线程
TerminateThread
进程
创建进程
方法 | 描述 |
---|---|
CreateProcess |
最底层、功能最全的方式。 |
ShellExecute / ShellExecuteEx |
启动应用程序或打开文档(调用 CreateProcess )。 |
WinExec (不推荐) |
老式API,已被淘汰。 |
命令行调用(例如 system(), popen()) | 底层也调用 CreateProcess 。 |
我们重点讲解 CreateProcess 背后的原理。
CreateProcess 是创建子进程的最底层 API,它做了很多复杂的工作:
1️⃣ 解析可执行文件
- 解析 EXE 的 PE 头,找到入口地址(EntryPoint)和映像基址。
- 加载器将其映射到新的地址空间。
2️⃣ 创建新进程内核对象
- 分配并初始化内核对象 EPROCESS。
- 初始化其 PEB 和 TEB(主线程)。
- 创建新的页目录,为进程分配虚拟地址空间。
3️⃣ 创建主线程
- 创建一个新的线程对象 ETHREAD。
- 绑定到新进程。
- 设置初始栈,TEB 和栈空间。
- 初始化线程上下文(CS:EIP指向 EntryPoint)。
4️⃣ 复制父进程句柄表(如果指定了继承)
- bInheritHandles = TRUE 时复制句柄。
5️⃣ 通知调试器、附加 DLL
- 如果注册了调试器,会触发 CREATE_PROCESS_DEBUG_EVENT。
- 加载器调用所有模块的 DLLMain(DLL_PROCESS_ATTACH)。
6️⃣ 启动线程
- ResumeThread 让子进程主线程开始运行。
子进程不会自动继承父进程的上下文(线程、变量等),父子之间不共享堆栈或静态变量,但可以通过句柄、管道等IPC机制通信。
🎯 问题:CreateProcess 创建的进程什么时候才真正开始执行入口函数?
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = {};
CreateProcess(..., CREATE_SUSPENDED, ..., &si, &pi);
- 子进程的地址空间、PEB、TEB、主线程、堆栈都已经创建完毕;
- 但主线程 并未运行,它处于“挂起”状态;
- 必须手动调用 ResumeThread(pi.hThread),主线程才会开始运行,即跳转到 EntryPoint。即使你没有显式指定 CREATE_SUSPENDED,系统也会在内部在某一时刻控制线程“未立即运行”,然后在合适时机自动 resume。
点位 | 状态说明 |
---|---|
CreateProcess 返回前 |
子进程内核对象已创建,但线程不一定运行 |
ResumeThread 调用后 |
主线程开始运行,执行 EntryPoint |
所以入口函数执行点 | 是在 ResumeThread 调用之后 |