CreateProcess函数原型

CreateProcess 函数用于创建一个新进程(子进程)及其主线程,其函数原型如下:

BOOL CreateProcess(
LPCWSTR lpApplicationName,//指向可执行模块名称的指针
LPWSTR lpCommandLine,//指向命令行字符串的指针。
LPSECURITY_ATTRIBUTES lpProcessAttributes,//指向 SECURITY_ATTRIBUTES 结构的指针,指定新进程的安全属性。
LPSECURITY_ATTRIBUTES lpThreadAttributes,//指向 SECURITY_ATTRIBUTES 结构的指针,指定新线程的安全属性。
BOOL bInheritHandles,//如果为 TRUE,新进程将继承调用进程的句柄。
DWORD dwCreationFlags,//指定附加的、用来控制优先类和进程的创建的标志。
LPVOID lpEnvironment,//指向新进程的环境块的指针。如果为 NULL,新进程将使用调用进程的环境。
LPCWSTR lpCurrentDirectory,//指向新进程的当前目录的指针。如果为 NULL,新进程将使用调用进程的当前目录。
LPSTARTUPINFOW lpStartupInfo,//指向 STARTUPINFOW 结构的指针,指定新进程的主窗口特性。
LPPROCESS_INFORMATION lpProcessInformation//指向 PROCESS_INFORMATION 结构的指针,接收新进程的标识符和句柄。
);

为了演示用一个进程来启动一个新的进程,我们在这里首先准备一个NewApp的进程,表示即将被CreateProcess 函数启动的进程,NewApp的代码如下,代码打印出进程的命令行启动参数

//这是NewApp程序
#include <iostream>
#include <Windows.h>
int main(int argc, char** argv)
{
for (int i = 0; i < argc; i++) {
std::wcout << argv[i] << std::endl;
}
system("pause");
}

lpApplicationName和lpCommandLine

lpApplicationName:即将启动的exe程序路径,该参数是一个字符串。我们可以传相对路径或者绝对路径,Windows在启动的时候,会按照一定的顺序查找exe。

1、如果该参数传递的是exe全路径,操作系统会直接启动指定的全路径exe,如果找不到要启动的exe文件,CreateProcess会启动失败。

2、查找主进程exe同级目录下是否存在要启动的exe文件。

3、查询主进程的当前目录下是否存在要启动的exe文件,一般情况下,主进程的当前目录和主进程exe是同一个目录,但是也不绝对,我们可以手动修改程序的当前目录。

4、查询Window系统目录下是否存在要启动的exe文件,就是GetSystemDirectory获取到的文件夹。

5、查询Window目录下是否存在要启动的exe文件。

6、查询环境变量Path所表示的那些目录下,数据存在要启动的exe文件。

从上面的流程看,操作系统查找要指定的exe文件是一个很复杂的流程,所以如果条件允许,我们建议传递全路径。如果不允许,也至少应该是将exe文件放到主进程exe的相对目录下,这也是我经常采用的一种方式。

lpCommandLine:表示要启动的进程需要接受的命令行参数。

接下来,我们用主进程来启动NewApp.exe ,主进程的代码如下:

#include <iostream>
#include <Windows.h>
int main()
{
//即将启动的exe程序路径
LPCWSTR lpApplicationName = L"D:\\project\\ConsoleApp1\\x64\\Debug\\NewApp.exe";
//即将传递给exe程序的命令行参数
LPWSTR lpCommandLine = const_cast<LPWSTR>(L"key1=value1 --key2=value2 /key3=value3 --key4 value4 /key5 value5"); // 定义启动信息和进程信息结构
STARTUPINFOW si;
PROCESS_INFORMATION pi; // 初始化启动信息结构
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi)); BOOL ret=CreateProcess(lpApplicationName,
lpCommandLine,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
NULL,
NULL,
&si,
&pi);
system("pause");
return 0;
}

