🐱作者:一只大喵咪1201
🐱专栏:《Linux学习》
🔥格言:你只管努力,剩下的交给时间!
进程间通信——共享内存 | 消息队列 | 信号量
system V是一种进程间通信策略,它包括共享内存,消息队列以及信号量。
共享内存区是最快的IPC(进程间通信)形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
- 共享内存也是由操作系统维护的共享资源。

两个进程的PCB各自维护着一个进程地址空间。当两个进程要进行通信时:
所以说,共享内存就是让不同的进程,看到同一块内存块。
在维持通信关系中,还涉及到几个概念:
- 挂接:将内存中创建好的内存块映射到进程的地址空间中。
- 去关联:不想通信时,取消进程和内存的映射关系。
注意: 去关联后,共享内存仍然存在,只是和去关联的进程没有了映射关系。

该系统调用接口就是用来在内存中创建共享内存的。它们的参数意义非凡,下面本喵来详细解释一下。
关于共享内存,首先需要理解几件事情:
共享内存多了,就需要有一个标识,让要通信的进程找到正确的共享内存。
key值就是共享内存的标识,让想要通信的进程双方看到同一块公共资源。
系统中既然存在很多个共享内存,操作系统势必要将它们管理起来,管理也是使用先描述再组织的方式。

管理共享内存并不是在管理内存块本身,而是在管理共享内存对应的结构体:
struct shm
{
key_t key;
size_t size;
//....
}
结构体类似与上面代码,包含许多共享内存的属性,最重要的是,结构体中有key值。
- 每创建一个共享内存,就会创建一个结构体对象,并且赋一个不同的key值。
- 所以这个key值就代表着一块唯一的共享内存。
问题又来了,怎么保证这个key值是唯一的呢?
从shmget函数的声明中可以看到,这个key值是我们传给操作系统的,也就是用我们传的key值来标定共享内存。
函数ftok()
这就需要用到另一个函数ftok(),来生成一个独一无二的key值。

- pathname:文件路径名,可以随意写,一般我们都写成当前路径"."。
- proj_id:项目ID,同样可以自定义,但是不能为0。
- 返回值:独一无二的key值。
该函数会根据我们传的路径名和项目id值生成一个key值,具体实现是通过一些算法实现的,我们不需要在意,只需要得到key值就行。
所以,在开辟共享内存之前,必须先使用ftok函数来生成一个独一无二的key值,这样才能保证我们内存块的标识是唯一的。
当两个进程通过key值和共享内存挂接起来后,就可以进行通信了。
结论:key值就是用来标识共享内存的唯一性的。
size
size是用来指定开辟的共享内存是多大的,以字节为单位。
操作系统在开辟共享内存的时候是以4KB为单位的。每次开辟的共享内存,最小也是4KB的。
- 假设我们指定4097字节大小的共享内存,但是在内存中实际开辟的共享内存是2*4KB的。
- 但是在使用的时候只能使用4097字节的空间,剩下的空间用户无法使用,操作系统也不会用,就浪费掉了。
所以,即使用不了那么大的空间,我们也要指定4KB的整数倍。
shmflg
这是一个标志位,和之前使用的open用法相似,也是一个int类型的数据,根据比特位不同,用法也不同。
常用的两个选项:
- IPC_CREAT:创建共享内存,如果不存在,创建新的,如果存在,获取相关信息。
- IPC_EXCL:无法单独使用,必须与其他标志组合使用。
IPC_CREAT | IPC_EXCL:创建共享内存,如果不存在,则创建,如果存在,错误返回。
IPC_CREAT | IPC_EXCL是专门给用户使用的,就是为了保证创建的共享内存是一块新的内存块。
返回值

- 创建成功返回共享内存的标识符,注意不是key值。
- 创建失败,返回-1。
来看看返回的值是什么样子:


- shmid也是连续的小整数,它和文件描述符fd一样,也是让用户使用的。
- 但是它和文件描述符代表的意义又不一样。
又存在一个问题,为什么返回的不是key值,而是shmid呢?key值也是唯一的啊。
在设计上是可以的,都是唯一的数字,用户在语言层和在系统使用的标识相同是没有问题的。
所以说,用户层使用shmid而不是key值是为了让用户层和系统层解耦。
结论:shmid是供用户使用的共享内存标识符。
shmget系统调用就是用来让通信双方获取同一块共享内存的。
刚刚我们使用shmget时创建了五个共享内存:

此时创建的5个共享内存就显示出来了。
此时,进程早已经结束了,但是共享内存还是存在,没有随进程的结束而消失。
所以说,共享内存的生命周期随内核,不随进程。
如果不想要这几个共享内存了,怎么删除呢?同样有指令:
- 指令:ipcrm -m shimd
- 功能:删除指定shimd标识的共享内存。

此时所有的共享内存就被删除了,但是需要我们手动的一个个去删除。
由于指令也是shell上运行的进程,也是属于用户层,所以操作共享内存时,使用的时shmid,而不是key值。
用命令行的形式未免也太麻烦了,所以有系统调用shmctl也可以用来删除共享内存,而且是自动的。

- shmid:获取共享内存后返回的标识符。
- cmd:指定控制共享内存的方式。
- buf:描述共享内存的数据结构指针。
cmd:IPC_RMID
删除是最常用的选项。


key值和shmid也打印了,说明共享内存创建成功了,但是使用ipcs查看时,发现什么都没有,证明shmctl将创建的共享内存删除了。
cmd:IPC_STAT
用来查看共享内存内核数据结构中的属性,不常用。

上图所示就是共享内存的数据结构struct_shmid_ds,也就是用来描述共享内存的数据结构,这只是其中的一部分。
该结构体的第一个成员变量是struct ipc_prem shm_prem,具体内容如红色箭头所指。可以看到,key值就在这里,共享内存就是通过它来标识唯一性的。
当cmd是IPC_STAT时,我们就可以获取到共享内存的属性:

此时共享内存内核数据结构中的属性信息就被我们看到了。
cmd可以传的值有很多,分别对应这不同的操作,但是最常用的还是删除。
shmat
shmat是让进程和共享内存挂接:

- shmid:创建共享内存后返回的标识符。
- shmaddr:指定共享内存映射到进程地址空间中的地址,一般设置成NULL,让系统自动来设置。
- shmflg:不用管它是啥,直接给0。
- 返回值:共享内存映射到进程地址空间中的地址。不成功返回-1,但是是void*类型的。

让该进程挂接共享内存,挂接成功打印映射到进程地址空间中的起始地址。

在运行的时候发现报错了,说我们没有权限,再查看共享内存,发现确实是创建了,但是共享内存的权限是0,也就是我们谁都不能访问。
解决办法就让给共享内存开发相应的权限:

- 在创建共享内存的时候,让其开发对拥有者的读写权限。
- 将0600和IPC_CREAT已经IPC_EXCL或在一起。

此时便挂接成功了,并且成功打印出了共享内存映射在进程地址空间中的起始地址。
shmdt
shmdt是让进程和共享内存去关联。

- shmaddr:要去关联的共享内存映射在进程地址空间中的起始地址。
- 返回值:成功返回0,不成功返回-1。
nattch值

在使用ipcs查看共享内存的时候,有一栏是nattch。


在进程和共享内存挂接后,查看共享内存信息,发现这个共享内存的nattch变成了1。
- nattch:表示和这块共享内存挂接的进程数量。

在和共享内存挂接以后,进行10s的延时,时间到进行去关联。

可以看到,在计时到之前,nattch的值是1,等计时到了,nattch的值变成了0,此时就将该进程和共享内存去关联了。
注意: 去关联后,共享内存还在,只是和进程没有联系了。
共享内存方式的进程间通信,用到的系统调用为shmget,shmctl,shmat,shmdt四个。
使用共享内存的方式,实现进程server和client之间的通信:
shmget,shmctl,shmat,shmdt四个系统调用通信双方都会使用,而且会对返回值进行严格判断,所以我们将这些共用的代码放在一个头文件中。
comm.h:

使用ftok生成key值,如果成功的话,返回key值,失败的话之间退出进程。

server方需要负责维护共享内存,所以就由它来创建,将创建的具体过程封装在一个函数里,只需要调用创建函数,传入一个key值就可以创建。
创建成功返回共享内存标识符shmid,失败的话退出进程。

client方只需获取共享内存的shmid即可,同样将具体实现封装,只需要调用获取函数,传入一个key值就可以,获取成功返回共享内存标识符,失败退出进程。

挂接共享内存时,同样将具体实现封装起来,只需要调用挂接函数,传一个shmid即可,挂接成功返回映射后的虚拟地址,失败则退出进程。
- (long long)memp == -1L解释说明:
- memp是一个void类型的指针,即使失败返回的-1也是void的,所以在判断时要将其强转为整形。
- Linux是64位的机器,所以指针的大小是8个字节,所以为了不发生数据截断,需要强转成对应8个字节的long long类型。
- 常量-1默认是int类型的,加一个后缀L成为-1L就表示这是一个long long类型的常量。
- 此时相同类型的数据才可以进行判断。

