当前位置: 首页 > news >正文

Linux驱动篇开篇——《驱动篇》

前言

我们正式迈入 Linux 真正的底层。之前的 Linux 系统编程,多半是在「系统调用之上」写功能,对内核底下究竟做了什么往往只有模糊印象:例如socket 怎样用到网卡open/write 落盘时块设备栈里发生了什么。带着这些问题,往下看会更有方向。

所以我们本次大章节开始学习Linux驱动,闲话少说我们开始进入真正的硬核。

首先我们先声明一个概念,什么是用户层,什么是内核层。

用户态 vs 内核态

上图展示的就是用户态和内核态的区别。我们之前写的程序其实都是运行在用户态(也叫用户层),在这个状态下,程序本身是不能直接和硬件打交道的,所有对硬件的操作都必须通过内核来实现,也就是经过内核态才能真正和硬件交互。而我们熟悉的单片机开发,程序其实就是直接运行在类似“内核态”的环境里,可以直接控制硬件IO,所以代码会显得很自由甚至有些混乱。Linux为了更好地管理和保护系统,把功能区分得很清楚:内核专注于提供底层功能和安全,上层(用户态)只用关注实现自己的业务逻辑,这样系统更稳定、更安全。

进入内核的编写习惯,建立内核语法

1. 内核代码风格和实现差异

在用户态编程中,我们习惯性地用到很多标准C库函数(如printf,malloc,free等),这些基本都是由glibc来实现的。而进到内核后,情况就大不一样了:

  • 标准库不可用: 内核空间没有所谓的标准库,所有的字符串、内存操作、输出输入等都由内核自己提供一套实现,通常有类似但名称不同的函数,比如:
    • 打印调试信息:printk()取代printf()
    • 字符串操作:strcpystrcat等依然存在,但是在内核单独实现版
    • 内存分配:kmalloc()kfree()取代用户空间的malloc()free()
  • 头文件不同:包含的头文件都是内核相关,比如<linux/module.h>,<linux/kernel.h>
  • 不能随意使用浮点:内核代码一般不允许使用浮点数运算。
  • 不同的规范与约束:内核对代码风格、注释方式、内存管理、同步等有自己的一套严格约束。

举个简单的内核模块代码片段:

#include<linux/init.h>#include<linux/module.h>staticint__inithello_init(void){printk(KERN_INFO"Hello, Linux kernel!\n");return0;}staticvoid__exithello_exit(void){printk(KERN_INFO"Goodbye, 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.org

Linux内核源码主要存放在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 dwarves
  • libelf-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 filesystem;QEMU 参数仍写-initrd,因为内核把它当作 initial ramdisk 加载)。

1. 内核镜像

make编出来即可:

  • ARM64arch/arm64/boot/Image
  • x86_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 SettingsBuild 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/init<<'EOF' #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev exec /bin/sh EOFchmod+x ./rootfs/init

上面这个是创建启动脚本,也就是我们第一个运行的程序。

# 这条命令的意思是:进入rootfs目录,将里面所有文件和目录用cpio打包成 newc 格式,再用gzip压缩为rootfs.cpio.gz(cd./rootfs&&find.-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指向该文件的绝对路径或相对路径均可。

小结Image=make产物;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\-append"console=ttyAMA0 rdinit=/init"

