第3章:Linux内核基础与稳定性

3.1 内核关键子系统概览

说实话,搞Android稳定性这些年,我最深的体会就是——你得懂内核。不是说要你成为内核维护者,但至少得知道那几个关键子系统在干嘛。我见过太多应用层工程师,遇到个死机问题就抓瞎,其实就是不懂内核在背后怎么运作的。

Linux内核,说白了就是一个资源大管家。它管着CPU、内存、存储、设备。Android跑在它上面,稳定性好不好,全看这几个子系统配合得怎么样。

3.2 进程调度:谁先跑,谁后跑

进程调度,就是决定下一个该谁用CPU。嗯,就这么简单。但实际做起来,门道很多。

Android用的是CFS调度器(完全公平调度)。名字叫“完全公平”,但实际不是绝对的公平。你想想看,前台App和后台Service,能一样对待吗?

核心要点:CFS用虚拟运行时间(vruntime)来决定调度顺序。vruntime越小,越优先被调度。

我在项目中遇到过一个问题:某个App一启动,系统就卡得不行。后来发现是它的线程优先级被设得太高,抢占了太多CPU时间。解决办法?把优先级调回正常水平,世界清净了。

// 查看进程调度策略和优先级
adb shell cat /proc/[pid]/sched | grep -E "policy|prio"

// 修改进程优先级(需要root)
adb shell renice -n -10 -p [pid]

我的建议:调试稳定性问题时,先看看是不是有进程在“霸占”CPU。用top命令看CPU占用率,用schedstat看调度延迟。

3.3 内存管理:别让系统“爆”了

内存管理这块,我吃过不少亏。Android的内存压力和PC不一样,它有个Low Memory Killer(LMK),内存不够就杀进程。但杀谁?什么时候杀?这里面有讲究。

内核把内存分成几个区域:DMA、Normal、HighMem。每个区域有水位线(watermark)。低于最低水位线,内核就开始回收内存。回收方式有几种:

  • kswapd:后台回收,温和一点
  • 直接回收:等不及了,直接动手
  • OOM Killer:最后手段,杀进程

我记得有一次,客户反馈手机用着用着就重启。查了半天,发现是某个驱动申请内存时不检查返回值,申请失败就直接NULL指针解引用,啪,内核Panic了。

避坑指南:我曾经在调试一个内存泄漏问题时,发现dmesg里全是"page allocation failure"的警告。这说明系统内存已经紧张到连小内存都分配不出来了。这时候别急着看应用层,先查内核模块有没有泄漏。

// 查看内存使用情况
adb shell cat /proc/meminfo

// 查看内存水位线
adb shell cat /proc/zoneinfo | grep -A 20 "Normal"

3.4 文件系统:数据怎么存

Android现在主流用的是ext4和f2fs。f2fs是专门为闪存设计的,随机读写性能更好。但不管用哪个,文件系统出问题,后果很严重——数据丢失、系统无法启动。

文件系统稳定性,核心就两点:

  1. 日志(Journal):保证崩溃后能恢复
  2. 挂载参数:比如noatime可以减少写操作

我遇到过最诡异的一个问题:手机存储空间还有10GB,但就是写不进文件。查到最后,是inode用完了。每个文件都要占用一个inode,小文件太多,inode先耗尽了。

检查inode使用情况:

adb shell df -i
adb shell dumpe2fs -h /dev/block/mmcblk0pXX | grep "Inode count"

3.5 驱动框架:设备怎么和内核通信

驱动,是内核里最容易出问题的地方。为什么?因为驱动代码质量参差不齐,很多是芯片厂商写的,测试覆盖不够。

Linux驱动框架分三类:

  • 字符设备:像串口、GPIO,按字节流读写
  • 块设备:像eMMC、UFS,按块读写
  • 网络设备:像Wi-Fi、蓝牙,处理数据包

Android特有的驱动,比如binder、ashmem、ion,都是字符设备。binder是进程间通信的核心,它要是崩了,整个系统就瘫了。

