多线程多进程编程
2026/7/29 3:11:03 网站建设 项目流程

当按下电源开关的那一刻开始,电脑首先创建 0 号进程,它完成操作系统的最初加载和设定。随后,0 号进程创建 1 号进程(通常是用户空间的第一个进程,如 init 或 systemd),而 1 号进程又会创建出无数管理线程,用于管理系统的各项服务和资源。这是电脑开机是系统内部的连锁反应,本篇文章我们将一同探讨系统多进程多线程实现的详细细节

进程

进程(Process)是操作系统进行资源分配和调度的基本单位。每个进程都拥有独立的地址空间、数据段、代码段和系统资源,进程之间相互隔离,通过进程间通信(IPC)机制进行数据交换。

进程的主要特点包括:

  • 独立性:每个进程都有自己独立的内存空间,一个进程崩溃不会影响其他进程。
  • 资源拥有者:进程是系统资源(如CPU时间、内存、文件句柄等)分配的基本单位。
  • 创建开销大:创建新进程需要复制父进程的地址空间,涉及大量内存复制和上下文切换,开销较大。
  • 通信复杂:进程间通信需要通过管道、消息队列、共享内存等专门的IPC机制。

在Linux系统中,进程通过fork()系统调用创建子进程,子进程继承父进程的大部分属性,但拥有独立的进程ID和地址空间。

僵尸进程孤儿进程守护进程

僵尸进程

僵尸进程是指失去了子进程的父进程,因为子进程死亡,返回了大部分的资源,但是子进程的PCB却并没有返回,而是一直在被占用,所以显而易见,大量的僵尸进程会耗尽进程号,导致无法创建新的进程

解决方法也很简单,主要有三种:

1.父进程可以主动调用wait()函数,阻塞回收,

2 注册SIGCHLD信号捕捉函数,子退出自动回收

3 杀死父进程,让子进程被init进程收养释放

孤儿进程

孤儿进程(Orphan Process)是指父进程已经终止或退出,但子进程仍在运行的进程。在类Unix系统中,当父进程先于子进程结束时,子进程会被系统的init进程(PID=1)systemd进程收养,成为孤儿进程。

孤儿进程的特点:

  • 自动被init收养:孤儿进程不会像僵尸进程那样占用系统资源,操作系统会自动将其父进程设置为init进程。
  • 不会造成资源泄漏:init进程会定期调用wait()系统调用,回收已终止的孤儿进程资源,避免资源泄漏。
  • 常见于后台服务:许多守护进程和后台服务在启动时会故意创建子进程然后父进程退出,使子进程成为孤儿进程,从而脱离终端控制。

孤儿进程的创建示例(C语言):

#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid < 0) { // fork失败 perror("fork failed"); return 1; } else if (pid == 0) { // 子进程 printf("子进程PID: %d, 父进程PID: %d\n", getpid(), getppid()); sleep(5); // 子进程睡眠5秒 printf("5秒后 - 子进程PID: %d, 父进程PID: %d (现在应该是init进程)\n", getpid(), getppid()); } else { // 父进程 printf("父进程PID: %d, 创建的子进程PID: %d\n", getpid(), pid); // 父进程立即退出,使子进程成为孤儿进程 return 0; } return 0; }

孤儿进程与僵尸进程的区别:

  • 状态不同:僵尸进程是已终止但未被回收的进程;孤儿进程是仍在运行但父进程已终止的进程。
  • 资源占用:僵尸进程占用进程表项但不占用内存;孤儿进程正常占用内存和CPU资源。
  • 处理方式:僵尸进程需要父进程显式回收;孤儿进程由init进程自动收养和回收。

实际应用场景:

  • 守护进程创建:许多守护进程通过两次fork()技术创建,使最终进程成为孤儿进程,脱离终端控制。
  • 后台任务:需要长时间运行且不需要用户交互的后台任务。
  • 服务器进程:网络服务器常创建子进程处理客户端请求,父进程继续监听新连接。

守护进程

守护进程(Daemon Process)是在后台运行的特殊进程,通常没有控制终端,不与用户直接交互,独立于控制终端并在系统启动时自动运行。守护进程通常用于提供系统服务,如Web服务器、数据库服务器、日志服务等。

守护进程的主要特征:

  • 脱离终端:守护进程在后台运行,不依赖于任何终端。
  • 会话组长:成为新会话的领导者,脱离原会话的控制。
  • 根目录:将工作目录更改为根目录(/),避免占用可卸载的文件系统。
  • 文件描述符:关闭所有从父进程继承的文件描述符,特别是标准输入、输出、错误。
  • umask清零:将文件创建掩码设置为0,确保守护进程创建文件时有最大权限。
  • 忽略信号:忽略某些终端产生的信号,如SIGHUP。

