目录
一、前言
二、共享内存
1、基本原理
2、实现代码
2.1、创建共享内存
2.2、释放共享内存
2.3、关联共享内存
2.4、与共享内存去关联
2.5、进程间通信
2.6、补充内容
三、system V信号量
1、概念
2、信号量
3、相关接口
3.1、获取信号量
3.2、释放信号量
3.3、信号量的PV接口
四、IPC资源管理方式
一、前言
在上篇文章《管道》中,已经介绍了通过管道实现进程间通信的方式。本篇文章主要着重于 system V共享内存进行讲解。
system V 是一套独立于操作系统外的标准,是一个专门为了通信设计出的内核模块,我们称之为 system V 的 IPC 通信机制。
因为进程具有独立性,所以任何进程间通信的方式,首先要做的就是让不同的进程看到同一份资源。
二、共享内存
1、基本原理
在物理内存中开辟一块空间,并在进程A和进程B的地址空间中分别通过页表与这一块空间建立映射关系,从而实现进程A和进程B共享一块内存。
当进程通信结束后,只需要通过修改页表,取消掉进程A和进程B与共享内存的映射关系,并释放这块内存就可以了。
操作系统中可能同时会有多对进程在通信。这就说明在任意时刻,可能有多个共享内存被用来进行通信。系统中有多个 shm 同时存在,就需要把他们根据先描述、再组织的方式管理起来。
所以共享内存并不是只需要在内存中开辟空间就可以了,系统也要为了管理共享内存,构建描述共享内存的结构体struct shm。struct shm中存放共享内存的全部属性。共享内存 = 共享内存的内核数据结构 + 真正开辟的内存空间。
2、实现代码
2.1、创建共享内存
创建共享内存的接口:
int shmget(key_t key, size_t size, int shmflg);
shmget 函数的参数列表中, key 是任意具有标定唯一性的数字。 size 表示所申请共享内存的大小。 shmflg 是创建共享内存的选项,常用以下两种选项:
- IPC_CREAT:创建一个共享内存。如果共享内存不存在,就创建。如果已经存在,则获取已经存在的共享内存并返回。
- IPC_EXCL:不能单独使用,一般都要配合IPC_CREAT使用。如果共享内存不存在,就创建。如果已经存在,则立刻出错返回。一旦创建成功,则对应的共享内存一定是最新的。
如果想要对共享内存设置权限,则也可以在 shmflg 选项中,按位或 指定权限。
对于 key 值,我们一般使用 ftok 函数来设置。
key_t ftok(const char *pathname, int proj_id);
ftok 函数会结合参数列表中的路径字符串 pathname 与项目id proj_id ,形成一个重复概率非常低的key值。这样互相通信的两个进程就可以通过相同的参数,得到一个唯一的key值,从而找到同一块共享内存,进而实现进程间通信的前提:让不同的进程看到同一份资源。key本质是在内核中使用的。
使用如下代码创建共享内存:
//comm.h
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
using namespace std;
#define PATHNAME "."
#define PROJID 0x0001
const int gsize = 4096;
key_t GetKey()
{
key_t k = ftok(PATHNAME, PROJID);
if(k == -1)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(1);
}
return k;
}
string toHex(int x)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%x", x);
return buffer;
}
int creatShmHelper(key_t k, int size, int flag)
{
int shmid = shmget(k, gsize, flag);
if(shmid == -1)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int creatShm(key_t k, int size)
{
return creatShmHelper(k, size, IPC_CREAT | IPC_EXCL);
}
int getShm(key_t k, int size)
{
return creatShmHelper(k, size, IPC_CREAT);
}
#endif
//server.cc
#include "comm.hpp"
int main()
{
//创建key
key_t k = GetKey();
cout << "server key: " << toHex(k) << endl;
//创建共享内存
int shmid = creatShm(k, gsize);
cout << "server shmid: " << shmid << endl;
return 0;
}
//client.cc
#include "comm.hpp"
int main()
{
key_t k = GetKey();
cout << "client key: " << toHex(k) << endl;
int shmid = getShm(k, gsize);
cout << "client shmid: " << shmid << endl;
return 0;
}
编译运行,运行 server 程序,并且等待 server 进程结束后,再次运行 server 程序,会发现以下现象:
原因是,共享内存的生命周期不随进程,随OS。进程结束后,所创建的共享内存不会被自动释放。
查看进程间通信内存资源的指令:
ipcs -m
可以看到共享内存依然存在。
2.2、释放共享内存
释放共享内存的指令:
ipcrm -m [shmid]
此时就可以再次运行 server 程序创建共享内存了:
共享内存除了使用指令释放外,还可以使用系统调用来释放:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl 函数的参数列表中, shmid 指定共享内存。 cmd 表示想对共享内存做什么操作。 buf 可以获取该共享内存的属性,并存放在 buf 中。
其中常用的 cmd 参数如下:
- IPC_RMID:直接释放共享内存。
- IPC_STAT:获取共享内存的属性。
1)IPC_STAT
获取共享内存的属性时,需要具备相应的权限,这里先设置一下:
编写代码:
编译运行:
函数获取到了共享内存的属性。
2)IPC_RMID
编译运行:
进程结束,共享内存已经被释放。
2.3、关联共享内存
挂接共享内存的系统调用:
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmat 函数的参数列表中, shmid 指定共享内存。 shmaddr 为共享内存挂接成功后得到的共享内存的虚拟地址的起始地址,一般设置为 NULL ,让系统自己挂接。 shmflg 为共享内存的选项,可以设置为只读属性,一般我们设置为 0 ,为读写属性。
新增代码:
编译运行:
观察到我们创建出的共享内存的 nattch 属性从 0 变为了 1 。这代表该共享内存的挂载数增加了。有几个进程与该共享内存相关联, nattch 就是几。
2.4、与共享内存去关联
与共享内存去关联的系统调用:
int shmdt(const void *shmaddr);
shmdt 的函数参数列表中,ahmaddr 为 shmat 函数的返回值,即共享内存挂载到虚拟内存中的起始地址。找到起始地址后,自然也就找到了共享内存的大小等属性,从而通过偏移量把虚拟地址对应的地址解关联。
新增代码:
2.5、进程间通信
为了使代码更加整洁,先把创建、关联、释放共享内存的代码封装成类:
#define SERVER 1
#define CLIENT 0
class Init
{
public:
Init(int t):type(t)
{
key_t k = GetKey();
if(type == SERVER)
shmid = creatShm(k, gsize);
else
shmid = getShm(k, gsize);
start = attachShm(shmid);
}
char* getStart() { return start; }
~Init()
{
detachShm(start);
if(type == SERVER)
delShm(shmid);
}
private:
char* start;
int type;
int shmid;
};
由于进程已经通过共享内存看到同一份资源了,接下来就借助共享内存实现进程间通信:
获取共享内存的起始地址,并通过起始地址读写共享内存中的数据。
2.6、补充内容
- 关于共享内存的大小:共享内存的大小是以 PAGE 页(4KB)为单位分配的。即OS所分配的共享内存的大小一定使 4KB 的倍数。但是,OS给分配了这么多,并不代表进程就可以使用这么多。比如,进程申请了 4097 字节的共享内存,因为超出了 4KB 大小,OS给该进程分配了 8KB 大小的共享内存,但是该进程只能使用其中的 4097 个字节,其他字节使用不了。
- 关于共享内存使用:可以看到上面的通信代码中并没有使用任何接口,这是因为一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程直接看到了,无需使用系统调用接口。
因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信最快的。也因此,共享内存没有任何的保护机制。
三、system V信号量
1、概念
- 互斥:任何一个时刻,都只允许一个执行流进行共享资源的访问。
- 临界资源:任何一个时刻,都只允许一个执行流进行访问的共享资源。
- 临界区:凡是访问临界资源的代码存放的地方,叫做临界区。对临界资源进行保护,实际上是对临界区进行保护,也是对代码进行保护。
- 原子性:要么不做,要么做完,这种只有两种确定状态的属性,被称为原子性。
2、信号量
信号量又被叫做信号灯,本质上是一个描述资源数量的计数器。
任何一个执行流,想访问临界资源中的一个资源的时候,不能直接访问,而要先申请信号量资源。如果申请到了,信号量执行 "--" 操作,表示一个临界资源已经被占用,这个过程称为 P 操作。这是一个预定机制,只要申请成功,那么这个执行流就一定能够拿到一个子资源,在需要的时候就能够进入临界区,访问对应的临界资源。如果信号量为 0 ,就表示已经没有临界资源了,后面再申请的执行流会进入阻塞状态。当执行流访问临界资源结束后,信号量执行 "++" 操作,表示将对应的资源进行了归还,这个过程称为 V 操作。
互斥功能本质就是将临界资源独立使用,即将信号量设置为 1 。
因为进程在访问临界资源时,都要申请信号量,这就意味着所有的进程都得看到同一个信号量,即信号量本身也是一个共享资源。为了保护自身的安全,就需要信号量自己的 "++" 与 "--" 操作都是原子性的。这部分内容在后面讲进程信号时会着重讲解。
3、相关接口
3.1、获取信号量
获取信号量的接口:
int semget(key_t key, int nsems, int semflg);
semget 函数的参数列表中, key 是一个具有唯一性的数字。 nsem 为申请信号量的个数(与一个信号量是几进行区分),称为信号量集。 semflg 是选项,常用的有 IPC_CREAT 与 IPC_EXCL ,不再重复介绍。返回值是信号量标示符。
查看信号量指令:
ipcs -s
3.2、释放信号量
释放信号量的指令:
ipcrm -s [信号量shmid]
释放信号量的系统调用:
int semctl(int semid, int semnum, int cmd, ...);
semctl 函数的参数列表中, semid 表示要对哪一个信号量集进行操作。 semnum 表示要对哪一个信号量进行操作。 cmd 是操作选项。 "..." 是可变参数,用来指定获取信号量的相关属性。
3.3、信号量的PV接口
int semop(int semid, struct sembuf *sops, unsigned nsops);
semop 函数的参数列表中, semid 表示要对哪一个信号量集进行操作。 sops 是一个结构体指针,需要自己定义,结构体由如下部分构成:
sem_num 表示哪一个信号量。 sem_op 表示进行什么操作(比如设置为 1 或 -1 ,表示加与减操作)。 sem_flg 是选项,设置为默认就可以。
nsops 与 sem_num 相同,表示哪一个信号量。
四、IPC资源管理方式
无论是共享内存、消息队列还是信号量,它们的结构体虽有很多不同,但是都在结构体第一个字段包含了另一个结构体 IPC_perm 。
共享内存:
消息队列:
信号量:
在OS中,是以 ipc_id_ary 数组的方式来管理所有的 ipc 资源的。因为所有 ipc 资源结构体的起始字段的类型都一样,都是 ipc_perm 。其简化模型与简化原理是下面这样的:
ipc_id_arr 是一个存储 struct ipc_perm* 类型数据的指针数组,因为每一个ipc资源的起始字段都是 struct ipc_perm* 类型,所以使用数组中的指针寻找到结构体起始地址,就相当于找到了整个结构体的地址,只需要找到起始地址后,把该指针强转成对应ipc资源的结构体类型就可以了。
其中数组的下标就是IPC资源的返回值,也叫做标示符。之所以我们所看到的标示符很大,是因为标示符是递增的,数组下标是循环使用的,当数组越界后,会回到下标为 0 的位置,但是标示符不会清零。
以上原理是对多态的应用。
关于system V共享内存与信号量的相关内容就讲到这里,希望同学们多多支持,如果有不对的地方,欢迎大佬指正,谢谢!