【Linux】进程间通信3——system V进程间通信

news2024/11/24 4:02:56

1.system V进程间通信

        管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。

system V IPC提供的通信方式有以下三种:

  1. system V共享内存
  2. system V消息队列
  3. system V信号量

其中,system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。

  • system V共享内存和system V消息队列就类似于手机,用于沟通信息;
  • system V信号量就类似于下棋比赛时用的棋钟,用于保证两个棋手之间的同步与互斥。

2.system V共享内存

共享内存,是一种进程间通信解决方案,并且是所有解决方案中最快的一个,在通信速度上可以做到一骑绝尘

这是 System V 标准中一个比较成功的通信方式,特点就是非常快

2.1.共享内存的基本原理

首先我们要明白,共享内存是为了让进程之间进行通信,所以共享内存一定也遵守着 让不同进程看到同一份资源 的原则,而共享内存可以让毫不相干的进程之间进行通信。

共享内存让不同进程看到同一份资源的方式就是:在物理内存中开辟一块公共区域,让两个不同的进程的虚拟地址同时对此空间建立映射关系,此时两个独立的进程能看到同一块空间,可以直接对此空间进行【写入或读取】,这块公共区域就是 共享内存

关于共享区:共享区作为虚拟地址空间中一块缓冲区域,既可作为堆栈生长扩展的区域,也可用来存储各种进程间的公共资源,比如这里的共享内存,以及之前学习的动态库,相关信息都是存储在共享区中

注意:
这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。 

2.2. 共享内存数据结构

在正式使用共享内存通信之前,需要先学习一下 共享内存的相关知识,因为这里的共享内存出自 System V 标准,所以 System V 中的消息队列、信号量绝大部分接口的风格也与之差不多

共享内存不止用于两个进程间通信,所以共享内存必须确保能持续存在,这也就意味着共享内存的生命周期不随进程,而是随操作系统,一旦共享内存被创建,除非被删除,否则将会一直存在,因此 操作系统需要对共享内存的状态加以描述

共享内存也不止存在一份,当出现多块共享内存时,操作系统不可能一一比对进行使用,秉持着高效的原则,操作系统会把已经创建的共享内存组织起来,更好的进行管理

所以共享内存需要有自己的数据结构,经过操作系统 先描述,再组织 后,构成了下面这个数据结构

注: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 */
};

当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。

可以看到上面共享内存数据结构的第一个成员是shm_permshm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:

struct ipc_perm
{
    __kernel_key_t key; 
    __kernel_uid_t uid;
    __kernel_gid_t gid;
    __kernel_uid_t cuid;
    __kernel_gid_t cgid;
    __kernel_mode_t mode; 
    unsigned short seq;
};

共享内存虽然属于文件系统,但它的结构是经过特殊设计的,与文件系统中的 inode 那一套结构逻辑不一样

共享内存的数据结构shmid_dsipc_perm结构体分别在/usr/include/linux/shm.h/usr/include/linux/ipc.h中定义。

2.3.逐步骤讲解共享内存的建立与释放 

如上图,这是两个普通进程A和B在系统上工作的原理图,现在想让进程A和B之间进行通信,共享内存的方法是如何做的呢?

        首先由通信的其中一方负责向系统申请共享内存通信,这里就让进程A负责好了,OS收到请求后,在物理内存划出一块内存区域,用来这保证了进程A和进程B能够看到并使用同一块内存空间,如下图的红色操作

        共享内存申请好了,但是并不意味着就可以直接用了,因为进程A,B的页表里并没有关于共享内存区域的映射,因此,进程A和B要分别与共享内存区域进行挂接,挂接的过程就是将共享内存区域的物理地址添加到进程的页表映射中,这样进程就能通过页表映射到共享内存区域了,如下图的蓝色操作

等到挂接完成后,进程A和B就能看到并使用同一块内存空间了,至此就可以开始通信,等到通信结束之后,通信双方要分别取消掉对共享内存区域的挂接操作,如下图绿色操作

