Windows系统编程——创建新进程

本文概述:

  1. 简要介绍Windows程序的启动过程;
  2. 详细介绍在控制台程序、窗口程序中获取命令行、环境变量和当前目录的方法;
  3. 以Windows API和C#为例,编写创建新进程、设置并获取命令行等数据的完整代码;
  4. 介绍环境变量、命令行在VS程序调试、业务开发平台切换中的实际应用。

程序启动过程

  1. 关键概念:启动函数、入口函数。
  2. 入口函数有4种(main、wmain、WinMain、wWinMain),main、wmain针对控制台程序,WinMain、wWinMain针对窗口程序,具体使用哪种,由程序的子系统类别确定,子系统类别可以在Visual Studio中设置;如果没有设置,以上述第一个出现的函数作为入口函数。w开头的入口函数用于Unicode编码的项目。
  3. 链接器根据入口函数种类确定启动函数,启动函数先进行必要的环境设置(包括设置命令行、环境变量等),再调用入口函数。
  4. 总结:启动程序时,程序被加载到内存,启动函数执行,先进行环境设置,然后调用入口函数,同时将命令行等参数以实参方式传递给入口函数。入口函数对开发者可见,因此开发者可以获取这些参数。

进程“四大件”

基地址

加载资源时需要使用进程基地址,获取方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <windows.h>
#include <tchar.h>

extern "C" const IMAGE_DOS_HEADER __ImageBase;

void test() {
// GetModuleHandle(NULL)获得当前可执行文件的基地址,而非DLL文件的基地址
HMODULE hModule = GetModuleHandle(nullptr);

HINSTANCE hInst = (HINSTANCE)&__ImageBase;
HMODULE hModule2 = nullptr;

// 获取当前函数test所在DLL的基地址
GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCTSTR)test, &hModule2);
}

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
TCHAR moduleName[256];
GetModuleFileName(GetModuleHandle(nullptr), moduleName, 256);
test();
return 0;
}

命令行

1、直接获取入口函数参数(控制台程序)

1
2
3
4
5
6
7
int _tmain(int argc, TCHAR** argv) {
_tprintf(_T("argc=%d\n"), argc);
for (size_t i = 0; i < argc; i++)
_tprintf(_T("argv[%d]=%s\n"), i, argv[i]);
system("pause");
return 0;
}

2、直接获取入口函数参数(窗口程序)

1
2
3
4
5
6
7
8
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine,
_In_ int nCmdShow)
{
MessageBox(NULL, lpCmdLine, _T("温馨提示"), MB_OK);
return 0;
}

3、通过函数获取(控制台程序、窗口程序)

当前,开发复杂项目时通常使用既有框架(MFC、QT等),有些框架对入口函数进行了封装,开发者无法直接获取入口函数实参,因此只能通过这种方式获取命令行等参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine,
_In_ int nCmdShow)
{
int nNumArgv;
PWSTR* ppArgv = CommandLineToArgvW(GetCommandLineW(), &nNumArgv);
for (size_t i = 0; i < nNumArgv; i++) {
TCHAR info[256];
swprintf_s(info, _T("argv[%d]=%s"), i, ppArgv[i]);
MessageBox(NULL, info, _T("温馨提示"), MB_OK);
}
return 0;
}

int _tmain(int argc, TCHAR** argv) {
int nNumArgv;
PWSTR* ppArgv = CommandLineToArgvW(GetCommandLineW(), &nNumArgv);
for (size_t i = 0; i < nNumArgv; i++)
wprintf(L"argv[%d]=%s\n", i, ppArgv[i]);

system("pause");
return 0;
}

环境变量

系统会为进程设置初始环境变量,其来源包括3个部分:内置环境变量、用户环境变量和系统环境变量。

子进程可以继承父进程的环境变量。

1、在控制台程序中通过实参获取环境变量

1
2
3
4
5
6
7
8
9
10
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) {
int i = 0;
// envp最后一个是nullptr
while (envp[i++] != nullptr) {
wprintf(L"%s\n", envp[i]);
}

system("pause");
return 0;
}

