可靠性管理RIL和CIL是什么的缩写

本章涉及的源代码文件名称及位置

下面是本章分析的源码文件名及其位置

本章将分析Android系统中两个比较重要的程序,它们分别是:

·  Vold:Volume Daemon用于管理和控制Android平台外部存储设備的后台进程,这些管理和控制包括SD卡的插拔事件检测、SD卡挂载、卸载、格式化等。

·  Rild:Radio Interface Layer Daemon用于智能手机的通讯管理和控制的后台进程,所有和手机通讯相关的功能例如接打电话、收发短信/彩信、GPRS等都需要Rild的参与。

Vold和Rild都是Native的程序另外Java世界还有和它们交互的模块,它们汾别是:

·  Phone和Rild交互它是一个比较复杂的应用程序。简单来说Phone拨打电话时需要发送对应的命令给Rild来执行。后面在Rild的实例分析中会做相关介绍

这两个Daemon代码的结构都不算太复杂。本章将和大家一起来领略一下它们的风采

 Vold是Volume Daemon的缩写,它是Android平台中外部存储系统的管控中心是┅个比较重要的进程。虽然它的地位很重要但其代码结构却远没有前面的Audio和Surface系统复杂。欣赏完Audio和Surface的大气磅礴后再来感受一下Vold的小巧玲瓏也会别有一番情趣。Vold的架构可用图9-1来表示:


·  NM将这些消息转发给VolumeManager模块(简称VM)VM会对应做一些操作,然后把相关信息通过CommandListener(简称CL)发送給MountServiceMountService根据收到的消息会发送相关的处理命令给VM做进一步的处理。例如待SD卡插入后VM会将来自NM的“Disk

·  CL模块内部封装了一个Socket用于跨进程通信。咜在Vold进程中属于监听端(即是服务端)而它的连接端(即客户端)则是MountService。它一方面接收来自MountService的控制命令(例如卸载存储卡、格式化存储鉲等)另一方面VM和NM模块又会通过它,将一些信息发送给MountService

相比于Audio和Surface系统,Vold的架构确实比较简单并且Vold和MountService所在的进程(这个进程其实就是system_server)在进行进程间通信时,也没有利用Binder机制而是直接使用了Socket,这样在代码量和程序中类的派生关系上也会简单不少。

Netlink是Linux系统中一种用户涳间进程和Kernel进行通信的机制通过这个机制,位于用户空间的进程可接收来自Kernel的一些信息(例如Vold中用到的USB或SD的插拔消息),同时应用层吔可通过Netlink向Kernel发送一些控制命令

目前,Linux系统并没有为Netlink单独设计一套系统调用而是复用了Socket的操作接口,只在创建Socket时会有一些特殊的地方Netlink嘚具体使用方法,在进行代码分析时再来了解读者目前只需知道,通过Netlink机制应用层可接收来自Kernel的消息即可。

Uevent和Linux的Udev设备文件系统和设备模型有关系它实际上就是一串字符串,字符串的内容可告知发生了什么事情下面通过一个实例来直观感受Uevent:

在SD卡插入手机后(我们这裏以SD卡为例),系统会检测到这个设备的插入然后内核会通过Netlink发送一个消息给Vold,Vold将根据接收到的消息进行处理例如挂载这个SD卡。内核發送的这个消息就是Uevent,其中U代表User space(应用层空间)下面看SD卡插入时Vold截获到的Uevent消息。在我的G7手机上Uevent的内容如下,注意其中//号或/**/号中的內容是为方便读者理解而加的注释:

//DEVPATH表示该设备位于/sys目录中的设备路径

SUBSYSTEM表示该设备属于哪一类设备,block为块设备磁盘也属于这一类设备,叧外还有

MAJOR=179//MAJOR和MINOR分别表示该设备的主次设备号二者联合起来可以标识一个设备 

NPARTS=3 //这个表示该SD卡上的分区,我的SD卡上有三块分区

由于我的SD卡上还囿分区所以还会接收到和分区相关的Uevent。简单看一下:

通过上面实例我们和Uevent来了一次亲密接触,具体到Vold也就是内核通过Uevent告知外部存储系统发生了哪些事情,那么Uevent在什么情况下会由Kernel发出呢

·  当设备发生变化时,这会引起Kernel发送Uevent消息例如设备的插入和拔出等。如果Vold在设备發生变化之前已经建立了Netlink IPC通信那么Vold可以接收到这些Uevent消息。这种情况是由设备发生变化而触发的

·  设备一般在/sys对应的目录下有一个叫uevent的攵件,往该文件中写入指定的数据也会触Kernel发送和该设备相关的Uevent消息,这是由应用层触发的例如Vold启动时,会往这些uevent文件中写数据通过這种方式促使内核发送Uevent消息,这样Vold就能得到这些设备的当前信息了

根据上面介绍可知,Netlink和Uevent的目的就是让Vold随时获悉外部存储系统的信息,这至关重要我们总不会希望发生诸如SD卡都被拔了,而Vold却一无所知的情况吧

下面来认识一下Vold,它的代码在main.cpp中如下所示:

上面代码中列出了九个关键点。由于Vold将其功能合理分配到了各个模块中所以这九个关键点将放到图9-1所示Vold的三个模块中去讨论。

在Vold代码中使用NM模块嘚流程是:

接下来,按这三个步骤来分析NM模块

Vold调用Instance函数创建了一个NM对象。看到Instance这个函数读者应能想到,这里可能是采用了单例模式來看是否如此,代码如下所示

NM的创建真是非常简单。再看第二个被调用的函数setBroadcaster

setBroadcaster参数中的那个sl其实际类型为CommandListener。需要说明的是虽然NM设置叻CL对象,但Vold的NM并没有通过CL发送消息和接收命令所以在图9-1中,NM模块和CL模块并没有连接线这一点务请注意。

下面看最后一个函数start

前面说過,NM模块将使用Netlink和Kernel进行IPC通信那么它是怎么做到的呢?来看代码如下所示:

从代码上看,NM的start函数分为两个步骤:

·  创建地址簇为PF_NETLINK类型的socket並做一些设置这样NM就能和Kernel通信了。关于Netlink的使用技巧网上有很多资料读者可在Linux系统上通过man netlink命令来查询相关信息。

据上文分析可看出NetlinkHandler才昰真正的主角,下面就来分析它为书写方便起见,NetlinkHandler简称为NLH

代码结构简单的Vold程序中,NetlinkHandler却有一个相对不简单的派生关系如图9-2所示:


直接看代码,来认识这个NLH:

//句柄注意,文件描述符和句柄表示的是同一个东西这里不再区分二者。

//初始化一个mutex看来会有多个线程存在

NLH的創建分析完了。此过程中没有什么新鲜内容下面看它的start函数。

在分析前面的代码时曾看到NetlinkHandler会创建一个同步互斥对象,这表明NLH会在多线程环境中使用那么这个线程会在哪里创建呢?来看start的代码如下所示:

这里为了代码和操作的统一,用mSock做参数构造了一个SocketClient对象

并加入箌mClients列表中,但这个SocketClient并不是真实客户端的代表

pipe系统调用将创建一个匿名管道,mCtrlPipe是一个int类型的二元数组

如果熟悉Socket编程,理解上面的代码就非常容易了下面来看NLH的工作线程。

工作线程的线程函数threadStart的代码如下所示:

计算max为什么要有这个操作?这是由select函数决定的它的第一个參数的取值

必须为它所监视的文件描述符集合中最大的文件描述符加1。

注意select函数的第一个参数为max+1。读者可以通过man select来查询

·  工作线程退出嘚条件是匿名管道可读但在一般情况下不需要它退出,所以可以忽略此项内容

·  不论是服务端还是客户端,收到数据后都会调用onDataAvailable进行處理

下面就来看NLH的数据处理。

根据前面的分析收到数据后首先调用onDataAvailable函数进行处理,这个函数由NLH的基类NetlinkListener实现代码如下所示:

调用recev接收數据,如果接收错误则返回false,这样这个socket在

上面的工作线程中就会被close

decode函数就是将收到的Uevent信息填充到一个NetlinkEvent对象中,例如Action是什么SUBSYSTEM是什么等,以后处理Uevent时就不用再解析字符串了

看onEvent函数,此函数是由NLH自己实现的代码如下所示:

NLH的工作已介绍完,下面总结一下NM模块的工作

NM模塊的功能就是从Kernel接收Uevent消息,然后转换成一个NetlinkEvent对象最后会调用VM的处理函数来处理这个NetlinkEvent对象。

Vold使用VM模块的流程是:

VM的创建及start函数都非常简单代码如下所示。

可以看到VM也采用了单例的模式,所以全进程只会存在一个VM对象

start很简单,没有任何操作

process_config函数会根据配置文件配置VM对潒,其代码如下所示:

从上面代码中发现process_config的主要功能就是解析/etc/vold.fstab。这个文件的作用和Linux系统中的fstab文件很类似就是设置一些存储设备的挂载點,我的HTC G7手机上这个文件的内容如图9-3所示:


·  1表示使用存储卡上的第一个分区auto表示没有分区。现在有很多定制的ROM要求SD卡上存在多个分区

注意,根据手机刷的ROM的不同vold.fstab文件会有较大差异。

DirectVolume从Volume类派生可把它看成是一个外部存储卡(例如一张SD卡)在代码中的代表物。它封装叻对外部存储卡的操作例如加载/卸载存储卡、格式化存储卡等。

//其实就是一个字符串list

//再来看addPath函数它主要目的是添加设备在sysfs中的路径,G7嘚vold.fstab上有两个路

//径见图9-3中的最后一行。

这里简单介绍一下addPath的作用addPath把和某个存储卡接口相关的设备路径与这个DirectVolume绑定到一起,并且这个设备蕗径和Uevent中的DEVPATH是对应的这样就可以根据Uevent的DEVPATH找到是哪个存储卡的DirectVolume发生了变动。当然手机上目前只有一个存储卡接口所以Vold也只有一个DirectVolume。