取消挂接了并不算彻底结束了,因为共享内存的申请是直接在物理内存上进行的,不会随着进程的退出而释放,只有手动释放,或者系统重启的时候才会释放,因此,进程不再通信后,应当由共享内存申请方在进程退出前释放共享内存,如下图黄色操作 

至此,共享内存的原理已经完成,总共分成了4个步骤实现共享内存通信

总结下来就说下面这些步骤

共享内存的建立大致包括以下两个过程:

  1. 在物理内存当中申请共享内存空间。
  2. 将申请到的共享内存挂接到地址空间,即建立映射关系。

共享内存的释放大致包括以下两个过程:

  1. 将共享内存与地址空间去关联,即取消映射关系。
  2. 释放共享内存空间,即将物理内存归还给系统。

2.4.共享内存的创建

创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:

  FIFO表示先进先出,而管道其实就是一种队列,它的字节流就是先进先出。

 shmget函数的参数说明:

  • 第一个参数key,表示待创建共享内存在系统当中的唯一标识。
  • 第二个参数size,表示待创建共享内存的大小。
  • 第三个参数shmflg,表示创建共享内存的方式。

shmget函数的返回值说明:

  • shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
  • shmget调用失败,返回-1。

注意: 我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作。  

返回值

        因为共享内存拥有自己的数据结构,所以 返回值 int 实际就是 shmid,类似于文件系统中的 fd,用来对不同的共享内存块进行操作,

 传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取

        这里需要解释一下key,共享内存是用来进程间通信的,那么系统中那么多进程,肯定会存在很多的共享内存,那么系统要管理这些共享内存就要给这些共享内存标号,标明它的唯一性,这个key值就是这段共享内存在系统中的唯一性编号,通过这个唯一性编号,以及你要申请的共享内存的大小,系统就可以帮你申请一块共享内存了。

        key_t 实际就是对 int 进行了封装,表示一个数字,用来标识不同的共享内存块,可以理解为 inode

  传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取

ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。

简单来说就是给一个文件路径名和一个int值,那么ftok函数就会生成一个能唯一标识共享内存的key值,也就是shmget()函数中第一个参数的key值       

需要注意的是,pathname所指定的文件必须存在且可存取。 (将pathname 和 proj_id当成数据通过算法生成了一个序号id)

注意:

  • 使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
  • 需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。

当shmget()三个参数齐全,创建共享内存成功的时候,就会返回一个共享内存的标识码shmid,可能你会惊讶,刚才用ftok已经生成了标识共享内存的码,怎么这里又返回了一个?

        其实这两个码都可以用来标识共享内存,shmid与key的关系就类似于文件系统中的fd和inode,key就像inode一样,是给系统看的,

shmid和fd类似,是给应用层的进程使用的,为何要多此一举呢?

        这是为了系统层和应用层之间的解耦,避免因应用层的shmid出现错误而影响了系统层的正常工作

        参数2为创建共享内存的大小,单位是字节,一般设为 4096 字节(4kb),与一个 PAGE 页大小相同,有利于提高 IO 效率

        如果size设置成4097 ,在OS底层给你分配了2页(按页对齐),但是你要4097字节那么我就只让你看到4097个字节的空间,绝对不少给你但也不多给你,少给了可能会出问题,多给了也可能出问题,用户要我怎么办我就怎么办,严格按照用户来 ; 所以最好设置4096的整数倍。

参数3是位图结构,类似于 open 函数中的参数3(文件打开方式),常用的选项有以下几个:

  1. IPC_CREAT 创建共享内存,如果存在,则使用已经存在的
  2. IPC_EXCL 避免使用已存在的共享内存,不能单独使用,需要配合 IPC_CREAT 使用,作用是当创建共享内存时,如果共享内存已经存在,则创建失败
  3. 权限 因为共享内存也是文件,所以权限可设为文件的起始权限 0666

参数3常用的组合方式:

  • IPC_CREAT    如果不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则直接返回该共享内存的句柄
  • IPC_CREAT | IPC_EXCL    如果不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则出错返回

