Android 大话binder通信

news2024/12/26 21:21:36

戳蓝字“牛晓伟”关注我哦!

用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章

由于 Android 大话binder通信(上)Android 大话binder通信(下) 分为两篇阅读体验不好,顾合并为一篇。

本文摘要

用故事的方式把binder通信的整个过程都描述出来,binder通信都经历了哪些节点,在这些节点上的数据有哪些变化,同时还对binder通信的关键细节进行介绍。通过本文您能对binder通信整体和细节都有一个全面的认识,比如startActivity方法到底都经历了哪些过程。(文章基于Android13代码分析)

下面是我以前写的binder通信的几篇文章,欢迎大家取阅:

通熟易懂分析binder:1.binder准备工作

通熟易懂的分析binder–2. binder进程通信协议及“记录链路”结构体

通熟易懂的分析binder–3. 探究binder全流程通信之请求篇

通熟易懂的分析binder–3. 探究binder全流程通信之回复篇

通熟易懂的分析binder–4.ServiceManager

由于篇幅的原因,故分为上下两篇。故事要从两个主人公白富美高富帅讲起。

白富美

我是白富美,性别女,我是Android系统里的一个进程,我有很多的追求者,它们不断的通过socket发送情书给我,而我却因此而很烦恼,因为其中很多的追求者我对他们都不感兴趣,但是socket通信又没有一个很好的办法来进行权限控制,比如针对不喜欢的追求者发送的情书,我可以根据他们的 pid (进程id)以及 uid (App的id) 来决定要不要接收他们的情书。

为了解决这个问题,我发布了各种悬赏公告,最终一个叫binder的小伙子找到了我。

他说:“白富美大小姐,你好啊,我先做下自我介绍,我叫binder,主要解决Android系统进程之间的通信。”

白富美问到:“打扰下,咱们直接进入主题吧,因为我现在真的很困惑,我的追求者通过socket来给我发送情书,而我要做权限控制,你能做到吗?”

binder非常自信的说到:“小菜一碟,我与socket相比,优势多了去了。首先在一次进程之间通信过程,socket需要两次拷贝,而我只需要一次;其次你可以知道是哪个追求者发送的情书,而socket我对它了如指掌,它实现这个功能很难;最后追求者在发送情书的时候是面向对象调用,犹如在调用他自己本地的对象方法一样。”

binder偷偷的瞄了瞄白富美,看到她的注意力非常集中,接着说:“上面介绍了我在哪些方面强于socket,那接下来介绍下关于我自己的吧。”

“binder通信是client/server模式,也就是分为binder clientbinder server两部分,binder server提供各种能力供binder client来调用,在java层只有继承了Binder类才是一个binder server,而在native层只有继承了BBinder的类才是一个binder server。Binder是对BBinder的封装。”

“而binder client在调用binder server提供的能力之前,是需要从ServiceManager获取对应信息的,而这个对应信息在java层是BinderProxy类,在native层是BpBinder类,BinderProxy是对BpBinder的一个封装。关于我的介绍就到此为止,你看下是否还有问题。” (这里需要备注下:binder server分为具名和匿名两种,具名就是可以从ServiceManager中通过name获取到的,否则就是匿名。而本文介绍的binder server是具名的。并且ServiceManager管理的也是具名binder server)

白富美:“那看来我是真找对人了,我的需求很简单,我只需要提供一个receiveLoveLetter的接口,对于同意接收的情书,会返回给追求者true,否则返回false。就这么简单,帮我实现下吧。”

“太简单了,看我的表演。”
932f119ab87ea4eab7ec9729e68e7e81

疯狂写代码中…

完了,看下面代码

//定义IBeautifulGirl接口,它继承了IInterface接口
public interface IBeautifulGirl extends android.os.IInterface {

    //binder server需要继承Stub类,实现receiveLoveLetter方法
    public static abstract class Stub extends android.os.Binder implements IBeautifulGirl {
        private static final java.lang.String DESCRIPTOR
                        = "IBeautifulGirl";
/**
          * Construct the stub at attach it to the interface.
          */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }
        
        public static IBeautifulGirl asInterface(android.os.IBinder obj)
        {
            if ((obj==null)) {
              return null;
            }
            //如果obj是Binder,则queryLocalInterface是可以获取到值的,否则获取不到
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin!=null)&&(iin instanceof IBeautifulGirl))) {
              return ((IBeautifulGirl)iin);
            }
            return new IBeautifulGirl.Stub.Proxy(obj);
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply,
                int flags) throws android.os.RemoteException {
            switch (code) {
              case TRANSACTION_receiveLoveLetter: {
                    data.enforceInterface(descriptor);
                    //binder server需要实现receiveLoveLetter方法
                    boolean _result = this.receiveLoveLetter(data.readString());
                    reply.writeNoException();
                    reply.writeBoolean(_result);
                    return true;
                }
            }
        }        


        //代理类,在binder client使用
        private static class Proxy implements IBeautifulGirl {

            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
            
            @Override
            public boolean receiveLoveLetter(String param)throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeStrongBinder((((cb != null)) ? (cb.asBinder()) : (null)));
                    _data.writeString(param);
                    mRemote.transact(Stub.TRANSACTION_receiveLoveLetter, _data, _reply, 0);
                    _reply.readException();
                    return _reply.readXXX();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
              }
          }
            static final int TRANSACTION_receiveLoveLetter = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    }
    
    //接收情书抽象方法,
    public boolean receiveLoveLetter(String param) throws android.os.RemoteException;

}

//BeautifuGirl类继承了IBeautifulGirl.Stub类
public BeautifulGirl extends IBeautifulGirl.Stub{
    
    //返回true:就代表接受情书并且同意交往;返回false:则代表不同意
    public boolean receiveLoveLetter(String letter){
       检查权限代码
       读情书内容代码
       返回结果代码
    }

}

“hi 白富美上面的代码,BeautifuLGirl类的receiveLoveLetter方法就是接收情书的方法,可以通过BindergetCallingPid方法获取追求者的pid,可以通过BIndergetCallingUid方法可以获取追求者的uid,你就可以根据这些值来做权限判断了,读取情书以及返回哪些值,这得你自己来实现了。”

“忘记了,还有非常重要的一步:需要调用ServiceManageraddService方法把你的BeautifuLGirl对象进行添加,因为ServiceManager也是一个具名binder server,它是处于servicemanager进程,因此在调用它的方法的时候需要特殊处理,下面的伪代码会把你的BeautifuLGirl类实例添加到Servicemanager” (当然app是不可以调用ServiceManageraddService方法的,这里是为了说明问题把白富美当成一个系统的进程)

添加BeautifuLGirlServiceManager的伪代码:

//获取ServiceManager实现了接口IServiceManager,而下面的方法获取的IServiceManager是一个代理类
IServiceManager serviceManager = ServiceManagerNative.asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));

//添加BeautifuLGirl
serviceManager.addService("beautiful",new BeautifulGirl());

如上代码,通过字符串beautifulBeautifulGirl实例添加到了ServiceManager中。

如何使用?

白富美:“那我的追求者如何使用呢?”
binder:“你是这么的美丽动人,我觉得你不输任何明星,明星们都有自己的经纪人,那我也为你量身定做了一个‘代理人’,他的名字就是IBeautifulGirl.Proxy,从名字上可以看出它使用了代理模式,只要你有的功能他都有。下面是如何使用的伪代码。”

/获取ServiceManager实现了接口IServiceManager,而下面的方法获取的IServiceManager是一个代理类
IServiceManager serviceManager = ServiceManagerNative.asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));

BinderProxy bp = serviceManager.getService("beautiful");
IBeautifulGirl beautifuGirl = IBeautifulGirl.Stub.asInterface(bp);
beautifuGirl.receiveLoveLetter("情书");

白富美:“广大的追求者们,我现在发个通告:以前的socket发送情书的方法已经废弃了。你们如果要想给我发送情书,请先获取到我的‘代理人’IBeautifulGirl.Proxy,进而调用receiveLoveLetter方法就可以把你们的情书发送给我,我可是整个Android系统中最美丽最动人最富有的没有之一的进程。错过了绝对没机会了。”

矮挫丑

我是矮挫丑,性别男,我是Android系统里的一个普通进程,我也是白富美众多追求者之一,你们都说我是癞蛤蟆想吃天鹅肉,我可不这么认为,只是你们认为我是癞蛤蟆,而我可不这么认为,并且谁说白富美就一定喜欢高富帅呢,我自有我的优点:那就是脸皮厚、情商高、文字功底一流。

那就看看我是如何用我优美的文字征服白富美的吧。

矮挫丑还真是厉害,没过几个小时就把情书写好了,而很多的追求者甚至用了好几天才把情书写好。

他自言自语的对情书说到:“情书啊情书,我创作你可是不易啊,我的终身大事可就完全拜托你了。”

情书突然张口说到:“放心吧主人,你的事情就是我的事情,我肯定会尽心尽力的。”

“啥情况,我写的情书竟然会说话,这也太奇妙了,有如此之情书,我志在必得。”

发送情书

矮挫丑细细的琢磨着:我记得白富美给大家公开了给她发送情书的方法,那就是需要先获取她的“代理人”IBeautifulGirl.Proxy,那就按照她公布的方式先获取“代理人”。

