Linux 消息队列信号量
目录
一、前言
二、消息队列
1、原理
2、接口
2.1创建消息队列
2.2释放消息队列
2.3发送数据
2.4 接收数据
四、IPC在内核中地数据结构设计
五、信号量
1、互斥
2、信号量
3、信号量的接口
3.1申请信号量
3.2删除信号量
3.3信号量的操作
一、前言
我们讲进程通信,本质是让不同的进程看到同一份资源。
我们已经给大家介绍了管道,共享内存,接下来我们给大家简单的介绍一下消息队列。
二、消息队列
1、原理
如果我们想让进程间通信,那么必要的条件就是让不同的进程看到同一份资源,这个资源可以是文件缓冲区,内存块,队列等。
对于消息队列,就是让同一个进程看到同一个队列
允许不同的进程向内核中发送带类型的数据块
A进程 <---- 数据块的形式发送数据 ----> B进程
这个消息队列只能由操作系统来提供,而且也一定要先描述在管理。
2、接口
2.1创建消息队列
man msgget
int msgget(key_t key, int msgflg);
key是我们上一篇博客讲的,用ftok这套算法创建同一个key
第二个参数也是IPC_CREAT和IPC_EXCL
msgget的返回值是成功返回一个消息队列标识符,失败返回-1。与前面的共享内存是极度相似的
我们可以用ipcs -q命令来查看消息队列
用ipcrm -q “消息队列的标识符"来去除消息队列
2.2释放消息队列
man msgctl
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
它与释放共享内存的接口msgctl是类似的,对于第二个参数,如果要释放,还是这个IPC_RMID
共享内存的属性结构体是 struct shmid_ds
消息队列的属性结构体是 struct msqid_ds
他们内部包含了一个同样的结构体struct ipc_perm
在下面我们给大家讲解IPC在内核中数据结构的设计会具体讲解。
2.3发送数据
man msgend
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 第一个参数是消息队列的标识符
- 第二个参数是要发送数据块的起始地址
- 第三个是要发送数据块的大小
- 第四个一般直接设置为0
这个第二个参数是void*的原因是要自己定义一个struct结构体,如同上面所示的那个msgbuf,只要保证第一个是类型,第二个是内容即可
2.4 接收数据
man msgsnd
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 第一个参数是从哪个消息队列拿
- 第二个参数和第三个参数与第一个函数是同样的道理,相当于一个缓冲区
- 第四个参数是要读取的哪一种类型的数据。也是我们在结构体中定义的
- 第五个参数和前面一样,默认为0即可
四、IPC在内核中地数据结构设计
在操作系统中,所有的IPC资源,被整合在一起,放到操作系统的IPC模块中。
如下所示,是System V方式的三种IPC方式的内核数据结构
我们在上面也提到过,他们的内核里都有一个结构体struct ipc_perm
在操作系统里有一个struct ipc_perm *Arry的数组,数组对所有的IPC进行管理
未来当我们要去寻找一个共享内存是否存在的时候,就会直接去遍历这个数组里面的key,然后从而就可以进行直接比对key就知道了它是否已经创建了。 我们创建共享内存和消息队列返回的id即为数组下标
那我们要不要担心这个数组会满呢?
这个数组下标是不断增大的,线性递增的,当达到最高值的时候,回绕到0。
那么如何访问其他的成员呢?
注意看,这里正好是放在了第一个字段,所以这个字段的地址正好就是这个数据结构的地址。我们只需要将这个地址强转为这个数据结构的地址,然后就可以访问其他成员了!
那么现在问题来了,我们怎么知道我们要转成什么类型呢?
这其实是什么这个第一个字段的 xxx_perm结构体里面,有一个字段标志着是哪种资源,所以可以知道强转成什么类型
即OS能区分指针指向的对象的类型
而上面的这个操作,其实我们仔细一想,这不就是多态吗。struct ipc_perm是基类,其他的这些struct xxxid_ds就是子类
五、信号量
当我们A进程和B进程向同一份资源写入数据的时候,当A正在写入时,写了一部分,就被B拿走了,导致双方和收的数据不完整。 这个就会导致数据不一致的问题。
将多个进程同时看到的那一份资源叫做临界资源。我们仔细观察可以看到,在 server 和 client 中访问共享内存 / 临界资源的代码实际上只有少部分几行。造成读写不一致可能就是这部分代码导致的,我们将访问临界资源的代码叫做临界区,为了避免这种数据不一致问题,我们要对临界区进行某种保护,这种保护就被称作为互斥。
互斥:只允许一个执行流访问共享资源。
1、互斥
所谓互斥就是有一块空间,在任何时候有且仅能有一个进程在进行访问(生活中最典型的互斥场景就是去上洗手间),互斥本身是一种串行化执行(也就是说,共享内存中就是因为并行读写执行才导致的数据不一致问题),而后面一般互斥是通过锁来完成的,这里可以提一种二元信号量来完成串行执行(我们也能猜到加锁和解锁是有代码的) 。所以串行化的过程本质是对临界区资源加锁和解锁,从而完成互斥操作。也就是说,client 和 server 都必须遵守 “要进入临界区就得加锁,退出临界区就得解锁” 这一原则。
这里我们理解一下什么是原子性概念:
这里再感性的理解一遍原子性概念,其实说白了,就是要么做了,要么没做。比如一个进程想往共享内存里写 "Hello World",写完 "Hello" 时的这个状态叫做写入中,那么在写入过程中是不能被打搅的,得等到全部写完为止。也就是说,在其他人看来,这里写入过程的状态只有两种,其一是还没写,而其二是写完了,这就是原子性。
最典型的应用就是,假设我们在农商银行里有 1000 元,在建设银行里有 500 元,然后我们想进行转帐:农商账号 -= 200;建设账号 += 200。其中,当我们从农商账号转账到建设账号的时候,系统崩溃了,此时建设银行账号还是 500 元,但是农商银行账号少了 200 元。这个现象说白了就是当某个任务正在进行时,突然因为某些原因而导致任务中断,这就叫做不是原子性。所以这个转账的过程要不就不做,要不就必须得做成功,或者转账失败了也能保证农商账号的钱不受影响,这就是原子性。
我们也可以采用互斥的方案来保证原子性。
总结:
- 于各个进程要求共享资源,而且有些资源需要互斥使用,那么各进程竞争使用这些资源,进程之间的这种关系就叫作进程的互斥。
- 系统中某些资源一次只允许一个进程使用,这样的资源被称为为临界资源或互斥资源。
- 在进程中涉及到互斥资源的程序段叫做临界区。
- 特性:IPC 资源必须删除,否则不会自己清除,除非重启,所以 System V IPC 资源的生命周期随内核。
2、信号量
信号量本质上是一把计数器,用来描述临界资源中资源数量的多少。
这个时候我们提出一个预定机制。
在宿舍的时候,虽然没躺着,但是那个床位依旧是属于我们的。我们在网上买电影票看电影,虽然还没到时间去看,但我们很清楚,到了一定的时间我们就能看到。所以现实生活中存在很多 “预定机制”,因为不提前享受,所以在卖票的时候就得保证一人一座,不能超过电影院的承受能力。
我们把临界资源划分为若干份
我们害怕多个执行流访问同一份资源,我们引用一个计数器,其实这个计数器就是信号量。
假设这里面被划分为12份,int cnt=12。 当申请一次计数器资源,本质上是预定临界资源内的一份资源,所以cnt--;当要释放一个进程不再访问,则cnt++;当cnt<=0,表示资源被申请完了,如果还有执行流申请,就不打算分配资源了。
总结
- 申请了计数器成功,就表示具有访问资源的权限。
- 申请了计数器资源,这里这是申请计数器资源,还没有访问想要的资源。这里只是对资源的预定机制。
- 计数器可以有效保证进入共享资源的执行流的数量。
- 每一个执行流想访问共享资源要先申请计数器资源。
如果我们前面所提到的放映厅只有一个座位!,那么我们只需要一个值为1的计数器。
此时只有一个人能抢到这份资源,只有一个人能进放映厅看电影。即看电影期间只有一个执行流在访问临界资源。
这就是互斥!!!
所以我们把值只能为1,0两态的计数器叫做二元信号量,本质就是一个锁
上面中,我们凭什么让计数器为1??这是因为资源为1了,也就是说本质就是将临界资源不要分成很多块了,而是当作一个整体,整体申请,整体释放!!
信号计数器保护的是临界资源,要想保护别人,就要保证自己的安全。
这里我们申请信号量,对计数器--为P操作。
释放资源,释放信号量,对计数器++为V操作。
这里我们把申请和释放的操作叫做PV操作。
PV操作是原子的。原子性
3、信号量的接口
3.1申请信号量
下面的系统调用的功能就是申请一个信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
第一个参数是key,我们用ftok获取
第二个参数申请几个资源,如果申请一个就是1
第三个参数是设置为O_CREAT和O_EXCL,和前面一样
返回值就是信号量集标识符
这里需要注意,多个信号量和信号量是几是不一样的
3.2删除信号量
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
第一个参数是信号量的标识符
第二个是几个信号量
第三个删除操作
可变部分可以传递信号量对应的结构体
同时这个函数除了删除信号量,也可以设置信号量
如果只有一个信号量,那么这个编号直接设置为0,cmd设置为SET。最后可变部分传递这个联合体,最终这个信号量初始值就被设置为对应的值
3.3信号量的操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);
第一个函数中
第一个参数是对哪一个信号量进行操作
第二个参数是一个我们自定义的结构体
可以看到 System V 标准下的 ipc 共享内存机制其实蛮复杂的, 但其实共享内存又是 System V 标准下最简单的一套机制,所以当我们看到这里的时候也不难,相对更复杂的是消息队列机制,最复杂的是信号量机制。实际在公司中很少自己写这些东西,特别是消息队列和信号量,所以目前就先了解共享内存机制,知道是其底层是怎么通信的即可。