进程
进程要做任何事情,必须让一个线程在它的上下文运行。该线程负责执行进程地址空间包含的代码。每个进程至少要有一个线程来执行进程地址空间包含的代码。当系统创建一个进程的时候,会自动为进程创建第一个线程,这称为主线程(primary thread)。
对于所有要运行的线程,操作系统会轮流为每个线程调度一些CPU时间。会采取round-robin的方式。
两部分
一个内核对象
一个地址空间
Windows程序
Windows支持两种类型的应用程序:GUI程序和CUI程序。前者是图形用户界面,后者是控制台用户界面。这两种应用程序的界限是模糊的。
用Microsoft Visual Studio来创建一个应用程序项目的时候,继承开发环境会设置各种链接器开关。
对于CUI程序,这个链接器开关是/SUBSYSTEM:CONSOLE
对于GUI程序,这个链接器开关是/SUBSYSTEM:WINDOWS
操作系统的加载程序会检查可执行文件映像的文件头,并获取这个子系统值。如果此值表明是一个CUI程序,加载程序会自动确保有一个可用的文本台窗口,如果此值表明是一个GUI程序,加载器就不会创建控制台窗口。
Windows程序必须有一个入口点函数,应用程序开始运行时,这个函数会被调用。
C/C++采用下面两种入口点函数
Int WINAPI_tWinMain(
HINSTANCE hInstanceExe,
HINSTANCE,
PTSTR pszCmdLine,
int nCmdShow);
int _tmain(
int argc,
TCHAR *argv[],
TCHAR *envp[]);
具体的符号取决于是否使用Unicode字符串,操作系统并不调用入口点函数。相反,它会调用C/C++运行库并在链接时使用-entry:命令行选项来设置的一个C/C++运行时启动函数。该函数将初始化C/C++运行库,使我们能调用malloc和free之类的函数。还确保了在我们的代码开始执行之前,我们声明的任何全局和静态C++对象都被正确的构造。
应用程序类型 | 入口点函数(入口) | 嵌入可执行文件的启动函数 |
处理ANSI字符和字符串的GUI应用程序 | _tWinMain(Winmain) | WinMainCRTStartup |
处理Unicode字符和字符串的GUI应用程序 | _tWinMain(w Winmain) | wWinMainCRTStartup |
处理ANSI字符和字符串的CUI应用程序 | _tmain(Main) | mainCRTStartup |
处理Unicode字符和字符串的CUI应用程序 | _tmain(Wmain) | wmainCRTStartup |
所有C/C++运行库启动函数所做的事情基本都是一样的,区别在于它们要处理的是ANSI字符串还是Unicode字符串,在初始化C运行库之后,它们调用的是哪一个入口点函数。Visual C++自带C运行库的源代码。可以在crtree.c文件中找到4个启动函数的源代码。
用途如下:
- 获取指向新进程的完整命令行的一个指针
- 获取指向新进程的环境变量的一个指针
- 初始化C/C++运行库的全局变量。如果包含了StdLib.h,我们的代码就可以访问一些变量
- 初始化C运行库内存分配函数和其他I/O例程使用的堆
- 调用所有全局和静态C++类对象的构造函数
完成这些初始化工作之后,C/C++程序就会调用应用程序的入口点函数。
调用过程如下
GetStartupInfo(&StartupInfo);
int nMainRetVal=&WinMain((HINSTANCE)&_ImageBase,NULL,pszCommandLineUnicode,(StartupInfo.dwFlags&STARTF_USESHOWWINDOW)?StartupInfo.wShowWindow:SW_SHOWDEFAULT);
_Image是操作系统定义的一个伪变量,表明可执行文件被映射到应用程序内存中的什么位置。
进程实例句柄
加载到进程地址空间的每一个可执行文件或者DLL文件都被赋予了一个独一无二的实例句柄。可执行文件的实例句柄。可执行文件的实例被当作(w)WinMain函数的第一个参数hInstanceExe传入。在需要加载资源的函数调用中,一般都要提供此句柄的值。例如,为了从可执行文件的映像中加载一个图标资源,就需要调用下面这个函数:
HICON LoadIcon(
HINSTANCE hInstance,
PCTSTR pszIcon);
LoadIcon函数的第一个参数指出哪个文件包含了想要加载的资源。许多应用程序都会将(w)WinMain的hInstanceExe参数保存在一个全局变量中,使其很容易被可执行文件的所有代码访问。
hInstanceExe参数的实际值是一个内存基地址,系统将可执行文件的映像加载到进程地址空间中的这个位置。例如,假如系统打开可执行文件,并将它的内容加载到0x00400000,则w(WinMain)的hInstanceExe参数值为0x04000000。
可执行文件的映像具体加载到哪一个基地址,是由链接器决定的。不同的链接器使用不同的默认基地址。Visual Studio链接器使用的默认基地址是0x00400000,这是在运行Windows 98时,可执行文件的映像能加载到的最低的一个地址。使用Microsoft链接器/BASE:address链接器开关,可以更改要将应用程序加载到哪个基地址。
为了知道一个可执行文件或DLL文件被加载到进程地址空间的什么位置,可以使用GetModuleHandle函数来返回一个句柄/基地址
HMUDULE GetModuleHandle(PCTSTR pszModule);
调用这个函数时,要传递一个以0为终止符的字符串,它指定了已在主调进程的地址空间中加载的一个可执行文件或DLL文件的名称。如果系统找到了指定的可执行文件或DLL文件名称,GetModuleHandle就会返回可执行文件DLL文件映像加载到的基地址。
进程的命令行
系统在创建一个新进程时,会传一个命令行给它。这个命令行几乎总是非空的
用于创建新进程的可执行文件的名称是命令行上的第一个标记(token)
C运行库的启动代码开始执行一个GUI应用程序时,会调用Windows函数GetCommandLine来获取进程的完整命令行,忽略可执行文件的名称,然后将指向命令行剩余部分的一个指针传给WinMain的pszCmdLine函数
应用程序可以通过自己选择的任何一种方式来分析和解释命令行字符串。
进程的环境变量
每个进程都有一个与它关联的环境块,这是在进程地址空间内分配的一块内存,其中包含字符串和下面类似:
可以使用GetEnvironmentStrings函数来获取完整的环境块。
用户登录Windows时,系统会创建shell进程,并将一组环境字符串与其关联。
系统通过检查注册表中的两个注册表项来获得初始的环境字符串。
第一个注册表包含应用于系统的所有环境变量的列表:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
第二个注册表项包含应用于当前登录用户的所有环境变量的列表
HKEY_CURRENT_USER\Environment
用户可以添加,删除或更该这些环境变量,具体做法是从Control Panel中选择System,然后单击Advanced System Settings,然后点击Environment Variable按钮。
要有管理员权限才能更改system variable列表中包含的变量
应用程序还可以使用各种注册表函数来修改这些注册表项
但是为了使改动对所有应用程序生效,用户必须注销并重新登录,有的应用程序在其主窗口接收到WM_SETTINGCHANGE消息时,用新的注册表来更新它们的环境块。
通常,子进程会继承一组环境变量,这些环境变量和父进程的环境变量相同。不过,父进程可以控制哪些环境变量允许子进程继承。子进程和父进程并不共享一个环境块。
可以用GetEnvironmentVariable函数来判断一个环节变量是否存在
DWORD GetEnvironmentVariable(
PCTSTR pszName,
PTSTR pszValue,
DWORD cchValue);
pszName用于指定预期的变量名称,pszValue指向保存变量的缓冲区,cchValue指出缓冲区大小
找到就返回复制到缓冲区的字符数,没有找到,就返回0
ExpandEnvironmentStrings函数
DWORD ExpandEnvironmentStrings(
PCTSTR pszSrc,
PTSTR pszDSt,
DWORD chSize);
pszSrc参数是包含“可替换环境变量字符串”的一个字符串地址。pszDst参数是用于接收扩展字符串的一个缓冲区地址。chSize参数是这个缓冲区的最大大小。
用SetEnvironmentVariable函数添加一个变量,删除一个变量,或者修改一个变量的值
BOOL SetEnvironmentVariable(
PCTSTR pszName,
PCTSTR pszValue);
将pszName所标识的一个变量设为pszValue参数所标识的值,如果已经有了这个名称的变量就会修改值,如果pszValue设置为NULL,就会删除该变量
进程的关联性
进程中的线程都在主机的任何CPU上执行,也可以强迫线程在可用CPU的一个子集上运行。这就是进程的管理性。
进程的错误模式
每个进程都关联了一组标志,这些标志的作用是让系统知道进程如何响应严重错误。
包括磁盘介质错误,未处理的异常,文件查找错误以及数据对齐错误
进程可以调用SetErrorMode函数来告诉系统如何处理这些错误
UINT SetErrorMode(UINT fuErrorMode);
fuErrorMode参数
标志 | 描述 |
SEM_FAILCRITICALERRORS | 系统不显示严重错误处理程序消息框,并将错误返回主调进程 |
SEM_NOGPFAULTERRORBOX | 系统不显示常规保护错误消息框,此标志只应该由调试程序设置,该调试程序用一个异常处理程序来自行处理常规保护 |
SEM_NOOPENFILEERRORBOX | 系统查找文件失败,不显示消息框 |
SEM_NOALIGNMENTFAULTEXCEPT | 系统自动修复内存对齐错误,并使应用程序看不到这些错误 |
默认情况下,子进程会继承父进程的错误模式标志
进程当前所在的驱动器和目录
使用CreateFile来打开一个文件,系统将在当前驱动器和目录查找该文件
一个线程可以调用以下两个函数来获取和设置其所在进程的当前驱动器和目录
DWORD GetCurrentDirectory(
DWORD cchCurDir,
PTSTR pszCurDir);
BOOL SetCurrentDirectory(PCTSTR pszCurDir);