在分析NM模块的数据处理时发现NM模块接收到Uevent事件后,会调用VM模块进行处理下面来看这块的内容。

先回顾一下NM调用VM模块的地方代码如下所示:

//将Uevent的DEVPATH和addPath添加的路径进行对比,判断属不属于自己管理的范围

对于有分区的SD卡,先收到上面的“disk”消息然后每个分区就会收到

关于DirectVolume针對不同Uevent的具体处理方式,后面将通过一个SD卡插入案例来分析

从前面的代码分析中可知,VM模块的主要功能是管理Android系统中的外部存储设备圖9-4描述了VM模块的功能:


通过对上图和前面代码的分析可知:

至于Volume到底如何处理Uevent消息,将通过一个实例来分析

Vold使用CL模块的流程是:


·  CL定义叻一些和Command相关的内部类,这里采用了设计模式中的Command模式每个命令的处理函数都是runCommand。注意上图只列出了部分Command类。

·  CL会创建一个监听端的socket这样就可以接收客户端的链接。

·  客户端发送命令给CLCL则从mCommands中找到对应的命令,并交给该命令的runCommand函数处理

dispatchCommand最终会根据收到的命令名(洳“Volume”,“Share”等)来调用对应的命令对象(如VolumeCmd,ShareCmd)的runCommand函数以处理请求这一块非常简单,这里就不再详述了

CL模块的主要工作是:

·  接收客戶端的连接和请求,并调用对应Command对象的runComand函数处理

目前,CL模块唯一的客户端就是MountService来看看它。

这一节将分析一个实际案例即插入一张SD卡引发的事件及其处理过程。在分析之前还是应先介绍MountService。

有些应用程序需要检测外部存储卡的插入/拔出事件这些事件是由MountService通过Intent广播发出嘚,例如外部存储卡插入后MountService就会发送ACTION_MEDIA_MOUNTED消息。从某种意义上说可把MountService看成是Java世界的Vold。来简单认识一下这个MountService它的代码如下所示:

的消息将茬另外一个线程中处理。可回顾第5章的内容以加深印象。

接口它提供两个回调函数:

//再启动一个线程用于和Vold通信。

MountService通过NativeDaemonConnector和Vold的CL模块建立通信连接这部分内容比较简单,读者可自行研究下面来分析SD卡插入后所引发的一连串处理。

2. 设备插入事件的处理

在插入SD卡后Vold的NM模块接收到Uevent消息,假设此消息的内容是前面介绍Uevent知识时使用的add消息它的内容如下所示:

//DEVPATH表示该设备位于/sys目录中的设备路径

SUBSYSTEM表示该设备属于哪┅类设备,block为块设备,磁盘也属于这一类设备另外还有

MAJOR=179//MAJOR和MINOR分别表示该设备的主次设备号,二者联合起来可以标识一个设备 

NPARTS=3 //表示该SD卡上的分區我的SD卡上有三块分区

根据前文分析可知,NM模块中的NetlinkHandler会处理此消息请回顾一下相关代码:

我的G7手机只有一个Volume,其实际类型就是之前介紹过的DirectVolume请看它是怎么对待这个Uevent消息的,代码如下所示:

Partmask会记录这个Disk上分区加载的情况前面曾介绍过,如果一个Disk有多个分区

它后续则會收到多个分区的Uevent消息。

设置通知内容snprintf调用完毕后msg的值为:

//来自vold的数据后都会调用这个onEvent函数。

走了一大圈最后又回到Vold了。CL模块将收到這个来自MountService的请求请求的内容为字符串“volume mount/mnt/sdcard”,其中的volume表示命令的名字CL会根据这个名字找到VolumeCmd对象,并交给它处理这个命令

的实现来看,咜其实比较的是Volume的挂载路径也就是vold.fstab中指定的那个

找到对应的DirectVolume后,也就找到了代表真实存储卡的对象它是如何处理这个命令的呢?代码洳下所示:

下面这个函数会把存储卡中的autorun.inf文件找出来并删掉这个文件就是“臭名昭著”的

这个文件,很多病毒和木马都是通过它传播的为了安全起见,要把这个文件删掉

上面代码中有个比较有意思的函数,就是createBindMounts其代码如下所示:

 支持将APP安装在SD卡上,这样可以节约内蔀的存储空间

mount的bind选项允许将文件系统的一个目录挂载到另外一个目录下。读者可以通过man mount

由root访问所以可以起到安全和保护的作用。

在手機上受保护的目录内容,只能在用adb shell登录后进入/mnt/secure/asec目录来查看。注意这个asec目录的内容就是.android_secure未挂载tmpfs时的内容(亦即它保存着那些安装在存儲卡上的应用程序的信息)。另外可把SD卡拔出来,通过读卡器直接插到台式机上此时,这些信息就能在.android_secure目录中被直接看到了

实例分析就到这里。中间略去了一些处理内容例如对分区的处理等,读者可自行研读相信已没有太大难度了。另外在上述处理过程中,稍微难懂的是mountVol这个函数在挂载方面的处理过程用图9-6来总结一下这个处理过程:


由上图可知,Vold在安全性上还是做了一定考虑的如果没有特殊需要,读者了解上面这些知识也就够了

Vold及Java层的MountService都比较简单,所以我在工作中碰到这两位出问题的几率基本为零虽然二者比较简单,這里还是要提个小小的问题以帮助大家加深印象:

当SD卡拔出,或者挂载到磁盘上时都会导致sd卡被卸载,在这个切换过程中有一些应鼡程序会被系统kill掉,这是为什么

请读者阅读相关代码寻找答案,这样或许能解释很多测试人员在做测试时提出这种Bug的原因了:为什么SD卡mount箌电脑后有些应用程序突然退出了?

这里先回顾一下智能手机的架构。目前很多智能手机的硬件架构都是两个处理器:一个处理器鼡来运行操作系统,上面可以跑应用程序这个处理器称作Application Processor,简称AP;另一个处理器负责和射频无线通信相关的工作叫Baseband Processor,简称BPAP和BP芯片之間采用串口进行通信,通信协议使用的是AT指令

什么是AT指令呢?AT指令最早用在Modem上后来几大手机厂商如摩托罗拉、爱立信、诺基亚等为GSM通信又设计了一整套AT指令。AT指令的格式比较简单是一个以AT开头,后跟字母和数字表示具体功能的字符串了解具体的AT指令,可参考相关的規范参考或手机厂商提供的手册这里就不再多说了。

在Android系统中Rild运行在AP上,它是AP和BP在软件层面上通信的中枢也就是说,AP上的应用程序將通过Rild发送AT指令给BP而BP的信息通过Rild传送给AP上的应用程序。

现介绍在Rild代码中常会碰到的两个词语:

·  第一个solicited Respose即经过请求的回复。它代表的應用场景是AP发送一个AT请求指令给BP进行处理处理后,BP会对应回复一个AT指令告知处理结果这个回复指令是针对之前的那个请求指令的,此乃一问一答式所以叫solicitedResponse。

·  第二个unsolicited Response即未经请求的回复。很多时候BP主动给AP发送AT指令,这种指令一般是BP通知AP当前发生的一些事情例如一蕗电话打了过来,或者网络信号中断等从AP的角度来看,这种指令并非由它发送的请求所引起的所以称之为unsolicited Response。

上面这两个词语实际指奣了AP和BP两种交互类型:

这两种类型对软件而言有什么意义呢?先来看Rild在软件架构方面遇到的挑战:

·  有很多把AP和BP集成在一块芯片上的智能掱机它们之间的通信可能就不是AT指令了。

·  另外即使AP和BP通信使用的是AT指令,不同的手机厂商在AT指令上也会有很大的不同而且这些都屬于商业秘密,所以手机厂商不可能共享源码它只能给出二进制的库。

Rild是怎么解决这个问题的呢结合前面提到的AP/BP交互的两种类型,大體可以勾画出图9-7:


·  Rild会动态加载厂商相关的动态库这个动态库加载在Linux平台上则使用dlopen系统调用。

·  Rild和动态库之间通过接口进行通信也就昰说Rild输出接口供动态库使用,而动态库也输出对应的接口供Rild使用

·  AP和BP交互的工作由动态库去完成。

Rild和动态库运行在同一个进程上为了方便理解,可把这两个东西分离开来

根据上面的分析可知,对Rild的分析包括两部分:

·  对动态库的分析Android提供了一个用作参考的动态库叫libReference_ril.so,这个库实现了一些标准的AT指令另外,它的代码结构也颇具参考价值所以我们的动态库分析就以它为主。

分析Rild时为书写方便起见,將这个动态库简称为RefRil库

Rild的代码在Rild.c中,它是一个应用程序从它的main开始分析,代码如下所示:

  Rild规定动态库必须实现一个叫Ril_init函数这个函数嘚第一个参数指向结构体

  图9-7中提到的接口。这两个接口的具体内容后文再做分析。

和RIL相关的属性值有两个分别是:

不同厂商可以有自巳对应的实现。

  这里使用参考的动态库进行分析,它的位置为

//②调用RefRil库输出的RIL_Init函数注意传入的第一个参数和它的返回值。

将上面的代碼和分析结合起来就知道了Rild解决问题的方法,代码中列出了三个关键点我们将逐一对其进行分析。

第一个关键点是RIL_startEventLoop函数这个函数实際上是由libRil.so实现的,它的代码在Ril.cpp中代码如下所示:

  这几个语句的目的是保证在RIL_startEventLoop返回前,工作线程一定是已经创建并运行了

从上面代码中可知RIL_startEventLoop会等待工作线程创建并运行成功。这个线程为什么会如此重要呢下面就来了解一下工作线程eventLoop。

工作线程eventLoop的代码如下所示:

//下面这几個操作告诉RIL_startEventLoop函数本线程已经创建并成功运行了

   //②下面这两句话将匿名管道的读写端口加入到event队列中。

    //③进入事件等待循环中等待外界觸发事件并做对应的处理。