换句话说:

  • 使用组合IPC_CREAT,一定会获得一个共享内存的句柄,但无法确认该共享内存是否是新建的共享内存。
  • 使用组合IPC_CREAT | IPC_EXCL,只有shmget函数调用成功时才会获得共享内存的句柄,并且该共享内存一定是新建的共享内存。

 至此我们就可以使用ftok和shmget函数创建一块共享内存了,创建后我们可以将共享内存的key值和句柄进行打印,以便观察,代码如下:

#include <stdio.h>
#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <unistd.h>
		
#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名

#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

int main()
{
	key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
	if (key < 0){
		perror("ftok");
		return 1;
	}
	int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
	if (shm < 0){
		perror("shmget");
		return 2;
	}
	printf("key: %x\n", key); //打印key值
	printf("shm: %d\n", shm); //打印句柄
	return 0;
}

该代码编写完毕运行后,我们可以看到输出的key值和句柄值:

Linux当中,我们可以使用ipcs命令查看有关进程间通信设施的信息。

单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:

  • -q:列出消息队列相关信息。
  • -m:列出共享内存相关信息。
  • -s:列出信号量相关信息。

例如,携带-m选项查看共享内存相关信息:

 此时,根据ipcs命令的查看结果和我们的输出结果可以确认,共享内存已经创建成功了。

ipcs命令输出的每列信息的含义如下:

 注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于fd和FILE*之间的的关系。

注意: 因为共享内存每次都是随机生成的,所以每次生成的 key 和 shmid 都不一样

 2.5.共享内存的释放

通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。

这说明,进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。

此时我们若是要将创建的共享内存释放,有两个方法,

  • 一就是使用命令释放共享内存,
  • 二就是在进程通信完毕后调用释放共享内存的函数进行释放。

2.5.1.使用命令释放共享内存资源

我们可以使用ipcrm -m shmid命令释放指定id的共享内存资源。

注意: 指定删除时使用的是共享内存的用户层id,即列表当中的shmid。 

2.5.2. 使用程序释放共享内存资源

参数:

  • 第一个参数shmid,表示所控制共享内存的用户级标识符。
  • 第二个参数cmd,表示具体的控制动作。
  • 第三个参数buf,用于获取或设置所控制共享内存的数据结构。

返回值:

  • shmctl调用成功,返回0。
  • shmctl调用失败,返回-1。

shmctl函数的第二个参数传入的常用的选项:

 例如,在以下代码当中,共享内存被创建,两秒后程序自动移除共享内存,再过两秒程序就会自动退出。

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名

#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

int main()
{
	key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
	if (key < 0){
		perror("ftok");
		return 1;
	}
	int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
	if (shm < 0){
		perror("shmget");
		return 2;
	}
	printf("key: %x\n", key); //打印key值
	printf("shm: %d\n", shm); //打印句柄

	sleep(2);
	shmctl(shm, IPC_RMID, NULL); //释放共享内存
	sleep(2);
	return 0;
}

我们可以在程序运行时,使用以下监控脚本时刻关注共享内存的资源分配情况:

while :; do ipcs -m;echo "###################################";sleep 1;done

通过监控脚本可以确定共享内存确实创建并且成功释放了。

 2.6.共享内存的关联

共享内存在被成功创建后,进程还不 “认识” 它,只有让待通信进程都 “认识” 同一个共享内存后,才能进行正常通信,让进程 “认识” 共享内存这一操作称为 关联

当进程与共享内存关联后,共享内存才会 通过页表映射至进程的虚拟地址空间中的共享区中

将共享内存连接到进程地址空间我们需要用shmat函数,shmat函数的函数原型如下:

shmat函数的参数说明:

  • 第一个参数shmid,表示待关联共享内存的用户级标识符。
  • 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
  • 第三个参数shmflg,表示关联共享内存时设置的某些属性。

shmat函数的返回值说明:

  • shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
  • shmat调用失败,返回(void*)-1。

