进程

来源:互联网 发布:java long 最大长度 编辑:程序博客网 时间:2024/06/14 22:00
一般讲进程定义成一个正在运行的程序的一个实例,它有以下的两部分组成:
1.一个内核对象,操作系统用它来管理进程,内核对象也是系统保存进程统计信息的地方
2.一个地址空间,其中包括所有的可执行文件或dll模块的代码和数据,此外他包包括动态内存分配,比如线程堆栈和堆的分配。
windows支持两种类型的应用程序,GUI程序和CUI程序。前者是图形用户界面,后者是控制台用户界面。
用vs来创建一个应用程序项目的时候,对于CUI程序,连接器开关是sbusystem:console,对于GUI程序,则是subsystem:windows。
用户运行应用程序时候,操作系统的加载程序会检查可执行文件影像的文件头,并获取这子系统值。
根据应用程序类型的不同,启动函数也不一样。
ANSI字符集下,GUI程序的启动函数是WinMainCRTStartup,入口函数是WinMain,CUI的启动函数是mainCRTStartup,入口函数是main。
Unicode字符集下,GUI程序的启动函数是wWinMainCRTStartup,入口函数是wWinMain,CUI的启动函数是wmainCRTStartup,入口点函数时wmain。
所有c/c++运行库启动函数所做的事情基本都是一样的,区别在于处理的是ANSI字符串还是UNICODE字符串。
这些启动函数的用途简单如下:
1.获取指向新进程的完整命令行的一个指针
2.获取指向新进程的环境变量的一个指针
3.初始化c/c++运行库的全局变量
4.初始化c运行库内存分配函数和其他底层io例程使用的堆
5.调用所有全局和静态c++l类对象的构造函数

加载到进程地址空间的每一个可执行文件或者dll文件都赋予了一个独一无二的实例句柄。可执行文件的实例被当作WinMain函数的第一个参数hInstanceExe传入,这个的实际值是一个内存基地址:系统将可执行文件的映像加载到进程地址空间的这位置。例如:假如系统打开可执行文件,并将他的内容加载到地址0x00400000,则WinMain的HinstanceExe的参数值为0x00400000。
为了知道一个可执行文件或dll文件被加载到进程地址空间的什么位置,可以使用如下的GetModuleHandle来返回一个句柄/基地址。
GetModuleHandle(lpModuleName)lpModuleName是模块的名称,可以是**.dll或者**.exe,如果没有扩展名,则默认为dll.如果模块名称通过路径来指定,则路径中必须使用"\",而不是"/".执行时,该函数通过名称(大小写不敏感)来查看调用进程已映射的模块,返回符合的模块句柄。如果GetModuleHandle(NULL),则返回调用进程本身的句柄。成功,则返回句柄,失败,返回NULL。错误信息:GetLastError()
ps:GetModuleHandle函数不会增加所指定模块的引用数,也就是说,不管调用该函数几次,只要调用一次FreeLibrary函数,该模块就从进程中卸载了。在多线程中,模块句柄在不同线程中不总是有效的。如:当在一个线程中调用了该函数获取了某一模块的句柄,但在使用该句柄之前,另一个线程把该句柄 Free了,并重新获取了其他模块的句柄。这个时候第一个线程再去使用这个句柄变量,就不再是之前它打算操作的那个模块了,而是第二个线程修改后的模块 句柄了。
如果函数在dll代码中,可以利用GetModuleHandleEx来知道代码在什么模块执行。
dwFlags:如果是0,则当调用该函数时,模块的引用计数自动增加,调用者在使用完模块句柄后,必须调用一次FreeLibrary如果是GET_MODULE_HANDLE_EX_FLAG_PIN,则模块一直映射在调用该函数的进程中,直到该进程结束,不管调用多少次FreeLibrary如果是GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,则同GetModuleHandle相同,不增加引用计数如果是GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,则lpModuleName是模块中的一个地址phModule存储要找的句柄。其他都和GetModuleHandle函数相同

每个进程都有一个与他关联的环境变量,这是在进程地址空间内分配的一块内存。
调用GetEnvironmentStrings函数来获取完整的环境块,如果不在需要GetEnvironment函数返回的内存块,应调用FreeEnvironmentString来释放。
同样可以使用SetEnvironmentVariable函数添加一个变量,删除一个变量或者修改一个变量。

进程的错误模式
与每个进程都关联了一组标志,这些标志的作用是让系统知道进程如何响应严重错误,包括磁盘介质错误等等。进程可以调用SetErrorMode函数来告诉系统如何处理这些错误。

进程当前所在的驱动器和目录
一个线程可以调用两个函数来获取和设置其所在进程的当前驱动器和目录,使用GetCurrentDirectory。

使用GetVersion函数可以获取系统版本。

下面介绍一下CreateProcess函数:
BOOL CreateProcess(LPCTSTR lpApplicationName,LPTSTR lpCommandLine,LPSECURITY_ATTRIBUTES lpProcessAttributes,LPSECURITY_ATTRIBUTES lpThreadAttributes,BOOL bInheritHandles,DWORD dwCreationFlags,LPVOID lpEnvironment,LPCTSTR lpCurrentDirectory,LPSTARTUPINFO lpStartupInfo,LPPROCESS_INFORMATIONlpProcessInformation);
一个线程调用CreateProcess,系统将创建一个进程内核对象,其初始使用计数为1。进程内核对象并不是进程本身,而且操作系统用来管理这进程的一个小型数据结构。系统为新进程创建一个虚拟地址空间,并将可执行文件和必要的dll代码和数据加载到进程的地址空间。