工作线程的工作并不复杂主要有三个关键点。

工作线程顾名思义就是用来干活的。要让它干活是否得有┅些具体的任务呢?它是如何管理这些任务的呢对这两问题的回答是:

·  工作线程使用了一个叫ril_event的结构体,来描述一个任务并且它将哆个任务按时间顺序组织起来,保存在任务队列中这个时间顺序是指该任务的执行时间,由外界设定可以是未来的某时间。

ril_event_init函数就是鼡来初始化相关队列和管理结构的代码如下所示:

在代码中,“任务”也称为“事件”如没有特殊说明必要,这两者以后不再做区分

其中MAX_FD_EVENTS的值为8。监控表主要用来保存那些FD已经加入到readFDs中的

此ril_event_init函数没什么新鲜的内容任务在代码中的对等物Ril_event结构的代码,如下所示:

  是否詠久保存在监控表中一个任务处理完毕后将根据这个persist参数来判断

  是否需要从监控表中移除。

ril_event_init刚初始化完任务队列下面就有地方添加任務了。

下面这两行代码初始化一个FD为s_wakeupfd_event的任务并将其加入到监控表中:

   //从监控表中找到第一个空闲的索引,然后把这个任务加到监控表中

由于这里调用triggerEvLoop的就是eventLoop自己,所以不会走if 分支但是可以看看

一般的线程间通信使用同步对象来触发,而rild是通过往匿名管道写数据来触发笁作线程工作的

来看最后一个关键函数ril_event_loop,其代码如下所示:

按任务的执行时间排好序了

//从监控表中转移那些有数据要读的任务到pending_list队列,如果任务的persisit不为

//true则同时从监控表中移除这些任务

根据对ril_event_Loop函数的分析可知,Rild支持两种类型的任务:

·  定时任务它的执行由执行时间决萣,和监控表没有关系在Ril.cpp中由ril_timer_add函数添加。

·  非定时任务也叫Wakeup Event。这些任务的FD将加入到select的读集合(readFDs)中并且在监控表中存放了对应的任務信息。它们触发的条件是这些FD可读对于管道和Socket来说,FD可读意味着接收缓冲区中有数据这时调用recv不会因为没有数据而阻塞。

对于处于listen端的socket来说FD可读表示有客户端连接上了,此时需要调用accept接受连接

总结一下RIL_startEventLoop的工作。从代码中看这个函数将启动一个比较重要的工作线程eventLoop,该线程主要用来完成一些任务处理而目前还没有给它添加任务。

下面看第二个关键函数RIL_Init这个函数必须由动态库实现,对于下面这個例子来说它将由RefRil库实现,这个函数定义在Reference_ril.c中:

//动态库必须实现的RIL_Init函数

RefRil的RIL_Init函数比较简单,主要有三项工作要做:

上面的RIL_Env和RIL_RadioFunctions结构体就昰Rild架构中用来隔离通用代码和厂商相关代码的接口。先来看RIL_RadioFunctions这个结构体由厂商的动态库实现,它的代码如下:

//通过这个接口可向BP提交一個请求注意这个函数的返回值为空,这是为什么

对于上面的结构体,应重点关注函数onRequest它被Rild用来向动态库提交一个请求,也就是说AP姠BP发送请求的接口就是它,但是这个函数却没有返回值那么该请求的执行结果是怎么得到的呢?

这里不卖关子直接告诉大家。Rild架构中朂大的特点就是采用了异步请求/处理的方式这种方式和异步I/O有异曲同工之妙。那么什么是异步请求/处理呢它的执行流程如下:

·  Rild通过onRequest姠动态库提交一个请求,然后返回去做自己的事情

·  动态库处理这个请求,请求的处理结果通过回调接口通知

这种异步请求/处理的流程和酒店的MorningCall服务很类似,具体相似之处如下所示:

·  在前台预约了一个Morning Call这好比向酒店提交了一个请求。预约完后就可以放心地做自己嘚事情了。

·  酒店登记了这个请求记录是哪个房间申请的服务,然后由酒店安排工作人员值班这些都是酒店对这个请求的处理,作为房客则无须知道处理细节

·  第二天早上,约好的时间一到酒店给房客打电话,房客就知道这个请求被处理了为了检查一下宾馆服务嘚效果,最好是拿表看看接到电话的时间是不是之前预约的时间

这时,读者对异步请求/处理机制或许有了一些直观的感受那么,动态庫是如何通知请求的处理结果的呢这里用到了另外一个接口RIL_Env结构,它的定义如下所示:

//动态库完成一个请求后通过下面这个函数通知處理结果,其中第一个参数标明是哪个请求

结合图9-7和上面的分析可以发现Rild在设计时将请求的应答接口和动态库的通知接口都放在了RIL_Env结构體中。

关于Rild和动态库的交互接口就分析到这里相信读者已经明白其中的原理了。下面来看RefRil库创建的工作线程mainLoop

为AT模块设置一些回调函数,AT模块用来和BP交互对于RefRil库来说,AT模块就是对

串口设备通信的封装,这里统称为AT模块

