Windows核心编程 第4章 进程
进程由两部分组成:
- 操作系统管理进程的内核对象。存放该进程 的统计信息的地方。
- 地址空间,包含可执行模块和DLL模块的代码和数据。动态分配的内存(线程堆栈和堆)。
进程是不活泼的,进程当中至少要有一个线程,每个线程要有自己的堆栈和自己的CPU寄存器。CPU通过算法给每个线程分配时间片的办法来造成假象是在同时工作(多核通过自己的算法实现同时运行)。
4.1 编写第一个Windiws应用程序
Windows两种类型的程序:
- CUI程序,比如CMD.EXE等等。Microsoft Visual C++连接开关为/SUBSYSTEM:CONDOLE(程序启动时不能创建GUI程序)。
- GUI程序,图形用户程序,比如Notepad,Word等等。Microsoft Visual C++连接开关为/SUBSYSTEM:WINDOWS(程序启动时不能创建CUI程序)。
注意:俩者的概念其实是很模糊的,CUI可以创建GUI图形界面,反之GUI程序可能用CUI程序。
Windows进入点函数(区分在于CUI和GUI程序,ANSI码和UNICODE码)
int WINAPI WinMain( HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow); int WINAPI wWinMain( HINSTANCE hinstExe, HINSTANCE, PWSTR pszCmdLine, int nCmdShow); int __cdecl main( int argc, char *argv[], char *envp[]); int __cdecl wmain( int argc, wchar_t *argv[], wchar_t *envp[]);
其实Windows程序启动时最开始并不调用自己写的入口函数,而是调用系统的几个入口函数,以便可以调用malloc和free之类的函数,初始化全局和静态C++对象等。
- 检索指向新进程的完整命令行的指针。
- 检索指向新进程的环境变量的指针。
- 对C/C++运行期的全局变量进行初始化。如果包含StdLib.h文件,代码就可以访问这些变量。
- 对C运行期的malloc和callo和其他底层输入/输出例程使用的内存栈进行初始化。
- 为所有全局和静态C++类对象调用构造函数。
应用程序类型 进入点 嵌入可执行文件的启动函数
ANSI码GUI应用程序 WinMain WinMainCRTStattup
UNICODE码GUI应用程序 wWinMain wWinMainCRTStattup
ANSI码CUI应用程序 main mainCRTStattup
UNICODE码CUI应用程序 wmain wmainCRTStattup
注意:应用程序会根据SUBSYSTEM开关来查找嵌入可执行启动函数,如果进入点函数和启动函数不匹配则显示链接错误。可以删除SUBSYSTEM(VS Project Settings)开关,这样应用程序会自动需找匹配的函数。
进入点函数返回时调用系统的exit函数,将返回值传递给它。exit函数负责下面操作:
- 调用由_onexit函数的调用而注册的任何函数。
- 为所有全局的和静态的C++类对象调用析构函数
- 调用操作系统的ExitProcess,并将返回值传递给他,关闭进程。
4.1.1 进程的实例句柄
WinMain/wWinMain函数的第一个参数表示进程加载的可执行文件的基地址/句柄。对于加载资源的调用都要使用此句柄,比如HICON LoadIcon(HINSTANCE, PCTSTR)。有的函数需要使用HMODULE,和HINSTANCE是一个意思(区分主要在于16位的操作系统中)。
HMODULE GetModuleHandle(PCTSTR pszModele);
函数作用,返回加载调用进程中的可执行文件或者DLL的基地址/句柄,参数是可执行文件或者DLL的名称。给pszModule赋值NULL,则返回的是进程中可执行文件的句柄。
注意:如果找不到则返回NULL。如果在DLL中传递NULL,返回的仍然是进程加载的可执行文件的句柄。
4.1.2 进程的前一个实例句柄
第二个参数都传递NULL,是为16位系统所保留的。
4.1.3 进程的命令行
注意:不要试图修改命令行内部内存的值,要使用修改先拷贝出来。
PTSTR GetCommandLine(); // 返回命令行字符串
PTSTR CommandLineToArgv(PTSTR pszCmdLine, int *pNumArgs); // 拆分命令行字符串函数
Demo:
int nNumargs; PTSTR *ppArgv = CommandLineToArgv(GetCommandLine(), &nNumargs); if ('x' == *ppArgv[1]) { // TODO: } // 手动释放内存,一般不需要释放,系统会进程关闭时候自动释放 HeapFree(GetProcessHeap, 0, ppArgv);
4.1.4 进程的环境变量
环境块是进程地址空间中分配的内存块每个环境块都包含一组字符串,格式如下:
VarName1=VarVarlue1\0
VarName2=VarVarlue2\0
VarName3=VarVarlue3\0
…..
VarNameX=VarVarlueX\0
\0
注意:
- 排序必须按照字母顺序。
- ‘=’号不能是变量名的一部分。
- 等号左右两边的空格将被算做名称或者值。
- 最后必须加个’\0’表示结束。
- 子进程和父进程不共用环境块,修改不会影响父/子进程。
DWORD GetEnvironmentVariable(PCTSTR pszName, PTSTR pszValue, DWORD cchValue);
pszName指变量名,pszValue指向变量值的缓存区,cchValue缓存区的大小。找不到变量名或者设置的长度不够存放就返回0。
ExpandEnvironmentStrings(PCSTR pszSrc, PSTR pszDst, DWORD nSize);
用来用现实出可替换的环境变量的字符串。
BOOL SetEnvironmentVariable(PCTSTR pszName, PCTSTR pszValue);
设置环境变量的值,如果不存在则创建,如果存在则替换他的值。
4.1.5 进程的亲缘性
子进程继承父进程的亲缘性。(具体什么意思没明白)
4.1.6 进程的错误模式
进程可以设置如何处理一些错误。
UINT SetErrorMode(UINT fuErrorMode);
各个模式用OR连接
标志 说明
SEM_FAILCRITICALERRORS 系统不显示关键错误句柄消息框,并将错误返回给调用进程
SEM_NOGOFAULTERRORBOX 系统不显示一般保护故障消息框。本标志只应该由采用异常情况处理程序来处理一般保护(GP)故障的调式应用程式来设定
SEM_NOOPENFILEERRORBOX 当系统找不到文件时,它不显示消息框。
SEM_NOALIGNMENTFAULTEXCEPT 系统自动排除内存没有对其的故障,并使应用程序看不到这些故障。本标志对X86处理器不起作用。
子进程继承父进程的错误模式,如果不想让子进程继承父进程的错误模式的话,可以再调用CreateProcess时设定CREATE_DEFAULT_ERROR_MODE标志。
4.1.7 进程的当前驱动器和目录
默认情况下不提供全路径的话,系统就会在当前驱动器和目录中查找文件,比如CreateFile,因为驱动器和目录是每个进程来维护的,所以某个线程改变了目录和驱动器会改变整个进程的目录和驱动器。
下面两个函数读取和设置:
DWORD GetCurrentDirectory(DWORD cchCurDir, PTSTR pszCurDir); BOOL SetCurrentDirectory(PCTSTR pszCurDir);
4.1.8 进程的当前目录
驱动器环境块的格式:
=C:=C:\Utility\Bin
程序查找驱动器环境块,如果没有则按驱动器名查找。
子进程不能继承父进程的驱动器块,如果想继承必须写到环境变量中去。(好像是这样,如果有不对请高人指点)。
DWORD GetFullPathName(PCTSTR pszFile, DWORD cchPath, PTSTR pszPath, PTSTR *ppszFilePart);
获取驱动器的当前目录,比如:
TCHAR szCurDir[MAX_PATH];
DWORD GetFullPathName(TEXT("C:"), MAX_PATH, szCurDir, NULL);
4.1.9 系统版本
DWORD GetVersion();此函数存在高地位的混论BUG,所以尽量不要使用。
BOOL GetVersion(POSVERSIONINFOEX pVersionInfomation); typedef struct _OSVERSIONINFOEXA { DWORD dwOSVersionInfoSize; // 在调用GetVersionEx函数之前,必必须置为sizeof(OSVERSIONINFOEX) DWORD dwMajorVersion; // 主系统的主要版本号 DWORD dwMinorVersion; // 主系统的次要版本号 DWORD dwBuildNumber; // 当前系统的构建号 DWORD dwPlatformId; // 识别当前系统的平台。可以使VER_PLATFORM_WIN32(WIN32),VER_PLATFORM_WIN32_WINDOWS(WINDOWS 95/WINDOWS 98),VER_PLATFORM_WIN32_NT(WINDOWS NT/WINDOWS 2000)或VER_PLATFORM_WIN32_CEHH(WINDOWS CE) CHAR szCSDVersion[ 128 ]; // Maintenance string for PSS usage 本域包含了附加文本,用于提供关于已经安装的操作系统的详细信息 WORD wServicePackMajor; // 最新安装的服务程序包的主要版本号 WORD wServicePackMinor; // 最新安装的服务程序包的次要版本号 WORD wSuiteMask; // 用于标识系统上存在那个程序组(VER_SUITE_SMALLBUSINESS,VER_SUITE_ENTERPRISE,VER_SUITE_BACKOFFICE,VER_SUITE_COMMUNICATIONS,VER_SUITE_TERMINAL,VER_SUITE_SMALLBUSINESS_RESTRICTED,VER_SUITE_EMBEDDEDNT和VER_SUITE_DATACENTER) BYTE wProductType; // 用于标识安装了下面的哪个操作系统:VER_NT_WORKSTATION,VER_NT_SERVER或VER_NT_DOMAIN_CONTROLLER BYTE wReserved; // 留作将来使用 } OSVERSIONINFOEXA, *POSVERSIONINFOEXA, *LPOSVERSIONINFOEXA;
这个是扩展版本。
BOOL CreateProcess( LPCSTR lpApplicationName, LPSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCSTR lpCurrentDirectory, LPSTARTUPINFOA lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation );
- 进程启动时,首先创建一个进程内核对象,该内核对象不是进程,是一个管理进程存储进程信息的小型数据结构。(计数为1)
- 创建一个虚拟地址空间,加载可执行文件和DLL。
- 为进程创建个主线程的内核对象,和进程内核对象一样,是用来管理和存储线程信息的小型数据结构。(计数为1)
- 调用C/C++运行期启动代码,主线程开始运行,最终调用启动函数,成功返回TRUE(未能正确加载DLL也返回TRUE,所以父进程无法查看)。
4.2.1 pszApplicationName和pszCommandLine
pszCommandLine参数:用来创建进程的命令行参数。查看第一个标记,如果没有”.exe”会自动添加”.exe”上去。(如果pszApplicationName参数NULL)
- 包含调用进程的”.exe”文件的目录。
- 调用进程的当前目录。
- windows系统目录
- windows目录
- PATH环境变量中列出的目录。
如果pszApplicationName参数不为NULL,系统将在当前目录中查找.exe文件(不会自动添加“.exe”),如果找不到将失败,此时pszCommandLine作为参数传递给可执行程序的进程。
4.2.2 psaProcess,psaThread和binHeritHandles
psaProcess,psaThread是进程和进程主线程内核对象的安全属性。默认值为NULL。
binHeritHandles设置为TRUE表示父进程在创建子进程可以继承安全属性标志里设置为TRUE的任何可继承的内核对象。如果设置为FALSE子进程将不继承任何内核对象。
4.2.3 fdwCreate
用于标识标志,定义规则如何创建新进程。我一般写默认值NULL。具体的太多了,请查看MSDN吧,不想写了。
4.2.4 pvEnvironment
设置子进程使用的环境内存块,一般默认值为NULL,表示子进程继承父进程的环境块。
PVOID GetEnvironmentString(); // 获取当前内存块的地址 BOOL FreeEnvironmentStrings(PTSTR pszEnvironmentBlock); // 不用的时候调用此函数释放内存块
4.2.5 pszCurDir
设定工作目录和驱动器号,如果为NULL则和应用程序的目录相同,如果设置比如以’\0’结尾的包含驱动器名的路径。
4.2.6 psiStartInfo
typedef struct _STARTUPINFO { DWORD cb; //(两者兼有,控制台和窗口程序) LPSTR lpReserved; // (两者兼有)保留,必须初始化为NULL LPSTR lpDesktop; // (两者兼有)用于标识启动应用程序所在的桌面的名字。如果该桌面存在,新进程便与指定的桌面相关联。如果桌面不存在,便创建一个带有默认属性的桌面,并使用为新进程指定的名字。如果lpDesktop是NULL(这是最常见的情况),那么该进程将与当前桌面相关联。 LPSTR lpTitle; // (控制台)用于设定控制台窗口名称。如果lpTitle是NULL,则可执行文件的名字将用作窗口名 DWORD dwX; // x,y坐标,只有当子进程用CW_USEDEFAULT作为CreateWindows的x参数来创建它的第一个重叠窗口时,才使用这两个坐标。若是创建控制台窗口的应用程序,这些成员用于指明控制台窗口的左上角。 DWORD dwY; DWORD dwXSize; //(两者兼有)设定窗口宽度和长度,只有子进程用WM_USEDEFAULT作为CreateWIndows的nWidth参数来创建它的第一个重叠窗口时才是用这个值。控制台就是控制台的宽和长 DWORD dwYSize; DWORD dwXCountChars; //(控制台)用于设定子应用程序控制台的长度和宽度(字符表示) DWORD dwYCountChars; DWORD dwFillAttribute;// (控制台)用于设定子应用程序的控制台背影颜色和文本。 DWORD dwFlags; // (两者兼有)参见下一段 WORD wShowWindow; // (窗口)用于设定子应用程序初次调用ShowWindow将SW_SHOWDEFAULT作为nCmdShow参数传递时,该应用程序的第一个重叠窗口应该如何出现。本成员可以是通常用于ShowWindow函数的任何一个SW_*标识符 WORD cbReserved2; // 保留,必须初始化为0 LPBYTE lpReserved2; // 保留,必须初始化为NULL HANDLE hStdInput; // (控制台)用于设定控制台输入和输出用的缓存的句柄。默认设置hStdInput是键盘缓存,hStdOutput和hStdError窗口的缓存。 HANDLE hStdOutput; // HANDLE hStdError; } STARTUPINFO, *LPSTARTUPINFO;
设置某些值,大部分需要默认值,必须初始化为0都。
STARTUPINFO si = {sizeof(si)};
dwFlags标志,用于修改如何来创建子进程。
标志
STARTF_USESIZE 使用dwXSize和dwYSize成员
STARTF_USESHOWWINDOW 使用wShowWIndow成员
STARTF_USEPOSITION 使用dwX和dwY成员
STARTF_USECOUNTCHARS 使用dwXCountChars和dwYCountChars成员
STARTF_USEFILLATTRIBUTE 使用dwFillAttribute成员
STARTF_USESTDHANDLES 使用hStdInput,hStdOutput和hStdError成员
STARTF_RUN_FULLSCREEN 强制再x86计算机上运行的控制台应用程序以全屏幕方式启动运行
STARTF_FORCEONFEEDBACK 光标设置为沙漏,过了2秒如果进程没启动GUI,CreateProcess程序将光标设置为箭头,5秒内显示一个窗口,成功调用GetMessage则反复箭头,如果没有成功,等待5秒 变为箭头
STARTF_FORCEOFFFEEDBACK
4.2.7 ppiProcInfo
typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; } PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
返回的分别是,子进程进程句柄,子进程中主线程的线程句柄,子进程ID,子进程中主线程的ID。
注意:
- hProcess和hThread被赋值后,内核对象的计数器分别被+1。
- 系统为每个进程和线程分配的ID值都是不同的,但是当某进程退出后,新进程很可能会使用退出进程的ID。
4.3 终止进程的运行
4.3.1 主线程的进入点函数返回
最好强力推荐使用这种方式。
- 调用C++析构函数。
- 释放堆栈内存。
- 将进程退出代码(进程内核对象中维护)设置为进入点函数返回值。
- 系统将进程内核对象的返回值减去1。
4.3.2 ExitProcess函数
避免使用这个方法。
VOID ExitProcess(UINT fuExitCode);
终止进程运行,并将退出码设置为fuExitCode。
注意:
- 调用ExitProcess后,所有的代码都将不会执行,关闭进程。
- 调用ExitProcess后,不会释放C++析构函数资源,有系统清理进程时候直接释放内存。
- 进程的主线程直接return退出后,启动函数中也会调用ExitProcess函数,进程终止,其他线程也会关闭。
- 进程的主线程调用_endThreadex或者EndThread函数关闭主线程后,进程没有被关闭,子线程继续运行。
4.3.3 TerminiateProcess函数
能不用就别用。
BOOL TerminiateProcess(HANDLE hProcess, UINT fuExitCode;)
关闭指定为hProcess句柄的进程,推出代码为fuExitCode。
注意:
- 关闭进程将丢失所有需要保存到硬盘的数据,因为进程关闭时候最后会自己释放内存资源,关闭内核对象的句柄值,所以不会造成内存泄露。
- 此函数是个异步函数,它返回的时候无法知道,需要关闭的进程是否已经被强制关闭了。
4.3.4 进程终止运行时出现的情况
- 进程中剩余的所有线程全部终止运行。
- 释放该进程引用的GDI和用户对象,内核对象被关闭(别的进程有引用则计数器减1,如果没有引用则关闭内核对象)。
- 推出代码将从STILL_ACTIVE(后面章节线程将介绍该结构)改为传递给ExitProcess和TerminiateProcess代码。
- 内核对象状态变为收到通知状态(线程中介绍),其他线程挂起,知道进程终止。
- 进程内核对象计数减去1,或者关闭。
注意:
进程内核对象的寿命可能远远大于进程本身,父进程保留子进程内核对象可以查看它的推出代码调用下面函数:
BOOL GetExitCodeProcess(HANDLE hProcess, PDWORD pdwExitCode);
可以调用这个函数来判断子进程是否关闭,如果子进程没有关闭,它的STILL_ACTIVE标识符定义为0x103。但是这么作效率不是很高。
4.4 子进程
没什么好说的,前面的都说了,用CloseHandle关闭子进程和子进程中主线程的句柄值来切断父进程和子进程的所有联系。
4.5 每局系统中运行的进程
利用ToolHelp函数族来开发管理操作系统上的进程。打算自己也写个试试。
本文章的内容是本人学习Windows核心编程第四章后的总结,有错误请大家纠正,转载注明出处: