进程间通信(IPC)
-
Inter_Process Communication
| 通信方式 | 双工模式 | 特点 |
|---|---|---|
| 管道(无名 / 有名) | 半双工 | 同一时间只能单向 |
| 共享内存 | 全双工 | 双向同时读写 |
| 消息队列 | 全双工 | 双向收发消息 |
| Socket | 全双工 | 网络双向通信 |
管道
-
管道文件 内存分配空间,速度快 同步,阻塞 (创建----->mkfifo (first in first out)) 需要读写同时打开
-
有名管道---->任意进程
cpp//写入 #include<stdio.h> #include<string.h> #include<unistd.h> #include<stdlib.h> #include<fcntl.h> #include<signal.h> void fun(int sig) { printf("sig=%d",sig); } int main() { signal(SIGPIPE,fun); int fd=open("fifo",O_WRONLY); if(fd==-1) { printf("open fifo err!!!\n"); exit(1); } printf("open fifo success\n"); while(1) { char buff[128]={0}; fgets(buff,128,stdin);//会吸收\n if(strncmp(buff,"end",3)==0) { break; } write(fd,buff,strlen(buff)-1); } close(fd); exit(0); } //读端 int main() { int fd=open("fifo",O_RDONLY); if(fd==-1) { printf("open err!!!\n"); } while(1) { char buff[128]={0}; int n=read(fd,buff,127); if(n==0) { break; } printf("buff=%s",buff); } close(fd); } -
无名管道----> 父子 pipe(int fd[2])
cpp#include<stdio.h> #include<string.h> #include<unistd.h> #include<stdlib.h> #include<fcntl.h> #include<signal.h> int main() { int fd[2]; int res=pipe(fd);//fd[0] 读端 fd[1] 写端 if(res==-1) { printf("pipe err!!!\n"); exit(1); } pid_t pid=fork(); if(pid==-1) { exit(1); } if(pid==0) { close(fd[1]); while(1) { char buff[128]=0; int n=read(fd[0],buff,127); if(read==0) { break; } printf("buff=%s\n",buff); } close(fd[0]); } else { close(fd[0]); while(1) { char buff[128]={0}; printf("input:"); fgets(buff,128,stdin); if(strncmp(buff,"end",3)==0) { break; } write(fd[1],buff,strlen(buff)-1); } close(1); } exit(0); }
-
面试:
1.有名管道和无名管道区别?
-
有名管道 可以在任意两个进程间通信
-
无名管道 只可以在父子/兄弟进程间通信
| 特性 | 无名管道(Pipe) | 有名管道(FIFO) |
|---|---|---|
| 创建方式 | pipe() 系统调用 |
mkfifo() 系统调用 / mkfifo 命令 |
| 标识 / 访问方式 | 无文件名,仅通过文件描述符 | 有文件名(存在于文件系统),可通过路径访问 |
| 通信进程关系 | 仅限有亲缘关系的进程(父子 / 兄弟) | 任意进程(无亲缘关系也可) |
| 生命周期 | 随进程退出而销毁 | 随文件系统存在(需手动 rm 删除) |
| 存在形式 | 内存中,不可见 | 文件系统中有标识(ls -l 显示 p 类型),但数据仍在内存 |
| 打开方式 | 只能单向(半双工),需两个 pipe 实现双向 | 可单向 / 双向,通过 open() 打开文件即可 |
2.管道通信方式? 单工,半双工,全双工
-
半双工 (同一时间只能一个方向)
| 管道类型 | 单向通信 | 双向通信 |
|---|---|---|
| 无名管道 | 关一端,用一个管道 | 必须造两个管道,各负责一个方向 |
| 有名管道 | 只开读 / 只开写,用一个管道 | 一个管道 + O_RDWR 打开,或造两个管道 |
3.写管道的数据在哪里?
- 内存
信号量:
- 多进程 / 多线程间资源竞争 和同步

