当前位置: 首页 > news >正文

解锁Linux共享内存:进程间通信的超高速通道(转)

在Linux系统的进程间通信 “江湖” 中,众多通信方式各显神通。管道,如同隐秘的地下通道,让有亲缘关系的进程能够悄然传递信息;消息队列则似邮局,进程可投递和接收格式化的消息包裹。然而,有一种通信方式却以其独特的 “高速” 特性脱颖而出,它就是共享内存。想象一下,进程们原本各自生活在独立的 “小天地” 里,有着自己专属的虚拟地址空间。但共享内存却如同神奇的 “任意门”,打破了进程间的隔阂,让多个进程能够直接访问同一块内存区域。这种独特的机制,使得数据在进程间的传递无需繁琐的复制过程,极大地提升了通信效率,堪称进程间通信的超高速通道。

在使用共享内存时,需要注意对于并发访问的控制,如使用锁或其他同步机制来保证数据的一致性和安全性。此外,还需要谨慎处理资源管理问题,确保正确地释放共享内存以避免内存泄漏。接下来,就让我们一同深入探索 Linux 共享内存的奥秘,揭开它神秘的面纱,看看它是如何在 Linux 系统中发挥这一独特且强大的作用 。

一、Linux内存管理初窥

1.1 虚拟内存与物理内存

Linux 采用虚拟内存管理机制,为每个进程分配独立的虚拟地址空间。这意味着每个进程都可以认为自己拥有 4GB(32 位系统)或更大(64 位系统)的连续内存空间,而不必担心物理内存的实际大小和其他进程的干扰。虚拟内存与物理内存通过内存映射机制建立联系,进程访问的虚拟地址会被转换为实际的物理地址。

举个例子,当你在 Linux 系统上同时运行多个程序时,每个程序都觉得自己独占了大量内存,但实际上物理内存是有限的。通过虚拟内存管理,操作系统可以巧妙地在物理内存和磁盘之间交换数据,使得系统能够运行比物理内存更大的程序集。就好比一个小型图书馆,虽然书架空间有限(物理内存),但通过一个庞大的仓库(磁盘)来存放暂时不用的书籍(数据),当读者需要某本书时,管理员(操作系统)会从仓库中取出并放到书架上供读者使用。

1.2 内存分页

为了更高效地管理内存,Linux 采用内存分页机制。将虚拟内存和物理内存按照固定大小的页(通常为 4KB)进行划分,页是内存管理的最小单位。操作系统通过维护页表来记录虚拟页和物理页之间的映射关系,当进程访问某个虚拟地址时,CPU 会根据页表将其转换为对应的物理地址。

想象一下,内存就像一本巨大的书籍,每一页都有固定的页码(虚拟页号和物理页号)。当你想要查找书中的某个内容(访问内存数据)时,通过目录(页表)可以快速定位到具体的页码,从而找到所需内容。

1.3 内存分配与回收

内存管理包括内存的分配和回收。当进程需要内存时,它会向操作系统请求分配内存,操作系统根据一定的算法从空闲内存中分配相应大小的内存块给进程;当进程不再需要某些内存时,它会将这些内存释放回操作系统,以便操作系统重新分配给其他需要的进程。

例如,当你在 Linux 系统上运行一个新的程序时,程序会向操作系统申请内存来存放代码和数据。操作系统会从空闲内存池中找到合适大小的内存块分配给该程序。当程序运行结束后,它占用的内存会被操作系统回收,重新加入空闲内存池,等待下一个程序的请求。

二、共享内存详解

2.1共享内存是什么

共享内存是一种高效的进程间通信(IPC,Inter - Process Communication)机制,它允许两个或多个进程直接访问同一块物理内存区域。简单来说,就好比多个房间(进程)都有一扇门可以直接通向同一个储物间(共享内存),大家可以直接在这个储物间里存放和取用物品(数据) 。

在 Linux 系统中,共享内存的实现依赖于操作系统的支持。当一个进程创建共享内存时,操作系统会在物理内存中分配一块区域,并为这块区域生成一个唯一的标识符。其他进程可以通过这个标识符将该共享内存映射到自己的虚拟地址空间中,从而实现对共享内存的访问。

2.2为什么要用共享内存

在进程间通信的众多方式中,共享内存之所以备受青睐,是因为它具有其他方式难以比拟的优势。

首先,与管道和消息队列等通信方式相比,共享内存的速度极快。管道和消息队列在数据传输时,需要进行多次数据拷贝,数据要在内核空间和用户空间之间来回传递,这会消耗大量的时间和系统资源。而共享内存则不同,多个进程直接访问同一块内存区域,数据不需要在不同进程的地址空间之间拷贝,大大减少了数据传输的开销,提高了通信效率。例如,在一个实时数据处理系统中,多个进程需要频繁地交换大量数据,如果使用管道或消息队列,可能会因为数据传输的延迟而影响系统的实时性;而使用共享内存,就可以快速地传递数据,满足系统对实时性的要求。

其次,共享内存的使用非常灵活。它可以用于任何类型的进程间通信,无论是有亲缘关系的进程(如父子进程)还是毫无关系的进程,都可以通过共享内存进行数据共享和交互。而且,共享内存区域可以存储各种类型的数据结构,开发者可以根据实际需求自定义数据格式,这为复杂应用场景的实现提供了便利。比如,在一个多进程协作的图形处理程序中,不同进程可以通过共享内存共享图像数据和处理参数,各自完成不同的处理任务,如一个进程负责图像的滤波处理,另一个进程负责图像的边缘检测,共享内存使得它们能够高效地协同工作。

此外,共享内存还能有效地节省内存资源。多个进程共享同一块内存区域,而不是每个进程都单独开辟一块内存来存储相同的数据,这在内存资源有限的情况下显得尤为重要。例如,在一个服务器系统中,可能同时有多个进程需要访问一些公共的配置信息或缓存数据,使用共享内存可以避免这些数据在每个进程中重复存储,从而提高内存的利用率。

