一,进程概念:
一般将进程定义成一个正在运行的程序的实例,它由以下两部分构成:
1)一个内核对象,操作系统用它来管理进程。内核对象也是系统保留进程统计信息的地方;
2)一个地址空间。其中包含所有可执行文件或DLL模块的代码和数据。此外它还包括动态内存分配,比如线程堆栈和堆的分配。
进程是有"惰性"的。进程要做任何事情,都必须让一个线程在它的上下文中运行。该线程负责执行进程地址空间包含的代码。事实上,一个进程可以有多个线程,所有线程都在进程的地址空间中"同时"地执行代码。为此,每个线程都有他自己的一组CPU寄存器和它自己的堆栈。每个进程至少要有一个线程来执行进程地址空间包含的代码。当系统创建一个进程的时候,会自动地为进程创建第一个线程,这称为主线程(primary thread)。然后,这个线程再创建更多的线程,后者再创建更多的线程……。如果没有线程要执行进程地址空间中包含的代码,进程就失去了继续存在的理由。这时,系统就会自动销毁进程及其地址空间。
二,与进程有关的信息:
1)Windows支持两种类型应用程序
GUI (Graphical User Interface)图形用户界面
CUI (Console User Interface)控制台用户界面
2) Windows应用程序的入口函数
int WINAPI WinMain( //在Visual Studio 2005下则为int APIENTRY WinMain
HINSTANCE hInstance, //应用程序当前实例的句柄
HINSTANCE hPrevInstance,//便于移植16位的Windows程序,对于一个32位程序,该参数总为NULL。
PSTR pszCmdLine, //传递给程序的命令参数,你可以使用GetCommandLine函数来使用它
int nCmdShow) // nCmdShow为窗口的显示方式,取值参考MSDN
创建窗口示例:
#include <sstream>
#include <Windows.h>
//独一无二的类名,一般用GUID字串,以免与其他程序的类名重复
static const wchar_t * CLASS_NAME =L"{198CEAB2-AD78-4ed3-B099-247639080CB0}";
/************************************************************************
回调函数,当主窗口收到任何Windows消息时被调用
************************************************************************/
LRESULT CALLBACK onMainWndMessage(HWND wnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0); //如果是“窗口销毁”事件,则应该在消息队列中投递
break; //一个WM_QUIT消息,使GetMessage()返回FALSE
default:
return DefWindowProc(wnd, msg, wParam, lParam);
}
return 0;
}
/************************************************************************
登记自己的窗口类
************************************************************************/
bool registerMyClass()
{
WNDCLASSEX wce = {0};
wce.cbSize = sizeof(wce);
wce.style = CS_VREDRAW | CS_HREDRAW;
wce.lpfnWndProc = &onMainWndMessage; //指明回调函数
wce.hInstance = GetModuleHandle(0);
wce.hIcon = LoadIcon(0, MAKEINTRESOURCE(IDI_WINLOGO));
wce.hCursor = LoadCursor(0, MAKEINTRESOURCE(IDC_ARROW));
wce.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE+1);
wce.lpszClassName = CLASS_NAME; //独一无二的类名
wce.hIconSm = wce.hIcon;
return 0 != RegisterClassEx(&wce);
}
/************************************************************************
创建并显示主窗口
************************************************************************/
bool createMyWindow(int cmdShow)
{
HWND mainWnd = CreateWindowEx(0, CLASS_NAME, L"Demo", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, GetModuleHandle(0), 0);
if (0 != mainWnd) {
ShowWindow(mainWnd, cmdShow);
UpdateWindow(mainWnd);
return true;
} else {
return false;
}
}
/************************************************************************
消息循环
************************************************************************/
int messageLoop()
{
MSG msg;
while (GetMessage(&msg, 0, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return static_cast<int>(msg.wParam);
}
/************************************************************************
WinMain,程序入口
************************************************************************/
int WINAPI WinMain(HINSTANCE, HINSTANCE, char *, int cmdShow)
{
if (registerMyClass() && createMyWindow(cmdShow))
{
return messageLoop();
} else {
std::ostringstream msg;
msg << "创建主窗口失败,错误代码:" << GetLastError();
MessageBoxA(0, msg.str().c_str(), 0, MB_OK | MB_ICONSTOP);
return 0;
}
}
int _main(
int argc,
TCHAR *argv[],
TCHAR *envp[])
创建控制台示例:
#include <iostream>
#include <windows.h>
#include <tchar.h>// #define _tmain main
int _tmain(int argc,TCHAR *argv[],TCHAR *envp[])
{
getchar();
return 0;
}
Windows系统中进程有很多的相关信息,主要的有以下几种:进程实例句柄、进程的命令行、进程的环境变量、进程当前所在的驱动器和目录。
三, Windows系统中进程有很多的相关信息
主要的有以下几种:进程实例句柄、进程的命令行、进程的环境变量、进程当前所在的驱动器和目录。
1)进程实例句柄:
加载到进程地址空间的每一个可执行文件或者DLL文件都被赋予了一个独一无二的实例句柄。可执行文件的实例被当作WinMain()函数的第一个参数hInstance传入。在需要加载资源的函数调用中,一般都需要提供此句柄的值。例如,为了从可执行文件的映像中加载一个图标资源,就需要调用下面这个函数:
HICON LoadIcon(HINSTANCE hInstance, // 想要加载图标资源的 句柄
PCTSTR pszICON);
LoadIcon函数的第一个参数指出哪个文件(可执行文件或DLL文件)包含了想要加载的资源。
WinMain的hInstance参数的实际值是一个内存基地址;系统将可执行文件的映像加载到进程地址空间中的这个位置。例如,假如系统打开可执行文件,并将它的内容加载到地址0x00400000,则WinMain的hInstance参数值为0x00400000.
可执行文件的映像具体加载到哪一个基地址,是由链接器决定的。不同的链接器使用不同的默认基地址。使用Microsoft链接器的/BASE:address链接器开关,可以更改要将应用程序加载到哪个基地址。
可执行文件或DLL文件被加载到进程地址空间的什么位置HMODULE GetModuleHandle(PCTSTR pszModule); //返回一个句柄/基地址:
调用函数时,需要传递一个以0终止的字符串,它指定了已在主调进程的地址空间中加载的一个可指定文件或DLL文件的名称。如果系统找到了指定的可执行文件或DLL文件的名称,GetModuleHandle就会返回可执行文件/DLL文件映像加载到的基地址。如果传递的参数是NULL的话,可以直接返回主调进程的可执行文件的基地址。
注:进程地址空间中存储了可执行文件的映像,这其中包含了代码、数据、还有集成到可执行文件中的一些资源。实例句柄是一个指向可执行文件在进程地址空间中地址的指针,所以LoadIcon之类的函数才可以通过实例句柄得到资源进行加载。注:验证了一下,调用GetModuleHandle(NULL)返回的实例句柄与WinMain函数的hInstance参数是相同的。
示例:
#define DLLWITHGETMODULE extern "C" __declspec(dllexport)
#include <Windows.h>
#include <stdio.h>
#include <tchar.h>
extern "C" const IMAGE_DOS_HEADER __ImageBase; //伪变量,指向当前正在运行的模块的基地址
void DumpModule()
{
HMODULE hModule = GetModuleHandle(NULL);//得到当前正在运行的应用程序的基地址
_tprintf(TEXT("with GetModuleHandle(NULL) = 0x%x\r\n"),hModule);
_tprintf(TEXT("with __ImageBase = 0x%x\r\n"),(HINSTANCE)&__ImageBase);//使用伪变量得到当前正在运行模块的基地址
//传入DumpModule函数的地址给GetModuleHandleEx函数,得到DumpModule函数所在模块的基地址
hModule = NULL;
::GetModuleHandleEx(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(PCTSTR)DumpModule,
&hModule);
_tprintf(TEXT("with GetModuleHandleEx = 0x%x\r\n"),hModule);
}
int _tmain()
{
DumpModule();
getchar();
return 0;
}
2)进程的命令行: 系统在创建一个新进程的时候,总是会传递一个命令行给它。这个命令行几乎总是非空的;至少,用于创建新进程的可执行文件的名称是命令行上的第一个标记。
可以在任何需要的地方调用GetCommandLine()来获得一个指向进程完整命令行的指针。
例如: LPWSTR str=GetCommandLine();
_tprintf(str);
3)进程的环境变量:
环境变量的定义:环境变量是一个string组成的array。它是计算机的一系列设置(setting),环境变量用以指定文件的搜索路径、临时文件目录、特定应用程序(application-specific)的选项和其他类似信息。默认情况下,每个进程(process)的环境变量拷贝自父进程。当然父进程也能够为子进程指定不同的环境变量。
环境变量是进程中一组变量信息,环境变量分为系统环境变量、用户环境变量和进程环境变量。系统有全局的环境变量,在进程创建时,进程继承了系统的全局环境变量、当前登录用户的用户环境变量和父进程的环境变量。进程也可以有自己的环境变量。
函数示例
1)GetEnvironmentStrings函数用于获取所有环境变量字符串:
LPTCH WINAPI GetEnvironmentStrings(void);
返回值:
成功时,返回指向保存环境变量的缓冲区;
失败时,返回值为NULL。
2)FreeEnvironmentStrings函数用来释放由GetEnvironmentStrings返回的内存块:
BOOL WINAPI FreeEnvironmentStrings(
__in LPTCH lpszEnvironmentBlock
);
返回值:
成功时,返回非零值;
失败时,返回零值,可调用GetLastError()查看进一步错误消息。
3)GetEnvironmentVariable函数用于获取指定的环境变量:
DWORD WINAPI GetEnvironmentVariable(
__in_opt LPCTSTR lpName, //环境变量名
__out_opt LPTSTR lpBuffer, //指向保存环境变量值的缓冲区
__in DWORD nSize //缓冲区大小(字符数)
);
返回值:
成功时,返回真实的环境变量值大小,不包括null结束符;
如果lpBuffer大小不足,则返回值是实际所需的字符数大小,lpBuffer内容未定义;
失败时,返回0;如果指定的环境变量找不到,GetLastError()返回ERROR_ENVVAR_NOT_FOUND。
4)SetEnvironmentVariable函数用于设置指定的环境变量:
BOOL WINAPI SetEnvironmentVariable(
__in LPCTSTR lpName, //环境变量名,当该值不存在且lpValue不为NULL时,将创建一个新的
__in_opt LPCTSTR lpValue //环境变量值
);
返回值:
成功时,返回非零值;
失败时,返回零值,调用GetLastError()查看具体的错误信息。
该函数对系统环境变量以及其他进程的环境变量不起作用!
示例 1:
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
int _tmain()
{
LPTSTR lpszVariable;
LPTCH lpvEnv;
//获得环境变量内存块的指针
lpvEnv = GetEnvironmentStrings();
if(lpvEnv == NULL)
{
printf("GetEnvironmentStrins failed(%d)/n", GetLastError());
return 0;
}
//环境变量字符串是以NULL分隔的,内存块以NULL结尾
lpszVariable = (LPTSTR)lpvEnv;
while(*lpszVariable)
{
_tprintf(TEXT("%s\n"), lpszVariable);
lpszVariable += lstrlen(lpszVariable) + 1; //移动指针
}
FreeEnvironmentStrings(lpvEnv);
system("pause");
return 1;
}
示例 2:
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>
#include <strsafe.h>
#define BUFSIZE 4096
int _tmain()
{
TCHAR chNewEnv[BUFSIZE];
LPTSTR lpszCurrentVariable;
DWORD dwFlags = 0;
TCHAR szAppName[] = TEXT("notepad");
STARTUPINFO si;
PROCESS_INFORMATION pi;
BOOL fSuccess;
//将环境变量字符串拷贝到环境变量内存块中
lpszCurrentVariable = (LPTSTR)chNewEnv;
if(FAILED(StringCchCopy(lpszCurrentVariable, BUFSIZE, TEXT("AsceSetting=Luffy"))))
{
printf("String copy failed/n");
}
lpszCurrentVariable += lstrlen(lpszCurrentVariable) + 1;
if(FAILED(StringCchCopy(lpszCurrentVariable, BUFSIZE, TEXT("AsceVersion=2.0"))))
{
printf("String copy failed/n");
}
//使环境变量内存块以NULL结尾
lpszCurrentVariable += lstrlen(lpszCurrentVariable) + 1;
*lpszCurrentVariable = (TCHAR)0;
//创建子进程,指定一个新的环境变量内存块
SecureZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
#ifdef UNICODE
dwFlags = CREATE_UNICODE_ENVIRONMENT;
#endif
fSuccess = CreateProcess(NULL, szAppName, NULL, NULL,
TRUE, dwFlags, (LPVOID)chNewEnv, //新的环境变量内存块
NULL, &si, &pi);
if(!fSuccess)
{
printf("CreateProcess failed(%d)/n", GetLastError());
}
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
printf("新进程的ID号:%d\n",pi.dwProcessId);
printf("新进程的主线程ID号:%d\n",pi.dwThreadId);
WaitForSingleObject(pi.hProcess, INFINITE);
system("pause");
return TRUE;
}
4)进程当前所在的驱动器目录:
如果不提供完整的目录,各种Windows函数会在当前驱动器的当前目录查找文件和目录。例如,如果进程中的一个线程调用CreateFile来打开一个文件(未指定完整路径名),系统将在当前驱动器和目录查找该文件。
一个线程可以调用以下两个函数来获取和设置其所在进程的当前驱动器和目录:
DWORD GetCurrentDirectory(
DWORD cchCurDir,
PTSTR pszCurDir);
BOOL SetCurrentDirectory(PCTSTR pszCurDir);
使用SetCurrentDirectory设定好进程当前驱动器目录之后,就相当于重新设定了系统的当前目录,之后调用各种需要路径的函数,只要路径没有填写完整,都会到指定的位置去进行文件或目录操作。
示例:
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>
int _tmain()
{
TCHAR *path = new TCHAR[MAX_PATH];
ZeroMemory(path, MAX_PATH);
GetCurrentDirectory(MAX_PATH, path);
_tprintf("CurrentDirectory:%s\n",path);
GetModuleFileName(NULL,path,MAX_PATH);
_tprintf("ModuleFileName:%s\n",path);
system("pause");
return TRUE;
}
如果:SetCurrentDirectory(_T("C:\\"));
则
TCHAR *path = new TCHAR[MAX_PATH];
ZeroMemory(path, MAX_PATH);
GetCurrentDirectory(MAX_PATH, path); //得到的是 C:\
四,CreateProcess 函数
BOOL CreateProcess(
PCTSTR pszApplicationName, //可执行文件的名称
PTSTR pszCommandLine, //传给新进程的命令行字符串
PSECURITY_ATTRIBUTES psaProcess, //进程对象安全属性
PSECURITY_ATTRIBUTES psaThread, //线程对象安全属性
BOOL bInheritHandles, //指定继承性
DWORD fdwCreate, //标识影响新进程创建方式的标志
PVOID pvEnvironment, //指向新进程要使用的环境字符串
PCTSTR pszCurDir, //允许父进程指定子进程的当前驱动器和目录
PSTARTUPINFO psiStartInfo, //指向一个STARTUPINFO结构或STARTUPINFOEX结构
PPROCESS_INFORMATION ppiProcInfo); //指向一个 PROCESS_INFORMATION结构
参数说明:
1> lpApplicationName:指向一个NULL结尾的、用来指定可执行模块的字符串。这个字符串可以是可执行模块的绝对路径,也可以是相对路径,在后一种情况下,函数使用当前驱动器和目录建立可执行模块的路径。
这个参数可以被设为NULL,在这种情况下,可执行模块的名字必须处于 lpCommandLine 参数的最前面并由空格符与后面的字符分开。
2> lpCommandLine:指向一个以NULL结尾的字符串,该字符串指定要执行的命令行。这个参数可以为空,那么函数将使用lpApplicationName参数指定的字符串当做要运行的程序的命令行。
如果lpApplicationName和lpCommandLine参数都不为空,那么lpApplicationName参数指定将要被运行的模块,lpCommandLine参数指定将被运行的模块的命令行。新运行的进程可以使用GetCommandLine函数获得整个命令行。C语言程序可以使用argc和argv参数。
如果lpApplicationName参数为空,那么这个字符串中的第一个被空格分隔的要素指定可执行模块名。
如果文件名不包含扩展名,那么.exe将被假定为默认的扩展名。
如果文件名以一个点(.)结尾且没有扩展名,或文件名中包含路径,.exe将不会被加到后面。
如果文件名中不包含路径,Windows将按照如下顺序寻找这个可执行文件:
1).当前应用程序的目录
2).父进程的目录。
3).Windows系统目录
4).Windows目录。可以使用GetWindowsDirectory函数获得这个目录。
5).列在PATH环境变量中的目录。
LPTSTR szCmdline = _tcsdup(TEXT("c:\\test.bat"));//用szCmdline做CreateProcess第2参数,VS2008测试通过
3> psaProcess:
指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的句柄可以被子进程继承。如果psaThread参数为空(NULL),那么句柄不能被继承。
在Windows NT中:SECURITY_ATTRIBUTES结构的lpSecurityDescriptor成员指定了新进程的安全描述符,如果参数为空,新进程使用默认的安全描述符。
在Windows95中:SECURITY_ATTRIBUTES结构的lpSecurityDescriptor成员被忽略。
4> psaThread:
指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的指向线程的句柄可以被子进程继承。如果psaThread参数为空(NULL),那么句柄不能被继承。
在Windows NT中,SECURITY_ATTRIBUTES结构的lpSecurityDescriptor成员指定了主线程的安全描述符,如果参数为空,主线程使用默认的安全描述符。
在Windows95中:SECURITY_ATTRIBUTES结构的lpSecurityDescriptor成员被忽略。
5> bInheritHandles:
指示新进程是否从调用进程处继承了句柄。
6> fdwCreate:
指定附加的、用来控制优先类和进程的创建的标志。
7> pvEnvironment:
指向一个新进程的环境块。如果此参数为空,新进程使用调用进程的环境。
一个环境块存在于一个由以NULL结尾的字符串组成的块中,这个块也是以NULL结尾的。每个字符串都是name=value的形式。
因为相等标志被当做分隔符,所以它不能被环境变量当做变量名。
与其使用应用程序提供的环境块,不如直接把这个参数设为空,系统驱动器上的当前目录信息不会被自动传递给新创建的进程。对于这个情况的探讨和如何处理,请参见注释一节。
8> pszCurDir:
指向一个以NULL结尾的字符串,这个字符串用来指定子进程的工作路径。这个字符串必须是一个包含驱动器名的绝对路径。如果这个参数为空,新进程将使用与调用进程相同的驱动器和目录。这个选项是一个需要启动应用程序并指定它们的驱动器和工作目录的外壳程序的主要条件。
9> psiStartInfo:
指向一个用于决定新进程的主窗体如何显示的STARTUPINFO结构体。
10> ppiProcInfo:
指向一个用来接收新进程的识别信息的PROCESS_INFORMATION结构体。
五,终止进程
1) 进程可以通过以下4种方式终止:
主线程的入口点函数返回(强烈推荐的方式);
进程中的一个线程调用ExitProcess函数(要避免这种方式);
另一个进程中的线程调用TerminateProcess函数(要避免这种方式);
进程中的所有线程都“自然死亡”(这种情况几乎从来不会发生);
2)主线程的入口点函数返回
设计一个应用程序时,应该保证只有在主线程的入口点函数返回之后,这个应用程序的进程才终止。只有这样才能保证主线程的所有资源都被正确清理:(一下操作将被执行)
该线程创建的任何C++对象都将由这些对象的析构函数正确销毁;
操作系统将正确释放线程栈使用的内存;
系统将进程的退出代码(在进程内核对象中维护)设为入口点函数的返回值;
系统递减进程内核对象的使用计数;
3)ExitProcess函数
进程中的某个线程调用 ExitProcess 函数来终止进程:
VOID ExitProcess(UINT fuExitCode);
该函数将终止进程,并将进程的退出代码设为 fuExitCode。 ExitProcess 不会返回值,因为进程已经终止了。
4)TerminateProcess函数
调用 TerminateProcess 也可以终止一个进程:
BOOL TerminateProcess(
HANDLE hProcess, //要终止的进程的句柄
UINT fuExitCode); //
与 ExitProcess 的一个明显区别是:任何线程都可以调用 TerminateProcess 来终止另一个进程或者它自己的进程。
当一个进程中的所有线程都终止了,操作系统就认为没有任何理由再保持进程的地址空间,就会终止这个进程。
5)当进程终止运行时
一个进程终止时,系统会一次执行以下操作:
终止进程中遗留的任何线程;
释放进程分配的所有USER对象和GDI对象,关闭所有内核对象(如果使用计数减为0,销毁内核对象);
进程的退出代码从STILE_ACTIVE变为传给ExitProcess或TerminateProcess函数的代码;
进程内核对象的状态变为已触发状态;
进程内核对象的使用计数递减1;
六,子进程
像创建多线程程序那样,有时我们需要一个新的进程来帮助我们完成工作,与新线程相比,新进程使用与主进程独立的地址空间,因此不会对主进程的地址空间产生影响,为了在处理工作期间保护主进程的地址空间,使用子进程比子线程更好一些。
通过同步机制,我们可以在主进程的某个线程中等待子进程执行完毕之后再进行操作。
遗憾的是,新进程可能需要操作我们的地址空间中的数据。在这种情况下,最好让进程在它自己的地址空间中运行,并只允许它访问父进程地址空间中与它的工作有关的数据,从而保护与正在进行的处理无关的其他数据。Windows提供了几种不同的方式在不同进程间传递数据,其中包括动态数据交换(Dynamic Data Exchange, DDE),OLE,管道邮件槽等。共享数据的方法之一是内存映射文件。
注:以上说明了何时需要创建子进程来帮助我们完成工作。子进程相对于线程来说不会影响到父进程地址空间中的数据