写在前面:这是本系列的第二十八篇。

虚拟化和容器技术使我们可以管理成千上万台计算机,为小到一个班集体、大到全世界的人提供各种云端服务。而连接每一个人的,不仅是云端的超级分布式应用,还有万千的终端物理设备。

虽然通用的 Linux、Windows 操作系统的 API 完全可以胜任管理这些设备,但面向特定领域的操作系统,和数据中心里的巨兽相比,又有着极其独特的设计哲学。

本讲内容:嵌入式(实时)和移动操作系统的设计与实现原理。每个人都可以是生活的黑客!

在这里插入图片描述

嵌入式操作系统 (Embedded OS)

我们之前学的都是通用操作系统,这里介绍一下特定领域的操作系统。

嵌入式的操作系统通常更关注并发特性与绝对的可靠性。
为了达到这个目的,它们甚至愿意放弃一些诸如高吞吐量 I/O 读写效率之类的通用性能。

什么是嵌入式系统?

“Embedded”: 计算机系统是“嵌入”到特定物理设备中的。

  • 人造卫星、工业控制 (工控)、家用电器、医疗器械、穿戴设备……
  • 核心要求:体积小、功耗低、可靠性极高。
  • 操作系统通常更简单 (因为领域限定);当然,经过高度裁剪的 Linux 也是可以跑在嵌入式设备上的。

例子:PLC (Programmable Logic Controller) 和工控

自动逻辑控制设备,支撑了现代工业的自动化流水线。

典型设计:Event-based (事件驱动) 与 Real-time (实时)

  • 通常有严格的实时 (real-time) 保障:
    • 必须在严格的时间限制内对外部事件作出可预测的响应。晚了一毫秒,可能就是车毁人亡。
  • 例子: 无论今天的新能源汽车车机屏幕多么花哨(跑着 Android 甚至玩 3A 游戏),最终负责底盘实时控制的,仍然是底层的 MCU 和硬实时操作系统。

(还记得导致至少 6 人死亡的 Therac-25 医疗事故并发 Bug 吗?这就是缺乏硬实时安全保障的代价)

例子:FreeRTOS

A FreeRTOS application will start up and execute just like a non-RTOS application until vTaskStartScheduler() is called.

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,                 // 函数指针
    const char * const pcName,                 // 名称
    const configSTACK_DEPTH_TYPE uxStackDepth, // 堆栈深度
    void *pvParameters,                        // 参数
    UBaseType_t uxPriority,                    // 任务优先级!(核心)
    TaskHandle_t *pxCreatedTask);              // 返回句柄
  • 这个 API 和普通的 thread.h 里的 spawn 差不多。
  • Task 通常是一个死循环。
  • 操作系统按绝对的优先级唤醒阻塞的 Tasks(高优先级无条件抢占低优先级)。
  • 虽然它没有绝对的 Hard real-time (硬实时),但严格的优先级机制能实现极强的控制力

优先级带来的致命麻烦:Priority Inversion

def jyy():
    set_priority(1)             # 优先级最低
    mutex_lock(restroom_lock)   # 拿到了厕所的互斥锁

def 马院长():
    set_priority(10)            # 优先级中等
    long_work()                 # 执行极其耗时的无锁计算任务

def 校长():
    set_priority(100)           # 优先级极高
    mutex_lock(restroom_lock)   # 想要进厕所,被锁住了,只能等待

如果只有一个处理器,会发生什么灾难?

  • jyy (低优先级) 拿到了锁。
  • 校长 (高优先级) 想要锁,被阻塞,只能等 jyy 用完。
  • 此时 马院长 (中优先级) 突然来了,因为他的优先级比 jyy 高,他直接抢占了 CPU,开始执行漫长的 long_work()
  • 结果:jyy 被赶下处理器无法释放锁 $ \rightarrow $ 校长 作为最高优先级,被迫一直等待中优先级的 马院长 执行完毕!
  • 这就是著名的 “优先级反转 (Priority Inversion)”

这个“事故”曾经在火星上真实发生过

Sojourner “探路者” (PathFinder),1997 年 7 月 4 日登陆火星。

  • Lander (登陆舱): IBM Rad6000 SC (20 MIPS), 128 MiB RAM, 6 MiB EEPROM。
  • Rover (火星车): Intel 80C85 (0.1 MIPS), 512K RAM, 176K Flash。
  • 运行着 VxWorks “实时” 任务操作系统。

任务优先级分配:

  • ASI/MET task: 大气成分监测 (低优先级)
  • bc_dist task: 分发任务 (中优先级)
  • bc_sched task: 总线调度 (高优先级)

