29、Kernel与设备驱动:Android Common Kernel、设备树、HAL与Kernel交互

各位好,今天我们来聊聊Android底层最硬核的一块——Kernel与设备驱动。说实话,做Android底层开发这么多年,我见过太多应用层工程师一听到Kernel就头疼。但你要想真正理解Android系统怎么跑起来的,这块绕不过去。

我刚开始接触Android底层时,也踩过不少坑。有一次调一个外设驱动,死活不工作,最后发现是设备树里一个引脚配置写错了。嗯,从那以后,我对设备树就格外上心。

Android Common Kernel(ACK)是什么?

先说说Android Common Kernel。说白了,它就是Google维护的一个“标准版”Linux内核,专门给Android用的。你想想看,市面上那么多芯片厂商——高通、联发科、三星、华为海思——每家都有自己的内核分支。如果没有一个统一的基础,那碎片化问题会严重到什么程度?

Google的做法是:维护一个ACK,各芯片厂商基于ACK做自己的定制。这样既保证了兼容性,又保留了灵活性。

核心要点:ACK = Linux LTS + Android特有补丁 + GKI(Generic Kernel Image)

GKI是Android 11之后引入的概念。它的目标是把内核和驱动模块解耦。以前你升级内核,所有驱动都得重新编译。现在?只要接口不变,驱动模块可以独立更新。我在项目中遇到过,某次安全补丁升级,只需要替换内核镜像,厂商的驱动模块完全不用动,省了大把时间。

设备树(Device Tree)

设备树,英文叫Device Tree,简称DT。这东西是干嘛的?简单说,它就是描述硬件信息的“配置文件”。

以前Linux内核里,硬件信息是硬编码在C代码里的。每换一个硬件平台,就得改内核代码。这显然不科学。设备树把硬件描述和内核代码分开了。你换了一块板子?换一个设备树文件就行,内核不用动。

设备树的文件格式有两种:

  • .dts:设备树源文件,人类可读的文本格式
  • .dtb:编译后的二进制文件,内核直接使用

来看一个简单的设备树示例:

/dts-v1/;

/ {
    model = "Qualcomm MSM8998";
    compatible = "qcom,msm8998";

    chosen {
        bootargs = "console=ttyMSM0,115200";
    };

    memory {
        device_type = "memory";
        reg = <0x0 0x0 0x0 0x80000000>;
    };

    i2c@f9923000 {
        compatible = "qcom,i2c-msm-v2";
        reg = <0xf9923000 0x1000>;
        interrupts = <0 96 0>;
        #address-cells = <1>;
        #size-cells = <0>;

        touch@20 {
            compatible = "synaptics,dsx";
            reg = <0x20>;
            interrupt-parent = <&msm_gpio>;
            interrupts = <125 0x2>;
        };
    };
};

看到没?每个硬件节点都有compatible属性,内核通过这个字符串来匹配对应的驱动。我曾经调试过一个触摸屏驱动,发现设备树里interrupts配错了,导致中断一直触发不了。查了两天才找到问题,你说坑不坑?

避坑指南:设备树里的reg属性,第一个值是基地址,第二个是地址范围。这两个值一定要和芯片手册对上。我曾经因为把地址范围写小了,导致驱动访问越界,系统直接panic。

HAL与Kernel的交互

HAL,全称Hardware Abstraction Layer,硬件抽象层。它是Android系统里连接Framework和Kernel的桥梁。

为什么需要HAL?你想想看,Framework是Java写的,Kernel是C写的。而且不同厂商的硬件实现千差万别。如果没有HAL,Google得为每款硬件写一套Framework代码,这显然不现实。

HAL的架构经历了几个阶段:

阶段 特点 典型接口
传统HAL 每个硬件模块一个.so库 hw_module_t, hw_device_t
Treble架构 HAL与Framework通过HIDL通信 I*@1.0::I*
Treble + AIDL 使用AIDL替代HIDL,更轻量 aidl接口定义

