【Linux 系统】进程间通信(共享内存、消息队列、信号量)(下)

简介: 【Linux 系统】进程间通信(共享内存、消息队列、信号量)(下)

【Linux 系统】进程间通信(共享内存、消息队列、信号量)(上)https://developer.aliyun.com/article/1515665?spm=a2c6h.13148508.setting.20.11104f0e63xoTy

(2)代码

至此就完成了关联共享内存。


4、shmdt

有 shmgat 关联,那么也有相对应的 shmdt 去关联,也就是将共享内存段与当前进程脱离


(1)认识接口

  • shmaddr 是shmat 所返回的指针。

shmdt 是系统提供来取消关联共享内存的一个系统接口。shmdt 和 shmat 是同一个文档下,它就更简单了,只有一个参数。shmaddr 就是刚刚获取成功的共享内存的虚拟地址。

注意 :将共享内存段与当前进程脱离不等于删除共享内存段。


(2)代码

至此就完成了去关联共享内存。


5、shmServer 和 shmClient 开始通信

至此,创建共享内存、释放共享内存、关联共享内存、去关联共享内存这几个系统接口就介绍完了。那么 shmClient 端一定比 shmServer 端更简单,因为它不用再创建共享内存,自然也就不用它来释放共享内存,但是它需要关联和去关联共享内存。

编写和完善代码,测试挂接数量由 0 - 1 - 2 - 1 - 0。


shmServer.cc:

shmClient.cc:

运行结果:

既然已经将物理内存映射到进程的地址空间,那么进程就可以直接使用虚拟地址直接对物理内存进行真正访问,而不再需要用 read 和 write 这些系统调用接口了。

此时,client 和 server 就看到了同一份资源,这里就可以通过指针访问共享内存了,然后 client 每隔 2 秒向共享内存写入 abcd…xyz,server 每 1 秒向共享内存读出 abcd…xyz。

毫不意外的是,这里 server 是死循环,所以只要不终止,那么最后挂接数会由 2 变为 1,不过没关系,这里只是测试。可以看到如下测试结果,server 每一秒读一次,client 每二秒写一次,client 明显写的比较慢,但是 server 并没有等 client,所以共享内存机制并没有像管道机制那样有同步机制(这里读的时候可以不休眠的读,就可以看到更明显的现象了,就是 client 2 秒写的时候,server 才不管),所以共享内存不提供任何同步与互斥的操作,双方彼此独立,这里可能就会引起一些问题,比如 client 想写 Hello,然后让 server 干净的读,但是对于共享内存机制而言,server 只能等 client 写完才可以读。


【结论】

  1. 只要是通信双方使用 shm,一方直接向共享内存中写入数据,另一方就可以立刻看到对方写入的数据。共享内存是所有进程间通信(IPC),是速度最快的,因为不需要过多的拷贝(不需要将数据给操作系统)。
  2. 共享内存缺乏访问控制,会带来并发问题 【如果想一定程度的访问控制呢,能否实现?

对应的程序在加载的时候会自动构建全局变量,就要调用该类的构造函数 —— 创建管道文件。

程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件。


6、总结

  1. 共享内存的生命周期随 OS。
  2. 共享内存不提供任何同步与互斥的操作,双方彼此独立。
  3. 共享内存是进程间通信中速度最快的。
  4. 相比之下,管道就很慢了,它需要写端把数据写到管道,读端再从管道读,和管道的交互至少需要两次拷贝。还不包括如果写端的数据是从 stdin 中来的,那么就要先写到用户层缓冲区。
  5. 共享内存没有进行同步与互斥。
为什么创建共享内存的 SIZE 要设置成 4kb 的倍数?

因为系统在分配共享内存时是按 4kb 也就是一页为单位,所以如果申请 4097byte,那么操作系统在分配时会分配 4096 + 4096,也就是 8kb。但是 ipcs -m 时也确实只是 4097,这里系统确实是分配了 8kb,但是我们能使用的就是我们所申请的。换而言之,如果申请了 4097byte,那么就有可能浪费 4095byte,所以在创建共享内存时,建议 SIZE 大小是 4kb 的整数倍。