运行以上代码之后,我们会看到以下运行结果,很明显,我们的子进程NewApp被成功启动,并且打印出了主进程传递的命令行参数。

lpProcessAttributes、lpThreadAttributes、bInheritHandles

这两个参数代表子进程的进程安全属性和线程安全属性,都指向SECURITY_ATTRIBUTES 的一个结构体,一般情况下,我们可以传NULL,表示子进程使用默认的进程安全属性和 线程安全属性。bInheritHandles表示子进程是否可以继承父进程的句柄(父进程设置了允许继承的安全属性)。如果设置为TRUE,则表示可以继承。

dwCreationFlags

dwCreationFlags参数用于指定创建新进程的时候的一些附加标志,用于控制新进程的一些行为,下面是常用的一些标识:

1、CREATE_NEW_CONSOLE:为新进程创建一个新的控制台窗口。上面的代码我们使用了这个表示,主进程和新进程是两个控制台窗口,如果没有这个flag的话,主进程和新创建的进程共用一个 控制台程序。

2、CREATE_NO_WINDOW:不要为新进程启动一个窗口。如果我们需要创建一个在后台运行没有界面的进程的话,可以使用这个flag。

3、CREATE_SUSPENDED:创建新进程,不要立即执行,将进程挂起,直到调用ResumeThread函数的时候,才开始调用进程。如果我们创建多个子进程之后,需要有一个统一的同步策略,由主进程统一控制多个子进程的执行顺序的话,可以使用这个flag,在上面的代码上稍微做一点修改,新增一个flag和新增两行代码,运行起来,你会发现新创建的进程不会立即执行,而是等待5s之后由主进程控制它继续执行。

	BOOL ret = CreateProcess(lpApplicationName,
lpCommandLine,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE | CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi); Sleep(5000);
ResumeThread(pi.hThread);

4、CREATE_UNICODE_ENVIRONMENT:创建Unicode字符的环境变量,如果使用了该flag,那么lpEnvironment参数指向的环境变量块将使用Unicode,否则将使用ANSI字符。

5、CREATE_DEFAULT_ERROR_MODE:每个进程都有自己的一个错误模式,可以通过SetErrorMode函数设置,默认情况下,新进程继承父进程的错误模式,如果使用该flag,新进程将使用默认的错误模式。

6、DETACHED_PROCESS:新进程和父进程分离,不继承父进程的控制台。该flag会阻止子进程访问父进程的控制台窗口,一般我们也很少使用该flag。这个flag和CREATE_NO_WINDOW作用一致,不能同时使用,否则程序会报错。

lpEnvironment

默认情况下,子进程将继承父进程的环境变量。如果想给子进程单独设置环境变量块,可以传递该参数。下面我们修改子进程NewApp的代码,在NewApp中输出环境变量VAR1的值,这个环境变量将由主程序传递给子进程。

#include <iostream>
#include <Windows.h>
int main(int argc, char** argv)
{
// 定义环境变量名
LPCWSTR envVarName = L"VAR1";
// 分配缓冲区,接受环境变量值
std::wstring envVarValue(1024, L'\0');
// 获取环境变量值的长度
DWORD bufferSize = GetEnvironmentVariable(envVarName, &envVarValue[0], 1024); // 输出环境变量值
std::wcout << envVarName << L" = " << envVarValue << std::endl;
while (true) {
Sleep(1000);
}
}

接下来我们来修改主程序的代码:

#include <iostream>
#include <Windows.h>
int main()
{
//即将启动的exe程序路径
LPCWSTR lpApplicationName = L"D:\\project\\ConsoleApp1\\x64\\Debug\\NewApp.exe";
//即将传递给exe程序的命令行参数
LPWSTR lpCommandLine = const_cast<LPWSTR>(L"key1=value1 --key2=value2 /key3=value3 --key4 value4 /key5 value5"); // 定义启动信息和进程信息结构
STARTUPINFOW si;
PROCESS_INFORMATION pi; // 初始化启动信息结构
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi)); WCHAR* envVarChars = const_cast<WCHAR*>(L"VAR1=VALUE1\0VAR2=VALUE2\0\0\0\0");//需要传递给子进程的环境变量块
BOOL ret = CreateProcess(lpApplicationName,
lpCommandLine,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,
envVarChars,
NULL,
&si,
&pi);
system("pause");
return 0;
}

