Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

操作系统进阶

Android 应用跑在 Linux 之上,面试里的 OS 题不是纯书本题:进程、线程、协程会落到主线程/线程池/协程调度;虚拟内存、mmap、page fault 会落到 Binder、文件映射和启动性能;epoll 会落到 Looper 与网络连接。

一、进程、线程、协程的进阶对比

维度进程线程协程
资源边界独立虚拟地址空间共享进程地址空间运行在线程上
调度者Linux 内核Linux 内核Kotlin/运行时协作调度
切换开销高,涉及地址空间等上下文中,保存寄存器/栈等低,挂起恢复状态机
Android 例子App 进程、WebView 多进程、remote serviceUI 线程、RenderThread、OkHttp Dispatchersuspend、Flow、ViewModelScope
  • 进程解决隔离:一个 App crash 不应拖垮系统;多进程可隔离 WebView、推送、风控 SDK。
  • 线程解决并行:CPU 密集放 Default/计算线程,IO 放 IO/线程池,UI 只能主线程更新 View。
  • 协程解决异步表达:挂起不等于新线程,suspend 只是把回调/状态机写成顺序代码。
  • 面试关键句:协程不是 OS 调度单位,最终仍要落在线程上执行;阻塞式 IO 放错调度器仍会占住真实线程。

二、虚拟内存、mmap 与 page fault

虚拟内存让每个进程看到连续独立地址空间,由页表映射到物理页。Android 中它直接影响大文件读取、so 加载、Dex/OAT 映射、Binder 缓冲区等。

进程虚拟地址
  ↓ 页表 / MMU / TLB
物理内存页
  ↑
缺页(page fault)时由内核把文件页/匿名页调入内存
  • mmap:把文件或设备映射到进程虚拟地址,按需加载,减少显式 read/copy;Binder 驱动也利用 mmap 建立用户态可访问缓冲区。
  • page fault:访问的虚拟页尚未在物理内存中,CPU 触发异常进入内核处理。轻微缺页可能只建映射,重大缺页可能要读磁盘。
  • 启动性能关联:冷启动读取 dex、resources、so 时可能产生大量缺页;Baseline Profile、预加载和减少冷路径大文件访问都能降低抖动。
  • 内存压力关联:匿名页、文件页、Ashmem/共享内存都会参与系统回收;低端机上大 Bitmap 和大 mmap 文件都要考虑峰值。

三、文件描述符、I/O 与 epoll

Linux 把文件、socket、pipe、eventfd 等都抽象成文件描述符(fd)。Android 的网络、数据库、日志、Looper 唤醒都离不开 fd。

I/O 机制特点Android 关联
阻塞 I/O调用线程等待结果主线程网络/磁盘会 ANR
非阻塞 I/O没数据立即返回需要轮询或事件通知
select/poll监听多个 fd,但扩展性一般传统多路复用方案
epoll事件驱动,适合大量 fdLooper、网络框架、native event loop

Looper 为什么不忙等? MessageQueue 没消息时,底层通过 epoll_wait 阻塞等待 fd 事件或超时;有消息、Binder、输入事件、定时器到期时再被唤醒。这也是“死循环不等于耗 CPU”的经典追问。

fd 常见问题:

  • 文件/网络流未关闭导致 fd 泄漏,最终 Too many open files
  • 日志、图片、数据库 Cursor 未及时 close。
  • 连接池过大或泄漏导致 socket fd 占用异常。

四、调度、优先级与 Android 卡顿

Linux 普通任务主要由 CFS 调度,目标是公平分配 CPU;Android 在此基础上叠加进程优先级、线程 nice 值、cgroup/cpuset、前后台策略。

  1. UI 线程不是绝对优先:它仍要参与调度,如果自己执行长任务或系统 CPU 被打满,就会错过 16.6ms 帧预算。
  2. 线程优先级要谨慎:后台下载/日志压缩不应抢 UI;音视频、渲染、输入链路要避免被低价值任务干扰。
  3. 协程调度器不是魔法:Dispatchers.Default 适合 CPU,Dispatchers.IO 适合阻塞 IO,乱用会导致线程饥饿。
  4. ANR 本质:主线程长时间无法处理输入、广播、服务生命周期等消息,可能是锁等待、IO、CPU、Binder 调用卡住。

五、锁、死锁与并发安全

死锁四条件:互斥、持有并等待、不可剥夺、循环等待。Android 中常见死锁并不只来自 synchronized,还来自主线程等待后台、后台反向切主线程、Binder 同步调用互等。

Thread-A: 持有 dbLock → 等待 networkLock
Thread-B: 持有 networkLock → 等待 dbLock
结果:循环等待,两边都无法推进

实践建议:

  • 固定锁顺序,避免 A→B 与 B→A 混用。
  • 不在持锁期间做网络、磁盘、Binder 或回调外部代码。
  • 优先缩小临界区,读多写少用读写锁或不可变快照。
  • Kotlin 协程中区分 Mutex 与 JVM 锁,不要在 synchronized 内调用可能挂起的逻辑。
  • 主线程不要等待后台锁;后台也不要同步等待主线程回调。

六、Android Linux 基础速记

  • Zygote:预加载类和资源后 fork App 进程,降低启动成本;fork 后进程拥有独立虚拟地址空间,通过 COW 共享只读页。
  • Binder:Android 主要 IPC,结合驱动、mmap、线程池和引用计数,比 Socket 更适合系统服务调用。
  • cgroup/cpuset:系统按前后台、任务类型限制 CPU/资源分配,解释为什么后台任务可能变慢。
  • LMK/内存回收:低内存时系统按进程重要性回收;App 要保存状态,不能假设进程永生。
  • SELinux/权限模型:限制进程访问系统资源,移动安全和文件访问都要考虑沙箱边界。

高频面试题

Q1:进程、线程、协程怎么区分? 进程是资源隔离单位,线程是内核调度单位,协程是用户态/运行时的异步抽象。协程挂起不阻塞线程,但执行仍要占用线程,因此阻塞操作放错调度器仍会影响性能。

Q2:mmap 和普通 read 有什么区别? read 把数据从内核缓冲复制到用户缓冲,mmap 把文件映射到虚拟地址空间,访问时按页加载,可减少拷贝和简化随机访问。但 mmap 仍可能触发 page fault,不是“免费加载”。

Q3:Looper 底层为什么用 epoll? Looper 要同时等待消息队列、输入、Binder/管道等 fd 事件。epoll 能高效等待多个 fd,无事件时阻塞让出 CPU,有事件再唤醒,避免忙等。

Q4:Android 死锁如何排查和避免? 看线程堆栈确认谁持有什么锁、谁在等待;避免嵌套锁顺序不一致、持锁做耗时操作、主线程同步等待后台。必要时用超时、锁顺序规范和异步化拆环。

易错点 / 追问

  • 易错:把协程说成轻量线程;准确说协程不是内核线程,它运行在线程之上。
  • 追问:page fault 是否一定是坏事?不是,按需分页依赖它;但冷启动大量重大缺页会带来磁盘 IO 抖动。
  • 易错:以为 epoll 只用于服务端高并发;Android Looper 和 native 事件循环同样依赖 fd 多路复用思想。
  • 追问:为什么主线程没有死循环占 CPU?因为 MessageQueue 空闲时阻塞在 epoll_wait,不是 while true 忙轮询。