创建守护进程的标准步骤:

  1. fork()创建子进程,父进程退出:使子进程成为孤儿进程,被init收养。
  2. setsid()创建新会话:使进程成为新会话的领导者,脱离终端控制。
  3. 再次fork():确保进程不是会话组长,防止其重新获取控制终端。
  4. 更改工作目录:通常改为根目录。
  5. 重设文件权限掩码:umask(0)。
  6. 关闭文件描述符:关闭所有不需要的文件描述符。
  7. 重定向标准I/O:将stdin、stdout、stderr重定向到/dev/null或日志文件。

守护进程创建示例(C语言):

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <signal.h> void daemonize() { pid_t pid; // 1. 第一次fork,创建子进程 pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } if (pid > 0) { // 父进程退出 exit(0); } // 2. 创建新会话,脱离终端控制 if (setsid() < 0) { perror("setsid failed"); exit(1); } // 3. 第二次fork,确保不是会话组长 pid = fork(); if (pid < 0) { perror("second fork failed"); exit(1); } if (pid > 0) { exit(0); } // 4. 更改工作目录到根目录 chdir("/"); // 5. 重设文件权限掩码 umask(0); // 6. 关闭所有文件描述符 for (int i = 0; i < 3; i++) { close(i); } // 7. 重定向标准I/O到/dev/null int fd = open("/dev/null", O_RDWR); dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); if (fd > 2) { close(fd); } } int main() { daemonize(); // 守护进程的主循环 while (1) { // 执行守护进程的任务 // 例如:日志记录、定时任务、服务监听等 sleep(60); // 每分钟执行一次 } return 0; }

常见的守护进程示例:

  • httpd:Apache Web服务器守护进程
  • mysqld:MySQL数据库服务器守护进程
  • sshd:SSH服务器守护进程
  • crond:定时任务守护进程
  • syslogd:系统日志守护进程

守护进程的管理:

  • 启动方式:系统启动时自动启动,或通过init脚本手动启动。
  • 停止方式:发送TERM信号正常停止,或发送KILL信号强制停止。
  • 状态查看:使用ps aux | grep 进程名查看守护进程状态。
  • 日志管理:守护进程通常将日志输出到系统日志文件(如/var/log/)或专用日志文件。

现代系统的守护进程管理:

在现代Linux系统中,systemd已成为主流的init系统,它提供了更强大的守护进程管理功能:

  • 服务单元文件:使用.service文件定义守护进程的启动、停止、重启行为。
  • 依赖管理:自动处理服务之间的依赖关系。
  • 日志集成:通过journald集中管理所有守护进程的日志。
  • 自动重启:配置守护进程崩溃后自动重启。

进程的优缺点

优点

  • 隔离性强:每个进程拥有独立的地址空间,一个进程崩溃不会影响其他进程,系统稳定性高。
  • 安全性好:进程间的内存隔离使得恶意程序难以直接访问其他进程的数据,增强了系统安全性。
  • 资源管理清晰:操作系统以进程为单位分配和回收资源(CPU时间、内存、文件句柄等),便于监控和管理。
  • 支持多道程序设计:多进程可以充分利用多核CPU,实现真正的并行计算,提高系统吞吐量。
  • 容错能力强:单个进程异常退出时,操作系统可以自动回收其资源,不影响其他进程的正常运行。
  • 权限控制灵活:每个进程可以拥有独立的用户权限和访问控制,便于实现最小权限原则。

缺点

  • 创建和销毁开销大:创建进程需要复制父进程的地址空间、建立新的PCB(进程控制块)、分配内存等,涉及大量内存复制和上下文切换,比线程创建慢数十倍。
  • 进程间通信复杂:由于进程地址空间相互隔离,进程间通信必须借助管道、消息队列、共享内存、信号量等专门的IPC机制,编程复杂度高。
  • 资源占用多:每个进程都需要独立的地址空间和系统资源,大量进程同时运行会消耗较多内存和系统资源。
  • 上下文切换成本高:进程切换需要保存和恢复完整的上下文信息(包括页表、寄存器、内核栈等),切换开销远大于线程切换。
  • 启动速度慢:相比线程,进程的启动和初始化过程更慢,不适合需要频繁创建和销毁执行单元的场景。
  • 调度开销大:操作系统需要在众多进程之间进行调度,进程数量过多时调度开销会显著增加。

进程间的通信

进程间通信(IPC,Inter-Process Communication)是指在不同进程之间传递数据和信息的机制。由于每个进程拥有独立的地址空间,进程间无法直接访问彼此的内存,必须借助操作系统提供的IPC机制来实现数据交换和同步。

常见的进程间通信方式包括:

1. 管道(Pipe)

管道是一种半双工的通信方式,数据只能单向流动。管道分为匿名管道命名管道(FIFO)两种:

  • 匿名管道:用于具有亲缘关系的进程之间(如父子进程),在内存中创建,使用简单但只能在相关进程间使用。
  • 命名管道:以文件形式存在于文件系统中,允许任意进程通过管道名称进行通信,不受亲缘关系限制。

2. 消息队列(Message Queue)

消息队列是由操作系统维护的消息链表,允许进程以消息为单位进行数据交换。消息队列克服了管道只能传输无格式字节流的缺点,支持结构化消息的传递,发送方和接收方可以按消息类型进行选择性读取。

优点:可以实现异步通信,发送方不需要等待接收方准备好;消息具有边界,不会出现粘包问题。缺点:消息队列存在于内核中,系统重启后消失;不适合传输大量数据。

3. 共享内存(Shared Memory)

共享内存是最快的IPC方式。操作系统将同一块物理内存映射到多个进程的地址空间中,进程可以直接读写这块共享区域,无需内核介入数据拷贝。

  • 优点:通信速度极快,适合大数据量传输,是效率最高的IPC机制。
  • 缺点:需要配合信号量、互斥锁等同步机制使用,防止多个进程同时读写导致数据不一致。

在Linux中,常用的共享内存实现有System V共享内存(shmget/shmat/shmdt)和POSIX共享内存(shm_open/mmap)。

4. 信号量(Semaphore)

信号量本质上是一个计数器,主要用于进程间的同步与互斥,控制多个进程对共享资源的访问。它解决的问题是:共享资源被多个进程同时访问时如何避免冲突。

信号量分为两类:

  • POSIX信号量:可用于进程间或线程间同步。
  • System V信号量:功能更强大,支持信号量集合操作。

5. 信号(Signal)

信号是Unix/Linux系统中一种异步通知机制,用于通知进程发生了某种事件(如进程终止、定时器到期、非法内存访问等)。常见的信号有SIGKILL(强制终止)、SIGTERM(请求终止)、SIGCHLD(子进程状态变更)、SIGALRM(定时器超时)等。

信号通信简单高效,但承载的信息量有限(通常只能传递一个信号编号),不适合传输大量数据,主要用于进程控制和异常处理。

6. 套接字(Socket)

套接字是最通用的IPC机制,不仅支持同一台机器上的进程间通信(Unix Domain Socket),还支持跨网络的进程间通信(TCP/IP Socket)。它提供了一种端到端的通信方式,是现代网络通信的基础。

Socket通信基于客户端-服务器模型,支持面向连接的TCP协议和无连接的UDP协议,适用于分布式系统和网络编程场景。

IPC机制对比总结

通信方式数据量速度适用场景
管道中等中等父子进程或相关进程间简单数据流传输
消息队列中等中等需要结构化消息传递的异步通信场景
共享内存最快大数据量、高频读写的进程间数据交换
信号量进程间同步与互斥控制
信号极小异步事件通知与进程控制
套接字中等跨网络或本机进程间通用通信

在实际开发中,选择哪种IPC机制需要综合考虑数据传输量、实时性要求、进程关系以及开发复杂度等因素。

线程

线程(Thread)是进程内的执行单元,是CPU调度和分派的基本单位。一个进程可以包含多个线程,这些线程共享进程的地址空间和系统资源,但拥有各自的栈空间、程序计数器和寄存器状态。

线程的主要特点包括:

  • 共享资源:同一进程内的线程共享内存空间、文件描述符等资源,通信更加高效。
  • 创建开销小:创建线程只需分配栈空间和少量寄存器,比创建进程快得多。
  • 轻量级:线程切换比进程切换开销小,因为不需要切换地址空间。
  • 同步需求:由于共享资源,线程间需要同步机制(如互斥锁、信号量)来避免竞态条件。

线程分为用户级线程和内核级线程。现代操作系统通常采用一对一模型(如Linux的pthread)或多对多模型,平衡线程管理的灵活性和性能。

进程与线程的关系:可以将进程看作一个"容器",线程则是容器内的"工作者"。进程提供资源环境,线程执行具体任务。多线程编程能充分利用多核CPU,提高程序并发性能,但需要仔细处理线程同步和资源共享问题。

线程的优缺点

优点

  • 资源开销小:线程的创建和销毁只需分配栈空间和少量寄存器状态,无需复制整个地址空间,创建速度比进程快数十倍。
  • 通信效率高:同一进程内的线程共享地址空间,数据交换直接通过读写共享变量即可完成,无需内核介入,通信延迟极低。
  • 并发性能好:多线程能充分利用多核CPU,实现真正的并行计算,提升程序吞吐量和响应速度。
  • 上下文切换快:线程切换不需要切换页表和地址空间,只需保存和恢复少量寄存器状态,切换开销远小于进程切换。
  • 资源利用率高:多个线程共享进程的文件描述符、内存等资源,避免了重复分配,整体资源占用更少。
  • 响应速度快:适合需要快速响应用户操作的场景(如GUI程序),可单独用线程处理耗时任务而不阻塞界面。

缺点

  • 安全性差:线程间共享地址空间,缺乏内存隔离保护,一个线程的越界写操作可能破坏其他线程乃至整个进程的数据。
  • 稳定性低:一个线程的致命错误(如段错误)通常会导致整个进程崩溃,无法像进程那样隔离故障。
  • 同步编程复杂:共享数据需要配合互斥锁、条件变量、信号量等同步机制来避免竞态条件,编程模型复杂,容易产生死锁、饥饿等问题。
  • 调试困难:多线程程序的执行顺序由调度器决定,具有不确定性,同一输入可能产生不同输出,复现和定位bug的难度远高于单线程程序。
  • 可扩展性受限:线程数量过多时,锁竞争和上下文切换会成为性能瓶颈,无法像多进程那样方便地扩展到多台机器。
  • 资源竞争:多个线程争抢同一把锁或共享变量时,可能引发性能抖动甚至活锁,需要精细的并发设计来缓解。

线程间的通信方式

线程间通信是指同一进程内多个线程之间进行数据传递和协调的机制。由于线程共享进程的地址空间,线程间通信比进程间通信更加轻量和高效,但也因此需要更精细的同步控制来避免数据竞争。

常见的线程间通信方式包括:

1. 共享内存(直接读写共享变量)

线程最基础的通信方式就是直接读写进程内的全局变量或堆上分配的内存。由于所有线程都处于同一地址空间,无需任何系统调用即可完成数据交换,效率极高。但必须搭配同步机制使用,否则会出现数据不一致。

2. 互斥锁(Mutex)

互斥锁是最常用的线程同步原语,用于保护临界区(Critical Section)。同一时刻只允许一个线程持有锁并进入临界区,其他线程必须等待锁释放。互斥锁保证了共享资源的互斥访问,防止竞态条件。

3. 条件变量(Condition Variable)

条件变量配合互斥锁使用,用于实现线程间的条件等待与通知机制。当某个条件不满足时,线程在条件变量上等待(阻塞);当条件满足时,另一个线程通过条件变量唤醒等待线程。典型场景是生产者-消费者模型。

4. 信号量(Semaphore)

信号量维护一个计数器,允许多个线程同时访问有限数量的共享资源。线程在访问资源前执行 P 操作(减计数),访问结束后执行 V 操作(加计数)。计数为 0 时后续线程阻塞等待,常用于控制资源池的最大并发数。

5. 读写锁(Read-Write Lock)

读写锁允许多个读线程同时持有锁(共享锁),但写线程必须独占锁(排他锁)。适合读多写少的场景,比互斥锁具有更高的并发度。写锁被持有时,所有读线程和其他写线程都会被阻塞。

6. 自旋锁(Spin Lock)

自旋锁与互斥锁类似,但等待线程不会进入睡眠,而是循环检查锁状态(忙等待)。由于避免了上下文切换开销,自旋锁适合临界区极短的场景;但在临界区较长或锁竞争激烈时,自旋锁会浪费CPU资源。

7. 屏障(Barrier)

屏障用于同步一组线程,使所有线程在达到某个同步点之前阻塞等待,直到最后一个线程到达后,所有线程一起继续执行。适用于分阶段计算的并行算法,如并行排序、迭代计算等场景。

8. 原子操作(Atomic Operation)

原子操作是最轻量级的线程间同步方式。通过CPU提供的原子指令(如CAS、原子加减),在多线程环境下无需显式加锁即可安全地对共享变量进行操作。适合计数器更新、标志位设置等简单场景,性能远优于互斥锁。

线程通信方式对比总结

通信方式粒度开销适用场景
共享变量数据交换极低多线程间直接读写共享数据
互斥锁临界区保护中等保护共享资源、防止竞态条件
条件变量条件同步中等生产者-消费者、等待特定条件
信号量资源计数中等资源池管理、限制并发访问数
读写锁临界区保护中等读多写少的共享资源保护
自旋锁临界区保护临界区极短、锁持有时间短的场景
屏障组同步中等分阶段并行计算、批量同步
原子操作变量操作极低计数器、状态标志等简单操作

在实际开发中,选择哪种线程通信方式需要综合考虑并发度要求、临界区长短、读写比例以及数据竞争风险等因素。对于简单计数场景优先使用原子操作,对于复杂数据结构保护优先使用互斥锁,对于读多写少场景可选择读写锁以提升并发性能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询