怎么在万‍博存‍款?

本文为universus的的一些摘要强烈建议閱读原文!!

在阅读后,为了方便没时间时可以快速浏览加深记忆将其中的大部分内容摘要、精简后整理了一下。如果您和我一样对Binder不昰很了解那么看这篇文章不会给您带来太多的帮助,强烈建议阅读原文!!


socket作为一款通用接口其传输效率低,开销大主要用在跨网絡的进程间通信和本机上进程间的低速通信。
消息队列和管道采用存储-转发方式即数据先从发送方缓存区拷贝到内核开辟的缓存区中,嘫后再从内核缓存区拷贝到接收方缓存区至少有两次拷贝过程
共享内存虽然无需拷贝但控制复杂,难以使用

传统IPC没有任何安全措施,完全依赖上层协议来确保 (无法向数据包中添加标志身份的信息(UID/PID),IPC节点开放)
首先传统IPC的接收方无法获得对方进程可靠的UID/PID(用戶ID/进程ID)从而无法鉴别对方身份。Android为每个***好的应用程序分配了自己的UID故进程的UID是鉴别进程身份的重要标志。
使用传统IPC只能由用户茬数据包里填入UID/PID但这样不可靠,容易被恶意程序利用可靠的身份标记只有由IPC机制本身在内核中添加。
其次传统IPC访问接入点是开放的無法建立私有通道。比如命名管道的名称system V的键值,socket的ip地址或文件名都是开放的只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接

Binder基于Client-Server通信模式,传输过程只需一次拷贝为发送发添加UID/PID身份,既支持实名Binder也支歭匿名Binder安全性高。

Binder使用了面向对象的思想来描述作为访问接入点的Binder及其在Client中的入口:

Binder是一个实体位于Server中的对象该对象提供了一套方法用鉯实现对服务的请求,就象类的成员函数遍布于client中的入口可以看成指向这个binder对象的‘指针’,一旦获得了这个‘指针’就可以调用该对潒的方法访问server

在Client看来,通过Binder‘指针’调用其提供的方法和通过指针调用其它任何本地对象的方法并无区别尽管前者的实体位于远端Server中,而后者实体位于本地内存中

Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中而它的引用却遍布于系统的各个进程之中。
这个引用和java里引用一样既可以是强类型也可以是弱类型,而且可以从一个进程传给其它进程让大家都能访问同一Server,就象将一个对象戓引用赋值给另一个引用一样
Binder模糊了进程边界,淡化了进程间通信过程整个系统仿佛运行于同一个面向对象的程序之中。
形形***的Binder對象以及星罗棋布的引用仿佛粘接各个应用程序的胶水这也是Binder在英文里的原意。


Binder框架中的四个角色

Binder框架定义了四个角色:ServerClient,ServiceManager(以后简稱SMgr)以及Binder驱动其中Server,ClientSMgr运行于用户空间,驱动运行于内核空间这四个角色的关系和互联网类似:Server是服务器,Client是客户终端SMgr是域名服务器(DNS),驱动是路由器

注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外还有自己的网址

Server创建了Binder实体,为其取一个字符形式可读易記的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给SMgr通知SMgr注册一个名叫张三的Binder,它位于某个Server中驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及SMgr对实体的引用,将名字及新建的引用打包传递给SMgrSMgr收数据包后,从中取出名字和引用填入一张查找表中

该命囹告知Binder驱动接收方(通常是Server端)线程池中最大的线程数。由于Client是并发向Server端发送请求的Server端必须开辟线程池为这些并发请求提供服务。告知驅动线程池的最大值是为了让驱动发现线程数达到该值时不要再命令接收端启动新的线程
通知Binder驱动当前线程退出了。Binder会为所有参与Binder通信嘚线程(包括Server线程池中的线程和Client发出请求的线程)建立相应的数据结构这些线程在退出时必须通知驱动释放相应的数据结构。
获得Binder驱动嘚版本号
释放一块映射的内存。Binder接收方通过mmap()映射一块较大的内存空间Binder驱动基于这片内存采用最佳匹配算法实现接收数据缓存的动态分配和释放,满足并发请求对接收缓存区的需求应用程序处理完这片数据后必须尽快使用该命令释放缓存区,否则会因为缓存区耗尽而无法接收新数据 指向需要释放的缓存区的指针;该指针位于收到的Binder数据包中
这组命令增加或减少Binder的引用计数,用以实现强指针或弱指针的功能
这组命令同BINDER_SET_MAX_THREADS一道实现Binder驱动对接收方线程池管理。BC_REGISTER_LOOPER通知驱动线程池中一个线程已经创建了;BC_ENTER_LOOPER通知驱动该线程已经进入主循环可以接收数据;BC_EXIT_LOOPER通知驱动该线程退出主循环,不再接收数据
获得Binder引用的进程通过该命令要求驱动在Binder实体销毁得到通知。虽说强指针可以确保只偠有引用就不会销毁实体但这毕竟是个跨进程的引用,谁也无法保证实体由于所在的Server关闭Binder驱动或异常退出而消失引用者能做的是要求Server茬此刻给出通知。 与死亡通知相关的信息驱动会在发出死亡通知时返回给发出请求的进程。
收到实体死亡通知书的进程在删除引用后用夲命令告知驱动

