android中AMS通知Zygote去fork进程为什么使用socket而不使用binder?

前言:

之前写过一篇文章

APP启动流程(android12源码)

中介绍到,AMS通知Zygote去fork进程的时候,使用的是socket的方式,而不是binder。我们都知道,安卓中默认跨进程的方式是binder,而为什么这里偏偏使用Socket呢?

网上说法:

目前网上的说明众说纷纭,甚至有的都不能自圆其说。总结一下,主要有以下几大类:

1.锁的问题

2.启动顺序问题

3.安全问题(这个就有点离谱了,socket不会比binder更安全)

我的理解:

按照我个人的理解,主要有以下五个原因:

1.先后时序问题:

首先,先看下安卓系统启动顺序:

android中AMS通知Zygote去fork进程为什么使用socket而不使用binder?_第1张图片

从图中可以知道,binder驱动是早于init进程加载的。而init进程是安卓系统启动的第一个进程。

那么,为什么有时序问题呢?

安卓中一般使用的binder引用,都是保存在ServiceManager进程中的,而如果想从ServiceManager中获取到对应的binder引用,前提是需要注册,而注册的行为是在对应的逻辑代码执行时才会去注册的。

流程上,是Init产生Zygote进程和ServiceManager进程,然后Zygote进程产生SystemServer进程。如果AMS想通过binder向Zygote发送信号,必须向ServiceManager获取Zygote的binder引用,而前提是Zygote进程中必须提前注册好才行。

而实际上,Init进程是先创建ServiceManager,后创建Zygote进程的。虽然Zygote更晚创建,但是并不能保证Zygote进程去注册binder的时候,ServiceManager已经初始化好了,因为两者属于两个进程,是并行的。

PS:我们可以通过进程ID来确定哪个进程更优先创建,如下,ServiceManager会更早创建。

system         311     1   11612   3204 binder_ioctl_write_read 0 S servicemanager
root           380     1 4014532 164032 poll_schedule_timeout 0 S zygote64
root           381     1 1457084 145624 poll_schedule_timeout 0 S zygote

当然有人说,等ServiceManager完全初始化好再去注册不就好了吗?这当然可以了,但是什么时候才能可以注册呢?这个时间点无法保证,如果想保证就必须通过另外一种跨进程通讯的方式来保证,这样设计不就变得复杂了吗?

还有人说,我Zygote启动后,延时10秒或者20秒再去向ServiceManager进程注册不就好了吗?这样做自然也是可以的,但是有没有考虑下,如果用户恰好在这期间点击了应用图标尝试去启动APP应该怎么办呢?岂不就是没有反应了吗?

所以,AMS无法获取到Zygote的binder引用,是原因之一。

2.多线程问题

Linux中,fork进程其实并不是完美的fork,linux设计之初只考虑到了主线程的fork,也就是说如果主进程中存在子线程,那么fork进程中,其子线程的锁状态,挂起状态等等都是不可恢复的,只有主进程的才可以恢复。

而binder作为典型的CS模式,其在Server是通过线程来实现的,Server等待请求状态时,必然是处于一种挂起的状态。所以如果使用binder机制,zygote进程fork子进程后,子进程的binder的Server永远处于一种挂起不可恢复的状态,这样的设计无疑是非常差的。

所以,zygote如果使用binder,会导致子进程中binder线程的挂起和锁状态不可恢复,这是原因之二。

3.效率问题

我们都知道,Binder基于mmap机制,只会进行一次拷贝,所以效率是很高的。那么Socket就一定低吗?

其实答案也许会出乎你的意料。如果这个问题你问GPT,GPT会告诉你,localsocket的效率是高于binder的。其原因,是虽然binder只有一次拷贝,比socket的两次更少,但是拷贝次数只是一个很重要的原因,但并不是所有影响的因素。binder因为涉及到安全验证等等环节,所以实际上效率反而没有localsocket高。据说腾讯小程序跨进程通讯使用的就是LocalSocket的方式。

所以,LocalSocket效率其实并不低,这是原因之三。

4.安全问题

普遍认为Socket是不安全的,因为它缺乏PID校验,所以导致任何进程都可以访问。但是LocalSocket也是不安全的吗?我们就实际去操作一下。

