云计算百科
云计算领域专业知识百科平台

Linux进程间通信——System V共享内存

上一篇主要学习了管道。匿名管道适合父子进程之间传输数据,命名管道则可以让两个互不相关的进程进行通信。

但是管道有一个特点:数据通过读写接口在进程之间传递。

如果两个进程需要频繁交换数据,有没有更直接的办法?

这就要用到共享内存了。

一、共享内存是什么?

共享内存可以理解成一块由多个进程共同使用的内存区域。

普通情况下,每个进程都有自己的地址空间。一个进程修改自己的普通变量,另一个进程不会因此直接看到变化。

而共享内存不一样:系统把同一块共享内存分别映射到多个进程的地址空间中。一个进程把数据写进去,另一个进程就能从这块内存中读取数据。

在这里插入图片描述

这里需要注意:两个进程中的虚拟地址不一定相同,但它们可以映射到同一块共享内存。

共享内存建立映射之后,数据读写可以直接通过内存地址完成,不需要每次都通过 read()、write() 这样的系统调用传递数据。因此,在常见 IPC 方式中,共享内存的速度很有优势。

不过,速度快不代表所有问题都解决了。共享内存本身并不负责处理多个进程同时读写的问题,这一点后面还会单独讨论。

二、System V共享内存的使用流程

Linux 下使用 System V 共享内存,主要会接触下面几个函数:

ftok()
shmget()
shmat()
shmdt()
shmctl()

它们分别负责获取 key、创建或获取共享内存、挂接、脱离以及控制共享内存资源。

可以先记住整体流程:

获取 key
↓
shmget() 创建或获取共享内存
↓
shmat() 挂接到进程地址空间
↓
读取或写入共享数据
↓
shmdt() 脱离共享内存
↓
shmctl() 删除共享内存资源

这里有两个动作很容易混淆:脱离共享内存不等于删除共享内存,删除共享内存也不是简单地调用 free()。

三、第一步:生成key

如果两个进程要使用同一块共享内存,它们首先需要找到同一个共享内存对象。

key_t ftok(const char *pathname, int proj_id);

使用 ftok() 生成 key:

key_t key = ftok(".", 0x6666);

if (key == –1)
{
perror("ftok");
return 1;
}

ftok() 根据路径名和项目标识生成一个 key_t 类型的 key。

两个进程如果希望访问同一块共享内存,需要得到一致的 key。实际使用时也要注意,ftok() 生成的 key 并不是绝对唯一的,因此不能把它当成永远不会冲突的全局唯一编号。

四、第二步:创建或获取共享内存

获取到 key 以后,使用 shmget():

int shmget(key_t key, size_t size, int shmflg);

其中:

  • key:共享内存对应的 key。
  • size:共享内存段的大小。
  • shmflg:创建标志和权限标志。

例如,创建一个大小为 4096 字节的共享内存:

int shmid = shmget(
key,
4096,
IPC_CREAT | IPC_EXCL | 0666
);

if (shmid == –1)
{
perror("shmget");
return 1;
}

这里的 shmid 是共享内存的标识符,后面的操作会用到它。

IPC_CREAT 表示对象不存在时创建;IPC_CREAT | IPC_EXCL 则表示只有对象不存在时才创建,如果它已经存在,就返回错误。权限部分 0666 表示给出相应的读写权限,实际权限还会受到进程的权限环境等因素影响。

如果是另一个进程来获取已经创建的共享内存,可以使用相同的 key 调用 shmget(),并根据使用场景设置参数。

这也是服务端和客户端常见的分工:一个进程负责创建共享内存,另一个进程负责获取它。

五、第三步:把共享内存挂接到进程地址空间

shmget() 返回的 shmid 只是一个标识符,还不能直接拿来读写数据。

接下来需要调用:

void* shmat(int shmid, const void* shmaddr, int shmflg);

例如:

char* addr = static_cast<char*>(
shmat(shmid, nullptr, 0)
);

if (addr == reinterpret_cast<char*>(–1))
{
perror("shmat");
return 1;
}

这里将 shmat() 返回的地址保存到 addr 中。

参数 shmaddr 传入 nullptr,表示让系统选择挂接地址;shmflg 为 0 时,按照默认方式挂接。

需要特别注意,shmat() 失败时返回的是 (void*)-1,不是普通的空指针 nullptr。所以不能直接用 addr == nullptr 判断它是否成功。

挂接之后,就可以把它当作普通内存地址使用。

例如:

strcpy(addr, "hello from shared memory");

cout << addr << endl;

这里写入的数据存放在共享内存里,而不是只属于当前进程的普通局部变量。

六、第四步:另一个进程读取数据

假设服务端已经创建共享内存,客户端就可以使用同一个 key 获取它:

int shmid = shmget(key, 4096, 0);

if (shmid == –1)
{
perror("shmget");
return 1;
}

然后挂接:

char* addr = static_cast<char*>(
shmat(shmid, nullptr, 0)
);

if (addr == reinterpret_cast<char*>(–1))
{
perror("shmat");
return 1;
}

此时,客户端可以直接读取服务端写入的数据:

cout << addr << endl;

反过来,客户端修改这块共享内存,服务端也可以读取到对应的变化。

这正是共享内存的特点:双方不需要通过一连串的管道读写来传递内容,而是直接访问同一块共享区域。

当然,两个进程必须约定好数据的格式和使用方式。例如,一方写入字符串时,需要考虑字符串末尾的 '\\0';如果保存的是结构体,也要约定好字段和数据长度。

七、第五步:脱离和删除共享内存

使用完成后,需要进行清理。

1. shmdt:脱离共享内存

函数原型:

int shmdt(const void* shmaddr);

调用:

if (shmdt(addr) == –1)
{
perror("shmdt");
}

它的作用是让当前进程与这块共享内存脱离。

但有一点必须区分清楚:

shmdt() 只是取消当前进程的映射,不会直接删除整个共享内存段。

其他进程仍然挂接着它时,仍然可以继续使用。

2. shmctl:控制共享内存

控制共享内存可以使用:

int shmctl(int shmid, int cmd, struct shmid_ds* buf);

例如:

if (shmctl(shmid, IPC_RMID, nullptr) == –1)
{
perror("shmctl");
}

这里的 IPC_RMID 用于标记删除共享内存段。对于仍然有进程挂接的共享内存,实际释放会等到不再有相关挂接后完成。

所以,这几个动作可以记成:

shmat() → 挂接
shmdt() → 当前进程脱离
shmctl() → 控制资源,IPC_RMID用于删除

不能把 shmdt() 和 shmctl() 当成同一个操作。

八、示例代码

// Comm.hpp
#pragma once

#include <cstdio>
#include <cstdlib>

#define ERR_EXIT(m) \\
do \\
{ \\
perror(m); \\
exit(EXIT_FAILURE); \\
} while (0)

// Shm.hpp
#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "Comm.hpp"

const int gdefaultid = –1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;
#define CREATER "creater"
#define USER "user"

class Shm
{
private:
void CreateHelper(int flg)
{
printf("key: 0x%x\\n", _key);
_shmid = shmget(_key, _size, flg);
if (_shmid < 0)
{
ERR_EXIT("shmget");
}
printf("shmid: %d\\n", _shmid);
}
// 创建的一定要是一个全新的共享内存
void Create()
{
// 共享内存的生命周期, 随内核
CreateHelper(IPC_CREAT | IPC_EXCL | gmode);
}
void Get()
{
CreateHelper(IPC_CREAT);
}
void Attach()
{
_start_mem = shmat(_shmid, nullptr, 0);
if ((long long)_start_mem < 0)
{
ERR_EXIT("shmat");
}
printf("attach success\\n");
}
void Detach()
{
int n = shmdt(_start_mem);
if (n == 0)
{
printf("detach success\\n");
}
}
void Destroy()
{
if (_shmid == gdefaultid)
return;
Detach();
if (_usertype == CREATER)
{
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n > 0)
{
printf("shmctl delete shm: %d success!\\n", _shmid);
}
else
{
ERR_EXIT("shmctl");
}
}
}