着陆后开始出现系统疯狂重启。原因正是优先级反转触发了硬件的看门狗 (Watchdog) 定时器,系统认为严重故障,被迫硬重启。

  • (低优先级) select $ \rightarrow $ pipeIoctl $ \rightarrow $ selNodeAdd $ \rightarrow $ mutex_lock
  • (高优先级) pipeWrite $ \rightarrow $ mutex_lock
  • 最终 JPL 工程师通过远程发送补丁,开启了互斥锁的 “优先级继承 (Priority Inheritance)” 特性(低优先级拿到锁后,临时提升到等待该锁的最高优先级),才挽救了这台价值上亿美元的火星车。

移动操作系统 (Android 演进史)

人手一个的“手机”

  • 曾经的手机可不是今天的样子!

我上大学的时候在用什么手机?

  • Nokia 5310 XpressMusic (2007): 320 x 240 屏幕; 200 万像素摄像头; 30 MB 存储(这在当时已经算是非常精细了)。
  • 运行着 Symbian S40 系统(相比于“功能机”已经是巨大的进步了)。

Symbian 和 J2ME 的时代

  • 没变的是:大学上课和现在一样无聊。
  • The K Virtual Machine (KVM) & J2ME。当时的高端玩家都在折腾如何用极小的 KVM 在几十 KB 内存里跑 Java 游戏。

那个男人改变了世界

  • 2007 年,乔布斯发布了初代 iPhone。

那个男人遇到了对手

  • Google 2005 年秘密收购了 Android Inc.,初创团队包含一些极其硬核的 BeOS 操作系统的成员(例如牛逼的 Binder IPC 机制就源自 BeOS)。

Android 的兴起:高瞻远瞩的豪赌

Android 架构:Linux + Full JVM + Framework API + 开放平台

  • 早期的 Dalvik VM 虚拟机 $ \rightarrow $ 后来演化成了 Android Runtime (ART)。
  • 那么,代价呢?卡到爆啊!
  • 第一代安卓机 HTC G1 使用的是 Qualcomm MSM7201 处理器。
  • 32-bit ARMv6 @ 528MHz (单核 CPU, TSMC 90nm工艺)。
  • 顺序八级流水线 (IPC < 1),拖着沉重的 Java 虚拟机,连滑动地图都会卡成 PPT。

但这其实是一个高瞻远瞩的决定:他们赌摩尔定律会生效!

  • 2008 年同年,Intel 桌面端已经发布了恐怖的 i7-965 (4C8T, 6 发射)。移动端算力大爆炸只是时间问题。
  • 对手们的下场:
  • Symbian:暴毙于 C++。C++ 代码谁写谁知道,指针飞舞,满地 Bug。
  • Windows Phone:暴毙于封闭和缓慢的反应。
  • (其实也许只要熬到了移动网页时代,大家就都能活下来了)
  • Android 选择了 Java。2005 年的互联网时代,Java 已经涌现出极其强大的生命力和生态。借着海量 Java 开发者的东风,虽然早期卡顿,但它硬生生熬死了 Symbian 和 Windows Phone。最终形成了如今 Android 和 iPhone 两强争霸的格局。

One More Comment (大模型时代的隐喻)

  • 初期的“低性能”总会解决的。算法 (软件) 会进步,硬件也会进步(摩尔定律还没死呢!)。
  • 在今天的大语言模型 (LLM) 时代,这同样成立:
  • 今天 600B 庞大模型才能做到的智能,通过算法演进,明天 30B 的小模型也能做到。
  • 一定存在一个临界点,能实现端侧“实时推理无处不在”。

Android Apps 的真面目

  • 首先,它是一个 Java 程序 (Kotlin)。
  • 其次,它有独特的生命周期运行模型。
  • Android Platform APIs
  • android.view.View: “the basic building block for user interface components” (UI 组件的基石)
  • android.webkit.WebView: 嵌入应用的网页引擎
  • android.hardware.camera2: 相机控制
  • android.database.sqlite: 原生内置的数据库
  • 四大组件: Activity (活动), Service (后台服务), ContentProvider (数据提供), BroadcastReceiver (广播接收)。

Platform API 之下:一个“微内核”架构

核心黑科技 1:Binder IPC

Remote Procedure Call (RPC) 远程过程调用:

  • remote.transact()

在性能优化和易用之间的极致权衡:

  • 相比于 UNIX 传统的管道 / 套接字 (Socket),它们太“底层”了,手工管理序列化极其痛苦。
  • Binder 基于共享内存实现 (Linux Kernel binder driver)。
  • 实现了绝无仅有的 “Copy once” (只需一次内存拷贝),性能碾压传统 IPC。
  • 自带服务端线程池和高效的注册机制。