psaApplicationName和pszCommandLine分别指定新进程要使用的可执行文件称和要传给新进程的命令行字符串,一般情况下psaApplicationName设置为NULL。
注意此处的pszCommandLine是非常量字符串。传入常量字符串将会导致访问违规,因为在内部CreateProcess会修改传入的命令行字符串,返回时再将这个字符串还原。
psaProcess psaThread和bInheritHandles:
为了创建一个新的进程,系统必须创建一个进程内核对象和线程内核对象,由于这些都是内核对象,所以父进程有机会将安全属性关联到这两个对象上。可以根据需要设置,一般情况下也是为NULL。如果要进行设置,使用SECURITY_ATTRIBUTE结构,这样的话对象句柄可以继承到子进程当中。如果BInheritHandles设置为false,则不会继承任何句柄。
fdwCreate参数影响新进程创建的方式。如:指定CREATE_SUSPENDEd标识让系统在创建新进程时挂起其主线程。这样父进程就可以修改子进程地址空间的内存、更改主线程优先级或是将其添加到作业中。修改完后可以调用ResumeThread来允许子进程执行代码。传入0 表示创建进程后立即运行。多个标志位可以组合使用。
pvEnvironment参数指向一块内存,其中包含新进程要使用的环境字符串。大多数时候还是传入NULL。
pszCurDir参数允许父进程设置子进程的当前驱动器和目录。如果设置为NULL,则新进程的工作目录和父进程一样。
psiStartInfo参数指向一个STARTUOINFO结构,使用默认值,需要将其初始化为0。
ppiProcInfo参数指向一个PROCESS_INFORMATION结构,里面包含了新进程的句柄信息(包含进程句柄和线程句柄)。

进程可以通过以下四种方式终止:
1.主线程的入口函数返回(推介使用)
2.进程中的一个线程调用ExitProcess函数(尽量避免)
3.另一个进程中的线程调用TerminateProcess(尽量避免)
4.进程中的所有线程自然死亡(基本不会发生)

让主线程入口函数返回可以保证以下操作可以被执行
1.该线程创建的任何c++对象都将由这些对象的析构函数正确销毁
2.操作系统将正确释放线程栈使用的内存
3.系统将进程的退出码设置入口点函数的返回值
4.系统递减进程内核对象的使用计数

正常情况下入口点函数会返回到启动函数,启动函数将正确清理进程使用的所有C运行时资源,清理之后启动代码显式调用ExitProcess并将入口函数返回值传给它。这也是为什么只需从入口函数返回却可以终止整个进程的原因。

进程的一个线程调用ExitProcess可以终止本进程。其后的别的代码将不会被执行。

与ExitProcess相类似的还有ExitThread,它会导致一个线程终止。在创建线程时常出现这种情况:子线程还没有怎么执行程序就已经结束了,这有可能是在创建完线程后,主线程没有调用WaitForSingleObject之类的函数,主线程创建完其他线程后就返回到启动函数函数返回整个进程被终止。这一点很容易出错。

调用ExitProcess或是ExitThread会导致进程或线程当场终止运行,再也不会返回到启动函数,清理工作(C++对象的析构)当然没法执行。虽然最终随着进程的结束,该进程内所有线程所使用的资源都会被释放,但是应该避免调用这些函数,它们阻止了C++对象析构函数对善后工作的处理。顺便提下,如果在主线程调用ExitThread,虽然主线程当场终止,但是如果进程内还有其他线程,则进程不会终止。

一个进程终止时,系统会依次执行以下操作:
1:终止进程中遗留的任何线程。
2:释放进程分配的所有用户对象,关闭所有内核对象。如果它们的使用计数变为0,内核对象将会释放。
3:将进程的退出代码从STILL_ACTIVE变为传给ExitProcess或是TerminateProcess的参数存储在内核对象中。
4:进程内核对象变为一触发状态。这也是为什么其他线程可以挂起他们自己直至另一个进程终止运行。
5:进程内核对象的使用计数递减1。

进程内核对象的生命期至少能像进程本身一样长。当进程终止时如果系统中还有另一个进程打开了这个进程的内核对象的句柄,进程内核对象的使用计数就不会变为0。当父进程忘记关闭子进程的句柄时往往发生这种情况。

进程终止了内核对象还没有被释放,这样做有用吗?当然有用!!即使进程终止了,存储在内核对象的信息也有可能被使用,如我们想知道进程占用了多少Cpu时间,或是想获得它的退出代码。GetExitCodeProcess此函数会查找进程内核对象并从内核对象的数据结构中取出退出代码。任何时候都可以调用此函数。如此时进程正在运行那么将会得到STILL_ALIVE。

WaitForSingleObject将会挂起当前线程,知道它所等待的对象变为已触发状态。进程或线程对象在终止时就会变成已触发状态。

最后说一下手动提升进程的权限(UAC)
ShellExecuteEx
SHELLEXECUTEINFO ShellInfo;memset(&ShellInfo, 0, sizeof(ShellInfo));ShellInfo.cbSize = sizeof(ShellInfo);ShellInfo.hwnd = NULL;ShellInfo.lpVerb = _T("open");ShellInfo.lpFile =(LPCWSTR)fname; // 此处写执行文件的绝对路径ShellInfo.nShow = SW_SHOWNORMAL;ShellInfo.fMask = SEE_MASK_NOCLOSEPROCESS;BOOL bResult = ShellExecuteEx(&ShellInfo);



2 0
原创粉丝点击