进程间通信(IPC)2_共享内存_消息队列

17 篇文章 0 订阅
7 篇文章 0 订阅

进程间通信?

因为每一个进程都有一个虚拟地址空间, 保证了进程的独立性, 但也正是因此导致进程间无法通信. 所以需要操作系统提供进程间通信方式, 又因为通信的场景不同, 所以提供了多种不同的进程间通信方式。

进程间通信的目的

数据传输
资源共享
进程控制
通知事件

进程间通信方式:

System V 标准的进程间通信方式:
管道: 用于进程间的数据传输
共享内存: 用于进程间的数据共享
消息队列: 用于进程间的数据传输

关于管道的相关知识和代码可以参考这篇文章

共享内存

顾名思义, 共享内存就是进程都能访问到的一块物理内存.[多个进程将同一块物理内存映射到自己的虚拟地址空间]共享内存是System V版本的一个进程间通信方式,也是最快的进程间通信方式.
共享内存是系统为了进程间的通讯而预留的一块内存区域.如下突所示:
在这里插入图片描述
一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不在涉及到内核,这就是说进程不在通过执行进行内核的系统调用来传递数据.

/proc/sys/kernel/ 目录下, 记录着共享内存的一些参数信息, 可以根据实际需要对这些参数进行一定的修改

shmall,系统上可以使用的共享内存总量(bytes)
shmmax, 内核所允许的一个共享内存区的最大字节数(bytes)
shmmni,系统范围内最大共享内存区标识符数 (个)
在这里插入图片描述

共享内存特点

  • 最快的进程间通信方式
    其他进程间通信方式都使用了内核中的一块缓冲区, 通信时都会涉及用户态与内核态之间的两次数据拷贝, 而共享内存直接通过虚拟地址映射访问物理内存, 减少了两次用户态与内核态之间的数据拷贝,因此通信速度最快
  • 生命周期随内核
    在物理内存上开辟空间, 信息存储在内核中,是属于内核的一个进程间通信资源,不会随着进程的退出而释放; 内核重启会释放或者可以用户手动释放(使用 shmdt);
  • 不具备同步与互斥关系,操作不安全
    共享内存本身是不具备同步与互斥的特性的, 需要用户自己实现(一般与信号量结合使用实现进程间的同步与互斥)
  • 数据写入是一种针对地址指向空间的覆盖式写入

共享内存相关API

  1. 创建共享内存(开辟物理内存空间)
    int shmget(key_t key, int size, int shmflag) // 创建共享内存
    key:共享内存的标识符, 多个进程通过相同的标识符可以打开同一块共享内存
    size: 共享内存大小
    shmflag: IPC_CREAT ---存在则打开| IPC_EXEL ---与IPC_CREAT同时使用,若存在则报错,不存在则创建| mode权限
    返回值: 成功返回一个非负整数---共享内存的操作句柄, 失败返回 -1
    
    // 生成一个key值
    key_t ftok(const char *pathname, int proj_id);---通过inode节点号与projid合成一个key
    系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
  1. 将共享内存映射到进程的虚拟地址空间
    void *shmat(int shmid, void *shmaddr, int shmflg) // 共享内存操作句柄
    shmid: 返回的共享内存操作句柄
    shmaddr : 映射到虚拟地址空间的首地址, 通常置 NULL,
    shmflag : 映射成功之后对共享内存可以进行的操作  SHM_RDONLY (只读) 通常置 0 (表示可读可写)
    返回值: 成功返回映射的虚拟地址空间首地址, 通过这个地址对内存进行操作; 失败返回 (void*)-1
  1. 解除虚拟地址空间映射关系
  // 解除映射关系
    int shmdt(void *shmstart)  // 解除映射
    shmstart: 映射到虚拟地址空间的首地址
    成功返回0 , 失败返回 -1
  1. 释放共享内存资源
// 删除共享内存
    int shmctl(int shmid, int cmd, struct shmid_ds *buf)
    shmid: 共享内存操作句柄
    cmd : 具体对共享内存要进行的操作 -----常用: IPC_RMID--删除共享内存
    buf : 用于获取/设置共享内存信息的结构,不使用则置 NULL
    成功返回 0 失败返回 -1

操作系统中进程间通信资源指令:

// 查看进程间通信资源
ipcs -[选项]
-m 常看共享内存
-s 查看信号量
-q 查看消息队列

共享内存的操作流程

  1. 创建共享内存
  2. 将共享内存映射到进程的虚拟地址空间
  3. 通过虚拟地址空间进行内存操作
  4. 解除映射关系
  5. 释放共享内存资源
    注意: 删除共享内存时并不会立即释放共享内存资源, 因为如果立即释放共享内存资源,可能会造成正在访问该共享内存的进程崩溃. 所以删除共享内存时, 先将其映射 key 修改为0, 表示这块共享内存不在接受新的映射连接, 当这块共享内存映射链接数为0的时候, 共享内存自动被释放.

以代码为例, 加深对共享内存理解
创建共享内存,一端向共享内存写入数据, 通过另一端对该共享内存数据进行读取

读取共享内存数据:

/*                                                                                                            
   * 学习共享内存的操作流程并体会接口的基本使用*/
// 创建并读取共享内存数据
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#define IPC_KEY 0x12345678
 