信号和信号量的区别:
-
信号 (signal)---->软中断 ,用于通知进程发生了某种事件
- 作用:进程可以通过注册信号处理函数来响应不同的信号
-
信号量 (semaphore)---->同步 工具,用于管理对共享资源的访问,通常为一个整数计数器 ,用来控制 对共享资源 的并发访问
- 多线程/多进程 间资源的同步 和竞争
-
总结:
-
信号 ----->进程间/进程内 事件的通知和处理 ---->异步通信机制
-
信号量 ---->进程/线程间 的同步和互斥 ,控制对共享资源 的并发访问
-
信号量的使用
一、核心函数解析
1. semget ------ 创建 / 获取信号量集
cpp
int semget(key_t key, int num_sems, int sem_flags);
-
功能 :根据
key创建一个新的信号量集,或获取一个已存在的信号量集的 ID。 -
参数:
-
key:类似文件名,用于标识 信号量集,多个进程可通过相同的key访问同一信号量集。 -
num_sems:信号量集中包含的信号量个数。 -
sem_flags:权限掩码和创建标志,如IPC_CREAT(不存在则创建)、IPC_EXCL(与IPC_CREAT联用,存在则失败)、0666(权限)。
-
-
返回值 :成功返回信号量集 ID(
sem_id),失败返回 - 1。
2. semctl ------ 控制信号量集
cpp
int semctl(int sem_id, int sem_num, int command, ...);
-
功能 :对信号量集 或其中的单个信号量 执行各种控制操作,如设置值 、获取值 、删除等。
-
参数:
-
sem_id:由semget返回的信号量集 ID。 -
sem_num:信号量 在集中的索引 (从 0 开始),某些命令(如IPC_RMID)会忽略此参数。 -
command:要执行的控制命令,常见的有:
-
SETVAL:设置指定信号量的值。 -
GETVAL:获取指定信号量的值。 -
IPC_RMID:立即删除信号量集,唤醒所有等待的进程。
-
-
...:可变参数 ,根据command的不同,可能需要传入一个union semun结构体,用于设置或返回值。
-
-
返回值 :成功时,返回值取决于
command;失败返回 - 1。
3. semop ------ 对信号量集执行原子操作
cpp
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
-
功能 :对信号量集中的一个或多个信号量执行 P/V 操作 ,所有操作作为一个原子整体完成,要么全部成功,要么全部不执行。
-
参数:
-
sem_id:信号量集 ID。 -
sem_ops:指向一个 struct sembuff数组的指针,每个元素描述一个操作。
cppstruct sembuf { unsigned short sem_num; // 信号量索引 short sem_op; // 操作值:正数为V操作,负数为P操作,0为等待信号量变为0 short sem_flg; // 操作标志,如IPC_NOWAIT(非阻塞)、SEM_UNDO(进程退出时撤销操作) };num_sem_ops:sem_ops, 一次性执行 多少个 信号量操作,和p,v统一 。
-
-
返回值:成功返回 0,失败返回 - 1。
二、关键要点
-
头文件依赖 :
sys/sem.h``sys/types.h``sys/ipc.h -
与 POSIX 信号量的区别:
特性 System V 信号量 POSIX 信号量 操作对象 信号量集(多个信号量) 单个信号量 原子性 支持对多个信号量的原子操作 仅支持单个信号量的原子操作 持久性 内核持久,进程退出后仍存在,需显式删除 无名信号量随进程销毁;有名信号量需显式删除 接口复杂度 较复杂,函数参数多 较简单,易于使用 -
典型使用流程:
-
用
semget创建或获取信号量集。 -
用
semctl初始化 信号量的值。 -
用
semop执行 P/V 操作来同步进程。 -
用
semctl(IPC_RMID命令)删除不再使用的信号量集。
-
三、实现
cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<time.h>
#include"sem.h"
//a.c
int main()
{
int n;
sem_init();
srand(time(0));
for(int i=0;i<5;i++)
{
sem_p();//获取资源 -1
printf("A");
fllush(stdout);
n=rand()%3;
sleep(n);
printf("A");
fflush(stdout);
sem_v();//释放资源 +1
n=rand()%3;
sleep(n);
}
sem_destroy();
return 0;
}
//b.c
int main()
{
int n;
sem_init();
srand(time(0));
for(int i=0;i<5;i++)
{
sem_p();
printf("B");
fflush(stdout);
n=rand()%3;
sleep(n);
printf("B");
fllush(stdout);
sem_v();
n=rand()%3;
sleep(n);
}
return 0;
}
./a& ./b& -------->同时运行(后台)
cpp
//sem.h
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/sem.h>
//初始化信号量值时,自己创建设置
union semun
{
int val;
};
//创建信号量,初始;获取已存在信号量
void sem_init();
void sem_p();
void sem_v();
void sem_destroy();
cpp
#include"sem.h"
static int semid=-1;
//创建信号量,初始;获取已存在信号量
void sem_init()
{
//IPC_EXCL---->(execlusive 排他的、互斥的、独有的)
semid=semget((key_t)1234,1,IPC_CREAT|IPC_EXCL|0600);//创建一个全新的
if(semid==-1)//全新创建失败,或已创建--->获取
{
semid=semget((key_t)1234,1,0600);
if(semid==-1)
{
printf("sem create err!!!\n");
}
}
else
{
//初始化信号量
union semun a;
a.val=1;
if(semctl(semid,0,SETVAL,a)==-1)//初始化信号量
{
printf("semctl setval err!!!\n");
}
}
}
void sem_p()
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=-1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
printf("p err!!!\n");
}
}
void sem_v();
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
printf("v err!!!\n");
}
}
void sem_destory();
{
if(semctl(semid,0,IPC_RMID)==-1)
{
printf("semctl err\n");
}
}
key_t key与semid的区别
key_t key ------->find IPC资源 全局地址,所有进程一样
semid------------->operate 资源的
-
特点:
-
只在当前进程 内有效,不能传给别的进程。
-
不同进程操作同一个信号量 ,用的
semid通常不同。
-
共享内存(Shared Memory)
共享内存 是 最快 的进程间通信(IPC)方式 ,它允许多个进程直接访问同一块 物理内存区域,数据不需要在进程间复制传输,是性能最高的 IPC 机制。
核心原理
-
操作系统开辟一块公共物理内存
-
多个进程将这块内存映射到自己的虚拟地址空间
-
进程直接读写这块内存,完成数据交换
-
无需内核中转,无数据拷贝,速度极快
关键特点
✅ 优点
-
速度最快 :无内核拷贝,直接内存访问
-
效率极高:适合大量数据、高频通信场景
-
支持多进程:任意数量进程共享同一块内存
❌ 缺点
-
无同步机制:进程同时读写会产生数据混乱(必须配合信号量 / 互斥锁)
-
数据不安全:需要开发者手动处理并发问题
ipcs = IPC status
作用:查看系统里的 进程间通信 (IPC) 资源
cpp
ipcs -s //查看信号量
ipcs -m //共享内存
ipcs -q //消息队列
ipcrm -m shid//删除共享内存
Linux 下共享内存 API(C 语言)
Linux 提供 sys/shm.h 标准接口,核心四步:
shmget = shared memory get(获取 / 创建共享内存)
shmdt = shared memory detach(卸载 / 分离)---->断开连接
shmctl = shared memory control(控制 / 删除)
1. 创建 / 获取共享内存
- 向操作系统申请 / 找到 那块公共内存
cpp
int shmget(key_t key, size_t size, int shmflg);
-
key:唯一标识(多个进程用同一个 key 访问同一块内存) -
size:内存大小(字节) -
shmflg:权限 + 创建模式(IPC_CREAT 不存在则创建)
2. 挂载到进程地址空间
- 映射 到自己的内存地址空间------->进程才可以访问
cpp
void *shmat(int shmid, const void *shmaddr, int shmflg);
-
返回值:进程可用的虚拟内存指针
-
shmaddr=NULL:系统自动分配地址(推荐)
为什么要挂载?
因为:
-
共享内存是系统公共区域
-
进程不能直接访问
-
必须挂载(映射) 到进程自己的空间
-
才能像普通变量一样读写
3. 读写数据
直接像操作普通内存一样读写指针即可。
4. 卸载 + 删除共享内存
cpp
// 卸载 断开连接
int shmdt(const void *shmaddr);
// 删除(所有进程卸载后执行才会真正释放) 删除内存
int shmctl(int shmid, IPC_RMID, NULL);
完整示例(两个进程通信)
写进程(写入数据)
读进程(读取数据)
cpp
#include "shm.h"
int main()
{
int shmid=shmget((key_t)1234,SIZE,IPC_CREAT|0600);
if(shmid==-1)
{
exit(1);
}
char* s=(char*)shmat(shmid,NULL,0);//建立映射关系
if(s==(char*)-1)
{
exit(1);
}
while(1)
{
char buff[128]={0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
strcpy(s,buff);
}
shmdt(s);//断开映射关系
exit(0);
}
cpp
#include "shm.h"
int main()
{
int shmid=shmget((key_t)1234,SIZE,IPC_CREAT|0600);
if(shmid==-1)
{
exit(1);
}
char* s=(char*)shmat(shmid,NULL,0);//建立映射关系
if(s==(char*)-1)
{
exit(1);
}
while(1)
{
if(s==NULL)
{
break;
}
printf("read=%s",s);
sleep(1);
}
shmdt(s);//断开映射关系
shmctl(shmid,IPC_RMID,NULL);//删除共享内存
exit(0);
}
必须注意:同步问题
共享内存自带并发风险,两个进程同时写会导致数据错乱。
✅ 解决方案:
-
信号量(Semaphore)
-
互斥锁(Mutex)
-
管道 / 消息队列做同步通知
共享内存+2个信号量

cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
#define SIZE 1024
//信号量声明
#include<sys/sem.h>
int semid;
enum INDEX{SEM1,SEM2};
#define SEM_SIZE 2
union semun
{
int val;
};
void sem_init();
void sem_p(enum INDEX in);
void sem_v(enum INDEX in);
void sem_destory();
//信号量定义
void sem_init()
{
int semid=semget((key_t)1234,SEM_SIZE,IPC_CREAT|IPC_EXCL|0600);
if(semid==-1)
{
semid=semget((key_t)1234,SEM_SIZE,0600);//获取已存在的信号量
if(semid==-1)
{
printf("semget err!!!\n");
exit(1);
}
}
else
{
union semun a;
int val[SEM_SIZE]={1,0};
for(int i=0;i<SEM_SIZE;i++)
{
a.val=val[i];
if(semctl(semid,i,SETVAL,a)==-1)
{
printf("sem init err!!!\n");
exit(1);
}
}
}
}
void sem_p(enum INDEX in)
{
struct sembuf buf;
buf.sem_num=in;
buf.sem_op=-1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
printf("p err!!!\n");
}
}
void sem_v(enum INDEX in)
{
struct sembuf buf;
buf.sem_num=in;
buf.sem_op=1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
printf("v err!!!\n");
}
}
void sem_destory()
{
if(semctl(semid,0,IPC_RMID)==-1)
{
printf("sem destroy err!!!\n");
}
}
//写端
int main()
{
int shmid=shmget((key_t)1234,SIZE,IPC_CREAT|0600);
sem_init();
if(shmid==-1)
{
exit(1);
}
char* s=(char*)shmat(shmid,NULL,0);//建立映射关系
if(s==(char*)-1)
{
exit(1);
}
while(1)
{
char buff[128]={0};
fgets(buff,128,stdin);
sem_p(SEM1);
strcpy(s,buff);
sem_v(SEM2);
if(strncmp(buff,"end",3)==0)
{
break;
}
}
shmdt(s);//断开映射关系
exit(0);
}
//读端
#include"sem.h"
int main()
{
int shmid=shmget((key_t)1234,SIZE,IPC_CREAT|0600);
sem_init();
if(shmid==-1)
{
exit(1);
}
char* s=(char*)shmat(shmid,NULL,0);//建立映射关系
if(s==(char*)-1)
{
exit(1);
}
while(1)
{
sem_p(SEM2);
if(strncmp(s,"end",3)==0)
{
break;
}
printf("read=%s",s);
sem_v(SEM1);
}
shmdt(s);//断开映射关系
shmctl(shmid,IPC_RMID,NULL);//删除共享内存
sem_destroy();//销毁信号量
exit(0);
}
常用命令(Linux)
cpp
# 查看系统共享内存
ipcs -m
# 删除指定共享内存
ipcrm -m [shmid]
总结
-
共享内存 = 最快 IPC:无拷贝、直接访问物理内存
-
核心四步:创建 → 挂载 → 读写 → 卸载 / 删除
-
致命缺陷 :无同步,必须配合锁 / 信号量使用
-
适用场景:高频、大数据量的进程通信
共享内存和管道的对比
| 特点 | 管道 | 共享内存 |
|---|---|---|
| 速度 | 慢(拷贝 2 次)(内核缓冲区) | 最快(0 拷贝)(内存) |
| 同步 | 自带同步 | 无同步,必须用信号量 |
| 数据方式 | 流式 | 内存块 |
| 复杂度 | 简单 | 复杂 |
| 方向 | 半双工 | 全双工 |
| 适用场景 | 简单、少量数据 | 高频、大数据量 |
消息队列