共享内存映射至共享区时,我们可以指定映射位置(即传递参数2),但我们一般不知道具体地址,所以 可以传递 NULL,让编译器自动选择位置进行映射

 第三个参数shmflg传入的常用的选项:

这时我们可以尝试使用shmat函数对共享内存进行关联。

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名

#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

int main()
{
	key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
	if (key < 0){
		perror("ftok");
		return 1;
	}
	int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
	if (shm < 0){
		perror("shmget");
		return 2;
	}
	printf("key: %x\n", key); //打印key值
	printf("shm: %d\n", shm); //打印句柄

	printf("attach begin!\n");
	sleep(2);
	char* mem = shmat(shm, NULL, 0); //关联共享内存
	if (mem == (void*)-1){
		perror("shmat");
		return 1;
	}
	printf("attach end!\n");
	sleep(2);
	
	shmctl(shm, IPC_RMID, NULL); //释放共享内存
	return 0;
}

 代码运行后发现关联失败,主要原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此server进程没有权限关联该共享内存。

我们应该在使用shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。

int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存

 此时再运行程序,即可发现关联该共享内存的进程数由0变成了1,而共享内存的权限显示也不再是0,而是我们设置的666权限。

注意: 程序运行结束后,会自动取消关联状态 

 2.7.共享内存的去关联

如同关闭 FILE*fdfree 等一些列操作一样,当我们关联共享内存,使用结束后,需要进行去关联,否则会造成内存泄漏(指针指向共享内存,访问数据)

取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:

这个函数使用非常简单,将已关联的共享内存地址传递进行去关联即可 

shmdt函数的参数说明:

  • 待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。

shmdt函数的返回值说明:

  • shmdt调用成功,返回0。
  • shmdt调用失败,返回-1。

注意:

  • 共享内存在被删除后,已成功挂接的进程仍然可以进行正常通信,不过此时无法再挂接其他进程

  • 共享内存被提前删除后,状态 status 变为 销毁 dest

现在我们就能够取消共享内存与进程之间的关联了。 

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名

#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

int main()
{
	key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
	if (key < 0){
		perror("ftok");
		return 1;
	}
	int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存
	if (shm < 0){
		perror("shmget");
		return 2;
	}
	printf("key: %x\n", key); //打印key值
	printf("shm: %d\n", shm); //打印句柄

	printf("attach begin!\n");
	sleep(2);
	char* mem = shmat(shm, NULL, 0); //关联共享内存
	if (mem == (void*)-1){
		perror("shmat");
		return 1;
	}
	printf("attach end!\n");
	sleep(2);
	
	printf("detach begin!\n");
	sleep(2);
	shmdt(mem); //共享内存去关联
	printf("detach end!\n");
	sleep(2);

	shmctl(shm, IPC_RMID, NULL); //释放共享内存
	return 0;
}

运行程序,通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联。

注意: 将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系。

2.8、共享内存控制 shmctl

System V 标准中还为共享内存提供了一个控制函数 shmctl,其原型如下图所示:

 

之前在释放共享内存时,我们就已经使用过了 shmctl,给参数2传入的是 IPC_RMID,表示删除共享内存,除此之外,还可以给参数2传递以下动作:

  • IPC_STAT 用于获取或设置所控制共享内存的数据结构
  • IPC_SET 在进程有足够权限的前提下,将共享内存的当前关联值设置为 buf 数据结构中的值

buf 就是共享内存的数据结构,可以使用 IPC_STAT 获取,也可以使用 IPC_SET 设置

当参数2为 IPC_RMID 时,参数3可以不用传递;其他两种情况都需传递 struct shmid_ds *buf

演示代码:通过 shmctl 获取共享内存的数据结构,并从中获取 pid、key

#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

using namespace std;

#define PATHNAME "." // 项目名
#define PROJID 0x29C         // 项目编号

const int gsize = 4096;
const mode_t mode = 0666;