代理人

代理人,你好啊,帮忙把我写好的情书交给白富美吧,我对她是非常非常真心的,见到她后也帮忙在她面前美言几句啊,谢谢了。”

代理人:“你不是对自己非常的自信吗?怎么还需要我美言几句呢。我特别想帮你美言几句,但是我做不到啊,因为我也根本见不到白富美本人。”

矮挫丑不耐心的插了一句:“不想帮忙就不想帮吧,还不说实话,你作为她的‘代理人’我就不信你见不到她。”

“你对于我不了解,说出这样的话我也不怪你,是这样的,我其实是使用了代理模式,首先我是一个Proxy类,我和白富美BeautifuGirl类都实现了同一个接口IBeautifulGirl,因此BeautifuGirl类有啥接口,我同样也有。”

//Proxy实现了IBeautifulGirl接口
private static class Proxy implements IBeautifulGirl {
 
            //binder server代理人,mRemote是BinderProxy
            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
            
            @Override
            public boolean receiveLoveLetter(String param)throws android.os.RemoteException {
               省略代码......
          }
          
}

//BeautifuGirl继承了IBeautifulGirl.Stub,IBeautifulGirl.Stub实现了IBeautifulGirl接口
public BeautifuGirl extends IBeautifulGirl.Stub{
    
    public boolean receiveLoveLetter(String letter){
       省略代码......
    }

}

public static abstract class Stub extends android.os.Binder implements IBeautifulGirl {
    省略代码......
}

“不知道你是否明白了,这就是我为啥叫‘代理人’的原因,我被创造出来的一个非常重要的原因就是为了让你们使用者通过obj.method调用某个对象的某个方法来使用。首先这样使用起来毋庸置疑肯定是非常方便的;其次让使用者感觉不到自己在调用的是一个别的进程的方法,犹如在使用本地对象的一个方法一样。”

“赞,binder的设计者真的是考虑的相当的周到、细致、用心,必须点个大大的赞。”

“谢谢,再来说下我为啥见不到白富美,其实我这个代理人只存在于你矮挫丑的进程,我是不会被传递到别的进程的,我只是在binder通信中起一个非常微小的作用,我存在的目的就是为了让你们使用者使用方便。”

“不好意思,是我错怪你了,我为我的不礼貌的话语向你道歉。”

“没事,你调用我的receiveLoveLetter方法,就可以把情书交给我,剩下的事情就是等待好消息了。”

矮挫丑:“receiveLoveLetter方法已调用,我就把情书交给你了,就劳驾您了,一路上注意安全。”

对了还有个小秘密告诉你:“我的情书会讲话啊,它可以在路上陪你聊聊天、解解乏。”

“啊!还有这等神奇的事情,那太好了。”

就这样代理人带着情书上路了,他们一路上有说有笑极其欢乐,在经过一个驿站的时候,趁着稍作休息的功夫,代理人有些伤感的对情书说:“情书啊,从这个驿站开始我就与你分离了,我会把你交给我的属性mRemote它的值是BinderProxy对象,其实也就是交给BinderProxy了。”

情书不知所措的说:“为啥啊?一路上咱们不是相处的很好嘛。”

“是这样的,把你送到白富美的路上需要很多的小伙伴来参与,我的使命暂时告一段落了,当然我也会等待白富美的回复,我会把回复交给你的主人矮挫丑。”

“当然还有非常重要的一个事情要做,为了让你路上不孤单,给你找了几个小伙伴,如下”

  1. 其中有methodCode它是int类型的,它的值是TRANSACTION_receiveLoveLetter白富美根据它就可以知道调用哪个方法了
  2. _data它是Parcel类型的,为了让你能顺利的进入别的进程,会调用它的writeString方法把你序列化,当然在这里你是非常的安全的
  3. _reply它也是Parcel类型的,它的作用是会把白富美的回复给带回来,我会从*_reply*中把回复拿到交给矮挫丑

我是代理类,我在binder通信中的作用主要是让binder server的使用者像使用本地对象的方法一样,使用起来非常方便,同时会创建methodCode变量,类型为Parcel的_data变量 (所有的参数都会放入该变量中),并且会构造一个类型为Parcel的*_reply*变量 (它会存储返回结果包括是否有异常)

BinderProxy你好,我调用你的transact方法就把TRANSACTION_receiveLoveLetter、_data、_reply这几个小伙伴交给你了,发送情书的事情就交给你了,对了情书它可是会说话的啊,路上可以和你聊天。”

下面这张图代表情书传递过程中经历的方法和参数的变化

image

BinderProxy

一路上情书BinderProxy有说有笑,渐渐熟悉了,它对BinderProxy说:“BinderProxy你能介绍下你自己吗?以及在binder通信中发挥了什么作用。”

BinderProxy:“我是java层的类,看我的名字后面有个单词proxy,可以猜出我其实也是个代理,我是与Binder类相对应的。而说到我在binder通信中发挥了什么作用?这个着实很惭愧,因为很多的事情根本都不是我做的,我只是一味的在调用native层的方法。非要说自己的作用的话就是把java层方法的各种参数传递到native层。”

我突然想起来了,我有一个独特的能力,你们肯定没听过:“我虽然身处java层,但是java层却没有任何类可以实例化一个BinderProxy对象,就是在java层没办法直接new一个BinderProxy对象。而谁来实例化我呢?那就是native层的代码,不信你们可以看我的构造方法是私有的。”

情书:“那如果在java层通过反射来实例化呢?”

BinderProxy瞟了一眼情书,心想它不单单会说话,竟然还懂java语言,这就奇了个大怪了。

“哈哈,是这样的,即使通过反射实例化了,也没用,因为我有一个long类型的mNativeData属性,它是native层BpBinder对象的地址。设计如此的目的与binder驱动有非常大的关系,到了内核的时候,让内核大佬们讲给你听吧。”

咱们不聊了,继续赶路吧,正如BinderProxy说的,它其实就是一味的在调用native方法,经过jni调用后,我和我的小伙伴到达了native层,到达native层后,第一件事情就是把我们几个小伙伴转换为native层的对象,java层Parcel对象转换为native层的Parcel对象,甚至BinderProxy对象也转换为native层的BpBinder对象。

情书突然感觉到有事要发生了,因为刚刚已经经历过了。

BinderProxy有些不舍的对情书说:“我的使命也暂时告一段落了,接下来我会把你们交给BpBinder,后会有期。BpBinder,调用你的transact方法后,情书和它的小伙伴就交给你了。你要好好照顾它们。”

我是BinderProxy,我在binder通信中的作用是起连接java层与native层的桥梁,同时把java层的各种参数传递到native层。我也大概能理解:为啥在java层不能new一个BinderProxy实例了,其实主要原因是先有native层的BpBinder,而在根据它的地址在native层new一个BinderProxy实例。我其实就是把BpBinder的各种方法都封装起来供java层来使用。在Android系统中像我这种例子是非常多的,比如Bitmap类也是只有native层才能new。

同样也用一张图代表情书传递过程中经历的方法和参数的变化

image

如下是相关代码,请自行取阅:

//文件路径:frameworks/base/core/jni/android_util_Binder.cpp
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
        jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
    省略代码......
    //转data
    Parcel* data = parcelForJavaObject(env, dataObj);
    if (data == NULL) {
        return JNI_FALSE;
    }
    //转reply
    Parcel* reply = parcelForJavaObject(env, replyObj);
    if (reply == NULL && replyObj != NULL) {
        return JNI_FALSE;
    }
    //根据BinderProxy的mNativeData获取到对应的BpBinder,这时候target就是BpBinder
    IBinder* target = getBPNativeData(env, obj)->mObject.get();
    
    省略代码......
    
    //调用BpBinder的transact方法
    status_t err = target->transact(code, *data, reply, flags);
    
    省略代码......
}

BpBinder

BpBinder:“情书你好,欢迎你来到native层,看到我的名字,应该不知道是啥意思吧?那我就来介绍下。”

Bp是binderProxy的缩写,为了与java层BinderProxy区别,我的名字就是BpBinder了。java层BinderProxy类对应的是Binder类,而native层BpBinder类对应的是BBinder类。在native层也是支持binder server的,那就是要继承BBinder类即可成为native层的binder server。

情书:“那您同样也是一个代理类吧,能介绍下您在binder通信中的作用吗?”

“是的,在binder通信中的作用我觉得我起了两个作用。首先我会把BpBinder传递下来的各种数据传递给我的底层,至于我的底层是谁稍后会说;其次我有一个非常重要的属性mHandle,这个属性在旧版Android系统是int类型,在Android13上是Handle类型 (Handle类型的BinderHandle类的handle也是一个int类型),也就是说不管在Android新旧版本上都有一个int类型的值,这个值的作用是非常非常重要的,在binder驱动也只有通过这个值才能找到目标binder server。这个值可不是凭空捏造出来的,它可是binder驱动生成的,也就是在binder驱动同样也保存了这么一个值。”

情书到了咱们告别的时候了,我给你和你的小伙伴又找了一个小伙伴,它就是上面提到的int类型的值,在binder驱动层是需要它的。”

IPCThreadState你好啊,那我就调用你transact方法,我把情书和它的小伙伴就交给你了。”

