80386处理器有3种工作模式:实模式、保护模式和虚拟86模式。其中,实模式和虚拟86模式是为了和8086处理器兼容而设置的,保护模式是80386处理器的主要工作模式。Windows操作系统运行在此模式下,保护主要是对存储器的保护。
80386对多任务操作系统的支持主要体现在两方面:一是在硬件上为任务之间的切换提供良好的条件;二是它实现了多任务隔离。多任务隔离技术可以使每个任务都有独立的地址空间,就像每个任务独享一个CPU一样。
在Windows中,任务被进程取代,进程是正在运行的应用程序的实例;但是占用CPU时间片执行指令的并不是进程,而是线程。
保护模式下,80386所有的32根地址线都可供寻址,处理器寻址的范围是0x00000000~0xFFFFFFFF(232,4GB),因此,32位Windows操作系统可寻址4GB的地址空间,这就允许一个指针有4 294 967 296个不同的取值,它覆盖了整个4GB的范围。
Windows为每个进程分配4GB的地址空间主要依靠CPU的支持。CPU在保护模式下支持虚拟存储,即虚拟内存,它可以帮助操作系统将磁盘空间当作内存空间来使用。在磁盘上应用于这一机制的文件被称为页文件(Paging File),它包含了对所有进程都有效的虚拟内存。
Windows实现虚拟内存机制就是基于上述的一个32位的线性地址空间。32位地址空间转化为4GB虚拟内存。在多数系统上,Windows将此空间的一半(4GB的前半部分,0x00000000~0x7FFFFFFF)留给进程作为私有存储,自己使用另一半(后半部分,0x80000000~0xFFFFFFFF)来存储操作系统内部使用的数据。
各进程的地址空间被分成了用户空间和系统空间两部分。用户空间是进程私有的地址空间;系统空间放置操作系统的代码,包括内核代码。设备驱动代码。设备I/O缓冲区等。系统空间在所有进程中是共享的。
内核对象是系统提供的用户模式下代码与内核模式下代码进行交互的基本接口。一个内核对象是一块内核分配的内存,它只能被运行在内核模式下的代码访问。内核对象记录的数据在整个系统中只有一份,所有它们也被称为系统资源。对于每一个内核对象,Windows都提供了在其上操作的API函数。内核对象和普通的数据结构之间最大的区别是其内部数据结构是隐藏的,必须调用一个对象服务才能从此对象中得到数据,或者向其输入数据,而不能直接读或者改变对象内部的数据。
引入内核对象之后,系统可以方便地完成下面4个任务:
1)为系统资源提供可识别的名字;
2)在进程间共享资源和数据;
3)保护资源不会被未经认可的代码访问;
4)跟踪对象的引用情况,这使得系统知道什么时候一个对象不再被引用了,以便释放它占用的空间。
内核对象的数据结构仅能够从内核模式访问,所以直接在内存中地位这些数据结构对应用程序来说是不可能的,应用程序必须使用API函数来访问内核对象。调用函数创建内核对象时,函数会返回标识此内核对象的句柄。为了使系统稳定,这些句柄是进程相关的,即仅对创建该内核对象的进程有效。如果将一个句柄值通过某种机制传给其他进程中的线程,那么,该线程以此句柄为参数调用相关函数就会失败。当然,多个进程共享一个内核对象也是可能的,调用DuplicateHandle函数复制一个进程句柄传给其他进程即可。
进程是一个正在运行的程序,它拥有自己的虚拟地址空间,拥有自己的代码、数据和其他系统资源,如进程创建的文件、管道、同步对象等。
组成Win32进程的两个部分:
1)进程内核对象:操作系统使用此内核对象来管理进程,这个内核对象也是操作系统存放进程统计信息的地方;
2)私有的虚拟地址空间:此地址空间包含了所有可执行的或者DLL模块的代码和数据,它也是程序动态申请内存的地方,比如说线程堆栈和进程堆。
应用程序必须有一个入口函数main,在程序开始运行时被调用。操作系统事实上并不是真的调用main函数,而是去调用C/C++运行期启动函数,此函数会初始化C/C++运行期库。因此,在程序中可以调用malloc和free之类的函数,它也会保证在用户的代码执行之前所有全局或静态的C++对象能够被正确的创建。在控制台应用程序中,C/C++运行期启动函数会调用程序入口函数main。
在Win32应用程序的启动过程就是进程的创建过程,操作系统是通过调用CreateProcess函数来创建新的进程的。系统接着会为新进程创建一个主线程,这个主线程通过执行C/C++运行期启动代码开始运行,C/C++运行期启动代码又会调用main函数。系统在创建新的进程时会为新进程指定一个STARTUPINFO类型的变量,这个结构包含了父进程传递给子进程的一些显示信息。对图形界面应用程序来说,这些信息将影响新的进程中主线程的主窗口的显示;对控制台应用程序来说,如果有一个新的控制台窗口被创建的话,这些信息将影响这个控制台窗口。
STARTUPINFO结构定义如下:
typedef struct
{
DWORD cb; //结构的长度,总是应该被设为sizeof(STARTUPINFO)
LPSTR lpReserved; //保留字段
LPSTR lpDesktop; //指定桌面名称
LPSTR lpTitle; //控制台应用程序使用,指定控制台窗口标题
DWORD dwX; //指定新创建窗口的位置坐标和大小信息
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars; //控制台程序使用,指定控制台窗口的行数
DWORD dwYCountChars;
DWORD dwFillAttributr; //控制台程序使用,指定控制台窗口的背景色
DWORD dwFlags; //标志它的值决定了STARTUPINFO结构中哪些成员的值是有效的
WORD wShowWindow; //窗口的显示方式
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput; //控制台程序使用,几个标准句柄
HANDLE hStdOutput;
HANDLE hStdError;
}STARTUPINFO, *LPSTARTUPINFO;
一个进程可以调用GetStartupInfo函数来取得父进程创建自己时使用的STARTUPINFO结果。事实上,Windows系统就是通过调用这个函数来取得当前进程的创建信息,以便对新进程中主窗口的属性设置默认值。函数定义如下:
VOID GetStartupInfo(LPSTARTUPINFO lpStartupInfo); //取得当前进程被创建时指定的STARTUPINFO结构
定义一个STARTUPINFO结构的对象后,总要在使用此对象之前将对象的cb成员初始为
STARTUPINFO结构的大小:
STARTUPINFO si = {sizeof(si)}; //将cb成员初始化为asizeof(si),其他成员初始化为0
::GetStartupInfo(&si);
初始化cb成员是必须的,因为随着Windows版本的改变,API函数支持的结构体的成员有可能增加,
但又要兼容以前版本,所以Windows要通过结构体的大小来确定其成员的数目。
CreateProcess函数创建一个新的进程和该进程的主线程。新的进程会在父进程的安全上下文中运行指定的可执行文件:
CreateProcess(
LPCSTR lpApplicationName, //可执行文件的名称
LPSTR lpCommandLine, //指定了要传递给执行模块的参数
LPSECURITY_ATTRIBUTES lpProcessAttributes, //进程安全性,值为NULL的话表示使用默认的安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全性,值为NULL的话表示使用默认的安全属性
BOOL bInheritHandles, //指定了当前线程中的可继承句柄是否可被新进程继承
DWORD dwCreationFlags, //指定了新进程的优先级以及其他创建标识
LPVOID lpEnvironment, //指定新进程使用的环境变量
LPCSTR lpCurrentDirectory, //新进程使用的当前目录
LPSTARTUPINFO lpStartupInfo, //指定新进程中主窗口的位置、大小和标准句柄等
LPPROCESS_INFORMATION lpProcessInformation //【out】返回新建进程的标志信息,如ID号、句柄等
);
上面的lpApplicationName和lpCommandLine参数指定了新的进程将要使用的可执行文件的名称和传递给新进程的参数。例如,下面代码启动了Windows自带的记事本程序:
STARTUPINFO si = {sizeof(si)};
PROCESS_INFORMATION pi;
char *szCommandLine = "notepad"; //"notepad.exe" is also OK
::CreateProcess(NULL, szCommandLine, NULL, NULL, FALSE,
NULL, NULL, NULL, &si, &pi);
LpCommandLine参数为新的进程指定了一个完整命令行,当CreateProcess函数复制此字符串时,它首先检查字符串中第一个单词,并假设该单词就是我们想要运行的可执行文件的名字,若此名字没有后缀,.exe后缀将被添加进来。CreateProcess函数会按以下路径搜索可执行文件:
1)调用进程的可执行文件所在的目录;
2)调用进程的当前目录;
3)Windows的系统目录(system32目录);
4)Windows目录;
5)在名称为PATH的环境变量中列出的目录。
当然,如果文件名中包含了目录,系统会直接在这个目录中查找,而不会再去其他目录中搜索。
如果系统找到了可执行文件,它会创建一个新的进程并将该可执行文件中的代码和数据映射到新进程的地址空间。之后系统会调用C/C++运行期启动函数,这个函数检查新进程的命令行,将文件名后第一个参数的地址传给新进程的入口函数。例如:
char *szCommandLine = "notepad readme.txt"; //指定了一个readme.txt参数,将使记事本打开此文件
lpProcessInformation参数是一个指向PROCESS_INFORMATION结构的指针,CreateProcess函数在返回之前会初始化此结构的成员:
typedef struct
{
HANDLE hProcess, //新建进程的内核句柄
HANDLE hThread, //新建进程中主线程的内核句柄
DWORD dwProcessId, //新建进程的ID
DWORD dwThreadId //新建进程的主线程ID
}PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
由上可见,创建一个新的进程将促使系统创建一个进程内核对象和一个现场内核对象。在创建它们时,系统将每个对象的使用计数初始化为1。然后,在CreateProcess返回之前,这个函数打开此进程内核对象和线程内核对象的句柄,并将它们的值传给上述结构的hProcess和hThread成员。CreateProcess在内部打开内核对象时,这两个对象的使用计数将会增加到2.。因此,父进程中必须有一个线程调用CloseHandle关闭CreateProcess函数返回的两个内核对象的句柄。否则即便是子进程已经终止了,该进程的进程内核对象和主线程内核对象仍然没有释放。
当进程内核对象或线程内核对象创建以后,系统会给其分配一个唯一的ID号,且进程和线程ID使用同一个号码分配器,因此同一时间不可能重复。但当一个内核对象销毁时,其拥有的ID号可能被重新分配给其他的对象,因此,应该避免使用ID号来跟随进程和线程。
创建进程的例子(打开Windows自带的命令行程序cmd.exe):
int main(int argc, char* argv[])
{
char szCommandLine[] = "cmd";
STARTUPINFO si = {sizeof(si)};
PROCESS_INFORMATION pi;
si.dwFlags = STARTF_USESHOWWINDOW; //指定wShowWindow成员
si.wShowWindow = TRUE; //此成员设为TRUE,表示显示新建进程的主窗口
BOOL bRet = ::CreateProcess(
NULL, //不在此指定可执行文件的文件名
szCommandLine, //命令行参数
NULL, //默认进程安全性
NULL, //默认主线程安全性
FALSE, //指定当前进程内的句柄不可以被子进程继承
CREATE_NEW_CONSOLE, //为新进程创建一个新的控制台窗口
NULL, //使用本进程的环境变量
NULL, //使用本进程的驱动器和目录
&si,
&pi
);
if(bRet)
{
//既然不使用两个句柄,最后立即关闭它们
::CloseHandle(pi.hThread);
::CloseHandle(pi.hProcess);
printf("新进程的进程ID号为:%d/n", pi.dwProcessId);
printf("新进程的主线程ID号位:%d/n", pi.dwThreadId);
}
}
使用ToolHelp函数可以获取当前正在运行的一系列进程。首先使用CreateToolhelp32Snapshot函数给当前系统内执行的进程拍快照,也就是获得一个进程列表,这个列表中记录着进程的ID、进程对应的可执行文件的名称和创建该进程的进程ID等数据。接着使用Process32First函数和Process32Next函数遍历快照中记录的列表,代码如下:
#include <windows.h>
#include <iostream>
#include <tlhelp32.h> //声明快照函数的头文件
int main(int argc, char* argv[])
{
PROCESSENTRY32 pe32;
//在使用这个结构之前先设置它的大小
pe32.dwSize = sizeof(pe32);
//给系统内所有进程拍一个快照
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(
TH32CS_SNAPPROCESS, 0);
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
printf("CreateToolhelp32Snapshot调用失败/n");
return -1;
}
//遍历进程快照,依次显示每个进程的信息
BOOL bMore = ::Process32First(hProcessSnap, &pe32);
while(bMore)
{
printf("进程名称:%s/n", pe32.szExeFile);
printf("进程ID号:%u/n/n", pe32.th32ProcessID);
bMore = ::Process32Next(hProcessSnap, &pe32);
}
//释放snapshot对象
::CloseHandle(hProcessSnap);
system("pause");
return 0;
}
CreateToolhelp32Snapshot用于获取系统内指定进程的快照,也可以获取被这些进程使用的堆、模块和线程的快照:
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags, //用来指定快照中需要返回的对象
DWORD th32ProcessID,//一个进程ID号,用来指定要获取哪一个进程的快照
//当获取系统进程列表或获取当前进程快照时可以设为0
);
这个函数不仅可获取进程列表,也可以用来获取线程和模块等对象的列表。dwFlags参数指定了获取的列表的类型:
TH32CS_SNAPHEAPLIST---枚举th32ProcessID参数指定的进程中的堆
TH32CS_SNAPMODULE---枚举th32ProcessID参数指定的进程中的模块
TH32CS_SNAPPROCESS---枚举系统范围内的进程,此时th32ProcessID参数被忽略
TH32CS_SNAPTHREAD---枚举系统范围内的线程,此时th32ProcessID参数被忽略
Process32First和Process32Next函数的第一个参数是快照句柄,第二个参数是一个指向PROCESSENTRY32结构的指针,进程信息将会被返回到这个结构中:
typedef struct
{
DWORD dwSize, //结构的长度,必须预先设置
DWORD cntUsage, //进程的引用计数
DWORD th32ProcessID, //进程ID
DWORD th32DefaultHeapID, //进程默认堆的ID
DWORD th32ModuleID, //进程模块的ID
DWORD cntThreads, //进程创建的线程数
DWORD th32ParentProcessID, //进程的父进程ID
LONG pcPriClassBase, //进程创建的线程的基本优先级
DWORD dwFlags, //内部使用
CHAR szExeFile[MAX_PATH] //进程对应的可执行文件名
}PROCESSENTRY32;
终止进程也就是结束程序的执行,从内存中卸载它,进程终止的原因有4种:
1)主线程的入口函数返回;
2)进程中一个线程调用了ExitProcess函数;
3)此进程中的所有线程都结束了;
4)其他进程中的一个线程调用了TerminateProcess函数。
要结束进程一般让主线程的入口函数(main函数)返回。当用户的程序入口函数返回时,启动函数会调用C/C++运行期退出函数exit,并将用户的返回值传递给它。Exit函数会销毁所有全局的或静态的C++对象,然后调用系统函数ExitProcess此时操作系统终止应用程序:
void ExitProcess(UINT uExitCode); //参数uExitCode为此程序的退出代码
我们也可以在程序的任何地方调用ExitProcess强制当前程序立即结束。对操作系统,这是很正常的;但对C/C++应用程序来说,应该避免直接调用这个函数,因为这会使C/C++运行期库得不到通知,而没有机会去调用全局或静态的C++对象的析构函数。
ExitProcess函数只能用来结束当前进程,不能用于结束其他进程。如果要结束其他进程的执行,可以使用TerminateProcess函数:
BOOL TerminateProcess(
HANDLE hProcess, //要结束的进程(目标进程)的句柄
UINT uExitCode //指定目标进程的退出代码,
//可以使用GetExitCodeProcess取得一个进程的退出代码
);
在使用TerminateProcess函数之前,我们首先需要取得目标进程的句柄,可以使用OpenProcess函数来取得这个进程的访问权限:
HANDLE OpenProcess(
DWORD dwDesiredAccess, //想得到的访问权限(PROCESS_ALL_ACCESS等)
BOOL hInheritHandle, //指定返回的句柄是否可继承
DWORD dwProcessId //指定要打开的进程的ID号
);
一般使用下面代码来终止一个进程:
BOOL TerminateProcessFromId(DWORD dwId)
{
BOOL bRet = FALSE;
//打开目标进程,取得进程句柄
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwId);
if(hProcess != NULL)
{
bRet = ::TerminateProcess(hProcess, 0);
}
CloseHandle(hProcess);
return bRet;
}
一旦进程终止,会有如下事件发生:
1)所有被这个进程创建或打开的对象句柄就会关闭;
2)此进程内的所有线程将会终止执行;
3)进程内核对象变成受信状态,所有等待在此对象上的线程开始运行,即WaitForSingleObject函数返回;
4)系统将进程对象中退出代码的值由STILL_ACTIVE改为指定的退出码。