当前位置: 首页 > news >正文

Linux 进程间通信 共享内存_消息队列_信号量

共享内存

共享内存是一种进程间通信(IPC)机制,它允许多个进程访问同一块内存区域。这种方法可以提高效率,因为数据不需要在进程之间复制,而是可以直接在共享的内存空间中读写。

 使用共享内存的步骤通常包括:

  1. 创建共享内存:一个进程shmget()创建共享内存区域。
  2. 映射共享内存:其他进程将该共享内存映射到自己的地址空间中的共享区中。
  3. 读写数据:进程可以在共享内存中读写数据,进行通信。
  4. 解除映射和删除:使用完后,进程解除映射并清理共享内存。

shmget()创建共享内存 获取标识符

int shmget(key_t key, size_t size, int shmflg);

#include <sys/shm.h>

参数

  • key: 一个唯一的标识符,用于区分不同的共享内存段。可以使用 ftok 函数生成。
  • size: 请求的共享内存段的大小(以字节为单位)。
  • shmflg: 位图 控制标志,用于指定权限和其他选项。常见的选项包括:
    • IPC_CREAT: 如果共享内存段不存在则创建它。如果存在就获取它
    • IPC_EXCL| IPC_CREAT :一起使用,表示如果共享内存段不存在就创建它,反之,存在则返回错误。(返回成功一定是创建新的共享内存)
    • 权限标志,例如 0666 表示可读可写。

返回值

  • 成功时,返回共享内存标识符(非负整数)。(相当于FILE*)
  • 失败时,返回 -1,并设置 errno 以指示错误类型。

key作为标识符但不能由系统创建分配给进程,而是由用户自己创建并给进程,但要确保key是唯一的。

我们知道key是不同进程找到同一个内存空间的关键,要确保它们的key是一样的。如果是系统分配fey,A进程先创建了共享内存获得一个keg,B进程如果想访问A创建的共享内存,就要获取到key,但A B进程相互独立不可能从A进程中获取到key。

但如果是用户给,可以让用户先生成唯一的key,再把key作为全局变量写在AB进程的源代码中,这样AB进程就可以通过同一个key访问到同一个共享内存了。

如何生成唯一的key?

ftok()

key_t ftok(const char *pathname, int proj_id);
key_t key = ftok("/path/to/file", 'R'); // 'R' 是一个项目标识符

ftok()函数是一个用于生成唯一键值的系统调用,它接受一个文件路径和一个项目标识符(也可以是数字,通常是一个字符),并返回一个唯一的key。这个方法基于文件的 inode 号,因此只要文件存在且未被删除,返回的key就会是唯一的。

ipcs -m命令 显示所有的共享内存信息

ipcs -m 用于显示系统中当前存在的共享内存段的信息。

ipcs -m
key        shmid    owner    perms      bytes      nattch    status
0x12345678  12345   user     666        1024       2

这条命令会列出所有的共享内存段,包括它们的 shmid、键值、大小和其他信息。

bytes 1024 操作系统申请空间是按块为单位(4kb...)来申请空间的,4096*x。假设该操作系统块的大小为4kb如果你申请4097字节,还是会申请8kb空间,但只让你用4097字节的空间,越界就报错。

ipcrm -m命令删除共享内存段

ipcrm -m shmid

shmctl() 控制共享内存 IPC_RMID删除

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数

  • shmid:共享内存段的标识符,通常是通过 shmget 函数获取的。
  • cmd:命令,用于指定要执行的操作,可以是以下常用值之一:
    • IPC_STAT:将共享内存段的信息填充到 buf 指向的结构中。
    • IPC_RMID:标记共享内存段以便删除。
    • SHM_LOCK:锁定共享内存段。
    • SHM_UNLOCK:解锁共享内存段。
    • SHM_INFO:获取共享内存的统计信息(POSIX扩展)。
  • buf:指向 shmid_ds 结构的指针,用于存储共享内存的元数据。
  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 以指示错误原因。

shmat() 将共享内存挂接到自己的地址空间中

void* shmat(int shmid, const void* shmaddr, int shmflg);

参数说明

  • shmid: 共享内存段的标识符,通常通过 shmget() 获取。
  • shmaddr: 指定共享内存挂接到虚拟地址空间的起始地址。如果为 NULL,系统会选择一个合适的地址。
  • shmflg: 标志位: 0默认附加模式,进程可以读写共享内存。 SHM_RDONLY: 只读模式,进程只能读取数据,不能写入。

