Bootstrap

Linux 进程间通信——共享内存

目录

0.前言

1.共享内存的数据结构

2.代码示例及共享内存函数

2.1代码示例

2.2 shmget 函数

2.3 shmat 函数

2.4 shmdt 函数

2.5 shmctl 函数

3.共享内存和管道的比较

4.进一步了解systemV

5.结语


(图像由AI生成) 

0.前言

在上篇博客中,我们介绍了 Linux 进程间通信的方式之一——管道。管道是一种简单、方便的进程间通信方式,但它的效率和数据持久性较低,适用于父子进程之间的数据传输。在本篇中,我们将继续深入 Linux IPC(Inter-Process Communication,进程间通信)机制,介绍另一种高效的通信方式——System V 共享内存。共享内存是一种允许多个进程直接访问内存区域的机制,能够提供比管道更快的数据交换速度,非常适合在进程间传输大量数据。

1.共享内存的数据结构

共享内存的示意图如下所示:

在 Linux 系统中,struct shmid_ds 是一个描述共享内存段的内核数据结构,用于管理每个共享内存段的状态和属性。以下是该结构体的主要成员及其作用说明:

struct shmid_ds {
    struct ipc_perm shm_perm;      /* 访问权限和标识符 */
    int shm_segsz;                 /* 共享内存段的大小(字节) */
    __kernel_time_t shm_atime;     /* 上次连接到共享内存的时间 */
    __kernel_time_t shm_dtime;     /* 上次断开共享内存的时间 */
    __kernel_time_t shm_ctime;     /* 共享内存的上次修改时间 */
    __kernel_ipc_pid_t shm_cpid;   /* 创建共享内存的进程 ID */
    __kernel_ipc_pid_t shm_lpid;   /* 上次操作共享内存的进程 ID */
    unsigned short shm_nattch;     /* 当前连接到该共享内存段的进程数 */
    unsigned short shm_unused;     /* 兼容性字段,已废弃 */
    void *shm_unused2;             /* 保留字段(DIPC 使用) */
    void *shm_unused3;             /* 未使用的保留字段 */
};

主要字段说明:

  • shm_perm:这是一个 ipc_perm 类型的结构体,包含共享内存的权限信息,包括用户 ID、组 ID 和访问权限等。它允许系统控制对共享内存段的访问。

  • shm_segsz:共享内存段的大小,以字节为单位。在分配共享内存段时,指定该段的大小,以确定可用内存空间。

  • shm_atimeshm_dtimeshm_ctime:这些字段记录了共享内存段的时间信息。shm_atime 表示上次连接的时间,shm_dtime 表示上次断开的时间,shm_ctime 表示最后一次更改(如调整权限)的时间。

  • shm_cpidshm_lpidshm_cpid 是创建共享内存段的进程 ID,shm_lpid 是最后一个操作该共享内存的进程 ID,用于跟踪对共享内存的访问和管理。

  • shm_nattch:表示当前连接到此共享内存段的进程数。该值用于确定共享内存是否在使用中,通常只有当 shm_nattch 为 0 时,才可以销毁该段。

  • shm_unusedshm_unused2shm_unused3:这些字段为兼容性和扩展预留,目前在大多数系统中未被使用。

这些字段的设置和更新由内核在共享内存的管理过程中自动完成,因此开发者在调用共享内存相关的 API(如 shmgetshmat 等)时无需直接操作这些字段。

2.代码示例及共享内存函数

2.1代码示例

以下代码展示了如何使用 System V 共享内存来进行进程间的通信。此示例包括创建共享内存、写入数据、读取数据,以及在多个进程之间同步共享内存的操作,旨在演示共享内存的基本用法。每个代码片段均附有详细注释以便理解。

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

#define SHM_SIZE 1024  // 共享内存的大小(字节)

int main() {
    // Step 1: 创建唯一的键值
    key_t key = ftok("shmfile", 65);  // 使用ftok生成一个唯一的key
    if (key == -1) {
        perror("ftok failed");
        exit(1);
    }

    // Step 2: 创建共享内存段
    int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);  // 0666代表读写权限
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }

    // Step 3: 创建子进程
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(1);
    } else if (pid == 0) {
        // 子进程:写入数据到共享内存
        char *data = (char*) shmat(shmid, NULL, 0);  // 连接到共享内存
        if (data == (char *)(-1)) {
            perror("shmat failed");
            exit(1);
        }

        printf("子进程:写入数据到共享内存...\n");
        strcpy(data, "Hello, Shared Memory! This is child process writing.");
        printf("子进程:数据写入完成,内容为:%s\n", data);

        // 分离共享内存
        if (shmdt(data) == -1) {
            perror("shmdt failed");
            exit(1);
        }

        exit(0);  // 子进程完成写入后退出
    } else {
        // 父进程等待子进程完成
        wait(NULL);

        // 父进程:读取共享内存中的数据
        char *data = (char*) shmat(shmid, NULL, 0);  // 连接到共享内存
        if (data == (char *)(-1)) {
            perror("shmat failed");
            exit(1);
        }

        printf("父进程:读取共享内存中的数据...\n");
        printf("父进程:读取的数据为:%s\n", data);

        // 分离共享内存
        if (shmdt(data) == -1) {
            perror("shmdt failed");
            exit(1);
        }

        // 删除共享内存段
        if (shmctl(shmid, IPC_RMID, NULL) == -1) {
            perror("shmctl failed");
            exit(1);
        }
    }

    return 0;
}

