了解消息队列 && 信号量

目录

  • [1. 消息队列](#1. 消息队列)
    • [1.1 基本原理](#1.1 基本原理)
    • [1.2 了解系统接口](#1.2 了解系统接口)
  • [2. 简谈 IPC 在内核中的数据结构设计](#2. 简谈 IPC 在内核中的数据结构设计)
  • [3. 信号量](#3. 信号量)
    • [3.1 理解信号量](#3.1 理解信号量)
    • [3.2 了解系统接口](#3.2 了解系统接口)

1. 消息队列

1.1 基本原理

消息队列也是进程间通信的一种方式,属于 IPC 通信模块中的一种,遵守 system V 标准。但不管是哪种通信方案,都是必须先让不同的进程看到同一份资源。

而这个所谓的 "资源",不同的通信方式代表不同,管道通信看到的资源是文件缓冲区,共享内存通信的资源是内存块(进程地址空间中的一段内存),而 消息队列通信的资源则是队列!

在消息队列通信时,双方进程都能以 数据块 的形式发送数据进行通信的,只不过这个数据块是 带有类型 的(因为双方进程都能够向队列发送数据块,因此需要区分数据块是哪个进程发送,所以需要带上类型表面数据块的身份,后续进程读取数据块时,则读取不属于它自己的数据块)

因为不同的进程都需要看到这个队列,因此这个队列不可能由某一方进程创建,这个队列肯定是操作系统创建的。而系统中肯定不止存在一个消息队列,因此操作系统需要对这些队列进行管理(先描述、再组织)。

1.2 了解系统接口

NAME
	msgget - get a System V message queue identifier

SYNOPSIS
    #include <sys/types.h>
	#include <sys/ipc.h>
    #include <sys/msg.h>

   int msgget(key_t key, int msgflg);		// 创建或获取消息队列

参数分析:
key: 与共享内存的 shmget 接口的 key 是一模一样的含义,key 的获取还是 ftok 接口,参数含义及用法与共享内存一致。
msgflg: 与共享内存的 shmget 相似。IPC_CREAT:消息队列不存在时创建,存在获取。IPC_CREAT | IPC_EXCL 不存在创建,存在出错返回

NAME
	msgctl - System V message control operations

SYNOPSIS
	#include <sys/types.h>
	#include <sys/ipc.h>
	#include <sys/msg.h>
	
	int msgctl(int msqid, int cmd, struct msqid_ds *buf);	// 释放消息队列

参数分析:
msqid:msgget 创建队列的返回值
cmd:对消息队列的描述结构体的操作标志,常用的有 IPC_STAT(拷贝)和 IPC_RMID(释放)
struct 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
	pid_t           msg_lrpid;    /* PID of last msgrcv(2) */			// 最后一个接收数据的进程的pid
};
// 这个与共享内存中的 ipc_perm 是一模一样的!它们就是同一个结构体,因为它们属于同一个标准 system V.
 struct ipc_perm {		
	key_t          __key;       /* Key supplied to msgget(2) */
	uid_t          uid;         /* Effective UID of owner */
	gid_t          gid;         /* Effective GID of owner */
	uid_t          cuid;        /* Effective UID of creator */
	gid_t          cgid;        /* Effective GID of creator */
	unsigned short mode;        /* Permissions */
	unsigned short __seq;       /* Sequence number */
};

共享内存创建好,需要挂接到进程地址空间才能使用,之后再去关联,释放等操作。而消息队列则是发送和接收数据,这也是唯一一处消息队列通信接口中与共享内存有点不一样的地方。

NAME
       msgrcv, msgsnd - System V message queue operations

SYNOPSIS
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

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

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

		struct msgbuf {		// 数据块
			long mtype;       /* message type, must be > 0 */		// 数据块的类型
			char mtext[1];    /* message data */					// 数据块的内容,可以自由设置大小(>0)
		};

参数分析:
msqid:msgget 的返回值,即具体某一个 消息队列的代表值
msgp:消息队列发送数据时是以数据块的形式发送的,因此这个参数传递的是数据块的起始地址
msgsz:数据块的大小
msgflg:设置为 0 代表以阻塞的方式发送信息,即如果队列内存不足,会阻塞等待直到队列中有可用空间为止,也可以设置为 IPC_NOWAIT 非阻塞等待。	
msgtyp:指定接收哪种类型的数据块

因为消息队列也属于 IPC 通信的一种,因此也可以通过 ipcs -q 查看系统中存在的消息队列, ipcrm -q msgid 删除指定消息队列。

对比 system V共享内存 所介绍的共享内存通信,我们是可以发现,只要属于 system V 标准,它们的接口相似度都是极高的,理解了一种方式的通信接口,了解其它的就易如反掌了。后面介绍信号量,也可以发现接口的相似度是很高的。


2. 简谈 IPC 在内核中的数据结构设计

在操作系统中,所有的 IPC 资源,都是整合为操作系统的 IPC 模块的,所有的 IPC 资源是被统一管理起来的。

在了解共享内存和消息队列的系统接口后,我们可以发现,不管是共享内存、消息队列还是信号量,都一个形如 xxxids_ds 的结构体,里面描述了通信所用到的资源的各自属性,而在该结构体内部第一个成员都是 struct ipc_perm xxx_perm,并且这个 struct ipc_perm 结构体是一模一样的(共享内存、消息队列、信号量都共同使用的一个结构体)。

在内核中,管理共享内存、消息队列和信号量,都是转变为管理 shmid_ds、msgid_ds、semid_ds 这样的结构体对象,然后再以数组 struct ipc_perm* arrary[] 这样的数据结构来统一管理 xxxid_ds 的结构体对象。而当我们创建了一个共享内存,那么操作系统就在内核为这个共享内存创建一个 struct shmid_ds 结构体对象,然后 把结构体内第一个成员 struct ipc_perm 的对象地址填入到 struct ipc_perm* arrary[] 数组内。后续如果再创建了消息队列还是信号量,一样的方式进行管理,先创建通信相关结构体对象,然后把其内部的第一个成员,struct ipc_perm 的地址顺着下标依次填入到数组内。因此管理各种通信资源,就是先通过结构体描述资源的各种属性,再用 struct ipc_perm* arrary[ ] 这样的数组把描述好的结构体组织起来,这样之后,对通信资源的增删查改,就转变为对数组内的元素的增删查改!

后续想要管理具体某一个通信资源时(比如在进程获取共享内存时,需要确认该资源在系统中是否唯一,就是拿着 key 遍历 struct ipc_perm* arrary[ ] 这个数组,然后找到每一个 struct ipc_perm ,比较该结构体内的 key 值是否相同,这样就可以判断该共享内存是否已经创建,而这个数组下标就是 shmget 返回的 shmid,消息队列的 msgid 等同理),就可以通过 struct ipc_perm* arrary[ ] 数组下标内存的指针 + 偏移量,((struct shmid_ds*)addr)-> xxx 找到这个资源的其它属性(即先做类型转换,然后指向其它成员即可访问),而不仅仅是访问 struct ipc_perm 结构体内的成员属性。


3. 信号量

两个进程在使用共享内存通信时,先在物理内存中申请一块内存空间,然后挂接到各自的进程地址空间,最后只需要拿着虚拟地址即可访问共享内存进行通信。

但是最基本的共享内存通信是没有同步互斥这样的保护机制的,即一方写入时,数据还没写完,另一方可能就读取数据了,这样就导致了读取到的数据不完整 ,即数据不一致问题。

而在解决类似数据不一致的问题之前,我们需要先建立一些概念理解。

  • 不同进程看到同一份资源,即共享资源,如果该共享资源不加以任何保护机制,那么就会导致数据不一致的问题

  • 通过加锁来保护共享资源的访问,任何时刻,只允许一个执行流访问共享内存,即互斥访问。 (互斥:一方进程在访问共享内存做写入操作时,另一方进程不得访问共享内存,即便另一方进程已经准备就绪了,也不能访问。同理,一方进程在读取共享内存中的数据时,另一方也不做写入操作)

    现实中的互斥场景:ATM 机存取钱时,同时只允许一个人在一台机器上做存钱/取钱操作。

    高铁票,一张票只卖一个用户,一个座位对应一张票。

  • 我们把共享的、任何时刻只允许一个执行流访问的资源,称为 临界资源。临界资源的本质还是一块由操作系统维护的内存空间。(管道就是临界资源,管道文件的缓冲区也是一种内存空间)

  • 上述常说的只允许一个执行流访问,访问的本质就是执行代码,诸如访问管道,本质就是在调用系统调用,对管道做拷贝、写入操作。而可能100行代码中,只有 5 - 10 行代码是真正在访问临界资源。我们把访问临界资源的代码,称为临界区。

  • 拓展场景:在多进程、多线程并发向一个显示器打印数据时,在显示器看到的数据总是错乱的,杂乱无章的。这是因为显示器也是文件,它也有自己的缓冲区,当多进程、多线程并发往显示器写入数据时,都是先把数据写到显示器文件的缓冲区中,最后再刷新到显示器上。而在多进程、多线程访问显示器的场景中,显示器本质也是一个共享资源(不同进程都能看到这个显示器,往显示器写入数据),而不同进程都往显示器打印数据时,都是往同一个缓冲区写入的,并没有加入任何的保护机制,因此在写入数据时导致数据不一致。而想要在多进程、多线程并发向一个显示器写入数据时不错乱,那就需要加入一些保护机制,让显示器这个共享资源变为临界资源。

3.1 理解信号量

  • 定义:信号量的本质就是一把计数器,用于描述临界资源中的资源数量。

在现实生活中,当我们要去电影院看电影,我们需要先买电影票,而同一张电影票只能卖给一个人,因为一张电影票对应着一个座位。当我们电影票支付完成这一刻,在一场次时间内,这个座位都是属于我的,即便还没有到电影院,我还没坐到这个座位上,再即便我没有去看这场电影,这个位置都还是属于我的!

电影院属于公共场所,概念上等同于共享资源!买票的本质,就是对资源的预定机制! 而电影院的座位是有限的,因此每出售一张票,可用资源就减少一份(即座位),这场电影院的剩余可用座位(即票数)就要减一,即维护一个类似计数器的东西,每卖一张票,计数器 - 1。当 票数计数器 == 0,就说明资源已经被申请完了。

整个电影院当作一种共享资源的话,那么这个共享资源可以划分为多个放映厅,每个放映厅又是一种共享资源,可以按照座位拆分为一个一个的小资源。

在计算机的视角就等于,一个执行流在访问一个共享资源时,可能只需要其中的一小部分资源,当另一个执行流也要访问该资源时,在划分另一块小资源给它,而没必要把整块资源都给一个执行流。在这种情况下(该共享资源能够被拆分成多个小资源,每个执行流的需求不多),我们就可以允许多个执行流同时访问该共享资源,这样就可以提高多个执行流访问临界资源的并发度,提高效率。

而面对多个执行流访问同一个共享资源时,我们需要顾及到,可能会存在多个执行流访问同一个小资源的情况(假设该共享资源可被拆分为多份小资源),排除代码层面的 bug 问题,我们就需要引入一个计数器,例如 int cnt = 100;当一个执行流想要访问某一块资源时,cnt - 1,代表着申请资源的行为。当 cnt == 0,代表资源申请完了。后续再有执行流想要申请资源,就拒绝申请。

  • 申请计数器成功,就表示该执行流具有访问资源的权限。(申请计数器资源,概念上就等同于买票)
  • 申请了计数器资源,即便执行流还没有开始访问,这块资源也属于它。换言之,申请计数器资源的本质是对资源的预定机制。
  • 计数器可以有效保证访问共享资源的执行流的数量。
  • 因此,对于每一个执行流,想要访问共享资源中的一部分资源时,都是先申请计数器资源,再访问资源。

上面所谈到的 "计数器",就称为信号量。

再假设 XXX电影院有一个 SSSVIP 豪华放映厅,每场电影只卖一张票,也即每场电影只能有一个人进去看电影。类似这种场景,只需要维护一个值为 1 的计数器,并且只能有一个执行流访问该临界资源。要么有执行流访问,要么没有,只有这两种状态,不存在有多个执行流的这种计数器,即 只有 1 和 0 两态的计数器,我们称为二元信号量,其本质就是一个锁。

那么凭什么让计数器设置为 1,本质是该资源只有一份。而资源只有一份的本质又是,该临界资源不允许被拆分,只能当作一个整体来使用访问。整体申请,整体释放。例如管道资源,一方进程写入时,另一方进程不能做读取操作。

  • 任何执行流要访问临界资源,都要先申请信号量计数器资源,换言之,任何执行流都有权能够申请信号量计数器(如同有权申请临界资源一样),那这样的话,信号量计数器不就也是共享资源了吗??

    信号量是要在多个执行流同时访问时保护临界资源的,但是要保证别人安全的前提,是先保证自己的安全,保证自己没有问题后,才能保证其它共享资源在分配给多个执行流时不会有问题。

    在信号量计数器中,如果一方执行流申请了临界资源,就等于要给 cnt - 1,但是在执行 cnt-- 这样的操作时,它不是安全的。 cnt-- 这条语句,在汇编上,一般会被转换为3条语句:把内存中的 cnt 数据读取到 cpu 寄存器中;cpu 做运算操作;最后将计算结果写回内存。

    而操作系统中的进程在运行时,是随时可能被切换的,所以有没有可能在执行 cnt-- 这样的操作时,还没执行完(在汇编上是有多条语句的),进程就被切换了。这样的话,多执行在申请信号量计数器资源时,访问 cnt 这个变量时,就有可能出问题(至于什么问题,这方面更为详细内容,请跳转线程相关的文章)。

    申请信号量,本质是对计数器做 -1 的操作,称为 P 操作

    后续释放资源就是释放信号量,本质是对计数器做 +1 的操作,称为 V 操作

    一方执行流在进行 P 操作 或着 V 操作时,操作系统会保证该行为是 原子的,即要么不做,要做就做到底,直接这个操作完成为止。

    总结:
    信号量本质是一把计数器,PV操作,该操作是原子的
    执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源
    信号量值只有 1 0 两态的,称为二元信号量,具有互斥功能
    申请信号量的本质是对临界资源的预订机制

3.2 了解系统接口

NAME
     semget - get a System V semaphore set identifier

SYNOPSIS
     #include <sys/types.h>
     #include <sys/ipc.h>
     #include <sys/sem.h>

     int semget(key_t key, int nsems, int semflg);		// 申请信号量
    
参数分析:
key:含义与 shmget、msgget 一致
nsems:设置为1,代表申请一个信号量
semflg:与含义与 shmget、msgget 一致,可设置为 IPC_CREAT and IPC_EXC

NAME
     semctl - System V semaphore control operations

SYNOPSIS
     #include <sys/types.h>
     #include <sys/ipc.h>
     #include <sys/sem.h>

     int semctl(int semid, int semnum, int cmd, ...);	 	// 释放信号量

参数分析:
semid:semget 的返回值,标识信号量的一个数字
semnum:如果信号量只有 1 个,那么就设置为 0,即设置为它的下标
cmd:操作方式,可设置 IPC_STAT、IPC_RMID 等操作

NAME
     semop, semtimedop - System V semaphore operations

SYNOPSIS
     #include <sys/types.h>
     #include <sys/ipc.h>
     #include <sys/sem.h>

     int semop(int semid, struct sembuf *sops, unsigned nsops);

参数分析:         
struct sembuf* sops	// 一个需要自定义的结构体,需要包含以下成员
{
	unsigned short sem_num;  /* semaphore number */		申请信号量的数量(可申请多个)
	short          sem_op;   /* semaphore operation */	设置为 1,代表释放信号量,即 V 操作;-1 代表申请信号量,即 P 操作
	short          sem_flg;  /* operation flags */ 		可设置的标志有 IPC_NOWAIT(不阻塞,调用后不管成功失败,立即返回) 和 SEM_UNDO(进程异常或崩溃终止时,自动撤销所做的信号量操作)
}
nsops:执行的信号量操作的数量。 sops 可指向一个struct sembuf 结构体数组,数组中每个 struct sembuf 结构体代表一个信号量操作。
  • 信号量并不传数数据,为什么也属于 system V 通信中的一种通信方案?

    通信不仅仅是数据的通信,互相协同也属于通信的范畴

    协同的本质也是通信,而信号量要先被所有的通信进程看到,才能进行协同。


如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

相关推荐
木子Linux23 分钟前
【Linux打怪升级记 | 问题01】安装Linux系统忘记设置时区怎么办?3个方法教你回到东八区
linux·运维·服务器·centos·云计算
mit6.82429 分钟前
Ubuntu 系统下性能剖析工具: perf
linux·运维·ubuntu
鹏大师运维30 分钟前
聊聊开源的虚拟化平台--PVE
linux·开源·虚拟化·虚拟机·pve·存储·nfs
watermelonoops37 分钟前
Windows安装Ubuntu,Deepin三系统启动问题(XXX has invalid signature 您需要先加载内核)
linux·运维·ubuntu·deepin
滴水之功1 小时前
VMware OpenWrt怎么桥接模式联网
linux·openwrt
ldinvicible2 小时前
How to run Flutter on an Embedded Device
linux
YRr YRr3 小时前
解决Ubuntu 20.04上编译OpenCV 3.2时遇到的stdlib.h缺失错误
linux·opencv·ubuntu
认真学习的小雅兰.3 小时前
如何在Ubuntu上利用Docker和Cpolar实现Excalidraw公网访问高效绘图——“cpolar内网穿透”
linux·ubuntu·docker
zhou周大哥3 小时前
linux 安装 ffmpeg 视频转换
linux·运维·服务器
不想起昵称9293 小时前
Linux SHELL脚本中的变量与运算
linux