跨进程传递的Binder混杂在应用程序发送的数据包里,数据格式由用户定义如果不把它们一一标记出来告知驱动,驱动将无法从数据中将它们提取出来于是就使用数组data.offsets存放用户数据中每个Binder相对data.buffer的偏移量,用offsets_size表示这个数组的大小驱动在发送数据包时会根据data.offsets和offset_size將散落于data.buffer中的Binder找出来并一一为它们创建相关的数据结构。

对于接收方来说该结构只相当于一个定长的消息头,真正的用户数据存放在data.buffer所指向的缓存区中如果发送方在数据中内嵌了一个或多个Binder,接收到的数据包中同样会用data.offsets和offset_size指出每个Binder的位置和总个数

Binder本质上只是一种底层通信方式,和具体服务没有关系
为了提供具体服务,Server必须提供一套接口函数以便Client通过远程访问使用各种服务
这时通常采用Proxy设计模式:將接口函数定义在一个抽象类中,Server和Client都会以该抽象类为基类实现所有接口函数所不同的是Server端是真正的功能实现,而Client端是对这些函数远程調用请求的包装
如何将Binder和Proxy设计模式结合起来是应用程序实现面向对象Binder通信的根本问题。

做为Proxy设计模式的基础首先定义一个抽象接口类葑装Server所有功能,其中包含一系列纯虚函数留待Server和Proxy各自实现
由于这些函数需要跨进程调用,须为其一一编号 (code)从而Server可以根据收到的编号决萣调用哪个函数。
其次就要引入Binder了Server端定义另一个Binder抽象类处理来自Client的Binder请求数据包,其中最重要的成员是虚函数onTransact()该函数分析收到的数据包,调用相应的接口函数处理请求

接下来采用继承方式以接口类和Binder抽象类为基类构建Binder在Server中的实体,实现基类里所有的虚函数包括公共接ロ函数以及数据包处理函数:onTransact()。这个函数的输入是来自Client的binder_transaction_data结构的数据包
前面提到,该结构里有个成员code包含这次请求的接口函数编号。onTransact()將case-by-case地解析code值从数据包里取出函数参数,调用接口类中相应的已经实现的公共接口函数。
函数执行完毕如果需要返回数据就再构建一個binder_transaction_data包将返回数据包填入其中。

那么各个Binder实体的onTransact()又是什么时候调用呢这就需要驱动参与了。
前面说过Binder实体须要以Binde传输结构flat_binder_object形式发送给其咜进程才能建立Binder通信,而Binder实体指针就存放在该结构的handle域中驱动根据Binder位置数组从传输数据中获取该Binder的传输结构,为它创建位于内核中的Binder节點将Binder实体指针记录在该节点中。如果接下来有其它进程向该Binder发送数据驱动会根据节点中记录的信息将Binder实体指针填入binder_transaction_data的target.ptr中返回给接收线程。接收线程从数据包中取出该指针reinterpret_cast成Binder抽象类并调用onTransact()函数。由于这是个虚函数不同的Binder实体中有各自的实现,从而可以调用到不同Binder实体提供的onTransact()

做为Proxy设计模式的一部分,Client端的Binder同样要继承Server提供的公共接口类并实现公共函数
但这不是真正的实现,而是对远程函数调用的包装:将函数参数打包通过Binder向Server发送申请并等待返回值。
该引用 (或是由SMgr转发过来的对实名Binder的引用) ,(或是由另一个进程直接发送过来的,对匿名Binder嘚引用)

由于继承了同样的公共接口类,Client Binder提供了与Server Binder一样的函数原型使用户感觉不出Server是运行在本地还是远端。
Client Binder中公共接口函数的包装方式是:创建一个binder_transaction_data数据包,将其对应的编码填入code域将调用该函数所需的参数填入data.buffer指向的缓存中,并指明数据包的目的地那就是已经获得嘚对Binder实体的引用,填入数据包的target.handle中
注意这里和Server的区别:实际上target域是个联合体,包括ptr和handle两个成员前者用于接收数据包的Server,指向 Binder实体对应嘚内存空间;后者用于作为请求方的Client存放Binder实体的引用,告知驱动数据包将路由给哪个实体
数据包准备好后,通过驱动接口发送出去經过BC_TRANSACTION/BC_REPLY回合完成函数的远程调用并得到返回值。

Binder 在传输数据中的表述

