MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++中创建新进程的方法与实现

2021-10-011.8k 阅读

C++ 创建新进程的方法概述

在 C++ 编程中,创建新进程是一项常见且重要的任务。新进程的创建可以用于多种场景,比如并行处理任务、运行外部程序等。在不同的操作系统平台上,C++ 创建新进程的方法有所不同,但总体上可以分为使用操作系统原生 API 和使用跨平台库两种方式。

Windows 平台下创建新进程

在 Windows 操作系统中,主要使用 CreateProcess 函数来创建新进程。这个函数功能强大,能够对新进程的创建过程进行详细的控制。

CreateProcess 函数详解

CreateProcess 函数的原型如下:

BOOL CreateProcess(
  LPCTSTR               lpApplicationName,
  LPTSTR                lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCTSTR               lpCurrentDirectory,
  LPSTARTUPINFO         lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);
  • lpApplicationName:指向要执行的模块的文件名。如果该参数为 NULL,则要执行的模块名必须在 lpCommandLine 参数的第一个空白分隔符之前。
  • lpCommandLine:指向以 null 结尾的字符串,该字符串指定要执行的命令行。如果 lpApplicationNameNULL,则 lpCommandLine 的第一个元素指定要执行的模块。
  • lpProcessAttributes:指向 SECURITY_ATTRIBUTES 结构的指针,该结构决定了新进程的进程对象是否可以被子进程继承。如果为 NULL,则进程句柄不能被继承。
  • lpThreadAttributes:指向 SECURITY_ATTRIBUTES 结构的指针,该结构决定了新进程的主线程对象是否可以被子进程继承。如果为 NULL,则线程句柄不能被继承。
  • bInheritHandles:一个布尔值,如果为 TRUE,新进程将继承调用进程的所有可继承句柄。
  • dwCreationFlags:指定附加的、用来控制优先类和进程创建的标志。例如,CREATE_NEW_CONSOLE 标志表示为新进程创建一个新的控制台窗口。
  • lpEnvironment:指向新进程的环境块的指针。如果为 NULL,新进程将使用调用进程的环境。
  • lpCurrentDirectory:指向以 null 结尾的字符串,该字符串指定新进程的当前驱动器和目录。如果为 NULL,新进程将使用调用进程的当前驱动器和目录。
  • lpStartupInfo:指向 STARTUPINFOSTARTUPINFOEX 结构的指针,该结构指定新进程的主窗口如何显示。
  • lpProcessInformation:指向 PROCESS_INFORMATION 结构的指针,该结构接收新进程的识别信息。

示例代码

下面是一个简单的示例,展示如何使用 CreateProcess 创建一个新进程来运行 notepad.exe

#include <windows.h>
#include <iostream>

int main() {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    // 启动记事本程序
    if (!CreateProcess(TEXT("C:\\Windows\\System32\\notepad.exe"), NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
        std::cout << "CreateProcess failed (" << GetLastError() << ")" << std::endl;
        return 1;
    }

    // 等待新进程退出
    WaitForSingleObject(pi.hProcess, INFINITE);

    // 关闭进程和线程句柄
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return 0;
}

在这个示例中,我们首先初始化了 STARTUPINFOPROCESS_INFORMATION 结构体。然后调用 CreateProcess 函数启动 notepad.exe。如果创建进程失败,会输出错误信息。接着使用 WaitForSingleObject 函数等待新进程退出,最后关闭进程和线程句柄。

Linux 平台下创建新进程

在 Linux 操作系统中,创建新进程主要使用 fork 函数。fork 函数创建一个与调用进程几乎完全相同的子进程。

fork 函数详解

fork 函数的原型如下:

#include <unistd.h>
pid_t fork(void);

fork 函数调用一次,返回两次。在父进程中,fork 返回子进程的进程 ID;在子进程中,fork 返回 0。如果 fork 失败,返回 -1,并设置 errno 来指示错误原因。

示例代码

下面是一个简单的 fork 示例:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        std::cerr << "fork failed" << std::endl;
        return 1;
    } else if (pid == 0) {
        // 子进程
        std::cout << "This is the child process. PID: " << getpid() << std::endl;
        // 子进程可以在这里执行 exec 系列函数来替换自身为其他程序
        execl("/bin/ls", "ls", "-l", NULL);
        std::cerr << "execl failed" << std::endl;
        return 1;
    } else {
        // 父进程
        std::cout << "This is the parent process. Child PID: " << pid << std::endl;
        // 父进程可以在这里等待子进程结束
        int status;
        waitpid(pid, &status, 0);
        std::cout << "Child process has terminated" << std::endl;
    }

    return 0;
}

