上一章节:
本章节代码
目录
[四、多进程在 Linux 和 Windows 系统中的实现与进程间通信](#四、多进程在 Linux 和 Windows 系统中的实现与进程间通信)
[1、Linux 系统](#1、Linux 系统)
[1.1、常见实现方式在 Linux 中,创建新进程最常用的方式是使用](#1.1、常见实现方式在 Linux 中,创建新进程最常用的方式是使用)
[1.2、Linux 系统](#1.2、Linux 系统)
[Linux 实现示例](#Linux 实现示例)
[1.3、Windows 实现示例](#1.3、Windows 实现示例)
一、引言
深入剖析多进程:从原理到跨平台实践
在软件开发的宏大版图中,多进程 技术犹如一座桥梁,横跨在提升系统性能与处理复杂任务的两岸 。无论是高并发的服务器场景,还是对资源进行高效管理的桌面应用,多进程都扮演着不可或缺的角色。本文从多进程实现高并发的原理出发,逐步探索多进程在 Linux 和 Windows 系统下的运作机制。
二、理解高并行/高并发,解锁高性能系统的密钥
在讲解多进程之前,先让我们理解"什么是并行?什么是并发?并了解二者的区别"
并行的前提是多核CPU ,指的是多个任务同时在多个核心上同时执行。如下图:

并发执行 是指系统能够同时处理多个任务,但这些任务并不一定在同一时刻执行,而是通过快速切换来模拟同时执行的效果。

多进程正是利用了并发和并行的优势来实现高并发。以 Web 服务器为例,当大量用户同时访问网站时,服务器可以为每个请求创建一个新的进程来处理。在单核 CPU 环境下,操作系统通过快速切换这些进程,使得它们看起来像是在同时执行。而在多核 CPU 环境下,不同的进程可以被分配到不同的核心上并行执行,大大提高了系统的吞吐量。
三、进程与进程状态切换:进程生命周期的蜕变
1、进程的定义
进程是操作系统进行资源分配和调度的基本单位。简单来说,进程包含了正在运行的程序的代码、数据以及执行上下文等信息。
1.1、一个可执行文件与进程的关系
一个可执行文件(程序/软件)可以对应多个进程 ,也可以只对应一个进程,这取决于程序的设计和使用方式。一个可执行文件最少要有一个进程。
当你多次启动同一个可执行文件时,操作系统会为每次启动创建一个新的进程。例如,你可以同时打开多个记事本程序(假设记事本程序的可执行文件是notepad.exe),**每个记事本窗口都对应一个独立的进程。这些进程虽然都源自同一个可执行文件,但它们在内存中是相互独立的,拥有各自的内存空间和执行上下文,彼此之间互不干扰。**另外,有些程序在设计上会主动创建多个进程来完成不同的任务。例如,浏览器通常会为每个标签页创建一个独立的进程,这样可以提高浏览器的稳定性和性能。当一个标签页崩溃时,不会影响其他标签页的正常运行。
2、进程状态
进程在整个执行周期中存在多种状态;
进程常见的状态划分:
1、就绪(Ready):进程已经准备好执行,等待CPU调度。就像运动员已经站在起跑线上,等待发令枪响;
2、运行(Running):进程正在CPU上执行。这是进程最活跃的阶段,如同运动员正在全力奔跑;
3、阻塞(Blocked):进程因等待某个事件(如 I/O 操作完成、信号量获取等)而暂时无法执行。比如运动员在比赛中等待接力棒。
4、终止(Terminated):进程已经完成执行或因异常而终止。就好像运动员完成了最后冲刺。
状态切换
进程状态的切换由操作系统内核负责。当一个运行状态的进程需要等待 I/O 操作完成时,内核会将其状态切换为阻塞状态,并将 CPU 资源分配给其他就绪状态的进程。当 I/O 操作完成后,内核会将该进程的状态切换为就绪状态,等待再次调度 。在抢占式调度系统中,内核还会根据进程的优先级,在合适的时机将运行状态的进程切换为就绪状态,以便让优先级更高的进程获得 CPU 资源。
下面是一个设置进程优先级的代码,基于Windows系统:
cpp
/**
* 多进程实践,这里是在windows系统下
*/
#include <iostream>
#include <windows.h>
int main() {
// 获取当前进程的句柄
HANDLE hProcess = GetCurrentProcess();
// 设置进程优先级为高
if (SetPriorityClass(hProcess, HIGH_PRIORITY_CLASS)) {
std::cout << "Process priority has been set to high." << std::endl;
} else {
// 获取错误代码
DWORD errorCode = GetLastError();
std::cerr << "Failed to set process priority. Error code: " << errorCode << std::endl;
return 1;
}
// 这里可以添加其他需要执行的任务代码
return 0;
}
四、多进程在 Linux 和 Windows 系统中的实现与进程间通信
1、Linux 系统
1.1、常见实现方式在 Linux 中,创建新进程最常用的方式是使用
fork()函数。
fork()函数会创建一个与父进程几乎完全相同的子进程,子进程会继承父进程的大部分资源,包括文件描述符、环境变量等。下面是一个简单的示例:
cpp
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid;
pid_t pid2;
// 获取当前父进程Id
pid = getpid();
printf("before fork: pid = %d\n",pid);//fork之前获取当前进程的pid
pid_t retFork = fork(); // 创建子进程
pid2 = getpid();
if (retFork == -1) {
perror("fork failed"); // 子进程创建失败
return -1;
} else if (pid != pid2) {
// 子进程
printf("I am the child process. My PID is %d\n", getpid());
} else {
// 父进程
printf("I am the parent process. My PID is %d\n", getpid());
}
return 0;
}
1.2、常见使用场景:
一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的------父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
代码示例:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
int data;
while(1)
{
{// 此处可以改成监听网络中信号或者外部请求
printf("please input a data\n");//父进程一直等待客户的消息
scanf("%d",&data);
}
if(data == 1)//收到数据为1就创建一个子进程来处理客户的消息,父进程继续等待其他客户的消息
{
pid = fork();
if(0 == pid)//在子进程里处理
{
while(1)//这里假设子进程处理时间比较长
{
printf("do net request,pid = %d\n",getpid());//子进程打印自己的pid
sleep(3);//延时一会
}
}
}
else
{
printf("wait,do nothing\n");//收到的数据不是1则继续等待
}
}
return 0;
}
2、windows系统
在 Windows 中,创建新进程使用CreateProcess()函数。该函数不仅可以创建新进程,还可以指定新进程的启动参数、安全属性等。下面是一个简单的示例:
cpp
#include <windows.h>
#include <stdio.h>
int main() {
// 创建进程
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcess(
NULL,
TEXT("processEx.exe"), // 调用可执行程序
NULL,
NULL,
FALSE,
0,
NULL,
NULL,
&si,
&pi))
{
printf("CreateProcess failed: %d\n", GetLastError());
return 1;
}else{
printf("CreateProcess succeed: %d\n", pi.dwProcessId);
}
// 等待子进程结束
// WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
3、进程间通信(5种)
管道(Pipe) :管道是一种半双工的通信方式,数据只能单向流动,有匿名管道/命名管道之分,匿名管道用于有亲缘关系的进程,命名管道可用于任意进程。匿名管道通常用于父子进程之间的通信。匿名管道使用pipe()函数创建,命名管道使用mkfifo()函数创建。用于简单数据传递,且实时性高的场景。
消息队列(Message Queue) :以**消息链表形式存在于内核中,可克服信号传递信息少、管道数据无格式和缓冲区受限等问题。一般用于多对多进程间通信,并且对实时性要求不高的异步场景下。**例如,一个日志记录进程和多个业务进程之间的通信,业务进程将日志信息发送到消息队列,日志记录进程从队列中取出消息并进行记录(该方式是linux系统中独有的)。
共享内存(Shared Memory) :共享内存允许不同进程访问同一块物理内存 ,无需数据复制 ,是最快的 IPC 方式,但需要同步和互斥操作 。通过shmget()、shmat()等函数实现。一般用于多个进程需要频繁共享大量数据,例如数据库系统中多个进程共享数据库索引信息,对实时性要求较高的应用,如视频处理、音频处理等,共享内存可以减少数据传输的延迟。
信号量(Semaphore):信号量用于实现进程间的同步和互斥。在 Linux 中,可以使用semget()、semop()等函数操作信号量。适用于进程间资源互斥,以及进程同步,用于协调多个进程的执行顺序,确保一个进程在另一个进程完成某个操作后再继续执行。
前面四个都在一个主机上的操作。
套接字(Socket) :可用于不同主机之间的进程通信,也可用于同一主机上的进程通信,支持多种协议。网络通信中服务器和客户端之间的通信,分布式系统中各个节点间通信;
五、进程的特殊用法
1、守护进程:后台运行的默默守护者
1.1、原理
守护进程是一种在后台运行的特殊进程,它独立于控制终端,通常在系统启动时自动启动,并一直运行直到系统关闭。守护进程的主要作用是执行一些需要长期运行的任务,如系统日志记录、定时任务执行等。
1.2、Linux 系统
创建守护进程通常需要以下步骤:
(1)、使用fork()函数创建一个子进程,然后父进程退出,这样可以使子进程脱离控制终端。
(2)、使用setsid()函数创建一个新的会话,使子进程成为新会话的领导者,从而脱离原有的控制终端。
(3)、改变当前工作目录为根目录,防止占用其他文件系统。
(4)、重设文件权限掩码,防止继承的文件权限影响守护进程的运行。
(5)、关闭不需要的文件描述符,防止资源泄漏。
Linux 实现示例
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid != 0) {
// 父进程退出
exit(0);
}
// 创建新会话
if (setsid() == -1) {
perror("setsid");
return 1;
}
// 改变工作目录
if (chdir("/") == -1) {
perror("chdir");
return 1;
}
// 重设文件权限掩码
umask(0);
// 关闭标准输入、输出和错误输出
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 守护进程主体,例如记录系统日志
while (1) {
// 模拟日志记录
int fd = open("/var/log/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd != -1) {
write(fd, "Daemon is running\n", 16);
close(fd);
}
sleep(10);
}
return 0;
}
1.3、Windows 实现示例
在 Windows 中,可以使用服务来实现类似守护进程的功能。下面是一个简单的服务示例:
cpp
#include <windows.h>
#include <stdio.h>
SERVICE_STATUS serviceStatus;
SERVICE_STATUS_HANDLE serviceStatusHandle;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv);
VOID WINAPI ServiceCtrlHandler(DWORD fdwControl);
int main(int argc, char **argv) {
SERVICE_TABLE_ENTRY serviceTable[] = {
{TEXT("MyService"), ServiceMain},
{NULL, NULL}
};
if (!StartServiceCtrlDispatcher(serviceTable)) {
printf("StartServiceCtrlDispatcher failed: %d\n", GetLastError());
}
return 0;
}
VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv) {
serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
serviceStatus.dwCurrentState = SERVICE_START_PENDING;
serviceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
serviceStatus.dwWin32ExitCode = 0;
serviceStatus.dwServiceSpecificExitCode = 0;
serviceStatus.dwCheckPoint = 0;
serviceStatus.dwWaitHint = 0;
serviceStatusHandle = RegisterServiceCtrlHandler(TEXT("MyService"), ServiceCtrlHandler);
if (serviceStatusHandle == NULL) {
return;
}
// 标记服务已启动
serviceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(serviceStatusHandle, &serviceStatus);
// 服务主体,例如记录系统日志
while (serviceStatus.dwCurrentState == SERVICE_RUNNING) {
// 模拟日志记录
FILE *fp = fopen("C:\\Logs\\ServiceLog.txt", "a");
if (fp != NULL) {
fprintf(fp, "Service is running\n");
fclose(fp);
}
Sleep(10000);
}
}
VOID WINAPI ServiceCtrlHandler(DWORD fdwControl) {
switch (fdwControl) {
case SERVICE_CONTROL_STOP:
serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;
SetServiceStatus(serviceStatusHandle, &serviceStatus);
// 执行清理操作
serviceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(serviceStatusHandle, &serviceStatus);
break;
default:
break;
}
}
2、唯一进程
这里用来避免在一台设备上进程多开。
六、总结
多进程技术作为操作系统和软件开发的核心技术之一,为我们提供了强大的工具来构建高性能、高可靠性的应用程序。通过深入理解多进程的原理和跨平台实现,我们能够更好地应对复杂的业务需求,为用户提供更优质的服务。无论是 Linux 还是 Windows 系统,多进程技术都在不断演进,为软件的高效执行提供了强有力的基础。