💐专栏导读
💐文章导读
共享内存是一种进程间通信的机制,允许多个进程访问同一块物理内存
,以实现数据的共享。通过共享内存,进程可以直接读写共享的内存区域,而无需通过中间的数据传输机制(例如管道或消息队列)进行通信,因此共享内存是最快
的IPC形式。
共享内存示意图
🐧共享内存原理
-
创建共享内存: 在一个进程中调用系统调用(例如
shmget
),请求创建一块共享内存。这个调用需要指定内存的大小以及一些标志,以控制共享内存的权限和行为。 -
关联共享内存: 其他进程通过调用系统调用(例如
shmat
)将共享内存附加到它们的地址空间中。这个调用返回指向共享内存区域的指针,使得进程可以直接读写这块内存。 -
读写共享内存: 一旦多个进程都关联了同一块共享内存,它们就可以直接对这块内存进行读写操作,就像操作普通的内存一样。因为它们共享同一块物理内存,一个进程对共享内存的修改会立即反映到其他进程的视图中。
-
分离共享内存: 当进程不再需要访问共享内存时,它可以调用系统调用(例如
shmdt
)将共享内存从它的地址空间中分离。这并不会导致共享内存的删除,只是使得该进程无法再访问这块内存。 -
删除共享内存: 当不再需要使用共享内存时,一个进程可以调用系统调用(例如
shmctl
)请求删除共享内存。这会导致释放共享内存所占用的系统资源。
特点和注意事项:
- 共享内存提供了高效的进程间通信方式,因为数据直接存储在物理内存中,无需复制或转移。
- 进程需要谨慎地协调对共享内存的访问,以避免数据一致性问题。例如,可以使用互斥锁等同步机制。
- 共享内存的使用需要确保不同进程使用相同的数据结构和协议,以便正确地进行数据交换和共享。
- 在使用共享内存时,应注意防范竞态条件和死锁等并发编程的问题。
🐧共享内存相关函数
-
创建共享内存区域: 使用
shmget
函数创建一个共享内存区域。该函数的原型为:#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
key
是一个用于标识共享内存的键值。size
是要分配的共享内存的大小(字节数)。shmflg
是一组标志,通常使用IPC_CREAT
表示如果内存不存在则创建。
-
连接到共享内存区域: 使用
shmat
函数将进程连接到已经存在的共享内存区域。该函数的原型为:#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
是共享内存区域的标识符,由shmget
返回。shmaddr
通常设置为NULL
,让系统自动选择合适的地址。shmflg
可以为 0。
-
使用共享内存: 一旦连接到共享内存,进程就可以直接在这块内存中读写数据。
-
分离共享内存: 使用
shmdt
函数将进程与共享内存脱离。该函数的原型为:#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
shmaddr
是连接到共享内存区域的地址。
-
删除共享内存区域: 使用
shmctl
函数可以删除或控制共享内存区域的属性。如果不再需要共享内存,可以使用shmctl
函数的IPC_RMID
命令删除它。函数原型为:#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
是共享内存区域的标识符。cmd
为控制命令,可以使用IPC_RMID
表示删除共享内存。
这些函数提供了对共享内存的创建、连接、使用和删除的基本操作。
🐦key 与 shmid 区别
在上面所示的接口中,shmget 函数需要一个 key_t 类型的参数 key。而其他的函数多数用到 shmid 而不会用到 key,那么这两个参数分别是什么,有什么区别呢?
-
key
(键值):key
是一个整数,用于在共享内存创建过程中唯一标识一个共享内存段。- 它并不是由系统自动生成的,而是由应用程序提供的,通常以某种方式与程序的逻辑相关。
- 可以使用
ftok
函数将路径名和一个整数标识符转换为key
,以便在创建共享内存时使用。 key
通常用于在不同的进程之间共享相同的内存块,因此它是创建共享内存的关键参数之一。
-
shmid
(共享内存标识符):shmid
是一个由系统生成的标识符,用于标识已经创建的共享内存段。- 在使用
shmget
函数创建共享内存时,它通过返回值返回给调用者,用于后续的操作。 shmid
是由系统内核分配的,通常是一个唯一的整数。- 通过
shmat
函数将进程连接到共享内存时,需要使用shmid
作为参数。
总的来说,key
是在共享内存创建时由应用程序指定的用户定义的标识符,而 shmid
是由系统内核在共享内存创建时自动生成的系统级标识符。key
用于唯一标识共享内存的名字,而 shmid
用于在程序运行时标识特定的共享内存实例。
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 */
};
两个进程使用共享内存进行通信的前提是,如何让两个进程使用同一块共享内存。内存中有许许多多的共享内存,我们如何让两个进程使用一个共享内存呢?
这就要提到另一个函数 ftok
了。
ftok
函数是一个用于生成System V IPC(Inter-Process Communication,进程间通信)的键值的函数。它的主要用途是在创建System V IPC对象(如消息队列、信号量、共享内存)时,为这些对象生成唯一的键值。
函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
pathname
是一个与文件相关的路径名,用于生成键值。通常是指向一个存在的文件的路径。proj_id
是一个用户定义的整数,用于区分不同的IPC对象。在不同的IPC对象中,如果pathname
相同,而proj_id
不同,生成的键值也会不同。
ftok
函数通过将 pathname
转换为一个唯一的键值,以确保在不同的进程中使用相同的 pathname
和 proj_id
参数生成的键值是一致的。
一般来说,ftok
函数的使用场景是在创建System V IPC对象之前,通过调用 ftok
来生成一个唯一的键值。这个键值将被传递给诸如 msgget
、semget
、shmget
等函数,用于创建具体的消息队列、信号量或共享内存段。
需要注意的是,ftok
存在一些限制和注意事项,比如需要确保 pathname
指向的文件是存在的,否则 ftok
会返回 -1。此外,由于 proj_id
是一个整数,因此其范围应在0到255之间,以保证生成的键值在合理的范围内。
所以,key 是在内核中使用的,类比文件的 inode 编号。而 shmid 是给用户使用的,类比文件的文件描述符 fd。
🐧代码实例
接下来,我们就通过一个简单的代码设计来熟悉共享内存的使用。该设计的内容是,通过共享内存让两个进程(Server 与 Client)进行通信。
communicate.hpp
该头文件内提供shm所用到的函数方法。
/* communicate.hpp */
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <unistd.h>
using namespace std;
#define PATHNAME "." // 文件路径(用于key生成)
#define PROJID 0x12138 // 项目ID(用于key生成)
const int gsize = 4096;
key_t getKey() // 获取键值
{
key_t key = ftok(PATHNAME, PROJID);
if (key == -1)
{
cerr << "error: " << errno << " : " << strerror(errno) << endl;
exit(1);
}
return key;
}
static int createShmHelper(key_t key, int size, int flag)
{
int shmid = shmget(key, gsize, flag);
if (shmid == -1)
{
cerr << "error: " << errno << " : " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int createShm(key_t key, int size) // 创建共享内存
{
umask(0);
return createShmHelper(key, size, IPC_CREAT | IPC_EXCL | 0666);
}
int getShm(key_t key, int size) // 获取共享内存
{
umask(0);
return createShmHelper(key, size, IPC_CREAT);
}
char *attachShm(int shmid) // 关联共享内存
{
char *start = (char *)shmat(shmid, nullptr, 0); // 将共享内存段连接到进程地址空间
return start;
}
void detachShm(char *start) // 去关联
{
int n = shmdt(start); // 将共享内存段与当前进程脱离
assert(n != -1);
(void)n;
}
void delShm(int shmid) // 释放共享内存
{
int n = shmctl(shmid, IPC_RMID, nullptr); // 释放共享内存
assert(n != -1);
(void)n;
}
#define SERVER 1
#define CLIENT 0
class Init
{
public:
Init(int type)
: type(type)
{
key_t key = getKey();
if (type == SERVER)
shmid = createShm(key, gsize);
else
shmid = getShm(key, gsize);
start = attachShm(shmid);
}
char *getStart() { return start; }
~Init()
{
detachShm(start);
if (type == SERVER)
delShm(shmid); // 只有共享内存创建者才负责释放
}
private:
char *start; // 起始地址
int type; // server or client
int shmid;
};
#endif
Server
/* Server.cc */
#include "communicate.hpp"
#include <unistd.h>
using namespace std;
int main()
{
// 建立连接
Init init(SERVER);
char *start = init.getStart();
// 开始通信
int n = 0;
while (n <= 30)
{
cout << "client -> server# " << start << endl;
sleep(1);
n++;
}
return 0;
}
Client
/* Client.cc */
#include "communicate.hpp"
using namespace std;
int main()
{
// 建立连接
Init init(CLIENT);
char *start = init.getStart();
// 开始通信
char c = 'A';
while (c <= 'Z')
{
start[c - 'A'] = c;
c++;
start[c - 'A'] = 0;
sleep(1);
}
return 0;
}
效果展示
注意
当我们运行完一次程序后,再次运行程序会发生错误:
$ ./server
error: 17 : File exists
原因是,上次程序运行时创建的共享内存仍然存在,可以使用 ipcs -m
指令来查看已经有的共享内存:
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x3801203f 0 hxy 666 4096 0
由此可见,共享内存的生命周期是随系统的,不随进程。
我们可以使用 ipcrm -m
指令删除指定的共享内存:
$ ipcrm -m shmid
本章的内容到这里就结束了!如果觉得对你有所帮助的话,欢迎三连~