# System V 进程通信详解:消息队列、信号量与共享内存
在Linux/Unix系统中,进程间通信(IPC)是一个经久不衰的话题。除了基于管道的通信方式,System V IPC提供了一套功能更强大的进程间通信机制,包括消息队列、信号量和共享内存。这些机制在设计上有很多共通之处,都使用类似的接口和概念。本文将深入解析System V消息队列和信号量的核心原理与实战用法。
## 一、System V IPC的共同特征
System V IPC的三个组件(消息队列、信号量、共享内存)有一些共同的设计特征:
1. **内核持久性**:IPC对象一旦创建,就一直存在于内核中,直到被显式删除或系统重启
2. **标识符与键值**:每个IPC对象有一个唯一的标识符(ID),同时可以通过键值(key)来引用
3. **权限控制**:类似于文件权限,IPC对象也有所有者、所属组和权限位
4. **命令行工具**:提供`ipcs`和`ipcrm`等工具查看和管理IPC对象
```bash
# 查看当前系统中的IPC对象
ipcs -a
# 查看消息队列
ipcs -q
# 删除IPC对象
ipcrm -q
ipcrm -s
ipcrm -m
```
## 二、消息队列
消息队列是内核中一个链表结构,允许进程以消息为单位进行数据交换。每个消息包含类型和数据两部分,可以按类型独立读取,这比流式数据(如管道)更灵活。
### 2.1 创建或获取消息队列
```c
#include
#include
#include
int msgget(key_t key, int msgflg);
```
- `key`:IPC键值,可以通过`ftok`生成,或使用`IPC_PRIVATE`创建私有队列
- `msgflg`:权限标志,如`0666 | IPC_CREAT`
### 2.2 发送消息
```c
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
```
消息结构必须至少包含一个`long`类型的成员作为消息类型:
```c
struct msgbuf {
long mtype; // 消息类型(必须 > 0)
char mtext[1024]; // 消息数据
};
```
### 2.3 接收消息
```c
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
```
`msgtyp`参数控制接收行为:
- `msgtyp == 0`:接收队列中的第一个消息
- `msgtyp > 0`:接收类型等于`msgtyp`的第一个消息
- `msgtyp < 0`:接收类型小于等于`|msgtyp|`的最小类型消息
### 2.4 实战:客户端-服务端通信
**服务端代码(接收消息)**:
```c
#include
#include
#include
#include
#include
#include
#define MSG_KEY 1234
#define MSG_SIZE 256
struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
};
int main() {
int msgid;
struct msgbuf message;
// 创建消息队列
msgid = msgget(MSG_KEY, 0666 | IPC_CREAT);
if (msgid == -1) {
perror("msgget");
exit(1);
}
printf("服务端启动,等待消息...\n");
while (1) {
// 接收类型为1的消息
if (msgrcv(msgid, &message, MSG_SIZE, 1, 0) == -1) {
perror("msgrcv");
break;
}
printf("收到消息: %s", message.mtext);
// 如果收到"exit",退出循环
if (strncmp(message.mtext, "exit", 4) == 0)
break;
}
// 删除消息队列
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
```
**客户端代码(发送消息)**:
```c
#include
#include
#include
#include
#include
#define MSG_KEY 1234
#define MSG_SIZE 256
struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
};
int main() {
int msgid;
struct msgbuf message;
// 获取消息队列
msgid = msgget(MSG_KEY, 0666);
if (msgid == -1) {
perror("msgget");
exit(1);
}
<"w9.s6k3.org.cn"><"g2.s6k3.org.cn"><"q5.s6k3.org.cn">
message.mtype = 1; // 消息类型
printf("客户端启动,输入消息(输入exit退出):\n");
while (fgets(message.mtext, MSG_SIZE, stdin) != NULL) {
if (msgsnd(msgid, &message, strlen(message.mtext) + 1, 0) == -1) {
perror("msgsnd");
break;
}
if (strncmp(message.mtext, "exit", 4) == 0)
break;
}
return 0;
}
```
编译运行:
```bash
gcc server.c -o server
gcc client.c -o client
./server & # 后台运行服务端
./client # 运行客户端
```
## 三、信号量
信号量是一种用于进程同步的计数器,控制多个进程对共享资源的访问。System V信号量是以集合为单位管理的,可以同时操作多个信号量。
### 3.1 创建或获取信号量集合
```c
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
```
- `nsems`:信号量集合中的信号量数量
- `semflg`:权限标志
### 3.2 信号量操作
```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; // 标志:IPC_NOWAIT, SEM_UNDO
};
```
### 3.3 信号量控制
```c
int semctl(int semid, int semnum, int cmd, ...);
```
常用命令:
- `SETVAL`:设置信号量值
- `GETVAL`:获取信号量值
- `IPC_RMID`:删除信号量集合
### 3.4 实战:使用信号量保护共享资源
以下示例演示两个进程通过信号量协调对共享文件的访问:
```c
#include
#include
#include
#include
#include
#include
#include
// 联合体用于semctl
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
// P操作:申请资源
void sem_p(int semid) {
struct sembuf sb = {0, -1, 0};
if (semop(semid, &sb, 1) == -1) {
perror("semop P");
exit(1);
}
}
// V操作:释放资源
void sem_v(int semid) {
struct sembuf sb = {0, 1, 0};
if (semop(semid, &sb, 1) == -1) {
perror("semop V");
exit(1);
}
}
int main() {
int semid;
union semun su;
// 创建包含1个信号量的集合
semid = semget(IPC_PRIVATE, 1, 0666);
if (semid == -1) {
perror("semget");
exit(1);
}
// 初始化信号量值为1
su.val = 1;
if (semctl(semid, 0, SETVAL, su) == -1) {
perror("semctl SETVAL");
exit(1);
}
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
}
if (pid == 0) {
// 子进程
for (int i = 0; i < 5; i++) {
sem_p(semid);
printf("子进程进入临界区 (i=%d)\n", i);
sleep(1); // 模拟工作
printf("子进程离开临界区\n");
sem_v(semid);
sleep(rand() % 2);
}
} else {
// 父进程
for (int i = 0; i < 5; i++) {
sem_p(semid);
printf("父进程进入临界区 (i=%d)\n", i);
sleep(1);
printf("父进程离开临界区\n");
sem_v(semid);
sleep(rand() % 2);
}
<"t1.s6k3.org.cn"><"i8.s6k3.org.cn"><"o0.s6k3.org.cn">
wait(NULL);
// 删除信号量集合
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl IPC_RMID");
exit(1);
}
}
return 0;
}
```
运行结果会显示父进程和子进程交替进入临界区,永远不会同时访问。
## 四、信号量的进阶用法
### 4.1 多资源管理
信号量集合可以管理多个资源。例如,实现一个读者-写者锁:
```c
// 信号量集合布局
// sem[0]: 读计数器互斥
// sem[1]: 写互斥
// sem[2]: 读计数(初始为0,用信号量值模拟)
```
### 4.2 使用SEM_UNDO避免死锁
`sembuf`中的`SEM_UNDO`标志让内核在进程意外终止时自动撤销信号量操作,避免资源泄漏:
```c
struct sembuf sb = {0, -1, SEM_UNDO}; // P操作带UNDO
```
## 五、System V IPC vs POSIX IPC
| 特性 | System V IPC | POSIX IPC |
|------|--------------|-----------|
| 接口风格 | 老派,函数多 | 现代,更简洁 |
| 对象命名 | 整数键值 | 名字字符串 |
| 内核持久性 | 是 | 是(除信号量)|
| 信号量操作 | 支持原子多操作 | 只支持单个 |
| 可移植性 | 所有Unix | Linux/BSD |
## 六、总结
System V消息队列和信号量提供了强大的进程间通信机制:
- **消息队列**:以类型化的消息为单位,支持按类型读取,适合客户端-服务端模型
- **信号量**:用于进程同步,支持原子多操作和SEM_UNDO等高级特性
掌握这些机制,可以在复杂的多进程场景下实现高效、可靠的通信与同步。但使用时需注意:
1. 显式删除IPC对象,避免资源泄漏
2. 合理设计权限,保证安全性
3. 考虑使用`ftok`生成唯一键值,避免冲突
无论是构建分布式系统、实现进程池,还是协调共享资源访问,System V IPC都是Linux程序员必备的工具箱。