本篇文章,继续来和大家分享与Linux相关的知识。本次内容主要会涉及到消息队列,信号量和System V标准。
消息队列
消息队列的原理
假设有A和B两个进程,通过消息队列进行通信。如果A进程给B进程发消息,A进程给B进程发送的消息,就会挂到消息队列中。如果说,B进程给A进程发送信息,B进程发送的信息也会被挂到消息队列中。这样一来,消息队列中就有了一个一个的节点,我们把这个节点称之为数据块。
A和B进程都往队列里放数据块,那我们怎么去做区分呢?所以,在数据块中会有一个字段用来标定类型,是A的数据块还是B的数据块。
消息队列的原理,可以总结为两点:
1.必须让不同进程看到同一个队列
2.允许不同进程,向内核中发送带类型的数据块
进程间通信的本质是看到同一份资源,这份资源以什么样的方式提供给我们,决定了它是什么。是共享内存,还是消息队列,亦或者文件缓冲区
操作系统中,会有很多的进程使用消息队列进行通信,消息队列不止一个。操作系统需不需要将它们管理起来?当然。怎么管理?先描述,后组织
用于通信的消息队列由操作系统提供,它属于System V标准。那这个System V标准体现在哪呢?
我们一起来看看和消息队列相关的函数
创建消息队列
msgget函数用来创建消息队列
它的第一个参数有没有点眼熟,这不就是我们创建创建共享内存时用的key。它们两者是相同的,都是通过ftok函数获取
msgget的第二个参数,是填选项,和创建共享内存的方式相同。创建新的消息队列传IPC_CREAT | IPC_EXCL,如果是存在获取,不存在创建,传IPC_CREAT
如果消息队列我们创建好了,但不想用了,想把它释放怎么办?
释放消息队列
msgctl函数释放消息队列,第一个参数就是msgget创建消息队列后的返回值。第二参数,传IPC_RMID告诉它要删除即可。
第三个参数,是描述消息队列使用的结构体。第二个参数传IPC_STAT就可以获取了。下图中,_msg_cbytes表示的是队列的总字节数是多少,msg_qnum表示队列中有多少个节点,msg_qbytes表示一条信息的最大字节数,msg_lspid表示最近发送信息的进程pid,msg_lrpid表示最近收到消息的进程pid
发送消息
发送消息用到的函数是msgsnd。第一个参数,向指定的消息队列发送消息。第四个参数,设置为0,以阻塞方式发送消息。第二个参数,数据块的起始地址。第三个参数,数据块的大小。
下图这个就是我们将来要发送的数据块,名字随便取。mtype是数据块的类型,必须大于0。mtext是消息的内容。
接受消息
接受消息,用到函数msgrcv。它的第四个参数,就传数据块的类型,告诉它,你要接受的数据,是A类型的还是B类型的。第五个参数,和msgsnd函数一样,设置为0即可。
我们把共享内存和消息队列接口,放到一块进行对比,就能看到它们诸多相似之处,比较个性的就是消息队列收发信息的接口
用ipcs -q指令,可以查看已创建的消息队列
用ipcrm -q msgid指令,可以删除相应的消息队列
信号量
申请信号量使用semget函数
信号量的控制使用semctl函数
下面这个是描述信号量属性的结构体
我们把它与共享内存和消息队列的结构体,进行对比,就能发现它们有相似之处。
你想表达什么呢?我想说,进程间通信是经过精心设计的,无论是共享内存,消息队列,还是信号量,都会有XXXid_ds这样的结构体。不管是那种通信方式,它的结构体中的第一个成员都叫做struct ipc_perm XXX_perm。struct ipc_perm这样的字段里面包含的内容都是一样的
信号量的具体使用,我们后面配合代码讲解
下面,我们了解IPC在内核中的数据结构设计
IPC在内核中的数据结构设计
在操作系统中,所有的IPC资源,都是整合到操作系统的IPC模块中。
共享内存,消息队列和信号量这三者,会被统一管理起来。怎么管理呢?其实很简单,描述共享内存,消息队列和信号量的结构体的第一个成员都是struct ipc_perm,我们将拿到它们的第一个成员的地址,放到数组里,就可以把对IPC资源的管理转换成对数组的管理。
那你怎么知道它是消息对立还是共享内存,又怎么进行放呢?struct ipc_perm XXX中的mode变量,可以带选项,增加类型标志位来代表是什么类型的ipc资源。如果我们要进行访问,我们拿到struct ipc_perm XXX的起始地址,struct ipc_perm XXX的起始地址也就是描述IPC资源结构体的起始地址,进行类型强转,就可以访问结构体中的其他成员了。这个访问的操作有点像什么?这不就是传说中的多态吗?struct ipc_perm XXX就是基类,而struct XXXid_ds就是子类。
shmid,msgid等XXXid本质就是数组下标,那为什么它们和我们之前讲的文件描述符不太一样,文件描述符很小,shmid却很大?
shmid它们和文件描述符没有关系。管理它们的这个数组,无法与进程产生关联,我们称之为边缘化。这里的数组下标的分配,采用的是线性递增的方式,无论你有没有释放上一次申请的IPC资源,下一次分配的数组下标都会增大。况且这个IPC资源,不只是我们在使用,内核中的其他程序也需要使用,进行通信。所以,shmid它们很大。
假设现在有A和B两个进程,它们获取好了共享内存。
A进程向共享内存写入数据,hello world。A进程刚写完hello,还没写完。但B进程就进来读取了,只读取了一部分。
我们是想给B进程发送hello world,可B进程只拿走了一部分,导致发送方和接受方数据不一致的现象,我们称之为数据不一致问题。
解决数据访问的方式是加锁,加锁的具体细节我们后面的文章细说。这里我们只需要理解,加锁,意味着互斥访问,任何时刻,只允许一个执行流访问共享资源,这种状态我们称之为互斥。
而共享的,任何时刻只允许一个执行流访问(就是访问执行的代码)的资源,我们称之为临界资源。临界资源,一般都是内存空间。比如说管道,管道具有互斥功能,你访问管道,是在访问文件缓冲区,文件缓冲区的本质就是内存空间。
共享内存是不是临界资源呢?不是,虽然它是共享的,但是它没有做保护,保证互斥。
假设现在有100行代码,有5行代码会对共享资源进行访问。我们把这5行访问临界资源的代码,称之为临界区。
思考一个问题:如果说多个进程或多个线程向显示器打印内容,会出现什么现象?
显示器上显示的信息是错乱的,混乱的,和命令混合在一起。为什么?多个进程或线程向显示器打印,首先得看到同一个显示器文件,。多个进程向同一个文件写入,也就是向文件缓冲区写入数据。文件缓冲区,此时作为共享资源,并没有做保护,你一句,我一句,当然会出现混乱啦!
理解信号量
信号量,是英译过来的名字,也有人翻译成信号灯。信号量本质是一把计数器,类似于int cnt = n,但两者并不等价。
举个例子,放映厅中有100个座位,对应的有100张电影票。放映厅老板能不能把票加到101,当然不能,座位总共就100张。当我们去看电影的时候,先做什么?是不是先买票,预定一个位置。每卖出一张票,电影票的数量就减一。当电影票的数量减为0,也就没有座位了。
座位是放映厅的一种资源,买票的本质是对座位资源的预定。每卖一张票,资源的数量就减一。当资源的数量减为0,也就意味着资源已被申请完毕了。
临界资源其实就相当于例子中的放映厅,临界资源的数量是可能是n,我们需要用一个计数器int cnt =15,来统计临界资源的数量。如果有资源被申请了,我们就cnt减减。当cnt减为0,就表示资源被申请完了,再有执行流来申请,就不给你。
程序员把这个计数器,称之为信号量。
如果说放映厅只有一个座位,那么我们只需要一个为1的计数器。
只有一个座位,对应只有一张票,一张票只有一个人能抢到,只有一个人能到放映厅里看电影。看电影期间,只能有一个执行流访问临界资源?这不就是互斥吗?
我们把值只能为0,1两态的计数器,称之为二元信号量。二元信号量本质就是一个锁。
为什么计数器为1呢?因为资源的数量为1。也可以理解为,我们把临界资源当成一个整体,整体申请,整体释放。
有四个点需要明确:
1.申请计数器成功,就表示我就具有访问资源的权限
2.申请了计数器资源,我当前访问了我要的资源吗?没有,申请了计数器资源是对资源的预定机制
3.计数器可以有效保证进入共享资源的执行流的数量
4.所以,每一个执行流,想要访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源。看电影先买票!
思考一个问题:要访问临界资源,先要申请信号量计数器资源,信号量计数器不也是共享资源吗?
申请信号量计数器资源,要进行cnt--。cnt--不是安全的,在C语言上,这是一条语句,变成汇编,它至少需要3条汇编语句。
1.cnt变量的内容,内存->cpu寄存器
2.cpu内进行--操作
3.将计算结果写回cnt变量的内存位置
在执行以上这三条汇编语句的时候,进程可能随时被切换。这部分是有问题的,后面的文章细说。
保护别人,先得保证自己的安全!
申请信号量,本质是对计数器--,我们称之为P操作。
释放资源,释放信号量,本质是对计数器进行++操作,我们称之为V操作。
要想保证字节的安全,就要保证申请和释放的PV操作是原子的。
什么是原子的?就是做一件事,要么不做,要做就做完--两态的,没有“正在做”这样的概念。
说了这么多,简单总结,记住四句话:
1.信号量本质是一把计数器,PV操作,原子的
2.执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源!
3.信号量值1,0两态的,二元信号量,就是互斥功能
4.申请信号量的本质:是对临界资源的预定机制!!
申请信号量
申请信号量用semget函数,semget函数可以一次申请多个信号量。这里注意:多个信号量和信号量是不同的概念
semget函数的第一个参数是key,第二个参数申请信号量的数量,如果你要申请一个信号量,这里就填1,第三个参数是选项,创建新就填O_CREAT | O_EXCL,这里和共享内存是一样的
控制信号量
控制信号量用函数semctl,semctl函数的第一个参数是信号量标识符,第二个参数是信号量的数量,第三个参数是指令,你需要释放还是获取信号量属性
semctl还可以初始化信号量,第三个参数传IPC_SET,然后,第四个参数传一个自己定义的联合体
信号量设置
设置信号量用函数semop,它的第一个参数是信号量标识符
第二个参数,是需要传一个结构体,这个结构体有如下三个成员,第一个成员sem_num,我们一次可以申请多个信号量,你要操作哪一个信号量,通过这个变量告诉它。如果你只申请了一个信号量,填0就可以了,这和数组下标是一样的
第二个参数,可以为-1,0,1。-1也就是对信号量减一,相当于P操作,1相当于V操作。
信号量这种System V的接口是最难的,多线程部分我们进行操作说明。
思考一个问题:信号量凭什么是进程间通信的一种?基于以下两点:
1.通信不仅仅是为了我们进行传输数据,也可以是互相协同
2.要协同,本质也是通信,信号量首先要被所有通信的进程看到
mmap函数
有时间的话,可以了解一下,mmap函数,它的本质也是共享内存。
我们之前说的共享内存是两个进程间的把共享内存映射到自己的进程地址空间,进行通信。现在,一边还是进程,另一边换成了磁盘文件,进程向共享内存写入,就是直接向磁盘文件写入。也就是进程向磁盘文件写入数据,不用使用open和write函数了。这项技术,就是mmap函数。
好了,到这里,我们本次的分享就到此结束了,不知道我有没有说明白,给予你一点点收获。关于更多和Linux相关的知识,会在后面的文章更新。如果你有所收获,别忘了给我点个赞,这是对我最好的回馈,当然你也可以在评论发表一下你的收获和心得,亦或者指出我的不足之处。如果喜欢我的分享,别忘了给我点关注噢。