还是老规矩,用一张图代表情书传递过程中经历的方法和参数的变化

image

IPCThreadState

一路上情书心里面总有些忐忑,为啥呢?因为它觉得IPCThreadState这个家伙像个骗子,你看前面的BinderProxyBpBinder最起码从名字上就能看出来和binder通信有关系,而IPCThreadState这名字呢鬼才能看出来和binder通信有关系。

终于情书鼓起勇气问IPCThreadState,但是话刚到嘴边又咽了回去,就赶紧换了个问题:“你好啊,我是要去白富美那里,我和我的小伙伴离目的地还有多远啊?”

IPCThreadState:“你们走了差不多三分之一的路程了,过了内核,就离目的地不远了。”

情书心里面嘀咕着,听着他的回答是有那么一点意思,但是自己心里面还是不确定,于是再次鼓足了勇气问到:“是这样的,咱们走了一路,我只知道你是护送我们到达目的地的,但是从你的名字上看,我感觉你和binder通信没有任何的关系啊,你不会是个骗子吧?还有如果你不是骗子那就证明下吧。”

IPCThreadState:“啊!BpBinder在把你们交给我的时候,告诉过我说你会说话,这一路上也没见你说一句话,原来是觉得我是一个骗子啊。好吧那我可要证明自己的清白。”

“话又说回来也不怪你,都是我这糟糕的名字惹的祸,当时我的设计者在给我起这名字的时候,我是强烈反对的,但是呢反对无效啊。首先我名字的IPC是Inter-Process Communication的缩写,翻译为中文就是进程之间通信的意思,而后面的ThreadState就是线程状态了。别看我的名字起的糟糕,但是我的工作内容可不糟糕。”

“我的工作内容主要就是与binder驱动进行通信,我会把像你们这样的信息发送给binder驱动 (binder驱动可是位于内核空间),而binder驱动也会随时把它那边的工作进度、工作情况等内容发送给我。”

情书:“我相信你了,那能展开说下,你是如何把我和我的小伙伴发送到binder驱动的吗?我特别感兴趣。”

IPCThreadState:“那我就讲一讲吧,毕竟稍等片刻你也会从我这离开的。为了让你听的更明白些,我觉得非常有必要先从科普知识开始。”

科普知识

系统调用

系统调用的英文是System Call,啥意思呢,就是Android操作系统分为用户空间内核空间,用户空间的进程只有通过系统调用才能与内核空间进行通信。进行系统调用,用户空间进程的线程会由用户态切换到内核态,当在内核空间处理完毕任务后,用户空间线程恢复原先状态。

ioctl

它的全称是Input/Output Control,它是一个系统调用函数,它的主要作用是实现用户空间进程与内核空间驱动之间通信。

下面是该函数的声明,其中fd是设备驱动对应的文件描述符,它是一个int类型;request是一个无符号long类型;而后面的…一般是一个指向数据的指针。

int ioctl(int fd, unsigned long request, ...);

IPCThreadState与binder驱动发送信息就是用的是ioctl这个系统调用函数。

介绍了科普知识后,那我在来介绍一些数据结构和cmd吧。

数据结构

大家在使用socket通信的时候,serverclient双方是不是要约定好一个cmd + 数据结构,cmd代表要执行哪些操作,而数据结构则是执行这些操作要使用到的数据。同理使用ioctl函数与binder驱动进行通信的时候也需要定义这样的cmd + 数据结构,那我与binder驱动定义了 BINDER_WRITE_READ + binder_write_readBINDER_THREAD_EXIT + 0BINDER_FREEZE + binder_freeze_info 等cmd和数据结构,而在binder通信中最常用的就是BINDER_WRITE_READ + binder_write_read这套组合。

那就先来介绍下binder_write_read这个数据结构

1 binder_write_read

如下是它的定义

struct binder_write_read {
	binder_size_t		write_size;	/* bytes to write */
	binder_size_t		write_consumed;	/* bytes consumed by driver */
	binder_uintptr_t	write_buffer;
	binder_size_t		read_size;	/* bytes to read */
	binder_size_t		read_consumed;	/* bytes consumed by driver */
	binder_uintptr_t	read_buffer;
};

如上它的属性,其中write_xxx的都是我IPCThreadState发送给binder驱动的数据,而read_xxx是binder驱动发送给我的数据。

而在binder_write_read中发送给binder驱动的数据中也定义了一套cmd + 数据结构,如BC_TRANSACTION + binder_transaction_data (调用binder server方法的时候用这个组合)、BC_REPLY + binder_transaction_data (当binder server方法返回结果的时候用这个组合) 等。而针对cmd也定义了一些规则:发送给binder驱动的使用BC_xxxx格式,而接收binder驱动的使用BR_xxxx格式

BC_TRANSACTION + binder_transaction_dataBC_REPLY + binder_transaction_data这俩组合是经常要用到的,那来介绍下binder_transaction_data数据结构吧。

2 binder_transaction_data

如下是它的定义

struct binder_transaction_data {
	/* The first two are only used for bcTRANSACTION and brTRANSACTION,
	 * identifying the target and contents of the transaction.
	 */
	union {
		/* target descriptor of command transaction */
		__u32	handle;
		/* target descriptor of return transaction */
		binder_uintptr_t ptr;
	} target;
	binder_uintptr_t	cookie;	/* target object cookie */
	__u32		code;		/* transaction command */

	/* General information about the transaction. */
	__u32	        flags;
	pid_t		sender_pid;
	uid_t		sender_euid;
	binder_size_t	data_size;	/* number of bytes of data */
	binder_size_t	offsets_size;	/* number of bytes of offsets */

	/* If this transaction is inline, the data immediately
	 * follows here; otherwise, it ends with a pointer to
	 * the data buffer.
	 */
	union {
		struct {
			/* transaction data */
			binder_uintptr_t	buffer;
			/* offsets from buffer to flat_binder_object structs */
			binder_uintptr_t	offsets;
		} ptr;
		__u8	buf[8];
	} data;
};

如上代码,这里主要介绍几个关键的属性:

  1. targetunion类型的,它的值是handle或者ptr,看到handle不知道你是否有印象。

情书拖着腮帮想了想:“知道的,BpBinder说过它有一个非常重要的属性说的就是它,还有BpBinder在把我和我的小伙伴交给你的时候也把自己的handle交给了你。也就是说handle其实对应的是上层的BpBinder类,而ptr则对应的就是上层的BBinder类了?”

“都会推测了,你说的非常对。ptr它是一个地址”

  1. cookie它其实对应的是上层的BBinder类,它也是一个地址,这个在白富美那边会涉及到。
  2. code这个你肯定也能猜出来吧

情书:“那它肯定是我的小伙伴TRANSACTION_receiveLoveLetter了。”

“没错,code就是指向了方法对应的值,这样在binder server端会根据code值来进入对应的方法。”

  1. data_sizeoffsets_sizedata就是对应的类型为Parce的data参数,也就是在调用binder server某个方法的时候,方法的参数都与这几个属性有关。其中data.ptr.offsets是binder对象的个数。

情书:“哦,原来我是放置在这块啊!那我的类型为Parcel的_reply小伙伴呢?怎么没有看到它对应的存放位置。”

IPCThreadState:“_reply是不会被传递到binder驱动层的,因为我可以从binder驱动层拿到回复数据,拿到后我会把回复数据放置到_reply内。”

发送数据到binder驱动

IPCThreadState自信的对情书说:“我就是这样一个做事认真仔细的人,要想让别人听明白你讲的东西,就得站在小白的角度考虑,把一些基础性的东西先科普下,在由浅入深的去讲,就可以非常容易的让别人听懂了。发送数据到binder驱动分为写数据发送数据两步,那就来介绍下它们。”

写数据

写数据其实就是情书和你的小伙伴还有非常重要的handle写到binder_transaction_data数据结构中,同时还需要写入BC_TRANSACTION这个cmd。

如下是相关代码,请自行取阅:

//文件路径:/frameworks/native/libs/binder/IPCThreadState.cpp
status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
    int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)
{
    binder_transaction_data tr;
    
    //因为是BpBinder,所以target.ptr是0
    tr.target.ptr = 0; /* Don't pass uninitialized stack data to a remote process */
    tr.target.handle = handle;
    tr.code = code;
    tr.flags = binderFlags;
    //因为是BpBinder,所以cookie是0
    tr.cookie = 0;
    tr.sender_pid = 0;
    tr.sender_euid = 0;

    const status_t err = data.errorCheck();
    if (err == NO_ERROR) {
        //情书在这写入
        tr.data_size = data.ipcDataSize();
        tr.data.ptr.buffer = data.ipcData();
        tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);
        tr.data.ptr.offsets = data.ipcObjects();
    } 
    
    省略代码......

    //写入cmd
    mOut.writeInt32(cmd);
    //写入binder_transaction_data
    mOut.write(&tr, sizeof(tr));

    return NO_ERROR;
}
发送数据

binder server的方法分为两种one way非one way两种。

心急的情书有些听不懂了,着急忙慌的就问:“one way非one way都是啥子嘛?赶紧解释下呗!”

IPCThreadState:“one way就是说不需要等待binder server的回复,而反之非one way自然就是需要等待binder server的回复了。”

