多进程编程是利用操作系统提供的进程管理功能,使程序能够同时执行多个任务的一种编程方式。与多线程不同,每个进程都有自己独立的地址空间,进程间通信需要特定的机制。

一、进程的特点

  • 独立性:每个进程有独立的地址空间
  • 动态性:进程有创建、执行、消亡的生命周期
  • 并发性:多个进程可以并发执行

二、进程的内容

2.1、fork() 系统调用

#include <unistd.h>
#include <iostream>

int main() {
    pid_t pid = fork();
    
    if (pid < 0) {
        // 错误处理
        std::cerr << "Fork failed" << std::endl;
        return 1;
    } else if (pid == 0) {
        // 子进程代码
        std::cout << "Child process, PID: " << getpid() 
                  << ", Parent PID: " << getppid() << std::endl;
    } else {
        // 父进程代码
        std::cout << "Parent process, PID: " << getpid() 
                  << ", Child PID: " << pid << std::endl;
    }
    
    return 0;
}

请添加图片描述

可以发下通过系统调用 fork() 后,主进程就和自进程的程序流分开了。

  • 调用一次,返回两次(父进程返回子进程PID,子进程返回0)
  • 子进程是父进程的副本,包括代码、数据、堆栈等
  • 写时复制(Copy-On-Write)技术优化性能

2.2、进程终止

  • 正常终止:exit()_exit()、从main返回
  • 异常终止:收到信号、调用abort()

2.2.1、exit()

int main() {
    exit(0);
    cout << "main()" << endl;
}

2.2.2、信号

在Linux/Unix系统中,进程间可以通过信号(Signal)进行通信。一个进程可以向另一个进程发送信号来通知某些事件或请求特定操作。

int main() {
    pid_t target_pid = 12345; // 目标进程的PID
    kill(target_pid, SIGTERM); // 发送SIGTERM(终止信号)
    return 0;
}
  • pid:目标进程的PID(>0),或使用特殊值(如0-1发送给进程组)。
  • sig:信号编号(如SIGTERMSIGINT等),若为0则仅检查权限而不发送信号。

2.3、等待子进程

#include <sys/wait.h>

pid_t wait(int *status);  // 等待任意子进程
pid_t waitpid(pid_t pid, int *status, int options);

三、进程间通信

3.1、管道

3.1.1、匿名管道(PIPE)

  • 单向通信:数据只能从写端(fd[1])流向读端(fd[0])。
  • 仅限父子进程:通过fork()继承文件描述符实现通信。
  • 内核缓冲区:数据存储在内核内存中,大小有限(通常64KB)。
#include <unistd.h>

int pipe(int fd[2]);  // fd[0]读端,fd[1]写端

示例:

int fd[2];
pipe(fd);

if (fork() == 0) {  // 子进程 - 写入
    close(fd[0]);
    write(fd[1], "Hello", 6);
    close(fd[1]);
    exit(0);
} else { // 父进程 - 读取
    close(fd[1]);
    char buf[256];
    int n = read(fd[0], buf, sizeof(buf));
    buf[n] = '\0';
    std::cout << "Received: " << buf << std::endl;
    close(fd[0]);
}

请添加图片描述

3.1.2、有名管道(FIFO)

  • 命名文件:通过文件系统路径(如/tmp/myfifo)访问,无关进程可通信。
  • 阻塞式读写:默认情况下,读端打开时会阻塞直到写端打开,反之亦然。
#include <sys/stat.h>

mkfifo("/tmp/myfifo", 0666);

请添加图片描述

有点类似于读写文件,实际上,有名管道就是文件系统中的一种特殊类型的文件

3.2、共享内存

  • 最高效的IPC:进程直接读写同一块物理内存,无需内核拷贝。
  • 需同步机制:通常配合信号量或互斥锁使用,避免竞态条件。
#include <sys/shm.h>

int shm_id = shmget(IPC_PRIVATE, size, IPC_CREAT | 0666); // 创建共享内存
void *shm_addr = shmat(shm_id, NULL, 0); // 附加到进程地址空间
shmdt(shm_addr); // 分离共享内存
shmctl(shm_id, IPC_RMID, NULL); // 删除共享内存