2、通过GetEnvironmentStrings()获取环境变量块(适用控制台程序、窗口程序,但需要编码解析环境块)

1
2
3
4
5
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) {
PTSTR pEnvBlock = GetEnvironmentStrings();
system("pause");
return 0;
}

3、实用函数

  • GetEnviromentVariable()、ExpandEnviromentStrings():根据环境变量名查询环境变量值。
  • SetEnviromentVariable():设置环境变量值。

当前目录

系统内部会跟踪每个进程的当前驱动器和当前目录,如果不提供文件的完整路径名,进程将在当前驱动器的当前目录查找文件。

如果采用D:ReadMe.txt的方式指定文件路径,默认查找D盘的当前路径,在该路径下找文件;如果没有当前目录,则在D盘根目录下找文件。

1
2
3
4
5
6
7
8
9
10
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) {
DWORD num = GetCurrentDirectory(0, nullptr);
PTSTR info = new TCHAR[num];
GetCurrentDirectory(num, info);
_tprintf(_T("%s"), info);
delete[] info;

system("pause");
return 0;
}

创建新进程的方式

Windows API

在Windows操作系统上,Windows API是创建进程的最底层方式;C#是上层封装。

创建新进程的函数是CreateProcess,其原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
BOOL CreateProcessW(
// 应用程序路径,一般不适用该参数,设置为NULL
[in, optional] LPCWSTR lpApplicationName,
// 进程命令行:空格分隔,第一个为应用程序路径
[in, out, optional] LPWSTR lpCommandLine,

// 指定进程权限和线程权限,以及本进程是否继承父进程的句柄
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,

// 指定进程创建标识符
[in] DWORD dwCreationFlags,

// 指定本进程的环境变量,为NULL则继承父进程的环境
[in, optional] LPVOID lpEnvironment,

// 指定本进程的当前目录
[in, optional] LPCWSTR lpCurrentDirectory,

// 进程创建的其他信息
[in] LPSTARTUPINFOW lpStartupInfo,

// 返回本进程及其主线程的句柄和ID
[out] LPPROCESS_INFORMATION lpProcessInformation
);

本文重点关注程序名、命令行和环境变量这三个参数,封装创建进程并设置这三个参数的函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <Windows.h>
#include <stdio.h>
#include <tchar.h>

/// @brief 封装创建进程的函数
/// @param lpApplicationName 程序名
/// @param numCmds 命令行数量(不包含程序名)
/// @param cmds 命令行字符串数组
/// @param numEnvs 环境变量数量
/// @param envNames 环境变量名字符串数组
/// @param envValues 环境变量值字符串数组
/// @return 成功返回TRUE,失败返回FALSE
BOOL CreateProcessWithCmdAndEnviroment(LPCWSTR lpApplicationName,
DWORD numCmds, WCHAR* cmds[],
DWORD numEnvs, WCHAR* envNames[], WCHAR* envValues[]) {
// 计算总的命令行字符串长度
int num = 0;
num += (wcslen(lpApplicationName) + 3);
for (size_t i = 0; i < numCmds; i++)
num += (wcslen(cmds[i]) + 1);
num++;

// 组装命令行字符串
WCHAR* cmd = new WCHAR[num];
wcscpy_s(cmd, num, L"\""); // 用引号包裹程序路径,应对路径中包含空格的情况
wcscat_s(cmd, num, lpApplicationName);
wcscat_s(cmd, num, L"\"");
for (size_t i = 0; i < numCmds; i++) {
wcscat_s(cmd, num, L" ");
wcscat_s(cmd, num, cmds[i]);
}

for (size_t i = 0; i < numEnvs; i++)
SetEnvironmentVariable(envNames[i], envValues[i]);

STARTUPINFO startupInfo = { sizeof(startupInfo) };
PROCESS_INFORMATION processInfo;
BOOL flag = CreateProcess(nullptr, cmd, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startupInfo, &processInfo);
delete[] cmd;

return flag;
}

