如下图所示
如果我们想让进程间通信,那么必要的条件就是让不同的进程看到同一份资源,这个资源可以是文件缓冲区,内存块,队列等
而消息队列也是一样,一定要让不同的进程看到同一个队列
除了上面的,还要允许不同的进程向内核中发送带类型的数据块
所以要让
A进程 <---- 数据块的形式发送数据 ----> B进程
这个消息队列只能由操作系统来提供,而且也一定要先描述在管理
#include
#include
#include
int msgget(key_t key, int msgflg);
这个与共享内存的接口很像,第一个参数是一个key,第二个参数也是IPC_CREAT和IPC_EXCL
而这个key它的值还是从前面的地方来的
#include
#include
key_t ftok(const char *pathname, int proj_id);
这个msgget的返回值是成功返回一个消息队列标识符,失败返回-1。与前面的共享内存是极度相似的
#include
#include
#include
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
还是同样的接口,对于第二个参数,如果要释放,还是这个IPC_RMID
第三个参数的结构体也是很相似的
里面区分消息队列的也是key
共享内存用的是挂接和去挂接。然后就可以直接用了
而消息队列用的是这个接口,发送数据和接收数据
#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);
对于第一个函数
第一个参数是消息队列的标识符
第二个参数是要发送数据块的起始地址
第三个是要发送数据块的大小
第四个一般直接设置为0
这个第二个参数是void*的原因是要自己定义一个struct结构体,如同上面所示的那个msgbuf,只要保证第一个是类型,第二个是内容即可
第二个函数是用于接收数据的
第一个参数是从哪个消息队列拿
第二个参数和第三个参数与第一个函数是同样的道理,相当于一个缓冲区
第四个参数是要读取的哪一种类型的数据。也是我们在结构体中定义的
第五个参数和前面一样,默认为0即可
和共享内存一样,下面这个指令可以去查找当前的消息队列
ipcs -q
如果我们要删除的话,用这个指令
ipcrm -q XXX(msqid)
我们先来看信号量的接口
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
这个与前面的是极度相似的
还有下面这个接口
#include
#include
#include
int semctl(int semid, int semnum, int cmd, ...);
它里面也有对应的数据结构
我们通过观察这些system v系列的接口,我们发现他们都有一个这样的数据结构,
XXXid_ds{
struct ipc_perm XXX_perm
//....
}
里面的这个结构,它里面的字段都是一样的都是些权限
首先我们要知道的是,在操作系统中所有的IPC资源全部被整合在了一个IPC模块当中的!
如下所示,是System V方式的三种IPC方式的内核数据结构
这些数据结构都是要被管理起来的
我们要注意的是,它的第一个字段都是一模一样的结构体的。
在操作系统中还有一个数组,每当创建一个共享内存、消息队列、信号量等时候,就会将这个第一个字段的地址填入到这个数组中
未来当我们要去寻找一个共享内存是否存在的时候,就会直接去遍历这个数组里面的key,然后从而就可以进行直接比对key就知道了它是否已经创建了。
而且这个数组的下标就是所谓的XXXid,比如shmid等等
那么如何访问其他的成员呢?
注意看,这里正好是放在了第一个字段,所以这个字段的地址正好就是这个数据结构的地址。我们只需要将这个地址强转为这个数据结构的地址,然后就可以访问其他成员了!
那么现在问题来了,我们怎么知道我们要转成什么类型呢?
这其实是什么这个第一个字段的 xxx_perm结构体里面,有一个字段标志着是哪种资源,所以可以知道强转成什么类型
即OS能区分指针指向的对象的类型
而上面的这个操作,其实我们仔细一想,这不就是多态吗。struct ipc_perm是基类,其他的这些struct xxxid_ds就是子类
还有一点值得注意的是,这个数组是单独存在的,不隶属于任何进程。他是操作系统层面维护的一个数组,他跟文件描述符表没任何关系,这也导致这个数组最后被边缘化了,因为它没法和任何进程关联。
而且这个数组的下标是不断增大的。线性递增的。当达到最大的时候,才会回绕到0
如下图所示,是之前共享内存的基本原理
这里会出现一个问题:
当我们的A正在写入,写入了一部分,就被B进程拿走了,导致双方发和收的数据不完整,就导致了数据不一致问题。
而管道就自带同步机制,不会出现这个问题
这个共享内存就是没有任何的保护机制的
- A B看到的同一份资源,共享资源,如歌不加保护,会导致数据不一致的问题
- 我们可以通过加锁 – 互斥访问 – 即任何时刻,只允许一个执行流访问共享资源 — 这就是互斥
- 我们将这种共享的,任何时刻只允许一个执行流访问的资源叫做临界资源 — 一般是内存空间,比如管道等
- 举例:100行代码,只有5~10行代码才在访问临界资源。而我们将降温临界资源的代码称之为临界区
像我们的一个现象:在多进程、多线程并发打印的时候。显示器上的信息:是错乱的,混乱的,和命令行混在一起的。
这就是因为显示器是一种临界资源。
信号量/信号灯的本质是一把计数器,类似于但不等于 int cnt = n
用于描述临界资源中资源的数量!
比如说放映厅放100张票,当我们看电影的时候,我们还没去看电影,先买票,而买票的本质就是对资源的预定机制。
对于票数的计数器,每卖一张票,计数器要减1,放映厅的资源就要少一个
票数的计数器到0之后,资源就已经被申请完毕了
不过我们最怕的是,多执行流访问同一个资源,即n个资源被n+个执行流访问,一旦出现,就会发生像前面的显示器打印混乱现象
int cnt = 15; //我们引入一个计数器 int number = cnt--; //申请资源 cnt <= 0 //资源申请完了,再有执行流,就不给了
- 申请计数器成功,就表示我们具有访问资源的权限了!
- 申请了计数器资源,我当前要访问我们要的资源了吗?当然没有,申请了计数器资源是对资源的预定机制
- 计数器可以有效保证进入共享资源的执行流的数量
- 所以每一个执行流,想访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源。看电影的先买票!
我们将这个“计数器”,叫做信号量!
如果我们前面所提到的放映厅只有一个座位!,那么我们只需要一个值为1的计数器。
此时只有一个人能抢到这份资源,只有一个人能进放映厅看电影。即看电影期间只有一个执行流在访问临界资源。
这就是互斥!!!
所以我们把值只能为1,0两态的计数器叫做二元信号量,本质就是一个锁
上面中,我们凭什么让计数器为1??这是因为资源为1了,也就是说本质就是将临界资源不要分成很多块了,而是当作一个整体,整体申请,整体释放!!
思考一下:
要访问临界资源,先要申请信号量计数器资源。
所以信号量计数器,不也就是共享资源吗???
int cnt = 10; cnt--;
而要保护别人,我们先要保证自己的安全!!
而直接对一个整数–,并不安全。
这个cnt–;在C语言上是一条语句。
如果变成汇编,是多条(一般是3条)汇编语句
①cnt变量的内容,内存先要到CPU寄存器上
②CPU内进行–操作
③将计算结果写回cnt变量的内存位置
所以进程在运行的时候,可以随时被切换。这里其实就是有问题的,我们后序在说明
要处理上面的问题,操作系统就有一个信号量
申请信号量,本质是对计数器–,P操作
释放资源,释放信号量,本质是对计数器进行++操作系,也叫做V操作
申请和释放PV操作一定就是原子的!!!
原子的意思是一件事情要么不做,要做就做完,是两态的。没有“正在做”这样的概念!
最终结论
信号量本质是一把计数器,PV操作是原子的
执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源!!
信号量值1,0两态的。二元信号量,就是互斥功能
申请信号量的本质,是对临界资源的预定机制!!
system V的信号量接口是最难的。
下面的系统调用的功能就是申请一个信号量集
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
第一个参数是key,我们用ftok获取
第二个参数申请几个资源,如果申请一个就是1
第三个参数是设置为O_CREAT和O_EXCL,和前面一样
返回值就是信号量集标识符
这里需要注意,多个信号量和信号量是几是不一样的
#include
#include
#include
int semctl(int semid, int semnum, int cmd, ...);
第一个参数是信号量的标识符
第二个是几个信号量
第三个删除操作
可变部分可以传递信号量对应的结构体
同时这个函数除了删除信号量,也可以设置信号量
如果只有一个信号量,那么这个编号直接设置为0,cmd设置为SET。最后可变部分传递这个联合体,最终这个信号量初始值就被设置为对应的值
#include
#include
#include
int semop(int semid, struct sembuf *sops, unsigned nsops);
int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);
第一个函数中
第一个参数是对哪一个信号量进行操作
第二个参数是一个我们自定义的结构体
这个结构体中,第一个参数是想对哪一个信号量进行操作,如果只有一个,那就直接传0。第二个op要么是1要么是-1。如果是1就是对这个信号量+1,如果是-1,就是对这个信号量-1。从而可以进行PV操作
- 通信不仅仅是通信数据,互相协同也是
- 要协同,本质也是通信,信号量首先要被所有的通信进程看到!!
mmap函数其实也是共享内存
只不过它与前面的共享内存不同的是,它是将数据放到了磁盘上