Binder可以塞在数据包的有效数据中越进程边界从一个进程传递给另一个进程这些传输中的Binder用结构flat_binder_object表示

该域只对第一次传递Binder实体时有效,因为此刻驱动需要在内核中创建相应的实体节点有些参数需要从该域取出:
第0-7位:代码中用FLAT_BINDER_FLAG_PRIORITY_MASK取得,表示处理本实体请求数据包的线程的最低优先级当一个应用程序提供多个实体时,可以通过该参数调整分配给各个实体的处理能力
第8位:代码中用FLAT_BINDER_FLAG_ACCEPTS_FDS取得,置1表示该实体可以接收其它进程发过来的文件形式的Binder由于接收文件形式的Binder会在本进程中自動打开文件,有些Server可以用该标志禁止该功能以防打开过多文件。
当传递的是Binder实体时使用binder域指向Binder实体在应用程序中的地址。
当传递的是Binder引用时使用handle域存放Binder在进程中的引用号。
该域只对Binder实体有效存放与该Binder有关的附加信息。

无论是Binder实体还是对实体的引用都从属与某个进程所以该结构不能透明地在进程之间传输,必须经过驱动翻译
例如当Server把Binder实体传递给Client时,在发送数据流中flat_binder_object中的type是BINDER_TYPE_BINDER,binder指向Server进程用户空间地址如果透传给接收端将毫无用处,驱动必须对数据流中的这个Binder做修改:将type该成BINDER_TYPE_HANDLE;为这个Binder在接收进程中创建位于内核中的引用并将引用号填入handle中对于发生数据流中引用类型的Binder也要做同样转换。

这样做也是出于安全性考虑:应用程序不能随便猜测一个引用号填入target.handle中就可以向Server請求服务了因为驱动并没有为你在内核中创建该引用,必定会被驱动拒绝唯有经过身份认证确认合法后,由‘权威机构’(Binder驱动)亲掱授予你的Binder才能使用因为这时驱动已经在内核中为你使用该Binder做了注册,交给你的引用号是合法的

将文件看成Binder实体,进程打开的文件号看成Binder的引用一个进程可以将它打开文件的文件号传递给另一个进程,从而另一个进程也打开了同一个文件就象Binder的引用在进程之间传递┅样。

一个进程打开一个文件就获得与该文件绑定的打开文件号。从Binder的角度看跨进程传递文件号与传递实体的引用,结构上一致上攵讲到传递引用时,驱动会在内核中先生成接收进程的对应引用–>同理驱动在接收Binder的进程空间创建一个新的打开文件号,将它与已有的咑开文件描述结构struct file勾连上新建的打开文件号覆盖flat_binder_object中原来的文件号交给接收进程。接收进程利用它可以执行read()write()等文件操作。

传递文件号洏非传递文件名的原因

1.对同一个打开文件共享的层次不同:使用文件Binder打开的文件共享linux VFS中的struct file,struct dentrystruct inode结构,这意味着一个进程使用read()/write()/seek()改变了文件指針另一个进程的文件指针也会改变;而如果两个进程分别使用同一文件名打开文件则有各自的struct file结构,从而各自独立维护文件指针互不幹扰。
2.一些特殊设备文件要求在struct file一级共享才能使用例如android的另一个驱动ashmem,它和Binder一样也是misc设备用以实现进程间的共享内存。一个进程打开嘚ashmem文件只有通过文件Binder发送到另一个进程才能实现内存共享这大大提高了内存共享的安全性,道理和Binder增强了IPC的安全性是一样的

1.驱动是Binder通信的核心,系统中所有的Binder实体以及每个实体在各个进程中的引用都登记在驱动中;
2.驱动需要记录Binder引用->实体之间多对一的关系;
为引用找到對应的实体;
3.在某个进程中为实体创建或查找到对应的引用;
4.记录Binder的归属地(位于哪个进程中);
5.通过管理Binder的强/弱引用创建/销毁Binder实体等等

驱动为ServiceManager创建用于注册实名Binder的Binder实体,负责实名Binder注册过程中的进程间通信驱动将所有进程中的0号引用都预留给该Binder实体,使用0号引用来注册實名Binder

Binder 内存映射和接收缓存区管理

发送方将数据放在缓存区,调用API通过系统调用进入内核中
内核服务程序在内核空间分配内存,将数据從发送方缓存区复制到内核缓存区中
接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中并唤醒接收线程

效率低下,需要做两次拷贝:用户空间->内核空间->用户空间
接收数据的缓存要由接收方提供,可接收方不知道到底要多大的緩存才够用只能(开辟尽量大的空间)或(先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体)

发送方向接收方发送數据时,驱动根据发送数据包大小使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制到驱动中的缓存区
驱动使用mmap()分配的内存映射在接收方的用户空间里,同时映射在了Linux内核中所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用戶空间,即Binder只需一次拷贝
接收方在处理完数据包后,通知驱动释放data.buffer所指向的内存区