在这个示例中,首先调用 fork 函数创建子进程。如果 fork 失败,输出错误信息并退出。在子进程中,输出自身的进程 ID,并尝试使用 execl 函数执行 /bin/ls -l 命令来列出当前目录的详细信息。如果 execl 失败,输出错误信息。在父进程中,输出子进程的进程 ID,然后使用 waitpid 函数等待子进程结束,最后输出子进程已终止的信息。

使用跨平台库创建新进程

除了使用操作系统原生 API,还可以使用跨平台库来创建新进程,这样可以在不同操作系统上使用统一的接口。其中,boost::process 库是一个不错的选择。

boost::process 库简介

boost::process 库提供了一组跨平台的函数和类,用于创建和管理进程。它封装了不同操作系统的原生进程创建 API,提供了更简洁、易用的接口。

示例代码

下面是一个使用 boost::process 库创建新进程的示例:

#include <iostream>
#include <boost/process.hpp>

namespace bp = boost::process;

int main() {
    try {
        // 创建一个子进程来执行 ls -l 命令
        bp::child c("ls", "-l", bp::std_out > bp::file("output.txt"));
        c.wait();
        std::cout << "Child process has finished" << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

在这个示例中,使用 bp::child 类创建一个子进程来执行 ls -l 命令,并将标准输出重定向到 output.txt 文件。然后调用 wait 方法等待子进程结束。如果在创建或管理进程过程中发生异常,捕获并输出异常信息。

不同方法的优缺点比较

  1. Windows 平台的 CreateProcess
    • 优点:功能非常强大,可以对新进程的各个方面进行精细控制,如进程的安全属性、环境变量、启动信息等。适合需要高度定制化新进程创建过程的场景。
    • 缺点:函数参数较多且复杂,使用起来相对困难。并且该函数是 Windows 平台特有的,不具备跨平台性。
  2. Linux 平台的 fork
    • 优点:简单直接,是 Linux 系统进程创建的基础机制。在 Linux 平台上效率较高,适合创建与父进程有一定继承关系的子进程,然后通过 exec 系列函数替换为其他程序执行。
    • 缺点:同样不具备跨平台性。而且 fork 创建的子进程几乎是父进程的完全拷贝,可能会带来一些不必要的资源复制,在某些场景下不够灵活。
  3. boost::process
    • 优点:具有跨平台性,代码可以在 Windows、Linux 等不同操作系统上使用相同的接口创建和管理进程,提高了代码的可移植性。接口相对简洁易用,封装了底层复杂的原生 API。
    • 缺点:依赖于 boost 库,如果项目中没有引入 boost 库,需要额外进行安装和配置。并且由于封装了底层 API,在某些对性能和定制化要求极高的场景下,可能无法满足需求。

进程间通信与同步

在创建新进程后,往往需要进行进程间通信(IPC)和同步,以确保各个进程之间能够协调工作。

进程间通信方式

  1. 管道(Pipe)
    • Windows 平台:可以使用匿名管道和命名管道。匿名管道用于父子进程之间的通信,而命名管道可以用于不同进程(包括不同机器上的进程)之间的通信。例如,使用 CreatePipe 函数创建匿名管道,然后通过 ReadFileWriteFile 函数进行读写操作。
    • Linux 平台:使用 pipe 函数创建匿名管道,子进程继承父进程的文件描述符后,就可以进行通信。例如:
#include <iostream>
#include <unistd.h>
#include <string>

int main() {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        std::cerr << "pipe creation failed" << std::endl;
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        std::cerr << "fork failed" << std::endl;
        close(pipefd[0]);
        close(pipefd[1]);
        return 1;
    } else if (pid == 0) {
        // 子进程
        close(pipefd[1]); // 关闭写端
        char buffer[1024];
        ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            std::cout << "Child received: " << buffer << std::endl;
        }
        close(pipefd[0]);
    } else {
        // 父进程
        close(pipefd[0]); // 关闭读端
        std::string message = "Hello from parent";
        ssize_t bytes_written = write(pipefd[1], message.c_str(), message.size());
        if (bytes_written != static_cast<ssize_t>(message.size())) {
            std::cerr << "write failed" << std::endl;
        }
        close(pipefd[1]);
        wait(nullptr);
    }

    return 0;
}
  1. 消息队列(Message Queue)
    • Windows 平台:使用 CreateMailslotCreateNamedPipe 结合消息机制实现类似消息队列的功能。
    • Linux 平台:使用 msggetmsgsndmsgrcv 等函数来创建、发送和接收消息队列中的消息。例如:
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string>