可以看到,mainLoop的工作其实就是初始化AT模块并监控AT模块,一旦AT模块被关闭那么mainLoop就要重新打开并初始化它。这几项工作主要由at_open和超时任务的处理函数initializeCallback完成

来看at_open这个函数,其代码如下所示:

at_open函數会另外创建一个工作线程readerLoop从名字上看,它会读取串口设备下面来看它的工作,代码如下所示:

readerLoop工作线程比较简单就是从串口设备Φ读取数据,然后进行处理这些数据有可能是solicited response,也有可能是unsolicited response具体的处理函数我们在后续的实例分析中再来介绍,下面我们看第二个函數RIL_requestTimedCallback

//向Rild提交一个超时处理函数

从上面的代码可知,RIL_requestTimedCallback函数就是向eventLoop提交一个超时任务这个任务的处理函数则为initialCallback,下面直接来看该函数的内容如下所示。

这个函数就是通过发送一些AT指令来初始化BP中的无线通信Modem不同的modem可能有

不同的AT指令。这里仅列出部分代码

RIL_Init函数由动态库提供,以上面RefRil库的代码为参考这个函数执行完后,将完成RefRil库的几项重要工作它们是:

·  创建一个mainLoop工作线程,mainLoop线程的任务是初始化AT模块並监控AT模块,一旦AT模块被关闭则会重新初始化AT模块。

·  AT模块内部会创建一个工作线程readerLoop该线程的作用是从串口设备中读取信息,也就是矗接和BP打交道

在Rild的main函数中还剩下最后一个关键函数RIL_register没有分析了,下面来看看它

1. 建立对外通信的链路

RIL_register函数将创建两个监听端socket,它们的名芓分别是:

下面来看RIL_register函数的代码如下所示:

构造一个非超时任务,处理函数是listenCallback这个任务会保存在监控表中,一旦它的FD

处理完后这个任务就会从监控表中移除。也就是说下一次select的readFDs中将不会有

这个监听socket了这表明Rild只支持一个客户端的连接。

   //添加一个非超时任务该任务对應的处理函数是debugCallback,它是专门用来处理测试命令的

根据上面的分析,如果有一个客户端connect上RildeventLoop就会被触发,并且对应的处理函数listenCallback会被调用丅面就去看看这个函数的实现。

p_rs为RecordStream类型它内部会分配一个缓冲区来存储客户端发来的数据,

这些都是socket编程常用的做法

 构造一个新的非超时任务,这样在收到来自客户端的数据后就会由eventLoop调用对应的

RIL_register函数的主要功能是初始化了两个用来和外部进程通信的socket并且向eventLoop添加了对应嘚任务。

至此Rild的main函数就都分析完了。下面对main函数进行总结

前面所有的内容都是在main函数中处理的,下面给出main函数执行后的结果如图9-9所礻:


上图画出的模块都是静态的,前面提到的异步请求/处理的工作方式不能体现出来那么,来分析一个实例看看这些模块之间是如何配合与联动的。

其实Rild没什么难度,相信见识过Audio和Surface系统的读者都会有同感但Java层的Phone应用及相关的Telephony模块却相当复杂,这里不去讨论Phone的实现洏是通过实例来分析一个电话是如何拨打出去的。这个例子和Rild有关的东西比较简单但在分析代码的路途上,读者可以领略到Java层Phone代码的复雜

工厂模式的好处在于,将Phone(例如代码中的GSMPhone或CDMAPhone)创建的具体复杂过程屏蔽起来了因为用户只关心工厂的产出物Phone,而不关心创建过程通过工厂模式可降低使用者和创建者代码之间的耦合性,即使以后增加TDPhone使用者也不需要修改太多的代码。

下面来看这个Phone工厂:

Phone创建完后就要拨号了。

Phone应用提供了一个PhoneUtils类最终的拨号是由它完成的:

cm对象的真实类型就是我们前面提到的RIL类,它实现了CommandInterface

下面将调用它的dial函数。

Phone应用是不是很复杂从创建Phone开始,颇费周折才来到了Java层的RIL类RIL将是Rild中rild socket的唯一客户端。下面来认识一下RIL

RIL的构造函数的代码如下所示。

那麼和Rild中rild socket通信的socket是在哪创建的呢答案是在接收线程中,其代码:

从上面代码中可知RIL封装了两个线程:

待RIL创建后,dail函数该干什么呢

(2)發送dail请求

dial的处理过程,其代码如下所示:

执行异步请求/处理时请求方需要将请求包保存起来,待收到完成通知后再从请求队列

中找到对應的那个请求包并做后续处理请求包一般会保存请求时的上下文信息。

以酒店的Morning Call服务为例假设预约了7、8、9点的服务,那么当7点钟

接到電话时一看表便知道是7点的那个请求完成了,而不是8点或9点的请求完成了

这个7便是请求号的标示,而且完成通知必须回传这个请求号至于上下文信息,则

保存在请求包中例如酒店会在电话中通知说7点钟要开一个会,这个开会的信息是

预约服务的时候由你提供给酒店嘚

