跳转至

翻译:Ross.Zeng 校对:

Linux 内核

Linux内核是所有传统上标记为Linux发行版的中心部分,包含了如Ubuntu、Fedora、Debian这些主流发行版。虽然linux内核归档在香草版本中提供,但大多数发行版在发布给用户之前都打上自己的补丁去修复漏洞,或是提高性能。安卓也一样,开发人员们也修复了内核来满足他们的需求。

然而,安卓并不按常规操作,能够在内核中找到一些与“香草”明显不同的自定义功能。事实上,linux发行版中的内核可以很容易地被kernel.org中的内核替换,并对发行版中其他的组件几乎没有影响,而安卓的用户空间组件只有在“安卓化”的内核中才能够运行。在之前的章节中也有提及,总的来说,安卓的内核是主线上的分支。

讨论Linux内核已经超出了本书的范围,但是我们还是看一看这些“安卓主义者”向内核中添加了哪些东西。你可以通过Robert Love的linux内核开发(第三版),以及订阅linux周报(LWN)来了解内核的内部信息。LWN包含了几篇关于内核结构的重要文章,并且提供了linux内核开发的最新信息。

请注意下面的小节只包含了最重要的部分,安卓化的内核通常在标准内核基础上打了上百个补丁,用于提供特定设备功能,修复以及增强。可以使用git在http://android.git.kernel.org中对安卓内核与主线内核进行详尽分析。另外,如PEME驱动程序是某些安卓内核的特定功能,不一定会在所有的安卓设备中使用。

唤醒锁

在所有安卓主义中,这或许是最具争议的。将这个功能包含进主线内核讨论了将近2000封邮件,但仍然没有明确的路径去合并唤醒锁功能。

理解唤醒锁是什么、能做什么,我们必须先讨论linux中的电源管理。最常见的就是笔记本电脑,当运行linux的笔记本电脑盖子合上时,操作系统通常会进入“暂停”或“休眠”模式。在这种模式下,操作系统的运行状态被保存在RAM中,其他的硬件功能都被关闭。因此,电脑可以尽可能少地使用电池。当盖子打开时,笔记本电脑就会“醒来”,用户几乎可以立即开始使用。

这种操作方式在笔记本电脑和桌面设备上应用相当出色,但是并不适合如手机这样的移动设备。因此,安卓团队设计了一个机制,稍稍改变了一下规则使得其更加适合移动设备场景。安卓化的内核并不是让系统按照用户命令去休眠,而是尽可能快地休眠。只是在重要进程执行或应用程序等待用户输入时防止系统进入休眠。唤醒锁就是用来保持系统的唤醒状态。

唤醒锁和提前休眠功能构建在linux现有的电源管理机制之上。然而,他们引入了完全不同的开发模式。因为应用程序和驱动开发人员必须在用户进行关进操作或输入时抓住唤醒锁。通常,应用程序开发者不需要直接处理唤醒锁,因为他们抽象出的相关操作会自动处理锁定事件。当然,他们也可以直接向电源管理服务申请唤醒锁。另一方面,驱动开发者可以调添加到内核中的唤醒锁原语去申请和释放唤醒锁。而在驱动中使用唤醒锁的缺点就是不能够将这个驱动推到主线内核中,因为主线内核不支持唤醒锁。

低内存杀手

如之前所提及的,安卓行为很大程度上取决于低内存条件。因此,OOM的行为是至关重要的。所以安卓研发团队在内核OOM杀进程之前添加了一个额外的低内存杀手。安卓的低内存杀手按照应用程序开发文档中描述的策略,淘汰了那些长时间未使用且优先级不高的组件进程。

LMK基于linux中OOM调节机制,可以支持对不同进程采用不同OOM杀手优先级。基本上,OOM调度允许用户空间控制一部分内核OOM杀除策略。OOM调整范围在-17到15之间,数字越高意味着在系统内存不足时,关联进程更容易被杀掉。

因此安卓将不同的OOM等级做了调整,根据运行的组件归纳到不同类型的进程上,并按照进程类型给自己的LMK配置不同的阈值。这让安卓的策略有效取代了内核的OOM killer,使用达到阈值的方式踢掉进程,而不是等到系统内存用尽时再开始处理。

用户空间是初始化阶段被init进程启动(见47页init章节),并在运行时由活动管理服务重新适配及局部加强,这也是系统服务的重要部分之一,负责执行先前介绍的组件生命周期以及其他许多工作。

Binder

Binder是一种类似Windows下RPC/IPC的机制。它的起源最早可以追溯到Be被Palm收购之前的BeOS中。它在Palm继续开发,并作为Open-Biner项目被发布。尽管Open-Biner并没有作为一个独立的项目存活下来,但是像Dianne Hackborn和Arve Hjonnevag这些核心开发者,最终加入了安卓开发团队。

安卓的Binder机制受到了先前工作的启发,但是安卓的实现并不是直接从OpenBinder代码中直接提取,相反,它重写了OpenBinder的功能子集。如果想了解这个机制的基础及其设计理念,那么OpenBiner的文档还是必读的。

