一、消息队列
1.1 什么是消息队列
消息队列是有格式的消息传递机制,数据按消息块发送和接收,每个消息可以指定类型。
1.2 IPC对象的查看与删除
bash
ipcs # 查看所有IPC对象
ipcs -q # 查看消息队列
ipcs -m # 查看共享内存
ipcs -s # 查看信号量
ipcrm -q msqid # 删除消息队列
ipcrm -m shmid # 删除共享内存
二、消息队列函数接口
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | ftok() |
生成IPC对象键值 |
| 2 | msgget() |
创建/获取消息队列 |
| 3 | msgsnd() |
发送消息 |
| 4 | msgrcv() |
接收消息 |
| 5 | msgctl() |
控制/删除消息队列 |
2.1 ftok : 生成键值
c
key_t ftok(const char *pathname, int proj_id);
c
key_t key = ftok(".", 'a');
if (key == -1) {
perror("ftok");
return -1;
}
2.2 msgget : 创建消息队列
c
int msgget(key_t key, int msgflg);
c
int msqid = msgget(key, IPC_CREAT | 0664);
if (msqid == -1) {
perror("msgget");
return -1;
}
2.3 msgsnd : 发送消息
c
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
消息结构体必须按照固定格式:
c
struct msgbuf {
long mtype; // 消息类型(必须 > 0)
char mtext[128]; // 消息内容
};
c
struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "hello");
msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0);
2.4 msgrcv : 接收消息
c
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
c
struct msgbuf msg;
ssize_t ret = msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0);
if (ret > 0) {
printf("收到:%s\n", msg.mtext);
}
msgtyp 取值:
| msgtyp | 效果 |
|---|---|
> 0 |
接收指定类型的消息 |
== 0 |
接收队列中第一条消息(FIFO) |
< 0 |
接收类型小于等于 abs(msgtyp) 的最小类型消息 |
2.5 msgctl : 控制消息队列
c
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
c
msgctl(msqid, IPC_RMID, NULL); // 删除消息队列
三、共享内存
3.1 什么是共享内存
共享内存是进程间通信最快的方式,多个进程直接映射同一块物理内存,数据不需要在内核和用户空间之间复制。
3.2 共享内存操作步骤
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | ftok() |
生成键值 |
| 2 | shmget() |
创建共享内存 |
| 3 | shmat() |
映射到进程地址空间 |
| 4 | 读写 | 使用共享内存 |
| 5 | shmdt() |
解除映射 |
| 6 | shmctl() |
删除共享内存 |
3.3 shmget : 创建共享内存
c
int shmget(key_t key, size_t size, int shmflg);
c
int shmid = shmget(key, 1024, IPC_CREAT | 0664);
3.4 shmat : 映射
c
void *shmat(int shmid, const void *shmaddr, int shmflg);
c
char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (void *)-1) {
perror("shmat");
return -1;
}
3.5 shmdt : 解除映射
c
int shmdt(const void *shmaddr);
c
shmdt(shmaddr);
3.6 shmctl : 控制共享内存
c
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
c
shmctl(shmid, IPC_RMID, NULL); // 删除共享内存
四、信号量
4.1 什么是信号量
信号量本质上是一个计数器,用于控制同时访问共享资源的进程数量。在进程间通信中,信号量通常配合共享内存使用,用来解决多个进程同时读写共享数据的同步问题。
4.2 信号量的基本概念
| 概念 | 含义 |
|---|---|
| 信号量数组 | 可以一次性创建多个信号量,用下标区分 |
| P操作(申请) | sem_op = -1,资源数减1,不够则阻塞 |
| V操作(释放) | sem_op = +1,资源数加1,唤醒等待进程 |
| SEM_UNDO | 进程异常退出时自动归还信号量,防止死锁 |
4.3 信号量函数接口
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | ftok() |
生成IPC对象键值 |
| 2 | semget() |
创建/获取信号量数组 |
| 3 | semctl() |
初始化/控制信号量 |
| 4 | semop() |
申请(P)或释放(V)信号量 |
| 5 | semctl(IPC_RMID) |
删除信号量 |
4.4 semget : 创建信号量
c
int semget(key_t key, int nsems, int semflg);
-
key:IPC对象键值(ftok生成) -
nsems:信号量的个数(创建多个信号量) -
semflg:IPC_CREAT | 0664
c
int semid = semget(key, 2, IPC_CREAT | 0664);
if (-1 == semid) {
perror("fail to semget");
return -1;
}
注意:nsems=2 表示创建2个信号量,用下标 0 和 1 来区分不同的信号量。
4.5 semctl : 初始化/控制信号量
c
int semctl(int semid, int semnum, int cmd, ...);
-
semid:信号量ID -
semnum:要操作信号量的下标 -
cmd:SETVAL设置信号量的值,IPC_RMID删除
第四个参数需要用到 union semun 联合体:
c
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
};
c
union semun myun;
myun.val = 0; // 初始化读信号量为0
semctl(semid, 0, SETVAL, myun); // 下标0:读信号量
myun.val = 1; // 初始化写信号量为1
semctl(semid, 1, SETVAL, myun); // 下标1:写信号量
4.6 semop : 申请和释放信号量
c
int semop(int semid, struct sembuf *sops, size_t nsops);
c
struct sembuf {
unsigned short sem_num; // 信号量下标
short sem_op; // -1=申请(P),+1=释放(V)
short sem_flg; // SEM_UNDO 等
};
申请信号量(P操作):
c
struct sembuf mybuf;
mybuf.sem_num = 1; // 下标1:写信号量
mybuf.sem_op = -1; // 申请(资源数减1)
mybuf.sem_flg = SEM_UNDO; // 异常退出自动归还
semop(semid, &mybuf, 1);
释放信号量(V操作):
c
mybuf.sem_num = 0; // 下标0:读信号量
mybuf.sem_op = +1; // 释放(资源数加1)
mybuf.sem_flg = SEM_UNDO;
semop(semid, &mybuf, 1);
sem_op 为负数就是"申请"(资源-1),为正数就是"释放"(资源+1)。配合 SEM_UNDO 是标准写法,能防止进程崩溃导致信号量卡死。信号量初始值设为1时,它就是一个互斥锁;设为N时,就是控制并发数量的计数器。
4.7 删除信号量
c
semctl(semid, 0, IPC_RMID);
五、完整示例
下面用代码演示信号量的典型用法:
5.1 初始化信号量
c
key_t key = ftok("/tmp", 'a');
int semid = semget(key, 2, IPC_CREAT | 0664); // 创建2个信号量
union semun myun;
myun.val = 0; // 下标0:读信号量,初始为0
semctl(semid, 0, SETVAL, myun);
myun.val = 1; // 下标1:写信号量,初始为1
semctl(semid, 1, SETVAL, myun);
这里用2个信号量实现读写同步:
| 信号量 | 初始值 | 含义 |
|---|---|---|
| 下标0(读) | 0 | 没有数据可读,读进程先阻塞 |
| 下标1(写) | 1 | 可以写入数据 |
5.2 写进程逻辑
c
struct sembuf mybuf;
// 申请写信号量
mybuf.sem_num = 1;
mybuf.sem_op = -1;
mybuf.sem_flg = SEM_UNDO;
semop(semid, &mybuf, 1);
printf("申请到写信号量,可以写数据\n");
// 写入共享内存...
// 释放读信号量(通知读进程)
mybuf.sem_num = 0;
mybuf.sem_op = +1;
mybuf.sem_flg = SEM_UNDO;
semop(semid, &mybuf, 1);
printf("释放读信号量,通知读进程\n");
5.3 读进程逻辑
c
struct sembuf mybuf;
// 申请读信号量
mybuf.sem_num = 0;
mybuf.sem_op = -1;
mybuf.sem_flg = SEM_UNDO;
semop(semid, &mybuf, 1);
printf("申请到读信号量,可以读数据\n");
// 读取共享内存...
// 释放写信号量(通知写进程)
mybuf.sem_num = 1;
mybuf.sem_op = +1;
mybuf.sem_flg = SEM_UNDO;
semop(semid, &mybuf, 1);
printf("释放写信号量,通知写进程\n");
六、今日总结
今天学了消息队列、共享内存、信号量,核心要点如下:
消息队列:
-
ftok()生成键值,msgget()创建队列 -
msgsnd()发送消息,msgrcv()按类型接收消息 -
消息结构体第一个成员必须是
long mtype -
支持按类型接收,比管道更灵活
共享内存:
-
最快的进程间通信方式,数据不需要内核中转(零拷贝)
-
shmget()创建,shmat()映射到进程空间 -
使用完
shmdt()解除映射,shmctl(IPC_RMID)删除 -
多进程同时访问需要配合信号量同步
信号量:
-
计数器,用于控制共享资源的并发访问
-
semget()创建信号量数组,semctl()初始化(SETVAL) -
semop()执行P操作(sem_op=-1申请)和V操作(sem_op=+1释放) -
配合
SEM_UNDO防止进程崩溃导致信号量卡死
今日感悟:
进程间通信的方式各有适用场景。共享内存最快但需要信号量配合同步,消息队列虽然慢一点但有格式、按类型接收更方便。实际开发中根据需求选择,追求速度用共享内存+信号量,追求灵活性用消息队列。