返回值

  • 成功时,返回指向共享内存挂接到虚拟地址空间的起始地址
  • 失败时,返回 (void*) -1,并设置 errno 指示错误类型。

注意共享内存也是有权限的,进程如果没有对应的权限是不能完成挂接的。

shmid = shmget(IPC_PRIVATE, size, IPC_CREAT | 0666); // 创建共享内存,权限为可读可写

在shmget()创建共享内存时,我们就可以设置权限0666:所有用户都可以读写。

如果不设置权限,默认权限是0600:创建者可读写 其他人无权限

shmdt()取消挂载

int shmdt(const void *shmaddr);

const void *shmaddr:挂载到进程的虚拟地址空间的起始地址 

shmdt()于从进程的地址空间分离已经附加的共享内存段。它不会删除共享内存段本身。

shmctl(IPC_RMID) :用于标记共享内存段为删除,等待所有附加的进程分离后释放资源。

共享内存通信速度最快

1.直接访问:共享内存允许多个进程直接访问同一块内存区域,避免了数据复制的开销。这与其他通信方式(如管道、消息队列等)不同,后者通常需要在进程之间复制数据

管道:数据从外设读取到进程A的内核空间,然后通过系统调用将数据写入管道。B通过系统调用从管道中读取数据,再将其拷贝到自己的内存中。

这种方式总共涉及四次拷贝:一次从外设到进程A的内核空间,一次从内核空间写入管道,一次从管道读取到进程B的内核空间,最后一次从内核空间到进程B的内存。

共享内存 :数据从外设读取到内核空间后,进程A和进程B可以直接访问共享的内存区域,避免了多次拷贝。

因此,总的拷贝次数减少为两次:一次从外设读取到内核空间,另一次是进程B直接从共享内存读取数据。

2.低延迟:由于不涉及操作系统内核的上下文切换,共享内存通信的延迟较低,特别适合需要高频率、低延迟的数据交换的场景。

3.高吞吐量:共享内存能够支持大量数据的快速传输,适合处理大规模数据或高并发的情况。

4.减少系统调用:其他通信机制往往需要进行系统调用,而共享内存可以减少这种需求,从而提升性能。

消息队列

os提供一个队列,A B进程都可以看到这个队列,把结构体struct data作为结点放入队列,再让另一个进程拿该节点,实现通信。

怎么知道这个节点是其他进程放的呢?
用户要自己创建struct data,里面有int type标识符,定义A进程标识符1 B为2,这样就可以区分拿与自己标识符不同的节点。

msgget() msgctl() 

这两个函数用法和shmget() shmctl() 差不多

int msgget(key_t key, int msgflg);

参数说明:

  • key: 消息队列的键值,可以通过 ftok() 函数生成。
  • msgflg: 控制消息队列的行为,通常使用以下标志:
    • IPC_CREAT: 如果消息队列不存在则创建一个。
    • IPC_EXCL: 如果消息队列已经存在,调用失败。
    • 权限位(如 0666)控制访问权限。

返回值:

  • 成功时返回消息队列的标识符(非负整数)。
  • 失败时返回 -1。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数说明:

  • msqid: 消息队列的标识符。
  • cmd: 控制命令,可以是以下之一:
    • IPC_STAT: 获取消息队列的状态信息。
    • IPC_SET: 设置消息队列的属性。
    • IPC_RMID: 删除消息队列。
  • buf: 指向 msqid_ds 结构的指针,用于存放或设置属性。

返回值:

  • 成功时返回 0。
  • 失败时返回 -1。

ipcs -q  ipcrm -m

ipcs -q 用于显示当前系统中所有的消息队列的信息。

------ Message Queues --------
key        msqid      owner      perms      used-bytes
0x12345678  12345      user1     666        0
0x87654321  12346      user2     666        0

ipcrm -m删除共享内存段

ipcrm -m <shmid>

msgsnd() msgrcv() 特有

msgsnd() msgrcv()是消息队列特有的函数

msgsnd() 发送消息

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数说明:

  • msqid: 消息队列的标识符。
  • msgp: 指向要发送的消息节点的指针,消息的结构体struct msgbuf要以标识符long mtype开头 
  • msgsz: 消息内容的字节数(不包括 mtype)。
  • msgflg: 自己的标识符,常用值包括 IPC_NOWAIT(如果队列已满则不等待,直接返回错误)。0: 默认行为,发送或接收消息时会阻塞,直到操作成功。