//将十进制数转为十六进制数
string toHEX(int x)
{
    char buffer[64];
    snprintf(buffer, sizeof buffer, "0x%x", x);
    return buffer;
}


// 获取key
key_t getKey()
{
    key_t key = ftok(PATHNAME, PROJID);
    if (key == -1)
    {
        // 失败,终止进程
        cerr << "ftok fail!  "
             << "errno: " << errno << " | " << strerror(errno) << endl;
        exit(1);
    }

    return key;
}

// 共享内存助手
int shmHelper(key_t key, size_t size, int flags)
{
    int shmid = shmget(key, size, flags);
    if (shmid == -1)
    {
        // 失败,终止进程
        cerr << "shmget fail!  "
             << "errno: " << errno << " | " << strerror(errno) << endl;
        exit(2);
    }

    return shmid;
}

// 创建共享内存
int createShm(key_t key, size_t size)
{
    return shmHelper(key, size, IPC_CREAT | IPC_EXCL | mode);
}

// 获取共享内存
int getShm(key_t key, size_t size)
{
    return shmHelper(key, size, IPC_CREAT);
}
#include <iostream>
#include "common.h"

using namespace std;

int main()
{
    // 服务端创建共享内存
    key_t key = getKey();
    int shmid = createShm(key, gsize);

    cout << "getpid(): " << getpid() << endl;
    cout << "server key: " << toHEX(key) << endl;

    char *start = (char*)shmat(shmid, NULL, 0); //去关联
    if ((void*)start == (void*)-1)
    {
        cerr << "shmat fail!"
             << "errno: " << errno << " | " << strerror(errno) << endl;
        shmctl(shmid, IPC_RMID, NULL);  //即使异常了,也要把共享内存释放
        exit(1);
    }

    struct shmid_ds buf;
    int n = shmctl(shmid, IPC_STAT, &buf);
    if (n == -1)
    {
        cerr << "shmctl fail!"
             << "errno: " << errno << " | " << strerror(errno) << endl;
        shmctl(shmid, IPC_RMID, NULL);  //即使异常了,也要把共享内存释放
        exit(1);
    }

    cout << "==================" << endl;
    cout << "buf.shm_cpid: " << buf.shm_cpid << endl;
    cout << "buf.shm_perm.__key: " << toHEX(buf.shm_perm.__key) << endl;


    shmdt(start);   //去关联
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

 

通过程序证明了 共享内存确实有自己的数据结构

结论: 共享内存 = 共享内存的内核数据结构(struct shmid_ds) + 真正开辟的空间

3.用共享内存实现serve&client通信

在知道了共享内存的创建、关联、去关联以及释放后,现在可以尝试让两个进程通过共享内存进行通信了。在让两个进程进行通信之前,我们可以先测试一下这两个进程能否成功挂接到同一个共享内存上。

服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,之后进入死循环,便于观察服务端是否挂接成功。

服务端代码如下:

//server.c
#include "comm.h"

int main()
{
	key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
	if (key < 0){
		perror("ftok");
		return 1;
	}

	int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存
	if (shm < 0){
		perror("shmget");
		return 2;
	}
	
	printf("key: %x\n", key); //打印key值
	printf("shm: %d\n", shm); //打印共享内存用户层id

	char* mem = shmat(shm, NULL, 0); //关联共享内存

	while (1){
		//不进行操作
	}

	shmdt(mem); //共享内存去关联

	shmctl(shm, IPC_RMID, NULL); //释放共享内存
	return 0;
}

 客户端只需要直接和服务端创建的共享内存进行关联即可,之后也进入死循环,便于观察客户端是否挂接成功。

客户端代码如下:

//client.c
#include "comm.h"

int main()
{
	key_t key = ftok(PATHNAME, PROJ_ID); //获取与server进程相同的key值
	if (key < 0){
		perror("ftok");
		return 1;
	}
	int shm = shmget(key, SIZE, IPC_CREAT); //获取server进程创建的共享内存的用户层id
	if (shm < 0){
		perror("shmget");
		return 2;
	}

	printf("key: %x\n", key); //打印key值
	printf("shm: %d\n", shm); //打印共享内存用户层id

	char* mem = shmat(shm, NULL, 0); //关联共享内存

	int i = 0;
	while (1){
		//不进行操作
	}

	shmdt(mem); //共享内存去关联
	return 0;
}

为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一种key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行挂接。这里我们可以将这些需要共用的信息放入一个头文件当中,服务端和客户端共用这个头文件即可。

共用头文件的代码如下:

//comm.h
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名

#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小

先后运行服务端和客户端后,通过监控脚本可以看到服务端和客户端所关联的是同一个共享内存,共享内存关联的进程数也是2,表示服务端和客户端挂接共享内存成功。

此时我们就可以让服务端和客户端进行通信了,这里以简单的发送字符串为例。

客户端不断向共享内存写入数据:

//客户端不断向共享内存写入数据
int i = 0;
while (1){
	mem[i] = 'A' + i;
	i++;
	mem[i] = '\0';
	sleep(1);
}

 服务端不断读取共享内存当中的数据并输出:

//服务端不断读取共享内存当中的数据并输出
while (1){
	printf("client# %s\n", mem);
	sleep(1);
}

此时先运行服务端创建共享内存,当我们运行客户端时服务端就开始不断输出数据,说明服务端和客户端是能够正常通信的。

4.共享内存与管道进行对比

4.1.通信速度比较

当共享内存创建好后就不再需要调用系统接口进行通信了(直接对地址空间进行操作),而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式 

4.2.数据拷贝过程

read是把数据从内核缓冲区复制到进程缓冲区 , write是把进程缓冲区复制到内核缓冲区

当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。

 我们先来看看管道通信:

从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:

  1. 服务端将信息从输入文件复制到服务端的临时缓冲区中。
  2. 将服务端临时缓冲区的信息复制到管道中。
  3. 客户端将信息从管道复制到客户端的缓冲区中。
  4. 将客户端临时缓冲区的信息复制到输出文件中。

 我们再来看看共享内存通信:

从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:

  1. 从输入文件到共享内存。
  2. 从共享内存到输出文件。

5.共享内存的补充知识

5.1、共享内存的大小

在上面的代码中,我们将共享内存的大小设为 4096 字节,即一个 PAGE 页的大小(4kb);如果申请 4097 字节大小的共享内存,操作系统实际上会分配 8192 字节(8kb 的空间),但供共享内存使用的只有 4097 字节

为什么会出现这种现象?

  • 因为操作系统为了避免因非法操作导致出现越界访问问题,所以会开辟 PAGE 页的整数倍大小空间,多开辟的空间不会给共享内存时,主要是用来检测是否出现了越界访问

5.2.为什么共享内存是速度最快的IPC方法?

  • ① 共享内存的拷贝次数少
  • ② 在使用共享内存时不涉及系统调用接口(也就是不会有内核态到用户态之间的转化,因为都是在用户层进行操作的)
  • ③ 不提供任何保护机制(没有同步与互斥) 

5.3、共享内存的缺点

共享内存这么快,为什么不直接只使用共享内存呢?

因为快是要付出代价的,因为 “快” 导致共享内存有以下缺点:

多个进程无限制地访问同一块内存区域,导致共享内存中的数据无法确保安全
即 共享内存 没有同步和互斥机制,某个进程可能数据还没写完,就被别人读走了,或者被别人覆盖了

总的来说,不加规则限制的共享内存是不推荐使用的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1834415.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

哪个充电宝牌子好用又实惠?盘点四大平价充电宝分享

在当今快节奏的生活中&#xff0c;充电宝已成为我们日常生活中不可或缺的一部分。然而&#xff0c;面对市场上琳琅满目的充电宝品牌和型号&#xff0c;许多消费者误以为选择容量越大、价格越高的充电宝就是最好的选择。实际上&#xff0c;买充电宝并不是一味追求高容量和高价格…

首批50辆苏州金龙纯电大巴交付!武汉通勤客运绿色发展提质升级

随着第一缕阳光跃上黄鹤楼的飞檐&#xff0c;城市逐渐苏醒。在车水马龙中&#xff0c;一辆辆通勤班车穿梭其中&#xff0c;确保通勤保障单位人员的安全出行。而这其中就有武汉市雄翔通勤汽车运输有限公司&#xff08;以下简称“武汉雄翔”&#xff09;的身影。 5月底&#xff…

MySQL数据库与基本操作(增删改查)

一、数据库的基本概念 数据库要学习的四个基本概念&#xff0c;主要是&#xff1a;数据、数据库系统、数据库、数据管理系统。数据&#xff08;Date&#xff09;是描述事物的记录&#xff0c;数据库系统&#xff08;DBS&#xff09;&#xff0c;数据库管理系统&#xff08;DBMS…

大学物理(动量定理)

目录 冲量与动量 质点动量定理 质点系动量定理:​编辑 例题 ​编辑 例题 ​编辑 动量守恒定律:。 例题 ​编辑 例题 ​编辑 角动量 ​编辑 例题 ​编辑 力矩与角动量定理​编辑 角动量守恒定律: ​编辑 例题 冲量与动量 质点动量定理 质点系动量定理: 例题 例…

Java的三个接口Comparable,Comparator,Cloneable(浅拷贝与深拷贝)

Comparable 当我们要进行对象的比较的时候&#xff0c;我们是不能直接用>、< 这些符号直接进行比较的。 由于这是引用类型变量也是自定义类型变量&#xff0c;直接进行比较的时候&#xff0c;我们是通过对象的地址进行比较的&#xff0c;我们可以使用、! 进行两个对象的…

用这个神级提示词插件,能让你的AI绘画工具Stable diffusion提示词直接写中文!

大家好&#xff0c;我是设计师阿威 最近&#xff0c;有同学在使用AI绘画工具 Stable Diffusion的时候和我说&#xff1a;老师&#xff0c;我英文不好&#xff0c;能不能直接让我写中文提示词啊&#xff1f;最好可以直接在SD的输入框就能直接写中文&#xff0c;不用切换网页或者…

在有限的分数有限下如何抉择?是选好专业还是选好学校

随着2024年高考的落幕&#xff0c;无数考生和家长站在了人生的重要十字路口。面对成绩单上的数字&#xff0c;一个难题摆在了面前&#xff1a;在分数限制下我们该如何平衡“心仪的专业”与“知名度更高的学校”之间的选择&#xff1f; 一、专业决定未来职业走向 选择一个好的专…

低压电器航空插座端子

低压电器航空插座的定义和功能 低压电器航空插座通常指在交流电压1200V或直流电压1500V以下工作的电器&#xff0c;其主要功能是连接或断开电路&#xff0c;以实现对电路或非电对象的切换、控制、保护、检测、变换和调节。航空插座具有多种芯数和配置&#xff0c;例如2芯、3芯…

Java+Angular+Nginx+RESTful API 医院云HIS系统源码 全国中小型诊所都在用的诊所his系统门诊业务流程 自主版权

JavaAngularNginxRESTful API 医院云HIS系统源码 全国中小型诊所都在用的诊所his系统门诊业务流程 自主版权 HIS系统&#xff08;Hospital Information System&#xff09;在门诊业务中的应用带来了许多显著的优势&#xff0c;这些优势不仅提高了医疗服务的质量和效率&#xf…

如何经营好中医诊所?方法有哪些

在当今竞争激烈的医疗市场中&#xff0c;要想成功经营一家中医诊所&#xff0c;并不仅仅是提供传统的医疗服务&#xff0c;更需要与时俱进的战略思维和精细化的管理。过去被动获客、低效管理的模式已经不再适用&#xff0c;而如何拓展客源、提升服务质量、优化业务模式成为了中…

国际期货常见技术面分析

技术分析方法&#xff1a;通过对市场行为本身的分析来预测价格的变动方向&#xff0c;及主要是对期货市场的日常交易状况&#xff0c;包括价格、交易量与持仓量等数据&#xff0c;按照时间顺序绘制成图形、图表/形成一定的指标系统。然而针对这些图形、图表/指标系统进行分析研…

超多细节—app图标拖动排序实现详解

前言&#xff1a; 最近做了个活动需求大致类似于一个拼图游戏&#xff0c;非常接近于咱们日常app拖动排序的场景。所以想着好好梳理一下&#xff0c;改造改造干脆在此基础上来写一篇实现app拖动排序的文章&#xff0c;跟大家分享下这个大家每天都要接触的场景&#xff0c;到底…

经纬恒润国内首个物理区域控制器量产

当前&#xff0c;智能化汽车的电子电气架构正在从传统的功能域架构向新一代的中央计算加区域控制的架构演进中&#xff0c;国内新能源汽车厂商都在竞相基于新一代架构理念推出新平台车型。物理区域控制器可以实现车辆区域智能传感器及执行器配电、网关路由、信号采集以及执行器…

OpenSearch 与 Elasticsearch主要差异

1. 什么是 Elasticsearch&#xff1f; Elasticsearch 是一个基于 Apache Lucene 构建的开源、RESTful、分布式搜索和分析引擎。它旨在处理大量数据&#xff0c;使其成为日志和事件数据管理的流行选择。 Elasticsearch 还以其实时功能而闻名&#xff0c;允许用户在数据模式发生…

解决linux下载github项目下载不下来,下载失败, 连接失败的问题

第一步&#xff1a;打开/etc/hosts文件 linux vim /etc/hosts 第二步&#xff1a;文件拉到最下面&#xff0c;输入以下内容 linux #GitHub Start 140.82.113.3 github.com 140.82.114.20 gist.github.com 151.101.184.133 assets-cdn.github.com 151.101.184.133 raw.githubus…

中国最厉害的改名大师颜廷利:食物的真正人生意义是识悟

在探索人生意义的深邃征途中&#xff0c;我们本应以“识悟”为航标&#xff0c;不断扬帆远航&#xff0c;以实现自我的升华。然而&#xff0c;当回望人世繁华&#xff0c;古往今来&#xff0c;无论男女老少&#xff0c;似乎都在“食物”的陪伴下&#xff0c;徘徊往复&#xff0…

Ubuntu下安装和配置MariaDB

Ubuntu下安装和配置MariaDB 简介 MariaDB 是一个流行的开源关系型数据库管理系统,是 MySQL 的一个分支,由 MySQL 的创始人开发和维护。MariaDB 完全兼容 MySQL,并且提供了许多增强功能和性能改进。MariaDB 以其稳定性和高性能受到广泛使用,适用于各种规模的应用。本文将详…

深度神经网络——什么是NLP(自然语言处理)?

自然语言处理&#xff08;NLP&#xff09; 是对使计算机能够处理、分析、解释和推理人类语言的技术和工具的研究和应用。 NLP 是一个跨学科领域&#xff0c;它结合了语言学和计算机科学等领域已建立的技术。 这些技术与人工智能结合使用来创建聊天机器人和数字助理&#xff0c;…

企业数字化转型好帮手蚓链,超多创新亮点等你来!

家人们&#xff0c;今天必须给大家分享一下蚓链这个超棒的数字化转型好帮手呀&#xff01; 在理念创新上&#xff0c;它做到了以用户为中心&#xff0c;给大家带来精准化、个性化的营销体验呢。 组织创新也超厉害&#xff0c;搭建了开放式创新平台&#xff0c;吸引外部合作伙伴…

SyntaxError: EOL while scanning string literal

背景&#xff1a; 在对字符串使用in关系运算符时&#xff0c;报错SyntaxError: EOL while scanning string literal 原因&#xff1a; 这是因为${var}中有换行符\n导致的&#xff0c;通过Log ${var}可以看出换行符确实导致的字符串hello的引号位于两行&#xff0c;从而导致…