在操作系统中,进程是独立的执行实体,拥有自己的独立地址空间,无法直接访问其他进程的内存。然而,在多进程程序中,有时需要多个进程之间共享数据,传统的文件、管道等通信方式往往伴随较大的系统开销。共享内存(Shared Memory)是进程间通信(IPC)中最高效的一种方式,允许多个进程直接共享一块物理内存空间,从而极大地提高数据传递的速度。


一、共享内存的基本概念

共享内存是一种将特定的物理内存片段映射到多个进程虚拟地址空间的 IPC 机制。不同于管道或消息队列等需通过系统内核复制数据的方式,进程通过共享内存可以直接访问同一块内存,不需要通过内核中转,因此读写效率极高。

然而,共享内存本身并不提供同步机制,也就是说,当多个进程并发访问共享内存时,可能会产生数据竞争问题,因此需要借助其他同步机制(如信号量、互斥锁)来确保数据一致性。

1.1 共享内存的优势

  1. 速度快:由于进程可以直接读写同一块内存,因此不需要进行数据复制。
  2. 高效:适用于需要频繁、大量传递数据的场景。
  3. 简单的内存模型:共享内存只需一次分配,多个进程就可以同时访问相同的数据。

1.2 共享内存的不足

  1. 无同步机制:共享内存没有自带的锁机制,需要额外的同步工具来防止数据竞争。
  2. 需要手动管理:进程退出后,必须显式释放共享内存,避免内存泄漏。
  3. 内存受限:共享内存的大小通常受系统参数(如 SHMMAX)限制,需合理分配和使用。

二、Linux 中的共享内存系统调用

在 Linux 中,使用共享内存需要借助一组系统调用,这些系统调用提供了从创建、映射到销毁共享内存的完整流程。

2.1 shmget: 创建或获取共享内存段

shmget 函数用于创建或获取共享内存段。它返回一个共享内存标识符(shmid),该标识符将用于后续的操作。

1
int shmget(key_t key, size_t size, int shmflg);
  • key:标识共享内存的键值。多个进程通过相同的键值来访问同一块共享内存。
  • size:共享内存的大小(以字节为单位)。
  • shmflg:权限和创建标志,常用 0666 | IPC_CREAT,表示可读写并创建共享内存(若不存在)。

返回值:成功时返回共享内存段的 ID(shmid),失败时返回 -1

2.2 shmat: 将共享内存连接到进程地址空间

shmat 函数用于将共享内存段连接到当前进程的地址空间中,成功后返回一个指向共享内存的指针,进程可以通过该指针读写共享内存。

1
void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid:由 shmget 返回的共享内存段标识符。
  • shmaddr:指定共享内存连接到进程地址空间的地址,通常设为 nullptr,由操作系统自动选择合适的地址。
  • shmflg:连接标志,常用 0

返回值:成功时返回共享内存的指针,失败时返回 (void*)-1

2.3 shmdt: 从进程地址空间分离共享内存

shmdt 函数用于将共享内存段从当前进程的地址空间分离。

1
int shmdt(const void *shmaddr);
  • shmaddr:共享内存的起始地址,由 shmat 返回。

返回值:成功时返回 0,失败时返回 -1

2.4 shmctl: 控制共享内存段

shmctl 函数用于控制共享内存段的行为,最常见的用法是删除共享内存段,释放系统资源。

1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid:共享内存标识符。
  • cmd:控制命令,常用 IPC_RMID 删除共享内存段。
  • buf:与共享内存段相关的信息结构体,用于设置或读取共享内存段的状态。

返回值:成功时返回 0,失败时返回 -1


三、共享内存示例:进程间数据共享

接下来通过一个简单的示例程序演示如何使用共享内存实现进程间通信。一个进程写入数据到共享内存,另一个进程读取这些数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstring>
#include <cstdlib>

// 定义共享内存结构体
struct SharedData {
    int num;
    char message[100];
};

int main() {
    // 1. 创建共享内存段
    int shmid = shmget(0x1234, sizeof(SharedData), 0666 | IPC_CREAT);
    if (shmid == -1) {
        std::cerr << "Failed to create shared memory." << std::endl;
        return 1;
    }

    // 2. 将共享内存连接到当前进程的地址空间
    SharedData *data = static_cast<SharedData*>(shmat(shmid, nullptr, 0));
    if (data == (void*)-1) {
        std::cerr << "Failed to attach shared memory." << std::endl;
        return 1;
    }

    // 3. 写入数据到共享内存
    data->num = 42;
    std::strcpy(data->message, "Hello from shared memory!");

    std::cout << "Data written to shared memory: num = " << data->num
              << ", message = " << data->message << std::endl;

    // 4. 分离共享内存段
    shmdt(data);

    return 0;
}

上述代码说明如下:

  1. 创建共享内存段:通过 shmget 创建大小为 SharedData 的共享内存段,键值为 0x1234
  2. 连接共享内存:使用 shmat 将共享内存映射到当前进程的地址空间。
  3. 写入数据:将数据写入共享内存中的 nummessage 字段。
  4. 分离共享内存:调用 shmdt 函数,将共享内存从当前进程的地址空间中分离。

另一个进程读取共享内存数据如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstring>
#include <cstdlib>

struct SharedData {
    int num;
    char message[100];
};

int main() {
    // 1. 获取已经存在的共享内存段
    int shmid = shmget(0x1234, sizeof(SharedData), 0666);
    if (shmid == -1) {
        std::cerr << "Failed to retrieve shared memory." << std::endl;
        return 1;
    }

    // 2. 将共享内存连接到当前进程的地址空间
    SharedData *data = static_cast<SharedData*>(shmat(shmid, nullptr, 0));
    if (data == (void*)-1) {
        std::cerr << "Failed to attach shared memory." << std::endl;
        return 1;
    }

    // 3. 读取共享内存中的数据
    std::cout << "Data read from shared memory: num = " << data->num
              << ", message = " << data->message << std::endl;

    // 4. 分离共享内存段
    shmdt(data);

    return 0;
}

上述代码说明如下:

  1. 获取共享内存段:通过 shmget 获取已经存在的共享内存段,键值为 0x1234
  2. 连接共享内存:使用 shmat 连接共享内存,允许进程访问数据。
  3. 读取数据:读取共享内存中的 nummessage
  4. 分离共享内存:通过 shmdt 将共享内存从当前进程分离。

四、共享内存中的注意事项

  1. 同步问题:共享内存并不提供同步机制。如果多个进程同时访问共享内存,可能会导致数据竞争。应使用同步机制(如信号量、互斥锁)来保证数据的一致性。

  2. 清理工作:不要忘记在进程结束时使用 shmdt 分离共享内存,并在最后一个进程完成后使用 shmctl 释放共享内存,避免内存泄漏。

  3. 内存限制:共享内存的大小受到系统设置的限制,如 SHMMAX,需要管理员权限来调整这些限制。

  4. 安全性问题:共享内存段可以通过键值共享,因此应设置合适的权限以避免未经授权的进程访问。