多进程之间、使用共享内存、实现图片的数据通信:包括POSIX共享内存(shm_open 和 mmap虚拟内存)、系统调用(shmat物理存储器)、内存映射文件等方法
共享内存是进程间通信(IPC)的重要方式之一,可以让多个进程访问相同的内存区域,从而实现数据共享。
一、共享内存(SHM)
像上图所示,当进程 P1 向其虚拟内存中的区域 1 写入数据时,进程 2 就能同时在其虚拟内存空间的区域 2 看见这些数据,中间没有经过任何的转发,效率极高。
使用共享内存的一般步骤是:
1,获取共享内存对象的 ID
2,将共享内存映射至本进程虚拟内存空间的某个区域
3,当不再使用时,解除映射关系
4,当没有进程再需要这块共享内存时,删除它。
方法总结
共享内存是进程间通信(IPC)的重要方式之一,可以让多个进程访问相同的内存区域,从而实现数据共享。实现共享内存的方式有多种,主要包括以下几种:
POSIX 共享内存 (POSIX Shared Memory)
使用 shm_open 和 mmap 等函数来创建和管理共享内存段。
POSIX 共享内存可以通过名字标识,多个进程可以通过名字来访问同一个共享内存段。
需要使用 shm_unlink 来删除共享内存对象。
mmap:它是一个系统调用,用于在用户空间和内核空间之间进行文件映射。
System V 共享内存 (System V Shared Memory)
使用 shmget、shmat、shmdt 和 shmctl 等函数来创建和管理共享内存段。
共享内存段通过一个整数键来标识,进程通过这个键来访问共享内存段。
System V 共享内存提供了更多的控制选项,但编程接口相对复杂。
shmat是Linux系统中的一个重要的内存管理函数
匿名映射(Anonymous Mapping)
使用 mmap 函数,可以在多个进程之间创建匿名共享内存区域。
通常通过 fork 创建子进程时,共享的匿名内存区域会自动继承。
内存映射文件(Memory Mapped Files)
使用 mmap 函数将文件映射到进程的地址空间,不同进程可以通过映射同一个文件来实现共享内存。
文件映射提供了一种持久化的数据共享方式,但需要文件系统的支持。
Windows 共享内存
使用 Windows API 提供的 CreateFileMapping 和 MapViewOfFile 函数来创建和管理共享内存段。
通过命名的内存映射文件对象可以在不同进程间共享内存。
需要使用 CloseHandle 来释放资源。
Boost.Interprocess (C++ 库)
Boost 库提供了跨平台的共享内存实现,使用 boost::interprocess::shared_memory_object 和 boost::interprocess::mapped_region 等类来管理共享内存。
提供了更高层次的封装,简化了共享内存的使用。
Python 的 multiprocessing 模块
Python 的 multiprocessing 模块提供了共享内存的支持,如 multiprocessing.Array 和 multiprocessing.Value。
适用于 Python 的多进程编程,简单易用。
每种共享内存方式都有其特点和适用场景,选择具体方式时需要根据系统平台、应用需求和编程语言等因素进行权衡。
举例:内存映射文件(Memory Mapped Files)
先用open函数打开一个文件,然后调用mmap函数把得到的描述符映射到当前进程地址空间中。这种方式访问速度相对较慢,因为需要内核同步或异步更新到文件系统中。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main() {
// 打开文件
int fd = open("example.txt", O_RDWR);
if (fd == -1) {
perror("Error opening file");
exit(EXIT_FAILURE);
}
// 获取文件大小
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("Error getting the file size");
exit(EXIT_FAILURE);
}
off_t length = sb.st_size; // 文件大小,单位是字节
// 映射内存到进程的地址空间
char* map = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
perror("Error mmapping the file");
exit(EXIT_FAILURE);
}
// 打印映射的内存内容,即文件内容
for (off_t i = 0; i < length; i++) {
printf("%c", map[i]); // 打印每个字符,即文件内容
}
printf("\n");
// 释放内存映射和文件描述符
if (munmap(map, length) == -1) {
perror("Error un-mmapping the file");
exit(EXIT_FAILURE);
}
close(fd);
return 0;
}
- open函数用于打开文件,其返回值是文件描述符。如果打开失败,则返回-1。第二个参数O_RDWR表示以读写模式打开文件。
- fstat函数用于获取文件的大小,其返回值是stat结构体,其中st_size成员表示文件大小(单位是字节)。如果获取失败,则返回-1。
- mmap函数用于将文件映射到进程的地址空间。第一个参数是映射区域的起始地址,通常为NULL。第二个参数是映射区域的长度。第三个参数是保护标志,这里设置为读、写和共享(可读、可写、可被其他进程共享)。第四个参数是映射对象的类型,这里设置为共享内存。第五个参数是文件描述符。第六个参数是文件映射的偏移量。如果映射成功,则返回映射区域的指针;否则返回MAP_FAILED。
- munmap函数用于释放内存映射。第一个参数是映射区域的指针。第二个参数是映射区域的长度。如果释放成功,则返回0;否则返回-1。
举例:POSIX 共享内存
先用shm_open打开一个Posix IPC名字(也可以是文件系统中的一个路径名),然后调用mmap将返回的描述符映射到当前进程的地址空间。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define SHM_SIZE 1024 // 共享内存大小
int main() {
int fd;
void *map_ptr;
// 打开共享内存对象,以读写模式打开,不创建新对象,如果对象不存在则返回-1
fd = shm_open("/Posix IPC", O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("shm_open");
exit(EXIT_FAILURE);
}
// 调整共享内存对象的大小,这里将其设置为1024字节
if (ftruncate(fd, SHM_SIZE) == -1) {
perror("ftruncate");
exit(EXIT_FAILURE);
}
// 将共享内存对象的描述符映射到当前进程的地址空间,map_ptr指向的就是这块内存的起始地址
map_ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map_ptr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
// 现在可以在map_ptr指向的内存区域进行读写操作了,这就像操作普通的内存一样简单
// ... 写入数据到 map_ptr 指向的内存区域 ...
// ... 从 map_ptr 指向的内存区域读取数据 ...
// 当进程不再需要访问共享内存时,可以通过调用munmap来撤销内存映射,参数是map_ptr和映射的长度
if (munmap(map_ptr, SHM_SIZE) == -1) {
perror("munmap");
exit(EXIT_FAILURE);
}
// 关闭共享内存对象的描述符,然后删除该对象,参数是fd和0,表示删除成功返回0,否则返回-1
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}
if (shm_unlink("/my_shm") == -1) {
perror("shm_unlink");
exit(EXIT_FAILURE);
}
return 0;
}
- shm_open函数用于打开或创建共享内存对象。第一个参数是对象名,第二个参数是打开模式(这里使用读写模式),第三个参数是权限设置(这里设置为0666,表示所有用户都可以读写这个对象)。如果对象不存在,shm_open会创建一个新对象。如果创建成功,shm_open会返回一个文件描述符。如果失败,返回-1。
- ftruncate函数用于调整共享内存对象的大小。第一个参数是文件描述符,第二个参数是新的文件大小。这里将文件大小设置为1024字节。如果成功,ftruncate返回0;否则返回-1。
- mmap函数用于将共享内存对象的描述符映射到当前进程的地址空间。第一个参数是映射区域的起始地址(通常为NULL),第二个参数是映射区域的长度,第三个参数是保护标志(这里设置为读、写和共享),第四个参数是映射对象的类型(这里设置为共享内存),第五个参数是文件描述符,第六个参数是文件映射的偏移量。如果映射成功,mmap返回映射区域的指针;否则返回MAP_FAILED。
文心一言给我生成的:
在C++中,可以使用POSIX IPC(进程间通信)机制,如共享内存,来实现多进程之间的图片共享。以下是一个简单的例子,演示如何使用共享内存在两个进程之间传输图片数据。
在这个例子中,我们首先打开一个已存在的图片文件,然后创建(或打开已存在的)一个共享内存对象,并将其大小设置为1MB。接着,我们将图片文件的内容读入共享内存,然后关闭文件描述符和共享内存映射。
在接收方的进程中,你需要重复类似的步骤,但是这次是从共享内存中读取数据,并将其保存为一个新的图片文件。
请注意,这个例子没有包含错误处理,并且假设图片大小小于1MB。在实际应用中,你需要添加错误处理和图片大小检查,以确保不会导致缓冲区溢出。
#include <iostream>
#include <cstring>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <stdexcept>
const char* SHM_NAME = "/my_shm";
const int SHM_SIZE = 1024 * 1024; // 1MB
int main() {
// 读取图片到共享内存
int fd = open("image.jpg", O_RDONLY);
if (fd == -1) {
perror("open");
return EXIT_FAILURE;
}
int shm_fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);
if (shm_fd == -1) {
perror("shm_open");
return EXIT_FAILURE;
}
ftruncate(shm_fd, SHM_SIZE);
void* mem = mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (mem == MAP_FAILED) {
perror("mmap");
return EXIT_FAILURE;
}
// 将图片数据拷贝到共享内存
ssize_t bytes_read = 0;
ssize_t bytes_written = 0;
while ((bytes_read = read(fd, static_cast<char*>(mem) + bytes_written, SHM_SIZE - bytes_written)) > 0) {
bytes_written += bytes_read;
if (bytes_written >= SHM_SIZE) {
std::cerr << "Shared memory is full, cannot fit the entire image." << std::endl;
break;
}
}
close(fd);
munmap(mem, SHM_SIZE);
close(shm_fd);
// 在另一个进程中,从共享内存读取图片
// ...
return 0;
}
要在另一个进程中接受共享内存中的图片数据,你可以编写一个独立的程序,它首先尝试打开与发送程序相同的共享内存对象,然后映射这块共享内存到其地址空间,并从中读取图片数据。下面是一个简单的示例代码,展示了如何在一个新的进程中读取共享内存中的图片数据:
#include <iostream>
#include <cstring>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <stdexcept>
const char* SHM_NAME = "/my_shm";
const int SHM_SIZE = 1024 * 1024; // 假设图片大小不超过1MB
int main() {
// 打开共享内存对象
int shm_fd = shm_open(SHM_NAME, O_RDONLY, 0666);
if (shm_fd == -1) {
perror("shm_open");
return EXIT_FAILURE;
}
// 将共享内存映射到进程地址空间
void* mem = mmap(0, SHM_SIZE, PROT_READ, MAP_SHARED, shm_fd, 0);
if (mem == MAP_FAILED) {
perror("mmap");
return EXIT_FAILURE;
}
// 读取共享内存中的图片数据
FILE* output_file = fopen("received_image.jpg", "wb");
if (!output_file) {
perror("fopen");
munmap(mem, SHM_SIZE);
close(shm_fd);
return EXIT_FAILURE;
}
size_t bytes_written = fwrite(mem, 1, SHM_SIZE, output_file);
if (bytes_written != SHM_SIZE) {
std::cerr << "Error writing to file: " << strerror(errno) << std::endl;
}
fclose(output_file);
munmap(mem, SHM_SIZE); // 解除映射
close(shm_fd); // 关闭共享内存文件描述符
// 如果需要,可以调用shm_unlink来删除共享内存对象,但请确保没有其他进程再使用它
// shm_unlink(SHM_NAME);
return 0;
}
在这个示例中,我们假设发送程序已经将图片数据完全写入了共享内存,并且接收程序能够读取整个共享内存区域的大小(在这个例子中是SHM_SIZE)。如果图片的大小可能超过这个预设的大小,你需要设计一种机制来在发送和接收程序之间同步,比如使用信号量或互斥锁来确保数据完整性和一致性。
请注意,当使用共享内存时,同步是一个非常重要的问题,特别是在多个进程并发读写共享内存的情况下。上述代码示例并没有涉及同步机制,因为它假设发送程序在接收程序尝试读取之前已经完成了写操作。在实际应用中,你可能需要添加适当的同步机制来确保数据的一致性和完整性。
此外,当不再需要共享内存时,可以使用shm_unlink来删除共享内存对象,但这应该在所有进程都完成共享内存的读写操作,并已经解除了映射之后进行。如果在任何进程还在使用共享内存时就调用了shm_unlink,可能会导致未定义的行为。
如果发送程序结束了,共享内存对象还存在吗?
如果发送程序结束了,共享内存对象是否还存在取决于发送程序是否显式地删除了该共享内存对象。在POSIX IPC中,共享内存对象在其所有打开的文件描述符都被关闭且所有进程都不再引用它时,并不会自动被删除。相反,需要显式地调用shm_unlink函数来删除共享内存对象。
如果发送程序在结束前没有调用shm_unlink来删除共享内存对象,那么即使发送程序结束了,共享内存对象仍然会存在,直到某个进程显式地调用shm_unlink来删除它,或者系统重启。这意味着其他进程仍然可以访问这块共享内存(如果它们知道共享内存的名字并且有足够的权限)。
因此,为了避免资源泄漏和潜在的安全问题,发送程序应该在结束前调用shm_unlink来删除不再需要的共享内存对象。同时,接收程序(或其他任何访问该共享内存的进程)也应该在它们不再需要共享内存时,确保正确地关闭它们的文件描述符,并在适当的时候调用shm_unlink。
这里是一个简单的示例,展示了如何在发送程序结束时删除共享内存对象:
cpp
// ... 其他代码 ...
// 在发送程序结束前删除共享内存对象
shm_unlink(SHM_NAME);
return 0;
请注意,shm_unlink只是从文件系统中删除了对共享内存对象的引用,并不会立即销毁共享内存。只有当最后一个进程解除了对共享内存的映射,并且没有任何进程再持有该共享内存对象的文件描述符时,共享内存区域才会被操作系统回收。因此,即使调用了shm_unlink,其他仍然引用该共享内存的进程仍然可以访问它,直到它们自己解除了映射。
本进程结束了就可调用shm_unlink吗?还是需要等全部的进程都运行完?
在POSIX共享内存机制中,shm_unlink函数用于删除共享内存对象的名称,这并不意味着立即销毁共享内存本身。实际上,共享内存区域在最后一个进程解除对它的映射(通过munmap)且关闭对它的最后一个文件描述符(通过close)后才会被系统回收。
因此,关于是否可以在某个进程接受共享内存后就调用shm_unlink,这取决于你的应用程序的具体需求。如果你确信没有其他进程(或线程)会再访问这块共享内存,或者你已经设计了一种机制来确保所有其他进程在shm_unlink被调用之前都完成了对共享内存的访问,那么你可以在当前进程接受共享内存后就调用shm_unlink。
然而,在大多数情况下,更安全的做法是让所有使用共享内存的进程在完成对共享内存的访问后,都调用munmap和close来解除映射和关闭文件描述符。然后,由某个进程(通常是最后一个不再需要共享内存的进程)调用shm_unlink来删除共享内存对象的名称。这样做可以确保在shm_unlink被调用之前,所有进程都已经完成了对共享内存的访问,从而避免潜在的数据竞争或访问已释放内存的问题。
总之,是否可以在某个进程接受共享内存后就调用shm_unlink取决于你的具体应用场景和需求。在大多数情况下,更安全的做法是让所有使用共享内存的进程都完成访问后再删除共享内存对象的名称。
System V 共享内存 和常用API
使用共享内存的步骤:
- 调用 shmget() 创建共享内存段 id ,
- 调用 shmat() 将 id 标识的共享内存段加到进程的虚拟地址空间,
- 访问加入到进程的那部分映射后地址空间,可用 IO 操作读写。


