安卓系统入门
参考书籍 《Android 系统源代码分析 》- 李俊
序
大概很早之前,我就有学习安卓的想法了,那时候知道它的内核是Linux 内核,也对 Linux 更加敬佩了,所以很好奇它的上层是怎么搭建起来的,所以这里的教程不是教你怎么编写安卓程序,首先我不会,其次肯定也不难,但是我会了解为什么可以这样编程。
安卓的架构与JNI

这是很抽象的说法,我以Linux 的角度来说,首先内核就是 Linux 内核,运行的第一个用户程序之后就是安卓的部分了,但是安卓对内核也做了适应性的优化,然后关键的一点,后台有一个虚拟机JVM进程,运行上层的Java 程序,并且和下层的Native Libarary交互。
因为内核的原生支持一定是 C/C++ ,但是上层的程序又想用 Java 编写,所以安卓系统的大量工作就在于一点,搭建一个中间库,我们称它为 JNI

比如最简单的一个 System.out.printl 函数,最终肯定是调用了 Linux 系统调用的 write 函数,但是 Java 如何和 C 交互呢,它们就不是一个时代的产物,在参考书里这么说的:
“绝大数的脚本引擎都支持一个显著的特性,一定要提供一种脚本中调用 C/C++ 编写的模块的机制,才能称得上是一个相对完善的脚本引擎”
所有的程序都离不开操作系统的支持,操作系统的接口都是以 C的规范暴露出来,所以其他语言必须得支持调用C/C++模块!Java当然也不例外,它利用了一个中间库的机制,建立一个 Java 可以识别的(规范的)中间库,然后中间库(C/C++)编写,又可以直接与C/C++的底层库交互。
中间库的存在就是作为一个承上启下的作用,因为Java有自己的规范,而C也有自己的规范,那怎么办,好,找一个代理人,这个代理人就是 JNI
我这里以安卓的设计的JNI为例子,安卓改进之后与传统的有点区别,但是本质估计都没有却别,只要理解原理就是,首先读者先理解一点,函数(方法)只是内存中一个代码段,我们只要知道了它的地址,往栈里放进相关的变量,那么这个函数不管是用什么语言编写的都是无所谓的,只要你遵循了这个函数调用所需的参数。
所以对于Java程序,关键是什么呢,得到自己类里Native关键字声明的方法的目标函数(库)地址,
如果了解 C++ 实现机制的人肯定不觉得奇怪,就算是Java中声明的函数,到了C这里也一定是有参数,因为所谓的面向对象特性(多态性)都是通过指针实现的,对于静态函数,调用的时候没有具体的对象指针(C++的this),其实说明一点,这就是全局函数,而 env 参数,我的理解就是一个线程的标识符,每个线程有不同的值,因为不同的线程会加载相同的动态库,必须得有自己的备份,不能共享,比如那些全局数据,必须有自己的一份,如果有同学了解 mmap 机制,在实现映射的时候,有一个标识符是不能更改,一更改相关数据,马上就执行复制一份新的数据,从而不在共享(类似COW机制)。
注意函数名,必须跟Java的包名相关,这应该是为了防止同名函数的一个措施,那么问题来啦,Java 如何调用 JNI 的函数呢。
动态库的设计很简单,就是一个暴露函数(以及变量)的地址的一段小程序,所以 Java 只需要把它加载到内存当中,分析它的符号表,就可以找到它的函数所在地址,当然 JNI 实现了一个更简单的操作。
在编写完 JNI 的中间函数之后,给每个函数填写一个结构体,用来实现映射
上述的函数省略了一些,在书本p70可以找到详细的,这里关键的一步就是调用了安卓提供了一个接口,将这个表和类结合了起来,其实很容易想到,无非就是 Java 类里面的函数(方法)修改了地址,其值为 gMethods 提供的表,当然 Java 不允许任意修改其函数地址,但是提供了这样一个接口,就好像反射的机制一样,在 C++ 很容易就能修改函数的地址,因为我们拿到指针以后就可以为所欲为了,私有的也不再是私有的。
JNI_OnLoad 函数是 Java 加载运行库时自动调用的,这样的好处就是以后加载运行库之后,一次性把其他 Native 的函数映射全部解决了,不再需要下次再来找。这种思想很常见,还有一种思想就是,不到用的时候都不加载,参考 ld 程序链接动态库的时候,或者内核 COW 还有 page fault 的处理。
总之一句话,JNI 是为了承接 Java 的规范同时又可以调用 C/C++ 的库(模块),然后呢,其实现的原理,就是地址(指针)的查找与赋值,表的存在加速了这一过程。
其实了解函数的本质之后,那些动态库的原理,驱动加载的原理,虽然很复杂,但是本质其实就是一个指针的查找以及挂钩的过程
设备的访问
在linux,编写一个驱动是非常容易的,复杂的地方都在于怎么实现如何于设备正常交互,Linux提供的接口非常容易使用,访问的时候也是抽象为 字符,块以及网络设备,最终都以文件的形式展现出来,安卓在这方面也是类似,但做了一些修改。
安卓系统所使用的内核与传统内核的区别
“ 传统的Linux系统把对硬件的支持完全实现在内核空间中,即把对硬件的支持完全实现在硬件驱动模块中”
“如果安卓系统像Linux系统一样,把对硬件的支持完全实现在硬件驱动模块中,那么必须把这些硬件驱动模块源代码公开,这样可能会损害移动设备厂商的利益,因为这相当于暴露了这些硬件的实现细节和参数”
上述引用中,说明了安卓与Linux的内核其实在驱动实现上是有区别的,但是上述的话可能是作者自己的理解,但是Linux同样有很多手段不暴露驱动的源代码,比如在线更新的时候,是直接加载一个模块的,只要不是要求集成在内核中,或者考虑实现在 initramfs 中,都是一种方案,再者,暴露了驱动的参数其实没有问题,很多设备的设备参数都是开源的,欢迎别人修改和移植,这样才能使自己设备更多人用啊。
安卓在驱动的实现上把需要放置在内核的部分可以屏蔽设备的参数,然后把实现交由用户空间中,然后用户空间以硬件抽象模块(Hardware Abstract Layer)的形式来支持,它封装了实现的细节和参数,这样就可以保护移动设备厂商的利益。(总之,想办法不开源就是)。
这只是作者的理解,其实很多驱动也是要有程序在应用程配套使用的,比如显卡驱动,就不是简单的实现在内核 。但是,在我这几天的阅读看来,Linux 驱动和安卓驱动没有太大的区别,只是安卓下的驱动,除了要包括内核的,还得在 HAL 层封装一个 相当于 Linux 用户层的支持程序,然后在构建 JNI 以及对应的 Java 的基类,让相关的服务可以被用户使用。
安卓程序如何与设备交互
前面提到了,尽管作者说了安卓是改动了,但是我至今还没发现本质的区别,所以我们只需要考虑,内核空间和用户空间是怎么交互就知道答案了。
sysfs
proc
device_node
其实上面这些的本质,都是 VFS 的实现,再本质一点,就是系统调用,只有这个东西,可以改变特权级,mmap 其实也是系统调用,但是它有一劳永逸的效果,所以适合大数据传输。
知道了上面几点,我们就会发现,安卓驱动程序(linux 亦是如此)本质上就是利用了文件操作函数和设备交互的,这里所说的文件操作函数,包括 read,write,open,close,poll,ioctl .... 无论底层怎么包装,最后一定是落实到了这里,只是它们有可能是通过直接读写设备文件,或者读写 sys 文件。
HAL 的封装
HAL 层为什么存在
Hardware Abstract Layer 名字的存在就暗示了它的作用就是要抽象,这是直观的映像,驱动开发人员会把 跟内核设备文件交互的底层函数封装在一个结构体中(以函数指针的形式),最后这个函数的代码以及数据都变成一个动态库,上层访问到相关类的时候,就动态的加载,并完成相关的初始化。
上层只需要调用相关的类就好了,中间 JNI 会完成和这个动态库的交互,所以这一层就是封装了驱动的实现,提供相关的接口,供上层 Java 相关的服务调用。
HAL 的总体设计 - 以 mokoid 为例子
首先了解三个结构体。
注意最上面的注释,所有的 HAL 模块,可以有自己的结构体,但是结构体的第一个域,必须是 struct hw_module_t 而且实例化的那个结构体的名字也必须是 HAL_MODULE_INFO_SYM 这里的作用很明显,首先必须得是这个结构体开头,其实就是一种 继承的关系,而名字必须是规定的,原因在于,动态加载的时候,找到了这个变量名字所在的地方,就知道了模块的全部信息,这俩者是相辅相成的。
struct hw_module_t 大部分都是存储版本信息,看注释就可以理解了,现在来看一个实际的例子。struct hw_device_t 设计同样如此,也是一种继承关系。
上面这个例子就很明显了说明了要求,刚才还有一个 hw_module_method 忘记解释了,里面只有一个 open 的函数,很容易想到,是这个模块被加载的时候调用的。open 函数在下面放出,不然太乱了。
下面说说自己的理解,module 这个结构的存在是为了让这个动态库(模块)可以被识别,它的固定变量名字就有这个好处,然后可以顺利的调用,open 函数,调用了open函数动态分配一个 “设备” 返回给调用它的函数,得到了这个设备,里面就有和内核接口交互的函数了,相当于得到了一个操作句柄。
以上面为例子,就是这个过程
找到 HAL_MODULE_INFO_SYM 变量所在地址
upcast 为 hw_module_t 调用 open 函数
open 函数返回一个设备,上层可以 downcast
限制了第一个域必须是 hw_module_t hw_device_t 目的就是为了方便操作,当然也可以像内核那样,使用一些宏就可以解决,这个其实就是继承的实质。
上层是什么?
注意到这篇文章的结构是自底向上的,因为针对的读者不一样,针对的读者已经学习过传统 Linux 驱动,但是想要了解安卓驱动的朋友。所以从熟悉到陌生,会容易掌握。
上层的存在就是想方设法让 Java 能够调用,第二节的讲述了 JNI 的原理,就是为了这种情况设计的。