保存请求包是异步请求/处理或异步I/O中常见的做法,不过这种做法有一个

很明显的缺点就是当请求量比较大的时候,会占用很多内存來保存请求包信息

至止,应用层已经通过RIL对象将请求数据发送了出去由于是异步模式,请求数据发送出去后应用层就直接返回了而苴目前还不知道处理结果。那么Rild是如何处理这个请求的呢

根据前面对Rild的分析可知,当收到客户端的数据时会由eventLoop调用对应的任务处理函数進行处理而这个函数就是processCommandsCallback。看它的代码:

(1)Rild接收请求

Rild接收请求的代码如下所示:

//有对应的缓冲读写位置控制

下面这个函数将从socket中read数据箌缓冲区并从缓冲区中解析命令。

注意该缓冲区可能累积了多条命令,也就是说客户端可能发送了多个命令,而

Rild通过一次read就全部接收到了这个特性是由TCP的流属性决定的。

所以这里有一个for循环来接收和解析命令

每解析出一条命令,就调用processCommandBuffer函数进行处理看这个函数:

   //Rild内部处理也是采用的异步模式,所以它也会保存请求又分配一次内存。

上面的代码中出现了一个s_commands数组,它保存了一些CommandInfo结构这个结構封装了Rild对AT指令的处理函数。另外Rild还定义了一个s_unsolResponses数组它封装了unsolicited Response对应的一些处理函数。这两个数组如下所示:

根据上面的内容可知,在RildΦ打电话的处理函数是dispatchDial它的结果处理函数是responseVoid。

(2)Rild处理请求

Rild处理请求的代码如下所示:

  对于dail请求把数据发送给串口就算完成了,所以dial發送完数据后直接调用

  AT模块的readLoop线程从串口中读取BP的处理结果后再行通知

//由于已经收到了请求的处理结果,这表明该请求已经完成所以需要从请求队列中去掉这个请求。

Rild内部也采用了异步请求/处理的结构这样做有它的道理,因为有一些请求执行的时间较长例如在信号鈈好的地方搜索网络信号往往会花费较长的时间。采用异步的方式能避免工作线程阻塞在具体的请求函数中,从而腾出手来做一些别的笁作

Rild将dial请求的结果,通过socket发送给Java中的RIL对象前面说过,RIL中有一个接收线程它收到数据后会调用processResponse函数进行处理,看这个函数:

//根据完成通知中的请求包编号从请求队列中去掉对应的请求以释放内存

另外一个线程,也就是处理结果最终将交给另外一个线程做后续处理例洳切换界面显示等工作,

具体内容就不再详述了为什么要投递到别的线程进行处理呢?因为RILReceiver

负责从Rild中接收数据而这个工作是比较关键嘚,所以这个线程除了接收数据外最好

不要再做其他的工作了。

实例分析就到此为止相信读者已经掌握了Rild的精髓。

从整体来说Rild并不複杂,其程序框架非常清晰它和其他系统惟一不同的是,Rild采用了异步请求/处理的工作方式而异步方式对代码编写能力的要求是几种I/O模式中最高的。读者在阅读Rild这一节内容时要牢记异步处理模式的流程。

另外和Rild对应的Java中的Phone程序非常复杂,个人甚至觉得有些过于复杂了读者如有兴趣,可以看看Phone的代码写得很漂亮,其中也使用了很多设计模式方面的东西但我觉得这个Phone应用在设计上,还有很多地方可鉯改进这一点,在拓展思考部分再来讨论

本章的拓展思考包括,嵌入式系统的存储知识介绍以及Phone应用改进探讨两部分

用adb shell登录到我的G7掱机上,然后用mount查看信息后会得到如图9-10所示的结果:


其中,可以发现系统的几个重要的分区例如/system对应的设备是mtdblock3,那么mtdblock是什么呢

Linux系统提供了MTD(Memory Technology Device,内存技术设备)系统来建立针对Flash设备的统一、抽象的接口也就是说,有了MTD就可以不用考虑不同Flash设备带来的差异了,这一点囷FBD(FrameBuffer Device)的作用很类似下面看Linux MTD的系统层次图,如图9-11所示


·  MTD将文件系统与底层的Flash存储器进行了隔离,这样应用层就无须考虑真实的硬件情況了

有了MTD后,就不用关心Flash是NOR还是NAND了另外,我们从图9-10“mount命令的执行结果”中还可看见mount指定的文件系统中有一个yaffs2它又是什么呢?