2.3共享内存原理

共享内存是System V版本的最后一个进程间通信方式。共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

特别提醒:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量。

在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。

共享内存的通信原理示意图:

图片

对于上图我的理解是:当两个进程通过页表将虚拟地址映射到物理地址时,在物理地址中有一块共同的内存区,即共享内存,这块内存可以被两个进程同时看到。这样当一个进程进行写操作,另一个进程读操作就可以实现进程间通信。但是,我们要确保一个进程在写的时候不能被读,因此我们使用信号量来实现同步与互斥。

对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。

为什么共享内存速度最快?

借助上图说明:Proc A 进程给内存中写数据, Proc B 进程从内存中读取数据,在此期间一共发生了两次复制

(1)Proc A 到共享内存       
(2)共享内存到 Proc B

因为直接在内存上操作,所以共享内存的速度也就提高了。

三、共享内存使用指南

3.1关键函数全解析

在 Linux 中使用共享内存,离不开一些关键的系统调用函数,它们是我们操作共享内存的有力工具。

(1)shmget 函数:用于创建共享内存段或获取已存在的共享内存段的标识符。其函数原型为:

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

key:是一个用于标识共享内存段的键值,它就像是共享内存的 “门牌号”。通常可以使用ftok函数根据文件路径和项目 ID 生成一个唯一的key值。例如:

key_t key = ftok("/tmp/somefile", 1);

这里/tmp/somefile是一个已存在的文件路径,1 是项目 ID。如果key取值为IPC_PRIVATE,则会创建一个新的私有共享内存段,通常用于父子进程间的通信。

size:指定共享内存段的大小,单位是字节。例如,若要创建一个 1024 字节大小的共享内存段,可以这样设置:

int shmid = shmget(key, 1024, IPC_CREAT | 0666);
  • shmflg:是一组标志位,常用的标志包括IPC_CREAT(如果共享内存不存在则创建)和IPC_EXCL(与IPC_CREAT一起使用,确保创建新的共享内存段,若已存在则报错)。权限设置与文件权限类似,如0666表示所有者、组和其他用户都有读写权限 。如果shmget函数执行成功,会返回一个非负整数,即共享内存段的标识符shmid;若失败,则返回 -1。

(2)shmat 函数:将共享内存段连接到调用进程的地址空间,使得进程可以访问共享内存中的数据。

其函数原型为:

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid:是由shmget函数返回的共享内存标识符。

  • shmaddr:指定共享内存连接到当前进程中的地址位置,通常设置为NULL,表示让系统自动选择合适的地址。例如:

void *shared_mem = shmat(shmid, NULL, 0);

shmflg:通常为 0,表示默认的连接方式。如果设置了SHM_RDONLY,则以只读方式连接共享内存。如果shmat函数调用成功,会返回一个指向共享内存起始地址的指针;若失败,返回(void *)-1。

(3)shmdt 函数:用于将共享内存段从当前进程的地址空间中分离。函数原型为:

int shmdt(const void *shmaddr);

shmaddr:是shmat函数返回的共享内存起始地址。调用该函数后,进程不再能够访问该共享内存,但共享内存本身并不会被删除。例如:

int result = shmdt(shared_mem);
if (result == -1) {perror("shmdt failed");
}

如果分离成功,shmdt返回 0;若失败,返回 -1。

(4)shmctl 函数:用于对共享内存进行控制操作,如获取共享内存信息、设置权限、删除共享内存等。函数原型为:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid:共享内存标识符。

  • cmd:指定要执行的控制命令,常用的命令有IPC_STAT(获取共享内存的状态信息,存入buf指向的结构体)、IPC_SET(设置共享内存的状态信息,如权限等,从buf指向的结构体中获取设置值)和IPC_RMID(删除共享内存段)。

  • buf:是一个指向shmid_ds结构体的指针,用于传递或获取共享内存的相关信息。当cmd为IPC_RMID时,buf通常设置为NULL。例如,删除共享内存段的操作如下:

int result = shmctl(shmid, IPC_RMID, NULL);
if (result == -1) {perror("shmctl IPC_RMID failed");
}

如果操作成功,shmctl返回 0;若失败,返回 -1。

3.2代码实战:共享内存的读写操作

下面通过一个完整的代码示例,展示如何在两个进程间使用共享内存进行数据读写。假设我们要在一个进程中写入数据,另一个进程读取这些数据。

首先,定义一个数据结构,用于在共享内存中存储数据。这里我们定义一个简单的结构体,包含一个整数和一个字符数组:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>#define SHM_SIZE 1024// 定义共享内存中使用的数据结构
typedef struct {int num;char text[100];
} SharedData;int main() {int shmid;key_t key;SharedData *shared_data;// 生成唯一的key值key = ftok(".", 'a');if (key == -1) {perror("ftok");exit(EXIT_FAILURE);}// 创建共享内存段shmid = shmget(key, sizeof(SharedData), IPC_CREAT | 0666);if (shmid == -1) {perror("shmget");exit(EXIT_FAILURE);}// 将共享内存连接到当前进程的地址空间shared_data = (SharedData *)shmat(shmid, NULL, 0);if (shared_data == (SharedData *)-1) {perror("shmat");exit(EXIT_FAILURE);}// 写入数据到共享内存shared_data->num = 42;strcpy(shared_data->text, "Hello, shared memory!");printf("Data written to shared memory: num = %d, text = %s\n", shared_data->num, shared_data->text);// 分离共享内存if (shmdt(shared_data) == -1) {perror("shmdt");exit(EXIT_FAILURE);}return 0;
}

