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,不能直接访问用户空间指针。否则会有安全漏洞。我见过有些新手驱动工程师直接解引用用户指针,结果内核崩溃了。
整体交互流程
为了让你更直观地理解,我画了一张图:
这张图把整个链路画得很清楚了。从上到下: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系统的整体认知会上一个台阶。下次遇到硬件相关的问题,你至少知道该从哪里下手查了。