创建进程详解:用CreatrProcess函数创建一个进程

本文摘自于《windows核心编程》并加上了自己的理解与代码

多读几遍,会理解的!重要的我都标记了的

用C r e a t e P r o c e s s函数创建一个进程:

函数原型如下:

BOOL CreateProcessA(//下面会介绍的

LPCSTR lpApplicationName,

LPSTR lpCommandLine,

LPSECURITY_ATTRIBUTES lpProcessAttributes,

LPSECURITY_ATTRIBUTES lpThreadAttributes,

BOOL bInheritHandles,

DWORD dwCreationFlags,

LPVOID lpEnvironment, LPCSTR lpCurrentDirectory,

LPSTARTUPINFOA lpStartupInfo,

LPPROCESS_INFORMATION lpProcessInformation );

1.当一个线程调用C r e a t e P r o c e s s时,系统就会创建一个进程内核对象,其初始使用计数是 1。该进程内核对象不是进程本身,而是操作系统管理进程时使用的一个较小的数据结构。

可以将进程内核对象视为由进程的统计信息组成的一个较小的数据结构。

2.然后,系统为新进程创建一个虚拟地址空间,并将可执行文件或任何必要的 D L L文件的代码和数据加载到该进程的地址空间中

3.然后系统为新进程的主线程创建一个线程内核对象(其使用计数为 1),与进程内核对象一样,线程内核对象也是操作系统用来管理线程的小型数据结构。通过执行 C / C + +运行期启动代码,该主线程便开始运行

它最终调用 Wi n M a i n、w Wi n M a i n、m a i n或w m a i n函数。如果系统成功地创建了新进程和主线程,CreateProcess便返回TRUE(1)。 (通常我们判断是否创建成功的时候都会用一个变量接受这个返回值)

注意、注意、注意:

在进程被完全初始化之前,C r e a t e P r o c e s s返回T R U E。这意味着操作系统加载程
序尚未试图找出所有需要的 D L L。如果一个D L L无法找到,或者未能正确地初始化,
那么该进程就终止运行。由于 C r e a t e P r o c e s s返回T R U E,因此父进程不知道出现的任
何初始化问题。

二】下面详细介绍每一个参数:

1.pszApplicationName和pszCommandLine,

pszApplicationName用于设定新进程将要使用的可执行文件的名字,可以设置为NULL 

pszCommandLine    用于传递给新进程的命令行字符串,

注意 请注意

            pszCommandLine参数的原型是P T S T R。(不是PTCSTR哦~)这意味着C r e a t e P r o c e s s期望你
将传递一个非常量字符串的地址(比如不能直接传“cmd”,这是个字符串常量,你可以先用个变量接受一下,比如wchar wstr[] = L"cmd")。从内部来讲, C r e a t e P r o c e s s实际上并不修改你传递
给它的命令行字符串。不过,在 C r e a t e P r o c e s s返回之前,它将该字符串恢复为它的原
始形式.

                当C r e a t e P r o c e s s分析p s z C o m m a n d L i n e字符串时,它将查看字符串中的第一个标记,并假
设该标记是想运行的可执行文件的名字。如果可执行文件的文件名没有扩展名,便假设它的扩
展名为. e x e。C r e a t e P r o c e s s也按下面的顺序搜索该可执行文件:
1) 包含调用进程的. e x e文件的目录。
2) 调用进程的当前目录。
3) Wi n d o w s的系统目录。
4) Wi n d o w s目录。
5) PAT H环境变量中列出的目录

            当然,如果文件名包含全路径,系统将使用全路径来查看可执行文件,并且不再搜索这些目录。如果系统找到了可执行文件,那么它就创建一个新进程,并将可执行文件的代码和数据映射到新进程的地址空间中。然后系统将调用 C / C + +运行期启动例程。正如前面我们讲过的那样,C / C + +运行期启动例程要查看进程的命令行,并将地址作为 ( w ) Wi n M a i n的p s z C m d L i n e参数传递给可执行文件的名字后面的第一个参数。这一切都是在p s z A p p l i c a t i o n N a m e参数是N U L L(9 9 %以上的时候都应该属于这种情况)时发生的。如果不传递N U L L,可以将地址传递给p s z A p p l i c a t i o n N a m e参数中包含想运行的可执行文件的名字的字符串。请注意,必须设定文件的扩展名,系统将不会自动假设文件名有一个. e x e扩展名。C r e a t e P r o c e s s假设该文件位于当前目录中,除非文件名前面有一个路径。如果在当前目录中找不到该文件,C r e a t e P r o c e s s将不会在任何其他目录中查找该文件,它运行失败了。

