共享内存(System V)——进程通信

发布于:2025-03-13 ⋅ 阅读:(19) ⋅ 点赞:(0)

个人主页:敲上瘾-CSDN博客

进程通信:

目录

一、共享内存的原理

二、信道的建立

1.创建共享内存

1.key的作用

2.key的选取

3.shmid的作用

4.key和shmid的区别

5.内存设定的特性

6.shmflg的设定

2.绑定共享内存

3.代码示例

三、利用共享内存通信

1.通信

2.解除绑定

3.销毁共享内存

1.命令行销毁

2.程序中销毁

四、共享内存的生命周期

五、数据安全问题

六、源码

1.comm.hpp

2.server.cc 

3.client.cc


一、共享内存的原理

        共享内存是通过在物理内存上开辟一块空间,然后让需要通信的进程都映射到这一块空间,这样就使它们看到同一块资源了。

        共享内存通信是双向的,也就是说一个进程可以既读又写,使用起来就和C语言的malloc申请到的内存差不多。这种通信方式存在着数据安全问题,会在下文细说。 

二、信道的建立

1.创建共享内存

        创建共享内存使用shmget函数,它的作用是创建或获取共享内存段的系统调用。

        对于shmget的使用来说,虽然操作起来相对简单,但要完全理解其各种参数的设定则较为困难。不过接下来我会进行详细讲解。

shmget声明如下:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  • 参数key:用户设定任意一个数,用于区分不同共享内存,通常由ftok生成。
  • 参数size:设定共享内存的大小。
  • 参数shmflg:标志位,用于指定共享内存段的创建方式和权限。常见的标志包括:
    • IPC_CREAT如果共享内存段不存在,则创建它。
    • IPC_EXCL与 IPC_CREAT 一起使用,确保创建的共享内存段是新的
    • 权限标志:如 0666,表示所有用户都有读写权限。
  • 返回值: 
    • 成功时返回共享内存段的标识符(shmid)。
    • 失败时返回 -1,并设置 errno 以指示错误类型。

1.key的作用

  • 思考1:在用户层面如何让两个独立进程共享同一块内存?
  • 思考2:在匿名管道和命名管道中,用户层面是如何让两个进程确定同一个资源的?

        问题2很显然,管道的本质是文件,用户通过让两个程序打开同一个文件名来实现看到同一个资源。

因此,共享内存同样需要一个key来充当类似文件名的功能。

2.key的选取

        key参数本质是一个int类型,我们可以直接指定一个数值传入,当然,为了更规范,更专业,我们通常都会使用ftok来生成。

ftok的声明如下:

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
  • 参数pathnme:一个存在的文件路径(例如 /tmp/myfile),文件必须存在,否则 ftok 会失败。
  • 参数proj_id:一个整数,用于进一步区分不同的 IPC 对象。
  • 返回值: 
    • 成功:返回生成的 key_t 键值。

    • 失败:返回 -1,并设置 errno 以指示错误原因。

3.shmid的作用

        shmid是一个int类型,由shmget返回,在作用上和物理意义上与文件系统中的fd类似。它的作用主要是让用户找到指定的共享内存。而它的物理意义在这里就不提了,感兴趣的话可以等我的下一篇博文(IPC系统)发布。

4.key和shmid的区别

        key最终成为系统层区分不同IPC的标志,而shmid则是用户层用来区分不同IPC的标志。

5.内存设定的特性

这里的内存设定指的是shmget函数中的参数size。

        当传入的内存不足4096字节(4KB)的倍数时,会扩到4096倍数。但是只会提供size大小的使用空间。这样做可以规避掉一些因为共享内存过多带来的问题。

6.shmflg的设定

对于共享内存,我们可以将程序简单分为创建端和使用端,它们的shmflg设定通常是:

  • 创建端:IPC_CREAT | IPC_EXCL | 0666
  • 使用端:IPC_CREAT

创建端要保证IPC是最新的,所以需要加IPC_EXCL,然后还需要设定权限。

使用端只需要获取共享内存段的系统调用,因此只用一个IPC_CREAT即可。

