【Linux】进程间通信(三):共享内存深度剖析与System V IPC机制

文章目录

    • [进程间通信(三):共享内存深度剖析与System V IPC机制](#进程间通信(三):共享内存深度剖析与System V IPC机制)
    • 一、为什么需要共享内存
      • [1.1 管道的性能瓶颈](#1.1 管道的性能瓶颈)
      • [1.2 共享内存的优势](#1.2 共享内存的优势)
    • 二、共享内存原理
      • [2.1 共享内存示意图](#2.1 共享内存示意图)
      • [2.2 共享内存的工作流程](#2.2 共享内存的工作流程)
    • [三、System V共享内存函数](#三、System V共享内存函数)
      • [3.1 共享内存数据结构](#3.1 共享内存数据结构)
      • [3.2 shmget - 创建/获取共享内存](#3.2 shmget - 创建/获取共享内存)
      • [3.3 shmat - 挂接共享内存](#3.3 shmat - 挂接共享内存)
      • [3.4 shmdt - 解除挂接](#3.4 shmdt - 解除挂接)
      • [3.5 shmctl - 控制共享内存](#3.5 shmctl - 控制共享内存)
    • 四、共享内存实战
      • [4.1 封装共享内存操作](#4.1 封装共享内存操作)
      • [4.2 Server进程 - 读取共享内存](#4.2 Server进程 - 读取共享内存)
      • [4.3 Client进程 - 写入共享内存](#4.3 Client进程 - 写入共享内存)
      • [4.4 Makefile](#4.4 Makefile)
      • [4.5 运行测试](#4.5 运行测试)
      • [4.6 查看系统中的共享内存](#4.6 查看系统中的共享内存)
    • 五、共享内存的并发问题
      • [5.1 共享内存缺乏访问控制](#5.1 共享内存缺乏访问控制)
      • [5.2 解决方案 - 借助管道实现访问控制](#5.2 解决方案 - 借助管道实现访问控制)
      • [5.3 实战:带访问控制的共享内存](#5.3 实战:带访问控制的共享内存)
      • [5.4 运行测试](#5.4 运行测试)
    • [六、System V IPC机制补充](#六、System V IPC机制补充)
      • [6.1 消息队列(了解)](#6.1 消息队列(了解))
      • [6.2 信号量(了解)](#6.2 信号量(了解))
    • 七、内核如何管理IPC资源
      • [7.1 IPC资源的特点](#7.1 IPC资源的特点)
      • [7.2 内核数据结构(参考Linux 2.6.11)](#7.2 内核数据结构(参考Linux 2.6.11))
    • 八、总结

进程间通信(三):共享内存深度剖析与System V IPC机制

💬 欢迎讨论:前两篇我们学习了管道机制,管道虽然简单易用,但性能并不是最优的。每次通信都需要经过内核缓冲区,涉及两次数据拷贝。有没有更快的IPC方式呢?答案是共享内存------最快的进程间通信方式!本篇将带你深入理解共享内存的原理、System V IPC机制,以及如何解决共享内存的并发问题。

👍 点赞、收藏与分享:这篇文章包含了共享内存的底层原理、完整代码实现、访问控制机制、以及System V IPC的管理方式,内容深入且实用,如果对你有帮助,请点赞、收藏并分享!

🚀 循序渐进:建议先掌握前两篇的管道知识,这样理解共享内存的优势会更清晰。


一、为什么需要共享内存

1.1 管道的性能瓶颈

我们先回顾一下管道的数据流向:

bash 复制代码
进程A写数据到管道:
用户空间(进程A) ──拷贝1──→ 内核缓冲区(管道)

进程B从管道读数据:
内核缓冲区(管道) ──拷贝2──→ 用户空间(进程B)

完整流程:
进程A的buf → 内核pipe缓冲区 → 进程B的buf
            ↑              ↑
          拷贝1          拷贝2

问题:

bash 复制代码
✗ 两次数据拷贝(效率低)
✗ 需要进入内核(系统调用开销)
✗ 管道容量有限(默认64KB)

场景:

假设两个进程需要共享一个100MB的视频缓冲区,用管道传输需要:

bash 复制代码
1. 进程A写100MB到管道 → 多次write,因为管道只有64KB
2. 内核拷贝100MB到管道缓冲区
3. 进程B从管道读100MB → 多次read
4. 内核拷贝100MB到进程B

总共拷贝了200MB数据!

1.2 共享内存的优势

共享内存的核心思想:让多个进程直接访问同一块物理内存!

bash 复制代码
传统方式(独立地址空间):
进程A虚拟地址  →  物理内存页A
进程B虚拟地址  →  物理内存页B
               ↑
            数据需要拷贝

共享内存方式:
进程A虚拟地址  ──┐
                 ├──→  同一块物理内存
进程B虚拟地址  ──┘
               ↑
           零拷贝!直接访问!

优势:

bash 复制代码
✓ 零拷贝 (最快的IPC方式)
✓ 不涉及内核操作 (进程直接读写内存)
✓ 容量大 (可以申请几GB)
✓ 适合大数据量传输

性能对比:

IPC方式 速度 数据拷贝次数 适用场景
管道 中等 2次 小数据流式传输
消息队列 较慢 2次 带优先级的消息
共享内存 最快 0次 大数据量传输

二、共享内存原理

2.1 共享内存示意图

bash 复制代码
进程A的地址空间              物理内存              进程B的地址空间
┌─────────────┐                                   ┌─────────────┐
│   栈        │                                   │   栈        │
├─────────────┤                                   ├─────────────┤
│             │                                   │             │
│   ↓         │                                   │   ↓         │
│             │                                   │             │
│   ...       │                                   │   ...       │
│             │                                   │             │
│   ↑         │                                   │   ↑         │
│             │                                   │             │
├─────────────┤         ┌─────────────┐           ├─────────────┤
│ 共享内存区   │────────→ │ 共享内存页    │ ←─────────│ 共享内存区   │
│ 0x7f00000   │  映射    │  (物理内存)  │   映射     │ 0x7f80000   │
├─────────────┤         └─────────────┘           ├─────────────┤
│   堆        │                                   │   堆        │
├─────────────┤                                   ├─────────────┤
│   数据段     │                                   │   数据段     │
├─────────────┤                                   ├─────────────┤
│   代码段     │                                   │   代码段     │
└─────────────┘                                   └─────────────┘

关键理解:

bash 复制代码
1. 两个进程的虚拟地址可能不同
   进程A看到的地址: 0x7f000000
   进程B看到的地址: 0x7f800000

2. 但它们映射到同一块物理内存
   物理地址: 0x12345000

3. 进程A写入数据,进程B立即可见
   A: *ptr = 100;
   B: printf("%d", *ptr);  // 输出100

2.2 共享内存的工作流程

bash 复制代码
步骤1: 创建共享内存
    shmget() → 在内核中分配一块物理内存
              返回共享内存标识符(shmid)

步骤2: 挂接到进程地址空间
    shmat() → 修改进程页表,建立虚拟地址到物理地址的映射
             返回虚拟地址指针

步骤3: 使用共享内存
    进程A: *ptr = data;     // 直接写入
    进程B: data = *ptr;     // 直接读取

步骤4: 解除挂接
    shmdt() → 从进程地址空间中解除映射
             (物理内存还在!)

步骤5: 删除共享内存
    shmctl(IPC_RMID) → 释放物理内存

完整流程图:

bash 复制代码
进程A                        内核                        进程B
  │                           │                           │
  ├─ shmget(key, size)       │                           │
  │         │                 │                           │
  │         └────────→ 创建共享内存对象                  │
  │                    分配物理内存                      │
  │                    返回shmid                         │
  │                           │                           │
  ├─ shmat(shmid)            │                           │
  │         │                 │                           │
  │         └────────→ 映射到进程A地址空间               │
  │                    返回虚拟地址ptr_a                 │
  │                           │                           │
  │                           │                  shmget(key) ←┤
  │                           │                           │  │
  │                    获取已存在的shmid ←────────────────┘  │
  │                    返回shmid                         │
  │                           │                           │
  │                           │                  shmat(shmid) ←┤
  │                           │                           │  │
  │                    映射到进程B地址空间 ←────────────────┘  │
  │                    返回虚拟地址ptr_b                 │
  │                           │                           │
  ├─ *ptr_a = 100            │                           │
  │         │                 │                           │
  │         └────────→ 写入物理内存 ────────────────────→ │
  │                           │                           │
  │                           │                  *ptr_b ←┤
  │                           │                  读到100  │
  │                           │                           │
  ├─ shmdt(ptr_a)            │                           │
  │                           │                           │
  │                           │                  shmdt(ptr_b) ←┤
  │                           │                           │
  │                           │                           │
  ├─ shmctl(IPC_RMID)        │                           │
  │         │                 │                           │
  │         └────────→ 删除共享内存                      │
  │                    释放物理内存                      │

三、System V共享内存函数

3.1 共享内存数据结构

内核用shmid_ds结构管理每一块共享内存:

c 复制代码
struct shmid_ds {
    struct ipc_perm shm_perm;      // 权限信息
    int shm_segsz;                 // 共享内存大小(字节)
    __kernel_time_t shm_atime;     // 最后一次attach时间
    __kernel_time_t shm_dtime;     // 最后一次detach时间
    __kernel_time_t shm_ctime;     // 最后一次改变时间
    __kernel_ipc_pid_t shm_cpid;   // 创建者PID
    __kernel_ipc_pid_t shm_lpid;   // 最后操作者PID
    unsigned short shm_nattch;     // 当前attach的进程数
    unsigned short shm_unused;
    void *shm_unused2;
    void *shm_unused3;
};

3.2 shmget - 创建/获取共享内存

函数原型:

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

// 参数:
//   key: 共享内存的键值(名字)
//   size: 共享内存大小(字节)
//   shmflg: 标志位
//
// 返回值:
//   成功: 共享内存标识符(shmid)
//   失败: -1

关键参数说明:

1. key的作用

bash 复制代码
key就像是共享内存的"名字"
不同进程通过相同的key找到同一块共享内存

进程A: shmget(0x1234, 4096, ...)
进程B: shmget(0x1234, 4096, ...)
      ↑
   相同的key → 访问同一块共享内存

如何生成key? 使用ftok()函数:

c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);

// pathname: 一个存在的文件路径
// proj_id: 项目ID(1-255)
//
// 返回: 生成的key值

// 示例
key_t key = ftok("/home/user", 0x66);
// 只要pathname和proj_id相同,返回的key就相同

2. shmflg标志位

标志 含义
IPC_CREAT 如果不存在则创建,存在则获取
IPC_EXCL 与IPC_CREAT配合,存在则报错
0666 权限(类似文件权限)

常用组合:

c 复制代码
// 组合1: 创建或获取
int shmid = shmget(key, size, IPC_CREAT | 0666);
// 不存在→创建
// 已存在→获取

// 组合2: 必须创建新的
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
// 不存在→创建
// 已存在→返回-1, errno=EEXIST

// 组合3: 只获取
int shmid = shmget(key, 0, 0);
// 存在→获取
// 不存在→返回-1

3.3 shmat - 挂接共享内存

函数原型:

c 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

// 参数:
//   shmid: 共享内存标识符
//   shmaddr: 指定挂接地址(通常传NULL,让内核自动选择)
//   shmflg: 标志位
//
// 返回值:
//   成功: 共享内存的虚拟地址
//   失败: (void *)-1

shmaddr参数说明:

c 复制代码
// 情况1: shmaddr = NULL (推荐)
void *addr = shmat(shmid, NULL, 0);
// 内核自动选择合适的地址

// 情况2: shmaddr != NULL 且 shmflg无SHM_RND
void *addr = shmat(shmid, (void *)0x50000000, 0);
// 精确挂接到0x50000000
// 如果该地址不可用,返回失败

// 情况3: shmaddr != NULL 且 shmflg有SHM_RND
void *addr = shmat(shmid, (void *)0x50001234, SHM_RND);
// 向下对齐到SHMLBA的整数倍
// 实际地址 = 0x50001234 - (0x50001234 % SHMLBA)

shmflg标志:

c 复制代码
// SHM_RDONLY: 只读挂接
void *addr = shmat(shmid, NULL, SHM_RDONLY);
// 只能读,不能写

// 默认(0): 可读可写
void *addr = shmat(shmid, NULL, 0);

3.4 shmdt - 解除挂接

函数原型:

c 复制代码
int shmdt(const void *shmaddr);

// 参数:
//   shmaddr: shmat返回的地址
//
// 返回值:
//   成功: 0
//   失败: -1

重要提示:

bash 复制代码
shmdt只是解除挂接,不会删除共享内存!

进程A: shmat() → shmdt()
进程B: shmat() → 仍然可以访问

只有调用shmctl(IPC_RMID)才会真正删除

3.5 shmctl - 控制共享内存

函数原型:

c 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

// 参数:
//   shmid: 共享内存标识符
//   cmd: 操作命令
//   buf: 指向shmid_ds结构的指针
//
// 返回值:
//   成功: 0(或其他值,取决于cmd)
//   失败: -1

常用cmd:

c 复制代码
// IPC_STAT: 获取共享内存信息
struct shmid_ds info;
shmctl(shmid, IPC_STAT, &info);
printf("大小: %d\n", info.shm_segsz);
printf("attach数: %d\n", info.shm_nattch);

// IPC_SET: 设置共享内存属性
struct shmid_ds new_info;
new_info.shm_perm.mode = 0644;  // 修改权限
shmctl(shmid, IPC_SET, &new_info);

// IPC_RMID: 删除共享内存(最常用)
shmctl(shmid, IPC_RMID, NULL);

IPC_RMID的特殊行为:

bash 复制代码
标记删除,而非立即删除!

时间线:
t1: shmctl(shmid, IPC_RMID, NULL)  ← 标记为删除
    此时如果有进程已attach,共享内存不会立即释放

t2: 进程A继续使用共享内存  ← 仍然可以访问

t3: 进程A调用shmdt()  ← 解除挂接

t4: 最后一个进程shmdt()后,共享内存才真正释放

四、共享内存实战

4.1 封装共享内存操作

comm.h - 头文件

c 复制代码
#ifndef _COMM_H_
#define _COMM_H_

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATHNAME "/tmp"      // ftok的路径
#define PROJ_ID 0x6666      // 项目ID

// 创建共享内存
int createShm(int size);

// 获取已存在的共享内存
int getShm(int size);

// 删除共享内存
int destroyShm(int shmid);

#endif

comm.c - 实现

c 复制代码
#include "comm.h"

// 通用函数:创建或获取共享内存
static int commShm(int size, int flags) {
    // 1. 生成key
    key_t key = ftok(PATHNAME, PROJ_ID);
    if (key < 0) {
        perror("ftok");
        return -1;
    }
    
    // 2. 创建/获取共享内存
    int shmid = shmget(key, size, flags);
    if (shmid < 0) {
        perror("shmget");
        return -2;
    }
    
    return shmid;
}

// 创建新的共享内存
int createShm(int size) {
    return commShm(size, IPC_CREAT | IPC_EXCL | 0666);
}

// 获取已存在的共享内存
int getShm(int size) {
    return commShm(size, IPC_CREAT);
}

// 删除共享内存
int destroyShm(int shmid) {
    if (shmctl(shmid, IPC_RMID, NULL) < 0) {
        perror("shmctl");
        return -1;
    }
    return 0;
}

4.2 Server进程 - 读取共享内存

c 复制代码
// server.c
#include "comm.h"
#include <unistd.h>
#include <string.h>

int main() {
    // 1. 创建共享内存(4096字节)
    int shmid = createShm(4096);
    if (shmid < 0) {
        return 1;
    }
    printf("Server: 创建共享内存成功, shmid=%d\n", shmid);
    
    // 2. 挂接共享内存
    char *shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) {
        perror("shmat");
        return 1;
    }
    printf("Server: 挂接共享内存成功, addr=%p\n", shmaddr);
    
    // 3. 使用共享内存
    sleep(2);  // 等待Client连接
    
    int i = 0;
    while (i++ < 26) {
        printf("Client写入: %s\n", shmaddr);
        sleep(1);
    }
    
    // 4. 解除挂接
    shmdt(shmaddr);
    sleep(2);
    
    // 5. 删除共享内存
    destroyShm(shmid);
    printf("Server: 删除共享内存\n");
    
    return 0;
}

4.3 Client进程 - 写入共享内存

c 复制代码
// client.c
#include "comm.h"
#include <unistd.h>
#include <string.h>

int main() {
    // 1. 获取共享内存
    int shmid = getShm(4096);
    if (shmid < 0) {
        return 1;
    }
    printf("Client: 获取共享内存成功, shmid=%d\n", shmid);
    
    sleep(1);
    
    // 2. 挂接共享内存
    char *shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) {
        perror("shmat");
        return 1;
    }
    printf("Client: 挂接共享内存成功, addr=%p\n", shmaddr);
    
    sleep(2);
    
    // 3. 写入数据
    int i = 0;
    while (i < 26) {
        shmaddr[i] = 'A' + i;
        i++;
        shmaddr[i] = '\0';  // 字符串结束符
        sleep(1);
    }
    
    // 4. 解除挂接
    shmdt(shmaddr);
    sleep(2);
    
    return 0;
}

4.4 Makefile

makefile 复制代码
.PHONY:all
all:server client

server:server.c comm.c
	gcc -o $@ $^

client:client.c comm.c
	gcc -o $@ $^

.PHONY:clean
clean:
	rm -f server client

4.5 运行测试

bash 复制代码
# 编译
$ make
gcc -o server server.c comm.c
gcc -o client client.c comm.c

# 终端1: 启动Server
$ ./server
Server: 创建共享内存成功, shmid=32768
Server: 挂接共享内存成功, addr=0x7f1234000000
Client写入: A
Client写入: AB
Client写入: ABC
...
Client写入: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Server: 删除共享内存

# 终端2: 启动Client
$ ./client
Client: 获取共享内存成功, shmid=32768
Client: 挂接共享内存成功, addr=0x7f5678000000
(Client写入数据...)

注意观察:

bash 复制代码
1. Server和Client的shmid相同(32768)
   → 说明它们访问同一块共享内存

2. Server和Client的虚拟地址不同
   Server: 0x7f1234000000
   Client: 0x7f5678000000
   → 但映射到同一块物理内存

3. Client写入后,Server立即能读到
   → 零拷贝,直接内存访问

4.6 查看系统中的共享内存

ipcs命令:

bash 复制代码
# 查看所有IPC资源
$ ipcs

# 只查看共享内存
$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner  perms  bytes  nattch status
0x66007f00 32768      user   666    4096   2

# 字段说明:
# key: 共享内存的键值
# shmid: 共享内存标识符
# owner: 所有者
# perms: 权限
# bytes: 大小
# nattch: 当前attach的进程数

ipcrm命令 - 删除IPC资源:

bash 复制代码
# 删除共享内存
$ ipcrm -m 32768

# 或者通过key删除
$ ipcrm -M 0x66007f00

注意事项:

bash 复制代码
如果程序异常退出,共享内存可能没有被删除

$ ./server
^C  ← Ctrl+C强制退出

$ ipcs -m
... 共享内存还在 ...

需要手动删除:
$ ipcrm -m 32768

五、共享内存的并发问题

5.1 共享内存缺乏访问控制

问题场景:

bash 复制代码
进程A正在写数据:
  shmaddr[0] = 'H';
  shmaddr[1] = 'e';
  shmaddr[2] = 'l';  ← 写到一半...

进程B开始读取:
  printf("%s", shmaddr);  ← 读到"Hel"(不完整!)

根本原因:

c 复制代码
共享内存只负责"共享",不负责"同步"!

管道自带同步:
  write() → read()  顺序执行

共享内存没有同步:
  进程A写 ---|
             |--- 可能同时发生!
  进程B读 ---|

5.2 解决方案 - 借助管道实现访问控制

思路:用管道作为"信号量"

bash 复制代码
规则:
1. 进程B先阻塞在管道read上
2. 进程A写完数据后,往管道写一个字节
3. 进程B收到信号,开始读取共享内存

完整架构:

bash 复制代码
进程A(writer)              管道              进程B(reader)
      │                     │                     │
      ├─ 写共享内存         │                     │
      ├─ write(pipe, "GO") ──→                   │
      │                     │                     ├─ read(pipe)阻塞
      │                     │                     ├─ 收到信号
      │                     │                     ├─ 读共享内存
      │                     │                     ├─ 处理数据

5.3 实战:带访问控制的共享内存

Comm.hpp - 封装管道和共享内存操作

cpp 复制代码
#pragma once

#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cassert>
#include <iostream>
#include <cstring>

#define PATH_NAME "/home/user"
#define PROJ_ID 0x66
#define SHM_SIZE 4096
#define FIFO_NAME "./fifo"

#define READ O_RDONLY
#define WRITE O_WRONLY

// 打开FIFO
int OpenFIFO(const char *pathname, int flags) {
    int fd = open(pathname, flags);
    assert(fd >= 0);
    return fd;
}

void CloseFifo(int fd) {
    close(fd);
}

// 等待信号(读管道)
void Wait(int fd) {
    std::cout << "[等待中...]\n";
    uint32_t temp = 0;
    ssize_t s = read(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
}

// 发送信号(写管道)
void Signal(int fd) {
    uint32_t temp = 1;
    ssize_t s = write(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    std::cout << "[唤醒对方...]\n";
}

// 初始化类:创建FIFO
class Init {
public:
    Init() {
        umask(0);
        int n = mkfifo(FIFO_NAME, 0666);
        assert(n == 0);
        std::cout << "[创建FIFO成功]\n";
    }
    ~Init() {
        unlink(FIFO_NAME);
        std::cout << "[删除FIFO成功]\n";
    }
};

ShmServer.cc - Server端(读取共享内存)

cpp 复制代码
#include "Comm.hpp"

Init init;  // 创建FIFO

int main() {
    // 1. 创建key
    key_t k = ftok(PATH_NAME, PROJ_ID);
    assert(k != -1);
    std::cout << "Server创建key: 0x" << std::hex << k << std::dec << "\n";
    
    // 2. 创建共享内存
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }
    std::cout << "Server创建共享内存, shmid=" << shmid << "\n";
    
    // 3. 挂接共享内存
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    assert(shmaddr != (void *)-1);
    std::cout << "Server挂接共享内存, addr=" << (void *)shmaddr << "\n";
    
    // 4. 打开FIFO(读端)
    int fd = OpenFIFO(FIFO_NAME, READ);
    
    // 5. 循环读取
    while (true) {
        // 等待Client写入
        Wait(fd);
        
        // 临界区:读取共享内存
        printf("Client写入: %s\n", shmaddr);
        
        // 检查退出条件
        if (strcmp(shmaddr, "quit") == 0) {
            break;
        }
    }
    
    CloseFifo(fd);
    
    // 6. 解除挂接
    int n = shmdt(shmaddr);
    assert(n != -1);
    std::cout << "Server解除挂接\n";
    
    // 7. 删除共享内存
    n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    std::cout << "Server删除共享内存\n";
    
    return 0;
}

ShmClient.cc - Client端(写入共享内存)

cpp 复制代码
#include "Comm.hpp"

int main() {
    // 1. 创建key
    key_t k = ftok(PATH_NAME, PROJ_ID);
    if (k < 0) {
        std::cout << "Client创建key失败\n";
        exit(1);
    }
    std::cout << "Client创建key: 0x" << std::hex << k << std::dec << "\n";
    
    // 2. 获取共享内存
    int shmid = shmget(k, SHM_SIZE, 0);
    if (shmid < 0) {
        std::cout << "Client获取共享内存失败\n";
        exit(2);
    }
    std::cout << "Client获取共享内存, shmid=" << shmid << "\n";
    
    // 3. 挂接共享内存
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    if (shmaddr == nullptr) {
        std::cout << "Client挂接失败\n";
        exit(3);
    }
    std::cout << "Client挂接共享内存, addr=" << (void *)shmaddr << "\n";
    
    // 4. 打开FIFO(写端)
    int fd = OpenFIFO(FIFO_NAME, WRITE);
    
    // 5. 循环写入
    while (true) {
        // 从标准输入读取
        ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
        if (s > 0) {
            shmaddr[s - 1] = '\0';  // 去掉换行符
            
            // 通知Server
            Signal(fd);
            
            if (strcmp(shmaddr, "quit") == 0) {
                break;
            }
        }
    }
    
    CloseFifo(fd);
    
    // 6. 解除挂接
    int n = shmdt(shmaddr);
    assert(n != -1);
    std::cout << "Client解除挂接\n";
    
    return 0;
}

Makefile

makefile 复制代码
.PHONY:all
all:ShmServer ShmClient

ShmServer:ShmServer.cc
	g++ -o $@ $^ -std=c++11

ShmClient:ShmClient.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f ShmServer ShmClient fifo

5.4 运行测试

bash 复制代码
# 编译
$ make
g++ -o ShmServer ShmServer.cc -std=c++11
g++ -o ShmClient ShmClient.cc -std=c++11

# 终端1: 启动Server
$ ./ShmServer
[创建FIFO成功]
Server创建key: 0x66001f48
Server创建共享内存, shmid=32769
Server挂接共享内存, addr=0x7f8a12345000
[等待中...]

# 终端2: 启动Client
$ ./ShmClient
Client创建key: 0x66001f48
Client获取共享内存, shmid=32769
Client挂接共享内存, addr=0x7f9b23456000
hello          ← 输入

# 终端1显示:
[唤醒对方...]
Client写入: hello
[等待中...]

# 终端2继续输入:
world

# 终端1显示:
Client写入: world
[等待中...]

# 终端2输入quit退出:
quit

# 终端1显示:
Client写入: quit
Server解除挂接
Server删除共享内存
[删除FIFO成功]

流程分析:

bash 复制代码
时间轴:
t1: Server创建FIFO和共享内存
t2: Server打开FIFO读端,阻塞在read
t3: Client连接FIFO写端,挂接共享内存
t4: Client写"hello"到共享内存
t5: Client发送信号(write 4字节到FIFO)
t6: Server收到信号,解除阻塞
t7: Server读取共享内存,打印"hello"
t8: Server再次阻塞在read
... 循环 ...

六、System V IPC机制补充

6.1 消息队列(了解)

特点:

bash 复制代码
✓ 消息有类型和优先级
✓ 可以按类型接收消息
✓ 消息有边界(不像管道是字节流)
✗ 性能比共享内存差(需要拷贝)

基本函数:

c 复制代码
// 创建消息队列
int msgget(key_t key, int msgflg);

// 发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

// 接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

// 控制消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

使用场景:

bash 复制代码
适合:
- 带优先级的任务调度
- 需要消息分类的系统

不适合:
- 大数据量传输(用共享内存)
- 高频率通信(用管道或共享内存)

6.2 信号量(了解)

并发编程基础概念:

bash 复制代码
临界资源:
  多个进程共享的资源(如共享内存、文件等)

临界区:
  访问临界资源的代码段

互斥:
  任何时刻只允许一个进程访问临界资源

同步:
  多个进程访问临界资源具有一定的顺序性

信号量的本质:

bash 复制代码
信号量 = 一个计数器

初始值表示资源数量:
sem = 3  → 有3个资源可用

P操作(申请资源):
sem--
if (sem < 0) 阻塞;

V操作(释放资源):
sem++
if (有进程在等待) 唤醒一个;

生活类比:电影院订票

bash 复制代码
电影院有100个座位 → sem = 100

观众A: P操作 → sem=99, 进入
观众B: P操作 → sem=98, 进入
...
观众100: P操作 → sem=0, 进入
观众101: P操作 → sem=-1, 阻塞(没票了)

观众A离开: V操作 → sem=0, 唤醒观众101
观众101: 进入

基本函数:

c 复制代码
// 创建信号量集
int semget(key_t key, int nsems, int semflg);

// P/V操作
int semop(int semid, struct sembuf *sops, size_t nsops);

// 控制信号量
int semctl(int semid, int semnum, int cmd, ...);

使用场景:

bash 复制代码
适合:
- 进程同步
- 资源计数
- 访问控制

实际开发中:
- System V信号量较复杂
- 推荐使用POSIX信号量
- 或者用互斥锁(pthread_mutex)

七、内核如何管理IPC资源

7.1 IPC资源的特点

bash 复制代码
System V IPC资源的生命周期:
✗ 不随进程 (进程退出,IPC资源不会自动释放)
✓ 随内核 (系统重启才释放)

这就是为什么需要手动删除:
shmctl(shmid, IPC_RMID, NULL);
msgctl(msgid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);

7.2 内核数据结构(参考Linux 2.6.11)

所有IPC资源都有一个共同的基类:

c 复制代码
struct kern_ipc_perm {
    key_t key;           // IPC键值
    uid_t uid;           // 所有者UID
    gid_t gid;           // 所有者GID
    uid_t cuid;          // 创建者UID
    gid_t cgid;          // 创建者GID
    mode_t mode;         // 权限
    unsigned long seq;   // 序列号
};

共享内存结构:

c 复制代码
struct shmid_kernel {
    struct kern_ipc_perm shm_perm;  // 基类
    struct file *shm_file;          // 关联的文件
    unsigned long shm_nattch;       // attach次数
    unsigned long shm_segsz;        // 大小
    time_t shm_atim;                // attach时间
    time_t shm_dtim;                // detach时间
    time_t shm_ctim;                // change时间
    pid_t shm_cprid;                // 创建者PID
    pid_t shm_lprid;                // 最后操作者PID
    // ...
};

这就是C语言实现多态的方式!

bash 复制代码
                kern_ipc_perm (基类)
                       │
        ┌──────────────┼──────────────┐
        │              │              │
  shmid_kernel   msqid_kernel   semid_kernel
  (共享内存)      (消息队列)      (信号量)

八、总结

本篇我们深入学习了共享内存和System V IPC机制。

核心知识点:

  1. 共享内存原理

    • 多个进程映射到同一块物理内存
    • 零拷贝,最快的IPC方式
    • 适合大数据量传输
  2. 共享内存函数族

    • shmget: 创建/获取
    • shmat: 挂接到进程地址空间
    • shmdt: 解除挂接
    • shmctl: 控制(删除)
  3. 共享内存的并发问题

    • 缺乏访问控制
    • 需要额外的同步机制
    • 可以用管道、信号量等实现
  4. System V IPC特点

    • 生命周期随内核
    • 需要手动删除
    • 用key标识资源
  5. IPC性能对比

IPC方式 速度 复杂度 适用场景
管道 中等 简单 小数据流式传输
命名管道 中等 简单 无亲缘关系进程
消息队列 较慢 中等 消息分类
共享内存 最快 需配合同步 大数据传输
信号量 - 复杂 进程同步

共享内存完整流程图:

bash 复制代码
进程A                        进程B
  │                           │
  ├─ shmget(key, size)       │
  ├─ shmat(shmid)            │
  │                           ├─ shmget(key, size)
  │                           ├─ shmat(shmid)
  │                           │
  ├─ Wait(pipe)阻塞          ├─ 写共享内存
  │                           ├─ Signal(pipe)唤醒
  ├─ 读共享内存              │
  │                           │
  ├─ shmdt()                 ├─ shmdt()
  ├─ shmctl(IPC_RMID)        │

💡 思考题

  1. 为什么共享内存需要配合同步机制?

  2. 如果多个进程同时写共享内存会发生什么?

  3. 除了管道,还能用什么方式实现共享内存的访问控制?
    📚 扩展学习

  4. POSIX共享内存(shm_open)

  5. 内存映射文件(mmap)

  6. POSIX信号量(sem_open)

  7. 多线程中的互斥锁和条件变量

至此,进程间通信系列完结!

相关推荐
傅泽塔2 小时前
类和对象(上)
c++
不怕犯错,就怕不做2 小时前
Linux内核默认允许多个进程打开同一字符设备
linux·驱动开发·嵌入式硬件
mjhcsp2 小时前
莫比乌斯反演总结
c++·算法
阿班d2 小时前
55555555
c++
Source.Liu2 小时前
【Ubuntu】关机重启命令
linux·运维·ubuntu
iCode5043 小时前
CentOS Stream 9修改静态IP
linux·tcp/ip·centos
不怕犯错,就怕不做3 小时前
RK3562+RK817在关机状态下提升充电电流至2A解决方案
linux·驱动开发·嵌入式硬件
stolentime3 小时前
P14978 [USACO26JAN1] Mooclear Reactor S题解
数据结构·c++·算法·扫描线·usaco
CSDN_RTKLIB3 小时前
C++多元谓词
c++·算法·stl