不管是one way还是非one way类型,发送数据都用的是ioctl方法,如下是我把情书和你们小伙伴发送到binder驱动的代码:

//文件路径:/frameworks/native/libs/binder/IPCThreadState.cpp
status_t IPCThreadState::talkWithDriver(bool doReceive)
{
    //mDriverFD小于0返回
    if (mProcess->mDriverFD < 0) {
        return -EBADF;
    }

    //构造binder_write_read实例
    binder_write_read bwr;

    省略代码......
    const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0;
    
    //把mOut放入bwr的write_xxx属性中
    bwr.write_size = outAvail;
    bwr.write_buffer = (uintptr_t)mOut.data();

    //省略代码......
    
    do {
        
        省略代码......
        
        //调用ioctl把bwr和BINDER_WRITE_READ发送到binder驱动
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        else
            err = -errno;
        省略代码......
    } while (err == -EINTR);

    省略代码......

    return err;
}

但是对于one way类型,我IPCThreadState把数据发送给binder驱动后,只要等待到BR_TRANSACTION_COMPLETE (binder驱动发送上来的数据)这个cmd就可以离开了,而对于非one way类型,把数据发生给binder驱动后,是需要等待binder server的回复的。

在调用ioctl系统调用后,咱们当前的线程会由用户态切换为内核态,并且咱们发送情书肯定是需要等待白富美的回复的,因此需要继续等待。

IPCThreadState松了口气,慢慢的说:“情书你还有啥不明白的吗?”

情书:“当然有了,调用ioctl函数的时候,传递的mProcess->mDriverFD这个文件描述符我看它非常重要,那它是啥时候初始化的,以及它的作用是啥?”

IPCThreadState心想这小子问的问题还真的很关键,那我就来讲讲它。

mDriverFD的由来和作用

java层的进程都是被zygote进程fork的,在fork成功后,ProcessState类的构造方法就会被调用,参数是/dev/binderProcessState类的实例在一个进程中是只存在一个,在ProcessState的构造方法中会调用open方法,参数为/dev/binder,这时候的binder驱动层的binder_open方法会被调用,进而会返回一个fd (文件描述符),这个fd会赋值给mDriverFD。

每个进程的mDriverFD都是不一样的,它作为ioctl函数的参数,在binder驱动层会根据mDriverFD来获取相应的信息,但这个信息具体是啥,我也不清楚了,我也只能介绍到这了,因为关于binder驱动的事情需要由它们来介绍了,它们更专业。

我是IPCThreadState,我在binder通信中的作用就是与binder驱动交互信息,我会通过ioctl系统调用函数把数据发送给binder驱动,binder驱动也会返回给我信息。

IPCThreadState:“调用ioctl方法后,我会把情书和你的小伙伴发送到内核,当然除了类型为Parcel的reply小伙伴不会进入内核,调用ioctl系统方法后,我对应的线程由用户态变为内核态,我也会处于等待阶段,等待白富美的回复,回复会放入replay中。”

情书:“我还真有些不舍,到了内核是谁来接待我们啊。”

IPCThreadState:“这个我还真不知道,我一辈子了都没进过内核,因此那边我没有熟人,不过你比我强多了,你还能到达内核甚至还能到别的进程溜达溜达。不过放心binder通信都已经非常成熟了,自然有人接待你,别害怕勇敢的往前走吧,加油。”

还是老规矩,用一张图代表情书传递过程中经历的方法和参数的变化

image

IPCThreadState把你情书和你的小伙伴发送到binder驱动,我可是等待着白富美的回复呢,如下是等待回复代码:

//文件路径:/frameworks/native/libs/binder/IPCThreadState.cpp
status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
    uint32_t cmd;
    int32_t err;
    
    //不断循序,除非遇到break
    while (1) {
        //发送数据到binder驱动后,会阻塞等待binder驱动返回数据
        if ((err=talkWithDriver()) < NO_ERROR) break;
        err = mIn.errorCheck();
        if (err < NO_ERROR) break;
        if (mIn.dataAvail() == 0) continue;
        //binder驱动发送的cmd 是以*BR*开头的
        cmd = (uint32_t)mIn.readInt32();

        IF_LOG_COMMANDS() {
            alog << "Processing waitForResponse Command: "
                << getReturnString(cmd) << endl;
        }

        switch (cmd) {
        省略代码......
        case BR_TRANSACTION_COMPLETE:
            //one way类型调用,直接就可以返回了
            if (!reply && !acquireResult) goto finish;
            break;

        省略代码......
        //白富美的回复会发送BR_REPLY cmd
        case BR_REPLY:
            省略代码......
            goto finish;

        default:
            err = executeCommand(cmd);
            if (err != NO_ERROR) goto finish;
            break;
        }
    }

    省略代码......
    return err;
}

情书到达内核

情书和它的小伙伴虽然被包裹在binder_transaction_data对象中,而binder_transaction_data对象和BC_TRANSACTION又被包裹在binder_write_read对象的write_buffer属性中,但它还是有些许的不安全感,毕竟来到了一个非常陌生的地方。

情书这时候有些伤感因为它的小伙伴reply没有来到内核,但是它又觉得不能这样想事情,因为它还多了三个小伙伴mDriverFDBINDER_WRITE_READbinder_write_read对象的地址。

突然一个声音打断了它的思绪:“快点来个干活儿的,从用户空间又来了一批数据,快点先根据mDriverFD找到对应的file结构,再把找到的file地址、BC_TRANSACTIONbinder_write_read对象的地址交给binder_ioctl方法来处理。”

情书心想他一定是个大人物,那我需要赶紧上前和他打打招呼,认识认识:“你好很高兴认识你,请问你怎么称呼,我是用户空间的情书,我是第一次来内核空间,请多多关照。”

“你好,我是binder驱动,整个内核的binder事情都归我管理,不用客气,有事情尽管说,还有你是要去往何处啊?”

情书急忙答到:“用户空间的矮挫丑进程要我去往白富美进程,刚来内核,人生地不熟,麻烦您能介绍下我该如何到达目的地。”

binder驱动:“好的,在内核空间您到达目的地可以分为查找目标复制数据激活目标这三步。那我就细说下这三步吧。”

查找目标

在介绍之前先来说四个关键的数据结构吧,它们在查找目标过程中起了非常重要的作用。

关键数据结构

binder_proc

从名字的后缀_proc来看是不是看到了进程啊,没错它确实是与进程有关系的,它与用户空间的除了zygote之外的java进程以及只要打开了binder驱动的native进程是一一对应的关系,比如systemserver进程、launcher、vold native进程、installd native进程在binder驱动都存在对应的binder_proc。也就是只有打开binder驱动的用户空间进程才会在binder驱动有一个对应的binder_proc,而除了zygote之外的java进程是只要进程被fork成功后,就会自动打开binder驱动。

在用户空间,进程之间是隔离的,而在binder驱动,那可不是,binder_proc之间是可以互相引用的。

说了这么多上干货吧,来看下它的数据结构的关键属性吧

struct binder_proc {
    省略其他属性......
    
    //使用红黑树存储所有的binder_thread
	struct rb_root threads;
    //使用红黑树存储所有的binder_node
	struct rb_root nodes;
    //使用红黑树存储所有的binder_ref (以desc的方式查找binder_ref)
	struct rb_root refs_by_desc;
    //使用红黑树存储所有的binder_ref(以binder_node的方式查找binder_ref)
    struct rb_root refs_by_node;

	
    省略其他属性......
};

如上代码,分别用红黑树存储了所有的binder_threadbinder_nodebinder_ref

何时创建binder_proc
在用户空间进程打开驱动的时候也就是调用open方法,参数为/dev/binder的时候,最终会调用到binder_open方法,在该方法中,会根据用户空间进程pid等创建对应的binder_proc

对了用户空间的binder相关的系统调用方法在binder驱动都有对应的方法,比如ioctl对应binder_ioctlopen对应binder_openmmap对应binder_mmap

binder_thread

既然用户空间打开binder驱动的进程会存在对应的binder_proc,那进程中与binder驱动交互的线程也存在对应的binder_thread情书关于这点你明白吗?

情书挠挠头想了想,好像有点思路急忙说到:“我好像有印象,我记得在矮挫丑是在他的主线程里面调用了发送情书的方法,那这个主线程也会存在对应的binder_thread是吗?”

binder驱动:“是的没错,在哪个线程里面进行了binder调用,该线程就会被记录到binder_thread,这里记录这些线程的作用是为了回复做准备。“

”当然除了这个还有用户空间的IPCThreadState启动的binder线程,它们也会被记录到binder_thread,为啥要叫它们为binder线程呢?因为它们主要的作用就是与我binder驱动进行通信的,这里记录binder线程的作用可就多了,比如binder线程离开会发送消息给我,binder线程是否是主binder线程也会发送消息给我。在App打开的时候,IPCThreadState会创建一个主binder线程,这个线程不会死掉,会一直循环下去,它可以监听binder驱动发送上来的消息比如启动一个binder线程,则启动成功后会发送消息通知binder驱动。App进程和systemserver进程启动的最大binder线程数是不一样的。超过了最大启动binder线程数,就不能再启动binder线程了。”

同样上干货,看它的关键属性:

