前言
因为之前淋过了雨,就想为后面来的朋友撑把伞。
有不少人总因为网上的面试题没有详解感到烦恼,所以我这整理了一系列的针对性的面试题详解,希望可以帮到大家。
Android中多进程通信的方式有哪些?
这道题想考察什么?
对计算机中进程的了解,为了完成跨进程通信需要解决的问题以及现有的跨进程通信方式大致实现原理与区别
考察的知识点
操作系统内存、进程与通信
考生应该如何回答
总的来说,进程间通信方案有很多他们分别是:管道,信号,信号量,内存共享,socket,binder,消息队列,但是使用最多的还是binder,尤其是用户空间的跨进程通信,基本大多采用的是binder。
进程隔离
操作系统有虚拟内存与物理内存的概念。物理内存指通过物理内存条而获得的内存空间,而虚拟内存则是计算机系统内存管理的一种技术,虚拟内存并非真正的内存,而是通过虚拟映射的手段让每个应用进程认为它拥有连续的可用的内存。在使用了虚拟存储器的情况下, 通过MMU(负责处理CPU的内存管理的计算单元)完成虚拟地址到物理地址的转换。
程序使用的虚拟内存被操作系统划分成两块:用户空间和内核空间。用户空间是用户程序代码运行的地方,内核空间是内核代码运行的地方,内核空间由所有进程共享。为了安全,内核空间与用户空间是隔离的,这样即使用户的程序崩溃了,内核也不受影响。同样为了安全,不同进程的各自的用户空间也是隔离的,这样就避免了进程间相互操作数据的现象发生,从而引起各自的安全问题。不同进程基于各自的虚拟地址不同,从逻辑上来实现彼此间的隔离。
IPC通信
为了能使不同的进程互相访问资源并进行协调工作,需要在不同进程之间完成通信。而通过进程隔离可知不同进程之间无法直接完成通信的工作。此时就需要特殊的方式来实现:IPC(Inter-Process Communication )即进程间通信。不同进程存在进程隔离,但是内核空间被所有进程共享,因此绝大多数的IPC机制就利用这个特点来实现通信的需求。因为Android是在Linux内核基础之上运行,因此Linux中存在的IPC机制在Android中基本都能使用。
管道
管道是UNIX中最古老的进程间通信形式,它实际上是由内核管理的一个固定大小的缓冲区。管道的一端连接一个进程的输出,这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。
可以通过pipe创建一个管道:
//匿名管道(PIPE)
#include <unistd.h>
int pipe (int fd[2]); //创建pipe
ssize_t write(int fd, const void *buf, size_t count); //写数据
ssize_t read(int fd, void *buf, size_t count); //读数据
pipe创建的是匿名管道,它存在以下限制:
1、大小限制(一般为4k)
2、半双工(同一个时刻只数据只能向一个方向流动,需要双方通信时,需要建立两个管道 )
3、只支持父子和兄弟进程之间的通信
另外还有FIFO实名管道,支持双向的数据通信,建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,但是要同时和多个进程通信它就力不从心了。需要了解更多关于管道机制的内容可以在腾讯课堂搜索享学课堂。
信号
信号主要用于用于通知接收进程某个事件的发生。信号的原理是在软件层次上对中断机制的一种模拟,一个进程收到一个信号与处理器收到一个中断请求是一样的。
信号是一种异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。也就是说信号接收函数不需要一直阻塞等待信号的到达。 进程可以通过sigaction
注册接收的信号,就会执行响应函数,如果没有地方注册这个信号,该信号就会被忽略。
int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);
//根据参数signum指定的信号编号来设置该信号的处理函数。
//参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。
在Android中,如果程序出现ANR问题会发出:SIGNALQUIT 信号,应用程序可注册此信号的响应实现监听ANR,爱奇艺xCrash,友盟+ U-APM、腾讯Matrix都实现了该方式。可以在腾讯课堂搜索享学课堂了解更多ANR监控相关内容。
信号量
信号量实际上可以看成是一个计数器,用来控制多个进程对共享资源的访问。它不以传送数据为主要目的,主要作为进程间以及同一进程内不同线程之间的同步手段。
信号量会有初值(>0),每当有进程申请使用信号量,通过一个P操作来对信号量进行-1操作,当计数器减到0的时候就说明没有资源了,其他进程要想访问就必须等待,当该进程执行完这段工作(我们称之为临界区)之后,就会执行V操作来对信号量进行+1操作。
共享内存
通过进程隔离可知,不同进程基于各自的虚拟地址不同,从逻辑上来实现彼此间的隔离。 而共享内存则是让不同进程可以将同一段物理内存连接到他们自己的地址空间中,完成连接的进程都可以访问这块共享内存中的数据。
由于多个进程共享同一块内存区域,所以通常需要用其他的机制来同步对共享内存的访问,如信号量 。而在Android中提供了独特的匿名共享内存Ashmem(Anonymous Shared Memory)。Android的匿名共享内存基于 Linux的共享内存,都是在临时文件系统上创建虚拟文件,再映射到不同的进程。 它可以让多个进程操作同一块内存区域,并且除了物理内存限制,没有其他大小限制。相对于 Linux 的共享内存,Ashmem 对内存的管理更加精细化,并且添加了互斥锁。
在开发中,可以借助Java中的 MemoryFile 使用匿名共享内存,它封装了 native 代码。 Java 层使用匿名共享内存的步骤一般为:
1. 通过 MemoryFile 开辟内存空间,获得ParcelFileDescriptor;
MemoryFile memoryFile = new MemoryFile("test", 1024);
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
FileDescriptor des = (FileDescriptor) method.invoke(memoryFile);
ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(des);
- 将 ParcelFileDescriptor 传递给其他进程;
- A进程往共享内存写入数据;
memoryFile.getOutputStream().write(new byte[]{1, 2, 3, 4, 5});
- B进程从共享内存读取数据。
ParcelFileDescriptor parcelFileDescriptor;
FileDescriptor descriptor = parcelFileDescriptor.getFileDescriptor();
FileInputStream fileInputStream = new FileInputStream(descriptor);
fileInputStream.read(content);
在第二步中一般的利用Binder机制进行FD的传输,传输完成后,就可以直接借助FD完成跨进程数据通信,而且没有内存大小的限制。因此当需要跨进程进行大数据的传递时,可以借助匿名共享内存完成!
在Android中视图数据与SurfaceFlinger的通信、腾讯MMKV、Facebook Fresco等等技术都有利用到匿名共享内存。
消息队列
消息队列是一个消息的链表,存放在内核中并由消息队列标识符标识。它克服了Linux早期IPC机制的很多缺点,比如消息队列具有异步能力,又克服了具有同样能力的信号承载信息量少的问题;具有数据传输能力,又克服了管道只能承载无格式字节流以及缓冲区大小受限的问题。
但是缺点是比信号和管道都要更加重量,在内核中会使用更多内存,并且消息队列能传输的数据也有限制,一般上限为两页 16kb。
int msgget(key_t, key, int msgflg); //创建和访问消息队列
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg); //发送消息
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg); //获取消息
受限于性能,数据量等问题的限制,Android系统没有直接使用Linux消息队列来进行IPC的场景,但是有大量的场景都利用了消息队列的特性来设计通信方案,比如进行线程间通信的Handler,就是一个消息队列。
socket
socket 原本是为网络通讯设计的,但后来在 socket 的框架上发展出一种 IPC 机制,就是 UNIX domain socket。虽然网络 socket 也可用于同一台主机的进程间通讯(通过 loopback 地址 127.0.0.1),但是 UNIX domain socket 用于 IPC 更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。 在Android系统中,Zygote进程就是通过LocalSocket(UNIX domain socket)接收启动应用进程的通知。
socket(AF_INET, SOCK_STREAM, 0); // 对应java Socket
socket(AF_UNIX, SOCK_STREAM, 0);// 对应java LocalSocket
Binder
Android Binder源于Palm的OpenBinder,在Android中Binder更多用在system_server进程与上层App层的IPC交互。 在Android中Intent、ContentProvider、Messager、Broadcast、AIDL等等都是基于Binder机制完成的跨进程通信。
总结
Android是在Linux内核基础之上运行,因此Linux中存在的IPC机制在Android中基本都能使用,如:
- 管道:在创建时分配一个page大小的内存,缓存区大小比较有限;
- 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;
- 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
- 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;
- 套接字:作为更通用的接口,传输效率低;
为什么Android选择Binder作为应用程序中主要的IPC机制?
Binder基于C/S架构,进行跨进程通信数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能虽然比管道等方式好,但是不如共享内存。
但是传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,只能由使用者在传递的数据包里填入UID/PID,伪造身份非常简单;而Binder不同,可靠的身份标记只有由IPC机制本身在内核中添加,binder就是这么做的,不由用户应用程序控制,直接在内核向数据中添加了进程身份标记。
因此综合考虑,Binder更加适合system_server进程与上层App层的IPC交互。