进程间通信

Android系统是基于Linux内核的,而Linux内核继承和兼容了丰富的Unix系统进程间通信(IPC)机制。而Android并不满足于传统的系统进程间通信,使用了Binder机制作为进程间通信的主要方式。本文主要是介绍传统的系统间通信方式与Binder机制并进行对比。

1、管道

管道工作图(https://blog.csdn.net/xiaohui987987/article/details/79957291)

image.png

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

局限性:1、只能用于父子进程或者兄弟进程之间。2、管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小)3、管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等

有名管道
管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间)

//这里的应用场景主要是指android日常开发
应用场景:用于shell,管道可用于输入输出重定向,通过'│'键,将一个命令的输出直接定向到另一个命令的输入.比如android中用来查看当前的activity的命令语句 adb shell dumpsys activity | grep Resume 就能过滤出Resume相关的从而看出系统当前的resumeActivity.

2、信号

信号是在软件层次上对底层的硬件异常机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了

信号流程:发送进程发送信号,接收进程可以忽略信号,或者是执行缺省操作,或者是由进程定义信号处理函数,当信号发生时,执行相应的处理函数。
工作图(《深入理解计算机操作系统》第八章第五节):


image.png

应用场景:在shell中通过kill命令发送信号给进程。比如kill -9(SIGKILL) [pid] 发送信号来杀死进程

3、信号量

信号量主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。信号可以设置值为1实现类似互斥锁的功能,也可以设置为其他非负值进行计数。

工作流程:
进程使用semget系统调用创建信号量(传入一个标识符来确定信号量)在内核创建相应的数据结构并返回一个信号量Id。调用semop系统调用操作信号量(传入信号量Id指明操作的信号量,传入一个值正数加到信号量的值对应释放资源,如果是负数对应与请求资源,当负数的绝对值大于信号量的值时进程将被阻塞直到信号量的值恢复到运行操作进行的程度为止)。其中对信号量值的并发操作由内核的一个信号量来互斥包含。

应用场景:

4、消息队列

消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。发送进程往消息队列发送消息(带编号和正文),接收线程监听消息队列(可以接受指定编号的消息,且多个进程监听只能由一个进程接收)。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

工作流程图(深入理解Linux内核架构第五章第三节):


image.png

应用场景:

5、共享内存

通过映射文件实现共享内存,一个进程对共享内存进行写操作对于另一个进程是可见的。相比管道,消息队列等减少了从用户空间复制数据到内核,再从内核复制数据到用户空间的两次消耗。

应用场景:在android的Ashmem(匿名共享内存)就是在linux的共享内存之上进行封装。Ashmem通过对内存区域进行锁定,解锁并且向内存管理系统注册了一个内存回收算法函数,在内存紧张时回收解锁的内存从而辅助内存管理系统来有效地管理不再使用的内存块。另外共享内存的前提是需要打开同一个文件,通过binder 传输Ashmem的文件描述符(文件描述符只是在本进程内有效,也就是说,在不同的进程中,相同的文件描述符的值,代表的可能是不同的打开文件。binder在传输过程检测到文件描述符时根据文件描述符打开文件结构,然后从目标进程查找可用的文件描述符,将目标进程,目标进程提供的文件描述符和文件结构关联起来,最后将目标进程提供的文件描述符填入数据包替换)。
老罗的Android之旅 的Ashmem系列:https://blog.csdn.net/Luoshengyang/article/details/6651971

6、Socket

主要用在跨网络的进程间通信和本机上进程间的低速通信
socket在创建时由三个要素决定,网域(AF_INET表示互联网类型socket,AF_UNIX用来本机的进程间通信),类型(可以选择 SOCK_DGRAM 或 SOCK_STREAM。SOCK_STREAM 意味着会提供按顺序的、可靠、双向、面向连接的比特流。SOCK_DGRAM 意味着会提供定长的、不可靠、无连接的通信。在AF_UNIX域上因为不涉及网络介质不存在可靠性的问题,区别更多的是连接的建立以及发送读取上语义的区别。如果使用SOCK_STREAM发送前没建立连接会出错),规程(网域和类型结合大体决定了规程,如果网域AF_INET且类型为SOCK_DGRAM 则会使用UDP,如果有些情况需要别的选择就用来进一步明确,否则默认为0就够了)
socket结合android 进程fork
Unix domain socket :https://www.cnblogs.com/sparkdev/p/8359028.html
《Linux内核源代码情景分析》第七章

UNIX域套接字和有名管道相比优点
1、可以将用于两个以上的进程通信(例如,服务器进程与潜在的多个客户端进程连接);
2、它们是双向的;
3、它们支持在进程之间传递内核验证的UID / GID凭据;
4、它们支持在进程之间传递文件描述符;
5、它们支持包和排序包模式。
https://stackoverflow.com/questions/9475442/unix-domain-socket-vs-named-pipes

