Android跨进程通信IPC之1——Linux基础

Android跨进程通信IPC整体内容如下

由于Android系统是基于Linux系统的,所以有必要简单的介绍下Linux的跨进程通信,对大家后续了解Android的跨进程通信是有帮助的,本篇的主要内容如下:

  • 1、Linux介绍
    1.1、Unix操作系统
    1.2、GNU
    1.3、Linux的诞生
    1.4、开源发展实验室和Linux基金
    1.5、Linux的全局图
    1.6、Linux的源码目录结构
  • 2、内核态与用户态
    2.1、内核态与用户态简介
    3.2、为什么要有用户态和内核态
    2.3、用户态与内核态的切换
  • 3、红黑树
    3.1、二叉搜索树
    3.2、红黑树
    3.3、数据结构设计
    3.4、树的旋转知识
  • 4、Linux的跨进程通信
    4.1、匿名管道(pipe)
    4.2、命名管道(FIFO)
    4.3、信号(signal)
    4.4、信号量(semaphore)
    4.5、消息队列(message queue)
    4.6、共享内存(share memory)

4.7、套接字(Socket)
4.8、Linux的几种跨进程通信的方式的比较

的旋转知识

一、Linux介绍

说到Linux操作系统,不的不说下Unix系统

(一)、Unix操作系统

Unix因为其安全可靠,高效强大的特点在服务器领域得到了广发的应用。直到GNU/Linux流行开始前,Unix也是科学计算、大型机、超级电脑等所用操作系统的主流。

1、Unix的诞生

汤普逊和里奇最早是在贝尔实验室开发Unix,此后10年,Unix在学术机构和大型企业中得到了广泛的应用,当Unix拥有者AT&T公司以低廉甚至免费的许可将Unix源码授权给学术机构做研究或教学之用,许多机构在此源码的基础加以扩展和优化,形成了所谓的"Unix变种",这种变种反过来也促进了Unix的发展,其中最著名的变种就是BSD产品。BSD在后续的发展中也衍生出了3个主要分支:FresBSD、OpenBSD和NetBSD。

2、Unix名字的由来

Unix的诞生和Multics(Multiplexed Information android Computing System) 是有一定渊源的。Multics是由麻省理工学院,AT&T贝尔实验室和通用电气合作进行的操作系统项目。由于Multics的失败,AT&T撤出了Multics投入的资源,其中一位开发者——肯.汤普逊则继续开发软件,并最终编写一个太空旅行游戏,但是他发现游戏速度很慢,并且成本高,在丹尼斯.里奇的帮助下,汤普逊用PDP-7的汇编语言重写了这个游戏,并让其再DEC PDP-7上运行起来。这次经历加上Multics项目的经验,汤普逊和里奇领导一组开发者开发了一个新的多任务操作系统,这个系统包括命令解释器和一些实用程序。1970年那部PDP-7却只能支持两个用户,所以他们就开玩笑的称他们的系统其实是"UNiplexed Information and Computing System",缩写为"UNICS",于是这个项目被称为UnICS(Uniplexed Information and Computing System)。后来,大家取其谐音这个名字就改为UNIX

3、Unix的文化

UNIX is not jus an operating system , but a way of life. (UNIX 不仅仅是一个操作系统,更是一种生活方式。) 经过几十年的发展,UNIX在技术上日臻成熟的过程中,他独特的设计哲学和美学也深深地吸引了一大批技术人员,他们在维护、开发、使用UNIX的同时,UNIX也影响了他们的思考方式和看待世界的角度。这些人自然而然地形成了一个社团

4、Unix的重要设计原则:
  • 简洁至上
  • 提供机制而非策略

(二)、GNU

GNU又称革奴计划,是由查理德.斯托曼在1983年9月27日发起的,它的目标是创建一套完全自由的操作系统。
《GNU宣言》是解释为什么发起该计划的文章,其中一个重要理由就是要"重现当年软件界合作互助的团结精神"。为了保证GNU软件可以自由地"使用、复制、修改和发布",所有GNU软件都有一份在禁止其他人添加任何限制的情况下授权所所有权利了给任何人的协议条款,GNU通用的公共许可证(GNU General Public License GPL)。即"反版权"(或者称copyleft)概念。

