第四章 进程
1、windows支持两种应用程序:GUI程序和CUI程序,即图形用户界面程序和控制台应用程序。
在Visual Studio中,可以使用项目属性的连接器开关设置选择哪种程序,/SUBSYSTEM:CONSOLE和/SUBSYSTEM:WINDOWS
当运行应用程序时操作系统会的加载程序会检查这个可执行文件的头,并获取这个子系统值。
我们知道,main函数之前的全局变量和全局对象会在进入main函数之前被初始化,这是因为在进入main函数之前还有一个真正的更早的入口点,这个入口函数由两个因素决定:①CUI还是GUI, ②Unicode还是ANSI字符
ANSI+GUI--_tWinMain---WinMainCRTStartup
UNICODE+GUI--_tWinMain---wWinMainCRTStartup
ANSI+CUI---_tmain---mainCRTStartup
UNICODE+CUI---_tmain-----wmainCRTStartup
1 #include "stdafx.h"
2
3 #include <iostream>
4
5 using namespace std;
8
9 int _tmain(int argc, _TCHAR* argv[], _TCHAR *enviro[]) 10
11 { 12
13 cout<<"windows版本_osver="<<_osver<<endl; 14
15 cout<<"十六进制表示的windows主版本号_winmajor="<<_winmajor<<endl; 16
17 cout<<"十六进制表示的windows次版本号_winminor="<<_winminor<<endl; 18
19 cout<<"_winver"<<_winver<<endl; //前四个为单条下划线
20
21 cout<<"参数个数__argc="<<__argc<<endl; 22
23
24
25 for (int i = 0; i < __argc; ++ i)
27 { 28
29 cout<<"参数argv["<<i<<"]="<<argv[i]<<endl; 30
31 } 32
33 for(int j = 0; j < sizeof(enviro) / sizeof(_TCHAR*); ++ j)
35 { 36
37 cout<<"环境变量environ["<<j<<"]="<<enviro[j]<<endl; 38
39 } 40
41 // cout<<"_pgmptr="<<_pgmptr<<endl; //报错;
42
43 return 0; 44
45 }
用上面这些C运行库全局变量直接来访问不被鼓励,在visual studio 2005中,上面的代码被警告为:
警告 1 warning C4996: '_osver': This function or variable may be unsafe. Consider using _get_osver instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. c:\users\cuish\documents\visual studio 2005\projects\windowscore4\windowscore4\windowscore4.cpp 10
2、进程实例句柄
加载到进程地址空间中的每个可执行文件或者DLL都被赋予一个独一无二的实例句柄,可执行文件的实例被当作(w)WinMain函数的一个参数hInstanceExe传入。
HMODULE和HINSTANCE等价,可以互相代替。
(w)WinMain的hInstanceExe参数的实际值是一个内存基地址,系统将可执行文件的映像加载到进程地址空间中的这个位置,visual studio默认的基地址为0x00400000,这是在运行windows98时可执行文件的映像能加载到的最低一个地址,使用microsoft 连接器开关/BASE:address链接器开关,可以更改要将应用程序加载到哪个基地址。
怎么知道一个可执行文件或DLL被加载到了进程地址空间中的哪个位置呢?
HMODULE GetModuleHandle(PCTSTR pszModule);
pszModule为一个可执行文件或DLL文件名称,如果pszModule传入NULL,则函数返回主调进程的可执行文件的基地址。 注意这里是主调进程!
如果在DLL想知道当前正在运行在哪个【模块】中,有两种方法:
① 使用链接器提供的伪变量__ImageBase,它指向正在运行的模块的基地址
② 调用GetModuleHandleEx,将GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS作为它的第一个参数,将当前函数地址作为第二个参数传入,最后一个输出参数为一个指向HMODULE的指针。
1 //xx.dll
2
3 void TestModule() 4 { 5
6 HMODULE hModule = NULL; 7
8 GetModuleHandleEx( 9
10 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, 11
12 (PCTSTR)TestModule, 13
14 &hModule 15
16 );
18
19 //取得的hModule应该和在主调进程中调用GetDoduleHandle(NULL)结果一致;
20
21 }
注意:上面提到的【模块】,指的可以是exe,也可以是DLL ? 待定
DLL中的导出函数如下:
///////////////////////////////
1 #include "stdafx.h"
2
3 #include "testmoduleHeader.h"
4
5 extern "C" _declspec(dllexport) HMODULE TestModule() 6 { 7
8 HMODULE hModule = NULL; 9
10 GetModuleHandleEx( 11
12 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, 13
14 (PCTSTR)TestModule, 15
16 &hModule 17
18 ); 19
20 return hModule; 21
22 } 23
24
25
26 extern "C" _declspec(dllexport) HMODULE GetModule() 27 { 28
29 return GetModuleHandle(NULL); 30
31 } 32
33 ////////////////////////////////////////////////////
34
35
36 #ifndef _TEST_MODULE_HEADER_ 37
38 #define _TEST_MODULE_HEADER_
39
40 #include "windows.h"
41
42
43
44 extern "C" _declspec(dllimport) HMODULE TestModule(); 45
46 extern "C" _declspec(dllimport) HMODULE GetModule(); 47
48 #endif
49
50
51 ///////////////////////////////////////////////
主调进程如下:
可以看到上面的代码运行结果:DLL中的GetModuleHandleEx的结果并不和主调进程中的GetModuleHandle(NULL)一致,而DLL中的函数GetModuleHandle(NULL)和主调进程中的GetModuleHandle(NULL)结果一致。
3、C/C++启动代码总是向(w)WinMain的hPrevInstance的参数传入NULL,这是为了兼容16为windows应用程序。
1 Int WINAPI _tWinMain( 2
3 HINSTANCE hInstanceExe, 4
5 HINSTANCE, //hPrevInstance
6
7 PSTR pszCmdLine, 8
9 Int nCmdShow 10
11 );
这里没有为hPrevInstance指定参数名,所以编译器不会报告“没有引用hPrevInstance”的警告。
在Visual Studio中,向导生成的C++ GUI项目中,利用了UNREFERENCED_PARAMETER宏来消除“没有引用hPrevInstance”的警告。
UNREFERENCED_PARAMETER(hPrevInstance); //消除此种警告;
在C运行库的启动代码开始执行一个GUI程序时,会调用windows函数GetCommandLine来获取进程的完整命令行,忽略可执行文件的名字,然后将剩余部分的一个指针传给WinMain的pszCmdLine参数。
pszCmdLine是可写的,但是不应该这么做,如果这么做了,GetCommandLine得到的结果就为修改后的。这么做不科学。
PTSTR GetCommandLine();
使用在ShellAPI.h中声明在Shell32.dll中导出的函数CommandLineToArgvW可以将任何Unicode字符串分解成单独的标记:
PWSTR *CommandLineToArgvw(
PWSTR pszCmdLine, //in, 命令行
int *pNunArgs //分解后的参数个数,out
);
有返回值可以看出,该函数返回一个字符串指针数组,数组中每个元素都是一个字符串(字符指针),即
1 #include "stdafx.h"
2
3 #include "windows.h"
4
5 #include <iostream>
6
7 using namespace std; 8
9
10 int _tmain(int argc, _TCHAR* argv[]) 11 { 12
13 int nums = 0; 14
15 PWSTR *ppArgv = CommandLineToArgvW(GetCommandLineW(), &nums); 16
17
18
19 for (int i = 0; i < nums; ++ i) 20 { 21
22 wcout<<ppArgv[i]<<endl; 23
24 } 25
26 return 0; 27
28 }
需要注意的是:该函数在内部分配内存,在外部手动释放的话用HeapFree(GetProcessHeap(), 0, ppArgv);
4、进程的环境变量
1 #include "windows.h"
2
3 #include <iostream>
4
5 using namespace std; 6
7
8 int _tmain(int argc, _TCHAR* argv[]) 9 { 10
11 LPTSTR pEnvBlock = GetEnvironmentStrings(); //函数内部分配内存;
12
13
14
15 cout<<"环境变量"<<pEnvBlock<<endl; 16
17 wcout<<pEnvBlock<<endl; 18
19
20 FreeEnvironmentStrings(pEnvBlock); //释放由GetEvnironmentStrings函数内存申请的内存;
21
22
23 return 0; 24
25 }
环境变量用等号隔开名字和值,形如:name=value
如果对于空格是不会忽略的,也就是说name =value和name= value是不同的名字和变量,前者名字为”name ”,值为”value”; 后者名字为”name”,值为” value”。
用户在登录windows的时候,系统会创建外壳(shell)进程,并将一组环境变量字符串与其关联,系统通过检查注册表中的两个注册表项来获得初始化的环境字符串。
环境变量分为用户环境变量和系统环境变量。
系统:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Evnironment
用户:HEKY_CURRENT_USER\Environment
修改了环境变量后为了能够让所有进程(正在运行的程序)生效,用户必须注销并重新登录windows,有的应用程序则可通过另外的方式就可以获得新的环境变量,比如资源管理器,任务管理器和控制面板等,这些程序可以在其窗口收到WM_SETTINGCHANGE消息时用新的注册表项来更新他们的环境块。
SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)TEXT(“Environment”)); //这里的lParam是固定的字符串。
子进程可以进程父进程的环境块,这里的继承指的是子进程得到父进程环境块的副本,同时父进程还可以控制哪些可以让子进程继承,哪些不能继承。
所以子进程在自己继承到的环境块中添加删除修改不会影响到父进程的环境块。
环境变量
1 DWORD GetEnvironmentVariable( 2
3 PCTSTR pszName, //环境变量名,如path
4
5 PTSTR pszValue, //用来接收环境变量值的缓冲区
6
7 DWROD cchValue //该缓冲区的长度,字符
8
9 );
当给pszValue传NULL,给cchValue传0时,函数将返回需要的缓冲区字符数(包括\0),而当不给这两个参数传NULL和0时函数成功返回环境变量字符串的长度,不包括\0,所以当前后两次用不同的参数调用此函数时,它的返回值第二次要比第一次的返回值小1.
1 DWORD dwChs = GetEnvironmentVariable(_T("path"), NULL, 0); //包含’\0’
2
3 if (0 != dwChs) 4 { 5
6 LPTSTR pszBuffer = new TCHAR[dwChs]; 7
8 memset(pszBuffer, 0, dwChs * sizeof(TCHAR)); 9
10
11 dwChs = GetEnvironmentVariable(_T("path"), pszBuffer, dwChs);//不含’\0’
12
13 wcout<<pszBuffer<<endl; 14
15
16 delete[] pszBuffer; 17
18 }
第二次得到的dwChs要比第一次得到的dwChs小1。
展开字符串
1 DWORD ExpandEnvironmentStrings( 2
3 PCTSTR pszSrc, //包含可替换字符串的串
4
5 PTSTR pszDes, //用来接收展开后的字符串的缓冲区
6
7 DWORD chSize //目的缓冲区的最大长度(字符数)
8
9 );
和GetEnvironmenVariable函数一样,这个函数也可以调用两次,第一次获得需要的需要的缓冲区的字符串字符个数长度,第二次调用去真正的展开。
PS:windows有很多函数的参数是字符个数和字节数,这个要注意区分,另外有的输入和输出可能包含’\0’,也可能不包含’\0’,因此为了避免花费精力去记这些东西,可以把所有情况都当作最坏的情况来考虑。
删除、修改、添加一个环境变量
1 BOOL SetEnvironmentVariable( 2
3 PCTSTR pszName, //环境变量名
4
5 PCTSTR pszvalue //环境变量值
6
7 );
删除:pszValue输入NULL
修改: 按照pszValue的值来修改
增加:如果pszName不存在则修改
5、进程的关联性
5.1
处理器关联性:进程中的线程可以在主机的任何CPU上运行,也可以强迫线程在可用CPU的一个子集上运行,称之为【处理器关联性】。
当使用QueryPerformanceFrequency和QueryPerformanceCounter来精确计时时,可能会有个副作用,那就是计算起点的CPU和计算终点的CPU不是同一个,这就会造成错误,因为两者不是同一个CPU,所以导致计时不准,因此需要把该线程控制在某一个CPU上运行,
DWORD SetThreadAffinityMask(HANDLE hThread, DWORD dwMaks);
第一个参数hThread的线程句柄,GetCurrentThread()可以获得,dwMask是掩码,如下:
0x00000001:运行在CPU1上
0x00000003: 运行在CPU1和CPU2上
即第几位为1就让该线程运行在哪些CPU上,这里的CPU指的是逻辑内核,而非物理。
当函数成功时返回在此次设置之前该线程的CPU掩码。
当不再需要限制的时候,可以选择将原来的掩码设置给该线程。
5.2 进程的错误模式
每个进程都关联了一组关于错误处理方式的标志,这些标志的作用就算告诉系统如何处理进程的错误。
UINT SetErrorMode(UINT fuErrorMode);
--------------------------------------------------
fuErrorMode的取值如下,可以为任意组合:
SEM_FAILCRITICALERRORS:系统不显示严重错误处理程序
SEM_NOGPFAULTERRORBOX:系统不显示常规保护消息框
SEM_NOOPENFILEERRORBOX:系统查找文件失败时不显示消息框
SEM_NOALLGNMENTFAULTEXCEPT:系统自动修复内存对齐错误并使程序看不到这些错误
默认情况下,子进程会继承父进程的错误模式标志。
5.3 进程当前所在驱动器和目录
我们在用vs编写程序的时候会有一个体验,当我们用到一个dll,我们在调试程序的时候应该把该dll放在什么位置呢? 书上说:可以放在工程目录下,也可以放在system32下。
而工程目录下又是哪呢? 当我们不确定的时候我们一般会在exe目录下,源码下等各个目录下都放一份,后来我们注意到了,F5或CTRL+F5的时候需要把dll放在源码目录下,而当我们学到了进程的的当前所在驱动器和目录后我们就明白了,当用函数会获取当前进程的默认目录后,我们发现其实就是源码所在目录,而当我们去debug或release目录下双击exe运行的时候发现又找不到dll了,原因就是此时该进程的当前目录默认就是exe同目录了,所以知道了这些我们就明白这个进程当前目录的意义了。
我们在一个程序中如果用到了文件名而又没有指定全路径的话,程序就会从当前驱动器的当前目录来查找该文件。
当前驱动器和目录是相对于进程来说的,也就是说进程中的任何一个线程如果改变了当前进程的所在目录,那么所有线程就会用新的进程目录。
获取和设置当前驱动器和目录:
DWORD GetCurrentDirectory(
DWORD cchCurDir, //接收当前驱动器和目录字符串的缓冲区大小(字符个数)
PTSTR pszCurDir //接收缓冲区
);
当目的缓冲区大小不够大时函数会返回接收此路径的缓冲区的长度(字符个数,包括’
\0’),当然,因为windows规定路径长度不会超过MAX_PATH(260,字符个数),所以如果目的缓冲区为MAX_PATH大小则非常安全。
1 DWORD dwChs = GetCurrentDirectory(0, NULL); 2
3 PTSTR pszBuffer = new TCHAR[dwChs]; 4
5 memset(pszBuffer, 0, dwChs * sizeof(TCHAR)); 6
7 dwChs = GetCurrentDirectory(dwChs, pszBuffer); 8
9 wcout<<pszBuffer<<endl; 10
11 delete[] pszBuffer;
。
系统跟踪记录着进程的当前驱动器和目录,但系统并没有记录每个驱动器的当前目录,利用进程的环境字符串来记录每个驱动器的当前目录。 如下:
=C:=C:\Utility\Bin
=D:=D:\Program Files
如果用CreateFile函数来打开一个文件D:\readme.txt,系统就会找环境变量=D: ,该变量存在,于是从 D:\Program Files这个目录下找readme.txt,如果变量=D:不存在,系统就会试着从D盘根目录打开readme.txt。 windows的文件操作函数不会添加或更改驱动器号环境变量,他们只是乖乖的读取而已。
如果一个父进程创建了一个希望传给子进程的环境块,子进程的环境块就不会自动继承父进程的当前目录,而子进程的的默认当前目录为每个驱动器的根目录,如果父进程希望子进程继承父进程的当前目录,那么父进程就必须在生成子进程之前来创建这些驱动器号环境变量,并把他们添加到环境块中。
父进程可以通过调用GetFullPathName来获得它的当前目录:
1 DWORD GetFullPathName( 2
3 PCTSTR pszFile, //盘符
4
5 DWORD cchPath, //接收缓冲区大小,字符数
6
7 PTSTR psPath, //接收缓冲区
8
9 PTSTR *ppsFilePart // 10
11 );
例如:想要获得D驱动器的当前目录,如下:
1 DWORD dwChs = GetCurrentDirectory(0, NULL); //此程序位于D盘
2
3 DWORD dwSize = dwChs; 4
5
6 PTSTR pszBuffer = new TCHAR[dwSize]; 7
8 memset(pszBuffer, 0, dwChs * sizeof(TCHAR)); 9
10 dwChs = GetCurrentDirectory(dwChs, pszBuffer); 11
12 wcout<<pszBuffer<<endl; 13
14
15 SetCurrentDirectory(_T("E:\MyPracticeJava"));//设置为E盘目录下
16
17 TCHAR tBuffer[MAX_PATH] = {0}; 18
19 GetCurrentDirectory(MAX_PATH, tBuffer); 20
21 wcout<<tBuffer<<endl; 22
23 delete[] pszBuffer; 24
25
26 TCHAR buffer[MAX_PATH] = {0}; 27
28 DWORD length = GetFullPathName(_T("D:"), MAX_PATH, buffer, NULL); //D盘目录没变,改变驱动器E的当前目录没有影响驱动器D的当前目录;
29
30
31
32 wcout<<buffer<<endl;
http://wenwen.soso.com/z/q335992115.htm各种路径