上述代码中,首先使用ftok函数生成一个key值,然后通过shmget创建一个共享内存段,其大小为SharedData结构体的大小。接着使用shmat将共享内存连接到当前进程地址空间,向共享内存中写入数据,最后使用shmdt分离共享内存。

下面是读取共享内存数据的代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>#define SHM_SIZE 1024// 定义共享内存中使用的数据结构
typedef struct {int num;char text[100];
} SharedData;int main() {int shmid;key_t key;SharedData *shared_data;// 生成唯一的key值,必须与写入进程一致key = ftok(".", 'a');if (key == -1) {perror("ftok");exit(EXIT_FAILURE);}// 获取已存在的共享内存段shmid = shmget(key, sizeof(SharedData), 0666);if (shmid == -1) {perror("shmget");exit(EXIT_FAILURE);}// 将共享内存连接到当前进程的地址空间shared_data = (SharedData *)shmat(shmid, NULL, 0);if (shared_data == (SharedData *)-1) {perror("shmat");exit(EXIT_FAILURE);}// 从共享内存读取数据printf("Data read from shared memory: num = %d, text = %s\n", shared_data->num, shared_data->text);// 分离共享内存if (shmdt(shared_data) == -1) {perror("shmdt");exit(EXIT_FAILURE);}// 删除共享内存段(这里仅演示,实际应用中需谨慎操作)if (shmctl(shmid, IPC_RMID, NULL) == -1) {perror("shmctl IPC_RMID");exit(EXIT_FAILURE);}return 0;
}

在读取代码中,同样先使用ftok生成与写入进程相同的key值,然后通过shmget获取共享内存段(注意这里没有使用IPC_CREAT标志,因为共享内存已经由写入进程创建),接着连接共享内存并读取数据,最后分离共享内存并删除共享内存段(在实际应用中,删除共享内存段的操作需要谨慎考虑,确保没有其他进程再使用该共享内存)。

3.3模拟共享内存

我们用server来创建共享存储段,用client获取共享存储段的标识符,二者关联起来之后server将数据写入共享存储段,client从共享区读取数据。通信结束之后server与client断开与共享区的关联,并由server释放共享存储段。

comm.h

//comm.h
#ifndef _COMM_H__
#define _COMM_H__#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>#define PATHNAME "."
#define PROJ_ID 0x6666int CreateShm(int size);
int DestroyShm(int shmid);
int GetShm(int size);
#endif

comm.c