GNU是“GNU is Not Unix”的缩写。斯托曼宣布GNU应当发音为Guh-NOO以避免与new这个单词混淆(注:Gnu在英文中原意为非洲牛羚,发音与new相同)。UNIX是一种广泛使用的商业操作系统的名称。由于GNU将要实现UNIX系统的接口标准,因此GNU计划可以分别开发不同的操作系统部件。GNU计划采用了部分当时已经可自由使用的软件。不过GNU计划也开发了大批其他的自由软件。

(三)、Linux的诞生

1、Linux的诞生

1991年,在赫尔辛基,Linus Toral开始写了一个项目,目的是用来访问大学里面的大型Unix服务器的虚拟终端。他专门写了一个用于他当时正在用的硬件的,与系统操作系统无关的程序,开发是在Minix,用的编译器是GCC来完成的,这个项目后面逐渐转变为Linux内核。

2、Linux的诞生

Linus Torvalds本要把他的发时叫做Freax——“fread”,“free”和“x”(暗指Unix)的合成词。在开发系统的前半年里,他把文件以文件名“Freax”存储。Torvalds考虑过Linux这个名字,但是因为觉得它过于自我本位而放弃了使用它。为便于开发,后面,他把那些文件上传到了赫尔辛基工业大学(HUT)的FTP服务器。Torvalds在HUT负责管理那个服务器的同事,觉得“Freax”这个名字不是很好,就在不咨询Torvalds的情况下,把项目的名字改成了“Linux”。但是之后,Torvalds也同意“Linux”这个名字了:“经过多次讨论,他承认Linux这个名字更好。在0.01版本Linux的源代码的makefile里仍然使用‘Freax’这个名字,在之后‘Linux’这个名字才被使用。所以,Linux这个名字并不是预先想好的,只是它被广泛接受了而已”。

(四)、开源发展实验室和Linux基金

开源码发展实验室(Open Source Development Lab)创立于2000年。它是一个独立的非营利性组织。它的目标是优化Linux以应用于数据中心和运营商的领域。它是Linus Torvalds和Andrew Morton工作的赞助来源。2006年年中,Morton去了Google(Google也是使用Linux内核的);Torvalds全职为OSDL开发Linux内核。非商业性运营机制的资金主要来源于Red Hat,Novell,三菱,英特尔, IBM ,戴尔和惠普等几家大公司。

2007年1月22日,OSDL和自由标准组织合并为Linux基金会,把它们的工作焦点集中在改进GNU/Linux以与Windows竞争。

(五)、Linux的全局图

下面是一幅Linux kernel map:


Linux kernel map.png

这是makeLinux网站提供的一幅非常经典的Linux内核图,涵盖了内核最为核心的方法,Linux除了驱动开发外,还有很多通用子系统,比如CPU,memory,file system等核心模块,即便不做底层驱动开发,掌握这些模块对于加深理解整个系统运转还是很有帮助的。

(六)、Linux的源码目录结构

目录 解释 部分子子目录
kernel 内核管理相关,进程调度等 sched/fork等
fs 文件子系统 ext4/f2fs/fuse/debugfs/proc等
mm 内存子系统
drivers 设备驱动 staging/cpufreq/gpu 等
arch 所有CPU系统结构相关的代码 arm/x86等
include 头文件 linux/uapi/asm_generic等
lib 标准通用的C库
ipc 进程通信相关
init 初始化过程(非系统引导)
block 块设备驱动程序
crypto 加密、解密、校验算法
Documentation 说明文档

二、内核态与用户态

这部分是临时加进来的,是在后面的Binder驱动里面会用到,原来是打算加到"Android跨进程通信IPC之1——Linux基础"里面,不过由于简书的篇幅限制,我加到这里来了。

1、内核态和用户态的简介

  • ** 内核态 **:CPU可以访问内存所有数据,包括外围设备,例如硬盘、网卡,CPU可以将自己从一个程序切换到另外一个程序。
  • ** 用户态 **: 只能受限的访问内存,且不允许访问外围设备,占用CPU的能力被剥削,CPU资源可以被其他程序获取。

2、为什么要有用户态和内核态

由于需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者获取外围设备的数据,并发送网络,CPU划分出两个权限等级 ----用户态 和 内核态。

3、用户态与内核态的切换

3.1 切换简介

所有用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核态的事情,例如从硬盘读取数据,或者从键盘获取输入等。而唯一可以这这些事情的就是 操作系统 ,所以这时候 程序 就需要先向 操作系统 请求,以 程序 的名字来执行这些操作。这时候就需要一个这样的机制:用户态 切换到 内核态,但是不能控制内核态中执行的执行这种机制叫做** 系统调用 **,在CPU中的实现称之为 "陷阱指令(Trap Instruction)"