Linux 消息队列是内核维护的带类型的消息链表,是进程间异步通信的经典 IPC 机制,支持按类型选择性接收、消息持久化,适合解耦、异步任务与多进程协作。
一、核心概念与特性
-
本质:内核维护的消息链表,每条消息含 类型(mtype,>0) 与数据,支持按类型过滤读取。
-
优势
-
异步通信:发送方写入后即可继续,接收方可稍后读取。
-
按类型接收 :可只取指定类型消息,实现定向通信。
-
持久化:队列与消息独立于进程,进程退出后仍保留。
-
双向 / 多对多:多进程可读写同一队列。
-
-
两种标准
-
System V 消息队列 :传统、兼容性好,接口为
msgget/msgsnd/msgrcv/msgctl。 -
POSIX 消息队列 :接口更现代(
mq_open/mq_send),支持优先级、超时,部分系统需额外库支持。
-
二、System V 消息队列(最常用)
1. 消息结构体(必须自定义)
cpp
// 约定:第一个字段必须是 long 类型的消息类型
struct msgbuf {
long mtype; // 消息类型(>0)
char mtext[1024]; // 消息数据(可自定义为任意结构体)
};
2. 核心系统调用(头文件:``)
(1) 创建 / 获取队列:msgget
cpp
int msgget(key_t key, int msgflg);
-
key :队列唯一标识(可用
ftok生成,或直接用整数如0x1234)。 -
msgflg:
-
IPC_CREAT:不存在则创建,存在则返回 ID。 -
IPC_EXCL:与IPC_CREAT联用,存在则报错(确保新建)。 -
权限位:如
0666(所有用户可读写)。
-
-
返回 :成功返回
msgid(队列 ID),失败返回-1。
(2) 发送消息:msgsnd(message send)
cpp
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
-
msgp:指向消息结构体指针。
-
msgsz :数据部分长度 (不含
mtype,如sizeof(msgbuf)-sizeof(long))。 -
msgflg :
0表示队列满时阻塞;IPC_NOWAIT表示非阻塞(满则返回-1)。 -
返回 :成功
0,失败-1。
(3) 接收消息:msgrcv(message recieve)
cpp
size_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
-
msgtyp:
-
0:读取队列中第一条消息。 -
>0:只读取类型等于 msgtyp 的第一条消息。 -
<0:读取类型 ≤|msgtyp|中最小类型的消息。
-
-
msgflg :
0阻塞;IPC_NOWAIT非阻塞;MSG_NOERROR:数据超长时截断不报错。 -
返回 :成功返回接收数据长度,失败
-1。
(4) 控制 / 删除队列:msgctl
cpp
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
-
cmd:
-
IPC_STAT:获取队列信息到buf。 -
IPC_SET:设置队列属性(权限、大小等)。 -
IPC_RMID:立即删除队列,并释放所有消息。
-
-
返回 :成功
0,失败-1。
3. 完整示例(发送 / 接收)
发送端(msg_send.c)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#define MSG_KEY 0x1234
#define MSG_SIZE 128
struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
};
int main() {
// 1. 创建/获取队列
int msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
if (msqid == -1) { perror("msgget"); exit(1); }
// 2. 准备消息
struct msgbuf msg;
msg.mtype = 1; // 类型1
strcpy(msg.mtext, "Hello from msg_send!");
// 3. 发送消息
if (msgsnd(msqid, &msg, MSG_SIZE, 0) == -1) {
perror("msgsnd"); exit(1);
}
printf("消息发送成功\n");
return 0;
}
接收端(msg_recv.c)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#define MSG_KEY 0x1234
#define MSG_SIZE 128
struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
};
int main() {
// 1. 获取队列
int msqid = msgget(MSG_KEY, 0666);
if (msqid == -1) { perror("msgget"); exit(1); }
// 2. 接收类型1的消息
struct msgbuf msg;
if (msgrcv(msqid, &msg, MSG_SIZE, 1, 0) == -1) {
perror("msgrcv"); exit(1);
}
printf("收到类型%ld的消息:%s\n", msg.mtype, msg.mtext);
// 3. 删除队列(可选,通常由最后一个进程清理)
msgctl(msqid, IPC_RMID, NULL);
return 0;
}
4. 编译与运行
cpp
gcc msg_send.c -o msg_send
gcc msg_recv.c -o msg_recv
./msg_send # 发送
./msg_recv # 接收
三、常用命令(查看 / 删除队列)
-
查看 系统消息队列:
ipcs -q -
删除指定队列:
ipcrm -q或ipcrm -Q
四、系统限制(可调整)
-
单条消息最大长度:
cat /proc/sys/kernel/msgmax(默认约 8KB)。 -
队列总字节上限:
cat /proc/sys/kernel/msgmnb。 -
系统最大队列数:
cat /proc/sys/kernel/msgmni。 -
临时修改(root):
sysctl -w kernel.msgmax=16384。 -
永久修改:编辑
/etc/sysctl.conf,添加kernel.msgmax=16384,执行sysctl -p。
五、POSIX 消息队列(简要对比)
-
接口:
mq_open/mq_send/mq_receive/mq_close/mq_unlink。 -
优势:支持消息优先级、超时、文件系统路径命名、更规范的错误处理。
-
劣势:部分嵌入式 / 旧系统支持不完善,需链接
-lrt。
套接字(socket)
支持网络通信的全双工方式
见下章相关网络编程(TCP)
线程
进程 :一个正在运行 的程序
线程 :进程内部 的一条执行路径
内核级线程

