行业知识
【Linux】SystemV IPC
进程间通信
- 一、SystemV 共享内存
- 1. 共享内存原理
- 2. 系统调用接口
- (1)创建共享内存
- (2)形成 key
- (3)测试接口
- (4)关联进程
- (5)取消关联
- (6)释放共享内存
- (7)测试通信
- 3. 共享内存的特性
- 二、SystemV 消息队列(了解)
- 1. SystemV 消息队列原理
- 2. 系统调用接口
- (1)创建消息队列
- (2)形成 key
- (3)发送/接收数据
- (4)释放消息队列
- 三、IPC在内核中的数据结构设计
- 四、SystemV 信号量
- 1. 引入概念
- 2. 理解信号量
- 3. 了解系统调用接口
- (1)申请信号量
- (2)释放信号量
- (3)���作信号量
一、SystemV 共享内存
1. 共享内存原理
那么我们知道,进程间通信的本质就是先让不同的进程看到同一份资源。我们以前学的管道都是基于文件的,那么我们还有其它方案进行进程间通信吗?有的,那么我们下面学习的共享内存就是由操作系统帮我们在地址空间中进行通信。
我们知道,每一个进程都有自己的 task_struct,也就是有自己的地址空间,然后通过让操作系统在物理内存创建一块内存空间,因为是操作系统,所以它也有资格修改进程的页表、地址空间等。然后将这块内存空间映射到对应进程地址空间的共享区中,最后给应用层返回这个起始的虚拟地址,如下图:
如上过程,就可以让不同的进程,看到了同一份资源!这个原理就叫做共享内存。
所以上面的步骤我们可以分为:
- 申请物理内存
- 将内存挂接(关联)到进程地址空间
- 返回起始地址
如何需要释放共享内存呢?首先需要将进程和共享内存去关联,再去释放共享内存。那么上面的操作,都不是进程直接做的,因为如果是进程去申请空间,那么这个空间就属于这个进程了!就不是共享内存了!所以这些操作都是由操作系统来做的!所以操作系统就必须需要给我们提供一系列的系统调用!
那么系统中肯定不止一两个进程进行进程通信,也就是物理内存中也不止一个共享内存,必定会有很多份,那么操作系统就要管理所有的共享内存!那么操作系统就要对这些共享内存先描述,再组织!所以内核中就得有一个 struct 结构体描述我们申请的共享内存有多大、有多少进程关联等等属性。
2. 系统调用接口
(1)创建共享内存
首先不管怎样,我们得在系统里创建一个共享内存,在 Linux 中创建一个共享内存的系统接口为:shmget(),手册如下:
其中返回值,成功返回共享内存的标识符,是一个整数;否则返回 -1,错误码被设置。
- size
而 shmget() 中有三个参数,我们先看第二个参数 size,这个 size 就是需要创建共享内存的大小,单位是字节。
- shmflg
关于第三个参数 shmflg,我们先理解,有进程申请空间,就会有进程使用,那么创建共享内存只需要创建一次就够了,其它进程想在这个共享内存中通信的时候,不需要创建了,只需要获取这个共享内存就行了。所以在使用共享内存时,肯定需要通过某种方式去表示如何创建、如何获取这样的概念,那么 shmflg 就是可以表示这些内容,其中有如下选项:
其中 IPC_CREAT 表示创建一个共享内存,如果不存在就直接创建,存在就直接获取并返回。如果这个选项单独使用就是以上效果。
IPC_CREAT | IPC_EXCL 表示创建一个共享内存,如果不存在就直接创建,存在就出错返回。那么这两个选项组合使用,就能确保我们申请的共享内存一定是一个新的!
而 IPC_EXCL 不单独使用。
- key
那么问题又来了,系统怎么知道这个共享内存是否存在呢?怎么保证让不同的进程看到同一个共享内存呢?所以这时候就要介绍第一个参数 key 了,就是通过这个参数 key 保证的!
关于参数 key,我们先理解,无论是创建共享内存还是获取共享内存,我们必须要拿到同一个 key,因为拿到同一个 key 才能保证访问的是同一个共享内存!所以 key 是一个数字,它是多少不重要,关键在于它必须在内核中具有唯一性,才能够保证让不同进程进行唯一性标识。
正因为有了 key,第一个进程就可以通过 key 创建共享内存,第二个进程之后,只要拿着同一个 key 就可以和第一个进程看到同一个共享内存了!
那么对于一个已经创建好的共享内存的 key 在哪呢?毫无疑问,key 在共享内存的描述对象中!
那么第一次创建的时候,这个 key 怎么有呢?首先我们需要确保这个 key 具有唯一性,而我们知道,路径天然就具有唯一性,所以我们就可以根据路径这样具有唯一性的属性形成对应的 key,那么在系统中有一个接口可以帮助我们形成一个 key,下面介绍。
(2)形成 key
其中手册如下:
返回值就是 key;第一个参数就是路径,第二参数是项目id;这两个参数我们都可以随意传,只要保证可以创建出具有唯一性的 key 即可,如果创建失败,我们只需要修改这两个参数即可。失败的原因可能有系统内存不足。或者 key 的唯一性不足等等。
其实 ftok() 就是一套算法,它会把我们的路径和项目 id 进行了数值的计算,转化为一个数字。
那么这个 key 为什么要我们用户自己形成呢?因为如果是操作系统帮我们形成,我们就无法将这个 key 交给另一个和我们通信的进程了,它也不知道我们需要和哪一个进程通信,只有我们用户才清楚!所以这个 key 是由用户约定的!
(3)测试接口
接下来我们就可以使用这两个接口进行测试了,我们也引入上一次写的日志函数进来,如下代码:
#define SHM_SIZE 4096const string pathname = "/home/lmy";const int proj_id = 0x2314;log lg;// 获取 keykey_t GetKey(){ key_t k = ftok(pathname.c_str(), proj_id); if(k我们启动一个进程A进行测试:
int main(){ int shmid = GetShareMem(); sleep(10); lg(Debug, "processA quit!"); return 0;}结果如下:
我们看到,返回的 key 和 shmid,它们为什么要同时存在呢?因为 key 是在操作系统内标定唯一性的;而 shmid 只在进程内用来标识资源的唯一性的!
为了方便观察我们可以将 key 打印成十六进制的;当我们创建好共享内存后,再去创建会如何呢?如下:
如上图,明明我们上次运行的进程A已经结束了,为什么重新创建会失败呢?首先我们可以使用 ipcs -m 查看操作系统内所有的 IPC 资源,如下:
其中 perms 是权限,我们还没有设置;nattch 表示当前这个共享内存有几个进程和它是关联的。
但是,我们的进程已经退出了,IPC资源还是存在的!这说明共享内存的生命周期是随内核的!如果用户不主动关闭,共享内存会一直存在,除非内核重启或者用户主动关闭。
那么我们可以使用指令 ipcrm -m shmid 直接删除,如下:
接下来我们就要把权限设置上,那么权限是在 shmget() 的接口第三个参数中设置,如下:
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
我们重新运行观察结果,发现权限就有了:
当前有进程创建共享内存了,但是也要有进程获取到共享内存,所以接下来我们需要将接口修改一下,让其它进程也可以获取到共享内存,如下:
int GetShareMem(int flag){ key_t k = GetKey(); int shmid = shmget(k, SHM_SIZE, flag); if(shmid那么下面我们介绍一下共享内存的大小,我们在上面设置的大小是 4096 字节,如果我们将它设置成 4097 呢?我们尝试一下:
如上,大小被设置成了 4097 字节,但是共享内存的大小一般是 4096 的整数倍,我们上面设置的 4097 实际上操作系统申请的共享内存大小是 4096 * 2,但是供我们使用的只有 4097 字节!
(4)关联进程
我们现在已经有了共享内存,接下来就要进行对共享内存和进程进行挂接了。那么使用到的系统接口是:shmat(),手册如下:
其中第一个参数 shmid 就是我们上面一直在说的 shmid,即创建或获取共享内存接口的返回值。第二个参数 shmaddr 就是我们想让当前的共享内存挂接到共享区的哪个位置,但是一般让系统决定挂接到哪里,所以设置为 nullptr 即可,那么最终挂接到的虚拟地址会以返回值的形式返回给我们。第三个参数 shmflg 就是有关挂接的权限,我们按照共享内存默认的权限即可,设置为0即可。
使用如下:
int main(){ int shmid = CreateShm(); char* ret = (char*)shmat(shmid, nullptr, 0); lg(Debug, "attach success"); sleep(3); return 0;}挂接成功后:
进程退出后:
(5)取消关联
我们上面演示的结果中,都是进程退出后自动关闭关联的,那么我们也可以使用系统接口取消关联,对应接口为:shmdt(),手册如下:
那么它只有一个参数 shmaddr,这个参数就是 shmat() 的返回值。
那么我们只需要传入起始地址就可以了吗?它怎么知道这个空间有多大呢?那么共享内存实际上被申请的时候,它有自己的管理属性,那么它自己会记录共享内存有多大,共享内存也必须是连续的,所以在进行地址空间映射的时候,从连续空间加上大小,我们就知道它的范围了,我们只需要知道从哪开始就行了。
接下来我们对该接口进行测试:
int main(){ int shmid = CreateShm(); char* ret = (char*)shmat(shmid, nullptr, 0); lg(Debug, "attach success"); sleep(3); int r = shmdt(ret); if(r结果如下:
(6)释放共享内存
我们从上面知道,共享内存的生命周期是随内核的,所以每次进程退出后 IPC 资源还是存在的,那么我们也可以使用指令直接把它释放,但是我们还有对应的系统接口释放共享内存,其接口为:shmctl(),手册如下:
那么第一个参数就是共享内存的 id;关于第三个参数,struct shmid_ds 就是类似于内核当中的管理共享内存所对应的 struct 结构体。
也就是说,它一定能让我们获取到共享内存的属性,那么我们要查看共享内存的属性还是修改还是什么呢?所以就有了第二个参数 cmd,表明我们要做什么操作,那么 cmd 的选项有如下:
其中我们需要的是 IPC_RMID,它的作用是标记共享内存被删除。我们删除就不关注共享内存的属性了,所以第三个参数设为 nullptr 即可。那么返回值成功也是返回0,失败返回-1.
测试代码如下:
int main(){ int shmid = CreateShm(); char* ret = (char*)shmat(shmid, nullptr, 0); lg(Debug, "attach success"); sleep(3); int r = shmdt(ret); if(r结果如下:
(7)测试通信
上面操作我们已经把一个共享内存的整个生命周期写完了,下面就可以让两个进程实现通信了。
首先我们测试一下让两个进程看到同一份资源。代码如下:
进程A:
int main(){ int shmid = CreateShm(); char* ret = (char*)shmat(shmid, nullptr, 0); lg(Debug, "attach success"); sleep(3); shmdt(ret); lg(Debug, "shmdt success: 0x%x", ret); sleep(3); shmctl(shmid, IPC_RMID, nullptr); lg(Debug, "destory shm done, shmaddr: 0x%x", ret); sleep(3); return 0;}进程B:
int main(){ int shmid = GetShm(); char* ret = (char*)shmat(shmid, nullptr, 0); lg(Debug, "attach success"); sleep(3); shmdt(ret); lg(Debug, "shmdt success: 0x%x", ret); sleep(3); return 0;}我们只需要观察共享内存中的 nattch 即可判断这两个进程是否已经看到了同一份资源:
如上,我们就能让两个进程看到了同一份资源。
接下来就可以进行通信了,那么我们可以把两个进程中的日志和休眠函数都去掉;我们让进程A进行读取,即把共享内存当作字符串;进程B进行写入,代码如下:
进程A:
int main(){ int shmid = CreateShm(); char* ret = (char*)shmat(shmid, nullptr, 0); // 开始通信 while(true) { // 直接访问共享内存 cout
上一篇:怎么进去他人服务器界面?
下一篇:阿里云服务号怎么申请?