返回值:

  • 成功时返回 0。
  • 失败时返回 -1。

msgrcv() 接收消息

int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数说明:

  • msqid: 消息队列的标识符。
  • msgp: 指向接收消息的缓冲区的指针。指向struct data
  • msgsz: 消息内容的最大字节数(不包括 mtype)。
  • msgtyp: 指定接收消息的标识符mtype,可以是特定类型的消息(如 1),也可以是 0(接收任何类型的消息)或 -1(接收队列中最早的消息)。
  • msgflg: 控制接收行为的标志,常用值包括 IPC_NOWAIT(如果队列为空则不等待,直接返回错误)。

返回值:

  • 成功时返回接收到的消息字节数。
  • 失败时返回 -1。

信号量

信号量是一种用于进程间同步和互斥的机制,主要用于控制多个进程对共享资源的访问。它通过维护一个整型计数器来实现,计数器的值表示可用资源的数量或状态。

我们之前学过管道,我们把管道的资源看作一个整体,要写整个资源所有地方都可以写,要读都可以读。

现在我们把整个资源,分成多份,一个进程访问一份。整个资源,有部分可能在写入,还有部分可能在读,不同进程,访问共享资源,有一定并发性。

1.但是我们怎么知道整个资源里面还有多少份没有进程占用?

我们可以用一个计算器count来记录里面剩余个数,count=16 每进入一个进程count--(这种操作称作P操作),出去count++(V操作)。

  1. P操作(等待)

    • 当线程或进程希望访问某个共享资源时,它会执行P操作。这个操作的作用是检查信号量的值:
      • 如果信号量的值大于0,表示资源可用,线程可以继续执行,并且信号量的值减1。
      • 如果信号量的值为0,表示资源不可用,线程会被阻塞,直到信号量的值变为正。
  2. V操作(释放)

    • 当线程或进程完成对共享资源的使用后,会执行V操作。这个操作的作用是将信号量的值增加1,表示资源现在可用。如果有其他线程在等待这个资源,执行V操作后会唤醒其中一个等待的线程。

2.现在有出现一个问题怎么让不同进程看到同一个count呢?
可以用共享内存 管道 让进程与进程间建立联系就可以

3.但这样又有一个新问题,count++这种操作并不是原子性的,多个线程同时修改同一数据引发数据竞争。

使用二进制信号量(或互斥锁),可以确保在任何时刻只有一个线程或进程能够访问临界区(共享资源)。当一个线程进入临界区时,它会调用 P 操作(等待),如果信号量值为 0,其他线程会被阻塞,直到该线程调用 V 操作(释放),将信号量值加 1。

信号量可以分为两种类型:

1.二进制信号量。count==1

2.计数信号量。count>1

1.semget()

可以创建nsems个信号量,一个信号量管理count个资源。

int semget(key_t key, int nsems, int semflg);
  • key: 唯一标识符。
  • nsems: 信号量的数量。
  • semflg: 权限标志(如 IPC_CREAT0666)。

2.semctl()

控制信号量集的操作,如获取、设置信号量值或删除信号量。

int semctl(int semid, int semnum, int cmd, ...);
  • semid: 信号量集标识符。
  • semnum: 信号量在集合中的索引。
  • cmd: 操作命令(如 SETVALGETVALIPC_RMID 等)。

semnum:信号量集合允许我们同时管理多个信号量,semnum 就是用来指定我们想要操作的具体信号量。在一个信号量集合中,每个信号量都有一个唯一的索引,从 0 开始编号。例如,如果一个信号量集合中有三个信号量,索引将是 0、1 和 2。

3.semop()

 执行对信号量的操作,如 P(wait)和 V(signal)。

int semop(int semid, struct sembuf *sops, size_t nsops);struct sembuf {unsigned short sem_num;  // 信号量在集合中的索引short sem_op;           // 要执行的操作(正数表示 V 操作,负数表示 P 操作)short sem_flg;          // 操作标志(如 `IPC_NOWAIT`)
};
  • semid: 信号量集标识符。
  • sops: 指向 sembuf 结构数组的指针,定义了要执行的操作。
  • nsops: 数组中操作的数量。(对nsops个信号量进行PV操作)