cpp
ps -eLf l greap +文件.exe -L----->显示线程id
线程实现
cpp
//mian.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include <pthread.h>
void *fun(void* arg)
{
for(int i=0;i<5;i++)
{
printf("fun run\n");
sleep(1);
}
pthread_exit("fun exit");
}
int main()
{
pthread_t id;//定义线程id
pthread_create(&id,NULL,fun,NULL);//创建后---->分配id
for(int i=0;i<5;i++)
{
printf("main run\n");
sleep(1);
}
char* s=NULL;
pthread_join(id,(char**)&s);//等待线程结束
exit(0);
}
//主线程 结束----->进程结束、退出
cpp
void* fun(void*arg)
{
int* p=(int*)arg;
int idx=*p;
printf("idx=%d\n",idx);
}
int main()
{
pthread_t id[5];
int i=0;
for(;i<5;i++)
{
pthread_create(&id[i],NULL,fun,&i);
}
for(i=0;i<5;i++)
{
pthread_join(id[i],NULL);
}
exit(0);
}
//随机打印---->线程启动快慢不一,i是共享的
//按顺序创建,但执行顺序不一,可能边创建边执行
//创建堆区
void* fun(void*arg)
{
int* p=(int*)arg;
int idx=*p;
free(p);
printf("idx=%d\n",idx);
}
int main()
{
pthread_t id[5];
int i=0;
for(;i<5;i++)
{
int* p=(int*)malloc(sizeof(int));
*p=i;
pthread_create(&id[i],NULL,fun,(void*)p);
}
for(i=0;i<5;i++)
{
pthread_join(id[i],NULL);
}
exit(0);
}
int g_val=1;
void* fun(void*arg)
{
for(int i=0;i<1000;i++)
{
printf("g_val=%d\n",g_val++);
}
}// g_val++ 非原子操作
//1.先从内存上读取g_val 当前值
//2.g_val+1
//3.新值 写回 内存
int main()
{
pthread_t id[5];
int i=0;
for(;i<5;i++)
{
pthread_create(&id[i],NULL,fun,&i);
}
for(i=0;i<5;i++)
{
pthread_join(id[i],NULL);
}
exit(0);
}
//引入 信号量、互斥锁
int g_val=1;
//pthread_mutex_t mutex;
sem_t sem;
void* fun(void*arg)
{
for(int i=0;i<1000;i++)
{
sem_wait(&sem);//p
//pthread_mutex_lock(&mutex);//上锁
printf("g_val=%d\n",g_val++);
sem_pos(&sem);//v
//pthread_mutex_unlock(&mutex);//解锁
}
}
int main()
{
pthread_t id[5];
sem_init(&sem,0,1);//0--->局部进程使用 初始化信号量=1
//pthread_mutex_init(&mutex,NULL);
int i=0;
for(;i<5;i++)
{
pthread_create(&id[i],NULL,fun,&i);
}
for(i=0;i<5;i++)
{
pthread_join(id[i],NULL);
}
sem_destory(&sem);
//pthread_mutex_destory(&mutex);
exit(0);
}
编译
bash
gcc -o main main.c -pthread
核心 API
| API | 作用 |
|---|---|
pthread_create |
创建并启动线程 |
pthread_join |
等待线程结束(阻塞) |
pthread_exit |
线程主动退出 |
pthread_detach |
分离线程 |
| 函数 | 参数 | 作用 |
|---|---|---|
| pthread_create | (tid, NULL, 函数,参数) | 创建线程 |
| pthread_join | (tid, & 返回值) | 等线程、收资源 |
| pthread_exit | (返回值) | 线程自己退出 |
| pthread_self | 无 | 拿自己线程 ID |
| pthread_cancel | (tid) | 杀死指定线程 |
| pthread_detach | (tid) | 设为自动回收 |
并发与并行
线程并发运行
-
并发 :1 个 CPU 核心,快速切换 多个线程(交替进行)
-
并行 :多个 CPU 核心,真正同时 运行多个线程(同时进行)
操作系统角度分析,线程实现的三种
-
用户级------>并发
-
内核级------>并行
-
组合模型
线程同步
目的:多线程同时访问共享资源时,按照预定顺序进行,防止数据混乱、保证结果正确
-
信号量(Semaphore)--->计数同步
-
互斥锁(Mutex)----->最常用
-
读写锁
-
条件变量(Condition)------->配合锁使用
一、互斥锁 (最核心)
作用:同一时间只允许一个线程访问共享资源
1. 核心函数
cpp
// 初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// 加锁(阻塞)
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
2. 参数说明
-
mutex:互斥锁变量 -
attr:属性,一般填 NULL(默认)
3. 使用规则
-
进入共享区域前 lock
-
退出共享区域后 unlock
-
必须成对使用,否则会死锁
二、条件变量 pthread_cond_t(等待 + 唤醒)
作用:让线程等待某个条件成立,不浪费 CPU
常和 互斥锁
1. 核心函数
cpp
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
// 等待条件成立(会自动释放锁,休眠后自动拿回锁)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
// 唤醒一个等待线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
三、信号量 sem_t(计数同步)
作用:控制最多 N 个线程同时进入临界区
用于生产者 - 消费者
1. 头文件
cpp
#include <semaphore.h>
2. 核心函数
cpp
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// P操作:申请资源(-1),不足则阻塞
int sem_wait(sem_t *sem);
// V操作:释放资源(+1)
int sem_post(sem_t *sem);
// 销毁信号量
int sem_destroy(sem_t *sem);
3. 参数
-
pshared:0 = 线程间使用,非 0 = 进程间使用 -
value:初始资源数
四、自旋锁 pthread_spinlock_t
作用:不睡眠,循环等待,适合锁内操作极快的场景
核心函数
cpp
pthread_spin_init();
pthread_spin_lock();
pthread_spin_unlock();
pthread_spin_destroy();
最经典:互斥锁完整示例代码(必背)
cpp
#include <stdio.h>
#include <pthread.h>
int share = 0; // 共享资源
pthread_mutex_t mutex; // 定义互斥锁
void* func(void* arg) {
// 加锁
pthread_mutex_lock(&mutex);
share++; // 安全访问共享资源
printf("share = %d\n", share);
// 解锁
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t t1, t2;
// 初始化锁
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, func, NULL);
pthread_create(&t2, NULL, func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 销毁锁
pthread_mutex_destroy(&mutex);
return 0;
}
编译:
bash
gcc test.c -o test -pthread
线程同步 极简总结(面试 / 考试直接背)
-
互斥锁:同一时间只允许一个线程访问(最常用)
-
条件变量:线程等待条件,不占 CPU(配合锁)
-
信号量:控制 N 个线程同时进入(生产者消费者)
-
自旋锁:忙等待,锁内极快时使用
-
所有同步的目的:保证共享资源安全、不乱序、不冲突
线程同步 指的是当一个线程 在对某个临界资源进行操作 时,其他线程都不可以 对这个资 源进行操作,直到该线程完成操作, 其他线程才能操作,也就是协同步调 ,让线程按预定的 先后次序进行运行。
生产者和消费者问题