//comm.c
#include"comm.h"static int CommShm(int size,int flags)
{key_t key = ftok(PATHNAME,PROJ_ID);if(key < 0){perror("ftok");return -1;}int shmid = 0;if((shmid = shmget(key,size,flags)) < 0){perror("shmget");return -2;}return shmid;
}
int DestroyShm(int shmid)
{if(shmctl(shmid,IPC_RMID,NULL) < 0){perror("shmctl");return -1;}return 0;
}
int CreateShm(int size)
{return CommShm(size,IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm(int size)
{return CommShm(size,IPC_CREAT);
}

client.c

//client.c
#include"comm.h"int main()
{int shmid = GetShm(4096);sleep(1);char *addr = shmat(shmid,NULL,0);sleep(2);int i = 0;while(i < 26){addr[i] = 'A' + i;i++;addr[i] = 0;sleep(1);}shmdt(addr);sleep(2);return 0;
}

server.c

//server.c
#include"comm.h"int main()
{int shmid = CreateShm(4096);char *addr = shmat(shmid,NULL,0);sleep(2);int i = 0;while(i++ < 26){printf("client# %s\n",addr);sleep(1);}shmdt(addr);sleep(2);DestroyShm(shmid);return 0;
}

Makefile

//Makefile
.PHONY:all
all:server clientclient:client.c comm.cgcc -o $@ $^
server:server.c comm.cgcc -o $@ $^.PHONY:clean
clean:
rm -f client server

运行结果:

图片

  • 优点:我们可以看到使用共享内存进行进程之间的通信是非常方便的,而且函数的接口也比较简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,加快了程序的效率。

  • 缺点:共享内存没有提供同步机制,这使得我们在使用共享内存进行进程之间的通信时,往往需要借助其他手段来保证进程之间的同步工作。

3.4权限与生命周期管理

权限设置:在创建共享内存时,可以通过shmget函数的shmflg参数设置共享内存的访问权限。权限设置与文件权限类似,使用三位八进制数表示,分别对应所有者、组和其他用户的读、写、执行权限。例如,0666表示所有者、组和其他用户都有读写权限;0644表示所有者有读写权限,组和其他用户只有读权限。合理的权限设置可以保证共享内存的安全性,防止未经授权的进程访问或修改共享内存中的数据。比如,在一个多用户的服务器环境中,如果有一些共享内存用于存储敏感数据,就需要严格设置权限,只允许特定的用户或用户组访问。

生命周期管理:共享内存的生命周期独立于使用它的进程。当最后一个使用共享内存的进程将其分离(调用shmdt)后,共享内存仍然存在于系统中,直到被显式删除(调用shmctl并传入IPC_RMID命令)或系统重启。这就需要开发者在使用共享内存时,谨慎管理其生命周期。在程序结束时,应该确保及时删除不再使用的共享内存,以避免内存泄漏和资源浪费。

比如,在一个长期运行的服务器程序中,如果不断创建共享内存而不删除,随着时间的推移,系统中会残留大量无用的共享内存,占用系统资源,影响系统性能。同时,在删除共享内存之前,要确保所有使用该共享内存的进程都已经将其分离,否则可能会导致其他进程访问非法内存地址,引发程序崩溃等问题。

四、深入共享内存的实现原理

4.1内核视角:共享内存的数据结构

在 Linux 内核中,有几个关键的数据结构用于管理共享内存,其中struct shmid_kernel和struct shmid_ds起着重要作用。

struct shmid_kernel是内核中用于表示共享内存对象的内部数据结构,它包含了共享内存的各种属性和状态信息。虽然这个结构体对于普通开发者来说并不直接可见,但了解它有助于深入理解共享内存的工作机制。它记录了共享内存段的大小、所属的进程组、创建时间、最后访问时间等重要信息。例如,通过这个结构体,内核可以跟踪共享内存的使用情况,判断哪些进程正在使用它,以及何时需要回收共享内存资源。

而struct shmid_ds则是一个更常用的数据结构,开发者可以通过shmctl函数来访问和修改这个结构体中的信息。它的定义如下:

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;    /* 创建共享内存的进程ID */pid_t           shm_lpid;    /* 最后一次执行shmat或shmdt操作的进程ID */shmatt_t        shm_nattch;  /* 当前连接到共享内存的进程数 */...
};
  • shm_perm:包含了共享内存的所有权和权限信息,如所有者 ID、组 ID、访问权限等,类似于文件的权限管理。例如,通过设置shm_perm中的权限位,可以控制哪些进程可以访问共享内存,以及以何种方式(读、写等)访问。

  • shm_segsz:明确了共享内存段的大小,以字节为单位。在创建共享内存时,开发者需要根据实际需求指定合适的大小。比如,在一个简单的进程间通信场景中,如果只是传递少量的状态信息,可能只需要分配几十或几百字节的共享内存;而在一个需要共享大量数据的场景中,如共享视频帧数据,可能需要分配几兆甚至更大的共享内存空间。

  • shm_atime、shm_dtime和shm_ctime:分别记录了共享内存的连接时间、分离时间和状态改变时间。这些时间戳对于调试和性能分析非常有帮助,例如,通过查看shm_atime和shm_dtime,可以了解进程对共享内存的使用时间间隔,判断是否存在长时间占用共享内存而不释放的情况;shm_ctime则可以帮助开发者追踪共享内存的状态变化历史。

  • shm_cpid和shm_lpid:记录了创建共享内存的进程 ID 和最后一次执行shmat或shmdt操作的进程 ID。这对于调试和管理共享内存的使用非常有用,当出现共享内存相关的问题时,可以通过这些 ID 来追溯问题的源头,查看是哪个进程创建了共享内存,以及最近哪些进程对共享内存进行了连接或分离操作。

  • shm_nattch:表示当前连接到共享内存的进程数。内核通过这个字段来管理共享内存的生命周期,当shm_nattch变为 0 时,并且没有其他进程持有对该共享内存的引用,内核可以考虑回收该共享内存资源。例如,在一个多进程协作的服务器程序中,当所有使用共享内存的进程都完成任务并与共享内存分离后,shm_nattch变为 0,此时内核可以及时释放共享内存,避免内存资源的浪费。

4.2映射机制:虚拟内存与物理内存的桥梁

共享内存能够实现高效的进程间通信,关键在于其巧妙的内存映射机制,通过页表将虚拟内存映射到物理内存。

在 Linux 系统中,每个进程都有自己独立的虚拟地址空间。当进程创建或连接到共享内存时,操作系统会在进程的虚拟地址空间中分配一段虚拟地址范围,并将这段虚拟地址与共享内存所在的物理内存区域建立映射关系。这个映射关系是通过页表来维护的。

页表是一种数据结构,它记录了虚拟页号(VPN,Virtual Page Number)与物理页号(PPN,Physical Page Number)之间的对应关系。当进程访问共享内存中的数据时,CPU 首先会根据当前进程的页表,将虚拟地址中的虚拟页号转换为物理页号,然后再加上页内偏移量,得到实际的物理内存地址,从而访问到共享内存中的数据。

例如,假设进程 A 和进程 B 共享一块大小为 4KB 的共享内存。当进程 A 创建共享内存时,操作系统会在物理内存中分配一块 4KB 大小的内存区域,并为这块区域分配一个物理页号。然后,操作系统在进程 A 的页表中创建一个页表项,将虚拟页号与该物理页号关联起来,使得进程 A 可以通过虚拟地址访问这块共享内存。当进程 B 连接到该共享内存时,操作系统同样在进程 B 的页表中创建一个页表项,将其虚拟地址空间中的一段虚拟页号也映射到相同的物理页号上。这样,进程 A 和进程 B 就可以通过各自的虚拟地址访问同一块物理内存区域,实现数据共享。

在这个过程中,如果所需的共享内存数据不在物理内存中(例如,由于内存不足,共享内存的部分数据被交换到磁盘上),会发生页面错误(page fault)。此时,操作系统会负责将所需的数据从磁盘读入物理内存,并更新页表,确保进程能够正确访问共享内存。这种动态的内存管理机制使得共享内存能够在有限的物理内存条件下高效运行,同时也保证了进程间通信的稳定性和可靠性。

Linux提供了内存映射函数mmap, 它把文件内容映射到一段内存上(准确说是虚拟内存上,运行着进程), 通过对这段内存的读取和修改, 实现对文件的读取和修改。mmap()系统调用使得进程之间可以通过映射一个普通的文件实现共享内存。普通文件映射到进程地址空间后,进程可以像访问内存的方式对文件进行访问,不需要其他内核态的系统调用(read,write)去操作。

这里是讲设备或者硬盘存储的一块空间映射到物理内存,然后操作这块物理内存就是在操作实际的硬盘空间,不需要经过内核态传递。比如你的硬盘上有一个文件,你可以使用linux系统提供的mmap接口,将这个文件映射到进程一块虚拟地址空间,这块空间会对应一块物理内存,当你读写这块物理空间的时候,就是在读取实际的磁盘文件,就是这么直接高效。通常诸如共享库的加载都是通过内存映射的方式加载到物理内存的。

mmap系统调用并不完全是为了共享内存来设计的,它本身提供了不同于一般对普通文件的访问的方式,进程可以像读写内存一样对普通文件进行操作,IPC的共享内存是纯粹为了共享。

内存映射指的是将 :进程中的1个虚拟内存区域 & 1个磁盘上的对象,使得二者存在映射关系。当然,也可以多个进程同时映射到一个对象上面。

实现过程

  • 内存映射的实现过程主要是通过Linux系统下的系统调用函数:mmap()

  • 该函数的作用 = 创建虚拟内存区域 + 与共享对象建立映射关系

其函数原型、具体使用 & 内部流程 如下:

/** * 函数原型 */ 
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);/** 
* 具体使用(用户进程调用mmap()) 
* 下述代码即常见了一片大小 = MAP_SIZE的接收缓存区 & 关联到共享对象中(即建立映射) 
*/ mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0); 
/** 
* 内部原理 
* 步骤1:创建虚拟内存区域 
* 步骤2:实现地址映射关系,即:进程的虚拟地址空间 ->> 共享对象 
* 注:
* a. 此时,该虚拟地址并没有任何数据关联到文件中,仅仅只是建立映射关系 
* b. 当其中1个进程对虚拟内存写入数据时,则真正实现了数据的可见 
*/

