标识符与IPC Key

System V IPC相关系统接口:在这里插入图片描述

System V IPC未遵循“一切都是文件”的Unix哲学,而是采用标识符ID和键值来标识一个System V IPC对象。每种System V IPC都有一个相关的get调用,该函数返回一个整形标识符ID,System V IPC后续的函数操作都要作用在该标识符ID上。

System V IPC对象的作用范围是整个操作系统,内核没有维护引用计数。调用各种get函数返回的ID是操作系统范围内的标识符,对于任何进程,无论是否存在亲缘关系,只要有相应的权限,都可以通过操作System V IPC对象来达到通信的目的。

System V IPC对象具有内核持久性。哪怕创建System V IPC对象的进程已经退出,哪怕有一段时间没有任何进程打开该IPC对象,只要不执行删除操作或系统重启,后面启动的进程依然可以使用之前创建的System V IPC对象来通信

此外,我们无法像操作文件一样来操作System V IPC对象。System V IPC对象在文件系统中没有实体文件与之关联。我们不能用文件相关的操作函数来访问它或修改它的属性。所以不得不提供专门的系统调用(如msgctl、semop等)来操作这些对象。在shell中无法用ls查看存在的IPC对象,无法用rm将其删除,也无法用chmod来修改它们的访问权限。幸好Linux提供了ipcs、ipcrm和ipcmk等命令来操作这些对象。

还有System V IPC对象不是文件描述符,所以无法使用基于文件描述符的多路I/O技术。

1. 标识符与IPC Key

System V IPC对象是靠标识符ID来识别和操作的。该标识符要具有系统唯一性。这和文件描述符不同,文件描述符是进程内有效的。一个进程内的文件描述符4和另一个进程的文件描述符4可能毫不相关。但是IPC的标识符ID是操作系统的全局变量,只要知道该值具有相应的权限,任何进程都可以通过标识符进行进程间通信。

三种IPC对象操作的起点都是调用相应的get函数来获取标识符ID,如消息队列的get函数为:

int msgget(key_t key, int msgflg);

其中第一个参数是key_t类型,它其实是一个整形的变量。IPC的get函数将key转换成相应的IPC标识符。根据IPC get函数中的第二个参数oflag的不同,会有不同的控制逻辑。

因为key可以产生IPC标识符,就是同一个key调用IPC的get函数总是返回同一个整形值。实际上并非如此。在IPC对象的生命周期中,key到标识符ID的映射是稳定不变的,即同一个key调用get函数,总是返回相同的标识符ID。但是一旦key对应的IPC对象被删除或系统重启后,则重新使用key创建的新的IPC对象分配的标识符很可能是不同的。

不同进程可通过同一个key获取标识符ID,进而操作同一个System V IPC对象。那么现在问题就演变成了如何选择key。

对于key的选择,存在以下三种方法

  1. 第一种方法是随机选择一个整数值作为key值。作为key值的整数通常被放在一个头文件中,所有使用该IPC对象的程序都要包含该头文件。需要注意的是,要防止无意中选择了重复的key值,从而导致不需要通信的进程之间意外通信,以至发生程序混乱。一个技巧是将项目用到的所有key放入同一个同文件中,这样就可以方便的检查是否有重复的key值。
  2. 第二种方法是使用IPC_PRIVATE,使用方法如下:
id = msgget(IPC_PRIVATE,S_IRUSR | S_IWUSR);

这种方法无须指定IPC_CREATE和IPC_EXCL标志位,就能创建一个新的IPC对象。使用IPC_PRIVATE 时总是会创建新的IPC对象,从这个角度看将其称之为IPC_NEW或许更合理。

不过,使用IPC_PRIVATE来得到IPC标识符会存在一个问题,即不相干的进程无法通过key值来得到同一个IPC标识符。因为IPC_PRIVATE总是创建一个新的IPC对象。因此IPC_PRIVATE一般用于父子进程,父进程调用fork之前创建IPC对象,创建子进程后,子进程也就继承了IPC标识符,从而父子进程可以通信。当然无亲缘关系的进程也可以使用IPC_PRIVATE,只是稍微麻烦了一点,IPC对象的创建者必须想办法将IPC标识符共享出来,让其他进程有办法获取到,从而通过IPC标识符进行通信。

  1. 第三种方法是使用ftok函数,根据文件名生成一个key。ftok是file to key的意思,多个进程通过同一个路径名获得相同的key值,进而得到同一个IPC标识符。