图片的上传:upload.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <string.h>
#define IPC_PATH "./" // 指定文件或者路径
#define IPC_MASK 1 // IPC标识符
#define PAGE_SIZE 4096 // 页内存大小
#define READ_ONLY_SIZE 100 // 每一次读取的字节大小
int main(int argc, char const *argv[])
{
// 获取当前未用的一个ipc的key值
key_t key = ftok(IPC_PATH, IPC_MASK);
if( key == -1)
{
perror("ftok()");
exit(-1);
}
else
{
printf("key值为: %x\n", key);
}
// 获取共享内存的id
int shm_id = shmget(key, PAGE_SIZE*247, IPC_CREAT | 0666) ;
if ( shm_id == -1)
{
perror("shmget()");
exit(-1);
}
// 对内存地址进行映射
char * shm_p = (char * )shmat(shm_id, NULL, 0);
if( shm_p == (char *)-1)
{
perror("shmat()");
exit(-1);
}
// 上传图片
int file_size;
struct stat statbuf;
memset(&statbuf, 0, sizeof(statbuf));
stat("./1.gif", &statbuf); // stat函数用于获取文件的相关信息,包括文件大小、修改时间等
file_size = statbuf.st_size;
memcpy(shm_p, &file_size, sizeof(int));
// 打开文件
FILE * fp = fopen("./1.gif", "r");
if( fp == NULL)
{
perror("fopen()");
exit(-1);
}
int upload_size; // 上传的文件大小
char * new_shm_p = shm_p;
char file_data[READ_ONLY_SIZE];
for( int upload_num = 0; upload_num < file_size/READ_ONLY_SIZE; upload_num++)
{
memset(file_data, 0, READ_ONLY_SIZE);
//读文件的数据
int fread_ret = fread(file_data, READ_ONLY_SIZE, 1, fp);
if( fread_ret < 1)
{
if( ferror(fp) )
{
perror("fread()");
exit(-1);
}
}
memcpy( new_shm_p, file_data, READ_ONLY_SIZE);
new_shm_p += READ_ONLY_SIZE;
upload_size += READ_ONLY_SIZE;
}
//如果有最后一次有剩余的数据,把放进共享内存里面
int skip_data = file_size % READ_ONLY_SIZE ; // 获取剩余的字节数
if( skip_data != 0)
{
memset(file_data, 0 , READ_ONLY_SIZE);
// 读文件的数据
if(fread(file_data, skip_data, 1, fp) < 1)
{
if( ferror(fp) )
{
perror("fread()");
exit(-1);
}
}
memcpy(new_shm_p, file_data, skip_data);
upload_size += skip_data;
}
printf("图片上传成功\n");
printf("上传的文件大小为 %d\n", upload_size);
if(fclose(fp) == -1)
{
perror("fclose()");
exit(-1);
}
return 0;
}
图片的下载:download.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <string.h>
#define IPC_PATH "./" // 指定文件或者路径
#define IPC_MASK 1 // IPC标识符
#define PAGE_SIZE 4096 // 页内存大小
#define WRITE_ONLY_SIZE 20 // 每一次读取的字节大小
int main(int argc, char const *argv[])
{
key_t key = ftok(IPC_PATH,IPC_MASK);
if(key == -1)
{
perror("ftok ... ");
return -1;
}
int shm_id = shmget(key,PAGE_SIZE*247,IPC_CREAT | 0666);
if(shm_id == -1)
{
perror("shmget ... ");
return -1;
}
char * shm_p = (char *)shmat(shm_id,NULL,0);
if(shm_p == (char *)-1)
{
perror("shmat ... ");
return -1;
}
char * new_shm_p = shm_p;
FILE * fp = fopen("./img/new.gif","w");
if(fp == NULL)
{
perror("fopen ... ");
return -1;
}
//先从共享内存前4个字节中获取要下载的文件的字节大小
int download_file_size;
memcpy(&download_file_size,new_shm_p,sizeof(int));
new_shm_p+= sizeof(int);
printf("即将下载文件大小:%d\n",download_file_size);
char file_data[WRITE_ONLY_SIZE ];
int download_size = 0 ; // 下载的文件大小
for(int download_num = 0; download_num < download_file_size/WRITE_ONLY_SIZE ; download_num++)
{
memset(file_data, 0, WRITE_ONLY_SIZE );
// 从共享文件中获取数据
memcpy(file_data, new_shm_p, WRITE_ONLY_SIZE );
// 将文件数据写入新的文件中
int write_ret = fwrite(file_data, WRITE_ONLY_SIZE , 1, fp );
if( write_ret < 1)
{
if( ferror(fp) )
{
perror("write()");
exit(-1);
}
}
new_shm_p += WRITE_ONLY_SIZE ;
download_size += WRITE_ONLY_SIZE ;
}
//如果有最后一次有剩余的数据,把放进共享内存里面
int skip_data = download_file_size % WRITE_ONLY_SIZE ; // 获取剩余的字节数
if( skip_data != 0 )
{
memset( file_data, 0, WRITE_ONLY_SIZE );
// 从共享文件中获取数据
memcpy(file_data, new_shm_p, WRITE_ONLY_SIZE );
// 将文件数据写入新的文件中
if( fwrite(file_data , skip_data, 1, fp ) < 1)
{
if( ferror(fp) )
{
perror("fwrite()");
exit(-1);
}
}
download_size += skip_data ;
}
printf("下载文件完成, 下载的文件大小为:%d \n", download_size);
if(fclose(fp) == -1)
{
perror("fclose()");
exit(-1);
}
// 删除共享内存
if(shmctl(shm_id, IPC_RMID, NULL) == -1)
{
perror("shmctl()");
exit(-1);
}
return 0;
}
查看共享内存的文件大小:
ipcs -m

