Loading

【进程间通信】——共享内存

Unix系统的System-V版本中就引入了三种进程间通信方式,分别是消息队列、共享内存、信号量集。这三种通信方式也被称为System-V IPC对象。

共享内存 (Shared Memory)

前言

​在下文或接下来的讨论中,我将借用了大佬的文章来深化对特定知识点的理解。我非常尊重原创内容和知识产权,如果有任何侵犯版权或不符合规范的地方,请不吝告知。我将立即采取措施进行修正,并确保内容的合法合规使用。感谢您的理解与支持。

在理解共享内存之前,想必在Linux上写过程序的同学之前都会有这么个疑问,你写的程序在运行时占用了多少内存(物理内存)?

通常我们可以通过top命令查看进程占用了多少内存。

image

这里我们深入探讨一下Linux系统中的VIRT、RES和SHR这三个关键性能指标,它们各自具有独特的含义。

​top命令是Linux系统中一个功能强大的性能监控工具,它不仅可以实时显示系统的总体性能,还能深入到每个进程层面,提供详细的CPU使用率、内存使用情况等信息。通过top命令,我们可以轻松获取到每个进程的VIRT、RES和SHR等关键指标,从而对系统的性能状况有一个全面的了解。

虚拟内存

​首先,我们需要明确区分两个概念:虚拟内存和物理内存。虚拟内存是操作系统内核为了管理进程地址空间而设计的一个逻辑上的概念,而物理内存则是实际存在于计算机硬件中的存储空间。一个程序可能拥有很大的虚拟内存空间,但这并不意味着它必然占用大量的物理内存。这是因为虚拟内存空间允许程序访问比物理内存更大的地址范围,而操作系统会根据需要将虚拟内存中的数据实际加载到物理内存中。

​虚拟内存的概念至关重要。当我们编写程序,例如C语言程序,并使用gcc编译器进行编译时,编译器分配给程序的地址实际上是虚拟内存空间中的地址。在程序运行之前,这些地址并不对应物理内存中的任何位置。程序运行过程中可能需要的所有指令和数据,都必须首先存在于虚拟内存空间中。

​既然虚拟内存是一个逻辑上的概念,为了让程序能够在物理机器上执行,就必须有一种机制将这些逻辑上的虚拟内存映射到物理内存上(实实在在的RAM内存条上的空间)。这就是操作系统中的页映射表发挥作用的地方。操作系统为系统中的每个进程维护一个独立的页映射表。

​页映射表的工作原理是将程序在运行时需要访问的虚拟内存空间的各个部分,通过页映射表映射到物理内存空间的相应部分。这样,当CPU需要访问某个虚拟内存地址时,它可以通过查找页映射表来找到对应的物理内存地址。这个过程是Linux操作系统分页机制的核心,其中“页(page)”是虚拟内存空间向物理内存空间映射的基本单位。通过这种方式,操作系统能够高效地管理内存资源,确保程序的顺利运行。
image

​图中演示了虚拟内存空间和物理内存空间的映射关系,它们通过 Page Table关联起来,其中虚拟内存空间中着色的部分分别被映射到物理内存空间对应相同着色的部分。而虚拟内存空间中灰色的部分表示在物理内存空间中没有与之对应的部分,也就是说灰色部分没有被映射到物理内存空间中。这么做也是本着“按需映射”的指导思想,因为虚拟内存空间很大,可能其中很多部分在一次程序运行过程中根本不需要访问,所以也就没有必要将虚拟内存空间中的这些部分映射到物理内存空间上。总结一下就是,虚拟内存是一个假象的内存空间,在程序运行过程中虚拟内存空间中需要被访问的部分会被映射到物理内存空间中。虚拟内空间大只能表示程序运行过程中可访问的空间比较大,不代表物理内存空间占用也大。

​VIRT表示的是进程虚拟内存空间大小。对应到图中的进程A来说就是A1、A2、A3、A4以及灰色部分所有空间的总和。也就是说VIRT包含了在已经映射到物理内存空间的部分和尚未映射到物理内存空间的部分和。RES表示的是进程虚拟內存空间中已经映射到物理內存空间的那部分的大小。对应到图中的进程A来说就是A1、A2、A3以及A4几个部分空间的总和。所以说,看进程在运行过程中占了多少内存应该看RES的值而不是VIRT的值。SHR是 share(共享)的缩写,它表示的是进程占用的共享内存大小。在上图中我们看到进程A虚拟内存空间中的A4和进程B虚拟内存空间中的B3都映射到了物理内存空间的A4/B3。为什么会出现这样的情况呢?

​其实我们写的程序会依赖于很多外部的动态库(.so),比如libc.so、libd.so等等。这些动态库在内存中仅仅会保存/映射一份,如果某个进程运行时需要这个动态库,那么动态加载器会将这块内存映到对应进程的虚拟内存空间中。多个进程之间通过共享内存的方式相互通信也会出现这样的凊况。这么一来,就会出现不同进程的虚拟内存空间会映射到相同的物理内存空间。这部分物理内存空间其实是被多个进程所共享的,某个进程占用的内存除了和别的进程共享的内存之外就是自己的独占内存了。所以要计算进程独占内存的大小只要用RES的值减去SHR值即可 。

驻留内存

