Linux驱动篇开篇——《驱动篇》
前言我们正式迈入 Linux 真正的底层。之前的 Linux 系统编程多半是在「系统调用之上」写功能对内核底下究竟做了什么往往只有模糊印象例如socket 怎样用到网卡、open/write 落盘时块设备栈里发生了什么。带着这些问题往下看会更有方向。所以我们本次大章节开始学习Linux驱动闲话少说我们开始进入真正的硬核。首先我们先声明一个概念什么是用户层什么是内核层。用户态 vs 内核态上图展示的就是用户态和内核态的区别。我们之前写的程序其实都是运行在用户态也叫用户层在这个状态下程序本身是不能直接和硬件打交道的所有对硬件的操作都必须通过内核来实现也就是经过内核态才能真正和硬件交互。而我们熟悉的单片机开发程序其实就是直接运行在类似“内核态”的环境里可以直接控制硬件IO所以代码会显得很自由甚至有些混乱。Linux为了更好地管理和保护系统把功能区分得很清楚内核专注于提供底层功能和安全上层用户态只用关注实现自己的业务逻辑这样系统更稳定、更安全。进入内核的编写习惯建立内核语法1. 内核代码风格和实现差异在用户态编程中我们习惯性地用到很多标准C库函数如printf,malloc,free等这些基本都是由glibc来实现的。而进到内核后情况就大不一样了标准库不可用: 内核空间没有所谓的标准库所有的字符串、内存操作、输出输入等都由内核自己提供一套实现通常有类似但名称不同的函数比如打印调试信息printk()取代printf()字符串操作strcpy、strcat等依然存在但是在内核单独实现版内存分配kmalloc()、kfree()取代用户空间的malloc()、free()头文件不同包含的头文件都是内核相关比如linux/module.h,linux/kernel.h。不能随意使用浮点内核代码一般不允许使用浮点数运算。不同的规范与约束内核对代码风格、注释方式、内存管理、同步等有自己的一套严格约束。举个简单的内核模块代码片段#includelinux/init.h#includelinux/module.hstaticint__inithello_init(void){printk(KERN_INFOHello, Linux kernel!\n);return0;}staticvoid__exithello_exit(void){printk(KERN_INFOGoodbye, Linux kernel!\n);}module_init(hello_init);module_exit(hello_exit);MODULE_LICENSE(GPL);MODULE_AUTHOR(YourName);MODULE_DESCRIPTION(A simple Hello World Linux kernel module);可以看到这里用到了printk()进行打印模块有自己的初始化和退出方式最后还要添加许可证、作者等信息。这里只是简要展示一下驱动的简单写法有一个初步概念以及为什么这么写传统的main函数为什么不见了我们后续慢慢会讲到。环境搭建就比如刚刚的这个实例我们先不考虑它的功能是什么我们如何编译、如何先运行跑一下呢体会一下这个内核编程究竟是怎样的。当然我们需要一个运行环境。与之前不同的是这次我们不再完全依赖 VMware 去装一套带桌面的发行版而是尽量用轻量、偏服务器向的 Linux没有图形桌面甚至没有明显的发行版“皮肤”更接近一台纯净的文本环境。我们在这个环境里观察开机与内核消息再编译、加载模块体会驱动是如何真正跑起来的。kernel.orgLinux内核源码主要存放在www.kernel.org。这个网站包含已经发布的内核版本。世界各地有大量的kernel.org镜像网站。我们就下载最新的Linux内核来编译启动。6.19.11版本的。下载下来的Linux内核源码目录结构很庞大但最顶层目录结构大致如下简化版linux-6.19.11/ ├── arch/ # 各平台体系结构相关代码 (如x86, arm等) ├── block/ # 块设备子系统 ├── drivers/ # 各类驱动的汇总目录 ├── fs/ # 各种文件系统实现 ├── include/ # 内核所需的头文件 ├── init/ # 引导初始化代码 ├── kernel/ # 核心代码 ├── lib/ # 内核库函数 ├── mm/ # 内存管理 ├── net/ # 网络协议栈 ├── scripts/ # 构建脚本和辅助工具 └── Makefile # 主Makefile入口这些目录中drivers/包含了大部分“驱动”实现代码比如网卡、显卡、串口、I2C、SPI 等各种外设的代码。下载好之后我们使用我们的Ubuntu虚拟机进行编译可以使用之前学习的虚拟机。编译 Linux 内核我们的流程大概是这样的在 Ubuntu 虚拟机或实体机里编译一份自己的内核再用QEMU把这份内核跑起来。QEMU 是什么QEMU是一台虚拟机/仿真器在当前这台 Ubuntu里再跑出一套虚拟的 CPU 和硬件让你指定的内核镜像和rootfs在里面启动。可以把它理解成三层套娃VMware可选→ 装 Ubuntu → 在 Ubuntu 里用 QEMU → 跑你编的内核 自己做的 rootfs。后两层是我们这篇主要在做的VMware 只是你平时练系统编程的那层可有可无。安装 QEMU在 Ubuntu 里执行sudoaptupdatesudoaptinstall-yqemu-system-arm qemu-system-x86 qemu-utils安装编译依赖安装内核编译依赖与内核版本配套一般如下sudoaptinstall-ybuild-essentialbcbison flex libssl-dev libelf-dev libncurses-dev dwarveslibelf-dev链接、BTF 等会用到缺了常见报错。libncurses-dev使用make menuconfig时需要。dwarves含pahole部分配置开启 BTF 时工具链会调用可按报错再装。配置与编译进入解压后的内核目录示例版本号与你下载的一致即可makedefconfig# 可选make menuconfigmake-j$(nproc).config是什么config 的概念在这个阶段config这个词会越听越多总之就是字面意思就是一个配置文件。自定义的规则文件然后照着规则执行。我们编译的时候要选一个配置然后编译根据对应的配置进行编译所以有了make defconfig选择配置然后就会把默认配置写到.config里面再make按.config编译。make defconfig按当前架构载入一份默认配置生成.config适合第一次先编通。make menuconfig在终端里图形化菜单增删选项改完同样写回.config。make olddefconfig升级内核源码后用旧.config对齐新选项新项按默认值填减少手工合并。最小 rootfs 压缩包喂给 QEMU需要两样东西内核镜像根文件系统压缩包。第二样就是你在本机「自己做」的一棵最小目录树打成一个文件习惯命名为rootfs.cpio.gz表示root filesystemQEMU 参数仍写-initrd因为内核把它当作 initial ramdisk 加载。1. 内核镜像make编出来即可ARM64arch/arm64/boot/Imagex86_64arch/x86/boot/bzImage这个我们已经编译出来了但是我们还缺少根文件系统如果没有根文件系统我们连基本的命令行也看不到ls等命令也用不了所以根文件系统也就是一套最基本的应用层工具里面有我们熟悉的基本的命令比如ls,cp…等工具。所以我们要构建一套根文件系统跟随内核一起给QEMU才能正常启动并使用。2. 根文件系统包官方源码https://busybox.net/downloads/sudoaptinstall-ybuild-essential libncurses-dev# menuconfig 要 ncursescd~/你的目录(选一个自己的目录按自己的方式去设计和管理)wgethttps://busybox.net/downloads/busybox-1.36.1.tar.bz2tarxf busybox-1.36.1.tar.bz2cdbusybox-1.36.1makedefconfigmakemenuconfig在菜单里打开Busybox Settings→Build static binary (no shared libs)保存退出打成静态链接rootfs 里不用拷glibc动态库最适合 initramfs 式 rootfs。然后编译并安装到BusyBox 源码目录下的./rootfs/make-j$(nproc)makeCONFIG_PREFIX./rootfsinstallmkdir-p./rootfs/{proc,sys,dev}这个就是根文件系统的雏形里面有一些基础命令你会发现我们平时用的基础命令也就是这些没有什么好奇怪的这个和当前你正在用的虚拟机的/usr/bin目录下一样。只不过这个是精简版本的。cat./rootfs/initEOF #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev exec /bin/sh EOFchmodx ./rootfs/init上面这个是创建启动脚本也就是我们第一个运行的程序。# 这条命令的意思是进入rootfs目录将里面所有文件和目录用cpio打包成 newc 格式再用gzip压缩为rootfs.cpio.gz(cd./rootfsfind.-print0|cpio--null-ov-Hnewc|gzip-9./rootfs.cpio.gz)# 运行完之后在 rootfs 目录下会看到 rootfs.cpio.gz$ls# 在 busybox-1.36.1/rootfs 目录下执行bin dev init linuxrc proc root rootfs.cpio.gz sbin sys usr路径约定全文统一压缩包在busybox-1.36.1/rootfs/rootfs.cpio.gz即「BusyBox 源码目录 /rootfs/rootfs.cpio.gz」。下面「运行内核」里的-initrd也按这个路径写你若改到别的目录请三处一起改打包命令、ls、QEMU。完成后它就是「最小根文件系统压缩包」QEMU 里用-initrd指向该文件的绝对路径或相对路径均可。小结Imagemake产物rootfs.cpio.gz 最小目录树打包结果启动时解压到内存里当根概念上就叫 rootfs。运行内核qemu-system-aarch64-machinevirt-cpumax-nographic\-kernel/home/jiaju/learn/Linux/Linux_src/linux-6.19.11/arch/arm64/boot/Image\-initrd/home/jiaju/learn/Linux/busybox-1.36.1/rootfs/rootfs.cpio.gz\-appendconsolettyAMA0 rdinit/init说明-kernel你编出来的Image或 x86 的bzImage。-initrd上一步生成的busybox-1.36.1/rootfs/rootfs.cpio.gzQEMU 参数名仍叫 initrd文件名叫rootfs.cpio.gz即可。运行日志摘录启动时内核会打印很长的日志大部分与「第一篇认路」关系不大可略读。下面只保留最能说明「跑通了」的几行版本号、内核命令行、解压 initramfs、执行/init、进到 shell。$ qemu-system-aarch64 -machine virt -cpu max -nographic \ -kernel .../linux-6.19.11/arch/arm64/boot/Image \ -initrd .../busybox-1.36.1/rootfs/rootfs.cpio.gz \ -append consolettyAMA0 rdinit/init [ 0.000000] Linux version 6.19.11 ... [ 0.000000] Kernel command line: consolettyAMA0 rdinit/init [ 0.533035] Unpacking initramfs... [ 1.839149] Run /init as init process /bin/sh: cant access tty; job control turned off ~ # ls bin dev init linuxrc proc root rootfs.cpio.gz sbin sys usr ~ # df Filesystem 1K-blocks Used Available Use% Mounted on devtmpfs 21440 0 21440 0% /devcant access tty在-nographic下很常见一般可忽略。完整日志可自行重定向保存例如qemu ... 21 | tee boot.log。我们自己手搓出来的内核和最小 rootfs这样就算跑通了。后面我们围绕这套环境继续往下学 Linux 驱动。退出 QEMU用的是-nographic串口占满当前终端时可以这样退出先按Ctrla按住 Ctrl按一下 a然后松开再按x这是 QEMU 默认的「先进入前缀再发命令」Ctrla是QEMU 监视器/转义前缀x表示立刻退出。若无效多半是前缀被改过可先试Ctrla再按h看帮助或在另一个终端对 QEMU 进程kill。注意在客户机里正在跑的 shell 里Ctrla有时先被 shell 或程序截获若进不了 QEMU 转义用Ctrla再x仍是最常见做法。结束本节弄清了什么是用户态、什么是内核态知道内核里不能当普通 glibc 程序那样写并且用QEMU 自编内核 BusyBox rootfs跑通了第一个「能进 shell」的 Linux。这节信息量偏大建议你自己按文档敲一遍路径以你本机为准对齐即可。