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了。”

预告

要想知道白富美是否接受了矮挫丑,请看下篇分解。

请添加图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

触控MCU芯片(1):英飞凌PSoC第6代第7代

前言: 说到触摸MCU芯片,这个历史也是很久了,比如日常经常接触到的洗衣机、电冰箱、小家电,隔着一层玻璃,轻轻一按就能识别按键,感觉比过去纯机械式的按键更高级更美观,不仅白电,现在很多汽车也都在进行触摸按键的改版,不再使用笨重的机械按键,比如空调调温按键、档位…

淘宝评论电商API接口,揭示用户真实评价

随着互联网的快速发展&#xff0c;电子商务已经成为了人们生活中不可或缺的一部分。淘宝作为中国最大的在线购物平台&#xff0c;拥有数以亿计的消费者和商家。而用户评价作为消费者了解商品和服务的重要途径&#xff0c;对于商家的信誉和销售有着至关重要的影响。因此&#xf…

SCADA软件地毯式介绍,你想知道的都在这里.

很多小伙伴对SCADA很陌生&#xff0c;殊不知这个可是智慧工业制造的大脑和中枢神经&#xff0c;很多指令的发出&#xff0c;监控状态的现实都得通过这个系统&#xff0c;本文详解介绍一下什么是SCADA&#xff0c;重大作用&#xff0c;其在工业制造中的位置&#xff0c;以及市面…

Export S parameter sweep data 导出 S 参数扫描代码

Export S parameter sweep data 导出 S 参数扫描代码 正文正文 相信有不少小伙伴们会比较苦恼一件事情,就是 Lumerical Script 中的绘图并不智能。功能较为简陋以至于图像展现时不够美观,因此,很多时候我们需要将仿真数据导出使用。那么如何导出仿真数据呢?在 Lumerical S…

【Linux进程通信】Linux进程间的无声对话:匿名管道与命名管道技术

W...Y的主页 &#x1f60a; 代码仓库分享 &#x1f495; 前言&#xff1a;我们已经知道了进程和文件的基本理论&#xff0c;知道了进程和文件的重要性。进程具有独立性&#xff0c;所以两个进程不能直接通信&#xff0c;那么进程间应该怎样通信呢&#xff1f;我们今天来解开其…

物联网技术-第4章物联网通信技术-4.1计算机网络

目录 1.1计算机网络拓扑与组成 &#xff08;1&#xff09;全连通式网络 &#xff08;2&#xff09;星型网 &#xff08;3&#xff09;环形网 &#xff08;4&#xff09;总线网 &#xff08;5&#xff09;不规则型网 1.2数据交换类型 &#xff08;1&#xff09;电路交换网 &…

硬件开发笔记(十八):核心板与底板之间的连接方式介绍说明:板对板连接器

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/139663096 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

DSP28335:独立按键控制LED灯

做任何事情不可操之过急&#xff0c;虽然我们可能在之前的单片机学过相关的原理&#xff0c;但是一个新的单片机依然有他的学习的地方&#xff0c;之前我觉得很简单&#xff0c;就跳过这个学习&#xff0c;结果到后面就很浮躁&#xff0c;导致后面的内容与这一章相连接的时候&a…

利用这个css属性,你也能轻松实现一个新手引导库

相信大家或多或少都在各种网站上使用过新手引导&#xff0c;当网站提供的功能有点复杂时&#xff0c;这是一个对新手非常友好的功能&#xff0c;可以跟随新手引导一步一步了解网站的各种功能&#xff0c;我们要做的只是点击下一步或者上一步&#xff0c;网站就能滚动到指定位置…

齐普夫定律在循环神经网络中的语言模型的应用

目录 齐普夫定律解释公式解释图与公式的关系代码与图的分析结论 使用对数表达方式的原因1. 线性化非线性关系2. 方便数据可视化和分析3. 降低数值范围4. 方便参数估计公式详细解释结论 来自&#xff1a;https://zh-v2.d2l.ai/chapter_recurrent-neural-networks/language-model…

智慧校园发展趋势:2024年及未来教育科技展望

展望2024年及未来的教育科技领域&#xff0c;智慧校园的发展正引领着一场教育模式的深刻变革&#xff0c;其核心在于更深层次地融合技术与教育实践。随着人工智能技术的不断成熟&#xff0c;个性化学习将不再停留于表面&#xff0c;而是深入到每个学生的个性化需求之中。通过精…

电感的本质是什么

什么是电感&#xff1f; 电感器件一般是指螺线圈&#xff0c;由导线圈一圈靠一圈地绕在绝缘管上&#xff0c;绝缘管可以是空心的&#xff0c;也可以包含铁芯或磁粉芯。 为什么把’线’绕成’圈’就是电感&#xff1f; 电感的工作原理非常抽象&#xff0c;为了解释什么是电感…

单片机 PWM输入捕获【学习记录】

前言 学习是永无止境的&#xff0c;就算之前学过的东西再次学习一遍也能狗学习到很多东西&#xff0c;输入捕获很早之前就用过了&#xff0c;但是仅仅是照搬例程没有去进行理解。温故而知新&#xff01; 定时器 定时器简介 定时器的分类 高级定时器 通用定时器 基本定时器…

Facebook与地方文化:数字平台的多元表达

在当今数字化时代&#xff0c;社交媒体不仅仅是人们交流的工具&#xff0c;更是促进地方文化传播和表达的重要平台。作为全球最大的社交网络之一&#xff0c;Facebook在连接世界各地用户的同时&#xff0c;也成为了地方文化多元表达的重要舞台。本文将深入探讨Facebook如何通过…

LabVIEW的热门应用

LabVIEW是一种图形化编程语言&#xff0c;因其易用性和强大的功能&#xff0c;在多个行业和领域中广泛应用。介绍LabVIEW在以下五个热门应用领域中的使用情况&#xff0c;&#xff1a;工业自动化、医疗设备与生物医学工程、科学研究与实验室自动化、能源管理与智能电网、航空航…

streamlit markdown里支持latex公式显示

参考&#xff1a; https://docs.streamlit.io/develop/api-reference/write-magic/st.write https://discuss.streamlit.io/t/streamlit-markdown-a-streaming-markdown-component-with-latex-mermaid-table-code-support/72187 也有独立支持的st.latex 接口单独显示公司&…

LeetCode347:前K个高频元素

题目描述 给你一个整数数组 nums 和一个整数 k &#xff0c;请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。 解题思想 使用优先队列 priority_queue<Type, Container, Functional> Type 就是数据类型&#xff0c;Container 就是容器类型&#xff08;C…

Next14的appRouter模式中使用状态管理React-Redux

安装依赖 npm install reduxjs/toolkit react-redux创建store模块 创建 app/store/counterSlice.js文件 "use client"// redux需要作为客户端渲染的模块import { createSlice } from "reduxjs/toolkit"export const counterSlice createSlice({name: &…

解读自然语言处理:技术、应用与未来展望

引言 自然语言处理&#xff08;Natural Language Processing&#xff0c;简称NLP&#xff09;是计算机科学、人工智能和语言学的一个跨学科领域&#xff0c;致力于实现人与计算机之间通过自然语言进行有效沟通的能力。NLP 的核心任务是理解、解释和生成人类语言&#xff0c;使计…

AI早班2024.6.18

先一步知道AI未来&#xff01; 全球AI新闻速递 1.绿米 AI 智能存在传感器 FP1E开售。 2.摩尔线程 师者AI&#xff1a;完成70亿参数教育AI大模型训练测试。 3.Google 在 AI 功能推出新功能&#xff0c;需要明确说明可能出错的地方。 4.北大、快手攻克复杂视频生成难题&#…