​顾名思义是指那些被映射到进程虛拟内存空间的物理内存。上图中,在系统物理内存空间中被着色的部分都是驻留内存。比如,A1、A2、A3和A4是进程A的驻留内存,B1、B2和B3是进程B的驻留内存。进程的驻留内存就是进程实实在在占用的物理内存。一般我们所讲的进程占用了多少内存,其实就是说的占用了多少驻留内存而不是多少虚拟内存。因为虛拟内存大并不意味着占用的物理内存大。

System V 共享内存

​从上面已经知道了共享内存其实是物理内存中的一段内存空间。所以进程想要一块共享内存必须向操作系统申请一块物理内存,然后映射到虚拟内存。而Linux系统也提供了一系列有关共享内存的函数接口,如 shmget, shmat, shmdt, shmctl 等函数。

函数及其用途

  • shmget

    • 用途:用于创建一个新的共享内存段或获取一个已存在的共享内存段的标识符;

    • 原型:int shmget(key_t key, size_t size, int shmflg);

    • @param

      • key_t key: 用于标识共享内存段的键值。可以使用 ftok 函数生成,也可以自己定义。如果使用 IPC_PRIVATE 作为键值,则会创建一个新的共享内存段,且只有调用 shmget 的进程能访问。

      • size_t size: 共享内存段的大小(以字节为单位)。如果共享内存段已经存在,此参数可以为 0。

      • int shmflg: 标志位,控制共享内存段的创建和权限。常用标志包括:

        • IPC_CREAT: 如果共享内存段不存在,则创建一个新的。

        • IPC_EXCL: 与 IPC_CREAT 一起使用,如果共享内存段已经存在,则返回错误。

        • 权限标志:如 0666,表示读写权限。

    • 示例

key_t key = ftok("shmfile", 66);
if (key == -1) {
    fprintf(stderr, "ftok error,errno:%d,%s\n", errno, strerror(errno));
    exit(EXIT_FAILURE);
}
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1) {
    fprintf(stderr, "shmget error,errno:%d,%s\n", errno, strerror(errno));
    exit(EXIT_FAILURE);
}

注意:申请成功的共享内存段里面存储的内容会被自动初始化为0,并且内核会为每一块创建的共享内存分配一个shmid_ds结构体来记录共享内存的属性和信息。

  • shmat

    • 用途:用于将共享内存段映射到调用进程的虚拟地址空间;

    • 原型:void *shmat(int shmid, const void *shmaddr, int shmflg);

    • @param

      • int shmid: 共享内存段的标识符,由 shmget 返回。

      • const void *shmaddr: 指定进程附加共享内存段的地址。通常设为 NULL,让系统自动选择合适的地址。

      • int shmflg: 标志位,控制共享内存段的附加行为。常用标志包括:

        • SHM_RDONLY: 只读模式附加共享内存段。

        • 0: 读写模式附加共享内存段。

char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (char *)(-1)) {
    fprintf(stderr, "shmat error, errno: %d, %s\n", errno, strerror(errno));
    exit(EXIT_FAILURE);
}
  • shmdt
    • 用途:解除映射;
    • 原型:int shmdt(const void *shmaddr);
    • 注意事项:
      • 正确的共享内存地址: shmdt 函数的参数 shmaddr 必须是先前通过 shmat 返回的地址。如果传递一个无效地址,shmdt 将会失败并返回 -1;
      • 多次分离: 确保每个 shmat 附加的共享内存地址只调用一次 shmdt 进行分离。如果多次调用 shmdt 进行分离,可能会导致未定义的行为或错误。
      • 清理资源: 分离共享内存段后,如果不再使用该共享内存段,应该调用 shmctl 进行清理,避免内存泄漏。特别是在进程终止之前,应确保所有附加的共享内存段都已正确分离和清理。
if (shmdt(shmaddr) == -1) {
	fprintf(stderr, "shmdt error, errno: %d, %s\n", errno, strerror(errno));
	exit(EXIT_FAILURE);
}
  • shmctl
    • 用途:用于控制共享内存段的各种操作,如获取共享内存段的状态、设置共享内存段的属性、删除共享内存段等;
    • 原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    • @param
      • int shmid: 共享内存段的标识符,由 shmget 返回。
      • int cmd: 要执行的操作命令。常用命令包括:
        • IPC_STAT: 获取共享内存段的状态,将信息存储在 buf 指向的 shmid_ds 结构体中。
        • IPC_SET: 设置共享内存段的属性,使用 buf 中的数据。
        • IPC_RMID: 删除共享内存段。
      • struct shmid_ds *buf: 指向一个 shmid_ds 结构体的指针,用于存储或设置共享内存段的信息。可以为 NULL,如果 cmd 不需要此参数。
/*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;   // 创建时间
    pid_t           shm_cpid;    // 创建者 PID
    pid_t           shm_lpid;    // 最后一个操作进程的 PID
    shmatt_t        shm_nattch;  // 当前附加到段的进程数
};

练习:设计三个程序,要求三个程序申请一块共享内存,并分别映射到各自进程的地址空间,进程A和进程B对共享内存段中的数据进行修改,然后进程C不断输出共享内存段中的数据,并观察效果,要求实现进程间的互斥,避免竞争。提示:信号

posted @ 2024-06-08 22:13  子非予  阅读(526)  评论(0)    收藏  举报