进程间通信 —— 《共享内存》
文章目录
- 🍕前言:
- 🍕共享内存:
- 🥚什么是共享内存?
- 🥚接口介绍:
- 🍖创建共享内存的前提工作
- 🍳创建共享内存
- 🍔关于释放共享内存
- 🍞关于返回值:
- 🧀挂接和去关联共享内存:
- 🥚利用进行共享内存通信:
- 🥗利用管道维护同步机制
🍕前言:
对于进程间通信的话题也来到了尾声,我们现在了解到了常见的通过管道实现进程间的文件级通信方式,但是对于实现进程间的通信还有很长一段故事需要我们去学习,接下来我们就来了解了解System V下的本地通行方式——“共享内存“,”消息队列“, ”信号量“。其中我们会对”共享内存“进行重点讲解。
🍕共享内存:
🥚什么是共享内存?
共享内存是一种进程间通信(IPC, Inter-Process Communication)机制,允许多个进程直接访问同一块内存空间,以实现高速的数据交换。由于共享内存区域位于系统的物理内存中,进程可以通过该内存区域进行数据的读写,而无需经过内核的中间缓冲区,从而大幅提高了通信的效率。
-
对于上图中,“共享内存”的创建均是由OS完成的。
-
OS提供上面“创建共享内存”和“映射到页表”的系统调用,供进程A和B来进行调用。
-
共享内存在系统中可以同时存在多份,供不同个数、不同进程之间进行通信。
-
OS注定了要对共享内存进行管理!(先组织、再描述)
共享内存,并不是简简单单的一段内存空间,同时要有描述并管理共享内存的数据结构(struct shm)和匹配算法。往后对共享内存的管理就变成了对该数据结构对象的管理。
struct shmid_ds {struct ipc_perm shm_perm; /* operation perms */int shm_segsz; /* size of segment (bytes) */__kernel_time_t shm_atime; /* last attach time */__kernel_time_t shm_dtime; /* last detach time */__kernel_time_t shm_ctime; /* last change time */__kernel_ipc_pid_t shm_cpid; /* pid of creator */__kernel_ipc_pid_t shm_lpid; /* pid of last operator */unsigned short shm_nattch; /* no. of current attaches */unsigned short shm_unused; /* compatibility */void *shm_unused2; /* ditto - used by DIPC */void *shm_unused3; /* unused */ };
- 共享内存 == 内存空间(数据) + 共享内存的属性
🥚接口介绍:
🍖创建共享内存的前提工作
-
shmget()
创建共享内存#include <sys/ipc.h> #include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);
功能:
是 Linux/Unix 系统中的一个系统调用,**用于创建或获取一个共享内存段。**
参数说明:
-
key
:一个用户定义的键值(通常使用ftok
函数生成)。key
是共享内存的唯一标识符,多个进程可以使用相同的key
来访问同一个共享内存段。- 如果
key
设置为IPC_PRIVATE
,那么该共享内存段只对创建它的进程可见(仅限子进程继承)。
- 如果
-
size
:共享内存的大小(以字节为单位)。如果指定的共享内存段不存在,系统会根据这个大小创建一个新的共享内存段。如果指定的共享内存段已经存在,可以忽略此参数(大小不会改变)。 -
shmflg
:权限标志和操作标志,可以是以下值的组合:-
权限位(类似文件的权限位):如
0666
表示共享内存的权限(读/写权限)。 -
IPC_CREAT
:如果指定的key
没有对应的共享内存段,则创建一个新的共享内存段。如果存在,则获取并返回。 -
IPC_EXCL
:单独使用没有意义。 -
IPC_CREAT | IPC_EXCL
:只有在共享内存段不存在时才会创建。否则,如果共享内存段已经存在,返回错误。(可以确保每次创建的共享内存是新的)
-
-
-
ftok()
生成key值(关于key值的介绍,后面讲)#include <sys/types.h> #include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
功能:
用于生成一个唯一的 **IPC(进程间通信)键值**,该键值可以用在各种 IPC 机制(如消息队列、共享内存、信号量等)中,确保多个进程可以使用相同的键值来访问同一个 IPC 对象。
参数说明:
pathname
:指向一个文件路径的指针。这个文件必须存在,并且调用进程有读取权限。通常是一个已经存在的文件路径名,ftok
函数会利用这个文件的 i-node 编号来生成键值。pathname
的作用是确保 IPC 键值可以和文件系统中的某个文件关联起来。- 该文件不必是特殊的文件,它可以是普通文件,也可以是目录。
proj_id
:一个整数值(0 到 255 之间)。proj_id
用作项目标识符,用于区分基于同一文件路径的不同键值。
返回值:
- 成功:返回一个类型为
key_t
的值(通常为正数),表示生成的唯一键值。 - 失败:返回
-1
,并设置errno
,可以通过perror
打印错误信息。
如何工作:
`ftok` 函数基于文件的 **i-node 号** 和 **proj_id** 生成一个唯一的键值。通过组合文件的 `inode` 信息(通常由操作系统保证唯一性)和项目标识符,`ftok` 能够生成一个**==唯一==的 IPC 键值**。在使用进程间通信时,多个进程只要指定相同的 `pathname` 和 `proj_id`,就可以得到相同的键值,用于创建或访问相同的 IPC 对象(如共享内存、信号量、消息队列)。
这里实际上我是创建了两个.cc文件,分别为
Client.cc
和Server.cc
,其它的都一样只是输出的方式不一样而已。最后我们以十六进制的方式进行输出,输出结果也是一样的!
所以现在我们已经有了key,那么我们就可以在内存中创建共享内存了!
🍳创建共享内存
刚刚不是讲解了创建共享内存的接口函数shmget
吗,所有的参数我们也了解了,最关键的那个key我们也有了
int shmget(key_t key, size_t size, int shmflg);
所以我们就可以着手开始创建了,代码如下:
int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL);
此时共享内存就创建好了,但是有一个问题,我们并没有研究函数shmget()
的返回值诶!那我们不妨将这个函数的返回值打印出来好了,然后为了方便看看在系统中,共享内存是否创建,我们可以使用输入指令来看看。
现在我们编译运行Server.cc,再执行指令:ipcs -m
// Server.cc#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>const std::string shm_path = "/home/ws/Linux_tutorial/CSDN/test_sharedMemory";std::string ToHex(key_t key)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", key);return buffer;
}key_t CreatKey()
{key_t key = ftok(shm_path.c_str(), 0x11223344);if (key < 0){perror("ftok");exit(1);}return key;
}int main()
{key_t key = CreatKey();int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL);if (shmid < 0){perror("shmget");exit(1);}std::cout << "Shm's key: " << ToHex(key) << std::endl;std::cout << "Shm's shmid: " << shmid << std::endl;return 0;
}
运行成功后再在监视窗口中就发现了这几个值是一样的,如果你这里是第一次创建共享内存,那么你输入指令后,你的shmid大概率是0,如果你后面多次创建共享内存,你的shmid的值就会不断加一,至于为什么我们后面讲。
🍔关于释放共享内存
共享内存不随着进程的结束而自动释放的,只要你创建了共享内存,共享内存就会一直存在,直到系统重启。
共享内存生命周期随内核,而文件随进程的!
如果我们要释放共享内存可以输入指令:ipcrm -m "shmid"
,这个shmid就是上述图片中的shmid。
也可也使用系统调用:
-
shmctl()
控制共享内存段的各种操作#include <sys/ipc.h> #include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
shmid
:共享内存段的标识符(shmid
),由shmget()
创建时返回,用于标识要控制的共享内存段。cmd
:要执行的控制命令,定义了对共享内存段的操作。常见的控制命令包括:IPC_STAT
:从内核中获取与共享内存段相关的状态信息,并将其存储在buf
中。IPC_SET
:根据buf
中的信息设置共享内存段的状态(如权限)。IPC_RMID
:标记共享内存段为删除状态。当没有任何进程附加到该共享内存段时,内核会自动释放该内存段。
buf
:指向shmid_ds
结构体的指针。如果cmd
是IPC_STAT
或IPC_SET
,此结构体将用于获取或设置共享内存的相关信息。对于IPC_RMID
,此参数可以为NULL
。
返回值:
- 成功时:返回
0
。 - 失败时:返回
-1
,并设置errno
来指示错误原因。
🍞关于返回值:
我们在创建共享内存的时候,你说要创建一个key代表共享内存的唯一标识符,那你为什么在这里释放的时候又整个shmid呢?而且你使用指令对共享内存进行监控时,你也整了一个shmid和key,这是什么意思呢?它们俩有什么区别呢?
特性 | key | shmid |
---|---|---|
类型 | 用户自定义的标识符(通常由 ftok 生成) | 操作系统分配的共享内存段的标识符 |
生成方式 | 通过 ftok() 函数生成 | 通过 shmget() 调用时生成 |
作用 | 标识要访问的 IPC 对象(如共享内存) | 标识一个具体的共享内存段 |
使用目的 | 用于创建或访问共享内存段、消息队列等 IPC 对象 | 用于附加、分离和控制共享内存段 |
范围 | 在创建共享内存或其他 IPC 对象时使用 | 用于进程管理共享内存的后续操作 |
关系 | 多个进程通过相同 key 访问同一个共享内存 | 每个共享内存段都有一个唯一的 shmid |
生命周期 | key 与IPC对象的生命周期无关,可以复用 | shmid 随着共享内存的创建和删除而改变 |
简单来说:
key属于用户形成,内核使用的一个字段,用户不能使用key来进行对shm的管理,而内核却可以用key来进行区分shm的唯一性。
shmid是内核给用户返回的一个标识符,用来进行用户级对共享内存进行管理的id值。
用户创建key交给OS,OS创建shmid交给用户。
🧀挂接和去关联共享内存:
现在我们已经可以在物理内存上创建共享内存了,也可以直接把共享内存从物理内存上直接删除,那我们该如何将共享内存通过页表的映射 挂接到mm_struct(虚拟地址空间)上呢?
-
shmat()
将指定的共享内存挂接到该进程的mm_struct(虚拟地址空间)。#include <sys/shm.h> #include <sys/types.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
shmid
:共享内存段的标识符,它是在调用shmget()
时返回的共享内存ID。通过这个ID,shmat
可以找到对应的共享内存段并将其映射到进程的地址空间。shmaddr
:指定进程中共享内存应该附加到的地址。如果传入NULL
(或(void*)0
),内核会自动选择合适的内存地址来映射共享内存段。通常,推荐使用NULL
,让操作系统决定最佳的内存地址。shmflg
:标志位,可以有以下两种选项:SHM_RDONLY
:以只读方式附加共享内存。调用进程只能读取共享内存,而不能写入。SHM_RND
:如果设置了这个标志,shmaddr
指定的地址会被舍入到页面大小的倍数(通常是4KB对齐)。如果你没有特别的对齐要求,通常不需要使用这个标志。
返回值:
-
成功时:返回共享内存段附加到的地址(即共享内存段在当前进程中的虚拟地址)。
-
失败时:返回
(void *) -1
,并设置errno
,可以通过perror
获取错误信息。针对这个函数的返回值,它是个void*的返回值,这就有点幽默了,但是其实我们回顾一下之前的C语言学习,发现好像malloc这个函数也是这样的,所以我们以后在使用这个函数后获得的返回值,就是我们需要进行挂接的地址,我们可以仿照malloc的使用方法大致的能猜出它的使用方法——
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
类似这样的。
-
shmdt()
将当前进程与共享内存去关联#include <sys/shm.h> #include <sys/types.h>int shmdt(const void *shmaddr);
参数说明
shmaddr
:这是通过shmat()
返回的指向共享内存段的指针,也就是当前进程的虚拟地址空间中的共享内存起始地址。当你调用shmdt
时,需要传递这个地址来告诉操作系统分离哪个共享内存段。
返回值
- 成功:返回
0
,表示成功将共享内存从进程地址空间中分离。 - 失败:返回
-1
,并设置errno
来指示错误的具体原因。
错误码
EINVAL
:传递的shmaddr
不是有效的共享内存段地址。
我们可以先浅浅的试一试使用以上的各个接口来简单的实现创建共享内存,再实现挂接最后去关联加释放。
// server.cc#include <iostream> #include <string> #include <cerrno> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h>const std::string shm_path = "/home/ws/Linux_tutorial/CSDN/test_sharedMemory";std::string ToHex(key_t key) {char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", key);return buffer; }key_t CreatKey() {key_t key = ftok(shm_path.c_str(), 0x11223344);if (key < 0){perror("ftok");exit(1);}return key; }int CreatShm(key_t key) {int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);if (shmid < 0){perror("shmget");exit(1);}std::cout << "Shm is been created!" << std::endl;return shmid; }void FreeShm(int _shmid) {int n = shmctl(_shmid, IPC_RMID, nullptr);if (n < 0){perror("shmctl");exit(1);}// std::cout << "as I am Server, I need to free " << std::endl;std::cout << "free Shm!" << std::endl; }void *ConnectShm(int shmid) {void *tmpshm = shmat(shmid, nullptr, 0);return tmpshm; }int main() {key_t key = CreatKey();int shmid = CreatShm(key);// 挂接共享内存char *shmaddr = (char *)ConnectShm(shmid);sleep(5);// 去关联int n = shmdt(shmaddr);if (n < 0){perror("shmdt");exit(1);}FreeShm(shmid);return 0; }
在这里我还创建了一个client.cc的代码,代码内容大部分一致,唯一不同的就是调用`shmget()` 函数的打开模式的参数不一样而已,此时我们运行并打开“监视窗口”:
这里的nattch显示的是2,代表着共享内存挂接到了两个进程的地址空间之中。
并且这里还有一个点需要注意,那就是注意看perms字段,如果你够仔细记得一开始我打开的“监视窗口”,就会发现这里是0,其实这里我是修改了33行代码,我增加了权限,perms代表着共享内存的权限,如果你没加这个权限,可能在实现挂接这个操作的时候会出现错误并返回。
🥚利用进行共享内存通信:
既然我们已经可以运用上述基础的接口来帮助我们实现共享内存的创建、挂接和去关联甚至是释放,那我们现在想要真正的利用共享内存实现进程间的通信,所以我们不妨对共享内存的一系列操作进行封装。代码如下:
Shm.hpp
用来封装
// Shm.hpp
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>#define Server 1
#define Client 2
#define SHM_SIZE 4096
#define SERVER_MODE IPC_CREAT | IPC_EXCL | 0666
#define CLIENT_MODE IPC_CREAT | 0666const std::string shm_path = "/home/ws/Linux_tutorial/CSDN/test_sharedMemory";class Shm
{
public:Shm(int user, int mode): _user(user), _mode(mode), _shmid(0), _key(0), _shmaddr(nullptr){if (_shmaddr != nullptr)DisConnect(_shmaddr);_key = CreatKey();_shmid = shmget(_key, SHM_SIZE, _mode);if (_shmid < 0){perror("shmget");exit(1);}std::cout << "Shm is been created!" << std::endl;_shmaddr = ConnectShm();}void *GetShmaddr(){return _shmaddr;}~Shm(){sleep(12);int n = shmctl(_shmid, IPC_RMID, nullptr);if (n < 0){perror("shmctl");exit(1);}DisConnect(_shmaddr);// std::cout << "as I am Server, I need to free " << std::endl;std::cout << "free Shm!" << std::endl;}private:key_t CreatKey(){key_t key = ftok(shm_path.c_str(), 0x11223344);if (key < 0){perror("ftok");exit(1);}return key;}std::string ToHex(key_t key){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", key);return buffer;}void *ConnectShm(){void *tmpshm = shmat(_shmid, nullptr, 0);return tmpshm;}void DisConnect(void *shmaddr){// 去关联int n = shmdt(shmaddr);if (n < 0){perror("shmdt");exit(1);}std::cout << "success to DisConnect" << std::endl;}private:int _user;int _mode;int _shmid;key_t _key;void *_shmaddr;
};
Server.cc
服务端用来读取共享内存的数据
// Server.cc
#include "Shm.hpp"int main()
{Shm my_shm(Server, SERVER_MODE);char *shmaddr = (char *)my_shm.GetShmaddr();while(true){std::cout << "shm content:> " << shmaddr << std::endl;sleep(1);}return 0;
}
Client.cc
用来往共享内存之中写数据
// Client.cc
#include "Shm.hpp"int main()
{Shm my_shm(Server, CLIENT_MODE);char *shmaddr = (char *)my_shm.GetShmaddr();char ch = 'A';while (ch <= 'Z'){shmaddr[ch - 'A'] = ch;printf("add %c to Server\n", ch);++ch;sleep(2);}return 0;
}
这里我们故意是让Client端过两秒才加一个字母,而Server端是过一秒就读取共享内存。
这里和管道好像有点不一样?
————还记得在管道那里,我读端是必须等待你写端进行写入的,你要是写端没写入我就得一直等,而且我读端要是读,也是直接把你管道里的数据全给读走了,而到了共享内存这里,你Client就一直在那追加,我Server就一直在读共享内存的数据,好像两者没有关系一样的。
而且我也没有使用什么read 和 write这样的系统调用接口?
其实,共享内存是所有进程通信中,速度最快的,因为共享内存大大减少了数据的拷贝次数。
而共享内存不保证像管道那样的,输入输出同步,所以共享内存不提供对自己的保护机制
而共享内存在mm_struct中,是位于用户空间当中的!
而管道在mm_struct中,是位于内核空间当中的!
因此这就是为什么共享内存不需要使用系统调用的接口
那针对输入输出的同步机制,有没有什么办法可以控制呢?
——当然有,我们可以利用管道来进行维护嘛。
🥗利用管道维护同步机制
新增上次写的命名管道头文件namedpipe.hpp
#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cerrno>
#include <fcntl.h>const std::string pipe_path = "./myfifo";
#define DEFAULT_FD -1
#define Server 1
#define Client 2
#define READ O_RDONLY
#define WRITE O_WRONLYclass NamedPipe
{
private:void OpenNamedPipe(int mode){_fd = open(pipe_path.c_str(), mode);if (_fd < 0){perror("open");exit(1);}std::cout << "NamedPipe 打开成功!" << std::endl;}public:NamedPipe(std::string path, int user): _path(path), _user(user), _fd(DEFAULT_FD){if (_user == Server){int n = mkfifo(pipe_path.c_str(), 0666);if (n < 0){perror("mkfifo");exit(1);}std::cout << "命名管道creat完毕!" << std::endl;}}void OpenNamedPipeByRead(){OpenNamedPipe(READ);}void OpenNamedPipeByWrite(){OpenNamedPipe(WRITE);}int WriteInPipe(std::string message){int n = write(_fd, message.c_str(), message.size());if (n < 0){perror("write");exit(1);}return n;}int ReadFromPipe(std::string *out){char file_buffer[1024];int n = read(_fd, file_buffer, sizeof(file_buffer));if (n > 0){file_buffer[n] = 0;*out = file_buffer;}return n;}~NamedPipe(){if (_user == Server){int n = unlink(pipe_path.c_str());if (n < 0){perror("mkfifo");exit(1);}std::cout << "命名管道free完毕" << std::endl;}}private:std::string _path;int _user;int _fd;
};
Server.cc
// Server.cc
#include "Shm.hpp"
#include "namedpipe.hpp"int main()
{ // 1、创建共享内存Shm my_shm(Server, SERVER_MODE);char *shmaddr = (char *)my_shm.GetShmaddr();// 2、创建管道NamedPipe fifo(shm_path, Server);fifo.OpenNamedPipeByRead();while(true){// 先接受管道的消息,再开始去找共享内存的内容std::string temp;fifo.ReadFromPipe(&temp);std::cout << "shm content:> " << shmaddr << std::endl;sleep(1);}return 0;
}
Client.cc
// Client.cc
#include "Shm.hpp"
#include "namedpipe.hpp"int main()
{// 1、创建共享内存Shm my_shm(Server, CLIENT_MODE);char *shmaddr = (char *)my_shm.GetShmaddr();// 2、创建管道NamedPipe fifo(shm_path, Client);fifo.OpenNamedPipeByWrite();char ch = 'A';while (ch <= 'Z'){shmaddr[ch - 'A'] = ch;// 往管道里写数据std::string temp = "wakeup";fifo.WriteInPipe(temp);std::cout << "add " << ch << " into shm " << std::endl;++ch;sleep(2);}return 0;
}
至此就形成了共享内存的输入输出同步通信!!!
有需要的话可以看我的代码gitee:
我的Gitee仓库