将具体判断细节放在函数中,使用的时候只需要调用函数即可,不用再判断。若去关联失败则进程退出。

调用shmctl的具体传参细节在函数中实现,删除的时候只需要调用删除函数即可,删除失败则退出进程。
comm.h头文件中的各个函数,都是为了通信双方更方便通信而设计的。
server.cpp和client.cpp:

通信双方的通信框架都是按照:
双方数据传送:

发送方一秒发送一次,接收方一秒接收一次。

- 必须先执行server,因为server负责维护共享内存,只有共享内存创建了才能进行通信。
- 在client没有发数据之前,server就开始读取数据了,但此时什么都没有。
- 当client开始发送时,server才能读取到数据。


可以看到,使用共享内存通信时,不像管道那样,通信双方是通过访问管道这个公共资源来实现数据交换的。
而是双方各自访问各自的虚拟地址就可以。
- 操作系统会通过各自的页表与共享内存建立联系。
- 所以双方各自访问自己进程地址空间中映射的起始地址时,就相当于访问到了同一块共享内存。
这样来看,双方各自管自己的就行,不用考虑对方,所以这种通信方式比较简单。
共享内存的优势:
考虑一个问题,通信双方一方写入,一方读取,采用管道和共享内存分别发生了几次数据拷贝。
管道:

一共需要四次。
- 键盘->写入端进程地址空间->管道->写出端进程地址空间->显示器
共享内存:

一共需要两次。
- 键盘->共享内存(写入写出端进程地址空间)->显示器
由于共享内存在双方各自的进程地址空间中都有映射,相当于三者是一个整体。
*如果考虑用户层缓冲区的话,两种方式各自再增加两次拷贝。
当通信的数据量非常大时,共享内存的方式大大减少了拷贝次数。
共享内存的劣势:

在发送方,有读取,有写入,每隔1秒发生一次。

在写入方,有写入,有读取,每隔3秒发生一次。

可以看到,结果非常混乱,两个进程想读就读,想写就写,这样就会导致数据混乱,通信的数据不准确。因为双方并不知道对方的存在。
- 原因是共享内存通信方式没有同步和互斥机制。
因为没有对公共资源的保护机制,所以就会导致通信混乱。
如果非要使用共享内存的方式,可以在通信双方之间再加入管道,利用管道的互斥机制来实现共享内存的互斥。有兴趣的小伙伴可以去试试。
共享内存的特征:
我们知道,操作系统中不仅有一块共享内存,它们是通过key值来进行唯一性标识的。
操作系统也要管理这些共享内存,上面本喵讲过采用的是先描述后组织的方式。

描述就是使用上图所示的结构体对象来描述的,该结构体中包含了共享内存的所有属性。
将这些描述共享内存的结构体对象组织成某一种数据结构,例如链表,这样操作系统就将共享内存管理起来了。
- 数据结构中存放的是描述共享内存结构体对象的指针。
这里这个指针有点特殊:

由于结构体的地址和结构体中第一个成员的地址在数值上相等,所以在访问结构体的时候,将第一个成员的指针强转成结构体类型的指针就可以。
消息队列的公共资源是链表结构。
通信双方不会和消息队列进行挂接,而是像管道一样,访问内存中的消息队列。

当发送方有数据发送时,将数据先打包成一个节点,然后尾插到内核中的消息队列中去。
当接收方接收数据时,从队列头部开始去找所需要的节点,然后进行解包得到数据。
- 消息队列和普通队列不一样,不是严格按照先进先出的规则。
- 读取方可以跳过队头寻找自己需要的数据。
- 但是相同的数据,必须先读取靠近队头的。
如上图中,当读取方需要的是香蕉,但是队头是苹果,此时就可以跳过苹果,读取香蕉,并且靠近队头的香蕉先被读取。
几乎和共享内存一样,本喵就不详细介绍了。
msgget()

- key:和共享内存一样,也需要生成,是消息队列唯一性的标识符。
- msgflg:和共享内存一样,可以是IPC_CREAT或者IPC_EXCL或者是二者的组合。
- 返回值:返回消息队列的标识符,供用户层使用。
msgctl()

参数和共享内存的shmctl一样。

这是消息队列属性描述的结构体。
- 第一个成员变量的类型是struct ipc_perm,变量名是msg_perm,结构类型和共享内存的一样。
只要是采用system V的通信策略,描述共享资源的结构体都是这个结构,第一个变量类型都是struct ipc_perm。
msgsnd()

