14.13 Android
上一节我们还在 PPPoE 的世界里琢磨怎么把以太网包装成点对点通道,这一节,我们要把视角拉高,看看 Linux 内核 networking 在移动端最大的「租客」之一——Android。
Android 是个有点特殊的租客。它住在 Linux 内核这栋楼里,但把里面的装修风格改了不少,甚至有些地方加了自己的门锁。
14.13.1 内核之上的帝国
最近这些年,Android 已经毫无疑问地统治了移动操作系统。它的底层是 Linux 内核——准确地说,是被 Google 工程师魔改过的 Linux 内核。
绝大多数 Android 设备都跑在 ARM 芯片上(虽然也有个叫 android-x86 的项目试图把它搬到 Intel 的 CPU 上,第一代 Google TV 也是基于 x86,但后来也转回 ARM 了)。Android 最早是 2003 年 Andy Rubin 在加州创办的,2005 年被 Google 收购。2007 年,开放手机联盟(OHA)一帮公司一起把它推到了台前。
虽然它是开源的,代码挂在 Apache License 下,但它的开发模式和 Linux 社区完全是两个物种。
Linux 的开发是在阳光下进行的——公开的邮件列表,任何人都可以发补丁,大家(在 CC 列表里)吵架,然后合并代码。Android 不是这样。它的开发大多是在 Google 紧闭的大门后面进行的。你当然可以去 Gerrit 提交补丁,但最后能不能进主干,全看 Google 的心情。这种「虽然开源但并不那么开放」的模式,让 Android 内核和主线 Linux 之间总是隔着一层纱。
尽管如此,Google 对主线 Linux 的贡献是巨大的。我们在这一章前面提到的 Cgroup 子系统,最早就是 Google 人搞出来的。网络方面还有两个重磅补丁:RPS(Receive Packet Steering)和 RFS(Receive Flow Steering),由 Google 的 Tom Herbert 提交,在 2.6.35 内核合入。这两个东西让多核系统处理网络包时更聪明——不是傻乎乎地让中断乱飞,而是根据流量的 hash 把包分发到特定的 CPU 上。
这背后有个朴素的道理:当你手里有几百个 CPU 核心时,如何让网络包顺滑地流过这些核心,且不打架,是个巨大的工程挑战。Google 把它在数据中心里攒下的经验回馈给了社区。
至于 Android 内核本身,虽然有很多代码躺在 Linux 内核的 staging 目录里,但要完全合并进主线,目前看还是个进行时的任务。历史上,Google 搞了不少自己的轮子,比如 WakeLocks(唤醒锁)、Binder(基于轻量级 RPC 的 IPC)、Ashmem(匿名共享内存)、Low Memory Killer 等。这些东西让内核社区的维护者们很头大,尤其是 WakeLocks,2010 年直接被拒了。
但情况在慢慢变好。有些特性已经被接纳,或者被更通用的机制取代(比如 Autosleep)。Linaro 这个非营利组织在其中起了不少推动作用,协调各家厂商把改动推到上游。
不过,深究 Android 内核的实现细节和主线化的过程,那就超出本书的范围了——那几乎是另一本书的厚度。
14.13.2 用户空间的网络困局
既然内核还是那个内核(差不多),那 Android 网络有什么好谈的?
有。Android 的网络大坑,不在内核,而在用户空间。
Android 极度依赖 HAL(Hardware Abstraction Layer),网络也不例外。这就导致了一个很反直觉的现象:哪怕你内核里的驱动写得完美无缺,只要上面的框架不认,你就是连不上网。
在早期的 Android 版本(一直到 4.2),框架层对以太网的支持几乎为零。如果你把驱动编译进内核,底层的 TCP/IP 协议栈确实能工作,你也能通过 ADB(Android Debug Bridge)看到网络通了,但 Android 系统自己并不知道这回事。
这种状态一直持续到 4.0 之后,android-x86 项目才强行加了一个以太网的实现(虽然设计得挺粗糙,但能用)。到了 4.2,官方源码终于支持以太网了——但仅仅是「支持」而已。它能检测到网线插拔,如果对面有 DHCP 服务器,它能拿到 IP。但如果你想配置静态 IP、想设代理、想强制所有 App 走这个接口……抱歉,没门。或者更准确地说,门是锁着的。
如果你真的需要像正经 Linux 那样配置网络(配置接口、DHCP/静态切换、设代理),你还得打一堆补丁,甚至去改框架层。
更糟糕的是,Android 的网络管理相当「独裁」。它通常只允许一个接口活动。哪怕你有 eth0 和 eth1,你也别指望能像路由器那样同时用两个口。Android 是手机系统,不是路由器系统,这种限制虽然让人不爽,但也符合它的产品定位。
14.13.3 四个典型的 Android 特殊化
为了让你在调试 Android 网络时不至于一头雾水,这里有四个必须知道的差异点。
1. 「多疑」的网络
如果你写了一个原生 Linux 的网络程序,把它扔到 Android 上跑,你可能会惊讶地发现——它被拒绝了。
在标准 Linux 上,任何程序都可以调用 socket() 打开一个套接字发数据。但在 Android 上,不是谁都有资格玩网络的。
Google 加了一个叫「Paranoid Network」(偏执网络)的安全机制。这玩意儿通过 GID(Group ID)来过滤进程。如果一个进程不在特定的组里,内核会直接拒绝它的网络操作。
这就是为什么你在写普通的 Android App 时,必须在 AndroidManifest.xml 里加那行 <uses-permission android:name="android.permission.INTERNET" />。这行字不是写给 Java 虚拟机看的,最终是传到底层内核去检查你的 GID 的。
这个机制因为太特立独行,想要合并进主线 Linux 难度很大。
2. Bluedroid 取代 BlueZ
我们在前面蓝牙那一节讲了 Linux 的 BlueZ 协议栈。但在 Android 4.2 之后,Google 决定不再用 BlueZ,而是换成了 Bluedroid。
这是一个基于 Broadcom 代码的蓝牙协议栈。换的原因有很多,性能、功耗、代码维护等。对于开发者来说,这意味着你在 Android 上看到的蓝牙实现和你在 Linux 笔记本上看到的不太一样。
低功耗蓝牙(BLE)的支持是在 Android 4.3(API Level 18)加进去的。在那之前,AOSP 里没有原生 BLE 支持,只能靠各家厂商自己搞的 API。
3. Netfilter 的扩展:xt_qtaguid
数据流量统计是手机系统的一个重要功能——「我这个月用了多少流量?」、「哪个 App 最费流量?」。
为了干这事,Google 给内核的 netfilter 子系统加了一个模块叫 xt_qtaguid。它的作用是让用户空间的应用给自己的 socket 打标签。
有了这个标签,内核在处理包的时候,就知道这个流量是属于哪个 App 的。这样,统计就精确到了应用级别。这个改动同样涉及到底层 Netfilter 的修改,Google 也尝试把这些补丁推到 LKML,虽然合并之路比较坎坷。
4. NFC 的用户空间实现
我们在前面 IEEE 802.15.4 和 NFC 那一节提到过 NFC 的分层。在 Android 上,NFC 的架构做了一个选择:协议栈的大部分逻辑放在了用户空间。
具体的实现是跑在用户空间的,通过 HAL 和底层的 Broadcom 或者 OEM 提供的驱动通信。这种设计和传统 Linux 设备驱动把所有逻辑都塞进内核的做法不太一样。
14.13.4 进阶资源推荐
虽然网上教你怎么写 Android App 的书多如牛毛,但讲 Android 内部实现(尤其是底层和内核交互)的资料少得可怜。如果你真的想深入这块,以下是一些硬核资源:
书籍:
- Embedded Android: Porting, Extending, and Customizing by Karim Yaghmour(O'Reilly Media, 2013)。这书是老牌经典,讲怎么把 Android 移植到新设备上。
幻灯片(Slide):
- Android System Development by Maxime Ripard, Alexandre Belloni(free-electrons.com):超过 400 页幻灯片,全是干货。
- Android Platform Anatomy by Benjamin Zores:解剖 Android 平台架构。
- Jelly Bean Device Porting by Benjamin Zores:讲怎么移植 Jelly Bean 的实战流程。
会议与网站:
- Android Builders Summit (ABS):每年一次的盛会,去不了就看视频和幻灯片。
- XDA Developers Conference:极客们的聚会,很多深度的 hack 分享。
- Android internals forum:gmane.comp.handhelds.android.platform,虽然有些年头了,但历史讨论很有价值。
最后,记得 Android 的源码都在 android.googlesource.com。不过因为项目太大,被拆成了几百个 git 仓库。Google 用了一个叫 repo 的 Python 工具来管理这堆仓库——如果你打算下载源码自己编译,先学会用 repo。
本章回响
至此,我们这一章的旅程就真的到头了。
回想一下,这一章我们跨了很大的步子:
- 我们从 Namespace 开始,建立了隔离的视角。
- 用 Cgroup 给资源套上了缰绳。
- 跑到 蓝牙、802.15.4 和 NFC 的无线世界里看了看那些奇怪的协议。
- 还钻进了内核的 通知链 和 PCI 子系统。
- 最后,我们看了一眼 PPPoE 和 Android 这种把底层技术包装成上层平台的复杂案例。
还记得这一章开头我们说的吗?Linux 网络子系统是一个巨大的机器。这一章里的每一个小节——无论是多核的 RPS,还是 Android 的 HAL——其实都是在回答一个问题:当这个机器变得更复杂、更多核、更移动化时,我们要怎么驾驭它?
现在书已经翻到了最后一页。Linux 网络是一个浩瀚的海洋,新的特性还在不断地涌入内核主线。希望这本书不仅能让你学到几个 API 或几段代码,更能给你一张地图,让你在以后面对 ethtool 的输出或者内核崩溃日志时,知道该往哪里看,该问什么问题。
调试愉快!