适用场景

  • 大数据量交换(如视频处理、数据库缓存)

请添加图片描述

3.3、消息队列

  • 结构化数据:消息包含类型(mtype)和内容(mtext)。
  • 异步通信:发送方和接收方无需同时运行。
  • 内核持久化:消息队列会一直存在,直到显式删除。
#include <sys/msg.h>

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

int msg_id = msgget(IPC_PRIVATE, IPC_CREAT | 0666);   // 创建消息队列
msgsnd(msg_id, &msg, sizeof(msg.mtext), 0);           // 发送消息
msgrcv(msg_id, &msg, sizeof(msg.mtext), mtype, 0);    // 接收消息

适用场景

  • 需要按消息类型过滤的通信(如多生产者-单消费者模型)

请添加图片描述

3.4、信号量

  • 同步工具:控制多个进程对共享资源的访问。
  • 计数型:可以设置初始值(如资源数量)。
#include <sys/sem.h>

int sem_id = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666); // 创建信号量集
semctl(sem_id, 0, SETVAL, 1); // 初始化信号量

struct sembuf sop = {0, -1, 0}; // P操作 减少
semop(sem_id, &sop, 1);

sop.sem_op = 1; // V操作 增加
semop(sem_id, &sop, 1);

适用场景

  • 进程间互斥或同步(如共享内存的读写保护)

请添加图片描述

3.5、socket

  • 跨网络/主机通信:支持TCP/UDP协议。
  • 全双工:双向通信,灵活性高。

网络相关,这里不再赘述…

四、考点

4.1、进程与线程的区别?

  • 进程:独立的内存空间、资源分配单位,上下文切换开销大,稳定性高(一个进程崩溃不影响其他进程)。
  • 线程:共享进程的内存空间,轻量级,切换开销小,但一个线程崩溃可能导致整个进程退出。

4.2、进程间的通信方式有哪些?

  • 管道(Pipe):单向通信,父子进程间使用(如pipe() + fork())。
  • 命名管道(FIFO):可在无关进程间通信,通过文件系统路径访问。
  • 消息队列:内核维护的消息链表,支持多进程读写(msgget/msgsnd)。
  • 共享内存:最高效的方式,进程直接访问同一块内存(shmget/shmat)。
  • 信号(Signal):异步通知(如kill发送SIGTERM)。
  • 信号量(Semaphore):同步机制,控制资源访问。
  • Socket:跨网络或本机的进程通信(如TCP/UDP)。

4.3、什么是孤儿进程和僵尸进程

  • 孤儿进程:父进程先退出,子进程被init进程(PID=1)接管,无害。
  • 僵尸进程:子进程退出后,父进程未调用wait()回收其资源,占用内核进程表项。
  • 避免僵尸进程
    • 父进程调用wait()waitpid()显式回收。
    • 父进程注册SIGCHLD信号处理函数,在回调中调用wait()
    • fork技巧:父进程fork()后立即wait(),子进程再fork()并退出,孙子进程由init接管。

4.4、进程的调度方法有哪些?

  • 先来先服务(FCFS):非抢占式,按到达顺序执行。
  • 短作业优先(SJF):优先执行预估时间短的进程。
  • 时间片轮转(RR):每个进程分配固定时间片,抢占式调度。
  • 多级反馈队列(MLFQ):结合优先级和时间片,动态调整进程优先级。

4.5、守护进程(Daemon)的特点和创建步骤?

  • 特点:后台运行、脱离终端(TTY)、通常以root权限启动。
  • 创建步骤
    1. fork()后退出父进程,子进程成为孤儿进程。
    2. setsid()创建新会话,脱离终端控制。
    3. 关闭文件描述符(如STDIN/STDOUT/STDERR)。
    4. 修改工作目录(如chdir("/"))。
    5. 重设文件掩码(umask(0))。

4.6、多进程和多线程如何选择?

  • 多进程:需高稳定性(如浏览器标签)、跨语言协作、避免全局锁竞争。
  • 多线程:需频繁共享数据(如计算密集型任务)、追求低延迟(如网络服务器)。
Logo

全面兼容主流 AI 模型,支持本地及云端双模式

更多推荐