int _tmain() {
// 设置命令行参数
WCHAR cmd1[] = L"hello";
WCHAR cmd2[] = L"world";
WCHAR* cmds[] = { cmd1,cmd2 };

// 设置环境变量参数
WCHAR envName1[] = L"env1";
WCHAR envName2[] = L"env2";
WCHAR envValue1[] = L"envValue1";
WCHAR envValue2[] = L"envValue2";
WCHAR* envNames[] = { envName1,envName2 };
WCHAR* envValues[] = { envValue1,envValue2 };

CreateProcessWithCmdAndEnviroment(L"E:\\Desktop\\WindowsViaC\\Debug\\LHTest.exe", 2, cmds, 2, envNames, envValues);
return 0;
}

为验证该函数的正确性,编写一个测试程序,用于打印命令行参数和环境变量,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <tchar.h>

/// @brief 打印指定名称的环境变量
/// @param name 环境变量名
void PrintEnviromentVariable(PCTSTR name) {
PTSTR value = NULL;
DWORD res = GetEnvironmentVariable(name, value, 0);
if (res != 0) {
DWORD size = res * sizeof(TCHAR);
value = (PTSTR)malloc(size);
GetEnvironmentVariable(name, value, size);
_tprintf(_T("%s=%s\n"), name, value);
free(value);
}
else
_tprintf(_T("'%s'=<unknown value>\n"), name);
}

int _tmain() {
// 打印环境变量名
wprintf(L"环境变量:\n");
PrintEnviromentVariable(_T("env1"));
PrintEnviromentVariable(_T("env2"));

// 打印命令行
wprintf(L"\n命令行:\n");
int nNumArgv;
PWSTR* ppArgv = CommandLineToArgvW(GetCommandLineW(), &nNumArgv);
for (size_t i = 0; i < nNumArgv; i++)
wprintf(L"argv[%d]=%s\n", i, ppArgv[i]);

system("pause");
return 0;
}

C#

由于Windows API使用起来较为复杂,业务开发时通常使用更高级的编程语言或程序库,例如C#。

使用C#创建新进程的核心是ProcessStartInfo类,其主要属性如下图所示:

创建新进程的程序示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.Data;
using System.Diagnostics;
using System.IO;

namespace Test {
class Program {
static void Main(string[] args) {
ProcessStartInfo process = new ProcessStartInfo();
process.UseShellExecute = false;
process.FileName = @"LHTest.exe";
process.Arguments = string.Format("{0} {1}", "C#", "Windows");
process.Environment.Add("env1", "This is env1");
process.Environment.Add("env2", "This is env2");
Process.Start(process);

Console.ReadLine();
}
}
}

业务开发场景

VS程序调试小技巧

以QT为例,在Visual Stusio中开发QT桌面软件时,如果没有指定QT依赖库路径,调试时会报错。

大多数开发者的做法是:将QT依赖库的路径直接添加到PATH系统变量(或PATH用户变量)中。

以上做法可行,但存在若干不足:(1)系统对PATH环境变量的长度有限制,随意增加PATH变量长度存在风险;(2)无法应对多版本QT的开发调试。

最佳做法:在Visual Stusio中针对单个项目设置环境变量,如下图所示:

平台切换最佳方案

在某些业务场景下,需要在程序1中启动程序2,并且希望在程序2启动后判断自己是否由程序1启动(众所周知,程序2也可以由用户双击启动),然后根据启动方式做不同的操作。

为此,我们可以借助命令行参数进行判断:

  • 程序1启动程序2之前,先生成一个不会重复的标识字符串(例如GUID),将该字符串写到本地文件(约定文件位置);
  • 程序1启动程序2时,将该字符串以命令行参数的方式传递给新进程;
  • 新进程启动后,获取命令行参数,并读取本地文件里的标识字符串,将两者进行对比,相同则说明该进程是由程序1启动的,不同则不是。

评论