但是,即使在 p s z A p p l i c a t i o n N a m e参数中设定了文件名, C r e a t e P r o c e s s也会将p s z C o m m a n d L i n e参数的内容作为它的命令行传递给新进程。例如,可以像下面这样调用

2 、psaProcess、p s a T h r e a d和b i n h e r i t H a n d l e s

        若要创建一个新进程,系统必须创建一个进程内核对象和一个线程内核对象(用于进程的主线程),由于这些都是内核对象,因此父进程可以得到机会将安全属性与这两个对象关联起来。可以使用p s a P r o c e s s和p s a T h r e a d参数分别设定进程对象和线程对象需要的安全性。可以为这些参数传递N U L L,在这种情况下,系统为这些对象赋予默认安全性描述符。也可以指定两个S E C U R I T Y _ AT T R I B U T E S结构,并对它们进行初始化,以便创建自己的安全性权限,并将它们赋予进程对象和线程对象。将S E C U R I T Y _ AT T R I B U T E S结构用于p s a P r o c e s s和p s a T h r e a d参数的另一个原因是,父进程将来生成的任何子进程都可以继承这两个对象句柄中的任何一个(windows核心编程第 3章介绍了内核对象句柄的继承性的有关理论,想要电子版的留邮箱)。

假设 Process A创建了Process B,方法是调用C r e a t e P r o c e s s,为p s a P r o c e s s参数传递一个S E C U R I T Y _ AT T R I B U T E S结构的地址,在这个结构中,b I n h e r i t H a n d l e s成员被置为T R U E。在同样这个函数调用中, p s a T h r e a d参数指向另一个S E C U R I T Y _ AT T R I B U T E S结构,在这个结构中,b I n h e r i t H a n d l e s成员被置为FA L S E。

当系统创建Process B时,它同时指定一个进程内核对象和一个线程内核对象,并且将句柄返回给p p i P r o c I n f o参数(很快将介绍该参数)指向的结构中的Process A。这时,使用这些句柄,rocess A就能够对新创建的进程对象和线程对象进行操作。

现在,假设Process A第二次调用C r e a t e P r o c e s s函数,以便创建Process C。Process A可以决
是否为Process C赋予对Process A能够访问的某些内核对象进行操作的能力。B I n h e r i t H a n d l e s
参数可以用于这个目的
。如果 b I n h e r i t H a n d l e s被置为T R U E,系统就使Process C继承Process A
中的任何可继承句柄。在这种情况下, Process B 的进程对象的句柄是可继承的。无论
C r e a t e P r o c e s s的b I n h e r i t H a n d l e s参数的值是什么, Process B的主线程对象的句柄均不能继承。
同样,如果Process A调用C r e a t e P r o c e s s,为b I n h e r i t H a n d l e s传递FA L S E,那么Process C将不能
继承Process A目前使用的任何句柄。

3 fdwCreate

        f d w C r e a t e参数用于标识标志,以便用于规定如何来创建新进程。

具体参数信息请看MSDN的详细介绍。 参数列表

4 pvEnvironment
          p v E n v i r o n m e n t参数用于指向包含新进程将要使用的环境字符串的内存块。大多数情况
下,为该参数传递N U L L
,使子进程能够继承它的父进程正在使用的一组环境字符串。也可以
使用G e t E n v i r o n m e n t S t r i n g s函数:

PVOID GetEnvironmentString();
该函数用于获得调用进程正在使用的环境字符串数据块的地址。可以使用该函数返回的地
址,作为C r e a t e P r o c e s s的p v E n v i r o n m e n t参数。如果为p v E n v i r o n m e n t参数传递N U L L,那么这正
是C r e a t e P r o c e s s函数所做的操作。当不再需要该内存块时,应该调用 F r e e E n v i r o n m e n t S t r i n g s函
数将内存块释放

5.pszCurDir
          pszCurDir参数允许父进程设置子进程的当前驱动器和目录。如果本参数是 N U L L,则新进
程的工作目录将与生成新进程的应用程序的目录相同
如果本参数不是 N U L L,那么p s z C u r D i r
必须指向包含需要的工作驱动器和工作目录的以 0结尾的字符串。注意,必须设定路径中的驱动器名。

//下面两个参数仔细多看几遍