我记得有一次,binder驱动里有个死锁问题。两个进程同时调用binder,互相等对方释放锁,结果卡死了。最后在dmesg里看到"binder: release proc"的日志,才定位到问题。

调试技巧:用binder的调试接口查看binder线程状态:

adb shell cat /sys/kernel/debug/binder/proc/[pid]
adb shell cat /sys/kernel/debug/binder/state

3.6 内核Panic机制:系统最后的“遗言”

内核Panic,就是内核遇到了无法恢复的错误,决定“自杀”。它会打印一堆信息,然后重启。这些信息,就是我们排查问题的关键。

Panic的触发条件:

  • NULL指针解引用
  • 内核态访问非法地址
  • spinlock死锁
  • BUG_ON()宏触发

嗯,这里要注意:不是所有内核错误都会Panic。有些错误,内核会尝试恢复,比如缺页异常。只有那些“没救了”的错误,才会触发Panic。

重要:生产环境建议开启pstore/ramoops,这样Panic信息能保存到重启后,不会丢失。

3.7 Oops信息解读:读懂内核的“遗书”

Oops,就是内核告诉你“我出错了,但我还能撑一会儿”。它比Panic温和,但同样致命——系统可能已经不稳定了。

一个典型的Oops信息包含:

  1. 错误类型:比如"Unable to handle kernel NULL pointer dereference"
  2. 寄存器值:PC、LR、SP等,告诉你执行到哪了
  3. 调用栈:函数调用链,从下往上读
  4. 代码段:出错的指令附近的反汇编

我解读Oops的步骤:

  • 先看错误类型,判断是空指针还是访问越界
  • 再看PC寄存器,找到出错的函数
  • 然后看调用栈,理清调用关系
  • 最后看代码段,确认具体哪一行代码有问题
// 示例Oops解读
[   45.123456] Unable to handle kernel NULL pointer dereference at virtual address 00000008
[   45.123457] PC is at binder_thread_read+0x1c4/0x8a0
[   45.123458] LR is at binder_ioctl+0x2f0/0x3c0
[   45.123459] Call trace:
[   45.123460]  binder_thread_read+0x1c4/0x8a0
[   45.123461]  binder_ioctl+0x2f0/0x3c0
[   45.123462]  do_vfs_ioctl+0x84/0x94
[   45.123463]  SyS_ioctl+0x88/0x98

看到这个Oops,我第一反应是:binder驱动里有个空指针。PC在binder_thread_read+0x1c4,说明是binder_thread_read函数里偏移0x1c4的位置出错了。查源码,发现是某个指针没判空就直接用了。

我的经验:解读Oops,最重要的是找到“根因”。不要被调用栈迷惑,有时候错误发生在A函数,但真正的原因是B函数传了个空指针过来。多看看调用链,多想想数据流。

3.8 实战:从Oops到修复

最后,我分享一个真实案例。某款手机在特定场景下会随机重启。dmesg里看到Oops:

[  123.456789] Unable to handle kernel paging request at virtual address ffffff8000000000
[  123.456790] PC is at __memcpy+0x20/0x60
[  123.456791] Call trace:
[  123.456792]  __memcpy+0x20/0x60
[  123.456793]  kgsl_sharedmem_page_alloc+0x1a0/0x2d0
[  123.456794]  kgsl_ioctl+0x3c0/0x500

PC在__memcpy,说明是内存拷贝时出错了。调用栈显示是kgsl(GPU驱动)在分配共享内存时调用了memcpy。我怀疑是源地址或目标地址有问题。

查kgsl_sharedmem_page_alloc的源码,发现它从用户态传了个地址过来,没做合法性检查就直接memcpy了。用户态传了个非法地址,内核就崩了。

修复方案:在memcpy之前,加上地址合法性检查。如果地址非法,返回错误,而不是直接崩溃。

总结:内核稳定性调试,说白了就是“看日志、找规律、查源码、修bug”。别怕Oops,它其实是内核给你的最直接的线索。多读几次,你就能看懂它在说什么。