ipcs -s 

显示当前系统中所有的信号量信息,包括信号量的标识符、拥有者、权限和其他相关信息。

------ Semaphore Arrays --------
KEY        ID      OWNER    PERMS      NSEMS
0x12345678 12345   user     666        1

System V怎么实现IPC的

1.应用角度,看IPC属性

struct shmid_ds里面是共享内存的属性,第一个成员变量就是struct ipc_prem结构体。而消息队列 信号量同样也是以struct ipc_prem结构体开头,struct ipc_prem结构体里面就保存着key值。也就是说不同 IPC 机制可以通过相同的方式来管理和控制访问权限,不同的IPC机制都有自己的一套key值,所以共享内存 消息队列 信号量的key可能重复,但在一种IPC机制中key是唯一的。

2.从内核角度,看IPC结构

有全局的结构体ipc_ids,里面有struct ipc_id_ary* entries指向结构体ipc_id_ary。

结构体ipc_id_ary最后一个成员变量是柔性数组,也就是说它可以动态保存kern_ipc_perm指针。kern_ipc_perm结构体里面保存的是IPC不同机制共同的属性。

为什么说kern_ipc_perm结构体里面保存的是IPC不同机制共同的属性?

因为在不同PC机制的属性中第一个成员变量就是kern_ipc_perm结构体,其他成员变量都是根据不同IPC的实现机制特别增加的。

为什么不同IPC机制的属性中第一个成员变量一定是kern_ipc_perm结构体?

因为结构体ipc_id_ary中柔性数组保存的是kern_ipc_perm指针,如果共享内存 消息队列 信号量它们属性第一个成员变量是kern_ipc_perm,那么就可以指向他们的结构体属性。

这样ipc_id_ary数组就可以找到每个创建的共享内存 消息队列 信号量的属性,进而进行管理。

其实我们shmget() msgget() semget()获取的id就是它们在ipc_id_ary数组的下标。

和key值一样,id在一种IPC机制中是唯一的,但在不同IPC机制中可能相同。

也就是说一个ipc_id_ary数组中指向的全是相同IPC机制的属性。

其实共享内存是一种文件

在共享内存的属性中有struct file*的指针,它指向一个文件,文件有inode,知道它的数据块的物理地址。vm_start vm_end是共享内存在虚拟地址空间的起始地址 结束地址,把它与指向文件的内存块的物理地址建立映射关系,进程可以通过这个映射直接访问文件内容。


http://www.mrgr.cn/news/64809.html

相关文章:

  • 鸿蒙进阶-AlphabetIndexer组件
  • 如何在 React 前端使用 Input 输入框的样式上传一个 Excel 文件,并读取文件内容转成 json 数据格式(对象数组)
  • 《欢乐饭米粒儿9》第五期:用笑声诠释生活,让爱成为日常
  • Selenium的下载及chrome环境搭建
  • 网络学习/复习4传输层
  • 【位域打包CAN报文】通过位域打包CAN报文的数据,实现精确控制每个(CAN信号)字段
  • 提高交换网络可靠性之端口安全配置
  • windows rdp 将远程技术嵌入到你的软件——未来之窗行业应用跨平台架构
  • 第四次:2024年郑州马拉松赛事记
  • 什么是三大范式, 为什么要有三大范式, 什么场景下不用遵循三大范式
  • 《GBDT 算法的原理推导》 11-15更新决策树的叶子节点值 公式解析
  • Linux内核编程(十八)ADC驱动
  • 深入解析RSA算法:加密与安全性
  • Spring DispatcherServlet详解
  • 在vue中 什么是slot机制,如何使用以及使用场景详细讲解
  • JWT 是什么?JWT 如何防篡改?JWT 使用【hutools 工具包】
  • python爬虫之JS逆向入门,了解JS逆向的原理及用法(18)
  • 003 配置网络
  • springBoot动态加载jar,将类注册到IOC
  • 【数据分析】怎么提升GMV
  • df_new_last.iloc[:,-1]与df_new_last.iloc[:,:-1]
  • Redis 的使⽤和原理
  • IT运维的365天--018 如何在内网布置一个和外网同域名的网站,并开启SSL(https访问),即外网证书如何在内网使用
  • Kubernetes中常见的volumes数据卷
  • SPI协议——笔记
  • cangjie仓颉程序设计-数据结构(四)