6 psiStartInfo

psiStartInfo参数用于指向一个STARTINFO结构:

        当Wi n d o w s创建新进程时,它将使用该结构的有关成员。大多数应用程序将要求生成的应用程序仅仅使用默认值。至少应该将该结构中的所有成员初始化为零,然后将 c b成员设置为该结构的大小:


	STARTUPINFO si;
	
	ZeroMemory(&si, sizeof(si));//用0填充内存块
	si.cb = sizeof(si);

        如果未能将该结构的内容初始化为零,那么该结构的成员将包含调用线程的堆栈上的任何
无用信息。
将该无用信息传递给 C r e a t e P r o c e s s,将意味着有时会创建新进程,有时则不能创建
新进程,完全取决于该无用信息。有一点很重要,那就是将该结构的未用成员设置为零,这样,
C r e a t e P r o c e s s就能连贯一致地运行。不这样做是开发人员最常见的错误。

这时,如果想要对该结构的某些成员进行初始化,只需要在调用 C r e a t e P r o c e s s之前进行这
项操作即可。我们将依次介绍每个成员。有些成员只有在子应用程序创建一个重叠窗口时才有
意义,而另一些成员则只有在子应用程序执行基于 C U I的输入和输出时才有意义

            现在介绍d w F l a g s的成员。该成员包含一组标志,用于修改如何来创建子进程。大多数标
志只是告诉C r e a t e P r o c e s s,S TA RT U P I N F O结构的其他成员是否包含有用的信息,或者某些成
员是否应该忽略。下面的图标出可以使用的标志及其含义:

            另外还有两个标志,即 S TA RT F _ F O R C E O N F E E D B A C K和S TA RT F _+F O R C E O F F F -
E E D B A C K,当启动一个新进程时,它们可以用来控制鼠标的光标。由于 Wi n d o w s支持真正的
多任务抢占式运行方式,因此可以启动一个应用程序,然后在进程初始化时,使用另一个程序。
为了向用户提供直观的反馈信息, C r e a t e P r o c e s s能够临时将系统的箭头光标改为一个新光标,
即沙漏箭头光标,下面的酱紫!

7 ppiProcInfo

         p p i P r o c I n f o参数用于指向你必须指定的 P R O C E S S _ I N F O R M AT I O N结构。C r e a t e P r o c e s s在返回之前要对该结构的成员进行初始化。该结构的形式如下面所示:

                 如前所述,创建新进程可使系统建立一个进程内核对象和一个线程内核对象。在创建进程的时候,系统为每个对象赋予一个初始使用计数值 1。然后,在c r e a t e P r o c e s s返回之前,该函数打开进程对象和线程对象,并将每个对象的与进程相关的句柄放入 P R O C E S S _ I N F O R M AT I O N结构的h P r o c e s s和h T h r e a d成员中。当C r e a t e P r o c e s s在内部打开这些对象时,每个对象的使用计数就变为2。

                这意味着在系统能够释放进程对象前,该进程必须终止运行(将使用计数递减为 1),并且父进程必须调用C l o s e H a n d l e(再将使用计数递减 1,使之变为0)。同样,若要释放线程对象,该线程必须终止运行,父进程必须关闭线程对象的句柄

                注意 必须关闭子进程和它的主线程的句柄,以避免在应用程序运行时泄漏资源。当然,当进程终止运行时,系统会自动消除这些泄漏现象,但是,当进程不再需要访问子进程和它的线程时,编写得较好的软件能够显式关闭这些句柄(通过调用C l o s e H a n d l e函数来关闭)。不能关闭这些句柄是开发人员最常犯的错误之一。由于某些原因,许多开发人员认为,关闭进程或线程的句柄,会促使系统撤消该进程或线程。实际情况并非如此。关闭句柄只是告诉系统,你对进程或线程的统计数据不感兴趣。进程或线程将继续运行,直到它自己终止运行

              当进程内核对象创建后,系统赋予该对象一个独一无二的标识号,系统中的其他任何进程内核对象都不能使用这个相同的I D号。线程内核对象的情况也一样。当一个线程内核对象创建时,该对象被赋予一个独一无二的、系统范围的 I D号。

                                                              进程I D和线程I D共享相同的号码池。
              这意味着进程和线程不可能拥有相同的 I D。另外,对象决不会被赋予 0作为其 I D。在C r e a t e P r o c e s s返回之前,它要用这些 I D填入P R O C E S S _ I N F O R M AT I O N结构的d w P r o c e s s I d和d w T h r e a d I d成员中。I D使你能够非常容易地识别系统中的进程和线程。一些实用工具(如 Ta s kM a n a g e r)对I D使用得最多,而高效率的应用程序则使用得很少。由于这个原因,大多数应用程序完全忽略I D。


         如果应用程序使用 I D来跟踪进程和线程,必须懂得系统会立即复用进程 I D和线程I D。例如,当一个进程被创建时,系统为它指定一个进程对象,并为它赋予 I D值1 2 2。如果创建了一个新进程对象,系统不会将相同的I D赋予给它。但是,如果第一个进程对象被释放,系统就可以将1 2 2赋予创建的下一个进程对象。记住这一点后,就能避免编写引用不正确的进程对象或线程对象的代码。获取进程 I D是很容易的,保存该 I D也不难,但是,接下来你应该知道,该I D标识的进程已被释放,新进程被创建并被赋予相同的 I D。当使用已经保存的进程 I D时,最终操作的是新进程,而不是原先获得I D的进程。


