第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是专门为闪存设计的,随机读写性能更好。但不管用哪个,文件系统出问题,后果很严重——数据丢失、系统无法启动。
文件系统稳定性,核心就两点:
- 日志(Journal):保证崩溃后能恢复
- 挂载参数:比如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信息包含:
- 错误类型:比如"Unable to handle kernel NULL pointer dereference"
- 寄存器值:PC、LR、SP等,告诉你执行到哪了
- 调用栈:函数调用链,从下往上读
- 代码段:出错的指令附近的反汇编
我解读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,它其实是内核给你的最直接的线索。多读几次,你就能看懂它在说什么。