3.2 系统调用机制流程:
  • 1、用户态程序将一些数据值放在寄存器中,或者使用参数创建一个堆栈(stack frame),以表明需要操作系统提供的服务。
  • 2、用户态程序执行陷阱指令
  • 3、CUP切换到内核态,并跳到内存指定位置的指令,这些指令是操作系统的一部分,他们具有内存保护,不可被用户态程序访问
  • 4、这些指令称之为 陷阱 (trap) 或者新系统调用处理器 ( system call hanlder )。他们会读取程序放入内存的数据参数,并执行程序请求的服务。
  • 5、系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。

三、红黑树

红黑树是60年代中期计算机科学界寻找一种算法复杂度稳定,容易实现的数据存储算法的产物。红黑树是常用的一种数据结构,它使得对数据的索引,插入和删除操作都能保持在O(lgn)的时间复杂度。在优先级队列、字典等实用领域都有广泛地应用,更是70年代提出的关系数据库模型——B树的鼻祖。当然,相比于一般的数据结构,红黑树实现的难度有所增加。** 关键是后面要讲解的Binder驱动里面用到了红黑树 **

(一)二叉搜索树

在具体实现红黑树之前, 必须弄清楚它的基本含义。红黑树本质上是一颗二叉搜索树,它满足二叉搜索树的基本性质——即树中的任何节点的值大于它的左子节点,且小于它的右子节点。

二叉搜索树.png

按照二叉搜索树组织数据,使得对元素的查找非常快捷。比如上图的中的二叉搜索树,如果查询值为48的节点,只需要遍历4个节点即可完成。理论上,一颗平衡的二叉树搜索树的任意节点平均查找效率为树的高度h,即O(lgn)。但是如果二叉搜索树的失去平衡(元素在一侧),搜索效率就退化为O(n),因此二叉搜索树的平衡是搜索效率的关键所在。为了维护树的平衡性,数据结构内出现了各种各样的树,比如AVL树通过维持任何节点的左右子树的高度差 不大于1保持树的平衡,而红黑树使用颜色维持树的平衡,使二叉搜索树的左右子树的高度差 保持在固定的范围。相比于其他二叉搜索树,红黑树对二叉搜索树的平衡性维持着自身的优势

(二) 红黑树

红黑树,顾名思义,红黑树的节点是有颜色概念的,即非红即黑,通过颜色的语速,红黑树为支持着二叉搜索树的平衡性。一个红黑树必须有下面5个特征

  • 1、节点是红色或黑色
  • 2、根是黑色
  • 3、所有叶子是黑色(叶子是NIL节点)
  • 4、每个红色节点的两个子节点都是黑色的(从每个叶子到跟的所有路径不能有两个连续的红色节点)
  • 5、从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

如下图


红黑树图1.png

这些特征强制约束了红黑树的关键性质:从跟到叶子的最长可能路径不多于最短可能路径的两倍长(特征4保证了路径最长的情况为1红1黑,最短的情况为全黑,再结合特征5,可以推导出)。结果是这个树大致上是平衡的。因为比如插入、删除和查找操作中,操作某个值的最坏情况的时间都要求与树的高度成比例,这个高度上的理论上限允许红黑树在最坏的情况都是高效的,而不同于普通的(二叉搜索树)

(三) 数据结构设计

  • 和一般的数据结构设计类似,我们用抽象数据类型表示红黑树的节点,使用指针保存节点之间的相互关系。
  • 作为红黑树的节点,其基本属性有:节点的颜色,左节点的指针,右节点的指针、父节点的指针、节点的值

如下图


红河书节点.png

为了方便红黑树关键算法的实现,还定义了一些简单的操作(都是内联函数)。

//红黑树节点
template<class T>
class rb_tree_node
{
    typedef rb_tree_node_color node_color;
    typedef rb_tree_node<T> node_type;
public:
    node_color color;//颜色
    node_type*parent;//父节点
    node_type*left;//左子节点
    node_type*right;//右子节点
    T value;//值
    rb_tree_node(T&v);//构造函数
    inline node_type*brother();//获取兄弟节点
    inline bool on_left();//自身是左子节点
    inline bool on_right();//自身是右子节点
    inline void set_left(node_type*node);//设置左子节点
    inline void set_right(node_type*node);//设置左子节点
};

