第三章 Linux内核基础:内核源码结构、内核配置系统与编译

好,咱们进入正题。这一章讲的是Linux内核的基础,说白了就是你要先搞清楚内核这玩意儿长什么样,怎么配置,怎么编译。我在高通平台上调驱动这么多年,发现很多新人上来就写代码,结果连内核目录是干嘛的都不知道,编译报错更是两眼一抹黑。嗯,咱们先把地基打牢。

3.1 内核源码结构:别迷路

你下载完Linux内核源码,解压后第一眼看到一堆目录,别慌。我个人的习惯是,先看顶层目录,记住几个关键的:

  • arch/:架构相关代码。比如arch/arm、arch/arm64、arch/x86。高通芯片基本都是arm64,所以你大部分时间会泡在arch/arm64里。
  • drivers/:驱动代码。这是咱们的主战场。里面按设备类型分,比如i2c、spi、gpio、usb、net等等。高通平台很多外设驱动都在这里。
  • include/:头文件。内核API、数据结构定义都在这里。include/linux/是通用头文件,include/asm-arm64/是架构相关的。
  • kernel/:内核核心代码,比如进程调度、中断管理。这部分咱们一般不动,但得知道它在哪。
  • mm/:内存管理。写驱动时偶尔会涉及,比如dma内存分配。
  • Documentation/:文档。说实话,有些文档写得比代码还难懂,但遇到不懂的宏或者API,先来这里翻翻。
我的小技巧: 刚接触一个新平台时,我会先看arch/arm64/boot/dts/目录下的设备树文件。高通平台的设备树文件命名很有规律,比如msm8998.dtsi、sdm845.dtsi。看懂设备树,基本就摸清了硬件拓扑。

3.2 内核配置系统:Kconfig与Makefile

配置内核,说白了就是告诉编译器:我要哪些功能,不要哪些功能。高通平台的内核配置尤其复杂,因为芯片集成的外设太多了。

3.2.1 Kconfig:菜单是怎么来的

Kconfig文件定义了配置选项。比如你在drivers/i2c/Kconfig里会看到:

config I2C_QUP
    tristate "Qualcomm Universal Peripheral (QUP) I2C controller"
    depends on ARCH_QCOM
    help
      Say Y here to enable the QUP I2C controller driver.
      This driver can also be built as a module.

这里有几个关键点:

  • tristate:三态,Y(编译进内核)、M(编译成模块)、N(不编译)。
  • depends on:依赖关系。比如这个驱动依赖ARCH_QCOM,如果你没选高通平台,这个选项就不会出现。
  • help:帮助信息。我建议你养成看help的习惯,里面经常有硬件配置的提示。
注意: 高通平台经常会在Kconfig里加一些私有的依赖,比如depends on MSM_GPIOMUX。如果你编译报错说某个符号找不到,八成是依赖没选对。我曾经在一个项目里折腾了两天,最后发现是少选了一个高通私有模块。

3.2.2 Makefile:编译规则

Makefile告诉编译器怎么编译。还是拿I2C驱动举例,在drivers/i2c/Makefile里:

obj-$(CONFIG_I2C_QUP) += i2c-qup.o

这行代码的意思是:如果CONFIG_I2C_QUP被配置为Y或M,就编译i2c-qup.c。如果配置为N,就不编译。

你可能会问:为什么是obj-y、obj-m?

  • obj-y:编译进内核镜像。
  • obj-m:编译成独立的.ko模块。
  • obj-n:不编译。

高通平台经常会有多级目录,比如:

obj-$(CONFIG_MSM_VIDC) += msm/
obj-$(CONFIG_MSM_VIDC) += msm/vidc/

这种写法表示如果配置了MSM_VIDC,就递归进入msm/和msm/vidc/目录编译。

3.3 内核编译与模块编译

好了,配置写完了,怎么编译?这里我分两种情况讲:编译整个内核,和单独编译一个模块。

3.3.1 编译整个内核

高通平台通常用这个流程:

# 1. 配置内核
make ARCH=arm64 defconfig        # 使用默认配置
# 或者
make ARCH=arm64 menuconfig       # 图形化配置界面

# 2. 编译内核
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8

# 3. 编译设备树
make ARCH=arm64 dtbs

这里要注意:

  • CROSS_COMPILE:交叉编译工具链前缀。高通平台一般用aarch64-linux-gnu-或者arm-linux-gnueabihf-。
  • -j8:并行编译,8个线程。根据你电脑CPU核数来设,别设太大,否则电脑会卡死。我吃过这个亏,一次设了-j32,结果编译到一半内存爆了。

3.3.2 单独编译模块

开发驱动时,你不可能每次都编译整个内核。太慢了。单独编译模块的方法:

# 进入驱动目录
cd drivers/i2c/

# 编译单个模块
make -C /path/to/kernel M=$(pwd) modules

# 或者指定模块
make -C /path/to/kernel M=$(pwd) modules CONFIG_I2C_QUP=m

这里-C指定内核源码路径,M指定模块源码路径。编译出来的.ko文件就在当前目录。

我的习惯: 我会写一个简单的脚本,把编译命令存起来。比如build.sh:
#!/bin/bash
make -C ~/kernel/msm-4.19 M=$(pwd) modules ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

这样每次改完代码,直接./build.sh就行,省得敲长命令。

3.3.3 模块加载与卸载

编译出.ko文件后,怎么用?

# 加载模块
insmod i2c-qup.ko

# 查看模块信息
modinfo i2c-qup.ko

# 卸载模块
rmmod i2c-qup

# 查看已加载模块
lsmod | grep i2c

这里有个坑:insmod和modprobe的区别。insmod是直接加载,不处理依赖。modprobe会自动加载依赖模块。高通平台驱动依赖关系复杂,我建议你用modprobe。

我曾经踩过的坑: 有一次我insmod一个GPIO驱动,结果报错说符号gpio_request未定义。后来发现这个符号在另一个模块里,必须先加载那个模块。用modprobe就自动处理了。所以,除非你很清楚依赖关系,否则就用modprobe。

3.4 高通平台的特殊之处

高通的内核和主线Linux内核有些区别。我总结几点:

  • 设备树覆盖:高通平台经常用设备树覆盖(DT overlay)机制。比如一个芯片支持多个硬件版本,通过不同的.dts overlay文件来适配。
  • 私有模块:drivers/soc/qcom/目录下有很多高通私有的驱动,比如smem、smp2p、glink。这些是芯片间通信的基础,写驱动时经常要调用它们的API。
  • 编译脚本:高通有自己的编译脚本,比如build/目录下的脚本。我建议你直接用高通的编译脚本,别自己写Makefile,否则很容易漏掉一些私有头文件路径。

嗯,这一章的内容就这些。说白了,内核源码结构就是让你知道东西在哪,Kconfig/Makefile是配置和编译的规则,编译命令是实际操作。把这些搞明白,后面写驱动时就不会一头雾水了。

下一章咱们开始讲真正的驱动开发——字符设备驱动。到时候我会拿一个高通平台的GPIO驱动做例子,手把手带你走一遍。