代码如下,我们尝试直接给zygote的进程发送socket消息,看zygote进程是否可以接受。如果可以接受的话,那么我们只要发送指定格式的字符串,APP也能启动其它应用进程了。相关代码是直接拷贝AMS中的。

 val localSocket = LocalSocket()
 val localSocketAddress =
 LocalSocketAddress("zygote", LocalSocketAddress.Namespace.RESERVED)
 localSocket.connect(localSocketAddress)
 val outputStream = localSocket.outputStream
 outputStream.write(1)
 outputStream.flush()

实验下来果然不出所料,实验结果如下。所以LocalSocket也是有权限验证的。android中AMS通知Zygote去fork进程为什么使用socket而不使用binder?_第2张图片

 connectLocal在native层的实现是socket_connect_local方法。

socket_connect_local(JNIEnv *env, jobject object,
                        jobject fileDescriptor, jstring name, jint namespaceId)
{
    int ret;
    int fd;

    if (name == NULL) {
        jniThrowNullPointerException(env, NULL);
        return;
    }

    fd = jniGetFDFromFileDescriptor(env, fileDescriptor);

    if (env->ExceptionCheck()) {
        return;
    }

    ScopedUtfChars nameUtf8(env, name);

    ret = socket_local_client_connect(
                fd,
                nameUtf8.c_str(),
                namespaceId,
                SOCK_STREAM);

    if (ret < 0) {
        jniThrowIOException(env, errno);
        return;
    }
}

所以最终的校验逻辑应该在Linux层的socket_local_client_connect方法,我这里没有Linux的源码,所以就不继续往下研究的。如果有知晓这方面知识的大佬,麻烦告之我一下。

所以,LocalSocket其实也有权限校验,并不意味着可以被所有进程随意调用,这是原因之四。

5.Binder拷贝问题

进程的fork,是拷贝一个和原进程一摸一样的进程,其中的各种内存对象自然也会被拷贝。所以用来接收消息去fork进程的binder对象自然也会被拷贝。但是这个拷贝对于APP层有用吗?那自然是没用的,所以就凭白多占用了一块无用的内存区域。

说到这你自然想问,如果通过socket的方式,不也平白无故的多占用一块Socket内存区域吗?是的,确实是,但是fork出APP进程之后,APP进程会去主动的关闭掉这个socket,从而释放这块区域。相关代码在ZygoteConnection的processCommand方法中:

try {
            if (pid == 0) {
            // in child
            zygoteServer.setForkChild();

            zygoteServer.closeServerSocket();
            IoUtils.closeQuietly(serverPipeFd);
            serverPipeFd = null;

            return handleChildProc(parsedArgs, childPipeFd,
                                    parsedArgs.mStartChildZygote);
            } else {
               // In the parent. A pid < 0 indicates a failure and will be handled in
               // handleParentProc.
               IoUtils.closeQuietly(childPipeFd);
               childPipeFd = null;
               handleParentProc(pid, serverPipeFd);
               return null;
            }
    } 

说到这肯定有人想问,那我释放掉Binder内存不就一样了吗?Binder的特殊性在于其是成对存在的,其分为Client端对象和Server端对象。假设我们使用binder,那么因为APP端的binder是拷贝自Zygote进程的,所以如果要释放掉APP的Server端binder引用对象,就必须释放掉AMS中的Client端binder对象,那这样就会导致AMS失去binder从而无法正常向Zygote发送消息。

所以,使用binder会造成额外的内存占用,这是原因之五。

总结:

综上所述,如果选择binder的话,会有各种各样的问题。

如果说非要使用binder可以吗?我认为是可以的,但是这样会让设计变得更复杂,而且出问题的可能性会更高。

所以,回答本文题目中的问题:为什么安卓选择使用socket而不是Binder?不是说使用binder绝对不行,而是通过各个层面的综合衡量,socket会比binder更为合适,所以安卓最终选择了socket的方式。架构设计,不正式寻求一种简单且安全的体系嘛。

声明:

由于写文本的时候,查询的文章过多,而且有的还是转载的,所以实在无法一一列举,所以这里说一声抱歉,有相关问题可以联系我,我会补上相关资料的原始链接来源。

本文使用到的相关资料来源:

@享学课堂Alvin老师

本文是结合网上的文章,以及作者自身的源码阅读和实际的实验得出来的结论,不能代表google官方的说法,相关答案仅供参考。

如果有想讨论的点,欢迎评论留言。

你可能感兴趣的:(#,安卓-源码分析,安卓,android)