了解消息队列 && 信号量

目录

  • [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 通信中的一种通信方案?

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

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


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

感谢各位观看!

相关推荐
hero_th9 分钟前
[Ubuntu] 文件/目录权限更改
linux·ubuntu
花花少年18 分钟前
pip在ubuntu下换源
linux·ubuntu·pip
y0ungsheep37 分钟前
[GXYCTF 2019]Ping Ping Ping 题解(多种解题方式)
linux·web安全·网络安全·php
海绵波波10741 分钟前
Webserver(1.6)Linux系统IO函数
linux·运维·服务器
czme1 小时前
线程和进程
linux·数据结构·计算机网络
华东设计之美1 小时前
etcd多实例配置
linux·服务器·etcd
dessler1 小时前
Linux系统-开关机
linux·运维·云计算
致奋斗的我们2 小时前
RHCE的学习(7)
linux·服务器·网络·学习·redhat·rhce·rhcsa
昨天今天明天好多天3 小时前
【Linux】ClickHouse 部署
linux·服务器·clickhouse
taolichao303 小时前
架设一台NFS服务器,按照要求配置
linux·运维·服务器