目录
- 简介
- System V共享内存
- 特点及用法
- 共享内存的创建
- 共享内存的关联与去关联
- 共享内存的删除
- 共享内存通信代码实现
- 总结
简介
System V共享内存是一种在Unix-like系统中广泛使用的共享内存机制。它是基于System V IPC(Inter-Process Communication,进程间通信)机制的一部分。System V共享内存提供了一块共享的内存区域,可以被多个进程同时访问。这种共享内存区域可以用于高效地在进程之间传递大量的数据,而无需进行数据的复制。相比较于命名管道和匿名管道的管道通信,共享内存区是最快的IPC形式。因为一旦这样的内存映射到共享它的进程地址空间,这些进程间数据传递就不再涉及到
内核,也就是说进程不再通过执行进入内核的系统调用来传递彼此的数据。
System V共享内存
共享内存的实现是通过将一块创建好的内存区域映射到多个进程的地址空间中,实现进程间的数据共享。共享内存的一个主要优势是用户进程可以直接在其地址空间中对共享内存中的数据进行操作,而无需频繁地进入内核空间。这与其他进程间通信机制(如管道)不同,它们通常需要在内核空间执行系统调用来进行数据传输。
当进程将共享内存连接到其地址空间时,操作系统会将共享内存区域的内容映射到进程的虚拟地址空间中的一个地址范围。这意味着进程可以像访问普通内存一样直接读写这个地址范围中的数据,而无需进入内核空间执行系统调用。尽管用户进程可以直接操作共享内存中的数据,但在多进程共享数据时,通常需要使用同步机制(如信号量或互斥锁)来协调进程之间的访问,以避免数据竞争和不一致性。这些同步机制可能需要进入内核空间执行,但这通常是在读写数据之前或之后的较少的操作。
特点及用法
-
创建和获取共享内存: 使用System V共享内存,首先需要使用shmget系统调用创建或获取一个共享内存标识符(ID)。该标识符用于标识共享内存区域。
-
连接和分离共享内存: 一旦获得共享内存标识符,可以使用shmat系统调用将共享内存连接到进程的地址空间,使进程可以访问共享内存中的数据。使用shmdt系统调用可以将共享内存从进程的地址空间中分离。
-
权限和控制: System V共享内存可以通过权限和控制来限制访问。
-
共享内存操作: 一旦共享内存连接到进程的地址空间,进程可以直接读取和写入共享内存中的数据。由于多个进程可以同时访问共享内存,因此需要使用同步机制(如信号量)来确保数据的一致性和避免竞态条件。
-
删除共享内存: 当不再需要共享内存时,可以使用shmctl系统调用删除共享内存区域。这将释放共享内存并使其不再可用。
共享内存的创建
创建共享内存需要用到系统调用接口shmget
函数,其原型为int shmget(key_t key, size_t size, int shmflg);
。其中三个参数作用如下:
-
key_t key:这是一个键值,用于唯一标识共享内存段。多个进程可以使用相同的键值来访问同一个共享内存段。
-
size_t size:指定共享内存段的大小,以字节为单位。这个参数用来确定需要多少内存来存储共享数据。
-
int shmflg:这是一个标志参数,用于指定共享内存的创建和访问权限,以及其他选项。
shmfig常用的标志包括:
IPC_CREAT:如果共享内存不存在,则创建新的共享内存段。
IPC_EXCL:与IPC_CREAT一起使用,如果共享内存已经存在,则返回错误。
例如 IPC_PRIVATE 或 IPC_CREAT | 0666,用于控制共享内存的访问权限。
使用shmget函数创建共享内存完成后返回一个整数值,通常表示共享内存的标识符。这个标识符用于在后续的共享内存操作中标识和访问特定的共享内存段。如果成功创建新的共享内存段或访问现有的共享内存段,则返回共享内存段的标识符,是一个非负整数。如果出现错误,则返回-1,并设置错误码。
对于创建共享内存的key值,按理来说是可以随便给的,只要保证你想新创建的这个共享内存的key值与之前的已存在的不冲突即可。因此,共享内存的key值一般用ftok
系统调用接口来生成。ftok函数是一个用于生成System V IPC键值的函数。它通常与共享内存、消息队列和信号量等进程间通信机制一起使用,以便多个进程可以识别和访问共享资源。
ftok函数有两个参数,其中pathname是一个指向路径名的字符串,通常用于关联键值。ftok函数会根据该路径名生成唯一的键值。通常,你可以指定一个存在的文件的路径作为参数,以确保不同进程使用相同路径名时生成相同的键值。proj_id是一个用户定义的项目标识符,通常是一个整数。可以为不同的共享资源(例如不同的共享内存)使用不同的proj_id,以确保它们拥有不同的键值。proj_id的范围通常是0到255。
ftok函数使用这两个参数来生成一个32位的键值(key_t类型)。这个键值在后续的IPC函数中用于识别和访问共享资源。需要注意的是,ftok生成的键值不是全局唯一的,而是在给定的pathname和proj_id组合下唯一。
共享内存的关联与去关联
在创建了共享内存后,还需要将共享内存链接到进程的地址空间中来,在通信结束后还需要将共享内存从自己地址空间中去除关联,因此还需要用到shmat
和shmdt
系统调用接口。
shmat函数用于将共享内存段附加到进程的地址空间,使得进程可以直接访问共享内存中的数据。他有三个参数,其作用分别为:
-
int shmid:共享内存段的标识符,通常是由shmget函数返回的值。
-
const void *shmaddr:指定共享内存段连接到进程地址空间的地址,通常设置为NULL,由系统选择合适的地址。
-
int shmflg:附加标志,可以是0或包含一些选项的标志,例如SHM_RDONLY表示只读模式。设置为0表示使用默认选项。
shmat函数返回一个void*指针,指向共享内存段在进程地址空间中的起始地址。如果附加失败,返回值是(void *)-1并设置错误码。
shmdt函数用于将共享内存段从进程的地址空间中分离,即不再让进程能够访问共享内存段中的数据。shmdt只有一个参数shmaddr,这是一个指向共享内存在进程地址空间中的起始地址的指针,通常是由shmat函数返回的地址。shmdt函数如果分离共享内存成功返回0,否则返回-1并设置错误码。
将共享内存段与当前进程脱离不等于删除共享内存段
共享内存的删除
在进程通信结束,所有进程都与共享内存没有关联时,就需要将共享内存进行删除。删除共享内存需要使用系统调用接口shmctl
函数。shmctl函数是用于控制(管理)共享内存的函数,它允许你对共享内存执行各种操作,如删除、获取信息、设置权限等。
shmctl函数有三个参数,shmid表示共享内存段的标识符,通常是由 shmget 函数返回的值,用于唯一标识一个共享内存段。cmd为命令参数,用于指定要执行的操作。可以取以下常用值:
IPC_STAT:获取共享内存的状态信息,并将结果存储在 struct shmid_ds 结构体中。
IPC_SET:设置共享内存的状态信息,需要提供一个填充好的 struct shmid_ds 结构体。
IPC_RMID:删除共享内存段。
struct shmid_ds *buf是一个指向 struct shmid_ds 结构体的指针,用于存储共享内存的状态信息(当 cmd 参数是 IPC_STAT 时),或者用于提供共享内存的新状态信息(当 cmd 参数是 IPC_SET 时)。当我们不关心状态信息时可以设置为nullptr。
shmctl 函数的返回值在不同的操作情况下有不同的含义:
-
当 cmd 参数是 IPC_STAT 时,shmctl 返回0表示成功,并将共享内存的状态信息存储在提供的 struct shmid_ds 结构体中。这时,你可以通过检查 struct shmid_ds 结构体中的字段来获取有关共享内存段的信息。
-
当 cmd 参数是 IPC_SET 时,shmctl 返回0表示成功,并且新的状态信息已经应用到共享内存段。
-
当 cmd 参数是 IPC_RMID 时,shmctl 返回0表示成功删除了共享内存段。此时,共享内存段将被销毁,不再可用。
-
如果 cmd 参数无效或操作失败,shmctl 返回-1,并设置全局变量 errno 以指示错误的类型。你可以使用 perror 函数或 strerror 函数来获取关于错误的更多信息。
总之,shmctl 函数的返回值主要用于指示操作是否成功,成功时返回0,失败时返回-1,并且错误信息可以通过 errno 获取。
共享内存通信代码实现
简单设计两个进程Server和client,让Server创建共享内存并负责接收Client发送来的数据,Client直接连接Server创建好的共享内存,然后向Server发送数据。最后Client发送完成后,去掉与共享内存的关联,然后退出。Server在接受完成后也去掉关联并删除共享内存。这样就完成了一次共享内存的通信过程。于是就可以写出这样的代码:
//comm.hpp
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstring>
#include <cassert>
#include <string>
#include <sys/stat.h>
//共享内存不随进程,随os
using namespace std;
#define pathname "."
#define proj_id 0x454
const int gsize = 4096;
key_t getKey()
{
key_t k = ftok(pathname, proj_id);
if(k == -1)
{
cout << errno << " : " << strerror(errno) << endl;
exit(1);
}
return k;
}
string toHex(int x)
{
char buffer[64];
snprintf(buffer, 64, "0x%x", x);
return buffer;
}
int createHelper(key_t k, int size, int flag)
{
int shmid = shmget(k, size, flag);
if(shmid == -1)
{
cout << errno << " : " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int createShm(key_t key, int size)
{
umask(0);
return createHelper(key, size, IPC_CREAT | IPC_EXCL | 0666);
}
int getShm(key_t key, int size)
{
return createHelper(key, size, IPC_CREAT);
}
void deleteShm(int id)
{
int n = shmctl(id, IPC_RMID, nullptr);
assert(n != -1);
}
char* attachShm(int id)
{
char* start = (char*)shmat(id, nullptr, 0);
return start;
}
char* detattachShm(char* start)
{
int n = shmdt(start);
assert(n != -1);
}
// enum{CLIENT = 0, SERVER};
#define CLIENT 0
#define SERVER 1
class Init
{
public:
Init(int type) : _type(type)
{
key_t key = getKey(); // 获取key
if(_type == SERVER) _shmid = createShm(key, gsize);
else _shmid = getShm(key, gsize);
_start = attachShm(_shmid); //绑定
}
char* getStart()
{
return _start;
}
~Init()
{
detattachShm(_start);
if(_type == SERVER) deleteShm(_shmid);
}
private:
int _type;
int _shmid;
char* _start;
};
//Server.cc
#include "comm.hpp"
#include <unistd.h>
int main()
{
Init s(SERVER);
char *start = s.getStart();
for(int i = 0; i < 26; i++)
{
sleep(1);
cout << " get massage : " << start << endl;
}
return 0;
}
//Client.cc
#include "comm.hpp"
#include <unistd.h>
int main()
{
Init c(CLIENT);
char* start = c.getStart();
char ch = 'A';
int cur = 0;
while(cur < 26)
{
start[cur] = ch + cur;
cur++;
sleep(1);
}
return 0;
}
总结
文章介绍了共享内存的通信机制,对共享内存的具体通信过程以及实现方式和涉及的系统调用函数都一一做了介绍,对于进程间通信来说,共享内存通信是一种高效的进程间通信方式,适用于需要频繁交换数据的多个进程,但要谨慎使用,以确保正确的同步和互斥,以及正确处理共享内存的生命周期。
最后,码文不易,如果觉得文章对你有帮助的话就点个小小的👍吧!