为了表示红黑树节点的颜色,我们定义了一个简单的枚举类型。

//红黑树节点颜色
enum rb_tree_node_color
{
    red=false,
    black=true
};

有了节点,剩下的就是实现红黑树的构造、插入、搜索、删除等关键算法了。

//红黑树
template<class T>
class rb_tree
{
public:
    typedef rb_tree_node<T> node_type;
    rb_tree();
    ~rb_tree();
    void clear();
    void insert(T v);//添加节点
    bool insert_unique(T v);//添加唯一节点
    node_type* find(T v);//查询节点
    bool  remove(T v);//删除节点
    inline node_type* maximum();//最大值
    inline node_type* minimum();//最小值
    inline node_type* next(node_type*node);//下一个节点
    inline node_type* prev(node_type*node);//上一个节点
    void print();//输出
    int height();//高度
    unsigned count();//节点数
    bool validate();//验证
    unsigned get_rotate_times();//获取旋转次数
private:
    node_type*root;//树根
    unsigned rotate_times;//旋转的次数
    unsigned node_count;//节点数
    void __clear(node_type*sub_root);//清除函数
    void __insert(node_type*&sub_root,node_type*parent,node_type*node);//内部节点插入函数
    node_type* __find(node_type*sub_root,T v);//查询
    inline node_type* __maximum(node_type*sub_root);//最大值
    inline node_type* __minimum(node_type*sub_root);//最小值
    void __rebalance(node_type*node);//新插入节点调整平衡
    void __fix(node_type*node,node_type*parent,bool direct);//删除节点调整平衡
    void __rotate(node_type*node);//自动判断类型旋转
    void __rotate_left(node_type*node);//左旋转    
    void __rotate_right(node_type*node);//右旋转
    void __print(node_type*sub_root);//输出
    int  __height(node_type*&sub_root);//高度
    bool __validate(node_type*&sub_root,int& count);//验证红黑树的合法性
};

在红黑树类中,定义了** 树根(root) ** 和** 节点数 (count) ,其中还记录了红黑树插入、删除时执行的旋转次数 rotate_times。其中核心操作有 插入操作(insert) 搜索操作 (find) , ** 删除操作(remove) 递减操作(prev) ** ——寻找比当前节点较小的节点, 递增操作(next) ** ——寻找比当前节点比较大的节点, ** 最大值(maximum) ** 和 ** 最小值(minimum) ** 。** 其中验证操作(__ validate) ** 通过递归操作红黑树,验证红黑树的基本颜色约束,用于操纵红黑树验证红黑树是否保持平衡。

(四) 树的旋转知识

当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的性质。所以为了继续保持红黑树的性质,我们可以通过对节点进行重新着色,以及对树进行相关的旋转操作,即修改树中某些节点的颜色及指针结构,来达到对红黑树进行插入和删除结点等操作后,继续保持它的性质或平衡。

树的旋转,分为左旋和右旋,借助下图来做介绍:

1、左旋
左旋.png

如上图所示:

  • 当在某个结点pivot上,做左旋操作时,我们假设它的右孩子不是NIL,pivot可以为任何不是不是NIL的左孩子的结点。
  • 左旋以pivot到y之间的链为"支轴"进行,它使y成为该孩子树新的根,而y的左孩子b则成为pivot的右孩子。
2、右旋
右旋.png

对于树的旋转,能保持不变的只有原树和搜索性质,而原树的红黑性质则不能保持,在红黑树的数据插入和删除后可利用旋转和颜色重涂来恢复树的红黑性质。

这样大家对红黑树就有了初步了解。这里就不详细介绍了,如果大家有兴趣,可以自行去了解。

四、Linux的跨进程通信(IPC)概述

(一)、跨进程通信(IPC)的目的

跨进程通信(IPC)的目的主要如下:

  • 数据传递
    一个进程需要将它的数据发送给另外一个进程,发送的数据量在一个字节到几M字节之间
  • 共享数据
    多个进程想要操作共享数据。
  • 通知事件
    一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
  • 资源共享
    多个进程之间共享资源。为了做到这一点,需要内核提供锁和同步机制
  • 进程控制
    有些进程希望完全控制另一个进程的执行(如debug进程),此时控制进程希望能够拦截另一个进程的所有步骤和异常,并能够及时知道它的状态改变。

(二)、Linux 进程间通信(IPC)的发展