【key & shmid】

key 是一个用户层生成的唯一键值,它的核心作用是为了区分唯一性,它不能用来进行 ipc 资源的操作。

shmid 是一个系统给我们返回的 ipc 资源标识符(其实它也是一个数组下标,用于维护 ipc 资源),用来操作对应的 ipc 资源。

这里的 key 有点类似文件的 inode 号,shmid 有点类似文件的 fd。所以我们就可以理解在代码或命令访问共享内存时使用的是 shmid,而不是 key,原因是无论是代码或命令都是用户层上的操作共享内存。

【共享内存数据结构】

下图是操作系统给我们提供的一个系统调用头文件共享内存数据结构,而系统调用本来就是操作系统提供的,所以这个数据结构基本上和内核中描述共享内存的结构类似。

struct shmid_ds 这个结构体就是我们在上面所说的 struct shm_ipc,系统中存在着大量的进程和对应的共享内存,所以每个共享内存创建出来都有这样一个结构。

简单看一下,这其中有 shm_segsz 共享内存大小,shm_atime/shm_dtime 共享内存最近挂接和去挂接时间,shm_ctime 共享内存修改时间,shm_cpid 由 pid 进程创建,shm_lpid 由 pid 进程操作,shm_nattch 有几个进程挂接到共享内存,shm_unused 未使用的共享内存等。还有一个 shm_perm,我们找一下 ipc_perm。其中我们看到了熟悉的 key、mode。

我们发现在文档中,消息队列中有 struct msqid_ds,其中也有 struct ipc_perm msg_perm;信号量中有 struct semid_ds,其中也有 struct ipc_perm msg_perm。这里只想说明,所有 System V 标准下的通信方案,都有一个描述其对应资源的结构体。

可以看到,这里的共享内存、消息队列、信号量结构体下第一行都有一个 struct ipc_perm xxx,那我们就可以定义一个数组 struct ipc_perm array[1024]; 我们都知道这里有一个嵌套结构体,假设只知道内部 obj 的地址,那么 struct A a 的地址就同 &a.obj,此时 (struct A*)&a.obj,那么就可以访问 x 和 y 了。所以 Linux 就将所有的 ipc_perm 放在一个数组中,然后 &ipc_perm 再强制类型转换成共享内存或消息队列或信号量类型。换而言之,Linux 内核的 ipc 资源可以用数组来维护,也就是说如果以后想要创建一个 ipc 资源,那么系统会给我们一个 ipc_perm,然后再给我们对应的 ipc 资源的其它属性,使用 key 值保证唯一性,然后再把数组的下标返回。

可以看到如下 Linux 内核框架图,sem_array,msg_queue,shmid_kerne 它们的第一个成员都是 xxx.perm,经过强转就可以访问 kern_ipc_perm。


二、System V —— 消息队列(了解)

操作系统会在系统中维护一个消息队列,这个消息队列默认情况下是空的,当用户 1 创建消息队列时,就会用 key 来标识其唯一性,此时用户 2 就可以通过 key 来获取这个消息队列,那么两个用户就可以看到同一个消息队列了。然后用户 1 就可以往这个消息队列里放节点,用户 2 自然也能看到,反之也可以。这就是消息队列,消息队列 = 消息队列本身 + 为了维护消息队列内核所创建的数据结构。

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法。
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同类型值。
  • 特性:IPC 资源必须删除,否则不会自动清除,除非重启,所以 System V IPC 资源生命周期随内核。

1、接口

msgget 获取消息队列,msgctl 释放消息队列,msgsnd 发送消息队列,msgrcv 接收消息队列。


三、System V —— 信号量(了解)

1、相关概念的铺垫

信号量主要用于同步和互斥的,下面先铺垫一些概念。信号量跟上面的内容也有一些关联,同时也为后面的多线程部分做铺垫。

进程通信的本质是让进程看到同一份资源,当同一份资源被多进程看到时,极有可能出现当 A 进程正在对空间进行写入时,B 进程就来读取了。如果像管道那样自带同步机制倒也不会产生什么影响,实际上前面所讲的共享内存就是一种读写错乱的机制。