说明:

  • -kernel:你编出来的Image(或 x86 的bzImage)。
  • -initrd:上一步生成的busybox-1.36.1/rootfs/rootfs.cpio.gz(QEMU 参数名仍叫 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 "console=ttyAMA0 rdinit=/init" [ 0.000000] Linux version 6.19.11 ... [ 0.000000] Kernel command line: console=ttyAMA0 rdinit=/init [ 0.533035] Unpacking initramfs... [ 1.839149] Run /init as init process /bin/sh: can't 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% /dev

can't access tty-nographic下很常见,一般可忽略。完整日志可自行重定向保存,例如:qemu ... 2>&1 | tee boot.log。)

我们自己手搓出来的内核和最小 rootfs,这样就算跑通了。后面我们围绕这套环境继续往下学 Linux 驱动。

退出 QEMU

用的是-nographic,串口占满当前终端时,可以这样退出:

  1. 先按Ctrl+a(按住 Ctrl,按一下 a,然后松开
  2. 再按x

这是 QEMU 默认的「先进入前缀,再发命令」:Ctrl+aQEMU 监视器/转义前缀x表示立刻退出

若无效,多半是前缀被改过,可先试Ctrl+a再按h看帮助,或在另一个终端对 QEMU 进程kill

注意:在客户机里正在跑的 shell 里,Ctrl+a有时先被 shell 或程序截获;若进不了 QEMU 转义,用Ctrl+ax仍是最常见做法。

结束

本节弄清了什么是用户态、什么是内核态,知道内核里不能当普通 glibc 程序那样写,并且用QEMU + 自编内核 + BusyBox rootfs跑通了第一个「能进 shell」的 Linux。这节信息量偏大,建议你自己按文档敲一遍,路径以你本机为准对齐即可。

http://www.jsqmd.com/news/632755/

相关文章:

  • 幻镜NEURAL MASK在文化遗产数字化中的应用:古籍插图主体提取
  • 荣耀最强数字旗舰来了!荣耀600 Pro真机揭晓
  • 【推荐】银发经济小程序
  • ANIMATEDIFF PRO效果展示:看看这些用文字生成的电影级动态画面
  • Llama-3.2V-11B-cot企业实操:中小企业低成本视觉AI部署方案
  • 构建基于SDMatte的智能相册:自动人物分类与场景相册生成
  • Phi-3-mini-4k-instruct-gguf赋能课程设计:自动生成Multisim电路仿真报告
  • CHORD-X在复杂网络环境下的部署:解决403 Forbidden等访问问题
  • Hunyuan-MT-7B-WEBUI部署详解:Jupyter环境下一键启动全流程
  • RMBG-2.0 API封装教程:将Streamlit工具转为REST接口供其他系统调用
  • LFM2.5-1.2B-Thinking-GGUF快速体验:无需安装的在线API调用演示
  • DeepSeek-R1-Distill-Qwen-1.5B开箱即用:本地AI服务搭建全攻略
  • 新概念英语第一册125_Tea for two
  • Python的__enter__中的预防泄漏资源
  • 板级支持包的构建
  • Hunyuan-MT-7B部署教程:vLLM推理+Chainlit前端完整配置
  • CosyVoice入门指南:使用Typora编辑Markdown并一键生成语音笔记
  • OFA图文匹配系统实战教程:3步搭建智能内容审核Web应用
  • Pixel Dream Workshop 软件测试实战:AI图像生成模型的自动化测试策略
  • 模型血缘追踪不是可选项,而是生存线:20年MLOps专家亲授7步构建不可篡改的AI溯源链
  • CentOS 7.9 SNAT/DNAT 详解与 VMware 17 实验全流程【20260412】001篇
  • Stable Diffusion Anything V5保姆级教程:零基础小白也能玩转AI绘画
  • 一文学习 Spring 声明式事务源码全流程总结脊
  • 2026年质量好的长春钝化处理工艺/汽车零部件钝化处理/铝合金钝化处理/压铸件钝化处理行业内知名厂家推荐 - 品牌宣传支持者
  • 锂电池测试规范MSDS与UN38.3认证的关系
  • 训练阶段未对齐,推理必然崩塌!,SITS2026首次公开长上下文预训练数据配比黄金公式(含Python验证脚本)
  • 翻译模型Hunyuan-MT-7B体验分享:开箱即用,38种语言互译效果超预期
  • 立知-多模态重排序模型lychee-rerank-mm实战:基于LangChain的智能文档处理系统
  • OpenCV多线程编程:从单线程到多线程的视频处理方
  • 5秒克隆声音!IndexTTS 2.0零基础教程:手把手教你制作专属配音