struct binder_thread {
    //指向它的binder_proc
	struct binder_proc *proc;
	struct rb_node rb_node;
	struct list_head waiting_thread_node;
    //线程id
	int pid;
    //是否是循环的binder线程
	int looper;              /* only modified by this thread */
	bool looper_need_return; /* can be written by other thread */
	struct binder_transaction *transaction_stack;
    //todo队列
	struct list_head todo;
	省略属性......
};
binder_node

binder_node其实是对用户空间的BBinder类的一个封装,其中ptrcookie属性指向了BBinder对象,如下是它的关键属性

struct binder_node {
	省略属性......
    
    //指向binder_proc
	struct binder_proc *proc;
    //所有的binder_ref
	
    省略属性......
    
    //下面两个属性都指向用户空间的BBinder
	binder_uintptr_t ptr;
	binder_uintptr_t cookie;
	
    省略属性......
};

何时生成binder_node

binder client调用binder server的某个方法时候,如果该方法的参数中包含了BBinder对象,当参数等数据则进入binder驱动后,binder驱动会检查binder client进程对应的binder_procnodes属性中有没有与BBinder对象对应的binder_node,如果没有则会为BBinder生成对应的binder_node,并且放入binder_procnodes属性中。

上面的一坨话比较枯燥,那就举个例子比如ActivityManagerService把自己放入ServiceManager中,会在systemserver进程中调用IServiceManageraddService方法,参数为activityActivityManagerService的实例,因为ServiceManager是一个binder server它在servicemanager进程,因此调用addService方法会进入binder驱动层,binder驱动层会检测systemserver进程对应的binder_procnodes属性中有没有ActivityManagerService实例对应的binder_node,如果没有则会为ActivityManagerService实例生成对应的binder_node,并且放入binder_procnodes属性中。(这个例子里binder client是在systemserver进程,binder server是在servicemanager进程,ActivityManagerService继承了BinderBinder类其实又是对native层BBinder的封装)

情书:“哦,我明白了,我曾经见过白富美调用了ServiceManageraddService方法,那它对应的binder_node就在它的binder_procnodes属性中存在了。”

binder驱动:“是的,非常正确,当然了并不是只有调用ServiceManageraddService方法才会生成binder_node,只要进行binder通信的方法都可以,比如在调用startActivity方法的时候,往Intent对象中放入一个Binder对象,这时候binder驱动也会为这个Binder对象生成一个binder_node,只不过这时候的binder server是匿名的。”

binder_ref

binder_ref就是对用户空间的BpBinder的封装,它的data.desc属性与BpBinderhandle属性是一样的。如下是它的关键属性

struct binder_ref_data {
	int debug_id;
    //它与用户空间的BpBinder的handle是一致的
	uint32_t desc;
	int strong;
	int weak;
};

struct binder_ref {
	//指向上面的binder_ref_data
	struct binder_ref_data data;
	省略属性......
    
    //指向binder_proc
	struct binder_proc *proc;
    //指向binder_node
	struct binder_node *node;
	struct binder_ref_death *death;
};

何时生成binder_ref

binder client调用binder server的某个方法时候,如果该方法的参数中包含了BBinder或者BpBinder对象,当参数等数据则进入binder驱动后,binder驱动会检查binder server进程对应的binder_procrefs_by_node属性中有没有与BBinder或者BpBinder对象对应的binder_ref,如果没有则会为BBinder或者BpBinder对象生成对应的binder_ref,并且放入binder_procrefs_by_noderefs_by_desc属性中。

上面的一坨话比较枯燥,那就还是上面ActivityManagerService把自己放入ServiceManager中的例子,activityActivityManagerService的实例数据进入binder驱动后,binder驱动会检测servicemanager进程对应的binder_procrefs_by_node属性中有没有ActivityManagerService实例对应的binder_ref,如果没有则会为ActivityManagerService实例生成对应的binder_ref,并且放入binder_procrefs_by_noderefs_by_desc属性中。而ServiceManageraddService方法被调用后,这时候的参数为变为activityBpBinder对象,这时候的BpBinder它的handle值就是binderr_refdesc值。

或者可以这样说,java层的BinderProxy对象或者native层的BpBinder对象它们的初始化,是在binder驱动层的对应binder_ref生成后 (它是根据对应的binder_node或者binder_ref),在根据binder_refdata.desc属性值构造BpBinder对象,BpBInder对象构造成功后,根据BpBinder在构造BinderProxy对象。

情书:“哈哈,我明白了,和我一起的小伙伴handle,它其实已经在矮挫丑进程的binder_proc的的refs_by_noderefs_by_desc属性中已经存在对应的binder_ref了,并且它的data.deschandle是一样的。”

binder驱动:“你真的太聪明了,赞。”

小结

用一张图展示下binder_procbinder_nodebinder_ref与用户空间进程的关系

image

查找目标思路

binder驱动:“关键数据结构介绍完毕,有了这个基础那我就来讲下查找目标的思路。”

  1. 找到当前用户空间进程的binder_proc (当前进程比如矮挫丑进程)
  2. 根据handlebinder_procrefs_by_desc属性中查找对应的binder_ref
  3. 若找到binder_refbinder_refnode属性就是目标binder_node,这个binder_nodeptrcookie属性就指向用户空间的BBinder (BBinder就是用户空间native层或者java层的binder server)
  4. 根据binder_node也可以找到目标binder_proc,目标binder_proc可以知道用户空间目标进程的一些信息。

查找目标

binder驱动情书说:“有了上面的思路,那我就先暂时让binder_ioctl方法来帮助你找白富美吧。我会把file地址、BINDER_WRITE_READbinder_write_read对象的地址交给binder_ioctl。我一会儿就过来。”

情书:“谢谢了binder驱动。”

binder_ioctl情书说:“准备好了吧,那咱们就开始吧,首先需要从fileprivate_data属性中可以拿到你的进程的binder_proc。”

情书:“冒昧的问一句,这个file是啥?”

“它啊,你还记得调用ioctl方法的时候需要传递一个mDriverFD的文件描述符吗?mDriverFD会被转换为对应的filefile里面存储了很多文件相关的属性,当用户空间进程在打开binder驱动的时候生成的binder_proc会存放在fileprivate_data属性中。这样凡是用户空间进程在调用binder相关的系统调用函数的时候如mmapioctl都需要携带mDriverFD,相关的函数如binder_mmapbinder_ioctl都会从fileprivate_data中拿到binder_proc。”

“那我们接着继续,这时候我还做了一件事情就是生成binder_thread,它会被放置在binder_procthreads属性中,生成binder_thread的作用是为了等待binder server的回复。”

这时binder驱动对binder_ioctl说:“我忙完了,剩下的过程就交给我来说吧。”

binder驱动:“情书你应该还记得你的小伙伴binder_write_read对象吧,它的write_buffer属性中存放了BC_TRANSACTIONbinder_transaction_data,而codehandle以及情书你都在binder_transaction_data内,那我们接下来的事情就是拆包,从binder_transaction_data把这些数据拆出来。”

“从binder_transaction_data中拆出handle后就可以依据上面的查找思路找到目标binder_node和目标binder_proc了。”

小结

查找白富美的总体思路如下:

  1. 调用ioctl方法的时候,根据传递的mDriverFD会拿到对应的file数据结构
  2. fileprivate_data属性中拿到矮挫丑进程的binder_proc
  3. 使用用户空间传递的handlebinder_proc中查找对应的binder_ref
  4. binder_refnode属性指向binder_node数据结构
  5. binder_nodeproc属性就是白富美进程对应的binder_proc

上面找到的binder_nodecookie指向用户空间的JavaBBinder对象 (它是BBinder的子类),而JavaBBinder对象是持有BeautifuLGirl对象的引用的,BeautifuLGirl是一个binder server,它位于白富美进程,它的receiveLoveLetter方法就是用来真正接收情书,然后交给白富美来阅览的。

情书恍然大悟说到:“哦!原来如此啊,BinderProxy对象到了native层是对应BpBinder对象,而BpBinder对象的handle属性到了binder驱动层,首先会根据mDriverFD找到对应进程的binder_proc,在根据handlebinder_proc找到对应的binder_ref,而binder_refnode属性是指向binder_node对象,而它的cookie属性又指向用户空间的JavaBBinder对象,JavaBBinder对象又持有BeautifuLGirl对象。”

“对,就是这样的,那咱们就进入下一步吧。”

复制数据

binder驱动情书说:“咱们既然找到了目标白富美在binder驱动层的binder_procbinder_node,那我就把情书你复制给目标。而你的其他小伙伴就直接交给目标吧。”

还没等binder驱动接着往下说,情书急忙问到:“不好意思啊打扰下,我的理解是在binder驱动层,不同的binder_proc之间以及其他的数据结构之间是可以互相引用的,而不像用户空间进程之间是隔离的,那为啥还要用复制而不是直接交给目标呢?还有为啥其他小伙伴是直接交给目标,而只有我是复制给目标呢?还有其他小伙伴都有哪些?”

为啥要复制数据