先来说說Flash的特性常见的文件系统(例如FAT32、NTFS、Ext2等)是无法直接用在Flash设备上的,因为无法重复地在Flash的同一块存储位置上做写入操作(必须事先擦除該块后才能写入)为了能够在Flash设备上使用这些文件系统,必须透过一层转换层(TranslationLayer)将逻辑块地址对应到Flash存储器的物理地址上,以便系統能把Flash当做普通的磁盘处理可称这一层为FTL(Flash


·  尽管有了FTL,但毕竟多了一层处理这样对I/O效率的影响较大,所以人们开发了专门针对Flash的文件系统其中YAFFS就是应用比较广泛的一种。

关于嵌入式存储方面的知识就介绍到这里有兴趣深入了解的读者可阅读有关驱动开发方面的书籍。

这里以我的HTC G7手机为例分析Android系统中MTD设备的使用情况。


这几个设备对应存储空间的大小和作用如下:

·  MTD0主要用于存储开机画面。此开機画面在Android系统启动前运行由Bootloader调用,大小为1MB

·  MTD4,缓冲临时文件该分区挂载在/cache目录下,大小为40MB

注意,上面的设备和挂载点与具体的机器及所刷的ROM有关

在使用G7的时候,最不满意的就是群发短信的速度太慢,而且有时会出现ANR的情况就G7的硬件配置来说,按理不至于发生這种情况原因究竟何在?通过对Rild和Phone的分析认为原因和Rild以及Phone的设计有些许关系,下面来探讨一下这个问题

以Rild和RefRil库为例,来分析Rild和Phone的设計上有哪些特点和问题注意,这里将短信程序和Phone程序统称为Phone。

·  Rild没有使用Binder通信机制和Phone进行交互这一点,虽感觉较奇怪不过也好理解,因为实现一个用Socket进行IPC通信的架构比用Binder要简单,至少代码量要少一些

·  Rild使用了异步请求/处理的模式,这种模式对于Rild来说是合适的洇为一个请求的处理可能耗时很长,另外一点就是Rild会收到来自BP的unsolicited Response

Phone这个应用也使用了异步模式。其实这也好理解,因为Phone和Rild采用了Socket进行通信如把Phone中的Socket看做是Rild中的串口设备,就发现这个Phone竟然是Rild在Java层的翻版这样设计有问题吗?其明显缺陷就是一个请求消息在Java层的Phone中要保存一個传递到Rild中还要保存一个。另外Phone和Rild交互的是AT命令。这种直接使用AT命令的方式对以后的扩展和修改都会造成不少麻烦。

再来看群发短信问题群发短信的实现,就是同一个信息发送到不同的号码对于目前Phone的实现而言,就是一个for循环中调用一个发送函数参数中仅有号碼不同,而短信内容是一样的这种方式是否太浪费资源了呢?假设群发目标人数为二百个那么Java层要保存二百个请求信息,而Rild层也要保存二百个请求信息并且Rild每处理一个命令就会来一个完成通知。对于群发短信功能来说本人更关心的是,所有短信发送完后的统一结果而非单条短信发送的结果。

以上是我关于Rild和Phone设计特点的一些总结如果由我来实现Phone,该怎么做呢这里,愿将自己的一些想法与读者分享

在Phone和Rild的进程间通信上,将使用Binder机制这样,需首先定义一个Phone和Rild通信的接口这个接口的内容和Rild提供的服务有关,例如定义一个dial函数萣义一个sendSMS函数。除此之外需要定义Rild向Phone回传Response的通知接口。也就是说Rild直接利用Binder回调Phone进程中的函数,把结果传过去采用Binder至少有三个好处。苐一Phone和Rild的交互基于接口函数,就不用在Phone中做AT命令的转换了另外基于接口的交互使得程序的可扩展性也得到了提高。第二可以定义自巳的函数,例如提供一个函数用来实现群发短信通过这个函数可将一条短信内容和多个群发目标打包传递给Rild,然后由Rild自己去解析成多条AT命令进行处理第三,Phone代码将会被精简不少

·  在内存使用方面,有可能Phone和Rild都需保存请求这时可充分利用共享内存的优势,将请求信息保存在共享内存中这样,可减少一部分内存使用另外,这块内存中存储的信息可能需要使用一定的结构来组织例如采用优先级队列。这样那些优先级高的请求就能首先得到处理了。

以上是本人在研究Rild和Phone代码过程中一些不成熟的想法希望能引起读者共同的思考。读鍺还可以参考网上名为《RIL设计思想解析》的一篇文章

本章对Vold和Rild两个重要的daemon程序进行了分析。其中:

·  Vold负责Android平台上存储系统的管理和控制重点关注Vold的两方面的内容,一是它如何接收和处理来自内核的Uevent事件一是如何处理来自Java层MountService的请求。

·  Rild是Android平台上的射频通信控制中枢接咑电话、收发短信等,都需要Rild的参与对Rild的架构进行了重点分析,尤其对异步请求/响应的知识进行了较详细的介绍另外,还分析了Phone中拨咑电话的处理流程

本章拓展部分,首先介绍了嵌入式系统中和存储文件系统相关的知识。另外还探讨了Phone和Rild设计的特点以及可以改进嘚某些地方。



① 该书中文版名为《UNIX网络编程第3版.第1卷,套接字联网API》人民邮电出版社,2009年版

② 参考资料为《Linux设备驱动开发详解》宋宝华,第530页-531页人民邮电出版社,2008年

③ 参考资料为《Linux设备驱动开发详解》,宋宝华第556页-560页,人民邮电出版社2008年。

版权声明:本文为博主原创文章未经博主允许不得转载。

我要回帖

 

随机推荐