代码说明:

  1. Step 1: 使用 ftok 函数生成一个唯一的键值 key。这个键将用于识别共享内存段。ftok 可以通过指定文件路径和项目标识符生成一个独特的 key,便于共享内存段在不同进程间的访问。

  2. Step 2: 使用 shmget 函数创建一个大小为 SHM_SIZE 的共享内存段,并设置其权限为读写。IPC_CREAT 标志表示如果该共享内存段不存在,则创建新的。

  3. Step 3: 使用 fork 创建一个子进程,演示不同进程对共享内存的访问。

子进程:

  • 使用 shmat 函数将共享内存段映射到自身的地址空间,获取指向共享内存的指针 data
  • 将字符串写入共享内存,并显示写入的内容。
  • 完成后使用 shmdt 函数分离共享内存。

父进程:

  • 使用 wait 等待子进程完成写入。
  • 使用 shmat 连接到共享内存段,读取内容并打印。
  • 断开连接并使用 shmctl 删除共享内存段,确保内存资源被释放。

运行结果

  • 子进程会将数据写入共享内存,而父进程读取并显示数据,展示了进程间共享内存通信的基本工作流程。

2.2 shmget 函数

功能shmget 用于创建或获取一个共享内存段。

原型

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

参数

  • key:共享内存段的标识符,通常通过 ftok 函数生成。用于区分不同的共享内存段。
  • size:共享内存段的大小(字节)。这是共享内存分配的内存空间大小。
  • shmflg:控制共享内存段的权限和行为,通常是访问权限和创建标志的组合。
    • 取值 IPC_CREAT:若共享内存段不存在,则创建新段;若已存在,则返回现有段。
    • 取值 IPC_CREAT | IPC_EXCL:若共享内存段不存在,则创建;若已存在,则返回错误。

返回值:成功时返回共享内存段的标识符(非负整数);失败时返回 -1,并设置 errno 标记错误原因。

2.3 shmat 函数

功能shmat 将指定的共享内存段附加到当前进程的地址空间,允许进程访问共享内存中的数据。

原型

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数

  • shmid:共享内存段的标识符,由 shmget 返回。
  • shmaddr:用于指定共享内存的连接地址。通常为 NULL,让系统选择合适的地址。
  • shmflg:附加模式,可设为 SHM_RDONLY 以只读方式附加共享内存;若为 0,则为读写模式。

返回值:成功时返回共享内存段在当前进程地址空间中的首地址;失败时返回 (void *) -1

2.4 shmdt 函数

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

原型

int shmdt(const void *shmaddr);

参数

  • shmaddr:共享内存段在进程地址空间中的首地址,即 shmat 返回的地址。

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

2.5 shmctl 函数

功能shmctl 用于控制共享内存段的操作,如删除共享内存段、获取共享内存信息等。

原型

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数

  • shmid:共享内存段的标识符。
  • cmd:控制命令,常用的有:
    • IPC_RMID:删除共享内存段。
    • IPC_STAT:获取共享内存段的状态信息,存储在 buf 中。
    • IPC_SET:设置共享内存段的状态信息,如更改权限。
  • bufshmid_ds 结构体指针,用于存储或设置共享内存段的状态信息。

返回值:成功时返回 0;失败时返回 -1,并设置 errno 标记错误原因。

3.共享内存和管道的比较

特性共享内存管道
数据交换速度高速(直接内存访问)较慢(需读写操作)
数据存储持久性手动控制,需显式删除随进程结束自动销毁
访问方式允许多个进程读写,需同步控制适用于父子进程之间
数据量适合大数据量传输适合小数据量传输
通信方向双向读写单向(单向管道)
编程复杂度较高(需同步机制)较低
创建方式使用 shmget 创建使用 pipemkfifo

4.进一步了解systemV

System V 是 UNIX 系统的一个重要版本,其提供了一套经典的进程间通信(IPC)机制,包括 共享内存消息队列信号量,它们被称为 System V IPC。这些机制用于满足多进程间的数据共享、消息传递和同步需求,是传统 UNIX 系统中常见的 IPC 手段。

System V 其他通信方式:

  1. 消息队列(Message Queue):消息队列允许进程通过有序的消息进行通信。进程可以将消息放入队列,其他进程按顺序读取消息,实现了异步、结构化的数据传递。

  2. 信号量(Semaphore):信号量用于控制多个进程对共享资源的访问,主要解决并发情况下的同步问题。信号量可以增加或减少资源计数,常用于进程的互斥控制,防止多个进程同时操作同一资源。

这些 System V IPC 方式各自有不同的应用场景,消息队列适用于消息传递,信号量适用于同步控制,而共享内存适用于高效的数据共享。

5.结语

通过本篇博客,我们深入探讨了 Linux 中 System V 共享内存的使用方法,包括其数据结构、关键函数及其使用示例。同时,我们将共享内存与管道进行了对比,并简要介绍了 System V 的其他通信方式,如消息队列和信号量。共享内存作为一种高效的进程间通信机制,适用于大数据量的传输和多进程的资源共享,是 Linux IPC 中不可或缺的一部分。希望通过本篇博客,大家能够更好地理解和运用共享内存来优化进程间的通信效率。

;