** Linux **下的跨进程通信手段基本上是从Unix平台上的进程通信手段继承而来。而对Unix发展做出大量贡献的量大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了"system V IPC",通信进程局限在单个计算机内;而后者则跳过了这个限制,形成了基于套接字(socket)的进程间通信机制。
** Linux **则把两者继承了下来。

所以可以把Linux中的进程间通信大体分为4类

  • 基于早期Unix的进程间通信:管道和信号
  • 基于System V的进程间通信:System V消息队列、System V 信号灯、System V 共享内存
  • 基于Socket 的进程间通信:socket
  • POSIX进程间通信:posix 消息队列、posix信号灯、posix共享内存

这里说下PSOIX:

由于Unix版本的多样性,电子电器工程协会(IEEE) 开发了一个独立的Unix标准,这个心的ANSI Unix标准被称为计算机环境的可移植性操作系统。现有的大部分Unix和流行版本都是遵循POSIX标准的,而Linux从一开始就是遵循POSIX标准。

三、Linux的跨进程通信详解

在Linux下进程通信有以下七种:

  • 1、匿名管道(pipe)
  • 2、命名管道(FIFO)
  • 3、信号(signal)
  • 4、信号量(semaphore)
  • 5、消息队列(message queue)
  • 6、共享内存(share memory)
  • 7、套接字(Socket)

那我们就来详细的了解下相关的内容

(一)、匿名管道(pipe)

1、什么是匿名管道?

匿名管道(pipe)是Linux支持的最初Unix IPC形式之一,具有以下特点:

  • 匿名管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立两个管道;
  • 只能作用于父子进程或者兄弟进程之间(具有亲缘关系的进程)
  • 单独构成的一种独立的文件系统:匿名管道对于管道两端的进程而言,就是一个文件,但它不是普通文件,它不属于某种文件系统,而是自理门户,单独构成一种文件系统,去并且只存在于内存中。
  • 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓存区的头部读出数据。
2、匿名管道的实现机制

匿名管道是右内核管理的一个缓冲区,相当于我们放入内存的中一个纸条。匿名管道的一端连接一个进程的输出。这个进程会向管道中放入信息。匿名管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓存区不需要很大,它被设计成为唤醒的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程就会等待,直到另一端的进程取出信息。两个进程都终结的时候,管道也会自动消失。如下图

image.png

从原理上,匿名管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。最开始的时候,上面的两个箭头都连接到同一个进程Process 1上(连接在Process 1上的两个箭头)。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭在自己不需要的一个连接(两个黑色的箭头被关闭;Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了上图的PIPE。
示例图如下:

image.png
3、匿名管道实现细节

在Linux中,匿名管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构,和VFS的索引节点inode。通过将两个file结构指向同一个临时的VFS节点,而这个VFS索引节点又指向了一个物理页面而实现的。如下图

管道实现细节.png
4、关于匿名管道的读写

匿名管道的实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即匿名管道pipe_read()读函数和匿名管道写函数pipe_write()。匿名管道写函数通过将字节复制到VFS索引节点指向物理内存而写入数据,而匿名管道读函数则通过复制物理内存而读出数据。当然,内核必须利用一定的 同步机制 对管道的访问,为此内核使用了 等待队列、和 信号

当写入进程向匿名管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的file结构。file结构中制定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查VFS索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:

  • 内存中有足够的空间可以容纳所有要写入的数据。
  • 内存没有被读程序锁定。

如果同时满足上述条件,写入函数首先会锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写进程就休眠在VFS索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接受到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。

匿名管道的读取过程和写入过程类型。但是,进程可以在没有数据或者内存被锁定时立即返回错误信息,而不是阻塞该进程,这一来于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页被释放。

PS:有些同学可能不清楚VFS,我这里就简单介绍下;

VFS(virtual File System/虚拟文件系统):是Linux文件系统对外的接口。任何要使用文件系统的程序都必须经由这层接口来使用它。它是采用标准的Unix系统调用读写位于不同物理介质上的不同文件系统。VFS是一个可以让open()、read()、write()等系统调用不用关系底层的存储介质和文件系统类型就可以工作的粘合层。在Linux中,VFS采用的是面向对象的编程方法。

(二)、命名管道(FIFO/named PIPE)

在上面,我们介绍了匿名管道(pipe),我们知道了如何匿名管道在进程之间传递数据,同时也是看到了这个方式的一个缺陷,就是这些就进程都是由一个共同的祖先进程启动,这给我们在不相关的进程之间交换数据带来了不方便。这里我将会介绍另一种通信方式——命名管道,来解决不相关进程之间的问题。

1、什么是命名管道?
  • 命名管道也被称为FIFO或者named pipe,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的匿名管道类似。
  • FIFO(First in, First out) 为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,因为管道本质上是一个** 先进先出的队列数据结构 ,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,命名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它是在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之小时。FIFO的好处在于我们可以通过 文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接 **。
2、命名管道的读写规则:
  • 1、从FIFO中读取数据的约定:如果一个进程为了从FIFO中读取数据而阻塞打开了FIFO,那么该进程内的读操作 为设置了阻塞标志的读操作。
  • 2、从FIFO中写入数据的约定:如果一个进程为了想FIFO中写入数据而阻塞打开了FIFO,那么该进程内的写操作 为设置了阻塞标志的写操作。
3、命名管道的安全问题:

大家想一下,只使用一个FIFO文件,如果有多个进程同时向同一个FIFO文件写数据,而只有一个读FIFO进程在同一个FIFO文件读取数据时,会发生怎么样的情况呢,会发生数据块的相互交错是很正常的?而且个人认为多个不同进程向一个FIFO读取进程发送数据是很正常的情况。

为了解决这个问题,就是让写操作原子化,怎么才能使写操作原子化呢?答案其实很简单:系统规定,在一个以O_WRONLY(即阻塞方式)打开的FIFO中,如果写入的数据长度小于等于PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。如果所有的写请求都是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据绝不会交错在一起。

(三)、信号(Signal)

1、什么是信号?

信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持Unix早期信号语义函数sigal外,还支持语义服务Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,有能够统一对外接口,用sigaction函数重新实现了signal函数)

