前言
进程(Process)是程序的一次执行;线程(Thread)可以理解为进程中的执行的一段程序片段。
序号 | 对比项 | 进程 | 线程 |
---|---|---|---|
1 | 资源分配 | 操作系统(OS)资源分配的基本单位,每个进程有独立的地址空间和资源,创建和销毁进程的开销较大。 | 处理器(CPU)任务调度和执行的基本单位,共享进程的地址空间和资源,创建和销毁线程的开销较小。 |
2 | 通信与同步 | 进程间通信需要使用特殊的机制(如管道、消息队列、共享内存等)。 | 由于多个线程共享同一个进程的资源,线程之间的通信和同步比进程之间更方便。线程间可以直接读写共享变量。 |
3 | 安全性 | 进程是独立的执行单元,具有自己的调度算法,在并发条件下更加稳定可靠,一个进程崩溃后,不会影响其他进程。 | 由于多个线程共享同一进程的资源,线程之间的操作可能会相互干扰,导致数据一致性问题;一个线程死掉导致整个进程死掉。 |
IPC(Inter-Process Communication)通信机制
Unix/Linux 系统调用中 System V 进程间通信(IPC)机制(即原生IPC)
一、管道
- 无名管道(pipe)
- 有名管道(FIFO)
序号 | 特点 | 无名管道 | 有名管道 |
---|---|---|---|
1 | 通信模式 | 半双工 | |
2 | 数据写入遵循原则 | 先进先出 | |
3 | 传输数据格式 | 无格式,需要读写双方事先约定 | |
4 | 数据读取 | 一次性读取 | |
5 | 文件性质 |
一种特殊的文件,不属于文件系统,只存在于内存中;在内存中对应一个缓冲区,不同的系统缓冲区不一样大 | |
6 | 适用范围 | 父子进程间通信 | 任意进程间通信 |
7 | 创建管道函数接口 | pipe() | mkfifo() |
二、信号(Signal)
信号是软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
信号的特点:1)简单;2)不能携带大量信息;3)满足某个特设条件才发送。
三、消息队列(Message Queue)
消息队列实质为消息的链表,数据存放于内存中,由内核维护。 进程通过队列发送结构化消息,支持优先级控制和异步通信,常用于解耦生产者-消费者模型(如订单系统)。
核心函数如
msgget()
(创建/获取队列)、msgsnd()
/msgrcv()
(发送/接收消息)、msgctl()
(管理队列)
创建消息队列的步骤如下:
- 获取一个唯一的消息队列的IPC键值
通过ftok()获取键值
函数原型:key_t ftok( char * fname, int id ); - 通过键值获取IPC对象标识符
通过msgget()创建消息队列(IPC对象),并返回消息队列的IPC对象标识符
函数原型:int msgget(key_t key, int msgflg); -
使用消息队列发送消息
使用msgsnd()将消息写入消息队列
函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg) -
使用消息队列接收消息
使用msgrcv()读取消息队列里的消息
函数原型: ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
四、共享内存(Share Memory)
多个进程可直接读写同一块物理内存区域,无需数据拷贝,适用于高性能场景如视频处理或实时计算。
关键函数包括
shmget()
(创建/获取内存段)、shmat()
/shmdt()
(附加/分离内存)、shmctl()
(控制权限或删除)
五、共享存储映射(Share Memory-mapped)
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。 当从缓冲区中取数据,就相当于读文件中的相应字节;将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。
5.1 存储映射函数常用接口如下:
- mmap函数: 创建内存映射区(将一个文件或其他对象映射进内存)
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- munmap函数: 释放内存映射区
#include <sys/mman.h> int munmap(void *addr, size_t length);
5.2 共享映射实现父子进程通信参考示例
int fd = open("xxx.txt", O_RDWR);// 打开一个文件
int len = lseek(fd, 0, SEEK_END);//获取文件大小
// 创建内存映射区
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
close(fd); //关闭文件
// 创建子进程
pid_t pid = fork();
if (pid == 0) //子进程
{
sleep(1); //演示,保证父进程先执行
// 读数据
printf("%s\n", (char*)ptr);
}
else if (pid > 0) //父进程
{
// 写数据
strcpy((char*)ptr, "i am u father!!");
// 回收子进程资源
wait(NULL);
}
// 释放内存映射区
int ret = munmap(ptr, len);
if (ret == -1)
{
perror("munmap error");
exit(1);
}
六、信号量(Semaphore)
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。
编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。
PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。
主要函数有
semget()
(创建/获取信号量集)、semop()
(执行操作)、semctl()
(设置初值或删除)
七、套接字(Socket)
套接字是支持TCP/IP网络通信的基本操作单元,可以让不同主机之间的进程进行通信。
八、进程间通信第三方库
在 Unix/Linux 系统开发中,实现进程间通信(IPC)的常用第三方库主要分为两类:封装原生 IPC 机制的轻量级库和提供高级抽象的消息中间件。
1、封装原生IPC的库
1)Boost.Interprocess
- 提供跨平台的共享内存、消息队列、信息量等高级封装,支持RAII模式管理资源生命周期。
- 示例:基于共享内存的进程间数据传输,避免手动调用shmget/shmat等原生接口。
2)POSIX消息队列封装库
- 相比System V消息队列,POSIX标准接口更简洁(如:mq_open、mq_send)。部分库(如:librt)提供对其的封装。
2、消息中间件(分布式IPC)
1)ZeroMQ(libzmq)
- 支持多种通信模式(发布-订阅、请求-响应),可基于 TCP/IPC 等传输层,适用于分布式系统 。
- 示例:替代原生消息队列,实现松耦合的进程通信架构。
2)Redis
- 通过内存键值存储提供 Pub/Sub 模型 和 List 队列,支持跨语言、跨主机通信。
- 适用场景:解耦复杂系统模块,如日志收集或任务分发。
3)Apache Kafka
- 高吞吐量的分布式消息系统,适合大数据场景下的进程/服务间数据流处理。
提示:若仅需单机 IPC,优先考虑原生 System V/POSIX 机制(如消息队列 msgsnd
/msgrcv
)或轻量封装库,避免中间件开销
---------------------------------------------------------------------------------------------------------------------------------
后记
补充知识点
1、 可重入函数与不可重入函数
可重入是指一个可以被多个任务同时调用的过程,任务在调用时不必担心数据会因其他任务的调用而修改。
不可重入函数满足下列条件 :
1)函数体内使用了静态的数据结构;
2)函数体内调用了malloc()或者free()函数(谨慎使用堆);
3)函数体内调用了标准I/O函数。
保证函数的可重入性的方法:
1)尽量使用局部变量;
2)对于全局变量要添加保护机制,如:采取关中断、信号量等互斥方法。
2、 守护进程(Daemon Process)
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
守护进程是个特殊的孤儿进程,这种进程脱离终端,以避免被任何终端所产生的信息打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。
Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。
3、 死锁(DeadLock)
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁的必要条件
1)互斥条件:某资源只能被一个进程使用,其他进程请求该资源时,只能等待,直到资源使用完毕后释放资源。
2)请求和保持条件:进程已经保持了至少一个资源,并且在等待其他资源时不释放已占有的资源。
3)不可剥夺条件:已分配给进程或线程的资源不能被强制性地剥夺,只能由持有资源的进程或线程主动释放。
4)循环等待条件:存在一个进程或线程的资源申请序列,使得每个进程或线程都在等待下一个进程或线程所持有的资源。
处理死锁的思路
1)预防死锁: 破坏死锁的必要条件;
2)避免死锁:系统在分配资源时根据资源的使用情况提前作出预测,从而避免死锁的发生;
3)检测死锁:运行时出现死锁,能及时发现死锁,把程序解脱出来;
4)解除死锁:发生死锁后,解脱进程,通常撤销进程,回收资源,再分配给正处于阻塞状态的进程。
预防死锁的具体方式
1)破坏请求和保持条件
a) 所有进程开始前,必须一次性地申请所需的所有资源;
b)允许一个进程只获得初期的资源就开始运行,再把运行完的资源释放出来,然后再请求新的资源。
2)破坏不可抢占条件
提出新资源请求不能被满足时,进程必须释放已经保持的所有资源,以后需要时再重新申请。
3)破坏循环等待条件
对系统中的所有资源类型进行线性排序,规定每个进程必须按序列号递增的顺序请求资源。假如进程请求到了一些序列号较高的资源,然后又请求一个序列较低的资源时,必须先释放相同和更高序号的资源后才能申请低序号的资源。多个同类型资源必须一起请求。
4、基于C++11实现线程安全消息队列
核心实现原理
- 数据结构:使用
std::queue
作为底层容器存储消息,支持先进先出(FIFO)操作 。 - 线程同步:通过
std::mutex
保护队列访问,std::condition_variable
协调生产者-消费者线程 。 - 消息传递:生产者线程调用
push()
添加消息,消费者线程通过pop()
阻塞获取消息 。
#include "MessageQueue.hpp"
#include <thread>
#include <iostream>
void producer(MessageQueue& mq) {
for (int i = 0; i < 5; ++i) {
mq.push("Message " + std::to_string(i));
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
void consumer(MessageQueue& mq) {
for (int i = 0; i < 5; ++i) {
std::string msg = mq.pop();
std::cout << "Received: " << msg << std::endl;
}
}
int main() {
MessageQueue mq;
std::thread p(producer, std::ref(mq));
std::thread c(consumer, std::ref(mq));
p.join();
c.join();
return 0;
}
//MessageQueue.hpp
#include <queue>
#include <mutex>
#include <condition_variable>
#include <string>
class MessageQueue {
public:
void push(const std::string& message);
std::string pop();
private:
std::queue<std::string> queue_;
std::mutex mtx_;
std::condition_variable cv_;
};
//MessageQueue.cpp
#include "MessageQueue.hpp"
void MessageQueue::push(const std::string& message) {
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(message);
cv_.notify_one(); // 唤醒一个等待线程
}
std::string MessageQueue::pop() {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] { return !queue_.empty(); }); // 队列空时阻塞
std::string msg = queue_.front();
queue_.pop();
return msg;
}