上述代码中envVarChars 是需要传递给子进程的环境变量块,它是一个字符串。格式为key=value\0,变量名和变量值用等号分开,后面跟一个\0区分多个变量。

如果环境变量参数lpEnvironment是一个Unicode字符串的话,注意dwCreationFlags要加上CREATE_UNICODE_ENVIRONMENT这个flag,否则进程会创建失败。

请注意,ANSI 环境块由两个零字节终止:一个用于最后一个字符串,另一个用于终止该块。 Unicode 环境块由四个零字节终止:两个用于最后一个字符串,两个用于终止该块。

上面代码运行如下:我们可以看到控制台输出了VAR1的变量值为VALUE1,这个变量是从父进程那里继承来的。同时我们在ProcessExplorer软件中,可以看到子进程的环境变量有两个,分别是VAR1和VAR2。这说明子进程成功继承了父进程的环境变量。

lpCurrentDirectory

设置子进程的当前目录,如果为 NULL,新进程将使用调用进程的当前目录。我们修改NewApp的代码,在代码中输出NewApp进程的当前目录

#include <iostream>
#include <Windows.h>
int main(int argc, char** argv)
{
// 定义缓冲区大小
WCHAR currentDir[MAX_PATH];
// 获取当前工作目录
DWORD length = GetCurrentDirectory(MAX_PATH, currentDir);
// 输出当前工作目录
std::wcout << L"Current Directory: " << currentDir << std::endl;
system("pause");
}

然后我们修改主进程CreateProcess的参数,lpCurrentDirectory参数:

	LPCWSTR cuuDir = L"D:\\Test\\";
BOOL ret = CreateProcess(lpApplicationName,
lpCommandLine,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
NULL,
cuuDir,//设置子进程的当前目录
&si,
&pi);

调试上述代码之后,新创建的进程会输出D:\\Test\\,运行效果如下:

lpStartupInfo

指定新进程的主窗体特性(指定窗体大小、初始位置、控制台背景颜色字体颜色、窗口标题等信息)、标准输入、输出、错误设备句柄等,其定义如下:

typedef struct _STARTUPINFOW {
DWORD cb;//结构的大小,以字节为单位。
LPWSTR lpReserved;//保留,必须为 NULL。
LPWSTR lpDesktop;//指向一个以空字符结尾的字符串,指定桌面名称。
LPWSTR lpTitle;//指向一个以空字符结尾的字符串,指定新进程的窗口标题。
DWORD dwX;//指定窗口的初始位置X
DWORD dwY;//指定窗口的初始位置Y
DWORD dwXSize;/指定窗口的初始大小X
DWORD dwYSize;//指定窗口的初始大小Y
DWORD dwXCountChars;//指定屏幕缓冲区的宽度,以字符为单位。
DWORD dwYCountChars;//指定屏幕缓冲区的高度,以字符为单位。
DWORD dwFillAttribute;//指定新控制台窗口的文本和背景颜色。
DWORD dwFlags;//指定有效的 STARTUPINFO 成员。
WORD wShowWindow;//指定窗口显示状态。
WORD cbReserved2;//保留,必须为 0。
LPBYTE lpReserved2;//保留,必须为 NULL。
HANDLE hStdInput;//新进程的标准输入句柄
HANDLE hStdOutput;//新进程的标准输出句柄
HANDLE hStdError;//新进程的标准错误设备句柄
} STARTUPINFOW, *LPSTARTUPINFOW;

