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);}@Overridepublic 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;}@Overridepublic 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是BinderProxyprivate android.os.IBinder mRemote;Proxy(android.os.IBinder remote) {mRemote = remote;}@Overridepublic 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
{省略代码......//转dataParcel* data = parcelForJavaObject(env, dataObj);if (data == NULL) {return JNI_FALSE;}//转replyParcel* reply = parcelForJavaObject(env, replyObj);if (reply == NULL && replyObj != NULL) {return JNI_FALSE;}//根据BinderProxy的mNativeData获取到对应的BpBinder,这时候target就是BpBinderIBinder* 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是0tr.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是0tr.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();} 省略代码......//写入cmdmOut.writeInt32(cmd);//写入binder_transaction_datamOut.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;elseerr = -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;//不断循序,除非遇到breakwhile (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 cmdcase 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_threadstruct rb_root threads;//使用红黑树存储所有的binder_nodestruct 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_procstruct binder_proc *proc;struct rb_node rb_node;struct list_head waiting_thread_node;//线程idint 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_procstruct binder_proc *proc;//所有的binder_ref省略属性......//下面两个属性都指向用户空间的BBinderbinder_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_datastruct binder_ref_data data;省略属性......//指向binder_procstruct binder_proc *proc;//指向binder_nodestruct 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了。”

预告

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

请添加图片描述

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

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

相关文章

▶《强化学习的数学原理》(2024春)_西湖大学赵世钰 Ch2 贝尔曼公式

PPT 截取有用信息。 课程网站做习题。总体 MOOC 过一遍 1、学堂在线 视频 习题 2、相应章节 过电子书 复习 GitHub界面链接 3、总体 MOOC 过一遍 学堂在线 课程页面链接 中国大学MOOC 课程页面链接 B 站 视频链接 PPT和书籍下载网址&#xff1a; 【github链接】 文章目录 计算…

泽众云真机-平台即将升级支持华为机型HarmonyOS NEXT系统

具小编了解&#xff0c;泽众云真机即将升级支持华为机型HarmonyOS NEXT系统。有些人可能对HarmonyOS NEXT系统了解不多。 之前我们有个银行项目&#xff0c;客户要求测试华为HarmonyOS NEXT系统环境&#xff0c;当时我们云真机尚未有该系统的机型&#xff0c;然后技术人员向华为…

护眼必看!台灯怎么选对眼睛好的方法

家长们是否和我一样发现孩子时常作出眯眼的行为&#xff01;那就要小心了&#xff01;最近我注意到家中的孩子开始表现出眯眼的习惯。经过仔细观察后发现&#xff0c;这可能与她长时间晚上熬夜写作业导致的光线不足有关。随着孩子学习负担的增加&#xff0c;我作为家长开始担心…

ubuntu下同时安装和使用不同版本的库 librealsense

apt 安装的最新版本在/usr 源码安装的旧版本在/usr/local set(realsense2_DIR /usr/local/) find_package(realsense2 2.50.0 REQUIRED) message( "\n\n ${realsense2_INCLUDE_DIR} ${realsense2_VERSION} RealSense SDK 2.0 is FINDINGING, please install it from…

英伟达和IBM搞事情!主攻“量子计算+AI”

内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 文丨娴睿/慕一 排版丨沛贤 深度好文&#xff1a;2000字丨8分钟阅读 Ismael Faro是一位计算机工程师&#xff0c;自2015年以来&#xff0c;他就成为开发IBM量子软件生态系统的重要人物。从2016…

【Linux】rouyiVue 项目部署全过程(含MySQL,Nginx等中间件部署)

查看nginx 进程命令 ps aux | grep server_name 1. 安装MySQL 1.1 下载压缩包 官网下载 1.2 解压 上传并解压好放在指定位置 创建soft文件夹 mkdir /soft上传文件&#xff0c;在该目录下再创建一个mysql文件夹&#xff0c;将安装包解压到新文件夹中 mkdir /soft/mysql-…

【odoo】常用的字符转义:“>“,“<“,““,“/“等

概要 字符转义是指在编写代码或处理文本数据时&#xff0c;将特殊字符转换为另一种形式&#xff0c;以便在特定的上下文中正确解析和处理这些字符。 内容 特殊字符描述XML转义表示法&和符号&amp;<小于符号<>大于符号>"双引号&quot;单引号&ap…

CentOS7 部署安装ClickHouse

一、什么是ClickHouse ClickHouse 是俄罗斯的Yandex于2016年开源的列式存储数据库&#xff08;DBMS&#xff09;&#xff0c;使用C语言编写&#xff0c;主要用于在线分析处理查询&#xff08;OLAP&#xff09;&#xff0c;能够使用SQL查询实时生成分析数据报告。 OLAP场景需要…

Laravel - excel 导入数据

在Laravel中&#xff0c;可以使用maatwebsite/excel这个库来处理Excel文件的导入。 1.用命令行窗口打开项目根目录&#xff0c;使用 Composer 安装 maatwebsite/excel composer require maatwebsite/excel --ignore-platform-reqs 在你的config/app.php文件中注册服务提供者&…

何在 Vue3 中使用 Cytoscape 创建交互式网络图

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 Vue.js 中加载 Cytoscape.js 的技术实现 应用场景 Cytoscape.js 是一个用于创建交互式网络的可视化库。在生物信息学、社会网络分析和药物发现等领域中得到了广泛应用。 基本功能 本代码片段演示了如何在 V…

二叉树的这五种遍历方法你们都会了吗?

说在前面 &#x1f388;二叉树大家应该都很熟了吧&#xff0c;那二叉树的这五种遍历方式你们都会了吗&#xff1f; 以这一二叉树为例子&#xff0c;我们来看看不同遍历方式返回的结果都是怎样的。 前序遍历 前序遍历的顺序是&#xff1a;首先访问根节点&#xff0c;然后递归地…

使用 jQuery 选择器获取页面元素,然后利用 jQuery 对象的 css() 方法设置其 display 样式属性,从而实现显示和隐藏效果。

在页面中显示电影排行榜 当单击“&#xff08;收起&#xff09;”链接时&#xff0c;排行榜中后三项的电影名称隐藏而且链接的文本更改为“&#xff08;展开&#xff09; ” 当单击“&#xff08;展开&#xff09;”的链接时&#xff0c;后三项的电影名称重新显示且链接的文本…

视频剪辑技巧大揭秘:轻松掌握为视频添加梦幻光晕效果的绝妙方法!

在这个视觉盛宴的时代&#xff0c;每一个画面都渴望被赋予独特的魅力与魔法。今天&#xff0c;我要为你揭秘一个神秘的视频剪辑技巧——给视频添加光晕效果&#xff0c;让你的作品瞬间脱颖而出&#xff0c;成为朋友圈的焦点 首先&#xff0c;你可以打开原视频进行查看。此时&am…

QT自定义标题栏窗口其一:实现拖动及可拉伸效果

1、效果 2、核心代码 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(paren

高速公路声光预警定向广播助力安全出行

近年来&#xff0c;高速重大交通事故屡见不鲜&#xff0c;安全管控一直是高速运营的重中之重。如何利用现代化技术和信息化手段&#xff0c;创新、智能、高效的压降交通事故的发生概率&#xff0c;优化交通安全管控质量&#xff0c;是近年来交管部门的主要工作&#xff0c;也是…

将强化学习重新引入 RLHF

我们很高兴在 TRL 中介绍 RLOO (REINFORCE Leave One-Out) 训练器。作为一种替代 PPO 的方法&#xff0c;RLOO 是一种新的在线 RLHF 训练算法&#xff0c;旨在使其更易于访问和实施。特别是&#xff0c; RLOO 需要的 GPU 内存更少&#xff0c;并且达到收敛所需的挂钟时间也更短…

前端模糊搜索关键字高亮

效果 代码 <template><view class"flexStart new-box"><view class"company"><!-- 输入框样式 --><view class"spaceBetween companyName" click.stop"isCompany true"><input type"text&quo…

C盘满了怎么清理?一招让你远离C盘空间不足的烦恼

C盘满了怎么清理&#xff1f;一招让你远离C盘空间不足的烦恼&#xff0c;当C盘空间满了时&#xff0c;会给我们来一系列烦恼和潜在问题。比如&#xff1a;系统运行缓慢、程序崩溃或无法安装、启动时间变长、系统不稳定、文件管理困难、游戏卡顿、电脑卡顿、系统故障等问题&…

Linux使用Docker部署mysql5.7

一、拉取镜像 docker pull mysql:5.7 二、查看镜像 docker images 三、创建容器 这里稍微解释下 -p参数是端口映射 -v参数是数据卷挂载 数据卷挂载是Docker中的一种功能&#xff0c;它允许将主机上的目录或文件系统与容器内的目录绑定&#xff0c;实现数据的持久化存储…

短剧片源授权,类目丰富优惠多,抢先一步更新你的短剧系统片库!

前言 如今的短剧作为一种新兴的视听艺术形式&#xff0c;正以其独特的魅力迅速占领市场高地。为了满足广大短剧爱好者和从业者的需求&#xff0c;我们提供短剧片源授权服务&#xff0c;凭借剧场独家提供的丰富片源&#xff0c;助力您轻松更新短剧系统片库&#xff0c;抢占市场…