我个人习惯用Treble架构。它把Framework和HAL完全分离,Framework更新不用管厂商的HAL实现。说白了,就是Google和厂商各管各的,互不干扰。

来看一个HAL与Kernel交互的典型流程:

// HAL层代码(C++)
class CameraHAL : public ICameraProvider {
    int openCamera(const char* id) {
        // 1. 打开Kernel设备节点
        int fd = open("/dev/video0", O_RDWR);
        if (fd < 0) {
            ALOGE("Failed to open /dev/video0: %s", strerror(errno));
            return -1;
        }

        // 2. 通过ioctl与Kernel驱动通信
        struct v4l2_format fmt;
        memset(&fmt, 0, sizeof(fmt));
        fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        fmt.fmt.pix.width = 1920;
        fmt.fmt.pix.height = 1080;
        fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12;

        if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
            ALOGE("Failed to set format: %s", strerror(errno));
            close(fd);
            return -1;
        }

        // 3. mmap映射内存,用于数据传递
        // ... 省略具体实现

        return 0;
    }
};

对应的Kernel驱动侧:

// Kernel驱动代码(C)
static long camera_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    switch (cmd) {
        case VIDIOC_S_FMT: {
            struct v4l2_format fmt;
            if (copy_from_user(&fmt, (void __user *)arg, sizeof(fmt)))
                return -EFAULT;

            // 配置硬件寄存器
            writel(fmt.fmt.pix.width, CAMERA_REG_WIDTH);
            writel(fmt.fmt.pix.height, CAMERA_REG_HEIGHT);

            // 分配DMA缓冲区
            // ... 省略具体实现

            if (copy_to_user((void __user *)arg, &fmt, sizeof(fmt)))
                return -EFAULT;
            return 0;
        }
        default:
            return -EINVAL;
    }
}

看到没?HAL通过open、ioctl、mmap这些系统调用和Kernel打交道。Kernel驱动则负责操作硬件寄存器、管理DMA缓冲区等底层工作。

注意事项:HAL和Kernel之间的数据传递,一定要用copy_from_user/copy_to_user,不能直接访问用户空间指针。否则会有安全漏洞。我见过有些新手驱动工程师直接解引用用户指针,结果内核崩溃了。

整体交互流程

为了让你更直观地理解,我画了一张图:

Android HAL与Kernel交互流程 Framework(Java层) HAL(C/C++层,.so库) 系统调用(open/ioctl/mmap) Kernel驱动(字符设备/平台驱动) 硬件设备(Camera/Sensor/Display) 用户空间 内核空间 硬件层 设备树(.dts/.dtb) 描述硬件信息

这张图把整个链路画得很清楚了。从上到下:Framework调用HAL接口,HAL通过系统调用与Kernel驱动通信,Kernel驱动操作硬件。设备树则在Kernel驱动初始化时提供硬件配置信息。

我个人的经验是,调试这类问题最有效的方法就是加日志。HAL层加ALOGE,Kernel层加printk,一层一层往下查。曾经有个项目,相机预览黑屏,我就是在HAL层发现open设备节点返回了-ENODEV,然后查Kernel驱动,发现设备树里compatible字符串写错了,导致驱动没加载。

核心总结:

  • Android Common Kernel是Google维护的统一内核基线,GKI进一步解耦了内核和驱动
  • 设备树将硬件描述从内核代码中分离,提高了可移植性
  • HAL是Framework和Kernel之间的抽象层,Treble架构让两者彻底解耦
  • HAL通过系统调用(open/ioctl/mmap)与Kernel驱动交互

好了,这一章的内容就到这里。Kernel和驱动这块确实比较底层,但理解了这些,你对Android系统的整体认知会上一个台阶。下次遇到硬件相关的问题,你至少知道该从哪里下手查了。


公众号:蓝海资料掘金营,微信deep3321