将多个进程同时看到的那一份资源叫做临界资源。我们仔细观察可以看到,在 server 和 client 中访问共享内存 / 临界资源的代码实际上只有少部分几行。换而言之,造成读写数据不一致问题的可能就是这一部分代码所引起的,我们将这部分访问临界资源的代码叫做临界区,所以为了必免数据不一致的问题,就需要保护临界资源,即对临界区代码进行某种保护,而这某种保护就被称为互斥

所谓互斥就是有一块空间,在任何时候有且仅能有一个进程在进行访问(生活中最典型的互斥场景就是去上洗手间),互斥本身是一种串行化执行(也就是说,共享内存中就是因为并行读写执行才导致的数据不一致问题),而后面一般互斥是通过锁来完成的,这里可以提一种二元信号量来完成串行执行(我们也能猜到加锁和解锁是有代码的) 。所以串行化的过程本质是对临界区资源加锁和解锁,从而完成互斥操作。也就是说,client 和 server 都必须遵守 “要进入临界区就得加锁,退出临界区就得解锁” 这一原则。

这里再感性的理解一遍原子性概念,其实说白了,就是要么做了,要么没做。比如一个进程想往共享内存里写 "Hello World",写完 "Hello" 时的这个状态叫做写入中,那么在写入过程中是不能被打搅的,得等到全部写完为止。也就是说,在其他人看来,这里写入过程的状态只有两种,其一是还没写,而其二是写完了,这就是原子性。

最典型的应用就是,假设我们在农商银行里有 1000 元,在建设银行里有 500 元,然后我们想进行转帐:农商账号 -= 200;建设账号 += 200。其中,当我们从农商账号转账到建设账号的时候,系统崩溃了,此时建设银行账号还是 500 元,但是农商银行账号少了 200 元。这个现象说白了就是当某个任务正在进行时,突然因为某些原因而导致任务中断,这就叫做不是原子性。所以这个转账的过程要不就不做,要不就必须得做成功,或者转账失败了也能保证农商账号的钱不受影响,这就是原子性。

我们也可以采用互斥的方案来保证原子性

  • 由于各个进程要求共享资源,而且有些资源需要互斥使用,那么各进程竞争使用这些资源,进程之间的这种关系就叫作进程的互斥。
  • 系统中某些资源一次只允许一个进程使用,这样的资源被称为为临界资源或互斥资源。
  • 在进程中涉及到互斥资源的程序段叫做临界区。
  • 特性:IPC 资源必须删除,否则不会自己清除,除非重启,所以 System V IPC 资源的生命周期随内核。

2、什么是信号量

信号量也叫做信号灯。举几个例子,假设我们买了一个房子,虽然我们没住在里面,但房子依旧是是属于我们的。在宿舍的时候,虽然没躺着,但是那个床位依旧是属于我们的。我们在网上买电影票看电影,虽然还没到时间去看,但我们很清楚,到了一定的时间我们就能看到。所以现实生活中存在很多 “预定机制”,因为不提前享受,所以在卖票的时候就得保证一人一座,不能超过电影院的承受能力。

这里有三个进程都要访问共享内存,这块共享内存就是这三个进程的临界资源,而要访问共享内存需要加锁,这里进程 A 先访问成功,然后解锁,紧接着进程 B,然后又进程 C,这就是互斥。

信号量的本质是一个计数器 int count(注意:这里的 int count 是错误的,先暂时这样理解,后面再详细解释),然后定义 int count = 3; 还有一段伪代码,任何进程想操作共享内存前必须先申请信号量。然后进程 A 要进来,所以 count- - 之后,count 是 2,而进程 A 要出去,也要对应进行释放信号量,也就是 count++。

这就类似于电影院的预订机制,电影院有 100 张票,我们预定了一张票,那么票数就变成 99 张,当我们看完离开电影院后,票数就变成 100 张。也就是说,申请信号量的本质就是:count- -,就是对临界资源的预定机制,count -- 后就一定要有资源给我们预留,而不是 pause,这里一共有 3 个资源,我们已经申请了一个,即使我们还没有开始访问,但最终我们也能访问,这就是一种预订,这就是信号量。所以信号量本质就是计数器,是用来描述临界资源中资源数目的计数器。


