第4章 进程
- 一般将进程定义成一个正在运行程序的一个实例,由2部分组成:内核对象和地址空间;
- 在单CPU的计算机上,操作系统以轮询方式为每个单独的线程分配时间片;
4.1 编写第一个Windows应用程序
- Windows支持两种类型的应用程序:GUI程序(图形用户界面)和CUI程序(控制台用户界面);
- 使用VS创建项目时,集成开发环境会设置连接器开关,GUI程序的连接器开关是/SUBSYSTEM:CONSOLE,CUI程序的是/SUBSYSTEM:WINDOWS;当使用VS创建项目时错误选择了项目类型,可在项目属性中通过更改连接器开关来更改项目类型;
- Windows应用程序必须有一个入口点函数,程序开始运行时,这个函数会被调用,C/C++程序员可以使用以下2种入口点函数:
int WINAPI _tWinMain(
HINSTANCE hInstanceExe, //可执行文件的实例,实际值是一个内存基地址
HINSTANCE, //无参数名,编译器不会发出“参数没有被引用到”警告
PTSTR,
int nCmdShow);
int _tmain(
int argc,
TCHAR *argv[],
TCHAR *envp[]);
应用程序类型和相应的入口函数:
应用程序类型 | 入口点函数(入口) | 嵌入可执行文件的启动函数 |
处理ANSI字符串的GUI应用程序 | _tWinMain(WinMain) | WinMainCRTStartup |
处理Unicode字符串的GUI应用程序 | _tWinMain(wWinMain) | wWinMainCRTStartup |
处理ANSI字符串的CUI应用程序 | _tMain(Main) | mainCRTStartup |
处理Unicode字符串的CUI应用程序 | _tMain(WMain) | wmainCRTStartup |
C/C++运行库启动函数的用途简单总结如下:
- 获取指向新进程的完整命令行的一个指针;
- 获取指向新进程的环境变量的一个指针;
- 初始化C/C++运行库的全局变量,如果包含StdLib.h,我们的代码就可以访问这些变量,变量总结如下:
变量名称 | 类型 | 描述和推荐使用的Windows函数 |
_osver | unsigned int | 操作系统的构建(build)版本号,请换用GetVersionEx |
_winmajor | unsigned int | 以十六进制表示的Windows系统的主版本号,请换用GetVersionEx |
_winminor | unsigned int | 以十六进制表示的Windows系统的次版本号,请换用GetVersionEx |
_winver | unsigned int | (_winmajor<<8) + _winmajor,请换用GetVersionEx |
_argc | unsigned int | 命令行上传递的参数个数,请换用GetCommandLine |
_argv _wargv |
char wchar_t |
长度为_argc的一个数组,请换用GetCommandLine |
_environ _wenviron |
char wchar_t |
一个 指针数组,请换用GetEnvironmentStrings或GetEnvironmentVarible |
_pgmptr _wpgmptr |
char wchar_t |
正在运行程序的名称及其ANSI/Unicode完整路径,请换用GetModuleFileName |
- 初始化C运行库内存分配函数(malloc和calloc)和其他底层I/O例程使用的堆(heap);
- 调用所有全局和静态C++类对象的构造函数;
- 完成了这些初始化工作,C/C++启动函数就会调用应用程序的入口点函数
4.1.1 进程实例句柄
- 加载到进程地址空间的每一个可执行文件或DLL都被赋予了唯一的实例句柄;
- HMODULE和HINSTANCE完全是一回事,在16位Windows中,则表示不同类型的数据;
//获取可执行文件/DLL文件被加载到进程地址空间的位置,函数返回一个句柄/基地址
HMODULE GetModuleHandle(PCTSTR pszModule);
4.1.2 进程前一个实例的句柄
C/C++运行库启动代码总是向(w)WinMain的hPrevInstance参数传递NULL,该参数用于16位Windows系统,不要在自己代码中引用这个参数;
4.1.3 进程命令行
系统在创建新进程时,会传一个命令行给它,这个命令行几乎总是非空的;
4.1.4 进程的环境变量
//每个进程都有环境块,环境块是在进程地址空间内分配的一块内存
=::=::\ ...
VarName1 = VarValue2\0 //环境变量的名称 = 赋予此变量的值
VarName2 = VarValue1\0
VarName3 = VarValue3\0 ...
VarNameX = VarValueX\0
\0
- 调用GetEnvironmentStrings函数来获取完整的环境块,当不再需要GetEnvironmentStrings函数返回的内存块,应调用FreeEnvironmentStrings函数来释放它:
BOOL FreeEnvironmentStrings(PTSTR pszEnvironmentBlock);
- 通过应用程序main入口点函数接收的TCHAR* env[]参数来获取环境块(CUI程序专用):空格是有意义的,等号前后的任何空格都会被考虑在内;
- 假如更新了注册表项,并希望应用程序立即更新它们的环境块,可以进行如下调用:
SendMessage(HWND _BROADCASR,WM_SETTINGCHANGE,0,(LPARAM) TEXT("Environment"));
//成功找到变量名,返回字符数;未找到就返回0;
DWORD GetEnvironmentVariable(
PCTSTR pszName, //指向预期的变量名称
PTSTR pszValue, //指向保存变量值的缓冲区
DWORD cchValue); //指出缓冲区大小(字符数),可传入0
- 许多字符串内部都有“可替换字符串”,如:%USERPROFILE%\Documents,两个%之间的内容就是一个“可替换字符串”,环境变量的值放在这里,执行字符串替换后,生成扩展字符串:
//Windows提供了相应的函数,返回值是保存扩展字符串所需的缓冲区大小
//如果参数chSize小于返回值,%%变量不会扩展,会被替换成空字符串
//通常要2次调用ExpandEnvironmentStrings函数
DWORD ExpandEnvironmentStrings(
PTCSTR pszSrc, //"可替换环境变量字符串"的字符串地址
PTSTR pszSrc, //接收扩展字符串的缓冲区地址
DWORD chSize); //缓冲区的最大大小(用字符数来表示)
//可添加、删除、修改一个变量的值
//如果指定的变量不存在就创建它,如果pszValue为NULL则从环境快中删除该变量
BOOL SetEnvironmentVarible(
PCTSTR pszName, //标识一个变量
PCTSTR pszValue); //想要修改成的值
4.1.5 进程的关联性
进程中的线程可以在主机的任何CPU上执行,也可以强迫线程在可用CPU的一个子集上运行,着称为“处理器的关联性”;
4.1.6 进程的错误模式
//进程调用该函数来告诉系统如何处理错误
UINT SetErrorMode(UINT fuErrorMode);
4.1.7 进程当前所在的驱动器和目录
//线程调用以下函数来获取和设置其所在进程的当前驱动器和目录
DWORD GetCurrentDirectory(DWORD cchCurDir,PTSTR pszCurDir);
BOOL SetCurrentDirectory(PCTSTR pszCurDir);
4.1.8 进程的当前目录
- Windows的文件函数不会添加或更改驱动器号环境变量,它们只是读取这种变量;
- 调用GetFullPathName来获得进程当前目录;
4.1.9 系统版本
OSVERSIONINFOEX结构;
//能比较主机系统的版本和应用程序要求的版本
BOOL VerifyVersionInfo(
POSVERSIONINFOEX pVersionInformation,
DWORD dwTypeMask,
DWORDLONG dwlConditionMask); //一个64位的值,决定着比较方式
4.2 CreateProcess函数
//创建一个进程,它注意不到任何初始化问题
BOOL CreateProcess(
PCTSTR pszApplicationName, //新进程要使用的可执行文件名称
PTSTR pszCommandLine, //传给新进程的命令行字符串
PSECURITY_ATTRIBUTES psaProcess, //
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles, //新进程对内核对象的继承
DWORD fdwCreate, //标识影响新进程创建方式的标志
PVOID pvEnvironment, //指向一块内存地址
PCTSTR pszCurDir,
PSTARTUPINFO psiStartInfo,
pPROCESS_INFORMATION ppiProcInfo);
4.2.1 pszApplicationName和pszCommandLine参数
4.2.2 psaProcess,psaThread和bInheritHandles参数
- 为了创建新进程,系统也必须创建一个进程内核对象和一个线程内核对象(用于进程的主线程);
- 分配2个SECURITY_ATTRIBUTES结构,以便创建安全权限、对象句柄的继承;
4.2.3 fdwCreate参数
fdwCreate参数可用标志如下,还允许指定一个优先级类(priority class):
DEBUG_PROCESS | 父进程希望调试子进程及子进程生成的所有进程,任何子进程发生事件,都要通知父进程 |
DEBUG_ONLY_PROCESS | 父进程只调试子进程,最近子进程发生事件,才通知父进程 |
CREATE_SUSPENDED | 创建新进程同时挂起主线程 |
DETACHED_PROCESS | 阻止一个CUI的进程访问其父进程的控制台窗口,并将它的输出发送到一个新的控制台窗口 |
CREATE_NEW_CONSOLE | 为新进程创建新的控制台窗口 |
CREATE_NO_WINDOW | 不要为应用程序创建任何控制台窗口 |
CREATE_NEW_PROCESS_GROUP | 创建一个新的进程组,修改用户按Ctrl+C或Ctrl+Break时获得通知的进程列表 |
CREATE_DEFAULT_ERROR_MODE | 新进程不会继承父进程所用的错误模式(SetErrorMode函数) |
CREATE_SEPARATE_WOW_VDM | 16位系统独有,创建一个单独的虚拟DOS机(Virtual DOS Machine,VDM),各个程序在单独VDM中运行 |
CREATE_SHARED_WOW_VDM | 16位系统独有,所有应用程序在共享VDM中运行 |
CREATE_UNICODE_ENVIRONMENT | 标志告诉系统子进程环境块包含Unicode字符,进程环境块默认包含ANSI字符串 |
CREATE_FORCEDOS | 强制运行一个嵌入在16位OS/2应用程序中的MS-DOS应用程序 |
CREATE_BREAKAWAY_FROM_JOB | 允许作业中的进程生成一个和作业无关的进程 |
EXTENDED_STARTUPINFO_PRESENT | 向系统表明传给psiStartInfo参数的是一个STARTUPINFOEX结构 |
4.2.4 pvEnvironment参数
pvEnvironment参数指向的内存块包含新进程要使用的环境字符串,使用GetEnvironmentStrings函数返回环境字符串数据块的地址,当不再需要这块内存时,调用FreeEnvironmentStrings函数来释放它;
4.2.5 pszCurDir参数
参数允许父进程设置子进程的当前驱动器目录
4.2.6 psiStartInfo参数
参数指向一个STARTUPINFO结构或STARTUPINFOEX结构,必须把这个结构不使用的成员清零,结构成员的初始化必须在调用CreateProcess之前完成;
STARTUPINFO结构和STARTUPINFOEX结构的成员:
typedef struct _STARTUPINFO{
DWORD cb; //STARTUPINFO结构中的字节数
PSTR IpReserved; //保留,必须初始化为NULL
PSTR IpDesktop; //在哪个桌面上启动应用程序
PSTR IpTitle; //控制台窗口的窗口标题
DWORD dwX; //应用程序窗口在屏幕上的位置(x,y坐标)
DWORD dwY;
DWORD dwXSize; //应用程序窗口的高度和宽度
DWORD dwYSize;
DWORD dwXCountChars; //指定子进程的控制台窗口的高度和宽度(用字符数表示)
DWORD dwYCountChars;
DWORD dwFillAttribute; //指定子进程的控制台窗口所用的文本和背景色
DWORD dwFlags;
WORD wShowWindow; //指定应用程序主窗口如何显示
WORD cbReserved2; //保留,必须初始化为0
PBYTE IpREserved2; //保留,必须初始化为NULL
HANDLE hStdInput; //指定到控制台输入缓冲区的句柄和输出缓冲区的句柄
HANDLE hStdOutput;
HANDLE hStdError;
}STARTUPINFO, *LPSTARTUPINFO;
typedef struct _STARTUPINFOEX{
STARTUPINFO StartupInfo;
struct _PROC_THREAD_ATTRIBUTE_LIST *IpAttributeList;
}STARTUPINFOEX, *LPSTARTUPINFOEX;
dwFlags成员有一组标志,用于告诉CreateProcess函数:PSTARTUPINFO结构中的其他成员包含的信息是否有用,或者忽略一些成员;
4.2.7 ppiProcInfo参数
参数指向一个PROCESS_INFORMATION结构,Create函数返回之前会初始化结构成员:
typedef struct _PROCESS_INFORMATION{
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
}PROCESS_INFORMATION:
GetCurrentProcessId 获得当前进程ID
GetCurrentThreadId 获得当前正在运行线程的ID
GetProcessId 获得与指定句柄对应的一个进程ID
GetThreadId 获得与指定句柄对应的一个线程ID
GetProcessIdOfThread 根据线程句柄,获得其所在进程的ID
应用程序要与它的创建者通信,最好不要使用ID,因为ID会被重用,此时ID标识的可能是另一个进程了,应该使用内核对象、窗口句柄来进行通信;
要使进程/线程ID不被重用,就是保证进程或线程对象不被销毁,即不关闭这些对象的句柄,直到应用程序不再使用ID时,再调用CloseHandle来释放内核对象;
4.3 终止进程
进程可通过以下4种方式终止:
4.3.1 主线程的入口点函数返回(强烈推荐的方式)
设计应用程序时,应保证主线程的入口点函数返回后,应用程序的进程才终止;
4.3.2 进程中的一个线程调用ExitProcess函数(避免这种方式)
VOID ExitProcess(UINT fuExitCode);
//ExitThread将使应用程序的主线程停止执行,进程只要还有其他线程在运行,进程就不会终止
4.3.3 进程中的一个线程调用TerminateProcess函数(避免这种方式)
//任何线程都能调用它来终止另一个进程或自己的进程
BOOL TerminateProcess(
HANDLE hProcess, //要终止的进程句柄
UINT fuExitCode); //进程终止时的退出代码,传入fuExitCode参数的值
4.4.4 进程中所有线程自然死亡(几乎不会发生)
BOOL GetExitCodeProcess(HANDLE hProcess,PDWORD pdwExitCode);
4.4 子进程
Windows提供了几种在不同进程之间传递数据的方式:动态数据交换(DDE),OLE,管道,邮件槽;
//创建新线程,让它执行工作,然后等候结果
PROCESS_INFORMATION pi;
DWORD dwExitCode;
//创建子进程
BOOL fSuccess = CreateProcess(...., &pi);
if(fSuccess){
//当不再需要就关闭线程句柄
CloseHandle(pi.hThread);
//暂停执行父进程的线程,直到子进程终止
WaitForSingleObject(pi.hProcess,INFINITE);
//子进程结束,获取退出代码
GetExitCodeProcess(pi.hProcess,&dwExitCode);
//当不再需要就关闭进程句柄
CloseHandle(pi.hProcess);
}
4.5 管理员以标准用户权限运行时
用户账户控制(UAC)
4.5.1 自动提升进程的权限
可执行文件中嵌入了特殊的资源(RT_MANIFEST),系统会检查清单文件的<trustInfo>段;
可以将清单保存到可执行文件所在目录,扩展名使用.manifest;
4.5.2 手动提升进程的权限
调用ShellExecuteEx函数来提升权限
4.5.3 何为当前权限上下文
GetProcessElevation函数能返回:提升类型和指出进程是否正在以管理员身份运行的布尔值;
4.5.4 枚举系统中正在运行的进程
“性能数据”数据库