binder驱动:“你这性子也太急了吧,你刚刚问的这些问题我也正要说呢,那我就来介绍下吧。先来介绍下为啥要复制数据,Android系统分为用户空间内核空间,当一个系统调用进入内核空间后,在内核空间是不能直接使用用户空间传递过来的数据的,原因如下:”

  1. 内存隔离和权限管理:现代操作系统如Linux通过引入SMEP(Supervisor Mode Execution Protection,即监管模式执行保护)和SMAP(Supervisor Mode Access Prevention,即监管模式访问防止)等机制,确保内核空间与用户空间是隔离的。这意味着内核态的指令无法直接存取用户态的数据,从而保证了系统的安全性和稳定性。
  2. 安全性考虑:如果内核态可以直接存取用户态的数据,将带来极大的安全风险。因为用户态的数据更容易被劫持或篡改,如果这些数据能够直接传递到内核态,可能会被恶意利用,导致系统被攻击或数据泄露。
  3. 内存管理差异:用户空间的内存数据是分页的,并且可能不在物理内存中,而是在交换空间(swap space)或其他存储设备上。当内核试图直接访问用户空间的数据时,如果这些数据不在物理内存中,将会导致页面错误(page fault),这是内核所不允许的,并可能导致进程崩溃。
  4. 驱动程序架构和内核配置:不同的驱动程序架构或内核配置可能导致用户空间数据指针在内核模式下无效。例如,用户空间数据指针可能没有对应的虚拟地址到物理地址的映射,或者可能直接指向一些无效的内存地址。
  5. 防止内核后门:如果内核代码可以直接访问用户内存指针,这将给内核留下后门,使得用户程序可以利用这一点来访问和操作整个地址空间,从而破坏系统的安全性。

“也就是说为了内核的安全,用户空间和内核空间的数据是隔离的。用户空间传递数据给内核空间时,需要把数据复制到内核空间,而copy_from_userget_user函数可以安全的复制用户空间数据到内核。而内核空间传递数据给用户空间,同样也需要把数据复制到用户空间,copy_to_userput_user函数可以帮助复制数据到用户空间。情书对于复制还有不明白的吗?”

情书:“有的,copy_from_userget_user这俩函数有啥区别。”

“前者用于复制整个数据块,而后者用于复制单个值,比如像上面提到的int类型的cmd。同理copy_to_userput_user这俩函数的区别也在于此。”

情书:“这个我明白了,谢谢。”

“那来解答你的第二个问题,情书你还记得和你一起来到内核的小伙伴吗,你们都是被包裹在了binder_transaction_data对象中,而binder_transaction_dataBC_TRANSACTION又是被包裹在了binder_write_read对象的write_buffer属性中 (如下图) ”
image

binder驱动停顿一下接着说:“在上一节查找目标的时候,其实我已经调用了copy_from_userget_user方法把binder_write_readBC_TRANSACTIONbinder_transaction_data都已经复制到内核空间了。在Android 大话binder通信 (上)介绍过binder_transaction_data的作用,它的target.handle属性作用就是找到目标binder_node,因此它的价值已经发挥完毕是不需要复制给目标的。而binder_transaction_data的属性codeflags已经复制到内核空间了,因此就剩下情书你处于用户空间,因此只需要把你复制到内核空间。”

情书:“我明白了,可不可以这样理解,因为receiveLoveLetter这个方法只有我一个参数,如果调用方法的有很多的参数,那这些参数其实也都还处于用户空间。”

binder驱动:“你说的非常正确,点赞。那我来问你个问题,如果你作为一个binder server,如果只把codeflags、方法参数传递给你,你是否觉得足够了呢?”

情书低着头思索着这个问题,突然眼前一亮说到:“不够啊,我总得知道是谁给我发的吧。”

“你说的非常对,而这么多信息,那肯定需要一个数据结构来承载了,因此binder_transaction就诞生了,它的作用可以理解为像一个汇款单,比如在银行给某人回款的时候,回款单上要有汇款人、汇款人账号、汇款银行、收款人、收款人账号、收款银行、钱数、时间等。binder_transaction它也有发送者信息、接受者信息、方法code值、参数等这些数据,把它交给目标,目标就能从这些属性中得到想要的信息,那就结合下面的数据结构来介绍下它吧。”

struct binder_work {
	struct list_head entry;
    //定义了各种枚举类型
	enum binder_work_type {
		BINDER_WORK_TRANSACTION = 1,
		BINDER_WORK_TRANSACTION_COMPLETE,
		BINDER_WORK_RETURN_ERROR,
		BINDER_WORK_NODE,
		BINDER_WORK_DEAD_BINDER,
		BINDER_WORK_DEAD_BINDER_AND_CLEAR,
		BINDER_WORK_CLEAR_DEATH_NOTIFICATION,
	} type;
};

struct binder_transaction {
	省略代码......
    
 //它的主要作用是用来标记数据类型
	struct binder_work work;
 //用于标记是哪个线程发送的请求
	struct binder_thread *from;
	struct binder_transaction *from_parent;
 //它的主要作用是用来标记处理请求的binder_proc
	struct binder_proc *to_proc;
 //它的主要作用是用来标记处理请求的binder_thread
	struct binder_thread *to_thread;
	struct binder_transaction *to_parent;
 //为1则是代表请求需要回复
	unsigned need_reply:1;
	/* unsigned is_dead:1; */	/* not used at the moment */
    
 //请求的参数、binder_node都是存放在这
	struct binder_buffer *buffer;
 //请求方法对应的code值
	unsigned int	code;
	unsigned int	flags;
	省略代码......
};

复制

binder驱动:“我会初始化binder_transaction对象,情书你还记得发送你是在哪个线程吗?”

情书有些不确定的回答到:“好像是主线程吧。”

“没关系,我已经把矮挫丑进程调用你的线程的信息封装到binder_thread对象,而它就作为binder_transaction对象的from属性的值,而在上一节查找目标的时候已经查找到了binder_node存放在binder_transaction对象的buffer->target_node属性,而binder_transaction对象的codeflags值也都指向了TRANSACTION_receiveLoveLetterflags,那就该把情书复制到内核空间了。”

一次复制

因为方法参数只有情书,因此调用copy_from_user方法从binder_transaction_data.data.ptr.buffer复制到binder_transaction对象的buffer属性,而该属性的值是在一块共享内存上,这也就是binder通信一次复制的体现,只要把方法参数从用户空间复制到binder_transaction对象的buffer属性,则在从内核空间进入用户空间时,用户空间就可以直接使用方法参数了。

情书:“那我问个问题,这片共享内存是啥时候打开的?”

binder驱动:“像java进程,在zygote进程fork子进程成功后,就会打开binder驱动,打开binder驱动后会接着调用mmap方法,进而调用到binder驱动的binder_mmap,而该方法中会打开一片匿名共享内存,这样对应的binder_proc就可以与对应用户空间进程共享一片内存了。而对于native层需要binder通信的进程,也是和上面步骤一样打开binder驱动,调用mmap方法进行匿名共享内存。”

参数转换

因为当前方法只有情书这一个参数,假如方法的参数中有BinderProxy或者Binder或者ParcelFileDescriptor (文件描述符java层对象)这三种类型的参数,则会对它们进行转换 ,比如调用startActivity方法时候,Intent参数里面放入上面这几个参数,则在该环节会对这几个特殊类型的参数进行转换,转换规则如下:

而对于ParcelFileDescriptor的转换,需要在目标进程根据ParcelFileDescriptor对应的fd来生成自己进程对应的fd。

以上就是针对参数是这三种类型的转换,大家可以想想,如果不转化,会有啥问题?比如不对ParcelFileDescriptor进行转换,若目标进程与调用进程不是同一进程,则目标进程会使用调用进程的fd,在目标进程肯定不能使用的,这时候肯定会出错。

交给目标

binder_transaction对象内的数据都已经准备好了,万事俱备只欠交给目标了,那如何交给目标呢?

先来回顾下如果线程之间要进行通信的话方法有很多,其中就有共享内存,其实现例子如下:
其中一个A线程拥有一个todo队列,A线程会检测todo队列是否有数据,没有的话则会调用wait方法进入等待状态,有的话则会从todo队列拿出数据进行处理,进而再次检测todo队列。
而其他线程往A线程的todo队列放入数据后,需要把A线程唤醒,这样A线程就检测到todo队列有数据了,进而处理它。

恰恰在binder驱动层,binder_procbinder_thread与其他binder_procbinder_thread进行通信和上面线程之间实现通信的方法一样,也使用了共享内存。对于参数为mDriverFDBINDER_WRITE_READbinder_write_read引用的ioctl函数调用,binder驱动层把用户空间传递的数据处理完毕后 (binder_write_read对象的write_xxx属性是用户空间传递给binder驱动的),会进入等待状态,这也导致用户空间处于等待状态。

而其他内核空间线程生产的数据 (如binder_transaction),要想交给目标binder_proc的话,需要先从binder_proc中选取空闲的binder_thread,若存在则把数据放到binder_thread的类型为list_headtodo的队列中,否则把数据放到binder_proc的类型为list_headtodo的队列中,进而在把对应的内核态线程唤醒。

因此把binder_transaction对象交给白富美目标的话,需要先从白富美binder_proc中选取空闲的binder_thread,如果找到则把binder_transaction对象的类型为binder_workwork属性放入binder_thread的todo队列中,否则放入binder_proc的todo队列中,最后在把对应的内核线程唤醒即可。

等待恢复数据