2.绑定共享内存

        以上我们完成的只是共享内存的创建,接下来还需要把进程绑定到共享内存,使用函数shmat,其中at指的是单词attach。

shmat的声明:

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 参数shmid:传入从shmget中返回的shmid来指定共享内存。
  • 参数shmaddr:指定共享内存段附加到进程地址空间的位置,通常设为nullptr,系统会自动选择一个合适的地址。
  • 参数shmflg:读写方式,常用的有:
    • SHM_RDONLY:以只读方式附加共享内存段。
    • 0:以读写方式附加共享内存段。
  • 返回值:
    • 成功时,返回共享内存段附加到进程地址空间的起始地址。
    • 失败时,返回 (void *) -1,并设置 errno

3.代码示例

创建端程序:

int main()
{
    //生成一个key
    int key = ftok(".", 48);
    //创建共享内存
    int shmid = shmget(key, 4069, IPC_CREAT | IPC_EXCL | 0666);
    //连接到共享内存
    void* p = shmat(shmid,nullptr,0);
    //使用共享内存
    //... ...
    return 0;
}

使用端程序:

int main()
{
    //生成一个相同key
    int key = ftok(".", 48);
    //获取到共享内存的系统调用
    int shmid = shmget(key, 4069, IPC_CREAT);
    //连接到共享内存
    void* p = shmat(shmid,nullptr,0);
    //使用共享内存
    //... ...
    return 0;
}

注意:为了简洁和方便说明问题,以上代码省略了头文件的包含和返回值有效性的判断等等,在实际开发中可不敢省略。 

三、利用共享内存通信

1.通信

        上文我们只是完成了信道的建立,接下来我们进行通信,通过上面的操作,我们已经获取到共享内存的起始地址。

它的用法与C语言的malloc申请的内存用法相同,只是共享内存可以同时被两个进程访问。

如下写端:

int main()
{
    int key = ftok(".", 48);
    int shmid = shmget(key, 4069, IPC_CREAT | IPC_EXCL | 0666);
    void* p = shmat(shmid,nullptr,0);
    //使用共享内存
    char* chp = (char*)p;
    for(int i='a';i<='z';i++)
    {
        sleep(1);
        *chp=i;
        chp++;
    }
    return 0;
}

读端: 

int main()
{
    int key = ftok(".", 48);
    int shmid = shmget(key, 4069, IPC_CREAT);
    void* p = shmat(shmid,nullptr,0);
    //使用共享内存
    char* chp = (char*)p;
    while(true)
    {
        sleep(1);
        cout<<chp<<endl;
    }
    return 0;
}

注意:为了获取到同一个共享内存,我们设定的key必须一致。 

2.解除绑定

        如果进程退出时没有解除绑定,共享内存段仍然会保留在系统的共享内存资源中,直到显式删除(通过 shmctl 或系统重启)。

使用shmdt来解除绑定,其中dt代表单词delete。

shmdt的声明:

int shmdt(const void *shmaddr);

参数shmaddr:需要断开连接的共享内存的起始地址。

返回值:

  • 成功:返回0。
  • 失败:返回-1,并设置errno以指示错误原因。

        一个共享内存,与它绑定的程序的个数是由一个引用计数机制进行维护的,当shmdt成功,引用计数减1。 

3.销毁共享内存

        共享内存不会随程序的结束而销毁,它是随内核的,因此需要显式地进行销毁,可以使用shmctl函数。或在命令行中使用指令进行销毁。

1.命令行销毁

1.1.查看共享内存信息

ipcs -m

如下: 

这里解释一下nattch信息:它表示与该共享内存连接的程序个数。 

1.2.销毁共享内存

ipcrm -m 2

这里需要填入shmid(即这里的2)来指定共享内存。 

2.程序中销毁

在程序中销毁我们使用函数shmctl,其中ctl代表单词control。

