Linux:进程间通信(二.共享内存详细讲解以及小项目使用和相关指令、消息队列、信号量)
上次结束了进程间通信一:Linux:进程间通信(一.初识进程间通信、匿名管道与命名管道、共享内存)
文章目录
- 1.System V共享内存
- 1.1相关函数介绍
- 写个小项目
- 项目规划
- Cnmm.hpp
- ShmClient.cpp
- PipeServe.cpp
- 1.2指令查看与删除
- 2.System V消息队列
- 消息队列的原理
- 相关函数
- 3.System V 信号量
- 3.1概念
- 3.2周边知识
- 4.内核中
1.System V共享内存
实现进程间通信的前提就是如何让不同的进程看到同一份资源
- 匿名管道我们是通过子进程继承父进程打开的资源
- 命名管道是通过两个进程都打开具有唯一性标识的命名管道文件(路径+文件名)
- 共享内存其实是通过OS创建一块shm
System V共享内存(Shared Memory)是一种Linux中用于进程间通信(IPC)的机制。它允许多个进程访问同一块物理内存区域,从而实现数据的快速共享和交换。
-
原理:
- 在物理内存中申请一块内存空间作为共享内存。
- 将这块内存空间与各个进程的页表建立映射关系,使得这些进程在虚拟地址空间中可以看到并访问这块共享内存。
- 通过这种方式,多个进程可以像访问自己的内存一样访问共享内存,从而实现数据的快速共享和交换。
-
使用方式:
- 创建:使用
shmget()
系统调用来创建共享内存。这个函数会分配一块指定大小的内存区域,并返回一个标识符,用于后续对这块共享内存的操作。 - 关联:使用
shmat()
系统调用来将共享内存关联到进程的地址空间。这个函数会将共享内存的地址告诉进程,使得进程可以通过这个地址来访问共享内存。 - 取消关联:当进程不再需要访问共享内存时,可以使用
shmdt()
系统调用来取消关联。这个函数会断开进程与共享内存之间的映射关系。 - 释放:当所有进程都不再需要这块共享内存时,可以使用
shmctl()
系统调用来释放它。这个函数会回收这块内存区域,并释放相关的资源。
- 创建:使用
-
特性:
- 共享内存的生命周期是在进程结束后仍然存在的,直到显式地将其删除或者系统重启。这种情况下,如果进程没有主动释放共享内存,那么共享内存将一直存在于系统中,直到以下情况之一发生才会被释放:
- 代码删除:可以通过调用shmctl函数来删除共享内存,释放其资源。删除共享内存后,系统会立即释放共享内存的资源。
- 指令删除:在Linux系统中,可以使用ipcs命令查看系统中的IPC资源(包括共享内存),并使用ipcrm命令来删除特定的IPC资源
- 共享内存是所有进程间通信中速度最快的原因
无需内核参与:在共享内存中,多个进程可以直接访问同一块物理内存区域,而无需通过内核进行数据的拷贝和传输。这样可以避免了进程间切换和内核态和用户态之间的数据拷贝,从而提高了通信的效率。
- 共享内存并不提供进程间协同的机制,也不提供同步和互斥的功能,需要我们用户自己来实现
1.1相关函数介绍
ftok()
函数 Linux中用于生成一个唯一的键值(key)的系统调用,这个键值通常用于在进程间通信(IPC)中标识共享内存段、消息队列或信号量集。ftok()
函数基于一个已经存在的文件路径和一个非零的标识符(通常是一个小的正整数)来生成这个键值。
#include <sys/ipc.h>
#include <sys/types.h> key_t ftok(const char *pathname, int proj_id);
参数:
pathname
:指向一个已经存在的文件路径的指针。这个文件通常被用作生成键值的“种子”或“基础”。proj_id
:一个非零的标识符,通常是一个小的正整数。这个值将与文件路径一起被用于生成键值。返回值:
如果成功,ftok()
函数返回一个唯一的键值(key_t
类型),该键值可以在后续的 IPC 调用(如 shmget()
, msgget()
, semget()
等)中用作参数。如果失败,则返回 (key_t) -1
并设置 errno
以指示错误。
shmget()
:创建或获取共享内存
shmget()
系统调用用于创建一个新的共享内存对象,或者如果它已存在,则返回该对象的标识符。
函数原型:
int shmget(key_t key, size_t size, int shmflg);
参数:
-
key
:一个键,用于唯一标识共享内存对象。通常使用ftok()
函数生成。- 共享内存在内核中同时可以存在很多个,OS必须要管理所有的共享内存
- 如何管理呢?先描述,在组织
- 系统中会存在很多共享内存,怎么保证,多个不同的进程看到的是同共享内存呢? 要给共享内存提供唯一性的标识
key
便是那个唯一性标识符。那么为什么这个key要由我们用户来传入呢?
- 如果然系统生成,将值返回让我们得到。那我们如何给另外一个进程呢?要做到就要有进程间通信,这不倒反天罡了?
-
size
:共享内存的大小(以字节为单位)。 -
shmflg
:权限标志和选项。通常设置为IPC_CREAT
(如果对象不存在则创建,存在的话直接获取)和权限(如0666
)。若设置为
IPC_CREAT|IPC_EXCL
(如果对象不存在则创建,存在的话出错返回)
返回值:成功时返回共享内存对象的标识符;失败时返回-1并设置errno
。
shmctl()
:控制共享内存
shmctl()
系统调用用于获取或设置共享内存的属性,或者删除共享内存对象。
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid
:共享内存对象标识符。cmd
:要执行的操作。例如,IPC_RMID
用于删除共享内存对象,IPC_STAT
用于获取其状态。buf
:指向shmid_ds
结构的指针,用于传递或接收共享内存的状态信息。
返回值:成功时返回0;失败时返回-1并设置errno
。
shmat()
:将共享内存关联到进程的地址空间
shmat()
(attach)系统调用用于将共享内存对象关联到调用进程的地址空间。
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid
:shmget()
返回的共享内存对象标识符。shmaddr
:希望将共享内存附加到的进程的地址。如果设置为NULL,则系统选择地址(一般都这样)。shmflg
:通常设置为0或SHM_RND
(使附加地址向下舍入到最接近的SHMLBA边界)。
返回值:成功时返回共享内存附加到进程的地址;失败时返回(void *)-1并设置errno
。
shmdt()
:取消共享内存的关联
shmdt()
系统调用用于取消之前通过shmat()
附加到进程的共享内存的关联。
函数原型:
int shmdt(const void *shmaddr);
参数:
shmaddr
:shmat()
返回的共享内存附加到进程的地址。
返回值:成功时返回0;失败时返回-1并设置errno
。
写个小项目
项目规划
- Cnmm.hpp:函数的声明定义,头文件的包含、宏定义等任务
- ShmClient.cpp:客户端,
- ShmServer.cpp:服务端(服务器)
makefile:
.PHONY:all
all:shm_client shm_servershm_server:ShmServer.ccg++ -o $@ $^ -std=c++11
shm_client:ShmClient.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f shm_client shm_server
Cnmm.hpp
#pragma once#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>using namespace std;const char *pathname = "/home/zc/study/lesson26/5.4_shm_ipc";
const int proj_id = 0x1;
const int defaultsize = 4096; // 单位是字节key_t GetShmKeyOrDie()
{key_t key = ftok(pathname, proj_id);if (key == -1){cerr << "ftok error, errno : " << errno << ", error string: " << strerror(errno) << std::endl;exit(1); // 出错就直接退出}return key;
}int CreateShmOrDie(key_t key, int size, int flag)
{int shmid = shmget(key, size, flag);if (shmid < 0){std::cerr << "shmget error, errno : " << errno << ", error string: " << strerror(errno) << std::endl;exit(2); // 出错就直接退出}return shmid;
}int CreateShm(key_t key, int size)
{return CreateShmOrDie(key, size, IPC_CREAT | IPC_EXCL | 0666); // 没有就创建,有就报错,权限666
}int GetShm(key_t key, int size)
{return CreateShmOrDie(key, size, IPC_CREAT); // 不存在则创建,存在的话直接获取
}void DeleteShm(int shmid)
{int n = shmctl(shmid, IPC_RMID, nullptr);if (n == -1){cerr << "shmctl error" << errno << endl;}else{cout << "delete successfully" << endl;}
}void *ShmAttach(int shmid)
{void *addr = shmat(shmid, nullptr, 0);if ((long long int)addr == -1){std::cerr << "shmat error" << std::endl;return nullptr;}return addr;
}void ShmDt(void *addr)
{int n = shmdt(addr);if (n < 0){cerr << "shmdt error" << endl;}
}
ShmClient.cpp
#include "Comm.hpp"int main()
{// 获取keykey_t key = GetShmKeyOrDie();cout << "key:" << key << endl;// 获取shmidint shmid = GetShm(key, defaultsize);cout << "shmid:" << shmid << endl;// 进行挂接void *tem = ShmAttach(shmid);char *addr = (char *)tem;cout << "Attch successfully" << endl;// 进行通信,这里进行写入for (char ch = 'A'; ch <= 'Z'; ch++){addr[ch - 'A'] = ch;}// 取消挂接ShmDt(tem);return 0;
}
PipeServe.cpp
#include "Comm.hpp"
#include <unistd.h>int main()
{// 获取keykey_t key = GetShmKeyOrDie();cout << "key:" << key << endl;// 创建共享内存int shmid = CreateShm(key, defaultsize);cout << "shmid:" << shmid << endl;// 进行挂接void *tem = ShmAttach(shmid);char *addr = (char *)tem;cout << "Attch successfully" << endl;// 进行通信,这里进行读for (;;){cout << "shm content: " << addr << std::endl;}// 取消挂接ShmDt(tem);// 删除共享内存sleep(50);DeleteShm(shmid);return 0;
}
1.2指令查看与删除
- 使用ipcs命令查看系统中的共享内存:
ipcs -m
显示的以下内容:
- 键值 (key):共享内存段的键值,用于唯一标识一个共享内存段。在创建共享内存段时可以指定一个键值,其他进程可以通过这个键值来访问同一个共享内存段。
- shmid:共享内存段的标识符,是系统为每个共享内存段分配的唯一标识符。其他进程可以通过
shmid
来操作同一个共享内存段。- 权限 (perms):共享内存段的权限信息,包括读、写、执行权限。通常以八进制形式表示,比如
0600
表示用户具有读写权限,其他用户没有权限。- 拥有者 (owner):共享内存段的拥有者,即创建该共享内存段的用户或进程。
- 大小 (bytes):共享内存段的大小,以字节为单位。表示该共享内存段所分配的内存空间大小。
- 附加进程数 (nattch):当前附加到共享内存段的进程数目。当一个进程附加到共享内存段时,
nattch
数加一;当一个进程分离(detach)时,nattch
数减一。
- 使用ipcrm命令删除指定的共享内存:
ipcrm -m <shmid>
<shmid>
是要删除的共享内存段的标识符。通过这个命令可以删除指定的共享内存段,释放其资源。
shmid与key分辨:
在共享内存的设计中,
key
和shmid
的使用确实是为了实现内核层和用户层之间的解耦,从而使它们在宏观层面上互不影响,具有独立性。下面详细解释一下这种设计的好处和原因:
- 内核层使用
key
来唯一标识共享内存段,而用户层使用shmid
来访问和操作已存在的共享内存段。这种设计使得内核层和用户层的代码逻辑相互独立,彼此不直接依赖。- 当内核层需要进行某些代码改动或优化时,只需在内核层处理
key
的逻辑,而不需要影响用户层的代码。用户层代码不需要关心内核层的具体实现细节,只需要通过shmid
来操作共享内存即可。
2.System V消息队列
System V消息队列是一种进程间通信的机制,允许进程之间通过消息进行通信。消息队列提供了一个消息缓冲区,进程可以向消息队列发送消息,也可以从消息队列接收消息。下面我们来详细讲解消息队列的原理以及相关函数。
消息队列的原理
-
消息队列结构:消息队列是一个由内核维护的消息缓冲区,通常由消息类型和消息数据组成。每个消息都包含一个消息类型和消息数据,进程可以根据消息类型选择接收特定类型的消息。
-
消息发送:进程可以调用系统调用函数向消息队列发送消息,将消息写入消息队列中。发送消息时,需要指定消息队列的标识符、消息类型以及消息数据等信息。
-
消息接收:进程可以调用系统调用函数从消息队列接收消息,读取消息队列中的消息。接收消息时,可以选择阻塞式接收或非阻塞式接收,进程可以根据需要选择适合的接收方式。
-
消息类型:每个消息都有一个消息类型,进程可以根据消息类型选择接收特定类型的消息。这样可以实现不同类型的消息传递和处理。
-
消息队列管理:消息队列由内核管理,进程可以通过系统调用函数对消息队列进行创建、删除、发送消息、接收消息等操作。
相关函数
当使用System V消息队列相关函数时,需要了解函数的原型、参数和返回值。以下是这些函数的介绍:
msgget
:int msgget(key_t key, int msgflg);
- 参数:
key
为消息队列的键值,msgflg
为权限标志和操作标志的组合。 - 返回值:成功时返回消息队列标识符,失败时返回-1并设置
errno
。
- 参数:
msgctl
:int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 参数:
msqid
为消息队列的标识符,cmd
为控制命令,buf
为消息队列信息结构体指针。 - 返回值:成功时返回0,失败时返回-1并设置
errno
。
- 参数:
msgsnd
:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 参数:
msqid
为消息队列的标识符,msgp
为消息缓冲区指针,msgsz
为消息长度,msgflg
为标志位。 - 返回值:成功时返回0,失败时返回-1并设置
errno
。
- 参数:
msgrcv
:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 参数:
msqid
为消息队列的标识符,msgp
为消息缓冲区指针,msgsz
为消息长度,msgtyp
为消息类型,msgflg
为接收标志。 - 返回值:成功时返回接收到的消息数据的长度,失败时返回-1并设置
errno
。
- 参数:
3.System V 信号量
3.1概念
- 信号量本质:
-
信号量通常被用来保护公共资源,确保多个进程或线程能够有序地访问和使用这些资源
-
信号量是一个整型变量,通常用于控制对共享资源的访问。信号量的值可以表示可用资源的数量或者某个资源的占用情况
-
信号量:信号量本质是一个计数器,描述临界资源数量的计数器
为什么不能直接用一个int来进行呢?
无法在进程间共享 而且一个int
++
和- -
不是原子的 -
信号量:公共资源中,对局部性资源进行预定的机制。可以被用于对局部性资源进行预定,实现对资源的合理分配和控制
-
信号量是让不同的进程看到同一个计数器资源,而进程间通信是让不同的进程看到一份资源。所以信号量也属于进程间通信
进程申请信号量一旦成功:就一定有这个进程的资源了(相当于去看电影买票,一定有我们的座位了,而且别人也买不了这个座位)
当我们释放信号量后,这份资源才能给别人(看完电影后,这个座位才能接着被下一个买)
申请信号量和释放信号量来保护临界资源, 是大家都要遵守的规则(我们程序员)
- 所有的进程,访问临界资源,都必须先申请信号量, 所有进程都得先看到同一个信号量
==>
信号量本身就是共享资源
-
信号量操作:
那谁来保护信号量呢? 通过使用原子的PV操作来保障
- P操作(等待操作):当进程需要访问共享资源时,首先执行P操作,即尝试获取信号量。如果信号量的值大于0,则将信号量的值减1,表示资源已被占用,进程可以继续执行;如果信号量的值为0,则表示资源已被占用,进程需要等待。(允许申请多个信号量)
- V操作(释放操作):当进程使用完共享资源后,执行V操作,即释放信号量。释放信号量会增加信号量的值,表示资源已经释放,其他进程可以继续访问。
3.2周边知识
-
互斥和同步:
- 互斥:指的是在访问共享资源时,任何时刻只允许一个执行流访问,其他执行流需要等待。这样可以避免多个执行流同时修改共享资源导致数据不一致。互斥是通过使用锁或信号量等机制来实现的。(任何时候都只有一个人能访问,当信号量为1——只有一个整体资源时,不就是互斥吗)
- 同步:指的是在访问共享资源时,具有一定的顺序性,确保多个执行流按照特定的顺序访问资源。同步可以通过信号量、条件变量等机制来实现,以确保执行流之间的协调和顺序性。
-
临界资源和临界区:
- 临界资源:是指被保护起来的,任何时刻只允许一个执行流访问的公共资源。访问临界资源的代码段被称为临界区。
- 临界区:是指访问临界资源的代码段,程序员需要在临界区内使用互斥和同步机制来确保对临界资源的安全访问。
-
原子性:指的是操作对象的操作是不可分割的,要么整个操作完成,要么没有开始。在多线程或多进程环境中,保证操作的原子性是非常重要的,可以通过锁或原子操作等机制来实现。
-
保护临界区的本质:程序员需要保护临界区,确保在任何时刻只有一个执行流可以访问临界资源,避免数据竞争和不一致性。这通常通过使用锁、信号量或其他同步机制来实现。
4.内核中
内核中,所有的描述管理IPC资源的结构体,第一个成员大家都一样 kern_ipc_perm。我们可以用指针数组来进行管理
在内核中,对IPC资源的管理也是转变为对数组的增删查改
类型不同我们怎么解决呢?——直接强转
- (msg_ queue*)ipc[0]> q _time;
- (sem_array)ipc[1]->q_time;
- (shmid kernel)ipc[2]-> 。。。
那怎么知道是什么类型呢?
kern_ipc_perm里有一个有个mode变量,能来表示类型
#define IPC TYPE SHM Ox1 #define IPC TYPE MSG (0x1<<1) #define IPC TYPE SEM(0x1 << 2)//定义这三个宏后shmid kernel* (kern ipc_perm* p) {if (p->mode & IPC TYPE SHM){return (shmid kernel)ipc}else if (p->mode & IPC TYPE MSG){///}else{///} }
好啦,我也是结束了实训,才到家!!!
感谢大家的支持