Windows程序设计
Posted chance0x1
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Windows程序设计相关的知识,希望对你有一定的参考价值。
1. Windows程序设计基础
1.1 代码风格
#include "stdafx.h"
#include <windows.h>
void Alert(int i);
int main(int argc, char* argv[])
{
Alert(2);
return 0;
}
void Alert(int i)
{
while(i > 0)
{
// Beep函数会使扬声器发出简单的声音,
// 要调用这个函数你应该包含上头文件“windows.h”
Beep(1000, 1000);
i--;
}
}
-
windows.h 头文件
-
链接到指定库文件,这一步VC帮我链接好了,对我们来说是透明的,如果要显示链接某个库,可以使用 #pragma comment(lib,"mylib.lib")
-
:: 为全局函数,帮助文档 F1
-
代码风格
-
名称简短
-
富有意义 g_ m_ bRet _nErrorCode g_szTitle MAXBUFFER
-
动作
-
代码对齐 {} 独立单元
-
注释,短注释在语句末尾,常注释在语句的上一行
-- Beep--
WINBASEAPI BOOL WINAPI Beep( DWORD dwFreq, DWORD dwDuration ); 说明 //用于生成简单的声音 返回值 //BOOL,TRUE 表示成功,否则返回零。会设置GetLastError 参数表 参数 类型及说明 //dwFreq Long,声音频率(从37Hz到32767Hz)。在windows95中忽略 //dwDuration Long,声音的持续时间,以毫秒为单位。如为-1
-
1.2 使用windowsAPI
创建工程 VS2015 创建 Win32 Console程序,带有预编译头文件,并且不是空工程
#include "stdafx.h"
#include <windows.h> // 包含MessageBox函数声明的头文件
int main(int argc, char* argv[])
{
// 调用API函数MessageBox
int nSelect = ::MessageBox(NULL, _T("Hello, Windows XP"), _T("Greetings"), MB_OKCANCEL);
if (nSelect == IDOK)
printf(" 用户选择了“确定”按钮
");
else
printf(" 用户选择了“取消”按钮
");
return 0;
}
-- MessageBox--
WINUSERAPI
int
WINAPI
MessageBoxA(
HWND hWnd ,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType);
WINUSERAPI
int
WINAPI
MessageBoxW(
HWND hWnd ,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType);
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif // !UNICODE
说明
//消息弹框
返回值
int
//IDOK用户按下了“确认”按钮
//IDCANCEL用户按下了“取消”按钮
//IDABORT用户按下了“中止”按钮
//IDRETRY用户按下了“重试”按钮
//IDIGNORE用户按下了“忽略”按钮
//IDYES用户按下了“是”按钮
//IDNO用户按下了“否”按钮
参数表
参数 类型及说明
hWnd
//1. 该消息框的父窗口句柄
//2. 如果此参数为NULL,则该消息框没有拥有父窗口
lpText
//消息框的内容
lpCaption
//消息框的标题
uType
//1. 指定一个决定对话框的内容和行为的位标志集
//2. 此参数可以通过指定下列标志或标志的组合,来显示消息框中的按钮以及图标
uType参数定义解析
按钮含义
MB_OK默认值,有一个“确认”按钮在里面
MB_YESNO有“是”和“否”两个按钮在里面
MB_ABORTRETRYIGNORE有“中止”,“重试”和“跳过”三个按钮在里面
MB_YESNOCANCEL有“是”,“否”和“取消”三个按钮在里面
MB_RETRYCANCEL有“重试”和“取消”两个按钮在里面
MB_OKCANCEL有“确定”和“取消”两个按钮在里面
图标含义
MB_ICONEXCLAMATION一个惊叹号出现在消息框:?
MB_ICONWARNING一个惊叹号出现在消息框(同上)
MB_ICONINFORMATION一个圆圈中小写字母i组成的图标出现在消息框:?
MB_ICONASTERISK一个圆圈中小写字母i组成的图标出现在消息框(同上)
MB_ICONQUESTION一个问题标记图标出现在消息框:?
MB_ICONSTOP一个停止消息图标出现在消息框:?
MB_ICONERROR一个停止消息图标出现在消息框(同上)
MB_ICONHAND一个停止消息图标出现在消息框(同上)
默认按钮含义
MB_DEFBUTTON1指定第一个按钮为默认按钮
MB_DEFBUTTON2指定第二个按钮为默认按钮
MB_DEFBUTTON3指定第三个按钮为默认按钮
MB_DEFBUTTON4指定第四个按钮为默认按钮
2. Win32程序运行原理
-
一,CPU的保护模式和windows操作系统
-
[x] windows 是多任务实现
-
[x] 虚拟内存和 各个进程的地址空间安排:2G系统空间,2G用户空间,2G用户空间是各个进程私有的,很少被其他应用打断,保证系统稳定
-
[x] 内核模式和用户模式:虽然用户模式下是独立4G空间,但是内核模式的系统和驱动共用一个2G空间;内核空间的数据只有在特定的权限下才能访问
-
[x] 当应用程序调用系统函数比如ReadFile,应用程序会从用户模式进入内核模式去执行;
因为访问系统内部数据,必须运行在内核模式下
-
-
二,内核对象
- [x] 内核对象是系统提供的用户模式下代码与内核模式下代码交互的基本接口
- [ ] 内核对象是内核分配的内存,只能在内核模式下运行,内核对象记录的数据在整个系统只有一份,他们也称为系统资源
- [ ] 内核对象和普通对象的最大区别,其数据结构是隐藏的,必须调用对象服务才能使用修改。
-
三,对象句柄
原则上,内核对象的句柄仅对当前进程有效;当然也有手段多进程共用一个内核对象
-
四,应用程序启动流程
应用程序的启动过程就是应用程序的创建过程
- 操作系统通过CreateProcess 创建新的进程,初始化引用计数,创建虚拟地址空间,加载应用程序运行时所需的代码和数据
- 创建主线程。主线程执行C/C++运行期启动代码开始运行,然后调用main函数
2.1 创建进程
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
// 如果使用 LPWSTR szCommandLine = _T("cmd");定义 错误:
//Exception thrown at 0x74507245 (KernelBase.dll) in 01FirstApp.exe: 0xC0000005: Access violation writing location 0x00B86B36.
TCHAR szCommandLine[] = TEXT("cmd.exe");
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
si.dwFlags = STARTF_USESHOWWINDOW; // 指定wShowWindow成员有效
si.wShowWindow = TRUE; // 此成员设为TRUE的话则显示新建进程的主窗口,
// 为FALSE的话则不显示
BOOL bRet = ::CreateProcess(
NULL, // 不在此指定可执行文件的文件名
szCommandLine, // 命令行参数
NULL, // 默认进程安全性
NULL, // 默认线程安全性
FALSE, // 指定当前进程内的句柄不可以被子进程继承
CREATE_NEW_CONSOLE, // 为新进程创建一个新的控制台窗口
NULL, // 使用本进程的环境变量
NULL, // 使用本进程的驱动器和目录
&si,
&pi);
if (bRet)
{
// 既然我们不使用两个句柄,最好是立刻将它们关闭,如果不关闭,在任务管理器会有一个cmd.exe进程一直在执行
// win10 环境中 cmd.exe 所在位置 ‘C:WindowsSysWOW64‘
::CloseHandle(pi.hThread);
::CloseHandle(pi.hProcess);
printf(" 新进程的进程ID号:%d
", pi.dwProcessId);
printf(" 新进程的主线程ID号:%d
", pi.dwThreadId);
}
return 0;
}
在这种情况下,可执行模块的名字必须处于 lpCommandLine 参数的最前面并由空格符与后面的字符分开,当CreateProcess解析lpCommandLine 字符串时,它会检查字符串中的第一个标记(token),并假记此标记为我们想运行的可执行文件的名称,如果可执行文件的名称没有扩展名,默认为.exe,并且如果文件名不包含一个完整的路径,CreateProcess还会按以下顺序来搜索可执行文件:
(1)主调进程.exe文件所在的目录
(2)主调进程的当前目录
(3)windows系统目录,即GetSystemDirectory返回的System32子文件夹
(4)windows目录
(5)PATH环境变量中列出的目录
-- CreateProcess--
WINBASEAPI
BOOL
WINAPI
CreateProcessW(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
#ifdef UNICODE
#define CreateProcess CreateProcessW
#else
#define CreateProcess CreateProcessA
#endif // !UNICODE
说明
//创建一个新进程(比如执行一个程序)
返回值
//Long,非零表示成功,零表示失败。会设置GetLastError
参数表
参数 类型及说明
lpApplicationName
//要执行的应用程序的名字。可设为vbNullString;在这种情况下,应用程序的名字应在lpCommandLine参数的起始处出现
lpCommandLine
//要执行的命令行。可用GetCommandLine函数取得一个进程使用的命令行。Windows会尽可能地根据下述搜索顺序来查找执行文件:
(1)包含了父进程执行文件的目录
(2)父进程当前的目录
(3)由GetSystemDirectory返回的系统目录
(4)仅适于windows NT:16位系统目录
(5)由GetWindowDirectory返回的Windows目录
(6)由PATH环境变量指定的目录
lpProcessAttributes
//指定一个SECURITY_ATTRIBUTES结构,或传递零值(将参数声明为ByVal As Long,并传递零值)——表示采用不允许继承的默认描述符。该参数定义了进程的安全特性
lpThreadAttributes
//指定一个SECURITY_ATTRIBUTES结构,或传递零值(将参数声明为ByVal As Long,并传递零值)——表示采用不允许继承的默认描述符。该参数定义了进程之主线程的安全特性
bInheritHandles
//表示允许当前进程中的所有句柄都由新建的子进程继承
dwCreationFlags
//来自API32.TXT文件的一个或多个下述常数之一,它们都带有前缀CREATE_。下面这些用于VB程序员:
//CREATE_SEPARATE_WOW_VDM(仅适用于NT) 启动一个16位的Windows应用程序时,强迫它在自己的内存空间运行
//CREATE_SHARED_WOW_VDM(仅适用于NT) 启动一个16位的Windows应用程序时,强迫它在共享的16位虚拟机(VM)内运行
//CREATE_SUSPENDED 立即挂起新进程。除非调用了ResumeThread函数函数,否则它不会恢复运行
也可能是下述常数之一,用于指定优先级
//IDLE_PRIORITY_CLASS 新进程应该有非常低的优先级——只有在系统空闲的时候才能运行。基本值是4
//HIGH_PRIORITY_CLASS 新进程有非常高的优先级,它优先于大多数应用程序。基本值是13。注意尽量避免采用这个优先级
//NORMAL_PRIORITY_CLASS 标准优先级。如进程位于前台,则基本值是9;如在后台,则优先值是7
//不要在VB中使用REALTIME_PRIORITY_CLASS
//lpEnvironment Any,指向一个环境块的指针(环境缓冲区的头一个字符,或者环境块的地址)
//lpCurrentDriectory String,新进程的当前目录路径。调用函数的时候,可用vbNullString指定当前目录
lpStartupInfo STARTUPINFO
//指定一个STARTUPINFO结构,其中包含了创建进程时使用的附加信息
lpProcessInformation
//该结构用于容纳新进程的进程和线程标识符。大多数情况下,一旦这个函数返回,父应用程序都会关闭两个句柄。
2.2 获取进程列表(跟任务管理器看到的一样)
获取系统内所有正在运行进程的信息
#include "stdafx.h"
#include <windows.h>
#include <tlhelp32.h> // 声明快照函数的头文件
int main(int argc, char* argv[])
{
PROCESSENTRY32 pe32;
// 在使用这个结构之前,先设置它的大小
pe32.dwSize = sizeof(pe32);
// 0 给系统内的所有进程拍一个快照
// 第二个参数传递 一个进程ID,获取某一个进程的快照
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE) //-1
{
printf(" CreateToolhelp32Snapshot调用失败!
");
return -1;
}
// 遍历进程快照,轮流显示每个进程的信息
BOOL bMore = ::Process32First(hProcessSnap, &pe32);
while (bMore)
{
printf(" 进程名称:%ws
", pe32.szExeFile); //宽字节输出,如果是%s,只会输出一个字符
printf(" 进程ID号:%u
", pe32.th32ProcessID);
bMore = ::Process32Next(hProcessSnap, &pe32);
}
// 不要忘记清除掉snapshot对象
::CloseHandle(hProcessSnap);
return 0;
}
输出
进程名称:atiesrxx.exe
进程ID号:1460
进程名称:dasHost.exe
进程ID号:1596
进程名称:atieclxx.exe
进程ID号:1632
进程名称:igfxCUIService.exe
进程ID号:1756
进程名称:svchost.exe
进程ID号:1916
进程名称:svchost.exe
进程ID号:2008
进程名称:svchost.exe
进程ID号:1812
进程名称:spoolsv.exe
可以枚举:
TH32CS_SNAPHEAPLIST:表示快照信息包含特定进程的堆栈列表
TH32CS_SNAPMODULE :表示快照信息包含特定进程的使用模块的列表
TH32CS_SNAPPROCESS:表示快照信息包含系统的所有进程的列表
TH32CS_SNAPTHREAD :表示快照信息包含系统所有线程的列表
TH32CS_SNAPPROCESS 进程ID可以为0.,
其他标志位的进程ID不能为0,枚举函数和返回值都应该相应的变化
标志位 | 枚举函数 | 返回值 | PID |
---|---|---|---|
TH32CS_SNAPPROCESS | Process32First | PROCESSENTRY32 | 0 |
TH32CS_SNAPTHREAD | Thead32ListFirst | THREADENTRY32 | 非0 |
TH32CS_SNAPMODULE | Module32First | MODULEENTRY32 | 非0 |
TH32CS_SNAPHEAPLIST | Heap32ListFirst | HEAPLIST32 | 非0 |
2.3 终止线程
终止当前进程
#include "stdafx.h"
#include <windows.h>
class CMyClass
{
public:
CMyClass() { printf(" Constructor
"); }
~CMyClass() { printf(" Destructor
"); }
};
void main()
{
CMyClass theObject;
::ExitThread(0);
// 在这个函数的结尾,编译器会自动添加一些必要的代码,
// 来调用theObject的析构函数
}
// 会打印输出 Destructor
终止其他进程
- ExitProcess只能终止当前进程,要终止其他进程,使用TerminateProcess
输入 notepad++的进程id,然后,运行该程序,nodepad++.exe进程会被关闭
如果你创建了一个新的文件new.file,但是文件并未保存,再次打开,还是会出现new.file.文件依然没有保存
#include "stdafx.h"
#include <windows.h>
BOOL TerminateProcessFromId(DWORD dwId)
{
BOOL bRet = FALSE;
// 打开目标进程,取得进程句柄
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwId);
if (hProcess != NULL)
{
// 终止进程
bRet = ::TerminateProcess(hProcess, 0);
}
CloseHandle(hProcess);
return bRet;
}
int main(int argc, char* argv[])
{
DWORD dwId;
printf(" 请输入您要终止的进程的ID号:
");
scanf("%u", &dwId);
if (TerminateProcessFromId(dwId))
{
printf(" 终止进程成功!
");
}
else
{
printf(" 终止进程失败!
");
}
return 0;
}
2.4 写一个修改内存的小游戏
小游戏程序:
#include "stdafx.h"
#include <stdio.h>
// 全局变量测试
int g_nNum;
int main(int argc, char* argv[])
{
int i = 198; // 局部变量测试
g_nNum = 1003;
while(1)
{
// 输出个变量的值和地址
printf(" i = %d, addr = %08lX; g_nNum = %d, addr = %08lX
",
++i, &i, --g_nNum, &g_nNum);
getchar();
}
return 0;
}
win10运行在 x86目标程序
#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include <iostream>
BOOL FindFirst(DWORD dwValue); // 在目标进程空间进行第一次查找
BOOL FindNext(DWORD dwValue); // 在目标进程地址空间进行第2、3、4……次查找
DWORD g_arList[1024]; // 地址列表
int g_nListCnt; // 有效地址的个数
HANDLE g_hProcess; // 目标进程句柄
//////////////////////
BOOL WriteMemory(DWORD dwAddr, DWORD dwValue);
void ShowList();
int main(int argc, char* argv[])
{
// 启动02testor进程,在 windows任务管理器会看到
char szFileName[] = "..\02testor\debug\02testor.exe";
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
::CreateProcess(NULL, szFileName, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
// 关闭线程句柄,既然我们不使用它
::CloseHandle(pi.hThread);
g_hProcess = pi.hProcess;
// 输入要修改的值
int iVal;
printf(" Input val = ");
scanf("%d", &iVal);
// 进行第一次查找
FindFirst(iVal);
// 打印出搜索的结果
ShowList();
while(g_nListCnt > 1)
{
printf(" Input val = ");
scanf("%d", &iVal);
// 进行下次搜索
FindNext(iVal);
// 显示搜索结果
ShowList();
}
// 取得新值
printf(" New value = ");
scanf("%d", &iVal);
// 写入新值
if(WriteMemory(g_arList[0], iVal))
printf(" Write data success
");
::CloseHandle(g_hProcess);
return 0;
}
BOOL CompareAPage(DWORD dwBaseAddr, DWORD dwValue)
{
// 读取1页内存
BYTE arBytes[4096];
if(!::ReadProcessMemory(g_hProcess, (LPVOID)dwBaseAddr, arBytes, 4096, NULL))
return FALSE; // 此页不可读
// 在这1页内存中查找
DWORD* pdw;
for(int i=0; i<(int)4*1024-3; i++)
{
pdw = (DWORD*)&arBytes[i];
if(pdw[0] == dwValue) // 等于要查找的值?
{
if(g_nListCnt >= 1024)
return FALSE;
// 添加到全局变量中
g_arList[g_nListCnt++] = dwBaseAddr + i;
}
}
return TRUE;
}
BOOL FindFirst(DWORD dwValue)
{
const DWORD dwOneGB = 1024*1024*1024; // 1GB
const DWORD dwOnePage = 4*1024; // 4KB
if(g_hProcess == NULL)
return FALSE;
// 查看操作系统类型,以决定开始地址
DWORD dwBase;
OSVERSIONINFO vi = { sizeof(vi) };
::GetVersionEx(&vi);
if (vi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
dwBase = 4*1024*1024; // Windows 98系列,4MB
else
dwBase = 640*1024; // Windows NT系列,64KB
// 在开始地址到2GB的地址空间进行查找
for(; dwBase < 2*dwOneGB; dwBase += dwOnePage)
{
// 比较1页大小的内存
CompareAPage(dwBase, dwValue);
}
return TRUE;
}
BOOL FindNext(DWORD dwValue)
{
// 保存m_arList数组中有效地址的个数,初始化新的m_nListCnt值
int nOrgCnt = g_nListCnt;
g_nListCnt = 0;
// 在m_arList数组记录的地址处查找
BOOL bRet = FALSE; // 假设失败
DWORD dwReadValue;
for(int i=0; i<nOrgCnt; i++)
{
if(::ReadProcessMemory(g_hProcess, (LPVOID)g_arList[i], &dwReadValue, sizeof(DWORD), NULL))
{
if(dwReadValue == dwValue)
{
g_arList[g_nListCnt++] = g_arList[i];
bRet = TRUE;
}
}
}
return bRet;
}
// 打印出搜索到的地址
void ShowList()
{
for(int i=0; i< g_nListCnt; i++)
{
printf("%08lX
", g_arList[i]);
}
}
BOOL WriteMemory(DWORD dwAddr, DWORD dwValue)
{
return ::WriteProcessMemory(g_hProcess, (LPVOID)dwAddr, &dwValue, sizeof(DWORD), NULL);
}
由于搜索出来的全局变量,的地址不止一个,需要在目标游戏程序再按下两次回车,直到,输出的地址唯一。
然后写入新的值,改变全局变量的值,从人
3. windows程序执行单元
基本概念
已通知状态(受信状态) 未通知状态(非受信状态)
进程内核对象
当进程正在运行时,进程内核对象处于未通知状态。当进程停止运行时,就处于已通知状态。可以通过等待进程来检查进程是否仍然运行。
无成功等待的副作用。
线程内核对象
当线程正在运行时,线程内核对象处于未通知状态。当线程停止运行时,就处于已通知状态。可以通过等待线程来检查线程是否仍然运行。
无成功等待的副作用。
线程
3.1 线程创建
#include <stdio.h>
#include <windows.h>
// 线程函数
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
int i = 0;
while(i < 20)
{
printf(" I am from a thread, count = %d
", i++);
}
return 0;
}
int main(int argc, char* argv[])
{
HANDLE hThread;
DWORD dwThreadId;
// 创建一个线程
hThread = ::CreateThread (
NULL, // 默认安全属性
NULL, // 默认堆栈大小
ThreadProc, // 线程入口地址(执行线程的函数),线程入口地址使用 __stdcall
NULL, // 传给函数的参数
0, // 指定线程立即运行
&dwThreadId); // 返回线程的ID号
printf(" Now another thread has been created. ID = %d
", dwThreadId);
// 等待新线程运行结束(导致主线程处于不可调度的状态)
::WaitForSingleObject (hThread, INFINITE);
::CloseHandle (hThread);
return 0;
}
3.2 线程内核对象
线程内核对象就是一个包含线程状态信息的数据结构。
每一次对crateThread函数的成功调用,系统会在内部为新的线程分配一个内核对象。
系统提供的管理线程的函数其实就是依靠访问线程内核对象来实现管理的
3.3 线程终止 见 2.3
3.4 线程优先级
#include <stdio.h>
#include <windows.h>
DWORD WINAPI ThreadIdle(LPVOID lpParam)
{
int i = 0;
while(i++<10)
printf("Idle Thread is running
");
return 0;
}
DWORD WINAPI ThreadNormal(LPVOID lpParam)
{
int i = 0;
while(i++<10)
printf(" Normal Thread is running
");
return 0;
}
int main(int argc, char* argv[])
{
DWORD dwThreadID;
HANDLE h[2];
// 创建一个优先级为Idle的线程,并且处于暂停状态
h[0] = ::CreateThread(NULL, 0, ThreadIdle, NULL,
CREATE_SUSPENDED, &dwThreadID);
::SetThreadPriority(h[0], THREAD_PRIORITY_IDLE);
//恢复线程的运行
::ResumeThread(h[0]);
// 创建一个优先级为Normal的线程
h[1] = ::CreateThread(NULL, 0, ThreadNormal, NULL,
0, &dwThreadID);
// 等待两个线程内核对象都变成受信状态
::WaitForMultipleObjects(
2, // DWORD nCount 要等待的内核对象的数量
h, // CONST HANDLE *lpHandles 句柄数组
TRUE, // BOOL bWaitAll 指定是否等待所有内核对象变成受信状态
INFINITE); // DWORD dwMilliseconds 要等待的时间
::CloseHandle(h[0]);
::CloseHandle(h[1]);
return 0;
}
/*
HANDLE h[2];
h[0] = hThread1;
h[1] = hThread2;
DWORD dw = ::WaitForMultipleObjects(2, h, FALSE, 5000);
switch(dw)
{
case WAIT_FAILED:
// 调用WaitForMultipleObjects函数失败(句柄无效?)
break;
case WAIT_TIMEOUT:
// 在5秒内没有一个内核对象受信
break;
case WAIT_OBJECT_0 + 0:
// 句柄h[0]对应的内核对象受信
break;
case WAIT_OBJECT_0 + 1:
// 句柄h[1]对应的内核对象受信
break;
}
*/
运行结果
Normal Thread is running
Normal Thread is running
Normal Thread is running
Normal Thread is running
Normal Thread is running
Normal Thread is running
Normal Thread is running
Normal Thread is running
Normal Thread is running
Normal Thread is running
Idle Thread is running
Idle Thread is running
Idle Thread is running
Idle Thread is running
Idle Thread is running
Idle Thread is running
Idle Thread is running
Idle Thread is running
Idle Thread is running
Idle Thread is running
只要有优先级高的线程调度是不允许 IDLE线程被CPU调度的
3.5 C++运行时库
创建线程使用 _begainthreadex 函数
线程同步
3.6 线程同步
所谓线程同步就是,保证一个时间内只有一个线程对某个共享资源有控制权。共享资源包括全局变量,公共数据成员或者句柄等
3.6.1错误的demo
#include <stdio.h>
#include <windows.h>
#include <process.h>
int g_nCount1 = 0;
int g_nCount2 = 0;
BOOL g_bContinue = TRUE;
UINT __stdcall ThreadFunc(LPVOID);
int main(int argc, char* argv[])
{
UINT uId;
HANDLE h[2];
h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
// 等待1秒后通知两个计数线程结束,关闭句柄
Sleep(1000);
g_bContinue = FALSE;
::WaitForMultipleObjects(2, h, TRUE, INFINITE);
::CloseHandle(h[0]);
::CloseHandle(h[1]);
printf("g_nCount1 = %d
", g_nCount1);
printf("g_nCount2 = %d
", g_nCount2);
return 0;
}
UINT __stdcall ThreadFunc(LPVOID)
{
while(g_bContinue)
{
g_nCount1++;
g_nCount2++;
}
return 0;
}
两个线程同时执行ThreadFunc 并修改静态区的变量。1秒之后,g_nCount1和g_nCount2 数值不一样,
原因g_nCount1++并非原子操作,
3.6.2 使用临界区对象
临界区对象是定义在数据段中的一个critical_section结构,windows内部使用这个结构记录一些信息,确保在同一时间只有一个线程访问改数据段的数据。
#include <stdio.h>
#include <windows.h>
#include <process.h>
BOOL g_bContinue = TRUE;
int g_nCount1 = 0;
int g_nCount2 = 0;
CRITICAL_SECTION g_cs; // 对存在同步问题的代码段使用临界区对象
UINT __stdcall ThreadFunc(LPVOID);
int main(int argc, char* argv[])
{
UINT uId;
HANDLE h[2];
// 初始化临界区对象
::InitializeCriticalSection(&g_cs);
h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
// 等待1秒后通知两个计数线程结束,关闭句柄
Sleep(1000);
g_bContinue = FALSE;
::WaitForMultipleObjects(2, h, TRUE, INFINITE);
::CloseHandle(h[0]);
::CloseHandle(h[1]);
// 删除临界区对象
::DeleteCriticalSection(&g_cs);
printf("g_nCount1 = %d
", g_nCount1);
printf("g_nCount2 = %d
", g_nCount2);
return 0;
}
UINT __stdcall ThreadFunc(LPVOID)
{
while(g_bContinue)
{
::EnterCriticalSection(&g_cs);
g_nCount1++;
g_nCount2++;
::LeaveCriticalSection(&g_cs);
}
return 0;
}
临界区对象能够很好地保护共享数据,但是不能够用于进程之间资源的锁定,因为它不是内核对象。如果要在进程间维持线程的同步,可以使用事件内核对象。
3.6.3 互锁函数
互锁函数为同步访问多线程共享变量提供一个简单机制。如果变量在共享内存,不同进程的线程也可以使用此机制。
- InterlockedIncrement
- InterlockedDecrement
- InterlockedExchangeAdd
- InterlockedExchangePointer
#include <stdio.h>
#include <windows.h>
#include <process.h>
int g_nCount1 = 0;
int g_nCount2 = 0;
BOOL g_bContinue = TRUE;
UINT __stdcall ThreadFunc(LPVOID);
int main(int argc, char* argv[])
{
UINT uId;
HANDLE h[2];
h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
// 等待1秒后通知两个计数线程结束,关闭句柄
Sleep(1000);
g_bContinue = FALSE;
::WaitForMultipleObjects(2, h, TRUE, INFINITE);
::CloseHandle(h[0]);
::CloseHandle(h[1]);
printf("g_nCount1 = %d
", g_nCount1);
printf("g_nCount2 = %d
", g_nCount2);
return 0;
}
UINT __stdcall ThreadFunc(LPVOID)
{
while(g_bContinue)
{
::InterlockedIncrement((long*)&g_nCount1);
::InterlockedIncrement((long*)&g_nCount2);
}
return 0;
}
3.6.4 事件内核对象
主线程通过事件状态设为"授信"来通知子线程开始工作。这是事件内核对象一个很重要的用途
事件内核对象主要用于线程间通信,因为它是一个内核对象,所以可以跨进程使用。
依靠在线程间通信就可以使用各线程的工作协调进行,达到同步的目的。
#include <stdio.h>
#include <windows.h>
#include <process.h>
HANDLE g_hEvent;//全局保存事件内核对象句柄,便于操作
UINT __stdcall ChildFunc(LPVOID);
int main(int argc, char* argv[])
{
HANDLE hChildThread;
UINT uId;
// 创建一个自动重置的(auto-reset events),未受信的(nonsignaled)事件内核对象
g_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
hChildThread = (HANDLE)::_beginthreadex(NULL, 0, ChildFunc, NULL, 0, &uId);
// 通知子线程开始工作
printf("Please input a char to tell the Child Thread to work:
");
getchar();
::SetEvent(g_hEvent);
// 等待子线工作,释放资源
::WaitForSingleObject(hChildThread, INFINITE);
printf("All the 程完成work has been finished.
");
::CloseHandle(hChildThread);
::CloseHandle(g_hEvent);
return 0;
}
UINT __stdcall ChildFunc(LPVOID)
{
::WaitForSingleObject(g_hEvent, INFINITE);
printf(" Child thread is working......
");
::Sleep(5*1000); // 暂停5秒,模拟真正的工作
return 0;
}
运行结果
Please input a char to tell the Child Thread to work:
c
Child thread is working......
All the work has been finished.
3.6.5 信号量内核对象
信号量允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问次资源的最大线程数目。
信号量是通过计数来对线程访问资源进行控制,实际上,信号量也被称作 Dijstra计数器3.
// 信号量对象句柄
HANDLE hSemaphore;
UINT ThreadProc15(LPVOID pParam)
{
// 试图进入信号量关口
WaitForSingleObject(hSemaphore, INFINITE);
// 线程任务处理
AfxMessageBox("线程一正在执行!");
// 释放信号量计数
ReleaseSemaphore(hSemaphore, 1, NULL);
return 0;
}
UINT ThreadProc16(LPVOID pParam)
{
// 试图进入信号量关口
WaitForSingleObject(hSemaphore, INFINITE);
// 线程任务处理
AfxMessageBox("线程二正在执行!");
// 释放信号量计数
ReleaseSemaphore(hSemaphore, 1, NULL);
return 0;
}
UINT ThreadProc17(LPVOID pParam)
{
// 试图进入信号量关口
WaitForSingleObject(hSemaphore, INFINITE);
// 线程任务处理
AfxMessageBox("线程三正在执行!");
// 释放信号量计数
ReleaseSemaphore(hSemaphore, 1, NULL);
return 0;
}
……
void CSample08View::OnSemaphore()
{
// 创建信号量对象
hSemaphore = CreateSemaphore(NULL, 2, 2, NULL);
// 开启线程
AfxBeginThread(ThreadProc15, NULL);
AfxBeginThread(ThreadProc16, NULL);
AfxBeginThread(ThreadProc17, NULL);
}
上述代码在开启线程前首先创建了一个初始计数和最大资源计数均为2的信号量对象hSemaphore。即在同一时刻只允许2个线程进入由hSemaphore保护的共享资源。
随后开启的三个线程均试图访问此共享资源,在前两个线程试图访问共享资源时,由于hSemaphore的当前可用资源计数分别为2和1,此时的hSemaphore是可以得到通知的,也就是说位于线程入口处的WaitForSingleObject()将立即返回,而在前两个线程进入到保护区域后,hSemaphore的当前资源计数减少到0,hSemaphore将不再得到通知,WaitForSingleObject()将线程挂起。直到此前进入到保护区的线程退出后才能得以进入。
3.6.6 互斥内核对象
保证多个线程对同一共享资源的互斥访问
#include "stdafx.h"
#include <string>
#include <iostream>
#include <Windows.h>
using namespace std;
HANDLE hMutex = NULL;
int arr[5] = {1, 2, 3, 4, 5};
DWORD CALLBACK Thread1(LPVOID Param)
{
while(1)
{
WaitForSingleObject(hMutex, INFINITE);
cout << "Thread1: ";
for (int i = 0; i < 5; i++)
{
cout << arr[i] << ‘ ‘;
Sleep(50);
}
cout << endl;
ReleaseMutex(hMutex);
}
return 0;
}
DWORD CALLBACK Thread2(LPVOID Param)
{
while(1)
{
WaitForSingleObject(hMutex, INFINITE);
cout << "Thread2: ";
for (int i = 0; i < 5; i++)
{
cout << arr[i] << ‘ ‘;
Sleep(50);
}
cout << endl;
ReleaseMutex(hMutex);
}
return 0;
}
int main()
{
HANDLE hArray[2] = {0};
hMutex = CreateMutex(NULL, NULL, NULL);
hArray[0]=CreateThread(NULL,0,Thread1,NULL,0,NULL);
hArray[1]=CreateThread(NULL,0,Thread2,NULL,0,NULL);
WaitForMultipleObjects(2, hArray, TRUE, INFINITE);
CloseHandle(hArray[0]);
CloseHandle(hArray[1]);
CloseHandle(hMutex);
return 0;
}
3.6.7 线程局部存储
线程局部存储(TLS)是一个方便存储线程局部数据的系统。
利用TLS机制可以为进程中所有的线程关联若干个数据,各个线程通过TLS 分配的全局索引来访问与自己关联的数据。这样每个线程都可以有线程局部的静态存储数据。
下面的例子就是将每个线程的创建时间与该线程关联起来,这样,在线程终止的时候就可以得到线程的生命周期。
#include <stdio.h>
#include <windows.h>
#include <process.h>
// 利用TLS记录线程的运行时间
DWORD g_tlsUsedTime;
void InitStartTime();
DWORD GetUsedTime();
UINT __stdcall ThreadFunc(LPVOID)
{
int i;
// 初始化开始时间
InitStartTime();
// 模拟长时间工作
i = 10000*10000;
while(i--) { }
// 打印出本线程运行的时间
printf(" This thread is coming to end. Thread ID: %-5d, Used Time: %d
",
::GetCurrentThreadId(), GetUsedTime());
return 0;
}
int main(int argc, char* argv[])
{
UINT uId;
int i;
HANDLE h[10];
// 通过在进程位数组中申请一个索引,初始化线程运行时间记录系统
g_tlsUsedTime = ::TlsAlloc();
// 令十个线程同时运行,并等待它们各自的输出结果
for(i=0; i<10; i++)
{
h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
}
for(i=0; i<10; i++)
{
::WaitForSingleObject(h[i], INFINITE);
::CloseHandle(h[i]);
}
// 通过释放线程局部存储索引,释放时间记录系统占用的资源
::TlsFree(g_tlsUsedTime);
return 0;
}
// 初始化线程的开始时间
void InitStartTime()
{
// 获得当前时间,将线程的创建时间与线程对象相关联
DWORD dwStart = ::GetTickCount();
::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart);
}
// 取得一个线程已经运行的时间
DWORD GetUsedTime()
{
// 获得当前时间,返回当前时间和线程创建时间的差值
DWORD dwElapsed = ::GetTickCount();
dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime);
return dwElapsed;
}
结果
This thread is coming to end. Thread ID: 7296 , Used Time: 593
This thread is coming to end. Thread ID: 10108, Used Time: 609
This thread is coming to end. Thread ID: 6612 , Used Time: 640
This thread is coming to end. Thread ID: 10132, Used Time: 609
This thread is coming to end. Thread ID: 6248 , Used Time: 641
This thread is coming to end. Thread ID: 4748 , Used Time: 641
This thread is coming to end. Thread ID: 10540, Used Time: 625
This thread is coming to end. Thread ID: 1876 , Used Time: 625
This thread is coming to end. Thread ID: 9164 , Used Time: 625
This thread is coming to end. Thread ID: 7860 , Used Time: 641
以上是关于Windows程序设计的主要内容,如果未能解决你的问题,请参考以下文章