优点

进程在读写磁盘的时候,大概的流程是:

以write 为例:进程(用户空间) -> 系统调用,进入内核 -> 将要写入的数据从用户空间拷贝到内核空间的缓存区 -> 调用磁盘驱动 -> 写在磁盘上面。

使用mmap之后进程(用户空间)--> 读写映射的内存 --> 写在磁盘上面。(这样的优点是 避免了频繁的进入内核空间,进行系统调用,提高了效率)

(1)mmap系统调用

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

这就是mmap系统调用的接口,mmap函数成功返回指向内存区域的指针,失败返回MAP_FAILED。

  • addr,某个特定的地址作为起始地址,当被设置为NULL,标识系统自动分配地址。实实在在的物理区域。

  • length说的是内存段的长度。

  • prot是用来设定内存段的访问权限。

PROT_READ	内存段可读
PROT_WRITE	内存段可写
PROT_EXEC	内存段可执行
PROT_NONE	内存段不能被访问

flags参数控制内存段内容被修改以后程序的行为。

MAP_SHARED	进程间共享内存,对该内存段修改反映到映射文件中。提供了POSIX共享内存
MAP_PRIVATE	内存段为调用进程所私有。对该内存段的修改不会反映到映射文件
MAP_ANNOYMOUS	这段内存不是从文件映射而来的。内容被初始化为全0
MAP_FIXED	内存段必须位于start参数指定的地址处,start必须是页大小的整数倍(4K整数倍)
MAP_HUGETLB	按照大内存页面来分配内存空间

fd参数是用来被映射文件对应的文件描述符,通过open系统调用得到,offset设定从何处进行映射。

(2)mmap用于共享内存的方式

  1. 我们可以使用普通文件进行提供内存映射,例如,open系统调用打开一个文件,然后进行mmap操作,得到共享内存,这种方式适用于任何进程之间。

  2. 可以使用特殊文件进行匿名内存映射,这个相对的是具有血缘关系的进程之间,当父进程调用mmap,然后进行fork,这样父进程创建的子进程会继承父进程匿名映射后的地址空间,这样,父子进程之间就可以进行通信了。相当于是mmap的返回地址此时是父子进程同时来维护。

  3. 另外POSIX版本的共享内存底层也是使用了mmap。所以,共享内存在在posix上一定程度上就是指的内存映射了。

五、Mmap和System V共享内存的比较

共享内存:

图片

这是System V版本的共享内存(以下我们统称为shm),下面看下mmap的:

图片

mmap是在磁盘上建立一个文件,每个进程地址空间中开辟出一块空间进行映射。而shm共享内存,每个进程最终会映射到同一块物理内存。shm保存在物理内存,这样读写的速度肯定要比磁盘要快,但是存储量不是特别大,相对于shm来说,mmap更加简单,调用更加方便,所以这也是大家都喜欢用的原因。

另外mmap有一个好处是当机器重启,因为mmap把文件保存在磁盘上,这个文件还保存了操作系统同步的映像,所以mmap不会丢失,但是shmget在内存里面就会丢失,总之,共享内存是在内存中创建空间,每个进程映射到此处。内存映射是创建一个文件,并且映射到每个进程开辟的空间中,但在posix中的共享内存就是指这种使用文件的方式“内存映射”。

六、POSIX共享内存

6.1 IPC机制

共享内存是最快的可用IPC形式。它允许多个不相关(无亲缘关系)的进程去访问同一部分逻辑内存。

如果需要在两个进程之间传输数据,共享内存将是一种效率极高的解决方案。一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传输就不再涉及内核。这样就可以减少系统调用时间,提高程序效率。

共享内存是由IPC为一个进程创建的一个特殊的地址范围,它将出现在进程的地址空间中。其他进程可以把同一段共享内存段“连接到”它们自己的地址空间里去。所有进程都可以访问共享内存中的地址。如果一个进程向这段共享内存写了数据,所做的改动会立刻被有访问同一段共享内存的其他进程看到。

要注意的是共享内存本身没有提供任何同步功能。也就是说,在第一个进程结束对共享内存的写操作之前,并没有什么自动功能能够预防第二个进程开始对它进行读操作。共享内存的访问同步问题必须由程序员负责。可选的同步方式有互斥锁、条件变量、读写锁、纪录锁、信号灯。

实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止。

6.2 POSIX共享内存API

使用POSIX共享内存需要用到下面这些API:

#include <sys/types.h>
#include <sys/stat.h>        /* For mode constants */
#include <sys/mman.h>
#include <fcntl.h>           /* For O_* constants */
#include <unistd.h>
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
int ftruncate(int fildes, off_t length);
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);
int munmap(void *addr, size_t len);
int close(int fildes);
int fstat(int fildes, struct stat *buf);
int fchown(int fildes, uid_t owner, gid_t group);
int fchmod(int fildes, mode_t mode);
  • shm_open:穿件并打开一个新的共享内存对象或者打开一个既存的共享内存对象, 与函数open的用法是类似的函数返回值是一个文件描述符,会被下面的API使用。

  • ftruncate:设置共享内存对象的大小,新创建的共享内存对象大小为0。

  • mmap:将共享内存对象映射到调用进程的虚拟地址空间。

  • munmap:取消共享内存对象到调用进程的虚拟地址空间的映射。

  • shm_unlink:删除一个共享内存对象名字。

  • close:当shm_open函数返回的文件描述符不再使用时,使用close函数关闭它。

  • fstat:获得共享内存对象属性的stat结构体. 结构体中会包含共享内存对象的大小(st_size), 权限(st_mode), 所有者(st_uid), 归属组 (st_gid)。

  • fchown:改变一个共享内存对象的所有权。

  • fchmod:改变一个共享内存对象的权限。

七、共享内存的同步问题