2、信号的种类

如下图:


信号种类.png

每种信号类型都有对应的信号处理程序(也叫信号的操作),就好像每个中断都有一个中断服务例程一样。大多数信号的默认操作是结束接受信号的进程;然而一个进程通常可以请求系统采取某些代替的操作,各种代替操作是:

  • 忽略信号。随着这一选项的设置,进程将忽略信号的出现。有两个信号不可以被忽略:SIGKILL,它将结束进程:SIGSTOP,它是作业控制机制的一部分,将挂起作业的执行。
  • 恢复信号的默认操作
  • 执行一个预先安排的信号处理函数。进程可以登记特殊的信号处理函数。当进程收到信号时,信号处理函数将像中断服务例程一样被调用,当从信号处理函数返回时,控制被返回给主程序,并且继续正常执行。

但是,信号和中断有所不同。中断的响应和处理都发生在内核空间,而信号的响应发生在内核空间,信号处理程序的执行却发生在用户空间。
那么什么时候检测和响应信号?通常发生在两种情况下:

  • 当前进程由于系统调用、中断或异常而进入内核空间以后,从内核空间返回到用户空间前戏
  • 当前进程在内核进入睡眠以后刚被唤醒的时候,由于检测到信号的存在而提前返回到用户空间
3、信号的本质

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

4、信号来源

信号事件的发生有两个来源:硬件来源(比如我们按下键盘或者其他硬件故障);润健来源,最常用发送信号的系统函数是kill,raise,alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

5、关于信号处理机制的原理(内核角度)

内核给一个进程发送中断信号,是在进程所在的进程表项的信号域设置对应于该信号的位。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒正在睡眠的进程;否则仅设置进程表中信号域相应的位,而不是唤醒进程。这一点比较重要,因为进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程进入或离开一个适当的低调度优先级睡眠状态时。

内核处理一个进程吸收的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。

内核处理一个进程收到的软中断信号是在该进程的上下文中。因此,进程必须处于运行状态。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数出,从函数返回再弹出栈顶时,才返回原先进入内核的地方,接着原来的地方继续运行。这样做的原因是用户定义的处理函数不能切不允许在内核态下执行。

6、信号的生命周期
信号的生命周期.png

(四)、信号量(semaphore)

1、什么是信号量

信号量又称为信号灯,它用来协调不用进程间的数据对象,而最主要的应用是共享内存方式的进程间通信。本质上,信号量时一个计数器,他用来记录某个资源(如共享内存)的存取状况。信号量的使用,主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。

信号量的值为正的时候,说明它空闲。所有的线程可以锁定而使用它。若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。