-
有限缓冲区
-
不可以同时访问 ----->互斥锁
-
生产者、消费者 是否可以进行操作------>信号量
-
缓冲区满 -------->生产者等待
-
缓冲区空 --------->消费者等待
-
优点:
-
解耦 :通过缓冲区,二者不会直接调用
-
支持并发 :生产者和消费者---->独立的个体
-
二者处理能力 达到动态平衡
cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<pthread.h>
#include<semaphore.h>
#include<time.h>
#define BUFF_MAX 30
#define SC_NUM 2
#define XF_NUM 3
int in=0;
int out=0;
sem_t sem_empty;
sem_t sem_full;
phread_mutex_t mutex;
int buff[BUFF_MAX]={0};
void* sc_thread(void* arg)
{
int idx=(int)arg;
while(1)
{
sem_wait(&sem_empty);//p操作
pthread_mutex_lock(&mutex);//加锁
buff[in]=rand()%100;
printf("生产者%d 产生数据%d,in=%d",idx,buff[in],in);
in=(in+1)%BUFF_MAX;
pthread_mutex_unlock(&mutex);//解锁
sem_post(&sem_full);//v操作--->消费者
int n=rand()%10;
sleep(n);
}
}
void* xf_thread(void* arg)
{
int idx=(int)arg;
while(1)
{
sem_wait(&sem_full);//p操作
phread_mutex_lock(&mutex);//加锁
printf("消费者%d 消费数据%d,out=%d",idx,buff[out],out);
}
out=(out+1)%SUFF_MAX;
phread_mutex_unlock(&mutex);//解锁
sem_post(&empty_empty);//v操作--->生产者可以生产+1
int n=rand()%10;
sleep(n);
}
int main()
{
phread_mutex_init(&mutex,NULL);
sem_init(&sem_empty,0,BUFF_MAX);//生产者 信号量
sem_init(&sem_full,0,0);//消费者 信号量
srand((int)time(0));
phread_t sc_id[SC_MAX];
phread_t xf_id[XF_MAX];
int i=0;
for(;i<SC_MAX;i++)
{
phread_create(&sc_id[i],NULL,sc_thread,(void*)i);
}
for(i=0;i<xf_MAX;i++)
{
phread_create(&xf_id[i],NULL,xf_thread,(void*)i);
}
for(;i<SC_MAX;i++)
{
phread_join(&sc_id[i],NULL);
}
for(i=0;i<xf_MAX;i++)
{
phread_join(&xf_id[i],NULL);
}
sem_destory(&sem_empty);
sem_destroy(&sem_full);
pthread_mutex_destory(&mutex);
exit(0);
}
线程安全=正确性
无论调度顺序如何,都可以得到正确的结果
多个线程 共享同一块内存 ,互相覆盖 、互相干扰 → 数据竞争 → 结果错乱。
-
同步:信号量、互斥锁、读写锁、条件变量
-
使用线程安全的函数(可重入函数)
eg:strtock_r
strtok ----->内部使用**静态变量(static)**保存上次切割位置
-
全局唯一 、线程共享 、不可重入
-
多线程同时调用 互相干扰
strtok_r ----->自己传指针,线程独立
- char *strtok_r(char *str, const char *delim, char **saveptr);