- msgid:消息队列标识符
- msgp:要发送数据所在的数组,元素类型是 struct msgbuf
- msgsz:要发生的数据大小,以字节为单位
- msgflg:创建标记,如果使用IPC_NOWAIT,失败就会立即返回。
0:阻塞发送
IPC_NOWAIT:非阻塞发送- 返回值:失败返回-1,成功返回0
在发送数据之前,需要先将数据进行打包:
struct msgbuf
{
long mtype; //数据类型,必须大于0
char mtext[1];//要发送的数据
};
在发送的时候,需要将数据进行打包,按照上面的规则。
msgrcv()

- msgid:消息队列标识符
- msgp:接收的数据后要存放的地址
- msgsz:要接收的数据大小
- msgtype:发送方设定的数据类型标识
- 0:读取队列中的第一条消息(不在乎当前队列头元素是什么消息类型,将他当作普通队列来处理)。
- 大于0(约定值):读取队列中类型为msgtyp 的第一条消息。(就是读取对列元素中第一个香蕉)
- 小于0:读取队列中最小类型小于或等于msgtyp 绝对值的第一条消息。
- msgflg:创建标记,如果指定IPC_ NOWAIT,获取失败会立刻返回
- 0:阻塞接收
- NOWAIT:非阻塞接收
- 返回值:成功返回读取数据的字节数,失败返回-1。
信号量本质上就是资源计数器,能够保证多个进程之间访问临界资源,执行临界区代码时。
- 临界资源:多个进程都可以访问到的资源(例如:同一块内存)。
- 临界区:访问临界资源时的代码,所在区域称之为临界区。

如上图,将一大块公共资源划分成了多个小块的公共资源,假设一个有100个小块。
当进程想要访问某一小块资源的时候,首先要进行的就是申请信号量,一旦申请成功,信号量就会减1。
当这个进程访问完这块小资源后,需要将资源释放,这时候信号量就会加1。
当信号量为0的时候,进程就不能再申请了。
此时再来看,所有进程在访问公共资源之前,都必须申请信号量,而申请信号量的前提是所有进程都能看到同一个信号量,所以这个信号量本身就是公共资源。
既然信号量是公共资源,就必须在很多进程对它进行PV操作时保证自身的安全。
- 试想,当一个进程正在申请,但是这个进程申请的比较慢,还有几个其他进程也在申请,但是申请的快。
- 后面几个进程把信号量都申请完了,当第一个进程申请完成以后,发现信号量没了,此时就会出错。
当然这是一种极端情况,在PV操作的时候,可能会因为时序问题,导致信号量有中间状态,从而导致数据不一致。
所以为了保证信号量的安全性:
- 原子性:要么不做,要做就做完。
- 也就是,信号量有互斥机制保护,当一个进程在申请信号量的时候,其他要申请信号量的进程处于阻塞状态。
信号量的具体使用,在后面用到的时候本喵会详细讲解,在这里只需要了解这些就可以。
重点介绍了system V通信策略的共享内存方式,包括它的原理及应该,至于消息队列和信号量仅做了解就行,在后面用到的时候本喵会详细讲解。
作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代
在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',
我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是
ruby如何管理内存。例如:如果我们在执行过程中采用C程序,则以下是内存模型。类似于这个ruby如何处理内存。C:__________________|||stack|||------------------||||------------------|||||Heap|||||__________________|||data|__________________|text|__________________Ruby:? 最佳答案 Ruby中没有“内存”这样的东西。Class#allocate分配一个对象并返回该对象。这就是程序
目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称
最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总
深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal
我正在尝试使用以下代码通过将ffmpeg实用程序作为子进程运行并获取其输出并解析它来确定视频分辨率:IO.popen'ffmpeg-i'+path_to_filedo|ffmpegIO|#myparsegoeshereend...但是ffmpeg输出仍然连接到标准输出并且ffmepgIO.readlines是空的。ffmpeg实用程序是否需要一些特殊处理?或者还有其他方法可以获得ffmpeg输出吗?我在WinXP和FedoraLinux下测试了这段代码-结果是一样的。 最佳答案 要跟进mouviciel的评论,您需要使用类似pope
我目前正在用Ruby编写一个项目,它使用ActiveRecordgem进行数据库交互,我正在尝试使用ActiveRecord::Base.logger记录所有数据库事件具有以下代码的属性ActiveRecord::Base.logger=Logger.new(File.open('logs/database.log','a'))这适用于迁移等(出于某种原因似乎需要启用日志记录,因为它在禁用时会出现NilClass错误)但是当我尝试运行包含调用ActiveRecord对象的线程守护程序的项目时脚本失败并出现以下错误/System/Library/Frameworks/Ruby.frame
我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or