有时,运行的应用程序想要确定它的父进程。首先应该知道只有在生成子进程时,才存在
进程之间的父子关系。在子进程开始执行代码前, Wi n d o w s不再考虑存在什么父子关系。较早
的 Wi n d o w s 版本没有提供让进程查询其父进程的函数。

          现在, To o l H e l p 函数通过P R O C E S S E N T RY 3 2结构使得这种查询成为可能。在这个结构中有一个 t h 3 2 P a r e n t P r o c e s s I D成员,根据文档的说明,它能返回进程的父进程的 I D。


           系统无法记住每个进程的父进程的I D,但是,由于I D是被立即重复使用的,因此,等到获
得父进程的I D时,该I D可能标识了系统中一个完全不同的进程。父进程可能已经终止运行。

         如果应用程序想要与它的“创建者”进行通信,最好不要使用 I D。应该定义一个持久性更好的机
制,比如内核对象和窗口句柄等。(以前觉得Id和handle都是标识的,有啥区别和联系呢?现在知道了)


          若要确保进程I D或线程I D不被重复使用,唯一的方法是保证进程或线程的内核对象不会被撤消。如果刚刚创建了一个新进程或线程,只要不关闭这些对象的句柄,就能够保证进程对象不被撤消。一旦应用程序结束使用该 I D,那么调用C l o s e H a n d l e就可以释放内核对象,要记住,这时使用或依赖进程 I D,对来说将不再安全。

            如果使用的是子进程,将无法保证父进程或父线程的有效性,除非父进程复制了它自己的进程对象或线程对象的句柄,并让子进程继承这些句柄。

感谢阅读完了进程怎么创建,下个博文将会更新,进程的终止。

提问?

什么是内核对象

createProcess的最后两个参数要怎么初始设置?为什么要这样设置?

createProcess的返回值是什么

如何防止进程和线程的内核对象不被销毁?

下面是完整代码,有个小坑,cout输出字符的时候,由于用的是unicode编码,所以输出的是字符的地址

,改成wcout就行了。

//进程创建
#include<windows.h>
#include <tchar.h>
#include <stdlib.h>
#include <iostream>

using namespace std;
//创建一个新的控制台
int main()
{
	STARTUPINFO si;
	PROCESS_INFORMATION pi;
	ZeroMemory(&si, sizeof(si));//用0填充内存块
	si.cb = sizeof(si);
	si.dwFlags = STARTF_USESHOWWINDOW;
	si.wShowWindow = TRUE;

	ZeroMemory(&pi, sizeof(pi));
	//这样,si 和 pi就进行了初始化

	


	WCHAR vi[] = L"cmd";

	int ret = CreateProcess(
		NULL, vi,
		NULL, NULL, FALSE,
		CREATE_NEW_CONSOLE,
		NULL, NULL,
		&si,&pi

		);
	if (ret)
	{
		CloseHandle(pi.hThread);
		CloseHandle(pi.hProcess);

		cout << vi;

		cout << pi.dwThreadId << endl;
		cout  << pi.dwProcessId<<endl;
		printf("%d \n", pi.dwProcessId);
		printf("%d ", pi.dwThreadId);
	}
	else
	{
		wcout << L"错误" << endl;
	}




	//getchar();
	return 0;
}

猜你喜欢

转载自blog.csdn.net/yonggandess/article/details/89204535
今日推荐