shmctl声明如下:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 参数shmid:传入从shmget返回的shmid来指定需要销毁的共享内存。
  • 参数cmd:需要传入一个操作选项,操作选项很多,而IPC_RMID就是用来销毁共享内存的。
  • 参数shmid_ds:这是一个输出型参数,如果你需要获取共享内存的信息,则传入一个shmid_ds类型的指针来接收,如果不是通常传入nullptr即可。
  • 返回值:
    • 成功时返回 0
    • 失败时返回 -1,并设置 errno 以指示错误类型。

注:命令行销毁和程序中销毁效果是一样的,因为命令行销毁底层还是调用了shmctl函数。

四、共享内存的生命周期

        共享内存的生命周期是不随进程的,而是随内核,如果没有显示删除它就会一直存在,尽管相关的进程已经退出。直到重装系统才得以释放。

使用shmctl释放共享内存存在的情况

1.正常释放

        当nattach(引用计数)为0时,即没有进程与它绑定,被正常释放。

2.共享内存段被标记为已删除,但仍有进程附加(shmat)

        共享内存段已经被标记为已删除(不能附加到新的进程),但之前仍有一些进程附加到该共享内存段并正在使用。所以共享内存段不会被立即释放。只有当所有附加的进程都调用 shmdt 分离后,系统才会释放资源。

五、数据安全问题

        共享内存最大的优点就是,相比使用管道技术,它减少了中间复杂转化和拷贝工作,而是直接对物理内存进行访问。

        但它也有一个致命的缺点,相比管道技术,共享内存它的读端和写端是不带有同步机制的,这就很容易使得数据混乱,也就是造成数据不一致问题。

        比如我们写端写入“hello world”,而读端读到的可能是“he”,“ll”,“o wor”,“ld”等等无法预测的奇葩数据。 读端一个劲地读,不会管写端这句话是否已经说完,而且也无法知道。

        当我们不了解锁的情况下想要解决这个问题,可以利用命名管道来解决,因为命名管道带有同步机制,我们用它的write和read函数来保护数据的安全,当然write和read并不用写或读什么有意义的数据。

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!74c0781738354c71be3d62e05688fecc.png

六、源码

1.comm.hpp

#pragma
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#define SIZE 1024
#define KEY_NUM 0x66
#define PATH "."
#define CREATE "create"
#define USE "user"
#define ERROR(str)   \
	{                \
		perror(str); \
		exit(1);     \
	}

using namespace std;
class Shm
{
private:
	void Create(int flg)
	{
		_shmid = shmget(_key, _size, flg);
		if (_shmid < 0)
		{
			ERROR("shmget");
		}
		printf("shmget success id:%d\n", _shmid);
	}
	void Attach()
	{
		_start_mem = shmat(_shmid, nullptr, 0);
		if ((long long)_start_mem < 0)
		{
			ERROR("shmat");
		}
	}
	void Destroy()
	{
		int m = shmdt(_start_mem);
		if (m < 0)
		{
			ERROR("shmdt");
		}
		int n = shmctl(_shmid, IPC_RMID, nullptr);
		if (n < 0)
		{
			ERROR("shmctl");
		}
	}

public:
	Shm(string path, int projid, string user)
		: _shmid(-1), _size(SIZE), _usertype(user)
	{
		_key = ftok(path.c_str(), projid);
		if (_key < 0)
		{
			ERROR("ftok");
		}
		printf("ftok success id:%d\n", _key);
		if (_usertype == CREATE)
			Create(IPC_CREAT | IPC_EXCL | 0666);
		else
			Create(IPC_CREAT);
		Attach();
	}
	void *VirtualAddr()
	{
		return _start_mem;
	}
	~Shm()
	{
		Destroy();
	}

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

2.server.cc 

#include"comm.hpp"
int main()
{
	Shm sm(PATH,KEY_NUM,CREATE);
	char* ps = (char*)sm.VirtualAddr();
	while(true)
	{
		sleep(1);
		printf("%s\n",ps);
	}
	return 0;
}

3.client.cc

#include"comm.hpp"
int main()
{
	Shm sm(PATH,KEY_NUM,USE);
	char* ps = (char*)sm.VirtualAddr();
	for(char ch='0';ch<'z';ch++)
	{
		sleep(1);
		*ps = ch;
		ps++;
		*ps='\0';
	}
	return 0;
}