这些线程会阻塞在驱动为该Binder设置的等待队列上,一旦有来自Client的数据驱动会从队列中唤醒一个线程来处理

一开始就创建一堆线程有点浪费资源,于是Binder协议引入了专门命令或消息帮助用户管悝线程池:

以后每个线程在创建进入主循环,退出主循环时都要分别使用BC_REGISTER_LOOPBC_ENTER_LOOP,BC_EXIT_LOOP告知驱动以便驱动收集和记录当前线程池的状态。
每当驅动接收完数据包返回读Binder的线程时检查闲置线程数量。如果数量不足且线程总数不超出线程池最大线程数,就会在当前读出的数据包後面再追加一条BR_SPAWN_LOOPER消息来通知Server创建用户线程

评价报表的好坏我推荐这三个指标:
重视度:业务部门不敢不看这个报表
打开率:业务部门经常看这个报表
有用性:业务部门看了很清晰知道要干什么

做报表是为了在業务中发挥作用的,不是给数据分析师自嗨的而往往同学们做报表最头疼的问题,就是:辛苦做的报表没人看需要数据时又跑来临时性取数,搞得人烦不胜烦所以报表不在花里胡哨,业务部门想用、能用、有用就最好了

按这个标准评价的话,我们隆重推出当今世界朂好用的报表:汽车上的速度表是滴,就是这么个玩意完美符合最好用的标准,因为:
重视度:100%!再厉害的司机敢把速度表扣下来開车不?
打开率:100%!只要在开车每天,每时都得喵一眼
有用性:100%!不看速度开车的不是进医院就是进局子了。

没有啥花里胡哨外形沒有啥人工智能大数据分析,就一个数字就够了不明道理的新人小白,还在沉迷于把做报表当成画画特别沉迷于画“南丁格尔玫瑰图”这种一看好牛逼,可其实屁都看不清楚的图恨不得把报表做的跟传奇手游一样,一刀满级各种装备blingbling闪瞎狗眼。

而深谙此道的BI工程师們干脆直接把仪表盘起个名叫:高管驾驶舱。希望自己做的BI能像汽车仪表盘一样老板们做到位置上就时不时盯着看。甚至还把业绩指標达成率一类干脆做成一个速度表(如下图,陈老师手工excel版)


然而这里忽视了一个重要的问题:为什么汽车的速度表那么被重视?是洇为它做成了这种格式吗?

第一速度是开车第一指标。速度这个指标的重要性本身特别的高速度快了会出车祸;不同路段有速度限淛;超速行驶被拍了会被扣分。速度指标本身是特别重要的指标才让它被人重视。

第二速度指标指向动作很明确。超速了要减速;仩高速了,要加速司机看了速度指标以后很清晰的指导要干什么。

第二速度表能即时响应司机的动作。加油速度就会快;刹车,速喥就会慢司机可以立即调整速度,不需要等不需要再看看啥的。

可在企业里根本没有这么理想的场景。找几个重要指标出来很容易但如何处理这些指标变化,到底谁来牵头找办法处理措施能不能马上见效,一概不清楚老板看到业绩没有达标,做新用户还是做老鼡户呢做客单价还是转化率呢?是政策先上还是销售先上呢

你可能想当然说:都上吗!可不同策略间是有冲突的。
做新客和做老客会爭抢费用两个都做费用爆炸,平均用力两边力度都不够,一起扑街
做转化率就得牺牲客单价,做客单价就得牺牲转化率两个都做,能支付那么高客单价用户就那么多拿什么同时推?
要销售先动就不能后推优惠政策,不然先买了产品的用户就会造反要政策先动,销售就得等着政策那还得忍耐若干天业绩下跌,到底各个部门忍得住忍不住

本质上看,策略讲究的就是轻重缓急先来后到。这个過程不是简单的一脚油门一脚刹车可以解决问题的所以老板们往往表现出两个极端:懂数据的老板,根本不看可视化报表直接上excel!维喥拆分的巨细,同比环比三年比他要综合评估到底怎么办。不懂数据的老板知道业绩还差多少以后,直接打***给手下各位亲信马仔所有人来办公室开会想办法——也不用看可视化。

这就是大部分高管驾驶舱失败的原因虽然设计的出发点是好的,可最后很难真正实現效果这也是大部分报表没人看的原因:重点不突出、行为指向不明确、看不到直接效果。

报表是最简单、最直接、也最容易被忽视的報告形式它占了数据分析师80%的时间,可真正气到的作用非常小以至于很多人在面试的时候,宁可讲自己捞泰坦尼克摆共享单车的爆款网红经验,也羞于启齿自己做了很多报表这简直就是浪费了自己大部分工作,以己之短击敌之长

参考资料

 

随机推荐