public:
Shm(const std::string &pathname, int projid, const std::string &usertype)
: _shmid(gdefaultid),
_size(gsize),
_start_mem(nullptr),
_usertype(usertype)
{
_key = ftok(pathname.c_str(), projid);
if (_key < 0)
{
ERR_EXIT("ftok");
}
if (_usertype == CREATER)
Create();
else if (_usertype == USER)
Get();
else
{
}
Attach();
}
void *VirtualAddr()
{
printf("VirtualAddr: %p\\n", _start_mem);
return _start_mem;
}
int Size()
{
return _size;
}
void Attr()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
printf("shm_srgsz: %ld\\n", ds.shm_segsz);
printf("key: 0x%x\\n", ds.shm_perm.__key);
}
~Shm()
{
Destroy();
}

private:
int _shmid;
key_t _key;
int _size;
void *_start_mem;
std::string _usertype;
};

九、为什么共享内存退出后可能还在?

System V IPC 资源由内核管理,不会像普通进程的局部变量一样,随着进程结束就必然自动消失。

比如程序创建共享内存之后,如果异常退出,没有执行删除操作,那么再次启动程序时,可能会遇到:

shmget: File exists

这时可以用:

ipcs -m

查看系统中的共享内存段。

输出中能看到共享内存的 key、shmid、权限、大小以及当前挂接数量等信息。

如果确认某个共享内存段已经不需要了,可以使用:

ipcrm -m shmid

其中 shmid 要替换成实际查询到的共享内存标识符。

这些命令适合排查实验过程中遗留的 IPC 资源。不过,删除之前一定要确认它确实已经不需要,不能看到资源存在就随意删除。

十、共享内存没有自动解决同步问题

这一点是共享内存中最容易被忽略的地方。

假设客户端正在写:

ABCDE

而服务端同时读取。

如果读写时机没有协调好,服务端可能读到尚未写完的数据。多个进程同时访问共享数据,还可能覆盖彼此的修改。

所以,共享内存解决的是数据共享和传递,并不自动保证访问的顺序以及互斥关系。

对于需要互斥访问的共享数据,可以考虑使用信号量、互斥锁等机制。关键是要明确:什么时候允许写,什么时候允许读,多个进程同时操作时如何避免冲突。

十一、把这几个API记清楚就够了

刚接触 System V 共享内存时,不必一上来就记所有结构体字段。先把使用顺序记熟:

ftok()
获取 key

shmget()
创建或获取共享内存

shmat()
挂接,得到可访问的地址

读写共享数据

shmdt()
当前进程脱离

shmctl(IPC_RMID)
删除共享内存资源

实际编程时,还要注意 shmget()、shmat() 等函数的错误返回值,记得处理失败情况。

总结

共享内存的优势是数据映射完成以后,进程可以直接访问公共内存区域,省去了通过管道逐次传输数据的过程。

它的使用流程并不复杂,真正容易搞混的主要是三个地方:

一是 shmid 只是标识符,必须通过 shmat() 挂接以后才能访问数据。

二是 shmdt() 负责当前进程脱离,shmctl(IPC_RMID) 才负责标记删除资源。

三是共享内存本身不提供完整的同步与互斥机制。多个进程共享一块内存之后,仍然需要考虑读写顺序和数据竞争。

把这几点搞清楚,System V 共享内存的基本用法就算入门了。

赞(0)
未经允许不得转载:网硕互联帮助中心 » Linux进程间通信——System V共享内存
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!