应用场景:android中app的进程创建通过SystemServer所在进程向Zygote请求fork进程。SystemServer与Zygote进行通信使用的就是unix-domain-socket。Zygote的socket服务端实现在ZygoteServer的runSelectLoop方法,Zygote通过poll监听socket描述符可读,判断客户端建立了一个新连接,调用accept获取已连接描述符。然后再调用poll监听已连接描述符是否可读。阻塞等到可读时再去获取从SystemServer写入的参数。SystemServer的socket客户端在ZygoteProcess中调用openZygoteSocketIfNeeded最后调用ZygoteState.connect来创建一个socket连接,最后在zygoteSendArgsAndGetResult中把参数(比如uid,gid,targetSdkVersion)通过socket的输出端输出。

这个过程可以参考《UNIX网络编程卷1第三版》第六章第十一节的例子。
//使用poll的好处是可以等待多个描述符就绪,比如当单进程的socket服务器(上面书中4.6的例子)调用read阻塞在io上等待客户端的写入,极端情况下客户端一直不写入,服务器因为单进程无法响应其他连接。而poll的话在于可以等到可读才去调用read,在此期间可以响应其他客户端连接。

image.png

image.png

7、Binder:

效率:从前面传统linux的进程间通信方式可以看到,因为虚拟地址的原因进程间是无法直接共享数据要么是通过映射文件实现共享内存,要么是借助内核先将数据拷贝到内核,再拷贝到目的进程,经历了两次的数据拷贝。Binder通过内核模块的形式运行在内核空间,Binder驱动通过mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用当驱动将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,从而实现了数据拷贝只需要一次。而共享内存虽然无需拷贝,但控制复杂,难以使用。
安全性:传统linux的进程间通信方式没有任何安全措施,完全依赖上层协议来确保,而接收方无法获得对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别身份,如果由用户手动填入在数据包里填入UID/PID,容易被伪造,从而被恶意程序利用;而Binder机制为发送方添加UID/PID身份,从本身就支持对通信双方做身份校检,因而大大提升了安全性。

工作图:
Binder框架定义了四个角色:Server,Client,ServiceManager,以及Binder驱动。Server提供服务向ServiceManager注册,以便Client向ServiceManager查询获取对应的Server调用。Binder也可以通过已经建立的Binder连接传递Binder。Binder驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,数据包在进程之间的传递和交互等等。


image.png

使用场景:1、android中Binder无处不在,应用使用四大组件往往需要与系统中的ActivityManagerService,WindowManagerService等核心服务进行通信,通信过程使用的便是Binder机制。
2、AIDL,android中应用一般使用aidl来实现进程间通信,而aidl实质也是通过Binder机制来完成进程间的通信。

AIDL过程的浅析
使用AIDL步骤
(1)、创建aidl文件,这里例子接口为IBusiness,方法为handleBusiness,然后在项目的gen目录下会生成一个IBusiness.java文件
(2)服务端:新建类BusinessService继承IBusiness.Stub作为AIDL的服务端,并重写handleBusiness方法实现业务,在组件Service的onBind方法中返回一个BusinessService的实例。
(3)客户端:调用bindService,传入ServiceConnection作为连接后的回调,重写onServiceConnected对传来的IBinder执行IBusiness.Stub.asInterface方法获得实现了IBusiness的实例对象(如果客户端和服务端在同一个进程,那么返回的是BusinessService即IBusiness.Stub,如果是跨进程那么返回的是IBusiness.Stub.Proxy)

IBinder接口表明跨进程通信的能力。Binder类作为Binder本地对象,BinderProxy表示跨进程的Binder代理对象。queryLocalInterface根据返回是否非null判断是否Binder本地对象。transact方法最终会进入Binder驱动,由Binder驱动完成通信(数据包的拷贝,根据进程对传入的binder实体或binder引用进行转换,调用线程的挂起,唤醒目的进程),最终会调用Binder本地对象的onTransact响应代理的请求。

IInterface接口表明Binder本地对象提供的能力。我们在AIDL定义的接口继承IInterface。

Proxy实现我们在AIDL定义的接口IBusiness,但它并没有真正实现业务功能,只是代理实际请求Binder本地对象来完成功能。通过持有BinderProxy完成代理请求。

Stub继承Binder表明是Binder本地对象,实现接口IBusiness。重写onTransact根据调用号识别BinderProxy请求的方法,获取数据包的参数。最后调用IBusiness的方法(抽象方法)留给子类实现真正的业务功能

aidl 类图 (1).png

参考:
Linux环境进程间通信 IBM社区M:https://www.ibm.com/search?q=Linux%E7%8E%AF%E5%A2%83%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1&lnk=mhsrch&en=utf&lang=zh&cc=cn&nr=20&tabType%5B0%5D=For+developers
Linux进程间通信的几种方式总结--linux内核剖析:https://blog.csdn.net/gatieme/article/details/50908749
技术内幕:Android对Linux内核的增强 Ashmem: http://www.jmpcrash.com/?p=315

Binder学习指南:http://weishu.me/2016/01/12/binder-index-for-newer/
Android进程间通信(IPC)机制Binder简要介绍和学习计划:https://blog.csdn.net/luoshengyang/article/details/6618363
Android Binder设计与实现 - 设计篇:https://blog.csdn.net/universus/article/details/6211589

你可能感兴趣的:(进程间通信)