共享内存
特点
速度最快:数据直接映射到进程地址空间,零拷贝、零内核切换。
容量大:受物理内存 + ulimit 限制,可放大文件(shm_open 版)。
无内置同步:读写双方必须自行加锁(信号灯、互斥量等)。
生命周期随内核:除非显式 shmctl(IPC_RMID) 或重启,否则一直存在。
作用
大吞吐量、低延迟场景:图像帧、高速日志、AI 推理结果、数据库 buffer pool。
频繁交换大块数据且已另有同步手段的模块。
ftok – 生成 System V IPC 键值:
key_t ftok(const char *path, int proj_id);
成功返回key值,失败返回 -1。
path 为随意一个地址;proj_id也是随便的值(只有前八位有效)。
path + id 会创建一个独一无二的键值,使后续的进程可以通过相同的参数创建一样的键值通信。
后续的通信方式都能遇到
shmget – 创建 / 获取共享内存:
int shmget(key_t key, size_t size, int shmflg);
成功返回 shmid,失败 -1。
shmflg 创建的参数,常用组合有:
- `IPC_CREAT | 0666` 不存在则创建,权限 0666
- `IPC_CREAT | IPC_EXCL | 0666` 不存在则创建,已存在则返回 -1(排他创建)
- `0` 仅打开已存在的段
size 是共享内存的大小,可按需求分配
同一 key 多次调用都只会返回同一 id。
shmat – 投影共享内存:
void *shmat(int shmid, const void *shmaddr, int shmflg);
成功返回映射首地址,失败 (void *)-1。
shmaddr 选择:
- `NULL` 内核自动选地址(最常用)
- `非零值` 用作映射起始地址(需页对齐,极少用)
推荐为NULL,因为时间编程并不能知道哪里地址没用占用
shmflg 选项:
- `0` 读写
- `SHM_RDONLY` 只读
shmdt – 解除投影:
int shmdt(const void *shmaddr);
成功 0,失败 -1。
仅脱离当前进程,共享内存段仍存在内核中,所以重新推出前要写这个函数和下面的函数。
shmctl – 控制共享内存:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
成功 0,失败 -1。
cmd 可选:
- `IPC_RMID` 标记销毁(引用计数为 0 时内核回收,退出程序和上面一个函数一起写)
- `IPC_STAT` 取当前属性到 buf
- `IPC_SET` 用 buf 改权限/属主
- `SHM_LOCK` 锁定页不被换出(需 CAP_IPC_LOCK)
- `SHM_UNLOCK` 解锁
示例:
common.h
#ifndef COMMON_H
#define COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
#include <fcntl.h>
//给ftok使用
#define PATHNAME "."
#define PROJ_ID 66
#define SHM_SIZE sizeof(shm_block_t)
typedef struct {
sem_t sem_writer;
sem_t sem_reader;
int counter;
} shm_block_t;
#endif
writer.h
#include "common.h"
int main(){
key_t key;
int shmid;
if((key = ftok(PATHNAME,PROJ_ID)) < 0 ){
perror("ftok");
exit(-1);
}
if((shmid = shmget(key,SHM_SIZE,IPC_CREAT | 0666)) < 0){
perror("shmget");
exit(-1);
}
shm_block_t *shm;
if((shm = (shm_block_t *)shmat(shmid,NULL,0)) == (void *)-1 ){
perror("shmat");
exit(-1);
}
if (sem_init(&shm->sem_writer, 1, 1) == -1) { perror("sem_init writer"); exit(-1); }
if (sem_init(&shm->sem_reader, 1, 0) == -1) { perror("sem_init reader"); exit(-1); }
shm->counter = 0;
while(1){
sem_wait(&shm->sem_writer);
if(shm->counter == 10){
break;
}
shm->counter++;
printf("writer: %d\n",shm->counter);
sem_post(&shm->sem_reader);
sleep(1);
}
shmdt(shm);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
reader.c
#include "common.h"
int main(){
key_t key;
int shmid;
if((key = ftok(PATHNAME,PROJ_ID)) < 0 ){
perror("ftok");
exit(-1);
}
if((shmid = shmget(key,SHM_SIZE,IPC_CREAT | 0666)) < 0){
perror("shmget");
exit(-1);
}
shm_block_t *shm;
if((shm = (shm_block_t *)shmat(shmid,NULL,0)) == (void *)-1 ){
perror("shmat");
exit(-1);
}
while(1){
sem_wait(&shm->sem_reader);
printf("reader: %d\n",shm->counter);
sem_post(&shm->sem_writer);
if(shm->counter == 10){
break;
}
}
shmdt(shm);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
消息队列
特点
自带同步:发送/接收是原子操作,无需额外锁。
按类型取消息:支持优先级、多路复用(msgtyp > 0)。
数据拷贝两次:用户→内核→用户,速度低于共享内存。
容量受限:单条长度 ≤ MSGMAX,队列总数 ≤ MSGMNI,内核参数可调。
生命周期随内核:msgctl(IPC_RMID) 后不再接收,但已读进程可继续读完。
作用
任务分发:父进程按类型投递子任务,多个 worker 取各自类型。
控制流通道:命令/应答短小消息,长度固定、频率中等的场景。
msgget – 创建 / 获取消息队列:
int msgget(key_t key, int msgflg);
成功返回队列id,失败 -1。
msgflg 常用:
- `IPC_CREAT | 0666` 不存在则创建
- `IPC_CREAT | IPC_EXCL | 0666` 排他创建
- `0` 仅打开已存在队列
msgsnd – 发送消息:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);、
成功 0,失败 -1。
msgsz 是写入的大小
msgflg 选项:
- `0` 阻塞直到可写
- `IPC_NOWAIT` 队列满时立即返回 -1 并设 errno=EAGAIN
msgrcv – 接收消息:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
成功返回实际字节数,失败 -1。
msgtyp 是接受的类型,如:sizeof(int)即4,就是整型。
msgflg 选项: 同上
msgctl – 控制消息队列:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
成功 0,失败 -1。
cmd 可选:
- `IPC_RMID` 立即删除队列(仍在使用的进程可继续)
- `IPC_STAT` 取属性到 buf
- `IPC_SET` 用 buf 改权限、属主、最大消息数等
示例:
common.h
#ifndef COMMON_H
#define COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//给ftok使用
#define PATHNAME "."
#define PROJ_ID 20
#define MSG_SIZE 64
typedef struct {
long mytpe;
int num;
} msg_t;
#endif
writer.c
#include "common.h"
int main(){
key_t key;
int msgid;
if((key = ftok(PATHNAME,PROJ_ID)) < 0 ){
perror("ftok");
exit(-1);
}
if((msgid = msgget(key,IPC_CREAT | 0666)) < 0){
perror("msgget");
exit(-1);
}
msg_t reader = { .mytpe = 1 };
msg_t writer = { 0 };
for(int i = 1; ;i++){
if(i == 11){
break;
}
reader.num = i;
if (msgsnd(msgid, &reader, sizeof(int), 0) == -1)
{
perror("msgsnd"); exit(1);
}
printf("writer sent: %d\n", i);
if (msgrcv(msgid, &writer, sizeof(int), 2, 0) == -1)
{
perror("msgrcv"); exit(1);
}
sleep(1);
}
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
reader.c
#include "common.h"
#include <time.h>
int main(){
key_t key = ftok(PATHNAME, PROJ_ID);
if (key == -1) { perror("ftok"); exit(-1); }
int msgid = msgget(key, 0666);
if (msgid == -1) { perror("msgget"); exit(-1); }
msg_t reader = { 0 };
msg_t writer = { .mytpe = 2 };
while (1) {
if (msgrcv(msgid, &reader, sizeof(int), 1, 0) == -1)
{ perror("msgrcv"); exit(-1); }
time_t now = time(NULL);
printf("[reader] 收到信息!num=%d 时间=%s", reader.num, ctime(&now));
//回复空信息,让writer继续
if (msgsnd(msgid, &writer, 0, 0) == -1)
{ perror("msgsnd ack"); exit(-1); }
if(reader.num == 10){
break;
}
}
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
信号灯集
特点
只做同步,不搬运数据:本质是计数器数组。
原子批量操作:一次 semop 可同时对多个灯加减,全成功或全失败。
支持“等待归零”:可实现资源耗尽或屏障同步。
持久化:创建后一直存在,需显式 semctl(IPC_RMID) 删除。
开销最低:单个灯仅 2 字节,适合高频加解锁。
作用
共享内存的配套锁:Ping-Pong 缓冲区、读写锁、环形队列指针。
多资源并发控制:连接池、缓冲区槽位、打印机票据序号。
semget – 创建 / 获取信号灯集:
int semget(key_t key, int nsems, int semflg);
成功返回 semid,失败 -1。
nsems为信号灯的个数
semctl – 对单个或整集灯进行控制:
int semctl(int semid, int semnum, int cmd, ...);
成功 0,失败 -1。
第四参数 union semun 按 cmd 选用:
- `SETVAL` 给第 semnum 灯赋初值(用 semun.val)
- `SETALL` 给整集赋初值(用 semun.array)
- `GETVAL` 取第 semnum 灯当前值
- `GETALL` 取整集当前值
- `IPC_RMID` 立即删除整个信号灯集
- `IPC_STAT` 取属性到 semun.buf
- `GETPID` 返回最后一次操作该灯的进程 PID
- `GETNCNT` 返回等待该灯值增加的进程数
- `GETZCNT` 返回等待该灯值归零的进程数
如果想删除信号灯就semctl(semid,0,IPC_RMID,NULL);
semop – P/V 操作 :
int semop(int semid, struct sembuf *sops, size_t nsops);
成功 0,失败 -1。
sops 数组元素 struct sembuf 字段为
- `sem_num` 灯编号(从0开始,到nsems-1)
- `sem_op`:
-1=P(等待资源), +1=V(释放资源), 0=等待归零(理论可以+2-2+3-3的,但是推荐都只操作1)
- `sem_flg` 0 / IPC_NOWAIT,表示阻塞或直接退出
nsops为操作的信号灯个数。
示例:
common.h
#ifndef COMMON_H
#define COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#define PATHNAME "."
#define PROJ_ID 66
#define SHM_SIZE 1024 // 1 KB 足够
enum { SEM_WRITER, SEM_READER, SEM_NSEMS };
void P(int semid, int semnum)
{
struct sembuf sb = { .sem_num = semnum, .sem_op = -1, .sem_flg = 0 };
if (semop(semid, &sb, 1) == -1) { perror("semop P"); exit(1); }
}
void V(int semid, int semnum)
{
struct sembuf sb = { .sem_num = semnum, .sem_op = +1, .sem_flg = 0 };
if (semop(semid, &sb, 1) == -1) { perror("semop V"); exit(1); }
}
#endif
writer.c
#include "common.h"
int main(void)
{
key_t key = ftok(PATHNAME, PROJ_ID);
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
char *shm = (char *)shmat(shmid, NULL, 0);
if (shm == (void *)-1) { perror("shmat"); exit(1); }
int semid = semget(key, SEM_NSEMS, IPC_CREAT | 0666);
unsigned short vals[SEM_NSEMS] = {1, 0};
union semun { int val; struct semid_ds *buf; unsigned short *array; } su;
su.array = vals;
semctl(semid, 0, SETALL, su);
while (1) {
P(semid, SEM_WRITER);
printf("writer> ");
fflush(stdout);
if (!fgets(shm, SHM_SIZE, stdin)) break;
shm[strcspn(shm, "\n")] = '\0';
if (strcmp(shm, "quit") == 0) {
V(semid, SEM_READER);
break;
}
V(semid, SEM_READER);
}
shmdt(shm);
shmctl(shmid,IPC_RMID,NULL);
semctl(semid,0,IPC_RMID,NULL);
return 0;
}
reader.c
#include "common.h"
int main(void)
{
key_t key = ftok(PATHNAME, PROJ_ID);
int shmid = shmget(key, SHM_SIZE, 0666);
char *shm = (char *)shmat(shmid, NULL, 0);
if (shm == (void *)-1) { perror("shmat"); exit(1); }
int semid = semget(key, SEM_NSEMS, 0666);
while (1) {
P(semid, SEM_READER);
if (strcmp(shm, "quit") == 0) break;
printf("reader 输出: \"%s\" ,大小=%zu 字节\n", shm, strlen(shm));
fflush(stdout);
V(semid, SEM_WRITER);
}
shmdt(shm);
shmctl(shmid,IPC_RMID,NULL);
semctl(semid,0,IPC_RMID,NULL);
return 0;
}
总结:
| 维度 | 共享内存 | 消息队列 | 信号灯集 |
|---|---|---|---|
| 拷贝次数 | 0(用户-用户直接) | 2(用户→内核→用户) | 0(仅计数) |
| 系统调用频率 | 仅 attach/detach | 每次收发各 1 次 | 每次 PV 各 1 次 |
| 数据吞吐量 | 最高 | 中等 | 无数据 |
| 单条上限 | ≈ 物理内存 | MSGMAX(通常 8 KB) | 2 字节 × 灯数 |
| 批量原子 | 不支持 | 单条原子 | 多灯原子 |
常见组合模式:
单生产者 + 单消费者(Ping-Pong)
共享内存(数据) + 2 个信号灯(空/满)
多生产者 + 单消费者
消息队列(type=1 表示任务)
消费者按 type 聚合或轮流读取;无需额外锁。
资源池管理
信号灯集初值 = 池大小
每次申请资源 P(-1),释放 V(+1);支持一次拿/放多个资源(sem_op ≠ ±1)。
零拷贝日志
共享内存环形缓冲
写指针、读指针各用 1 个灯保护;读线程写完指针后 V,写线程等待 P。
共享内存流程
一句话:先“拿地”,再“装修”,再“搬家”,最后“拆房子”。
拿地 – shmget:向内核申请一块物理内存,拿到门牌号 shmid。
装修 – shmat:把这块内存挂到你家进程地址空间里,现在你能像普通数组一样用。
搬家 – 读/写:随便 strcpy、memcpy,速度飞起,但记得先加信号灯“锁门”。
搬离 – shmdt:进程自己不用了,解除映射,房子还在。
拆房子 – shmctl(IPC_RMID):真正把内存归还给内核,别人再也打不开。
消息队列流程
一句话:先“开邮箱”,再“投信”,再“取信”,最后“注销邮箱”。
开邮箱 – msgget:内核给你建一个链表邮箱,返回 msqid。
投信 – msgsnd:把你的消息(带类型标签)塞进链表尾,满了就排队。
取信 – msgrcv:按类型挑一条消息出来,没有就睡觉(可设不阻塞)。
注销邮箱 – msgctl(IPC_RMID):邮箱销毁,已投的信读完就消失。
信号灯集流程
一句话:先“造红绿灯”,再“设初始灯色”,再“日常过路口”,最后“拆红绿灯”。
造红绿灯 – semget:告诉内核要几个灯,拿到灯控器 semid。
设灯色 – semctl(SETALL):一次性把每个灯设为红/绿数字(如 {1,0})。
过路口 – semop:
红灯 -1(P):灯值够就减,不够就排队等。
绿灯 +1(V):释放资源,唤醒等待队列。
拆红绿灯 – semctl(IPC_RMID):灯控器销毁,所有等待进程立即报错返回。
4007

被折叠的 条评论
为什么被折叠?