ftok函数接口定义如下:

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

在Linux实现中,该接口把通过path-name获取的信息和传入的第二个参数的低8位糅合在一起,得到一个整形的IPC key值 ,如图所示。需要注意的是,pathname对应的文件必须是存在的。

这个函数在Linux上的实现是:按照给定的路径名,获取到文件的stat信息,从stat信息中取出st_dev和st_ino,然后结合给出的proj_id,按照图所示的算法获取到32位的key值。
在这里插入图片描述

2. IPC的公共数据结构

三种System V IPC对象有很多共性,从代码层面上看也有很多公共的部分。权限结构就是其中一个。IPC的权限结构至少包括以下成员:

struct ipc_perm{
key_t key;
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
ulong_t seq ;
};
/消息队列控制相关的结构体/
struct msqid_ds {
struct ipc_perm msg_perm;
...
}
/*信号量控制相关的结构体*/
struct semid_ds {
struct ipc_perm sem_perm;
...
}
/*共享内存控制相关的结构体*/
struct shmid_ds {
struct ipc_perm shm_perm;
...
}

uid和gid字段用于指定IPC对象的所有权。cuid和cgid字段保存着创建该IPC对象的进程的有效用户ID和有效组ID。初始情况下,用户ID(uid)和创建者ID(cuid)的值是相同的。它们都是调用进程的有效ID。但是创建者ID(cuid)是不可改变的,而所有者ID则可以通过IPC_SET来改写

下面的代码演示了如何修改共享内存的uid字段

struct shmid_ds shm_ds;
if(shmctl(id,IPC_STAT,&shm_ds)) == -1
{
/*error handler*/
}
shm_ds.shm_perm.uid = newuid;
if(shmctl(id,IPC_SET,&shm_ds) == -1)
{
/*error handle*/
}

mode是用来控制读写权限。所有的System V IPC对象都不具备执行权限,只有读写权限。其中对于信号量而言,写权限意味着修改权限。IPC对象的权限控制,分别具有读写权限。

和文件的权限有点类似,IPC对象的权限被分成了三类:owner、group和other。创建对象时可以为各个类别设定不同的访问权限,代码如下:

msg_id = msgget(key,IPC_CREAT | S_IRUSR | S_IWUSR |S_IRGRP);
msg_id = msgget(key,IPC_CREAT | 0640);

当以一个进程尝试对IPC对象执行某种操作的时候,首先会检查权限。检查逻辑如下:

  1. 如果进程是特权进程,那么进程拥有对IPC对象的所有权限。
  2. 如果进程的有效用户ID与IPC对象的所有者或创建者ID匹配,那么会将对象的owner的权限赋值给进程。
  3. 如果进程的有效用户ID或任意一个辅助组ID与IPC对象的所有者组ID或创建者组ID匹配,那么会将IPC对象的group的权限赋予进程。
  4. 否则,将IPC对象的other权限赋予进程。

数据结构ipc_perm中的key和seq也很有意思。key比较简单,就是调用get函数创建IPC对象时传递进去的key值。如果key的值是IPC_PRIVATE,则实际的key值是0。

和key相比,成员变量seq就不那么好理解了。进程分配文件描述符时采用的是最小可用算法。比如文件描述符5曾经被分配给文件A,但是很快进程关闭了文件A。如果进程尝试打开另外一个文件,此时如果5是最小可用的槽位,那么新打开的文件描述符就是5.但是IPC对象的标识符ID分配不能采用这个算法。因为对个进程要通过标识符ID来通信,而标识符ID是整个系统内有效的。如果采用最小可用的算法,一般来讲,IPC对象的个数不会太多,那么这个数字很容易就被猜到了。举例来说,如果存在一个恶意程序要攻击消息队列,它只需要尝试很小范围内的数字,就可以猜到IPC对象的标识符ID,进而偷偷取走信息队列里面的信息。

内核为每一种System V IPC维护了一个ipc_ids类型的结构体。该结构体的组成如图所示:
在这里插入图片描述
结构体中seq字段记录了开机依赖创建该IPC对象的流水号。创建时seq的值自加,但是销毁的时候seq的值并不会自减。seq的值随着该种IPC对象的创建而单调递增,直到递增到上限(max_seq)在溢出回绕,重新从0开始。

当需要创建新的IPC对象时,三种IPC对象都会走到ipc_addid处。