虽然共享内存为进程间通信提供了高效的数据共享方式,但由于多个进程可以同时访问同一块内存区域,这就带来了同步和互斥的问题。如果没有合适的同步机制,可能会出现以下情况:

  • 竞态条件(Race Condition):当多个进程同时访问和修改共享内存中的数据时,由于进程执行的先后顺序不确定,可能导致最终的数据结果不可预测。例如,有两个进程 P1 和 P2 同时读取共享内存中的一个整数变量 count,然后各自对其加 1,最后再写回共享内存。如果没有同步机制,可能会出现 P1 和 P2 读取到相同的 count 值,然后各自加 1 后写回,这样 count 只增加了 1,而不是预期的 2 。

  • 数据不一致性:一个进程正在对共享内存中的数据进行修改时,另一个进程可能同时读取这些未完全修改的数据,从而导致数据不一致。比如,一个进程正在更新共享内存中的一个复杂数据结构,在更新过程中,另一个进程读取该数据结构,可能会读到部分更新的数据,使数据处于不一致的状态,进而导致程序出现错误。

    解决方案:信号量与互斥锁的应用

    为了解决共享内存带来的同步和互斥问题,通常会使用信号量(Semaphore)和互斥锁(Mutex)等同步机制。

    (1)信号量:信号量是一种计数器,用于控制对共享资源的访问。它可以用来实现进程间的同步和互斥。在共享内存的场景中,信号量可以用来控制对共享内存的访问权限。例如,我们可以创建一个信号量,初始值设为 1,表示共享内存资源可用。当一个进程要访问共享内存时,它首先尝试获取信号量(通过对信号量执行 P 操作,即减 1 操作)。如果信号量的值大于等于 0,说明资源可用,进程可以继续执行对共享内存的访问操作;如果信号量的值小于 0,说明资源已被其他进程占用,该进程会被阻塞,直到信号量的值大于等于 0。当进程完成对共享内存的访问后,它会释放信号量(通过对信号量执行 V 操作,即加 1 操作),通知其他进程可以访问共享内存。在 Linux 中,有 POSIX 有名信号量、POSIX 无名信号量和 System V 信号量等不同类型,开发者可以根据具体需求选择使用。例如,使用 POSIX 有名信号量实现共享内存同步的代码示例如下:

    #include <stdio.h>
    #include <stdlib.h>
    #include <semaphore.h>
    #include <sys/mman.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/stat.h>#define SHM_SIZE 1024
    #define SEM_NAME "/my_semaphore"int main() {int shm_fd;void *shared_memory;sem_t *sem;// 创建共享内存对象shm_fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);if (shm_fd == -1) {perror("shm_open");exit(1);}// 配置共享内存大小if (ftruncate(shm_fd, SHM_SIZE) == -1) {perror("ftruncate");exit(1);}// 将共享内存映射到进程地址空间shared_memory = mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);if (shared_memory == MAP_FAILED) {perror("mmap");exit(1);}// 创建信号量sem = sem_open(SEM_NAME, O_CREAT, 0666, 1);if (sem == SEM_FAILED) {perror("sem_open");exit(1);}// 等待信号量,获取共享内存访问权限if (sem_wait(sem) == -1) {perror("sem_wait");exit(1);}// 访问共享内存printf("Accessed shared memory: %s\n", (char *)shared_memory);// 释放信号量,允许其他进程访问共享内存if (sem_post(sem) == -1) {perror("sem_post");exit(1);}// 取消映射并关闭共享内存if (munmap(shared_memory, SHM_SIZE) == -1) {perror("munmap");exit(1);}if (close(shm_fd) == -1) {perror("close");exit(1);}// 删除共享内存对象if (shm_unlink("/my_shared_memory") == -1) {perror("shm_unlink");exit(1);}// 关闭并删除信号量if (sem_close(sem) == -1) {perror("sem_close");exit(1);}if (sem_unlink(SEM_NAME) == -1) {perror("sem_unlink");exit(1);}return 0;
    }

    (2)互斥锁:互斥锁是一种二元信号量,用于保证在同一时刻只有一个进程能够访问共享资源,即实现对共享内存的互斥访问。当一个进程获取到互斥锁后,其他进程如果试图获取该互斥锁,会被阻塞,直到持有互斥锁的进程释放它。在 Linux 中,使用 pthread 库中的互斥锁相关函数来实现互斥锁的操作。例如,初始化互斥锁可以使用pthread_mutex_init函数,获取互斥锁使用pthread_mutex_lock函数,释放互斥锁使用pthread_mutex_unlock函数,销毁互斥锁使用pthread_mutex_destroy函数。以下是使用互斥锁实现共享内存同步的简单代码示例:

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <sys/mman.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/stat.h>#define SHM_SIZE 1024typedef struct {pthread_mutex_t mutex;char data[SHM_SIZE];
    } SharedData;int main() {int shm_fd;SharedData *shared_data;// 创建共享内存对象shm_fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);if (shm_fd == -1) {perror("shm_open");exit(1);}// 配置共享内存大小if (ftruncate(shm_fd, sizeof(SharedData)) == -1) {perror("ftruncate");exit(1);}// 将共享内存映射到进程地址空间shared_data = (SharedData *)mmap(0, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);if (shared_data == MAP_FAILED) {perror("mmap");exit(1);}// 初始化互斥锁pthread_mutexattr_t attr;pthread_mutexattr_init(&attr);pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);if (pthread_mutex_init(&shared_data->mutex, &attr) != 0) {perror("pthread_mutex_init");exit(1);}// 获取互斥锁,访问共享内存if (pthread_mutex_lock(&shared_data->mutex) != 0) {perror("pthread_mutex_lock");exit(1);}printf("Accessed shared memory: %s\n", shared_data->data);// 释放互斥锁if (pthread_mutex_unlock(&shared_data->mutex) != 0) {perror("pthread_mutex_unlock");exit(1);}// 取消映射并关闭共享内存if (munmap(shared_data, sizeof(SharedData)) == -1) {perror("munmap");exit(1);}if (close(shm_fd) == -1) {perror("close");exit(1);}// 删除共享内存对象if (shm_unlink("/my_shared_memory") == -1) {perror("shm_unlink");exit(1);}return 0;
    }

    通过合理使用信号量和互斥锁等同步机制,可以有效地解决共享内存带来的同步和互斥问题,确保多个进程能够安全、高效地共享内存数据。

    八、实际应用场景及常见问题解答

    8.1实际应用场景

    (1)数据库缓存优化

    在数据库系统中,共享内存发挥着至关重要的作用,尤其是在缓存优化方面。以 Oracle 数据库为例,它使用共享全局区(SGA,Shared Global Area)来实现共享内存。SGA 是一个共享的内存结构,用于存储数据块、SQL 语句和其他共享信息 。

    当数据库接收到查询请求时,首先会在共享内存的缓存中查找相关数据。如果数据存在于缓存中,即命中缓存,数据库可以直接从共享内存中读取数据并返回给用户,这大大减少了磁盘 I/O 操作。因为从磁盘读取数据的速度远远低于从内存读取数据的速度,通过共享内存缓存数据,可以显著提高查询性能。例如,在一个高并发的在线交易系统中,大量用户频繁查询订单信息。如果没有共享内存缓存,每次查询都需要从磁盘读取数据,磁盘 I/O 很快就会成为系统的瓶颈,导致查询响应时间变长。而使用共享内存缓存订单数据后,大部分查询可以直接从内存中获取数据,大大提高了系统的响应速度和吞吐量。

    同时,共享内存还可以减少内存的重复使用,提高内存利用率。多个数据库进程可以共享同一块内存区域,避免了每个进程都单独开辟内存来存储相同的数据,从而节省了内存资源。比如,在一个包含多个数据库实例的系统中,这些实例可以共享 SGA 中的数据缓存,减少了内存的浪费,使得系统能够在有限的内存资源下高效运行。

    (2)高性能计算中的数据共享

    在高性能计算领域,共享内存同样有着广泛的应用。在大规模的科学计算和工程模拟中,往往需要处理海量的数据和复杂的计算任务,这些任务通常需要多个处理器核心或多个计算节点协同工作。

    以分子动力学模拟为例,这是一种用于研究分子系统微观行为的计算方法,需要对大量分子的运动轨迹进行模拟计算。在计算过程中,不同的处理器核心需要共享分子的初始位置、速度等数据,以及模拟过程中的中间结果。通过共享内存,这些数据可以被多个处理器核心直接访问,避免了数据在不同处理器之间通过网络或其他方式传输的开销,提高了计算效率。

    再比如,在气象预报模型中,需要对全球范围内的气象数据进行分析和预测。这些数据量巨大,计算任务复杂,通常会在分布式计算集群上进行。共享内存可以用于在不同计算节点之间共享气象数据和计算参数,使得各个节点能够协同工作,共同完成气象预报的计算任务。在这种场景下,共享内存不仅提高了数据共享的效率,还减少了节点之间的通信开销,对于提高整个高性能计算系统的性能起着关键作用。

    7.2避坑指南与常见问题解答

    在使用 Linux 共享内存的过程中,开发者常常会遇到一些棘手的问题,下面我们就来总结一下这些常见问题,并给出相应的解决方案。

    (1)共享内存创建失败

    ①问题描述:调用shmget函数创建共享内存时,返回值为 -1,导致创建失败。

    ②可能原因

    • 系统资源限制:系统对共享内存的数量和大小有限制,如SHMMAX(单个共享内存段的最大大小)和SHMMNI(系统中共享内存段的最大数量)等参数。如果要创建的共享内存超过了这些限制,就会导致创建失败。例如,当系统的SHMMAX设置为 32MB,而你尝试创建一个 64MB 的共享内存段时,就会失败。

    • 权限不足:创建共享内存需要适当的权限。如果当前用户没有足够的权限(如在一些安全限制较严格的系统中),shmget调用也会失败。比如,普通用户在没有特殊权限配置的情况下,无法创建共享内存。

    ③解决方案

    检查系统参数:通过cat /proc/sys/kernel/shmmax和cat /proc/sys/kernel/shmmni等命令查看系统的共享内存参数设置。如果需要,可以通过修改/etc/sysctl.conf文件来调整这些参数,例如:

    echo "kernel.shmmax = 2147483648" >> /etc/sysctl.conf
    sysctl -p

    上述命令将SHMMAX设置为 2GB,并使其立即生效。

    ④确认权限:确保当前用户具有创建共享内存的权限,必要时可以切换到具有足够权限的用户(如 root 用户)来创建共享内存,或者通过修改文件权限和用户组等方式来赋予相应权限。

    (2)共享内存访问异常

    ①问题描述:在进程访问共享内存时,出现段错误(Segmentation Fault)或其他访问异常。

    ②可能原因

    • 未正确映射共享内存:调用shmat函数时,可能由于参数设置错误,导致共享内存没有正确映射到进程的地址空间。例如,shmat返回的指针为(void *)-1,表示映射失败,但程序没有正确处理这种情况,仍然尝试使用该指针访问共享内存,就会导致访问异常。

    • 内存越界访问:在对共享内存进行读写操作时,没有正确检查边界条件,导致访问超出了共享内存的范围。比如,共享内存大小为 1024 字节,而程序尝试写入 2048 字节的数据,就会造成内存越界。

    • 同步问题:多个进程同时访问共享内存时,如果没有正确的同步机制(如信号量、互斥锁等),可能会导致数据竞争和访问冲突,进而引发访问异常。例如,一个进程正在修改共享内存中的数据,另一个进程同时读取这些未完全修改的数据,就可能导致数据不一致和访问错误。

    ③解决方案

    检查映射结果:在调用shmat后,仔细检查返回值。如果返回(void *)-1,则根据errno变量的值进行错误处理,例如:

    void *shared_mem = shmat(shmid, NULL, 0);
    if (shared_mem == (void *)-1) {perror("shmat failed");exit(EXIT_FAILURE);
    }
    • 边界检查:在对共享内存进行读写操作时,务必进行严格的边界检查,确保不会越界访问。例如,在写入数据时,要检查数据大小是否超过共享内存的剩余空间;在读取数据时,要确保读取的长度不超过共享内存的有效范围。

    • 完善同步机制:引入合适的同步机制,如使用信号量或互斥锁来确保对共享内存的访问是安全的。在访问共享内存之前,先获取同步锁(如信号量的 P 操作或互斥锁的加锁操作),访问完成后再释放同步锁(如信号量的 V 操作或互斥锁的解锁操作)。

    (3)共享内存未及时释放

    ①问题描述:共享内存不再被使用,但没有被及时删除,导致系统资源浪费。

    ②可能原因

    • 程序逻辑错误:在程序中没有正确处理共享内存的生命周期,例如没有在合适的时机调用shmctl函数并传入IPC_RMID命令来删除共享内存。

    • 进程异常退出:使用共享内存的进程由于某种原因(如程序崩溃、收到异常信号等)异常退出,而没有来得及执行共享内存的删除操作。

    ③解决方案

    优化程序逻辑:在程序设计时,明确共享内存的生命周期,确保在不再需要共享内存时,及时调用shmctl函数删除共享内存。例如,在程序结束时,添加如下代码:

    if (shmctl(shmid, IPC_RMID, NULL) == -1) {perror("shmctl IPC_RMID failed");exit(EXIT_FAILURE);
    }

    捕获异常信号:在进程中捕获常见的异常信号(如SIGSEGV、SIGABRT等),在信号处理函数中添加释放共享内存的操作。例如,使用signal函数注册信号处理函数:

    #include <signal.h>void cleanup_shared_memory(int signum) {// 释放共享内存相关资源if (shmctl(shmid, IPC_RMID, NULL) == -1) {perror("shmctl IPC_RMID in signal handler failed");}exit(EXIT_FAILURE);
    }int main() {// 注册信号处理函数signal(SIGSEGV, cleanup_shared_memory);signal(SIGABRT, cleanup_shared_memory);// 其他程序逻辑
    }

    通过上述方法,可以有效避免共享内存未及时释放的问题,提高系统资源的利用率。

    相关文章:

  • 今日行情明日机会——20250425
  • 什么是CN2专线?全面解析中国电信的高性能网络服务
  • Android Studio开发中Application和Activity生命周期详解
  • MySQL基础——聚合查询(全面解析)
  • 入侵检测系统(IDS)与入侵防御系统(IPS):功能对比与部署实践
  • Linux-06 ubuntu 系统截图软件使用简单记录
  • 大型工程里 AI 编码困境重重,未来如何破局?
  • CodeMeter Runtime 安装失败排查与解决指南
  • Operating System 实验二 内存管理实验
  • 华硕NUC产品闪耀第31届中国国际广播电视信息网络展览会
  • 打造高功率、高电流和高可靠性电路板的厚铜PCB生产
  • Unity Shader Properties详解
  • 项目实战-基于大数据分析的暖通系统改造模型【感谢Akila公司以及学院的支持】
  • 如何通过电路测量运放的增益带宽积(GBP)和压摆率(SR)
  • 深入 Vue 核心:通信、生命周期与 API 的全面解析
  • 在 Ubuntu 22.04|20.04|18.04 上安装 PostgreSQL 13
  • Pycharm(三):梯度下降法
  • 【AI平台】n8n入门3:第二个工作流,链接网上大模型(含三种方式)
  • SQL语句练习 自学SQL网 基础查询
  • 各类前端开发的框架比较及其核心特性、开发体验、生态系统以及在不同项目中的适用性
  • 网警侦破特大“刷量引流”网络水军案:涉案金额达2亿余元
  • 上海第三家“胖永辉”在浦东开业,设立了外贸产品专区
  • 凝聚多方力量,中国农科院油菜产业专家团部署单产提升新任务
  • 四川甘孜州白玉县发生4.9级地震,震源深度10千米
  • 中国太保一季度净赚96.27亿元降18.1%,营收同比下降1.8%
  • 好未来:2025财年收入增长51%,下个财年提高整体盈利能力是首要任务