3、为什么要有信号量

有的时候进程并不是把共享内存全部使用,这里的共享内存被分为三块空间,有很多进程。如果想让不同的进程访问不同的共享内存区域,那么它们是不受影响的,但最怕的就是一个进程在访问一块空间的时候,另一个进程也来访问这块空间。这里想说明的是,进程不是对共享内存的整体进行访问,而是可能只使用共享内存中的一部分,所以只要多个进程访问的那部分共享内存是不重叠的,那么就可以并行访问。也就是说,有七八个进程,每个进程都把这个共享内存占有就是互斥,但这显然不太合理,所以允许在访问共享内存不重叠的前提下,可以允许少量进程同时访问,而这样的工作就是由信号量来完成的。


4、如何使用信号量

每个进程想对共享内存访问都必须先申请信号量,我们称之为 p 操作,而访问完之后要执行非临界区代码时要释放信号量,我们称之为 v 操作,所以信号量最重要的操作我们称之为 pv 原语

如果同时有 5 个进程都想访问共享内存,都想对计数器进行减减操作,那么下面有两个问题。

多个进程能不能操作同一个 count 值 ?

不能,因为有写时拷贝,我们定义全局变量,甚至 malloc。无论如何,只要子进程去进行操作时,不可能减减加加去影响其它进程的,count 一开始是 3,每个进程写时拷贝都认为是 3。所以信号量 != count,因为必须保证多个进程操作的是同一个信号量。

信号量是干什么的 ?

保护临界资源的安全性。


假设还认为信号量是一个类似全局变量,且多个进程能操作一个全局变量 count,那么每个进程去执行上面的伪代码不就行了吗 ?

不行。因为申请信号量过程中需要:

  1. 进行 if 判断
  2. 内存 --> cpu
  3. cpu 执行计算
  4. cpu --> 内存。

而此时进程 A 执行判断成功后,进程 B 已经减到 0 了,进程 A 再减就是 -1,相当于给别人多分配了资源,因为它是多条语句构成,有可能会导致操作乱序,有可能会多分配资源出去,所以就不是原子性的。

  • 计算是在 CPU 内的,数据存储在内存的 count 变量里面。
  • CPU 在执行指令的时候,首先将内存中的数据加载到 CPU 内的寄存器中(读指令),接着进行 count--(分析和执行指令),最后将 CPU 修改完毕的 count 写回内存中。
  • 执行流在执行的任何时刻都有可能会被切换。
  • 寄存器只有一套,被所有的执行流共享。但是寄存器里面的数据属于每一个执行流,属于该执行流的上下文数据。

每个进程都得先申请信号量,前提是每个进程都得先看到信号量。但如果每个进程都能看到信号量时,信号量本身就是一个临界资源,所以这样就变成了信号量原本是保护临界资源的,但自己却变成了临界资源。这当然有问题,你要保护其它人的前提是先保护好自己的安全,所以上面所讲的信号量 pv 操作,它本身就是原子的,所以它被称为 pv 原语,简单点来说,就是那个计数器本身就是原子的。在同一时间内,它只允许一个进程进行操作。

实现伪代码:假设这里有若干个进程要访问临界资源,那么首先只有进程 A 先申请锁成功,然后往下执行后 count = 2 解锁,进程 A 就可以访问共享内存的一部分了。另外进程 B 也在申请锁成功,然后往下执行后 count = 1 解锁,进程 B 就可以访问共享内存的一部分了。再另外进程 C … … count = 0 解锁,进程 C 就可以访问共享内存的一部分了。再另外进程 D 也申请锁成功,但是因为 count = 0,代表无多余的资源,此时就 goto 跳转到 begin,重复执行,此时就用这段代码,约束了访问临界资源的进程。接着进程 A 访问完毕,然后申请锁成功,count++ 变成 1,最后解锁成功。此时进程 D 申请锁成功,count 是 1 表示有资源可以访问,然后往下执行 count = 0 解锁,进程 D 就可以访问共享内存的一部分了。


在多进程环境下,如何保证信号量被多个进程看到 ?

