System V 进程通信详解:消息队列、信号量与共享内存

# 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程序员必备的工具箱。


请使用浏览器的分享功能分享到微信等