2、Linux内核基础:进程调度、内存管理、文件系统、设备驱动模型

好,咱们进入正题。做Android性能优化,Linux内核是绕不开的坎。你想想看,上层App卡顿、掉帧、内存泄漏,追根溯源,十有八九都能在内核里找到影子。我个人习惯是,遇到疑难杂症,先看内核日志,再查调度和内存。今天我就把这四个核心模块掰开揉碎讲给你听。

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

进程调度,说白了就是CPU决定「下一个该执行谁」。Android是交互式系统,用户点一下屏幕,App必须立刻响应。如果调度策略不对,就会出现「我明明点了,手机却愣了半天」的情况。

Linux内核里,调度器用的是完全公平调度(CFS)。它不搞时间片轮转那一套,而是维护一个红黑树,每个进程有一个vruntime(虚拟运行时间)。谁跑得少,谁就靠左,优先被调度。

核心公式:

vruntime += 实际运行时间 * (NICE_0_LOAD / 进程权重)

权重由nice值决定。nice值越低,权重越高,vruntime增长越慢,越容易被调度。

我在项目中遇到过一个问题:某个视频App播放时频繁掉帧。查了半天,发现是后台一个日志同步进程抢了CPU。解决方案很简单——把视频播放线程的nice值调低(优先级提高),或者用sched_setscheduler设为SCHED_FIFO实时调度。嗯,这里要注意,实时调度要慎用,搞不好会卡死系统。

避坑指南:我曾经在某个项目里,把UI线程设成了SCHED_FIFO,结果一个死循环直接让系统冻住。后来我学乖了:UI线程用SCHED_NORMAL(CFS),关键渲染线程用SCHED_FIFO,但必须加看门狗。

2.2 内存管理:虚拟地址与物理页的博弈

内存管理是Android性能优化的重灾区。你想想看,每个App都觉得自己有4GB内存可用,实际上物理内存可能只有6GB。这就是虚拟内存的魔力。

Linux把虚拟地址空间分成两部分:用户空间(0~3GB)和内核空间(3GB~4GB)。每个进程有自己的页表,通过MMU把虚拟地址翻译成物理地址。翻译过程是分级的——四级页表(PGD、PUD、PMD、PTE)。

关键数据结构:

struct page {
    unsigned long flags;    // 页状态
    atomic_t _refcount;     // 引用计数
    struct address_space *mapping; // 文件映射
    pgoff_t index;          // 页偏移
};

Android里有个经典问题:内存碎片。App频繁创建和释放对象,物理内存会变得支离破碎。这时候kswapd内核线程会启动,把不常用的页面换到zRAM(压缩内存)里。我建议你关注/proc/meminfo里的SwapTotalSwapFree,如果zRAM使用率超过80%,说明内存压力很大了。

注意:不要盲目调大zRAM。我曾经在低端机上把zRAM开到2GB,结果压缩/解压缩消耗了大量CPU,反而更卡。一般建议zRAM设为物理内存的25%~50%。

2.3 文件系统:数据怎么存,怎么读

文件系统是内核和存储设备之间的桥梁。Android早期用YAFFS,后来换成ext4,现在主流是F2FS(Flash Friendly File System)。为什么?因为F2FS专门为闪存优化,减少了写放大。

Linux文件系统有个通用抽象层——VFS(虚拟文件系统)。它定义了一套标准接口:open()read()write()close()。不管底层是ext4还是F2FS,上层调用都一样。

VFS核心对象:

对象作用
super_block描述已挂载的文件系统
inode描述文件元数据(权限、大小、位置)
dentry描述目录项(文件名到inode的映射)
file描述打开的文件(读写位置、标志)

做性能优化时,我经常看/proc/fs/f2fs/stat。如果gc_called(垃圾回收次数)很高,说明文件系统碎片严重,App读写会变慢。这时候可以手动触发fstrim命令,让闪存回收空闲块。

个人经验:我曾经优化一个相册App的启动速度。发现它启动时要扫描几千张图片,每次stat()系统调用都要查inode。后来我用inotify监控文件变化,只维护一个内存缓存,启动时间从3秒降到了0.5秒。

2.4 设备驱动模型:硬件和内核的桥梁

设备驱动模型,说白了就是让内核知道「这个硬件怎么用」。Android里最常见的驱动是字符设备驱动(如GPIO、I2C、SPI)和块设备驱动(如eMMC、UFS)。

Linux 2.6之后引入了设备模型(Device Model),核心概念是:

  • device:代表一个物理设备
  • driver:代表一个驱动程序
  • bus:代表设备连接的总线(如platform总线、I2C总线)
  • class:代表设备的功能分类(如input、sound)

驱动加载流程是这样的:内核启动时,总线扫描设备树(Device Tree),发现设备后,匹配对应的驱动,然后调用驱动的probe()函数。嗯,这里要注意,设备树文件(.dts)里如果写错了寄存器地址,驱动根本不会加载。

一个简单的platform驱动示例:

static int my_probe(struct platform_device *pdev) {
    struct resource *res;
    void __iomem *base;
    
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);
    
    // 初始化硬件寄存器
    writel(0x1, base + 0x00);
    return 0;
}

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .driver = {
        .name = "my_device",
        .of_match_table = my_of_match,
    },
};
module_platform_driver(my_driver);

避坑指南:我曾经在调试一个传感器驱动时,发现probe()函数返回了0,但设备就是不工作。查了两天,才发现是设备树里interrupts属性写错了中断号。记住:驱动不工作,先查设备树,再查寄存器。

做Hal层调优时,你经常要和驱动打交道。比如调优相机性能,需要修改V4L2驱动的buffer分配策略;调优音频延迟,需要优化ALSA驱动的DMA传输。说白了,不懂驱动模型,Hal层优化就是空中楼阁。

我的建议:遇到性能问题,先看/proc/interrupts,确认中断是否均衡。再看/sys/kernel/debug下的驱动调试节点。最后用trace-cmd抓调度和中断的时序。这三板斧下去,80%的问题都能定位。

好了,Linux内核基础就讲到这里。进程调度让你知道谁该跑,内存管理让你知道内存怎么用,文件系统让你知道数据怎么存,设备驱动模型让你知道硬件怎么控。这四个模块是Android性能优化的地基。下一章,我会带你深入Hal层,看看怎么在驱动和框架之间做调优。