关于这些信息大家可以自定去设置和实验,文章中就不给大家一一设置和实现了。

lpProcessInformation

lpProcessInformation是一个输出参数,进程创建之后,会把子进程的进程句柄、线程句柄、进程id、线程id返回给主进程。参数的定义如下:

typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

首先我们来说线程Id和进程Id,我们在创建成功进程之后,将进程id和线程id打印出来,代码如下:

	BOOL ret = CreateProcess(lpApplicationName,
lpCommandLine,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE ,
NULL,
NULL,
&si,
&pi);
std::cout << "dwProcessId = " << pi.dwProcessId << std::endl;
std::cout << "dwThreadId = " << pi.dwThreadId << std::endl;

输出结果如下:

可以很明显看到进程Id为18492,线程Id为18736.然后我们到进程管理器查看这个进程的Id就是18492,如下:

关于线程Id:18736在进程管理器是无法查看的,但是我们可以通过ProcessExplorer软件查看主线程Id,查看方式如下:选中指定的进程,点击下方的Threads 标签栏,可以查看当前进程的所以线程,我们会发现只有一个主线程:18736.这和我们主程序打印出来的结果是一致的。

默认情况下,ProcessExploer下方的tab栏是不显示的,如果需要显示,可以通过选中View->ShowLowerPane来打开

关于进程Id和线程Id需要注意的是:Windows有一个ID编号池,用于给进程和线程分配ID,并且这个ID池中的ID永远不会重复,这意味着永远不会有进程ID和线程ID重复。不过当进程线程推出的时候,这个进程的ID会回到ID池,后续可能会分配给别的进程。

试想一下。我用进程111创建了一个子进程222,子进程222的父进程确实是111,但是因为某些原因进程111被回收,并且重新分配给了一个其他的进程,如果这个时候,子进程再拿着父进程的ID:111去处理业务的话,就会出现意想不到的错误。同理,线程ID也是如此。所以,一般情况下,我们习惯用句柄操作具体业务,很少会使用ID来处理业务。那么hProcess参数和hThread参数就代表子进程的进程句柄和主线程句柄,父进程可以使用这两个参数做对应的业务逻辑。

Windows编程----CreateProcess函数的更多相关文章

  1. windows 编程—— 常用函数 与 操作

    目录: MessageBox() 和 PlaySound() 获得窗口 或屏幕大小 获得字体大小 输出文字 屏蔽和显示控制台窗口 1. MessageBox() 和 PlaySound() Messa ...

  2. windows 编程—— 使用函数笔记

    目录: 创建滚动条 滚动条函数(新老版本) 取得设备内容句柄hdc 设置 hdc 中的属性 画点画线 画填充图形 使用自定义的 画笔 和 画刷 矩形.区域和剪裁 关于GDI映像模式 其他常用的方便计算 ...

  3. Windows编程MessageBox函数

    API: int MessageBox(HWND hWnd, LPCTSTRlpText, LPCTSTRlpCaption, UINTuType); MSDN描述: This function cr ...

  4. 2.C++标准库函数:getline函数 定界流输入截取函数 -windows编程

    引言:今天工作遇到了一个需要按行读取txt文件数据的需求,查询了一下getline()函数,发现这竟然是一个C++的标准库函数,而且设计的很好,特地做一下记录.getline本质是一个定界流输入截取函 ...

  5. Windows编程入门程序详解

    引用:http://blog.csdn.net/jarvischu/article/details/8115390 1.     程序 /******************************* ...

  6. Direct3D 10学习笔记(四)——Windows编程

    本篇将简单整理基本的Windows应用程序的实现,并作为创建Direct3D 10应用程序的铺垫.具体内容参照< Introduction to 3D Game Programming with ...

  7. windows编程 进程的创建销毁和分析

    Windows程序设计:进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,在Windows编程环境下,主要由两大元素组成: • 一个是操作系统用来管理进程的内核对象.操作系统使用内 ...

  8. 【Windows编程】系列第六篇:创建Toolbar与Statusbar

    上一篇我们学习了解了如何使用Windows GDI画图,该应用程序都是光光的静态窗口,我们使用Windows应用程序,但凡稍微复杂一点的程序都会有工具栏和状态栏,工具栏主要用于一些快捷功能按钮.比如典 ...

  9. 【Windows编程】系列第十一篇:多文档界面框架

    前面我们所举的例子中都是单文档界面框架,也就是说这个窗口里面的客户区就是一个文档界面,可以编写程序在里面输入或者绘制文本和图形输出,但是不能有出现多个文档的情况.比如下面的UltraEdit就是一个典 ...

  10. 【Windows编程】系列第十篇:文本插入符

    大家知道,在使用微软的编程环境创建工程时会让你选择是控制台模式还是Windows应用程序.如果选择控制台的console模式,就会在运行时出现一个黑洞洞的字符模式窗口,里面就有等待输入一闪一闪的插入符 ...