| 不安全 | 安全版本(线程可用) | 作用 |
|---|---|---|
| strtok | strtok_r | 切割字符串 |
| asctime | asctime_r | 时间转字符串 |
| ctime | ctime_r | 时间转字符串 |
| gmtime | gmtime_r | 时间解析 |
| localtime | localtime_r | 本地时间 |
| gethostbyname | gethostbyname_r | DNS 解析 |
多线程fork()
子进程 只有一条执行路径(fork()所在的路径)
-
千万不要在多线程程序里随便调用 fork ()
-
fork () 只会复制 当前调用线程, 其他线程 全部消失!
-
父进程的锁、状态、全局变量 会被原样复制,极易死锁!
问题 1:多线程中某个线程调用 fork (),子进程会有和父进程相同数量的线程吗?
结论
绝对不会!
子进程只有调用 fork () 的这一个线程其他所有线程在子进程中全部消失
底层原理
fork() 的设计规则:
-
只复制当前调用 fork 的线程的执行上下文
-
父进程的其他线程在子进程地址空间中直接终止,不会进入子进程
-
子进程 启动时,只有一个线程在运行
简单比喻
父进程是一个有 5 个人的办公室,只有 1 个人按下了复制按钮 ,复制出来的新办公室里,只有这一个人,其他 4 个人都不存在。
问题 2:父进程被加锁的互斥锁,fork 后在子进程中是否已经加锁?
结论
是的!子进程中的互斥锁会保持 "已加锁" 状态,且永远无法解锁!
底层原理
-
fork()会完整复制父进程的内存状态(包括互斥锁的状态) -
如果父进程中某个线程持有锁,fork 时,锁的 "已加锁" 标记会被原样复制到子进程
-
关键:持有锁的那个线程,在子进程里已经消失了
-
结果:子进程里的锁永远处于加锁状态,无法解锁 ,任何尝试加锁的操作都会死锁
-
fork 做的事只有两件:
-
复制整个内存(包括 mutex 那块数据)
-
只把线程 A 复制过去当子进程唯一线程
-
致命风险
这是多线程 + fork 最危险的坑:
-
子进程拿到一把永远锁死的互斥锁
-
子进程调用任何依赖这个锁的函数(如
malloc、printf、线程库函数)都会直接死锁
核心总结(必背)
-
多线程调用 fork,子进程只有单线程,父进程其他线程全部丢失
-
父进程加锁的互斥锁,fork 后子进程中依然是加锁状态,且无法解锁,极易死锁
-
最佳实践 :多线程程序中,fork 后子进程必须立即调用 exec 系列函数替换进程,不要在子进程里做复杂操作
总结
-
多线程调用
fork,子进程仅保留调用 fork 的单个线程,父进程其他线程全部消失 -
父进程已加锁的互斥锁,
fork后子进程中保持加锁状态且永久无法解锁,会直接引发死锁 -
多线程环境下,
fork后子进程应立刻调用 exec 函数,避免操作共享资源
读写锁
-
读可以共享,写必须独占
-
读锁(共享锁):多个线程可以同时加读锁
-
写锁(独占锁):只能一个线程加写锁
规则:
-
读 + 读 → 不互斥,可以并发
-
读 + 写 → 互斥
-
写 + 写 → 互斥
优点:读多写少场景,并发性能大幅提升
缺点:逻辑更复杂,可能出现写饥饿
cpp
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 定义读写锁
pthread_rwlock_t rwlock;
int data = 0;
// 读线程
void* read_fun(void* arg)
{
// 加读锁
pthread_rwlock_rdlock(&rwlock);
printf("线程%d 读 data=%d\n", (int)arg, data);
sleep(1); // 模拟读耗时
// 解读锁
pthread_rwlock_unlock(&rwlock);
return NULL;
}
// 写线程
void* write_fun(void* arg)
{
// 加写锁
pthread_rwlock_wrlock(&rwlock);
data++;
printf("线程%d 写 data=%d\n", (int)arg, data);
sleep(1);
// 解写锁
pthread_rwlock_unlock(&rwlock);
return NULL;
}
int main()
{
pthread_t tid1, tid2, tid3, tid4;
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 创建读线程
pthread_create(&tid1, NULL, read_fun, (void*)1);
pthread_create(&tid2, NULL, read_fun, (void*)2);
// 创建写线程
pthread_create(&tid3, NULL, write_fun, (void*)3);
pthread_create(&tid4, NULL, write_fun, (void*)4);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
// 销毁锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
读者和写者模型
提高并发效率,同时保证安全
读可以一起,写必须独占
cpp
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
// 共享数据
int data = 0;
// 读者计数器
int read_count = 0;
// 互斥锁:保护 read_count
pthread_mutex_t mutex;
// 读写互斥锁:读和写互斥,写和写互斥
sem_t rw_sem;//初始化为1
// 读者线程
void *reader(void *arg) {
int id = *(int *)arg;
// 进入临界区:修改 read_count
pthread_mutex_lock(&mutex);
read_count++;
// 第一个读者,加写锁
if (read_count == 1)
sem_wait(&rw_sem);
pthread_mutex_unlock(&mutex);
// 读操作
printf("读者 %d 读取 data = %d\n", id, data);
// 离开临界区
pthread_mutex_lock(&mutex);
read_count--;
// 最后一个读者,释放写锁
if (read_count == 0)
sem_post(&rw_sem);
pthread_mutex_unlock(&mutex);
return NULL;
}
// 写者线程
void *writer(void *arg) {
int id = *(int *)arg;
// 加写锁(独占)
sem_wait(&rw_sem);
// 写操作
data++;
printf("写者 %d 修改 data = %d\n", id, data);
// 释放写锁
sem_post(&rw_sem);
return NULL;
}
int main() {
pthread_t r1, r2, w1, w2;
int id1 = 1, id2 = 2;
// 初始化
pthread_mutex_init(&mutex, NULL);
sem_init(&rw_sem, 0, 1);
// 创建线程
pthread_create(&r1, NULL, reader, &id1);
pthread_create(&r2, NULL, reader, &id2);
pthread_create(&w1, NULL, writer, &id1);
pthread_create(&w2, NULL, writer, &id2);
// 等待结束
pthread_join(r1, NULL);
pthread_join(r2, NULL);
pthread_join(w1, NULL);
pthread_join(w2, NULL);
// 销毁
pthread_mutex_destroy(&mutex);
sem_destroy(&rw_sem);
return 0;
}
条件变量
加锁 = 强制让 "判断条件 + 进入等待" 变成一个不可分割的整体!
wait和signal 加锁----->保证操作的原子性
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
char buff[128] = {0};
void * funa(void* arg)
{
while( 1 )
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);//放在等待队列,解锁,自己阻塞。唤醒返回时加锁
pthread_mutex_unlock(&mutex);
if( strncmp(buff,"end",3) == 0 )
{
break;
}
printf("funa buff=%s\n",buff);
}
}
void * funb(void* arg)
{
while( 1 )
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);//放在等待队列,解锁,自己阻塞。唤醒返回时加锁
pthread_mutex_unlock(&mutex);
if( strncmp(buff,"end",3) == 0 )
{
break;
}
printf("funb buff=%s\n",buff);
}
}
int main()
{
pthread_mutex_init(&mutex,NULL);//锁
pthread_cond_init(&cond,NULL);//条件变量初始化
pthread_t ida, idb;
pthread_create(&ida,NULL,funa,NULL);
pthread_create(&idb,NULL,funb,NULL);
while( 1 )
{
fgets(buff,128,stdin);
if( strncmp(buff,"end",3) == 0 )
{
//唤醒所有
pthread_mutex_lock(&mutex);
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);
break;
}
else
{
//唤醒一个线程
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
}
pthread_join(ida,NULL);
pthread_join(idb,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
exit(0);
}
条件是共享变量 ,多线程访问必须加锁
✅ wait 加锁:保护条件,防止判断错乱
✅ signal 加锁:防止丢信号,保证一定唤醒
✅ 同一把锁:让等待和唤醒有序,不会乱序
生产者和消费者(条件变量+互斥锁)
cpp
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define MAX 5 // 缓冲区最大容量
// 共享缓冲区
int buf[MAX];
int head = 0; // 队头(取数据)
int tail = 0; // 队尾(放数据)
int size = 0; // 当前数据个数
pthread_mutex_t mutex;
pthread_cond_t not_full; // 不满 -> 生产者可以放
pthread_cond_t not_empty; // 不空 -> 消费者可以取
// 生产者:生产数据
void *producer(void *arg)
{
int num = 0;
while (1) {
pthread_mutex_lock(&mutex);
// ======================
// ✅ 关键:队列满了就等待!
// ======================
while (size == MAX) {
printf("队列满了,生产者等待...\n");
pthread_cond_wait(¬_full, &mutex);
}
// 生产数据
buf[tail] = ++num;
printf("生产者生产:%d\n", num);
tail = (tail + 1) % MAX;
size++;
// 唤醒消费者
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}
// 消费者:消费数据
void *consumer(void *arg)
{
while (1) {
pthread_mutex_lock(&mutex);
// ======================
// ✅ 关键:队列空了就等待!
// ======================
while (size == 0) {
printf("队列空了,消费者等待...\n");
pthread_cond_wait(¬_empty, &mutex);
}
// 消费数据
int val = buf[head];
printf("消费者消费:%d\n", val);
head = (head + 1) % MAX;
size--;
// 唤醒生产者
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mutex);
sleep(2);
}
return NULL;
}
int main()
{
pthread_t pro, con;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(¬_full, NULL);
pthread_cond_init(¬_empty, NULL);
pthread_create(&pro, NULL, producer, NULL);
pthread_create(&con, NULL, consumer, NULL);
pthread_join(pro, NULL);
pthread_join(con, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
return 0;
}
pthread_cond_wait 醒来 后,必须重新检查条件!
while----->防止被虚假唤醒