shmat和mmap的区别?
shmat和mmap在Linux系统中都是用于实现进程间共享内存的机制,但它们在使用方式、应用场景和特性上存在一些区别。
使用方式和应用场景:
shmat:它是Linux系统中的一个重要的内存管理函数,用于将已经创建的共享内存连接到某进程的地址空间中,或者断开与某进程的地址空间的连接。shmat函数接收两个参数,一个是共享内存的句柄,另一个是指定的地址空间的虚拟地址。它是构建多任务操作系统的重要基础,支持进程间的信息交换和多任务的并发执行。
mmap:它是一个系统调用,用于在用户空间和内核空间之间进行文件映射。mmap允许文件在物理内存中的特定区域被映射到进程的地址空间中,从而使进程可以通过内存访问操作来读取和写入文件。此外,mmap系统调用还使得进程之间可以通过映射同一个普通文件实现共享内存。
存储和访问特性:
shmat:它将共享内存直接映射到实际的物理存储器(主存)里面,因此进程间访问速度(读写)比磁盘要快。然而,由于它直接占用主存,因此其存储量不能非常大,多于主存容量时就会受限。
mmap:它可以将文件内容映射到虚拟内存上,对文件的读取和修改可以通过对这段内存的读取和修改来实现。这种方式并不直接消耗物理存储器(主存),因此可以映射非常大的文件,但其进程间读取和写入速度相比直接访问主存要慢一些。
灵活性和扩展性:
mmap:除了作为共享内存机制外,它本身还提供了不同于一般对普通文件的访问方式。进程可以像读写内存一样对普通文件进行操作,提供了更高的灵活性。
shmat:主要专注于共享内存的连接和断开操作,对于文件操作没有mmap那么灵活。
总结:选择shmat还是mmap取决于具体的应用场景和需求。如果需要在进程间共享小量数据且追求高速访问,那么shmat可能更合适;而如果需要处理大文件或需要文件操作的灵活性,那么mmap可能更合适。在使用这些机制时,还需要注意同步和错误处理等问题,以确保数据的完整性和安全性。
参考
https://blog.csdn.net/weixin_66634995/article/details/134471431
https://blog.csdn.net/m0_68865445/article/details/132797238
更多推荐
所有评论(0)