int main()
{
    // 1. 创建共享内存
    /* int shmget(key_t key, int size, int shmflag) key--标识符 size--共享内存shmglag--标志位|权限*/
    int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
    if(shm_id < 0)
    {
        perror("shmget error");
        return -1;
        }
     // 2. 建立映射关系
     /* void *shmat(int shmid, void *shmaddr, int shmflg)
     * shmid---操作句柄
     * shmaddr---映射首地址
     * shmflag---操作权限*/
     void *shm_start = shmat(shm_id, NULL, 0);
     if(shm_start == (void*)-1)
     {
         perror("shmat error");
         return -1;
     }
     // 3. 进行内存操作
     while(1)
     {
         sleep(1.5);
         printf("[%s]\n", shm_start);
     }                                                                                                            
     // 4. 解除映射关系
     // shmdt(映射首地址 )
     shmdt(shm_start);
     // 5. 删除共享内存
     // shmctl(操作句柄, 要进行的操作--IPC_RMID, 共享内存信息结构)
     shmctl(shm_id, IPC_RMID, NULL);
     return 0;
}

向共享内存写入数据:

/*
 * 学习共享内存的操作流程并体会接口的基本使用*/
// 创建共享内存, 并写入数据
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#define IPC_KEY 0x12345678

int main()
{
  // 1. 创建共享内存
  /* int shmget(key_t key, int size, int shmflag) key--标识符 size--共享内存大小  shmglag--标志位|权限*/
  int shm_id = shmget(IPC_KEY, 32, IPC_CREAT|0664);
  if(shm_id < 0)
  {
    perror("shmget error");
    return -1;
  }
  // 2. 建立映射关系
  /* void *shmat(int shmid, void *shmaddr, int shmflg)
   * shmid---操作句柄
   * shmaddr---映射首地址
   * shmflag---操作权限*/
  void *shm_start = shmat(shm_id, NULL, 0);
  if(shm_start == (void*)-1)
  {
    perror("shmat error");
    return -1;
  }
  // 3. 进行内存操作
  int i = 0;
  while(1)
  {
    sleep(1);
    sprintf(shm_start, "%s+%d", "我好菜啊", i++);
  }
  // 4. 解除映射关系
  // shmdt(映射首地址 )
  shmdt(shm_start);
  // 5. 删除共享内存
  // shmctl(操作句柄, 要进行的操作--IPC_RMID, 共享内存信息结构)
  shmctl(shm_id, IPC_RMID, NULL);

  return 0;
}

结果:
在这里插入图片描述
在这里插入图片描述

消息队列

本质上是内核中的一个优先级队列, 多个进程通过向同一个队列中添加和获取节点实现通信,传输数据块, [其实现分为 System V标准的消息队列 和 POSIX标准的消息队列]

特点:
自带同步与互斥
生命周期随内核
数据传输自带优先级

为什么使用消息队列?

目前没有接触过必须要使用消息队列的项目, 所以简单总结下:
使用消息队列的优点:

  1. 通过异步处理提高系统性能(削峰、减少响应所需时间)
    不使用消息队列:
    不使用消息队列
    使用消息队列:
    在这里插入图片描述
    如上图,在不使用消息队列的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。
  2. 降低系统耦合性
    比如在下单系统中,
    在这里插入图片描述
    如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的耦合度就低。

如上图所示:消息队列可以利用发布-订阅模式工作,消息发送者发布消息,一个或多个消息接受者订阅消息。 从上图可以看到消息发送者和消息接受者之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响。

消息队列相关API:
头文件

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
  • 创建消息队列
int msgget(key_t key, int msgflg);
key: 同共享内存一样, 使用 ftok()产生,表示某个消息队列
msgflg: 
	IPC_CREAT: 消息队列不存在则创建, 存在则打开返回
	IPC_CREAT | IPC_EXCL : 两个选项同时使用--消息队列不存在则创建,存在则报错返回
	IPC_EXCL: 单独使用没有意义
return: 成功返回消息队列的标识码(非负整数), 失败返回 -1
  • 添加节点(将一条消息添加到消息队列中)
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
msqid: 消息队列的标识码
msgp: 是一个指向准备添加消息的指针
	消息结构一般形式如下:
	struct msgbuf {
               long mtype;       /* message type, must be > 0 */
               char mtext[1];    /* message data */
           };
	其中 mtext 可以是数组(也可以是其它结构--由调用者自定义), 大小由msgsz指定
msgsz: 指向的消息的长度
msgflg: 默认为0

return: 成功返回0 ,失败返回 -1
  • 从消息队列中获取节点(接受消息)
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
msqid: 消息队列的标识码
msgp: 是一个指向准备添加消息的指针
msgsz: 指向的消息的长度
msgtyp: 该参数等于0,读取队列中第一条消息; 大于0, 读取队列中第一条 msgtyp 类型的消息,(若在 msgflg 中指定了 MSG_EXCEPT,在这种情况下,队列中第一条类型不等于 msgtyp 的消息将被读取); 小于0, 队列中第一个类型小于或等于 msgtyp 绝对值的消息将被读取。
msgflg: 默认为0

return: 成功返回实际放到缓冲区里的字符个数, 失败返回 -1
  • 删除消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
msqid: 消息队列标识码
cmd: 三个选项: 
	IPC_STAT 将msqid_ds 结构中的数据设置为消息队列的当前关联值
	IPC_SET 将消息队列的当前关联值设置为 msqid_ds 数据结构中给出的值
	IPC_RMID 删除消息队列
buf: msqid_ds 数据结构如下:
	    struct msqid_ds {
               struct ipc_perm msg_perm;     /* Ownership and permissions */
               time_t          msg_stime;    /* Time of last msgsnd(2) */
               time_t          msg_rtime;    /* Time of last msgrcv(2) */
               time_t          msg_ctime;    /* Time of last change */
               unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
               msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
               msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
               pid_t           msg_lspid;    /* PID of last msgsnd(2) */
               pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
           };

参考:
https://www.jianshu.com/p/36a7775b04ec
https://blog.csdn.net/qq_35190492/article/details/103153444

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值