struct msgbuf {
    long mtype;
    char mtext[1024];
};

int main() {
    key_t key = ftok(".", 'a');
    if (key == -1) {
        std::cerr << "ftok failed" << std::endl;
        return 1;
    }

    int msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        std::cerr << "msgget failed" << std::endl;
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        std::cerr << "fork failed" << std::endl;
        msgctl(msgid, IPC_RMID, nullptr);
        return 1;
    } else if (pid == 0) {
        // 子进程
        msgbuf buf;
        if (msgrcv(msgid, &buf, sizeof(buf.mtext), 1, 0) == -1) {
            std::cerr << "msgrcv failed" << std::endl;
        } else {
            std::cout << "Child received: " << buf.mtext << std::endl;
        }
        msgctl(msgid, IPC_RMID, nullptr);
    } else {
        // 父进程
        msgbuf buf = {1, "Hello from parent"};
        if (msgsnd(msgid, &buf, sizeof(buf.mtext), 0) == -1) {
            std::cerr << "msgsnd failed" << std::endl;
        }
        wait(nullptr);
    }

    return 0;
}
  1. 共享内存(Shared Memory)
    • Windows 平台:使用 CreateFileMappingMapViewOfFile 等函数来创建和映射共享内存。
    • Linux 平台:使用 shmgetshmatshmdt 等函数来实现共享内存的创建、附加和分离。例如:
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>

int main() {
    key_t key = ftok(".", 'a');
    if (key == -1) {
        std::cerr << "ftok failed" << std::endl;
        return 1;
    }

    int shmid = shmget(key, 1024, IPC_CREAT | 0666);
    if (shmid == -1) {
        std::cerr << "shmget failed" << std::endl;
        return 1;
    }

    void* shmaddr = shmat(shmid, nullptr, 0);
    if (shmaddr == reinterpret_cast<void*>(-1)) {
        std::cerr << "shmat failed" << std::endl;
        shmctl(shmid, IPC_RMID, nullptr);
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        std::cerr << "fork failed" << std::endl;
        shmdt(shmaddr);
        shmctl(shmid, IPC_RMID, nullptr);
        return 1;
    } else if (pid == 0) {
        // 子进程
        std::string* data = static_cast<std::string*>(shmaddr);
        std::cout << "Child received: " << *data << std::endl;
        shmdt(shmaddr);
        shmctl(shmid, IPC_RMID, nullptr);
    } else {
        // 父进程
        std::string* data = static_cast<std::string*>(shmaddr);
        *data = "Hello from parent";
        shmdt(shmaddr);
        wait(nullptr);
    }

    return 0;
}

进程同步

  1. 信号量(Semaphore)
    • Windows 平台:使用 CreateSemaphore 函数创建信号量,WaitForSingleObject 函数等待信号量,ReleaseSemaphore 函数释放信号量。
    • Linux 平台:使用 semgetsemopsemctl 等函数来实现信号量的创建、操作和控制。例如:
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

int main() {
    key_t key = ftok(".", 'a');
    if (key == -1) {
        std::cerr << "ftok failed" << std::endl;
        return 1;
    }

    int semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        std::cerr << "semget failed" << std::endl;
        return 1;
    }

    union semun arg;
    arg.val = 1;
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        std::cerr << "semctl SETVAL failed" << std::endl;
        semctl(semid, 0, IPC_RMID, nullptr);
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        std::cerr << "fork failed" << std::endl;
        semctl(semid, 0, IPC_RMID, nullptr);
        return 1;
    } else if (pid == 0) {
        // 子进程
        struct sembuf sops = {0, -1, 0};
        if (semop(semid, &sops, 1) == -1) {
            std::cerr << "semop wait failed" << std::endl;
        }
        std::cout << "Child entered critical section" << std::endl;
        sops.sem_op = 1;
        if (semop(semid, &sops, 1) == -1) {
            std::cerr << "semop release failed" << std::endl;
        }
        semctl(semid, 0, IPC_RMID, nullptr);
    } else {
        // 父进程
        struct sembuf sops = {0, -1, 0};
        if (semop(semid, &sops, 1) == -1) {
            std::cerr << "semop wait failed" << std::endl;
        }
        std::cout << "Parent entered critical section" << std::endl;
        sops.sem_op = 1;
        if (semop(semid, &sops, 1) == -1) {
            std::cerr << "semop release failed" << std::endl;
        }
        wait(nullptr);
        semctl(semid, 0, IPC_RMID, nullptr);
    }

    return 0;
}
  1. 互斥锁(Mutex)
    • Windows 平台:使用 CreateMutex 函数创建互斥锁,WaitForSingleObject 函数等待互斥锁,ReleaseMutex 函数释放互斥锁。
    • Linux 平台:使用 pthread_mutex_t 类型的变量以及 pthread_mutex_lockpthread_mutex_unlock 函数来实现互斥锁的功能。例如:
#include <iostream>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* child_function(void* arg) {
    if (pthread_mutex_lock(&mutex) != 0) {
        std::cerr << "pthread_mutex_lock failed in child" << std::endl;
    }
    std::cout << "Child entered critical section" << std::endl;
    if (pthread_mutex_unlock(&mutex) != 0) {
        std::cerr << "pthread_mutex_unlock failed in child" << std::endl;
    }
    return nullptr;
}

int main() {
    pthread_t thread;
    if (pthread_create(&thread, nullptr, child_function, nullptr) != 0) {
        std::cerr << "pthread_create failed" << std::endl;
        return 1;
    }

    if (pthread_mutex_lock(&mutex) != 0) {
        std::cerr << "pthread_mutex_lock failed in parent" << std::endl;
    }
    std::cout << "Parent entered critical section" << std::endl;
    if (pthread_mutex_unlock(&mutex) != 0) {
        std::cerr << "pthread_mutex_unlock failed in parent" << std::endl;
    }

    if (pthread_join(thread, nullptr) != 0) {
        std::cerr << "pthread_join failed" << std::endl;
        return 1;
    }

    pthread_mutex_destroy(&mutex);
    return 0;
}

错误处理与调试

在创建新进程以及进行进程间通信和同步的过程中,错误处理和调试是非常重要的环节。

错误处理

  1. Windows 平台:在使用 CreateProcess 等函数时,如果函数返回 FALSE,可以通过调用 GetLastError 函数获取详细的错误代码,然后根据错误代码查找对应的错误信息。例如:
#include <windows.h>
#include <iostream>

int main() {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    if (!CreateProcess(TEXT("nonexistent.exe"), NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
        DWORD error = GetLastError();
        std::cout << "CreateProcess failed with error code: " << error << std::endl;
        return 1;
    }

    // 关闭进程和线程句柄
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    return 0;
}
  1. Linux 平台:在使用 forkexec 等函数时,如果函数返回错误值(如 fork 返回 -1,exec 返回 -1),可以通过 errno 全局变量获取错误代码,然后使用 strerror 函数将错误代码转换为错误信息字符串。例如:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        std::cerr << "fork failed: " << strerror(errno) << std::endl;
        return 1;
    } else if (pid == 0) {
        if (execl("/nonexistent", "nonexistent", NULL) == -1) {
            std::cerr << "execl failed: " << strerror(errno) << std::endl;
            return 1;
        }
    } else {
        int status;
        waitpid(pid, &status, 0);
    }

    return 0;
}
  1. boost::processboost::process 库在发生错误时会抛出异常。可以通过捕获 boost::process::process_error 等异常类型来处理错误,并获取错误信息。例如:
#include <iostream>
#include <boost/process.hpp>

namespace bp = boost::process;

int main() {
    try {
        bp::child c("nonexistent", bp::std_out > bp::file("output.txt"));
        c.wait();
    } catch (const bp::process_error& e) {
        std::cerr << "Process error: " << e.what() << std::endl;
    }

    return 0;
}

调试

  1. 使用调试工具
    • Windows 平台:可以使用 Visual Studio 等集成开发环境进行调试。在调试过程中,可以设置断点,观察变量的值,单步执行代码,查看调用堆栈等,以定位创建进程和进程间通信过程中的问题。
    • Linux 平台:可以使用 gdb 调试器。通过在编译时加上 -g 选项生成调试信息,然后使用 gdb 加载可执行文件,设置断点,进行调试。例如:
g++ -g -o my_program my_program.cpp
gdb my_program
(gdb) break main
(gdb) run
  1. 日志记录 在代码中添加日志记录语句,记录进程创建、通信和同步过程中的关键信息,如函数调用结果、变量值等。可以使用 std::coutstd::cerr 进行简单的日志输出,也可以使用专门的日志库,如 spdlog,来进行更灵活和强大的日志管理。例如:
#include <iostream>
#include <spdlog/spdlog.h>

int main() {
    auto logger = spdlog::stdout_logger_mt("main_logger");

    pid_t pid = fork();
    if (pid < 0) {
        logger->error("fork failed: {}", strerror(errno));
        return 1;
    } else if (pid == 0) {
        logger->info("This is the child process. PID: {}", getpid());
        // 子进程相关代码
    } else {
        logger->info("This is the parent process. Child PID: {}", pid);
        // 父进程相关代码
    }

    return 0;
}

性能优化与资源管理

在创建新进程并进行相关操作时,性能优化和资源管理是需要考虑的重要方面。

性能优化

  1. 减少进程创建开销
    • 复用进程:在某些场景下,如果频繁创建和销毁进程会带来较大的开销。可以考虑复用进程,例如使用进程池技术。在 Windows 平台上,可以使用线程池结合 CreateProcess 来实现类似功能;在 Linux 平台上,可以预先创建一定数量的子进程,通过管道等方式与父进程通信并分配任务。
    • 优化参数设置:在使用 CreateProcessfork 等函数时,合理设置参数可以提高进程创建效率。例如,在 Windows 平台上,对于不需要继承句柄的场景,将 bInheritHandles 设置为 FALSE,可以减少系统资源的复制。
  2. 优化进程间通信
    • 选择合适的通信方式:根据实际需求选择合适的进程间通信方式。如果数据量较小且对实时性要求较高,管道可能是一个不错的选择;如果数据量较大且需要异步通信,消息队列或共享内存可能更合适。同时,要注意不同通信方式的性能特点,如管道的读写操作可能会有阻塞,而共享内存的同步开销需要妥善处理。
    • 减少数据拷贝:在进程间通信过程中,尽量减少数据的不必要拷贝。例如,使用共享内存时,可以直接在共享内存区域进行数据操作,避免将数据从共享内存复制到进程的私有空间再处理。

资源管理

  1. 文件描述符管理 在 Linux 平台上,使用 fork 创建子进程时,子进程会继承父进程的文件描述符。需要注意在进程结束时正确关闭文件描述符,避免文件描述符泄漏。例如,在父进程中打开了一个文件,在 fork 后,父进程和子进程都有该文件的文件描述符,需要根据实际需求在合适的地方关闭文件描述符。
  2. 内存管理 无论是在 Windows 还是 Linux 平台上,创建新进程后,要注意内存的合理使用和释放。在进程间使用共享内存时,要确保在不再使用共享内存时正确地分离和删除共享内存段。同时,在进程内部也要注意动态内存的分配和释放,避免内存泄漏。例如,在使用 boost::process 库创建进程时,如果在进程执行过程中分配了动态内存,要确保在进程结束前正确释放这些内存。

实际应用场景

  1. 并行计算:在科学计算、数据处理等领域,常常需要将一个大任务分解为多个子任务并行执行,以提高计算效率。可以通过创建多个进程,每个进程负责一部分子任务,然后通过进程间通信和同步机制协调各个进程的工作。例如,在图像识别任务中,可以将图像分割为多个部分,每个进程处理一个部分的特征提取,最后将结果合并。
  2. 运行外部程序:在开发工具、脚本等场景下,需要运行外部程序来完成特定功能。比如在一个自动化测试框架中,通过创建新进程来运行测试用例程序,然后获取测试结果。在 Windows 平台上可以使用 CreateProcess 运行各种可执行文件,在 Linux 平台上可以使用 fork 结合 exec 系列函数运行外部脚本或二进制程序。
  3. 服务进程管理:在服务器开发中,可能需要创建多个服务进程来提供不同的服务。例如,一个 Web 服务器可能会创建多个进程分别负责处理 HTTP 请求、数据库连接管理等任务。通过合理的进程管理和进程间通信,可以提高服务器的性能和稳定性。

通过深入了解 C++ 在不同平台下创建新进程的方法、进程间通信与同步、错误处理、调试、性能优化以及实际应用场景,可以更好地利用进程机制开发出高效、稳定的应用程序。无论是在系统级编程还是应用开发中,这些知识都具有重要的价值。