binder驱动情书说:“还有最重要的一点,在矮挫丑进程中调用你的线程在binder驱动层进入等待状态,它会等待白富美的回复,如果白富美的回复到达binder驱动层,我也同样会把回复数据及其他相关数据封装到一个binder_transaction对象中,同时把它放入等待状态线程对应的binder_threadtodo队列中,并且把等待线程唤醒,这样等待线程就会把回复数据复制到用户空间,进而用户空间就能收到回复数据。”

激活目标

白富美对应的内核态线程被唤醒后,会从该线程对应的binder_threadbinder_proctodo队列中把类型为binder_workwork取出来,并且把它转换为binder_transaction对象。根据binder_transaction对象的属性构造binder_transaction_data对象 (如下图)

image

这时候binder_transaction_data对象的codeTRANSACTION_receiveLoveLettercookietarget.ptr分别指向目标binder_node的对应属性,data属性中则存放了情书。

为了让回复数据能正确的回复,被唤醒的内核态线程对应的binder_threadtransaction_stack属性会把binder_transaction对象保存起来,这样这个内核态线程再次收到用户空间发送的回复数据后,保存的binder_transaction对象可就起非常大的作用。

还记得在Android 大话binder通信 (上)介绍过用户空间发送给binder驱动的cmd是以BC开头的,而binder驱动返回给用户空间的cmd是以BR开头的,因此会把BR_TRANSACTION cmd使用put_user方法把它从内核空间复制到用户空间的,会调用copy_to_user方法把binder_transaction_data对象从内核空间复制到用户空间。

情书:“binder通信不是一直在强调,一次通信只需要一次拷贝吗?但是我发现不是这样的,我记得我和我的小伙伴是使用copy_from_user方法把我们从用户空间拷贝到了内核空间,而现在使用copy_to_user方法把我们从内核空间拷贝到用户空间,难道你们binder通信是虚假宣传吗?”

binder驱动:“我个人觉得binder通信的一次通信只需要一次拷贝,这里的拷贝指的是方法参数的拷贝,而对于像codeflagscmd这些值它们是两次拷贝,这些值非常的小所以即使两次拷贝也并无大碍,而整个binder通信中方法参数才是大头 (当然如果参数非常简单就另说了),因此只需要保证方法参数只拷贝一次就可以加快整个binder通信的效率。”

情书:“我还是不明白,调用copy_to_user方法把binder_transaction_data对象从内核空间复制到用户空间,而方法参数是在binder_transaction_data对象内的,那这时候不是又拷贝了吗?”

binder驱动:“哈哈,我明白你的困惑点了,方法参数是在binder_transaction_data对象的data.ptr.buffer属性中,而它是一个指向共享内存的地址,调用copy_to_user方法只是把该地址拷贝了一下,方法参数内容是没有拷贝的。”

情书:“恍然大悟,我明白了,谢谢你的指导。”

binder驱动有些伤感的对情书说:“我把binder_transaction_dataBR_TRANSACTION从内核空间复制到用户空间,内核空间的工作就完毕了,对应的线程会从内核态进入用户态。同时你在binder驱动的行程就结束了,咱们就此告别吧。”

情书:“我也非常的不舍,谢谢你一路上对我的关照,后会有期。”

还是老规矩,用一张图来展示情书传递过程中方法和参数的变化

image

情书送达

情书自言自语到:“虽然离开内核心情确实有些伤感,但是好在我离目的地越来越近了,因此心情不好的时候,可以换个角度考虑问题,那样心情会不一样啊。”

突然一个声音的出现吵醒了我:“情书你好啊,欢迎来到白富美进程,来到这就先由我来接待你吧,我的名字叫IPCThreadState。”

情书惊诧的看着IPCThreadState说:“我记得你啊,离开矮挫丑进程的时候还是你送的我。”

IPCThreadState:“你搞混了吧,每个进程都有自己的IPCThreadState,我可是第一次见你哦。刚刚我的一个binder线程告诉我从binder驱动发送上来了一些数据,这个binder线程获取到cmd为BR_TRANSACTION,因此它根据这个cmd,就把binder_transaction_data对象读取出来了,并且调用ParcelipcSetDataReference方法把binder_transaction_data对象中的data相关的信息放入Parcel中,这样方法的参数就可以从Parce对象中获取到了。”

情书急忙插了一句:“也就是说我现在是在Parcel对象中吧。”

“是的,并且这个binder线程还发现binder_transaction_data对象的target.ptr属性是有值的,因此它非常坚信这一定是在调用某个BBinder对象的transact方法。因此在和我商量后,决定把binder_transaction_data对象的cookie属性转换为BBinder对象,并且调用它的transact方法,把binder_transaction_data对象的code属性、Parcel对象、类型为Parcel的reply、binder_transaction_data对象的flags属性传递给BBinder。”

IPCThreadState情书说:“这么快就要和你告别了,我就把你和你的小伙伴交给了BBinder,还有当白富美给了回复后,刚刚解析你们的binder线程会把回复发送到binder驱动。”

老规矩,用一张图来展示情书传递过程中方法和参数的变化

image

BBinder

BBinder情书说:“欢迎你来到我这,你可是要去往白富美BeautifulGirl。”

情书:“你好,我是一封情书,你咋知道我要去往那里呢?”

BBinder自信的答到:“哈哈,首先我也是在白富美进程;其次白富美进程也只存在BeautifulGirl这样一个binder server,并且它是java层的;最后这段时间可是有非常非常多的情书从别的进程发送过来。因此我可以断定你一定是去往BeautifulGirl的。”

情书:“你的推断非常的正确,那还需要你帮我下,如何才能到达目的地。我先提前谢谢你了。”

BBinder:“因为BeautifulGirl是一个java层的binder server,我会通过onTransact方法,把情书和你的小伙伴交给JavaBBinder,它是我的子类,它丰富了我的onTransact方法。”

JavaBBinder

JavaBBinder情书说:“欢迎你来到我这,我这可是离目的地最近最近了,我虽然处于native层,但是我持有BeautifulGirl对象,我会把你和你的小伙伴们通过JNIEnvCallBooleanMethod方法发送到java层,在java层接待你们的是IBeautifulGirl.Stub。”

情书:“你刚刚不是说持有BeautifulGirl对象吗?为啥没有把我们发送到BeautifulGirl,而是发送到IBeautifulGirl.Stub呢?”

JavaBBinder:“啊!这个吗?你到了java层,问下IBeautifulGirl.Stub吧。”

就这样情书带着问题来到了java层。

老规矩,用一张图来展示情书传递过程中方法和参数的变化

image

IBeautifulGirl.Stub

IBeautifulGirl.Stub情书说:“亲爱的欢迎来到java层,”

还没等IBeautifulGirl.Stub说完话呢,情书问到:“你好,我能问下,为啥我没有直接到达BeautifulGirl,而是先到您这呢?”

“是这样的,BeautifulGirl它是我的子类,而JavaBBinder是通过调用我的onTransact方法把code、类型为Parceldata、类型为Parcelreplyflags带到了我这,而我的子类BeautifulGirl是没有重写该方法的,因此你们需要先到我这,不过先别急,我先看看和你一起到达的小伙伴code,它的值是TRANSACTION_receiveLoveLetter,那我就明白了,你们是要去往BeautifulGirlreceiveLoveLetter方法,对了该方法只需要一个参数就是字符串的情书,那我就调用datareadString方法把情书你的内容解析出来,好了现在我就调用BeautifulGirlreceiveLoveLetter方法只把情书交给BeautifulGirl,你的小伙伴reply会把白富美的回复保存起来交给它的上级,而另外的小伙伴code根据它已经找到了要调用的方法,因此它的价值也发挥完毕了。”

“恭喜你情书到达目的地,也祝福你能有一个好的结果,确实有很多很多的进程发送情书过来,都被白富美拒绝了,说实话我也搞不懂她的标准是啥,咱们就此告别吧,我可等着白富美的回复呢。”

老规矩,用一张图来展示情书传递过程中方法和参数的变化

image

BeautifulGirl

BeautifulGirl:“你好啊,欢迎来到情书大世界,那我就把你交给白富美吧。”

情书终于见到了白富美,它的心情非常的激动,但是发现高富美和它想象的完全不一样,它认为的白富美应该是端庄大气、温文尔雅、穿着得体,而见到的白富美却是一副高高在上、时尚潮流的感觉,一点也不接地气。

白富美情书说:“你好啊,来我这的情书可是千千万,要想赢得我的芳心,先来朗读下你的内容吧。”

情书心想为了我的主人矮挫丑,我一定要用最优美的声音把情书朗读给白富美听。

亲爱的 白富美

在这个星光璀璨的夜晚,我提笔写下这封情书,只为向你倾诉我内心深处的情感。你的出现,如同晨曦中的第一缕阳光,温暖而耀眼,让我为之倾心。

你是一位才华横溢的女子,你的智慧与美丽并存,让我为之倾倒。你的眼神中闪烁着智慧的光芒,每一次与你交谈,我都能感受到你独特的见解和深邃的思考。你的笑容如春风拂面,总能在我心中激起层层涟漪。

我欣赏你的才华,更被你的温柔与善良所吸引。你总是那么细心地照顾着周围的人,用你的善良和爱心温暖着每一个人。你的存在,让我的世界变得更加美好。