2、信号量的注意事项
  • 为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要这一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能由一个执行线程访问代码的临界区域
  • 临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就说信号量临界区是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就说信号量是用来协调进程对共享资源的访问。
  • 信号量时一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P-信号变量)和发送(即V信号变量)信息操作。
  • 最简单的信号量只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量
3、信号量的原理

由于信号量只能进行两种操作即"等待"和"发送",即P(sv)和V(sv),他们的行为是这样:

  • P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,他将得到信号量,并可以进如临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会挂起以等待第一个进程离开临界区并执行(sv)释放信号。

3、信号量的分类

Linux提供两种信号量

  • 内核信号量:由内核控制路径使用
  • 用户态进程使用的信号量:这种信号量又分为POSIX信号量和SYSTEM V信号量

POSIX辛信号量又分为有名信号量和无名信号量

  • 有名信号量:其值保存在文件中,所以它可以用于线程也可以用于进程间同步
  • 无名信号量:其值保存在内存。

POSIX信号量和SYSTEM V信号量的比较

  • 对POSIX来说,信号量是个非负数。常用于线程间同步
    而SYSTEM V信号量则是一个或者多个信号量集合,他对应的是一个信号量的结构体,这个结构体为SYSTEM V IPC服务的,信号量只不过是它的一部分。常用语进程间同步。
  • POSIX信号量的引用头文件是<semaphore,h>,而SYSTEM V信号量的引用头文件是<sys/sem.h>
  • 从使用的角度,System V信号量是简单的。比如,POSIX信号量的创建和初始化PV操作就很方便。

(五)、消息队列(message)

1、消息队列也称为报文队列:

消息队列也成为报文队列,消息队列是随内核持续的,只有在内核重其或者显示删除一个消息队列时,该消息队列才会真正删除系统中记录消息队列的数据结构体 struct ipc_ids_msg_ids位于内核中,系统中所有消息队列都可以在结构msg_ids中找到访问入口。

2、消息队列的原理及注意事项:
  • 消息队列其实就是一个消息的链表,每个消息都有一个队列头,称为struct_msg_queue,这个队列头描述了消息队列的key值,用户ID,组ID等信息,但它存于内核中而结构体struct msqid_ds能够返回或设置消息队列的信息,这个结构体位于用户空间中,与msg_queue结构相似的消息队列允许一个或多个进程向它写入或读取消息,消息队列是消息的链表。
  • 消息是按消息类型访问,进程必须指定消息类型来读取消息,同样,当向消息队列中写入消息事业必须给出消息的类型,如果读队列使用消息的类型为0,则读取队列中的第一条消息。
  • 内核空间的结构体msg_queue描述了对应key值消息队列的情况,而对应用户空间的msqid_ds这个结构体,因此,可以操作msgid_ds这个结构体来操作消息队列。

(六)、共享内存(share memory)

共享内存是进程间通信中最简单的方式之一。

1、什么是共享内存?

共享内存是系统处于多个进程之间通讯的考虑,而预留的一块内存区。共享内存允许两个或更多的进程访问同一块内存,就如同malloc()函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其他进程都会觉察到这个更改。

2、关于共享内存
  • 当一个程序加载进内存后,它就被分成叫做页的块。通信将存在内存的两个页之间或者两个独立的进程之间。总之,当一个程序想和另外一个程序通信的时候,那内存将会为这两个程序生成一块公共的内存区域。这块被两个进程分享的内存区域叫做共享内存。
  • 由于所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其他需要切入内核的过程来完成。同时它也也避免了对数据的跟中不必要的复制。
  • 如果没有共享内存的概念,那一个进程不能存取另外一个进程的内存部分,因而导致共享数据或者通信失效。因为系统内核没有对访问共享内存进行同步,开发者必须提供自己的同步措施。
  • 解决了这些问题的常用方法是是通过信号量进行同步。不过通常我们程序只有一个进程访问了共享内存,因此在集中展示了共享内存机制的同时,我们避免了让代码被同步逻辑搞的混乱不堪。

为了简化共享数据的完整性和避免同时存取数据,内核提供了一种专门存取共享内存资源的机制。这称为互斥体或者Mutex对象。

3、Mutex对象

例如,在数据被写入前不允许进程从共享内存中读取信息、不允许两个进程同时向一个共享内存地址写入数据等。

当一个基础想和两一个进程通信的时候,它将按以下顺序运行:

  • 1、获取Mutex对象,锁定共享区域
  • 2、将要通信的数据写入共享区域
  • 3、释放Mutex对象