只要使用系统提供的一批接口,就可以保证信号量被多个进程看到。

如果信号量计数器的值是 1,此时信号量的值无非就是 1 或 0,如果我们要申请信号量,但只让我们一个进程申请成功,这种信号量叫做二元信号量,其本质就是一种互斥语义。换而言之,信号量计数器的值 大于 1,它就是多元信号量。

  • semget 中 nsems 是系统可以允许你一次创建多个信号量,底层是用数组来维护这多个信号量,所以 ipcs -s 时 ,可以发现它是一个信号量数组。
  • semctl 中 semnum 是我们想对第几个信号量进行操作。
  • semop 是需要对特定的信号量传入 sembuf 结构,这个结构如下图,sem_op 对应上面所说的 pv 操作,如果是 -1,就表示对计数器 -1,如果是 +1,就表示对计数器 +1。nsops 是想对第几个信号量操作。
  • 共享内存的优点:进程间通信速度最快的。
  • 共享内存的缺点:不会维护同步和互斥机制。

可以看到 System V 标准下的 ipc 共享内存机制其实蛮复杂的, 但其实共享内存又是 System V 标准下最简单的一套机制,所以当我们看到这里的时候也不难,相对更复杂的是消息队列机制,最复杂的是信号量机制。实际在公司中很少自己写这些东西,特别是消息队列和信号量,所以目前就先了解共享内存机制,知道是其底层是怎么通信的即可。


相关文章
|
1月前
|
缓存 Java Linux
如何解决 Linux 系统中内存使用量耗尽的问题?
如何解决 Linux 系统中内存使用量耗尽的问题?
130 48
|
17天前
|
机器学习/深度学习 人工智能 缓存
【AI系统】推理内存布局
本文介绍了CPU和GPU的基础内存知识,NCHWX内存排布格式,以及MNN推理引擎如何通过数据内存重新排布进行内核优化,特别是针对WinoGrad卷积计算的优化方法,通过NC4HW4数据格式重排,有效利用了SIMD指令集特性,减少了cache miss,提高了计算效率。
35 3
|
19天前
|
监控 Java Android开发
深入探索Android系统的内存管理机制
本文旨在全面解析Android系统的内存管理机制,包括其工作原理、常见问题及其解决方案。通过对Android内存模型的深入分析,本文将帮助开发者更好地理解内存分配、回收以及优化策略,从而提高应用性能和用户体验。
|
21天前
|
机器学习/深度学习 人工智能 算法
【AI系统】内存分配算法
本文探讨了AI编译器前端优化中的内存分配问题,涵盖模型与硬件内存的发展、内存划分及其优化算法。文章首先分析了神经网络模型对NPU内存需求的增长趋势,随后详细介绍了静态与动态内存的概念及其实现方式,最后重点讨论了几种节省内存的算法,如空间换内存、计算换内存、模型压缩和内存复用等,旨在提高内存使用效率,减少碎片化,提升模型训练和推理的性能。
39 1
|
1月前
|
监控 Java Android开发
深入探讨Android系统的内存管理机制
本文将深入分析Android系统的内存管理机制,包括其内存分配、回收策略以及常见的内存泄漏问题。通过对这些方面的详细讨论,读者可以更好地理解Android系统如何高效地管理内存资源,从而提高应用程序的性能和稳定性。
68 16
|
1月前
|
消息中间件 Java Kafka
初识Apache Kafka:搭建你的第一个消息队列系统
【10月更文挑战第24天】在数字化转型的浪潮中,数据成为了企业决策的关键因素之一。而高效的数据处理能力,则成为了企业在竞争中脱颖而出的重要武器。在这个背景下,消息队列作为连接不同系统和服务的桥梁,其重要性日益凸显。Apache Kafka 是一款开源的消息队列系统,以其高吞吐量、可扩展性和持久性等特点受到了广泛欢迎。作为一名技术爱好者,我对 Apache Kafka 产生了浓厚的兴趣,并决定亲手搭建一套属于自己的消息队列系统。
68 2
初识Apache Kafka:搭建你的第一个消息队列系统
|
1月前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
1月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
260 1
|
19天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
28天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80