核心黑科技 2:Activity Stack (活动栈)

  • 当你在微信里点击链接拉起知乎,看完后按返回键,知乎会销毁并优雅地返回到微信界面。
  • 这种行为在用户“切换应用”后仍能完美保持,这与传统桌面操作系统的窗口管理器(Alt + Tab)逻辑有着本质的体验区别,极大地适应了移动端的小屏操作流。

每个人都可以是生活的黑客 (Hacking Android 😈)

Android: 不只是应用生态,更是极客生态

  • UNIX 哲学:POSIX system calls $ \rightarrow $ libc $ \rightarrow $ coreutils $ \rightarrow $ 应用程序世界(幕后是 gcc, gdb, vscode)。
  • Android 也为我们留下了极其丰富的后门(开发者工具):
  • Android Debug Bridge (adb): 神器中的神器!
  • adb push/pull/install 管理文件和应用。
  • adb shell 获取底层终端 (比如 screencap /sdcard/screen.png 瞬间截图)。
  • adb forward, logcat 抓取日志。

我们可以做什么花活?

1. scrcpy

  • 利用 ADB 在设备上捕获高帧率视频流 (通常有硬件加速),通过 USB 极低延迟投屏到电脑 Host 端进行控制。

2. UI Automation (Monkey 测试)

  • 无限猴子定理: 给 10,000 只猴子和打字机,有一只能盲打出 C 程序,其他只能打出 Perl 乱码。
  • 让“电子猴子”去疯狂随机点击、滑动屏幕,用来测试我们的应用会不会 Crash,这就是高强度的自动化鲁棒性测试。

甚至可以“任意改变” App 的行为 (Hook 注入)

  • LSPosed: 强大的 Xposed 框架分支,可以在运行时动态劫持和修改任意 Java 方法的返回值(比如拦截广告、伪造定位、微信自动抢红包)。
  • 现在甚至有 VirtualXposed 等技术,利用双开容器的原理,连 Root 权限都不需要就能实现 Hook。

极客实战:保活与反杀 (Diehard Apps)

如何彻底杀死一个 Android 进程?

  • 读读内核源码 ActivityManagerService.java
  • Android 为了安全,给每一个 App 都分配了一个独立的沙箱 uid
  • 遍历进程表,找到属于这个 uid 的所有进程组。
  • 执行 Process.KillProcessGroup
  • 为了防止那些生命力顽强的流氓 App,系统会间隔 5ms,连续发送 40 次 SIGKILL 信号,确保彻底斩草除根!

科普:kill -9** 代表了什么?**
普通的 kill 默认发送 SIGTERM(信号编号 15),进程可以捕获它,并在退出前优雅地保存数据。
kill -9 发送的是 SIGKILL 信号。这是一个底层的强杀命令,进程根本无法捕获或忽略,连喊救命的机会都没有,直接被内核干掉。

黑客的反制:利用数据竞争进行进程保活!

  • 既然系统要杀我,那我就利用并发漏洞!
  • 让子进程故意成为“孤儿进程”(脱离原本的进程组),从而在组强杀中幸存,逃避 SIGKILL 信号。
  • 利用文件锁互相监控。一旦主进程被杀死,孤儿进程立刻在极短的时间窗口内重新拉起另一个主进程!
  • 于是,手机里出现了连系统强制停止都杀不死的僵尸 App。“Diehard apps” (EuroSys’19 顶级学术会议论文探讨了这种奇观)。

在 Android 的浪潮里折腾

  • 作为系统程序员,我们也想过做一些极其有趣的事。
  • 甚至把它做成了学术论文发表 (Jigsaw paper)。
  • 在 LLM 大模型时代,基于 UI 的自动化和语义识别,这些 Hack 行为会变得更容易做成、更好玩。
  • 很多时候我们都被学术界 “Publish or perish (要么发表,要么出局)” 的枷锁束缚了,但只要跳出框架:
  • 每个人都可以是生活中的极客与黑客!

总结

Take-away messages:

在每一个细分的计算领域,操作系统的底层架构师都会做出一些面向该领域的特殊设计妥协:例如嵌入式领域的绝对实时性控制 (real-timeliness),或是 Android 面向庞大应用生态而建立的 JVM + Binder 运行模型。

但剥开层层迷雾,回到应用视角去俯瞰操作系统:它本质上永远是一组封装好的底层 API。这组 API 屏蔽了枯燥的硬件细节,使得无数开发者能够愉快、高效地构建出改变世界的应用。

是的,大道至简,操作系统的魔力就是这么简单。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