当一个进程从这个区域读取数据的时候,它将重复同样的步骤,只是将第二步变成读取。

4、内存模型

要使用一块共享内存

  • 进程必须首先分配它
  • 随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中。
  • 当完成通信之后,所有进程都脱离共享内存,并且由一个进程释放该共享内存块。

在/proc/sys/kernel/目录下,记录着共享内存的一些限制,如一个共享内存区的最大字节数shmmax,系统范围内最大的共享内存区标志符数shmmni等。

5、Linux系统内存模型
  • 在Linux系统中,每个进程的虚拟内存是被分为许多页面的。这些内存页面中包含了实际的数据。每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个页面页面映射到自己的地址空间,从而达到共享内存的目的。
  • 分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标示该内存块的标识符。
  • 一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。
  • 这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。
  • 当再也没有京城需要使用这个共享内存块的时候,必须有一个(有且只有一个)进程负责释放这个被共享的内存页面。
  • 所有共享内存块的大小必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包含的字节数。在Linux系统中,内存页面大小是4KB,不过您仍然应高通过调用getPageSize获取这个值。
6、Linux共享内存的实现步骤

共享内存的实现分为两个步骤:

  • 创建共享内存,使用shmget函数
  • 映射共享内存,将这段创建的共享内存映射到具体的进程空间中,使用shmat函数

(七)、套接字(socket)

套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是右Unix系统的BSD分之开发出来的,但是现在一般可以移植其他类Unix系统上。比如Linux和System V的变种都支持套接字。

(八) Linux的几种跨进程通信的方式的比较

1、效率比较
类型 无连接 可靠 流控制 优先级
匿名PIPE N Y Y N
命名PIPE(FIFO) N Y Y N
信号量 N Y Y Y
消息队列 N Y Y Y
共享内存 N Y Y Y
UNIX流SOCKET N Y Y N
UNIX数据包SOCKET Y Y N N

PS:无连接是指无需调用某种行动是OPEN,就有发送消息的能力流控制,如果系统资源短缺或者不能接受更多的消息,则发送进程能进进行流量控制。

2、优缺点比较
  • 匿名管道(pipe):速度慢,容量有限,只有父子进程能通讯
  • 有名管道(FIFO): 任务进程都能通讯,但速度慢
  • 消息队列(message queue):容量收到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据问题。
  • 信号量:不能传递复杂消息,只能用来同步
  • 共享内存区:能够容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题。相当于线程中的线程安全,当然,共享内存区同样可以做线程间通讯,不过没有这个必要,线程间本来就已经共享了同一进程内的一块内存
3、使用场景
  • 如果用户传递的信息较少或是需要通过信号来出发某些行为,上面提到的软中断限号机制不失为一种简洁有效的一种进程间通信方式。但若是进程间要求传递的信息量比较大或者进程间存在交换数据的要求,那就需要考虑别的通信方式。
  • 匿名管道简单方便,但局限于单向通信的工作方式,并且只能创建它的进程及其子孙进程之间实现管道的共享。
  • 有名管道虽然可以提供给任意关系的进程使用,但是由于其长期存在于系统之中,使用不当容易出错。所以不建议初级开发者使用。
  • 消息缓存可以不再局限于父子进程,而允许任意进程间通过共享消息队列来实现进程间通信,并由系统调用函数来实现消息发送和接受方之间的同步,从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题,使用方便,但是信息的复制需要额外的消耗CPU的时间,不适宜信息量大或操作频繁的场合。
  • 共享内存针对消息缓存的缺点而改进,利用了内存缓存区直接交换信息,无需复制,快捷、信息量大的是其优点。但是共享内存的通信方式是通过将共享内存缓存直接附加到进程的虚拟地址空间中来实现的。因此这些进程之间的读写操作的同步问题操作系统无法实现。必须由各进程利用其它同步工具解决。另外, 由于内存实体存在于计算机系统中,所以只能由处于同一个计算机系统中的其它进程共享,不方便网络通信。

补充一点:
共享内存块提供了在任意数量的进程之间进行高效双向通信的机制。每个使用者都可以读取写入数据,但是所有程序之间必须达成并遵守一定的协议,以防止诸如在读取信息之前覆写内存空间等竞争状态的出现。不行的是,Linux无法严格保证提供对共享内存块的独占访问,同时,多个使用共享内存块的进程之间必须协调使用同一个键值。

推荐阅读更多精彩内容