本质上,Binder试图在经典操作系统之上提供远程对象调用功能。换言之,Binder是去“尝试接受并超越经典操作系统”,而不是重新设计它们。因此,开发者们只需要面对远程服务这个对象,而不是去面对一个全新的操作系统。那么通过添加远程可调用对象扩展系统功能就比实现新的守护进程更加容易,非常符合UNIX哲学。远程对象可以用任何所需的语言实现,并可以与其他远程服务共享相同的进程空间,或是拥有自己的独立进程。使用时只需要定义接口及引用方法即可。

正如在图2-1所示,Binder是安卓架构的一个组成部分。它允许应用程序与系统服务及其他服务组件对话。不过,如之前提到的,应用开发人员并不会直接与Binder对话,相反,他们使用aidl工具生成的接口与stubs。就算程序与系统服务对接,安卓API也会将这些服务抽象,开发人员不会直接看到Binder的实际调用。

Binder机制的内核驱动是一个可通过/dev/binder访问的字符驱动程序。它使用ioctl()方法在通讯双方间传递数据包。它也允许一个进程将自己定义为“上下文管理器”。上下文管理器的重要性以及Binder驱动的实际用户空间使用将会在本章稍后部分详细讨论。

匿名共享内存(ashmem)

另一个在大多数操作系统中使用到的IPC机制就是共享内存。在Linux中,这一般由System V IPC机制中的POSIX SHM功能提供。如果你看过AOSP中的ndk/docs/system/libc/SYSV-IPC.html文件,你会发现安卓开发团队似乎并不喜欢SysV IPC。实际上,文件中的一个论点是,在Linux中使用SysV IPC机制可以导致内核资源泄漏,继而让恶意软件或行为不当的软件跑挂系统。

虽然并没有任何安卓开发人员或ashmem代码或者文档中这样声明,ashmem很可能将其存在的愿意归功于安卓团队看到了SysV IPC的缺点。所以ashmem被描述为与POSIX SHM相似“但有着不同的行为。”例如当所有进程退出时,它会引入内存销毁区域计数,并在系统需要内存时收缩映射区域。它还启用了内存压力,“去掉”一个区域允许它被收缩,而“固定”则不允许被收缩。

通常,第一个进程通过ashmem创建一个共享内存区域,并使用Binder与其他愿意共享区域的进程分享相应的文件描述符。例如Dalvik的JIT代码缓存通过ashmem为Dalvik提供实例。有许多系统服务组件,如Surface Flinger和Audio Finger也都依赖ashmem,尽管通过IMemory接口而不直接使用ashmem。

~§IMemory是一个仅在AOSP中可用的内部接口,不提供给应用发开人员。给app使用的最接近的类是MemoryFile。~

闹钟

将闹钟驱动加入内核则是另一种情况,默认的内核功能并不完全满足安卓的需求。安卓的闹钟驱动实际上在内核已有的实时时钟(RTC)与高精度计时器(HRT)的上层。内核的RTC功能为驱动开发人员提供了一个框架,用于实现定制的板级RTC功能,而内核通过主RTC驱动提供了一个独立与硬件的接口。另一方面,也允许调用者在特定的时间点被唤醒。

在香草内核中,应用开发人员通常会调用setitimer()系统方法来获取一个超时信号。系统调用允许几类计时器,其中一种是ITIMER_REAL,使用了 内核高精度计时器(HRT),但是不能在系统被挂起时使用。换言之,如果应用使用setitimer()在给定的时间后请求被唤醒,在此期间设备被挂起,应用只会在设备被唤醒时才能收到信号。

除了setitimer()系统调用,内核RTC驱动可以通过/dev/rtc访问,并允许用户使用ioctl()方法,设置一个闹钟将被系统的RTC硬件设备激活。无论系统是否被挂起,这个闹钟都会触发。因为它基于行为或RTC设备,即使系统其他部分被挂起,它依旧可以保持活动状态。

安卓的闹钟驱动聪明地结合了最好的部分。驱动默认使用内核的高精度计时器为用户提供闹钟,就像构建在内核的计时器功能。如果系统被挂起,就会调用RTC让系统在指定时间被唤醒。因此,用户空间的应用无论在何时需要一个特定的闹钟,只需要让安卓的闹钟驱动在指定时间唤醒,无需关注系统是否是挂起的。

在用户空间,闹钟驱动以/dev/alarm的字符设备体现,允许使用者通过ioctl()设置闹钟并调整系统时间。有几个关键的AOSP组件依赖/dev/alarm。如Toolbox和SystemClock类,能够通过应用接口获取或设置系统时间。更为重要的是,作为系统服务的一部分,应用开发人员调用的AlarmManager类,就是闹钟管理服务通过这种方法为应用提供了闹钟服务。

无论驱动还是闹钟管理都使用唤醒锁机制来保持闹钟和其他安卓唤醒锁相关行为之间的一致性。因此,当闹钟触发时,这个消费者应用程序有机会在系统下一次挂起前做任何操作,如果有必要的话。

