【Linux】进程间通信(3):共享内存

目录

一、共享内存概述

二、共享内存相关函数

1、shmget函数

2、ftok函数

3、shmctl函数

[4、 shmat函数](#4、 shmat函数)

[5、 shdt函数](#5、 shdt函数)

三、使用共享内存相关函数管理共享内存的一般过程

[1. 生成唯一的键值](#1. 生成唯一的键值)

[2. 创建或获取共享内存段](#2. 创建或获取共享内存段)

[3. 连接到共享内存段](#3. 连接到共享内存段)

[4. 操作共享内存](#4. 操作共享内存)

[5. 断开与共享内存段的连接](#5. 断开与共享内存段的连接)

[6. 删除共享内存段(可选)](#6. 删除共享内存段(可选))

总结

四、基于共享内存进行进程间通信的代码实践

五、共享内存相关命令行指令集

[1. ipcs --- 显示系统的 IPC(进程间通信)状态](#1. ipcs — 显示系统的 IPC(进程间通信)状态)

[2. ipcrm --- 删除 IPC 对象](#2. ipcrm — 删除 IPC 对象)

[3、ipcs -m 输出解释](#3、ipcs -m 输出解释)


一、共享内存概述

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间,进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种互斥与同步的机制,互斥锁和信号量都可以。【注意:互斥锁与信号量将会在后续多线程部分着重讲解,此处同步机制由命名管道进行协助实现,因为管道的读写阻塞自带同步机制。】

采用共享内存通信的一个显而易见的好处就是效率高,因为进程可以直接读写内存,而不需要复制任何数据。而管道和消息队列等通信方式,则需要在内核和用户空间进行4次数据复制:

管道通信在Linux等操作系统中,需要在用户空间和内核空间进行四次数据复制。这四次数据复制的具体过程如下:

  1. 用户空间到内核空间(写入管道):当一个进程向管道写数据时,数据从用户空间被复制到内核空间中的管道缓冲区。

  2. 内核空间到内核空间(在管道缓冲区中):数据被复制到内核空间后,内核会将这些数据存储在管道的内部缓冲区中。这个缓冲区是内核为管道通信特别设置的一部分内存区域,用于临时存储待传输的数据。

  3. 内核空间到内核空间(从管道缓冲区到读取缓冲区):当另一个进程从管道读取数据时,数据从管道缓冲区复制到内核中的读取缓冲区。这一步确保了读取进程能接收到数据。

  4. 内核空间到用户空间(读取管道):最后,数据从内核中的读取缓冲区复制到读取进程的用户空间中。

综上所述,管道通信的四次数据复制分别是:从用户空间到内核空间、内核空间内部存储(尽管这一步通常不被单独视为一次复制,但它是数据传递的关键环节)、以及从内核空间到用户空间。这些步骤共同构成了管道通信中数据在用户和内核空间之间的高效传递机制。

而共享内存只需要复制两次数据:1、从输入文件到共享内存区;2、从共享内存区到输出文件。

实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,等到有新的通信时再重新建立共享内存区,**而是保持共享内存区,直到通信完毕为止,如此,数据内容就一直保存在共享内存中,并没有写回文件。**因此,采用共享内存的通信方式的效率是非常高的。

本章节主要对 System V 共享内存 进行讲解。

共享内存内核原理(了解):

page cache 和swap cache 中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache 中,一个页面的所有信息由struct page 来描述。struct page中有一个域为指针mapping,它指向一个struct address_space 类型结构。page cache 或swap cache中的所有页面就是根据address_space 结构和一个偏移量来区分的。

文件与address_space 结构的对应:在打开一个具体的文件后,内核会在内存中为之建立一个 struct inode 结构,其中的imapping 域指向一个address space结构。这样,一个文件就对应一个address_space 结构,一个address_space与一个偏移量能够确定一个page cache或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件和数据在文件内的偏移量找到相应的页面。

当进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,在第一次访问该空间时,会引发缺页异常。

对于共享内存映射的情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space 和偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(Swap Area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中,进程最终将更新进程页表。

(注:对于映射普通文件的情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space和数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有被读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。)

所有进程在映射同一个共享内存区时的情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区对应的物理页面。

(注:一个共享内存区可以看作特殊文件系统shm中的一个文件,shm的安装点在交换区上.

上面涉及了一些数据结构,围绕数据结构理解问题会容易一些。)

二、共享内存相关函数

1、shmget函数

shmget 是一个用于操作共享内存的系统调用函数,主要用于在 Linux 操作系统中创建或获取共享内存段。下面是对 shmget 函数的详细解释:

函数原型

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

参数说明

  • key : 共享内存的键值,通常是一个整型值。这个键值用于标识共享内存段。通常使用 ftok 函数生成这个键值

  • size : 共享内存段的大小,以字节为单位。这个大小应该是大于 0 的整数。如果共享内存段已经存在,size 参数会被忽略。

  • shmflg: 控制标志,用于指定创建共享内存的权限和行为。常见的标志有:

    • IPC_CREAT:如果共享内存段不存在,则创建一个新的共享内存段。
    • IPC_EXCL:必须与 IPC_CREAT 一起使用,如果共享内存段不存在,则创建共享内存段;如果共享内存段已经存在,则返回错误。
    • 权限标志:如 06000666 等,表示共享内存段的访问权限(只读、只写、读写等)。

返回值

  • 成功 :返回一个非负整数,这是共享内存段的标识符(shmid)
  • 失败 :返回 -1,并设置 errno 来指示错误原因。

【说明】:key值由用户将通过调用ftok函数生成的返回值作为参数传给key,用于使操作系统生成一个唯一的共享内存的标识符,即 shmget 的返回值。可以理解为key是用户层面用来生成唯一的共享内存的标识,而shmid则是操作系统根据用户提供的唯一的key值生成的唯一的共享内存段的标识符,是系统层面的。进而当需要管理共享内存时,需要通过系统层面的唯一标识符shmid来对其标识的共享内存进行管理。

示例代码

下面是一个简单的示例,展示了如何使用 shmget 函数:

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

int main() {
    key_t key = ftok("shmfile", 65);  // 生成一个唯一的键值
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    int shmid = shmget(key, 1024, 0666 | IPC_CREAT);  // 创建共享内存段
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    printf("Shared memory segment created with ID: %d\n", shmid);

    return 0;
}

常见用法

  1. 创建共享内存 :使用 shmgetIPC_CREAT 标志来创建一个新的共享内存段。
  2. 访问现有共享内存 :如果共享内存已经存在,使用 shmget 获取现有的共享内存段的标识符。
  3. 检查权限 :通过 shmflg 参数设置对共享内存段的访问权限。

2、ftok函数

ftok 函数是一个用于生成唯一键值的函数,常用于创建 IPC(进程间通信)机制中的共享内存、消息队列或信号量。

函数原型

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

参数说明

  • pathname: 指定的路径名,用于生成唯一的键值。这个路径名必须是系统上存在的文件路径,一般是一个用于标识进程间通信对象的文件。例如,可以使用当前目录下的某个文件。

  • proj_id : 项目标识符,是一个字符(通常是 int 类型),用于在同一路径下生成不同的键值。它可以是任意的,但通常选择不同的字符来区分不同的 IPC 对象。

返回值

  • 成功 : 返回一个 key_t 类型的唯一键值。
  • 失败 : 返回 -1,并设置 errno 以指示错误原因。常见的错误包括:
    • EINVAL:提供的路径名无效。
    • ENOENT:指定的文件不存在。

示例代码

以下是一个简单的示例,展示了如何使用 ftok 函数生成一个唯一的键值:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <errno.h>

int main() {
    key_t key = ftok("examplefile", 'a');  // 使用路径 "examplefile" 和项目标识符 'a'
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    printf("Generated key: %d\n", key);

    return 0;
}

使用说明

  1. 文件路径 : pathname 参数必须是系统上存在的文件。一般来说,这个文件不需要有特殊权限,只需要存在即可。常用的做法是选择一个项目目录中的文件或临时文件。

  2. 项目标识符 : proj_id 参数可以是 0255 之间的整数,这样可以在同一个文件路径下生成不同的键值。通常选择 ASCII 字符作为项目标识符来提高可读性。

错误处理

  • 文件不存在 : 确保提供的 pathname 参数指向一个存在的文件。
  • 路径无效: 确保路径名参数格式正确且有效。

ftok 是在 Unix-like 系统中处理 IPC 机制时非常有用的函数,它帮助生成唯一的标识符来避免不同 IPC 对象之间的冲突。

3、shmctl函数

shmctl 是一个系统调用函数,用于对共享内存段进行控制和操作。它允许程序获取共享内存段的信息、修改共享内存段的属性,或删除共享内存段。

函数原型

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

参数说明

  • shmid : 共享内存段的标识符,通常是通过 shmget 函数获得的。

  • cmd : 控制命令,指定 shmctl 函数应执行的操作。常见的控制命令包括:

    • IPC_RMID:删除共享内存段。
    • IPC_STAT:获取共享内存段的信息。
    • IPC_SET:设置共享内存段的属性(如权限)。
    • SHM_LOCK:锁定共享内存段(在某些系统上可用)。
    • SHM_UNLOCK:解锁共享内存段(在某些系统上可用)。
  • buf : 指向 shmid_ds 结构体的指针,用于存储共享内存段的信息或设置属性。这个结构体的定义如下:

    struct shmid_ds {
        struct ipc_perm shm_perm;  // 共享内存段的权限
        size_t shm_segsz;          // 共享内存段的大小
        time_t shm_atime;          // 最后一次附加时间
        time_t shm_dtime;          // 最后一次分离时间
        time_t shm_ctime;          // 最后一次改变时间
        unsigned short shm_cpid;   // 创建共享内存段的进程 ID
        unsigned short shm_lpid;   // 最后一次操作共享内存的进程 ID
        unsigned short shm_nattch; // 连接到共享内存段的进程数
    };
    

返回值

  • 成功 : 对于 IPC_STATIPC_SET 命令,返回 0。对于 IPC_RMID 命令,返回 0
  • 失败 : 返回 -1,并设置 errno 以指示错误原因。

常见命令的使用

  1. 删除共享内存段 (IPC_RMID)

    这个命令用于标记共享内存段以待删除。共享内存段会在所有进程都分离之后被实际删除。

    #include <sys/shm.h>
    #include <stdio.h>
    
    int main() {
        int shmid = /* obtain shared memory ID */;
        if (shmctl(shmid, IPC_RMID, NULL) == -1) {
            perror("shmctl");
            return 1;
        }
        return 0;
    }
    
  2. 获取共享内存段的信息 (IPC_STAT)

    这个命令用于获取共享内存段的状态信息。你需要提供一个 shmid_ds 结构体的指针来存储这些信息。

    #include <sys/shm.h>
    #include <stdio.h>
    
    int main() {
        int shmid = /* obtain shared memory ID */;
        struct shmid_ds shm_info;
        if (shmctl(shmid, IPC_STAT, &shm_info) == -1) {
            perror("shmctl");
            return 1;
        }
        printf("Shared memory segment size: %zu bytes\n", shm_info.shm_segsz);
        return 0;
    }
    
  3. 设置共享内存段的属性 (IPC_SET)

    这个命令用于修改共享内存段的权限和其他属性。你需要提供一个包含新属性的 shmid_ds 结构体。

    #include <sys/shm.h>
    #include <stdio.h>
    
    int main() {
        int shmid = /* obtain shared memory ID */;
        struct shmid_ds shm_info;
        // 先获取当前信息
        if (shmctl(shmid, IPC_STAT, &shm_info) == -1) {
            perror("shmctl");
            return 1;
        }
        // 修改权限
        shm_info.shm_perm.mode = 0666;  // 设为读写权限
        if (shmctl(shmid, IPC_SET, &shm_info) == -1) {
            perror("shmctl");
            return 1;
        }
        return 0;
    }
    

错误处理

  • EINVAL : 提供的命令无效或 buf 参数无效。
  • EIDRM: 共享内存段已经被删除。
  • ENOMEM: 内存不足,无法完成操作。

shmctl 函数提供了对共享内存段的全面控制,可以在进程间通信中对共享内存进行管理和操作。

4、 shmat函数

shmat 是一个系统调用函数,用于将共享内存段附加到当前进程的地址空间中。这使得进程可以直接访问共享内存段的内容,从而实现进程间通信。下面是 shmat 函数的详细说明。

函数原型

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

参数说明

  • shmid : 共享内存段的标识符,由 shmget 函数返回。它标识了要附加的共享内存段。

  • shmaddr: 请求附加共享内存段的地址。这个参数的意义因平台而异:

    • NULL:表示让系统选择附加地址。
    • 非 NULL :请求将共享内存段附加到指定的地址。如果指定的地址不适合或不可用,通常会返回 -1
  • shmflg: 附加共享内存段的标志。常用的标志包括:

    • SHM_RDONLY:以只读模式附加共享内存段。如果不设置此标志,则默认以读写模式附加。
    • 0:默认情况下,以读写模式附加共享内存段。

返回值

  • 成功: 返回指向共享内存段的映射地址的指针。这个地址是进程的虚拟地址空间中的一个有效地址。
  • 失败 : 返回 -1,并设置 errno 以指示错误原因。如果失败,shmat 不会对 shmaddr 参数做任何修改。

错误处理

  • EACCES: 权限不足,无法以指定的模式附加共享内存段。
  • EINVAL : shmid 无效,或 shmaddr 地址无效。
  • ENOMEM: 内存不足,无法附加共享内存段。

示例代码

以下是一个示例,展示了如何使用 shmat 函数将共享内存段附加到进程的地址空间中:

#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    int shmid;
    void *shmaddr;
    size_t size = 1024;  // 共享内存段的大小
    key_t key = 1234;    // 共享内存段的键值

    // 创建共享内存段
    shmid = shmget(key, size, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    // 附加共享内存段
    shmaddr = shmat(shmid, NULL, 0);  // 让系统选择附加地址
    if (shmaddr == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    // 使用共享内存
    printf("Shared memory attached at address %p\n", shmaddr);
    snprintf((char *)shmaddr, size, "Hello, Shared Memory!");

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

    return 0;
}

注意事项

  1. 地址选择 :如果 shmaddr 参数为 NULL,系统会选择一个合适的地址来附加共享内存段。如果提供了一个非 NULL 地址,系统会尝试将共享内存段附加到该地址,如果无法附加,则 shmat 会失败。

  2. 权限控制 :使用 SHM_RDONLY 标志可以以只读模式附加共享内存段,确保不会修改共享内存中的数据。这对于只需要读取数据的进程很有用。

  3. 分离共享内存 :使用 shmdt 函数可以将共享内存从进程的地址空间中分离。确保在不再使用共享内存时调用 shmdt 以释放资源。

  4. 多进程访问:多个进程可以同时附加同一个共享内存段,允许它们通过共享内存进行数据交换。然而,要注意同步和互斥问题,以避免数据竞争和不一致。

shmat 是在进行进程间通信时,将共享内存映射到进程的虚拟地址空间中,使得进程能够直接读取和写入共享内存中的数据。

5、 shdt函数

shmdt 是一个标准的系统调用函数,用于从进程的地址空间中分离共享内存段。

函数原型

int shmdt(const void *shmaddr);

参数说明

  • shmaddr : 指向要分离的共享内存段的地址。这个地址是通过之前调用 shmat 函数获得的。

返回值

  • 成功 : 返回 0
  • 失败 : 返回 -1,并设置 errno 以指示错误原因。

错误处理

  • EINVAL : shmaddr 不在当前进程的地址空间中。
  • EPERM: 当前进程没有足够的权限执行该操作。

示例代码

以下是一个简单的示例,展示如何使用 shmdt 函数将共享内存段从进程的地址空间中分离:

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

int main() {
    int shmid;
    void *shmaddr;
    size_t size = 1024;  // 共享内存段的大小
    key_t key = 1234;    // 共享内存段的键值

    // 创建共享内存段
    shmid = shmget(key, size, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    // 附加共享内存段
    shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    // 使用共享内存
    printf("Shared memory attached at address %p\n", shmaddr);

    // 从进程的地址空间中分离共享内存
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }

    return 0;
}

解释

  1. 附加共享内存 : 之前调用 shmat 函数将共享内存段附加到进程的地址空间中。
  2. 使用共享内存 : 进程可以通过 shmaddr 指针访问共享内存中的数据。
  3. 分离共享内存 : 调用 shmdt 函数将共享内存段从进程的地址空间中分离,释放了对该内存区域的访问。

shmdt 是管理共享内存的重要函数,确保在不再需要访问共享内存时调用它以释放资源。

三、使用共享内存相关函数管理共享内存的一般过程

使用共享内存相关函数管理共享内存的一般过程可以分为以下几个步骤:

1. 生成唯一的键值

使用ftok函数生成一个唯一的键值(key_t),用于标识共享内存段。这一过程通常如下:

key_t key = ftok("/path/to/some/file", 'R');

这里,/path/to/some/file是一个存在的文件路径,'R'是项目标识符(通常可以是任意字符)。

2. 创建或获取共享内存段

使用shmget函数创建或获取共享内存段。shmget的调用形式如下:

int shmid = shmget(key, size, IPC_CREAT | 0666);
  • key:由ftok生成的唯一键值。
  • size:共享内存段的大小(以字节为单位)。
  • IPC_CREAT:如果共享内存段不存在,则创建它。
  • 0666:设置共享内存的权限(读写权限)。

shmget函数返回一个共享内存段的标识符(shmid),用于后续操作。

3. 连接到共享内存段

使用shmat函数将共享内存段附加到调用进程的地址空间。这样,进程可以访问共享内存。shmat的调用形式如下:

void *shm_addr = shmat(shmid, NULL, 0);
  • shmid:由shmget返回的共享内存标识符。
  • NULL:系统会选择一个合适的地址进行映射。
  • 0:指定附加的选项(通常为0,表示默认行为)。

shmat函数返回一个指向共享内存的指针(shm_addr),通过它,进程可以访问共享内存段的内容。

4. 操作共享内存

现在,你可以通过shm_addr指针读取或写入共享内存中的数据。操作共享内存就像操作普通内存一样。例如,可以将shm_addr 当作转换为char*类型的字符数组的指针,使用snprintf函数进行写入。

5. 断开与共享内存段的连接

当进程不再需要访问共享内存时,使用shmdt函数断开共享内存段:

shmdt(shm_addr);
  • shm_addr:之前通过shmat函数获得的共享内存地址。

6. 删除共享内存段(可选)

如果不再需要共享内存段,可以使用shmctl函数删除它。删除操作通常由共享内存段的所有者执行。shmctl的调用形式如下:

shmctl(shmid, IPC_RMID, NULL);
  • shmid:由shmget返回的共享内存标识符。
  • IPC_RMID:请求删除共享内存段。
  • NULL:不需要提供额外的信息。

总结

  1. 生成键值 :使用ftok函数生成唯一的键值。
  2. 获取共享内存段 :使用shmget创建或获取共享内存段。
  3. 连接共享内存 :使用shmat将共享内存附加到进程地址空间。
  4. 操作共享内存:通过指针访问和修改共享内存中的数据。
  5. 断开连接 :使用shmdt断开共享内存段。
  6. 删除共享内存段(可选) :使用shmctl删除共享内存段。

四、基于共享内存进行进程间通信的代码实践

当获取了共享内存的虚拟地址后,可以将其强制转换为期望的指针类型(比如 char*int* 等),就可以将这块内存区域当作数组来使用。进而我们可以直接使用下标来读取和修改共享内存中的数据。例如:

1. 创建和写入共享内存的程序

对于共享内存的数据的写入,

1、我们可以使用strcpy/strncpy函数将字符串拷贝给共享内存。

2、我们可以使用sprintf/snprintf函数实现向共享内存中进行数据写入。

3、也可以通过使用数组下标的方式来读取和修改共享内存中的数据,需要注意的是数组下标不能超过共享内存的容量所允许的最大下标,即不能超过SHM_SIZE-1。

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

#define SHM_KEY 0x1234      // 共享内存的唯一标识符
#define SHM_SIZE 1024       // 共享内存的大小

int main() {
    int shm_id;
    char *shm_ptr;

    // 创建共享内存段
    shm_id = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    if (shm_id == -1) {
        perror("shmget");
        exit(1);
    }

    // 将共享内存段附加到当前进程的地址空间
    shm_ptr = (char*)shmat(shm_id, NULL, 0);
    if (shm_ptr == (char*)-1) {
        perror("shmat");
        exit(1);
    }

    // 1、通过strcpy或snprintf函数修改共享内存中的数据
    strcpy(shm_ptr, "Hello, System V shared memory!");

    // 2、使用下标方式修改共享内存的内容
    shm_ptr[0] = 'H';
    shm_ptr[1] = 'e';
    shm_ptr[2] = 'l';

    // 打印共享内存中的内容
    printf("Shared memory contains: %s\n", shm_ptr);

    // 解除共享内存的映射
    if (shmdt(shm_ptr) == -1) {
        perror("shmdt");
        exit(1);
    }

    return 0;
}
2. 读取共享内存的程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>

#define SHM_KEY 0x1234      // 共享内存的唯一标识符
#define SHM_SIZE 1024       // 共享内存的大小

int main() {
    int shm_id;
    char *shm_ptr;

    // 获取共享内存段的标识符
    shm_id = shmget(SHM_KEY, SHM_SIZE, 0666);
    if (shm_id == -1) {
        perror("shmget");
        exit(1);
    }

    // 将共享内存段附加到当前进程的地址空间
    shm_ptr = (char*)shmat(shm_id, NULL, 0);
    if (shm_ptr == (char*)-1) {
        perror("shmat");
        exit(1);
    }

    // 读取共享内存中的内容
    printf("Shared memory contains: %s\n", shm_ptr);

    // 解除共享内存的映射
    if (shmdt(shm_ptr) == -1) {
        perror("shmdt");
        exit(1);
    }

    return 0;
}
3、模拟服务端与客户端的信息交互,使用命名管道协助共享内存实现同步机制。

代码整体思路:1、服务端负责创建共享内存与命名管道。2、客户端负责向共享内存中写入信息,当写入完成后,通过对管道写入信息来唤醒服务端进行数据的读取。3、当服务端的管道接收到信息后,再通过共享内存接收服务端写入的信息。4、服务端负责共享内存和命名管道的创建与销毁。5、共享内存在服务端进程和客户端进程的地中空间中都需要进行连接和断开连接的操作。

namedpipe.hpp:

#pragma once
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <cstring>

#define PATHNAME "./myfifo"

#define Creater 0
#define User 1
#define LIMIT 0666
#define BUFFER_SIZE 256

class NamePipe
{
private:
    std::string _path;
    int _fd;
    int _identity;
public:
    NamePipe(const std::string& path, int identity)
    :_path(path), _identity(identity), _fd(-1)
    {
        if(_identity == Creater){
            if(mkfifo(PATHNAME, LIMIT) == -1){
                perror("create fifo fail!");
                exit(-1);
            }
        }
    }
    ~NamePipe(){
        if (_identity == Creater)
        {
            int res = unlink(_path.c_str());
            if (res != 0)
            {
                perror("unlink fail!");
            }
        }
        Close();
    }
    void OpenForRead()
    {
        if((_fd = open(_path.c_str(), O_RDONLY, LIMIT)) == -1){
                perror("open for read fail!");
                exit(-1);
            }
            else{
                std::cout << "客户端准备进行通信..." << std::endl;
            }
        
    }
    int Read()
    {
        int r_num = 0;
        char buffer[BUFFER_SIZE];
        memset(buffer, 0, sizeof(buffer));
        if((r_num = read(_fd, buffer, sizeof(buffer) - 1)) == -1){
            perror("read fail!");
            return -1;
        }
        else if(r_num == 0)
        {
            std::cout << "客户端关闭,通信终止..." << buffer << std::endl;
            return 0;
        }
        else {
            buffer[r_num] = '\0';
        }
        return r_num;
    }
    void OpenForWrite()
    {
        if((_fd = open(_path.c_str(), O_WRONLY, LIMIT)) == -1){
                perror("open for write fail!");
                exit(-1);
            }
        else{
            std::cout << "服务端端准备进行通信..." << std::endl;
        }
    }
    void Write(const std::string& buffer)
    {
        int w_num = 0;
        if((w_num = write(_fd, buffer.c_str(), buffer.size())) == -1){
            perror("read fail!");
            exit(-1);
        }
    }
    void Close()
    {
        close(_fd);
    }
};

shared_memory.hpp:

#pragma once
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>

#ifndef Creater
#define Creater 0
#endif

#ifndef User
#define User 1
#endif

#define LIMIT 0666
#define SHM_SIZE 4096
#define PATH_SIZE 128

char path_name[PATH_SIZE] = {0};

class shm
{
private:
    int _identity;
    int _shmid;
    void *_shm_addr;

public:
    // 获取当前文件路径,作为ftok函数的路径参数
    char *GetCwd()
    {
        
        char *ptr = nullptr;
        if ((ptr = getcwd(path_name, sizeof(path_name))) == nullptr)
        {
            perror("getcwd fail!!!");
            exit(errno);
        }
        return ptr;
    }

    key_t GetKey()
    {
        key_t k = 0;
        if ((k = ftok(GetCwd(), Creater)) == -1)
        {
            perror("ftok fail!!!");
            exit(errno);
        }
        return k;
    }

    int GetShmIdForCreater()
    {
        int id = 0;
        if ((id = shmget(GetKey(), SHM_SIZE, IPC_CREAT | IPC_EXCL | LIMIT)) == -1)
        {
            perror("shmget for creater fail!!!");
            exit(errno);
        }
        return id;
    }

    int GetShmIdForUser()
    {
        int id = 0;
        if ((id = shmget(GetKey(), SHM_SIZE, IPC_CREAT | LIMIT)) == -1)
        {
            perror("shmget for user fail!!!");
            exit(errno);
        }
        return id;
    }

    void *ShmAttch()
    {
        void *res = shmat(_shmid, nullptr, 0);
        if (res == (void *)-1)
        {
            perror("shmat fail!!!");
            exit(errno);
        }
        return res;
    }

    void WriteShm(const char *str)
    {
        size_t len = strlen(str);
        if (len >= SHM_SIZE)
        {
            perror("Input string is too large for shared memory\n");
            exit(errno);
        }
        if (snprintf(static_cast<char *>(_shm_addr), SHM_SIZE, "%s", str) < 0)
        {
            perror("WriteShm fail!!!");
            exit(errno);
        }
    }

    void ReadShm(char *str)
    {
        if (str == nullptr)
        {
            fprintf(stderr, "Output buffer is null\n");
            exit(EXIT_FAILURE);
        }
        strncpy(str, static_cast<char *>(_shm_addr), SHM_SIZE - 1);
        str[SHM_SIZE - 1] = '\0'; // 确保字符串结尾符
    }

    shm(int identity)
        : _identity(identity)
    {
        if (_identity == Creater)
        {
            _shmid = GetShmIdForCreater();
            std::cout << "Shared memory has been created..." << std::endl;
        }
        else
        {
            _shmid = GetShmIdForUser();
            std::cout << "Shared memory has been used..." << std::endl;
        }
        _shm_addr = ShmAttch();
    }

    ~shm()
    {
        if (shmdt(_shm_addr) == -1)
        {
            perror("shmdt fail!!!");
            exit(errno);
        }
        std::cout << "Shared memory mounting has been released..." << std::endl;
        if (_identity == Creater)
        {
            if (shmctl(_shmid, IPC_RMID, nullptr) == -1)
            {
                perror("shmctl fail!!!");
                exit(errno);
            }
            std::cout << "Remove Shared_Memory Successfully !!!" << std::endl;
        }
    }
};

server.cc:

#include "shared_memory.hpp"
#include "namedpipe.hpp"

int main()
{
    shm t(Creater);
    std::string fifo_path = std::string(t.GetCwd()) + std::string("/myfifo");
    NamePipe p(fifo_path.c_str(), User);
    std::cout << "Waiting for the client to suspend... " << std::endl; 
    sleep(5);
    p.OpenForRead();
    while(true)
    {
        if(p.Read() == 0) break;
        char buffer[SHM_SIZE];
        t.ReadShm(buffer);
        std::cout << "server read : " << buffer << std::endl; 
    }
    return 0;   
}

client.cc:

#include "shared_memory.hpp"
#include "namedpipe.hpp"
#include <signal.h>

int main()
{
    shm t(User);
    std::string fifo_path = std::string(t.GetCwd()) + std::string("/myfifo");
    NamePipe p(fifo_path.c_str(), Creater);
    p.OpenForWrite();
    char ch = 'a';
    std::string s;
    while(ch <= 'g')
    {
        s += ch++;
        std::cout <<"client write : " << s << std::endl; 
        t.WriteShm(s.c_str());
        p.Write("User");//向共享内存中写完信息后,再向管道中写入信息,唤醒服务端读取共享内存的信息
        sleep(1);
    }
    return 0;   
}

makefile:

.PHONY:all
all:server client

server:server.cc
	g++ server.cc -o server

client:client.cc
	g++ client.cc -o client

.PHONY:clean
clean:
	rm -f client server

五、共享内存相关命令行指令集

在 Linux 操作系统中,处理共享内存的常用命令的包括 ipcsipcrm。这些工具可以用于查看和管理系统中的共享内存段。以下是这些命令的详细用法:

1. ipcs --- 显示系统的 IPC(进程间通信)状态

ipcs 命令用于显示当前系统中各种 IPC 对象的状态,包括共享内存段、消息队列和信号量。

语法

ipcs [options]

常用选项

  • -m: 显示共享内存段的信息。
  • -q: 显示消息队列的信息。
  • -s: 显示信号量的信息。
  • -a: 显示所有 IPC 对象的信息(共享内存段、消息队列、信号量)。
  • -l: 显示 IPC 资源的限制信息。

示例

  1. 显示所有共享内存段

    ipcs -m
    
  2. 显示所有 IPC 对象(包括共享内存段、消息队列和信号量)

    ipcs -a
    
  3. 显示 IPC 资源的限制

    ipcs -l
    

2. ipcrm --- 删除 IPC 对象

ipcrm 命令用于删除系统中的 IPC 对象,如共享内存段、消息队列和信号量。

语法

ipcrm [options] id

常用选项

  • -m: 删除共享内存段。

  • -q: 删除消息队列。

  • -s: 删除信号量集。

参数

  • id : IPC 对象的标识符(可以通过 ipcs 命令获得)。

示例

  1. 删除指定的共享内存段

    ipcrm -m shmid
    

    其中 shmid 是共享内存段的标识符。

  2. 删除指定的消息队列

    ipcrm -q msgid
    

    其中 msgid 是消息队列的标识符。

  3. 删除指定的信号量集

    ipcrm -s semid
    

    其中 semid 是信号量集的标识符。

使用示例

假设你创建了一个共享内存段,其标识符是 12345,可以通过 ipcs -m 查看共享内存段的详细信息:

ipcs -m

输出示例(部分):

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x12345678 12345      user       666        1024       2

如果你想删除这个共享内存段,可以使用 ipcrm 命令:

ipcrm -m 12345

这将删除标识符为 12345 的共享内存段。

总结

  • ipcs 命令用于查看共享内存段及其他 IPC 对象的信息。
  • ipcrm 命令用于删除指定的共享内存段、消息队列或信号量集。

3、ipcs -m 输出解释

ipcs -m 命令的输出中,各列的含义如下:

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

1. key

  • 含义 : 共享内存段的键值(key_t 类型)。这是一个整数值,用于标识共享内存段。可以使用 ftok 函数生成键值,确保在不同进程间唯一。

2. shmid

  • 含义 : 共享内存段的标识符(shmid)。这是系统分配给共享内存段的唯一标识符,供进程在使用 shmatshmdtshmctl 等系统调用时引用。

3. owner

  • 含义: 共享内存段的拥有者。显示拥有该共享内存段的用户的用户名或用户ID(UID)。

4. perms

  • 含义: 权限设置,显示共享内存段的权限。这通常是一个三位八进制数(类似文件权限),其中每一位表示不同用户组的权限。权限位的含义如下:

    • 第一位表示所有者的权限(读、写、执行)。

    • 第二位表示同组用户的权限(读、写、执行)。

    • 第三位表示其他用户的权限(读、写、执行)。

    例如,666 表示所有用户都具有读写权限,但没有执行权限。

5. bytes

  • 含义: 共享内存段的大小(以字节为单位)。表示分配给共享内存段的总内存大小。

6. nattch

  • 含义 : 当前附加到共享内存段的进程数量。表示有多少个进程当前附加了这个共享内存段。进程通过 shmat 系统调用将共享内存附加到其地址空间中。

7. status

  • 含义 : 共享内存段的状态。通常显示为 (没有状态信息)或 (指示是否是共享内存段的创建/删除状态的标志,具体视系统和情况而定)。在一些系统中,这一列可能为空或不显示特定的状态信息。

示例解释

假设 ipcs -m 命令的输出如下:

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x12345678 12345      user       666        1024       2

这个输出表示:

  • 键值 (key) : 0x12345678,这是共享内存段的标识符键值。
  • 共享内存标识符 (shmid) : 12345,这是系统分配的共享内存段的唯一标识符。
  • 拥有者 (owner) : user,表示该共享内存段的拥有者是用户名为 user 的用户。
  • 权限 (perms) : 666,表示所有用户都有读写权限,没有执行权限。
  • 大小 (bytes) : 1024,共享内存段的大小为 1024 字节。
  • 附加进程数量 (nattch) : 2,表示有 2 个进程当前附加到这个共享内存段。
  • 状态 (status): 空或无特定状态,具体信息可能依赖于系统。
相关推荐
九河云29 分钟前
AWS账号注册费用详解:新用户是否需要付费?
服务器·云计算·aws
Lary_Rock34 分钟前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
幺零九零零1 小时前
【计算机网络】TCP协议面试常考(一)
服务器·tcp/ip·计算机网络
云飞云共享云桌面2 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
励志成为嵌入式工程师2 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer3 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
一坨阿亮4 小时前
Linux 使用中的问题
linux·运维
hikktn5 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust