【Linux】第三十五站:信号量和消息队列

文章目录

  • 一、消息队列
    • 1.消息队列原理
    • 2.消息队列的接口
      • 2.1 创建一个消息队列
      • 2.2 释放消息队列
      • 2.3 发送数据和接收数据
    • 3.查找消息队列
    • 4.信号量的接口(了解)
    • 5.结论
  • 二、IPC在内核中的数据结构设计
  • 三、信号量
    • 1.临界资源与临界区
    • 2.信号量原理
    • 3.信号量的接口
      • 3.1申请信号量
      • 3.2删除信号量
      • 3.3信号量的操作
    • 4.信号量凭什么是进程间通信的一种?
  • 四、mmap

一、消息队列

1.消息队列原理

如下图所示

如果我们想让进程间通信,那么必要的条件就是让不同的进程看到同一份资源,这个资源可以是文件缓冲区,内存块,队列等

【Linux】第三十五站:信号量和消息队列_第1张图片

而消息队列也是一样,一定要让不同的进程看到同一个队列

【Linux】第三十五站:信号量和消息队列_第2张图片

除了上面的,还要允许不同的进程向内核中发送带类型的数据块

【Linux】第三十五站:信号量和消息队列_第3张图片

所以要让

A进程 <---- 数据块的形式发送数据 ----> B进程

这个消息队列只能由操作系统来提供,而且也一定要先描述在管理

2.消息队列的接口

2.1 创建一个消息队列

#include 
#include 
#include 
int msgget(key_t key, int msgflg);

【Linux】第三十五站:信号量和消息队列_第4张图片

这个与共享内存的接口很像,第一个参数是一个key,第二个参数也是IPC_CREAT和IPC_EXCL

image-20240123173553276

而这个key它的值还是从前面的地方来的

#include 
#include 
key_t ftok(const char *pathname, int proj_id);

这个msgget的返回值是成功返回一个消息队列标识符,失败返回-1。与前面的共享内存是极度相似的

2.2 释放消息队列

image-20240123174009658

#include 
#include 
#include 

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

还是同样的接口,对于第二个参数,如果要释放,还是这个IPC_RMID

image-20240123174103697

第三个参数的结构体也是很相似的

【Linux】第三十五站:信号量和消息队列_第5张图片

里面区分消息队列的也是key

2.3 发送数据和接收数据

共享内存用的是挂接和去挂接。然后就可以直接用了

而消息队列用的是这个接口,发送数据和接收数据

#include 
#include 
#include 
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);

【Linux】第三十五站:信号量和消息队列_第6张图片

【Linux】第三十五站:信号量和消息队列_第7张图片

对于第一个函数

第一个参数是消息队列的标识符

第二个参数是要发送数据块的起始地址

第三个是要发送数据块的大小

第四个一般直接设置为0

这个第二个参数是void*的原因是要自己定义一个struct结构体,如同上面所示的那个msgbuf,只要保证第一个是类型,第二个是内容即可

第二个函数是用于接收数据的

第一个参数是从哪个消息队列拿

第二个参数和第三个参数与第一个函数是同样的道理,相当于一个缓冲区

第四个参数是要读取的哪一种类型的数据。也是我们在结构体中定义的

第五个参数和前面一样,默认为0即可

3.查找消息队列

和共享内存一样,下面这个指令可以去查找当前的消息队列

ipcs -q

【Linux】第三十五站:信号量和消息队列_第8张图片

如果我们要删除的话,用这个指令

ipcrm -q XXX(msqid)

4.信号量的接口(了解)

我们先来看信号量的接口

#include 
#include 
#include 
int semget(key_t key, int nsems, int semflg);

这个与前面的是极度相似的

还有下面这个接口

#include 
#include 
#include 
int semctl(int semid, int semnum, int cmd, ...);

【Linux】第三十五站:信号量和消息队列_第9张图片

它里面也有对应的数据结构

【Linux】第三十五站:信号量和消息队列_第10张图片

5.结论

我们通过观察这些system v系列的接口,我们发现他们都有一个这样的数据结构,

XXXid_ds{
	struct ipc_perm XXX_perm
    //....
}

里面的这个结构,它里面的字段都是一样的都是些权限

二、IPC在内核中的数据结构设计

首先我们要知道的是,在操作系统中所有的IPC资源全部被整合在了一个IPC模块当中的!

如下所示,是System V方式的三种IPC方式的内核数据结构

【Linux】第三十五站:信号量和消息队列_第11张图片

这些数据结构都是要被管理起来的

我们要注意的是,它的第一个字段都是一模一样的结构体的。

在操作系统中还有一个数组,每当创建一个共享内存、消息队列、信号量等时候,就会将这个第一个字段的地址填入到这个数组中

【Linux】第三十五站:信号量和消息队列_第12张图片

未来当我们要去寻找一个共享内存是否存在的时候,就会直接去遍历这个数组里面的key,然后从而就可以进行直接比对key就知道了它是否已经创建了。

【Linux】第三十五站:信号量和消息队列_第13张图片

而且这个数组的下标就是所谓的XXXid,比如shmid等等

【Linux】第三十五站:信号量和消息队列_第14张图片

那么如何访问其他的成员呢?

注意看,这里正好是放在了第一个字段,所以这个字段的地址正好就是这个数据结构的地址。我们只需要将这个地址强转为这个数据结构的地址,然后就可以访问其他成员了!

【Linux】第三十五站:信号量和消息队列_第15张图片

那么现在问题来了,我们怎么知道我们要转成什么类型呢?

这其实是什么这个第一个字段的 xxx_perm结构体里面,有一个字段标志着是哪种资源,所以可以知道强转成什么类型

即OS能区分指针指向的对象的类型

而上面的这个操作,其实我们仔细一想,这不就是多态吗。struct ipc_perm是基类,其他的这些struct xxxid_ds就是子类

【Linux】第三十五站:信号量和消息队列_第16张图片

还有一点值得注意的是,这个数组是单独存在的,不隶属于任何进程。他是操作系统层面维护的一个数组,他跟文件描述符表没任何关系,这也导致这个数组最后被边缘化了,因为它没法和任何进程关联。

而且这个数组的下标是不断增大的。线性递增的。当达到最大的时候,才会回绕到0

三、信号量

1.临界资源与临界区

如下图所示,是之前共享内存的基本原理

【Linux】第三十五站:信号量和消息队列_第17张图片

这里会出现一个问题:

当我们的A正在写入,写入了一部分,就被B进程拿走了,导致双方发和收的数据不完整,就导致了数据不一致问题。

而管道就自带同步机制,不会出现这个问题

这个共享内存就是没有任何的保护机制的

  1. A B看到的同一份资源,共享资源,如歌不加保护,会导致数据不一致的问题
  2. 我们可以通过加锁 – 互斥访问 – 即任何时刻,只允许一个执行流访问共享资源 — 这就是互斥
  3. 我们将这种共享的,任何时刻只允许一个执行流访问的资源叫做临界资源 — 一般是内存空间,比如管道等
  4. 举例:100行代码,只有5~10行代码才在访问临界资源。而我们将降温临界资源的代码称之为临界区

像我们的一个现象:在多进程、多线程并发打印的时候。显示器上的信息:是错乱的,混乱的,和命令行混在一起的。

这就是因为显示器是一种临界资源。

2.信号量原理

信号量/信号灯的本质是一把计数器,类似于但不等于 int cnt = n

用于描述临界资源中资源的数量!


比如说放映厅放100张票,当我们看电影的时候,我们还没去看电影,先买票,而买票的本质就是对资源的预定机制。

对于票数的计数器,每卖一张票,计数器要减1,放映厅的资源就要少一个

票数的计数器到0之后,资源就已经被申请完毕了

不过我们最怕的是,多执行流访问同一个资源,即n个资源被n+个执行流访问,一旦出现,就会发生像前面的显示器打印混乱现象

int cnt = 15//我们引入一个计数器
int number = cnt--; //申请资源
cnt <= 0 //资源申请完了,再有执行流,就不给了
  1. 申请计数器成功,就表示我们具有访问资源的权限了!
  2. 申请了计数器资源,我当前要访问我们要的资源了吗?当然没有,申请了计数器资源是对资源的预定机制
  3. 计数器可以有效保证进入共享资源的执行流的数量
  4. 所以每一个执行流,想访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源。看电影的先买票!

我们将这个“计数器”,叫做信号量!

如果我们前面所提到的放映厅只有一个座位!,那么我们只需要一个值为1的计数器。

此时只有一个人能抢到这份资源,只有一个人能进放映厅看电影。即看电影期间只有一个执行流在访问临界资源。

这就是互斥!!!

所以我们把值只能为1,0两态的计数器叫做二元信号量,本质就是一个锁


上面中,我们凭什么让计数器为1??这是因为资源为1了,也就是说本质就是将临界资源不要分成很多块了,而是当作一个整体,整体申请,整体释放!!

思考一下:

要访问临界资源,先要申请信号量计数器资源。

所以信号量计数器,不也就是共享资源吗???

int cnt = 10;
cnt--;

而要保护别人,我们先要保证自己的安全!!

而直接对一个整数–,并不安全。

这个cnt–;在C语言上是一条语句。

如果变成汇编,是多条(一般是3条)汇编语句

①cnt变量的内容,内存先要到CPU寄存器上

②CPU内进行–操作

③将计算结果写回cnt变量的内存位置

所以进程在运行的时候,可以随时被切换。这里其实就是有问题的,我们后序在说明


要处理上面的问题,操作系统就有一个信号量

申请信号量,本质是对计数器–,P操作

释放资源,释放信号量,本质是对计数器进行++操作系,也叫做V操作

申请和释放PV操作一定就是原子的!!!

原子的意思是一件事情要么不做,要做就做完,是两态的。没有“正在做”这样的概念!


最终结论

  1. 信号量本质是一把计数器,PV操作是原子的

  2. 执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源!!

  3. 信号量值1,0两态的。二元信号量,就是互斥功能

  4. 申请信号量的本质,是对临界资源的预定机制!!

3.信号量的接口

system V的信号量接口是最难的。

3.1申请信号量

下面的系统调用的功能就是申请一个信号量集

#include 
#include 
#include 
int semget(key_t key, int nsems, int semflg);

第一个参数是key,我们用ftok获取

第二个参数申请几个资源,如果申请一个就是1

第三个参数是设置为O_CREAT和O_EXCL,和前面一样

返回值就是信号量集标识符

这里需要注意,多个信号量和信号量是几是不一样的

3.2删除信号量

#include 
#include 
#include 
int semctl(int semid, int semnum, int cmd, ...);

第一个参数是信号量的标识符

第二个是几个信号量

第三个删除操作

可变部分可以传递信号量对应的结构体

【Linux】第三十五站:信号量和消息队列_第18张图片

同时这个函数除了删除信号量,也可以设置信号量

如果只有一个信号量,那么这个编号直接设置为0,cmd设置为SET。最后可变部分传递这个联合体,最终这个信号量初始值就被设置为对应的值

【Linux】第三十五站:信号量和消息队列_第19张图片

3.3信号量的操作

#include 
#include 
#include 
int semop(int semid, struct sembuf *sops, unsigned nsops);
int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);

【Linux】第三十五站:信号量和消息队列_第20张图片

第一个函数中

第一个参数是对哪一个信号量进行操作

第二个参数是一个我们自定义的结构体

image-20240123224656830

这个结构体中,第一个参数是想对哪一个信号量进行操作,如果只有一个,那就直接传0。第二个op要么是1要么是-1。如果是1就是对这个信号量+1,如果是-1,就是对这个信号量-1。从而可以进行PV操作

4.信号量凭什么是进程间通信的一种?

  1. 通信不仅仅是通信数据,互相协同也是
  2. 要协同,本质也是通信,信号量首先要被所有的通信进程看到!!

四、mmap

mmap函数其实也是共享内存

只不过它与前面的共享内存不同的是,它是将数据放到了磁盘上

【Linux】第三十五站:信号量和消息队列_第21张图片

你可能感兴趣的:(【Linux】,linux,网络,运维,centos,服务器,c语言,c++)