上面这张图 有很多名词不懂,没关系我也不懂,来慢慢分析,现在我们到了 系统服务这一层了,直接甩代码,再理解。首先,为了上层 Java 程序可以调用,必须建立 jni 中间层。
Java 和 C 之间桥梁 - JNI
现在我们假设,上面的 HAL 层代码已经被编译成一个动态库,名字叫 libled.so
来看看 Java 和 C 之间的设计
有人问 aidl 的作用,请出门维基,这里没有这么多篇幅啦,总之就是一个接口的设计。
下面来看 JNI 的中间代码,也就是上面 加载的 libmokoid_runtime.so
省略了一下加载的代码,关键我们要理解,Java 里面的 Native 关键字表面的意思就是一个外部的函数的指针,这里一个映射表表明了他们的关系。
关键的一个函数, init() 里面调用的 hw_get_module ta他的作用,是通过传进去的参数,找到模块的动态库的路径,然后 dlopen,然后就是我们在上一节说的 HAL 的作用里面的事情了。
假设上面根据规则 找到了 我们上面说的 led.so 最后的做的事情就是 dlopen
看到此,恍然大悟,万事不过 dlopen ,接着 init 函数调用了 open 函数得到了设备的结构,接着就是把它存储在一个全局变量了。另外俩个开关的函数,只是利用这个全局变量来调用它的接口。
Service 和 Manager
现在忽略底层,我们从 LedService 起步,做一个本本分分的 Java 编程人员。注意它继承了一个基类,也就是刚才说的篇幅不够解释的一个接口,
其实我们 new 一个 LedService 就可以访问硬件了,这是没错的,实际上已经链接在一起了,hw_device_t 可以 downcast 为 led_control_device_t 的一个结构里面就有 set_on set_off 指针,即真正的操作。
但是这不是规范,我也不知道怎么解释其中的缺点,我个人拙见,是因为不好管理,如果每一个使用 led 的人都 new 一个对象,其实不能保证它们的并发访问的安全?底层的加载代码保证的是不同的线程只 dlopen 一次,即不同的线程都有 刚才 JNI 层里面提到的那个全局变量,但是相同的线程其实都在访问一个地址,所以我认为有点隐患。
所以正确的方案?就是所有的访问都调用一个接口,一个可以管理的接口。
这里实例化了一个对象,并把它注册在了 ServiceManager 上,一个静态的全局对象,以后所有的访问都经过它就好了。添加服务这种说法,其实就是一个进程间通信的幌子,大家都在操作系统里注册一个位置,然后别人访问的时候,操作系统就负责通信。进程间通信的手段很多,不过这里不是我们的重点。
除了要有一个 服务,还需要有一个服务的管理者
如此这个管理者,每次跟系统的服务管理者拿到一个服务,同时又把它 upcast 到当时它注册到的基类,借此来调用。
应用程序的调用
那么我们用户如何做呢,只需要 new 一个 LedManager
看,底层做了这么多的事情,都是为了上层的便利呀 O(∩_∩)O
总结
终于到了敲黑板环节,又到了结尾了。自己对着掌握与否打勾。
注意 3,4,5 这一层我们称之为(Framework)框架层,一切为了用户的层,而 HAL 和 内核就很容易辨识了。
Last updated