日志系统

日志是目前包括嵌入式系统中,任何Linux系统上的必备组件。通过实时或出现问题后分析系统日志,可以在警告与错误中定位到致命问题所在,尤其是瞬间发生的错误。大部分Linux发行版都自带两套日志系统:通过dmesg命令访问的内核日志与以文件形式存储在/var/log路径下的系统日志。内核日志通常由各处printk()、核心内核代码或设备驱动调用输出。相对而言,系统日志包含的信息来自于运行在系统中各种守护进程与应用实时消息。实际上,你可以使用logger命令将自己的消息发送至系统日志中。

就安卓而言,日志的功能就是原样使用。然而,安卓的日志系统软件包无法在大多数Linux发行版中找到。相反,安卓定义了自己的日志机制,在内核中添加了安卓日志驱动。syslog依赖于通过套接字发送消息,并以此引出了任务开关。它还使用文件存储信息,这就涉及到存储设备的写操作。反之,安卓日志功能管理了一系列独立的内核缓冲区,用于记录来自用户空间的数据。因此,事件的记录无需任务切换或文件写入。相反,驱动维护的循环缓冲区能够记录记录每个事件并立即返回给调用方。

正因为其轻量而高效的设计,安卓日志才能真正在运行时以用户空间组件规律地记录事件。实际上,对应用开发者开放Log类,或多或少直接授权了日志驱动写入主事件缓存。显然,所有的好东西都会被滥用,最好能够保持日志框架轻量。但是综合考虑应用程序API公开Log,以及AOSP本身对日志的使用,如果基于syslog则很难维持这样的使用级别。

图2-2描述了安卓日志框架的细节。如图所示,日志驱动是所有日志相关功能得以运行的基础核心模块。在/dev/log/目录下各个缓冲区作为单独的条目。然而,没有用户空间组件直接与驱动交互。相反,他们都依赖liblog提供一系列不同的日志功能。根据功能使用与传递的参数,事件会记录到不同的缓存中。LogSlog类使用liblog库,例如首先检测事件是否来自音频相关模块,如果是,则事件被分发至音频缓存区。否则,Log类会将事件传给“主”缓冲区,此时Slog类再将事件给“系统”缓冲区。主缓冲区事件在没有任何参数的情况下会在logcat命令中显示。

Figure 2-2. 安卓的日志系统框架

LogEventLog类都可以通过应用的API调用,但Slog只能由AOSP内部使用。尽管EventLog开放给了应用开发人员,但文档中明确说明了其主要开放给系统集成商而非应用开发者。事实上,绝大部分开发代码样例中都使用了Log类。通常,EventLog是系统组件使用,尤其是系统服务器主机?类服务将结合LogSlogEventLog来记录各类事件。与应用开发人员相关的日志可能使用Log,而与平台集成相关的日志就可能使用SlogEventLog

要注意的是,logcat这样的实用工具也依赖于liblog。除了添加访问日志驱动的接口,liblog还提供了美化打印与过滤器格式化事件功能。另一个特性是,liblog要求每一个事件需要标明优先级、标签与数据。优先级可以选择verbosedebuginfowarnerror。标签是一个唯一的字符串用于区分日志属于不同的模块或组件。数据就是需要被记录的具体信息。

现在最后一块拼图就是adb命令了,在后文中会讨论到,AOSP包含了安卓调试通道(ADB)的守护进程,可以通过adb命令行工具在主机端访问安卓设备。当输入adb logcat时,守护进程实际上在本地发送logcat命令,获取主缓冲区输出并推到主机的终端中显示。

其他值得一提的安卓主义

除了上文所描述的部分,还有一些其他“安卓派”的设计值得一提。

Paranoid网络

在Linux中,所有进程都被允许创建进程并连接网络。在安卓的安全模式下,网络访问是受到控制的。因此,内核添加了一个选项,通过判断当前进程属组或能力来筛选套接字创建及网络接口管理的访问。这套机制在IPv4,IPv6和蓝牙上都适用。

RAM控制台

正如之前提到的,内核管理自己的日志,可以使用dmesg命令访问这些日志。日志内容非常有价值,其中常常包含驱动或内核子系统的关键信息。在崩溃或内核挂逼时,日志可以帮助问题的事后分析。由于这些信息常常在重启后丢失,安卓添加了一个驱动注册基于RAM的控制台,控制台在重启后仍然存在,并可以通过/proc/last_kmsg访问日志内容。

物理内存(pmem)

正如匿名共享内存,物理内存也驱动允许进程间共享内存。但不同的地方在于,pmem允许共享物理上相连的大片内存,而非虚拟内存。此外,这些内存区域也可能在进程与驱动间共享。例如HTC G1手机上就使用pmem进行2D硬件加速。但需要注意,pmem并不适用于所有设备,实际上,来自安卓内核团队的Brain Swetland说,它是专门针对MSM7201A(G1的处理器)编写。