我喜欢和你一起探索生活的奥秘,喜欢与你一起分享彼此的喜怒哀乐。每一次与你相处,我都感到无比幸福和满足。你的话语总能让我茅塞顿开,你的笑容总能让我忘却烦恼。

亲爱的,我愿意为你付出一切,只希望你能感受到我深深的爱意。你的存在是我生命中最美好的礼物,我会珍惜你,爱护你,直到永远。

愿我们的爱情如同这繁星点点的夜空,璀璨而美丽,愿我们的未来如同这无尽的宇宙,广阔而深邃。

仰慕你的矮挫丑

白富美听了情书的朗读,内心有了些许的感动,这真的是她收到的情书中最有文采、最发自内心的,同时情书也朗读的非常的有感情不像别的情书一点情感都没有。

她突然想到一个问题:“情书你刚刚说你的主人叫啥名字?”

情书有些不好意思,低声的说:“矮挫丑”

白富美:“啊!咋能叫这样的名字呢,不行我得调用BindergetCallingPid方法来验证下。果不其然,情书的主人果然是矮挫丑。”

白富美深深的陷入了犹豫中,她不知道该咋办了,虽然矮挫丑的情书是最棒的,但是他的长相却令她难以接受,最终还是她的感性战胜了理性,她选择了拒绝。

下面这种图,展示了情书从矮挫丑进程到达白富美进程所经历的方法和参数的变化
image

矮挫丑

白富美的回复信息被IPCThreadState的处于等待状态的binder线程发送到了binder驱动,经过binder驱动,回复信息最终到达了矮挫丑

矮挫丑收到白富美的回复信息是拒绝,但是矮挫丑却没有一丝的伤感、气馁,他坚信自己是不会放弃的,还要继续给白富美发送情书,直到她同意为止。

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1883400.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机械原理介绍

机械原理介绍 1 介绍1.1 概述1.2 资料书籍在线资料 2 [机械原理知识整理](https://tomm.muzing.top/) 【muzing整理编写】1 绪论2 机构的结构分析2-2 机构的组成及分类2-3 机构运动简图2-4 机构具有确定运动的条件及最小阻力定律2-5 2-6 机构自由度的计算2-7 平面机构的组成原理…

【深度学习】图生图img3img论文原理,SD EDIT

https://arxiv.org/abs/2108.01073 摘要 引导图像合成技术使普通用户能够以最小的努力创建和编辑逼真的图像。关键挑战在于平衡对用户输入&#xff08;例如&#xff0c;手绘的彩色笔画&#xff09;的忠实度和合成图像的真实感。现有的基于GAN的方法试图通过使用条件GAN或GAN反…

64.WEB渗透测试-信息收集- WAF、框架组件识别(4)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;63.WEB渗透测试-信息收集- WAF、框架组件识别&#xff08;3&#xff09;-CSDN博客 我们在…

面经-计算机基础

1.计算机⽹络 1.1 OSI与TCP/IP各层的结构与功能,都有哪些协议? 计算机网络体系结构 应⽤层 应⽤层 (application-layer &#xff09;的任务是通过应⽤进程间的交互来完成特定⽹络应⽤。 应⽤层协议定 义的是应⽤进程&#xff08;进程&#xff1a;主机中正在运⾏的程序&…

便签 Pro(Mac 智能便签工具)专业版怎么样,值得购买吗?

使用 Mac 的小伙伴平时都是怎么记录工作生活中的碎片信息&#xff1f;用聊天软件&#xff0c;还是系统备忘录呢&#xff1f; 实际体验下来&#xff0c;其实都难以称得上好用。 赶紧来了解一下 Mac 多彩思维速记工具便签 Pro&#xff01;拥有智能边框大小、iCloud 同步、历史记…

昇思25天学习打卡营第1天|MindSpore 全流程操作指南

目录 MindSpore 库相关操作的导入指南 处理数据集 网络构建 模型训练 保存模型 加载模型 MindSpore 库相关操作的导入指南 首先&#xff0c;我们导入了 MindSpore 这个库的整个模块。然后&#xff0c;从 MindSpore 库中引入了 nn 模块&#xff0c;一般来说&#xff0c;它是…

JavaEE—什么是服务器?以及Tomcat安装到如何集成到IDEA中?

目录 ▐ 前言 ▐ JavaEE是指什么? ▐ 什么是服务器&#xff1f; ▐ Tomcat安装教程 * 修改服务端口号 ▐ 将Tomcat集成到IDEA中 ▐ 测试 ▐ 结语 ▐ 前言 至此&#xff0c;这半年来我已经完成了JavaSE&#xff0c;Mysql数据库&#xff0c;以及Web前端知识的学习了&am…

ROS2在rviz2中实时显示轨迹和点

本文是将《ROS在rviz中实时显示轨迹和点》博客中rviz轨迹显示转为ROS2环境中的rviz2显示。 ros2的工作空间创建这里就不展示了。 包的创建 ros2 pkg create --build-type ament_cmake showpath --dependencies rclcpp nav_msgs geometry_msgs tf2_geometry_msgsshowpath.cpp…

【微服务网关——Websocket代理】

1.Websocket协议与原理 1.1 连接建立协议 1.1.1 客户端发起连接请求 客户端通过 HTTP 请求发起 WebSocket 连接。以下是一个 WebSocket 握手请求的例子&#xff1a; GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key…

python 中的 下划线_ 是啥意思

在 Python 中&#xff0c;_&#xff08;下划线&#xff09;通常用作占位符&#xff0c;表示一个变量名&#xff0c;但程序中不会实际使用这个变量的值。 目录 忽略循环变量&#xff1a;忽略函数返回值&#xff1a;在解释器中使用&#xff1a;举例子1. 忽略循环变量2. 忽略不需…

APP逆向 day8 JAVA基础3

一.前言 昨天我们讲了点java基础2.0&#xff0c;发现是又臭又长&#xff0c;今天就是java基础的最后一章&#xff0c;也就是最难的&#xff0c;面向对象。上一末尾也是提到了面向对象&#xff0c;但是面向对象是最重要的&#xff0c;怎么可能只有这么短呢&#xff1f;所以今天…

怎样将word默认Microsoft Office,而不是WPS

设置——>应用——>默认应用——>选择"word"——>将doc和docx都选择Microsoft Word即可

【嵌入式DIY实例】- LCD ST7735显示DHT11传感器数据

LCD ST7735显示DHT11传感器数据 文章目录 LCD ST7735显示DHT11传感器数据1、硬件准备与接线2、代码实现本文介绍如何将 ESP8266 NodeMCU 板 (ESP-12E) 与 DHT11 (RHT01) 数字湿度和温度传感器连接。 NodeMCU 从 DHT11 传感器读取温度(以 C 为单位)和湿度(以 rH% 为单位)值,…

连锁品牌如何做宣传?短视频矩阵工具助轻松千万流量曝光!

今天给大家分享一家烘焙行业连锁品牌&#xff08;可可同学&#xff09;&#xff0c;通过小魔推获得了1031万的话题曝光&#xff0c;旗下的连锁门店登顶同城人气榜单第一名&#xff0c;​让自己的流量和销量获得双增长 01 品牌连锁店如何赋能旗下门店&#xff1f; 作为一家全国…

昇思25天学习打卡营第13天|基于MobileNetV2的垃圾分类

MobileNetv2模型原理介绍 相比于传统的卷积神经网络&#xff0c;MobileNet网络使用深度可分离卷积&#xff08;Depthwise Separable Convolution&#xff09;的思想在准确率小幅度降低的前提下&#xff0c;大大减小了模型参数与运算量。并引入宽度系数α和分辨率系数β使模型满…

【面试题】IPS(入侵防御系统)和IDS(入侵检测系统)的区别

IPS&#xff08;入侵防御系统&#xff09;和IDS&#xff08;入侵检测系统&#xff09;在网络安全领域扮演着不同的角色&#xff0c;它们之间的主要区别可以归纳如下&#xff1a; 功能差异&#xff1a; IPS&#xff1a;这是一种主动防护设备&#xff0c;不仅具备检测攻击的能力&…

利用pyecharts制作2023全国GDP分布图

完整代码&#xff1a; from pyecharts import options as opts from pyecharts.charts import Map import pandas as pddf pd.read_excel(各省份GDP.xlsx) # print(df.head())year 2023 info df[[省份,year]] # print(info)info_list info.values.tolist() print(info_lis…

YTM32的HA系列微控制器启动过程详解

YTM32的HA系列微控制器启动过程详解 文章目录 YTM32的HA系列微控制器启动过程详解IntroductionPricinple & MachenismHA01的内存地址空间BOOT ROM简介安全启动Security Boot快速从Powerdown模式下唤醒对内核进行例行自检&#xff08;Structural Core Self-Test&#xff0c;…

Python容器 之 字符串--下标和切片

1.下标&#xff08;索引&#xff09; 一次获取容器中的一个数据 1, 下标(索引), 是数据在容器(字符串, 列表, 元组)中的位置, 编号 2, 一般来说,使用的是正数下标, 从 0 开始 3, 作用: 可以通过下标来获取具体位置的数据. 4, 语法&#xff1a; 容器[下标] 5, Python 中是支持…