ipc_addid函数会初始化IPC对象的很多成员变量,比如权限相关的uid、gid、cuid,也会维护该IPC对象的seq值。

前面提到,内核分配IPC对象标识符的时候,使用的并不是最小可用算法,如下:

#define IPCMNI 32768
#define SEQ_MULTIPLIER (IPCMIN)
static inline int ipc_buildid(int id, int seq)
{
return SEQ_MULTIPLIER * seq + id;
}

上面公式中的id就是最小可用的槽位,而seq是开机以来内核创建IPC对象的流水号。因此,返回的ID是一个比较大的值。仍然以消息队列为例,如果开机后,消息队列为空,创建的第一个消息队列的标识符必然为0。第二个是32769,第三个是65538。

根据上面的讨论可知,IPC对象的标识符ID虽然是通过get函数来获得的,但是和key值并不存在永久的关系,即不存在公式可以通过key值来计算出标识符ID。内核仅仅是关联了两者。重启系统之后,后者删除IPC对象之后,根据相同的key值再次创建,得到的标识符ID很可能并不相同。

内核面临着如何根据IPC对象的标识符ID,快速地找到内核中的IPC对象的难题,根据前面的计算公式,可得:

slot_index = 标识符ID % SEQ_MULTIPLIER

这个公式透露出了一个问题:整个系统内,每一种IPC对象的槽位有限,最多有IPCMNI个槽位。在ipc_addid函数中也证实了这一点,系统内的硬上限为IPCMNI,即32768。这个限制就决定了不能无限制地创建IPC对象。

Linux系统提供了各种系统调用API用于进程之间的通信:    无名管道PIPE    命名管道FIFO    消息队列    共享内存    信号量    文件锁    信号signal....其中还包括system V和POSIX 两种接口标准,除此之外,Linux系统自身还扩展了自己的一套API接口用于进程间通信,比如signalfd、timerfd、eventfd等。本视频教程为《Linux系统编程》第05期,本期课程将会带领大家学习Linux下将近15种进程间通信IPC工具的使用,了解它们的通信机制、编程实例、使用场景、内核中的实现以及各自的优缺点。本课程会提供PDF版本的PPT课件和代码,学员购买课程后可到课程主页自行下载嵌入式自学路线指导图:------------------------------------------------------------------------------------------------------                   《嵌入式工程师自我修养》嵌入式自学系列教程                                          作者:王利涛------------------------------------------------------------------------------------------------------一线嵌入式工程师精心打造,嵌入式学习路线六步走: 第 1 步:Linux三剑客零基础玩转Linux+UbuntuGit零基础实战:Linux开发技能标配vim从入门到精通基础篇:零基础学习vim基本命令vim从入门到精通定制篇:使用插件打造嵌入式开发IDEmakefile工程实践基础篇:从零开始一步一步写项目的Makefilemakefile工程实践第2季:使用Autotools自动生成Makefile软件调试基础理论printf打印技巧Linux内核日志与打印使用QEMU搭建u-boot+Linux+NFS嵌入式开发环境第 2 步:C语言嵌入式Linux高级编程第1期:C语言进阶学习路线指南第2期:计算机架构与ARM汇编程序设计第3期:程序的编译、链接和运行原理第4期:堆栈内存管理第6期:数据存储与指针第7期:嵌入式数据结构与Linux内核的OOP思想第8期:C语言的模块化编程第9期:CPU和操作系统入门      搞内核驱动开发、光会C语言是不行的!      你还需要学习的有很多,包括:计算机体系架构、ARM汇编、程序的编译链接运行原理、CPU和操作系统原理、堆栈内存管理、指针、linux内核中的面向对象思想、嵌入式系统架构、C语言的模块化编程.....第 3 步:Linux系统编程第00期:Linux系统编程入门第01期:揭开文件系统的神秘面纱第02期:文件I/O编程实战第03期:I/O缓存与内存映射第04期:打通进程与终端的任督二脉第05期:进程间通信-------------------we are here!‍    第 4 步:Linux内核编程‍    练乾坤大挪移,会不会九阳神功,是一道坎。搞驱动内核开发,懂不懂内核也是一道坎。第 5 步:嵌入式驱动开发    芯片原理、datasheet、硬件电路、调试手段、总线协议、内核机制、框架流程....第 6 步:项目实战    嵌入式、嵌入式人工智能、物联网、智能家居...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值