随机推荐

  1. Golang实战:深入解析国密算法在Go语言中的应用与优化

    Golang实战:深入解析国密算法在Go语言中的应用与优化 引言 随着信息技术的迅猛发展,数据安全成为企业和个人关注的焦点.国密算法(SM系列算法)作为我国自主研发的密码算法标准,逐渐在各个领域中得到 ...

  2. idea 2023.1.2 破解/激活

    参考:(感谢作者提供的破解教程,谢谢) IntelliJ IDEA 2023.1.1最新激活破解教程(永久激活,亲测有效) - 异常教程 (exception.site) 下载idea : 链接:ht ...

  3. 如何判断平台是x86还是arm

    case $(uname -m) in x86_64) echo x86;; aarch64) echo arm;; esac ref 上面的代码片改自这里 https://stackoverflow ...

  4. 龙哥量化:通达信财富币不够怎么办:K线训练营100%胜率,赚财富币

    通达信app的K线训练营中,[K线训练]和[K线对战]都需要花费[5财富币]进行训练,[K线对战]胜利的话可以获得10财富币.注意:是对战,对战,对战,那怎么才能每场都胜呢,哈哈,我们找到历史K线,对 ...

  5. pytorch模型降低计算成本和计算量

    下面是如何使用PyTorch降低计算成本和计算量的一些方法: 压缩模型:使用模型压缩技术,如剪枝.量化和哈希等方法,来减小模型的大小和复杂度,从而降低计算量和运行成本. 分布式训练:使用多台机器进行分 ...

  6. Flutter一些概念(一)

    1 简述Flutter是什么以及它的主要优势 Flutter是一种由Google开发的开源移动应用开发框架,可以用于构建高度定制化.美观并且性能卓越的移动应用程序,其主要优势有: 跨平台,一次编码,可 ...

  7. 最新AI智能体开发案例:Coze工作流必备神器!教你用Coze平台搭建「扣子工作流生成神器」智能体!

    点击 疯狂老包  > 点击右上角"···" > 关注我 各位小伙伴们,大家好呀!我是疯狂老包.我精心打造的<疯狂AI智能体开发:100个实战案例, 从 入门到精通 ...

  8. SpringBoot 集成腾讯云(对象存储、短信)

    https://developer.aliyun.com/article/831473 https://blog.csdn.net/weixin_45626288/article/details/11 ...

  9. ClickHouse-2接口

    客户端 ClickHouse提供了两个网络接口(两个都可以选择包装在TLS中以增加安全性): HTTP, 包含文档,易于使用. Native TCP,简单,方便使用. 在大多数情况下,建议使用适当的工 ...

  10. Redis常用指令(详细)

    # Redis 常用指令## 基础命令### 启动与连接```bash# 启动 Redis 服务redis-server# 连接 Redis 客户端redis-cli```### 基本操作```bas ...