linux内核驱动开发视频课程

Linux 内核驱动开发入门:从环境搭建到第一个字符设备驱动

面向读者:

  • 会写 C 和 make、但对 insmod 心存敬畏的 Linux 应用开发工程师
  • 想打开"内核世界大门"的嵌入式在校生
  • 被"段错误"劝退、又心痒想点亮小灯的极客
    读完收获:

① 一套能反复编译 >=20 次不炸的驱动开发环境(Ubuntu 22.04 + QEMU + 5.15 内核)

② 亲手写出 < 100 行的"hello-char"字符设备,并看懂 dmesg 每一行

③ 掌握"加载-查看-卸载-调试"完整闭环,为后续 platform/device-tree/中断/时钟树打地基


一、前置知识清单(只需 3 项)

技能 最低要求 推荐速补链接
C 语言 能写结构体 + 函数指针 《C Primer Plus》第 6 章
Makefile 看得懂目标、依赖、变量 GNU Make 官方 10 分钟教程
Shell 基础 ls /dev dmesg -H man 手册即可

不懂内核 API?------没关系,本文出现的每一个函数都会给原型 + 一行注释。

没硬件?------用 QEMU 跑纯内存虚拟板子,同样能 insmod


二、20 分钟搭好"不踩坑"开发环境

1. 宿主机选型

  • Windows:WSL2 + Ubuntu 22.04(内核 5.15+)
  • macOS:UTM 装 Ubuntu 22.04 ARM64
  • Linux:直接真机,建议 distro >= Ubuntu 20.04

2. 一键脚本(Ubuntu 22.04 验证通过)

bash 复制代码
#!/bin/bash
# file: setup_env.sh
sudo apt update && sudo apt install -y \
  build-essential git vim cscope ctags \
  linux-headers-$(uname -r) \
  qemu qemu-system-x86 \
  flex bison libssl-dev libelf-dev bc

执行完占用磁盘约 2.1 GB。

3. 验证工具链

bash 复制代码
$ gcc --version        # >= 9.0 即可
$ make -v              # >= 4.0
$ qemu-system-x86_64 --version

三、下载并编译"同源"内核(5.15 LTS)

目标:让驱动模块和内核版本号 完全一致 ,避免 vermagic 加载失败。

bash 复制代码
# 1. 取官方长期支持版
$ git clone --depth 1 -b linux-5.15.y \
    https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git ~/kernel-5.15
$ cd ~/kernel-5.15

# 2. 生成默认配置(x86_64)
$ make x86_64_defconfig

# 3. 打开"启用模块版本校验"方便调试
$ make menuconfig
    -> Enable loadable module support
       -> Module versioning support (CONFIG_MODVERSIONS) = y
    -> Kernel hacking
       -> Compile-time checks and compiler options
          -> Compile the kernel with debug info (CONFIG_DEBUG_INFO) = y

# 4. 编译(i7-11800H 约 12 min)
$ make -j$(nproc)

# 5. 安装模块到 /lib/modules/5.15.0+
$ sudo make modules_install
$ sudo cp arch/x86/boot/bzImage /boot/vmlinuz-5.15-lab

用 QEMU 启动自编译内核(无真实硬件也 OK)

bash 复制代码
$ qemu-system-x86_64 \
  -kernel /boot/vmlinuz-5.15-lab \
  -drive file=rootfs.img,format=raw  \  # 可共用宿主机目录
  -append "root=/dev/sda rw console=ttyS0 nokaslr" \
  -nographic -m 2G -smp 2

nokaslr 关闭地址随机化,方便 gdb 调试。


四、字符设备驱动"hello-char"源码

功能:

  • 注册设备节点 /dev/hello
  • 支持 open/close/read/write
  • kmalloc 分配 4 KB 环形缓冲区
  • pr_info 打印每次操作

目录树:

复制代码
hello_char/
├── hello_char.c
├── Makefile
└── README.md

1. hello_char.c(代码共 88 行,含空行)

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>   /* copy_to_user */
#include <linux/slab.h>      /* kmalloc */

#define DEVICE_NAME "hello"
#define BUF_SIZE  4096

static int major;
static char *buf;
static int wp;
static dev_t dev;
static struct cdev hello_cdev;

/* open */
static int hello_open(struct inode *inode, struct file *filp)
{
    pr_info("hello: open\n");
    return 0;
}

/* read */
static ssize_t hello_read(struct file *filp, char __user *user,
                          size_t sz, loff_t *off)
{
    int ret;
    if (sz > BUF_SIZE) sz = BUF_SIZE;
    ret = copy_to_user(user, buf, sz);
    return ret == 0 ? sz : -EFAULT;
}

/* write */
static ssize_t hello_write(struct file *filp, const char __user *user,
                           size_t sz, loff_t *off)
{
    int ret;
    if (sz > BUF_SIZE - wp) sz = BUF_SIZE - wp;
    ret = copy_from_user(buf + wp, user, sz);
    if (ret == 0) {
        wp += sz;
        return sz;
    }
    return -EFAULT;
}

/* release */
static int hello_release(struct inode *inode, struct file *filp)
{
    pr_info("hello: release\n");
    return 0;
}

static const struct file_operations hello_fops = {
    .owner   = THIS_MODULE,
    .open    = hello_open,
    .read    = hello_read,
    .write   = hello_write,
    .release = hello_release,
};

/* 模块初始化 */
static int __init hello_init(void)
{
    int err;
    /* 1. 申请设备号 */
    err = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
    if (err < 0) goto fail;
    major = MAJOR(dev);

    /* 2. 分配缓冲区 */
    buf = kmalloc(BUF_SIZE, GFP_KERNEL);
    if (!buf) { err = -ENOMEM; goto unregister; }

    /* 3. 注册字符设备 */
    cdev_init(&hello_cdev, &hello_fops);
    hello_cdev.owner = THIS_MODULE;
    err = cdev_add(&hello_cdev, dev, 1);
    if (err) goto free_buf;

    pr_info("hello: loaded, major=%d, device /dev/%s\n", major, DEVICE_NAME);
    return 0;

free_buf:
    kfree(buf);
unregister:
    unregister_chrdev_region(dev, 1);
fail:
    return err;
}

/* 模块卸载 */
static void __exit hello_exit(void)
{
    cdev_del(&hello_cdev);
    kfree(buf);
    unregister_chrdev_region(dev, 1);
    pr_info("hello: unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("lab<lab@example.com>");
MODULE_DESCRIPTION("The simplest char device driver");

2. Makefile(万能模板,兼容多内核版本)

makefile 复制代码
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD       := $(shell pwd)
obj-m     := hello_char.o

all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

五、编译-加载-测试一条龙

1. 编译

bash 复制代码
$ cd hello_char
$ make
  LD [M]  hello_char.ko

2. 插入模块 & 查看设备号

bash 复制代码
$ sudo insmod hello_char.ko
$ dmesg | tail
[  112.345] hello: loaded, major=240, device /dev/hello

3. 手动创建设备节点(也可自动,见下一节)

bash 复制代码
$ sudo mknod /dev/hello c 240 0
$ sudo chmod 666 /dev/hello

4. 读写测试

bash 复制代码
$ echo "kernel driver" > /dev/hello
$ cat /dev/hello
kernel driver

5. 卸载

bash 复制代码
$ sudo rmmod hello_char
$ dmesg | tail
[  180.112] hello: unloaded

六、进阶:自动创建设备节点(udev)

device_create + class_create 即可省去 mknod

c 复制代码
static struct class *hello_class;

/* 在 hello_init 末尾添加 */
hello_class = class_create(THIS_MODULE, DEVICE_NAME);
device_create(hello_class, NULL, dev, NULL, DEVICE_NAME);

/* 在 hello_exit 开头移除 */
device_destroy(hello_class, dev);
class_destroy(hello_class);

重新加载后,/dev/hello 自动生成,权限 660,可写 udev 规则改为 666。


七、调试三板斧:打印、查看、断点

1. pr_info + dmesg -w

  • 级别:KERN_INFO 可显示,KERN_DEBUG 需打开 DEBUG
  • 限速:同一行多次打印用 pr_info_once()

2. 模块信息 & 符号表

bash 复制代码
$ modinfo hello_char.ko
$ cat /proc/modules | grep hello
$ sudo cat /sys/module/hello_char/sections/.text

3. gdb + qemu 源码级调试(可选)

QEMU 启动加 -s -S,另开终端:

bash 复制代码
$ gdb vmlinux
(gdb) target remote :1234
(gdb) add-symbol-file hello_char.ko 0xffffffffc0000000
(gdb) b hello_write
(gdb) c

八、常见错误对照表(收藏级)

现象 原因 解决
insmod: ERROR: could not insert module hello_char.ko: Invalid module format vermagic 不匹配,模块用 5.15 编译,内核是 5.4 确保用同版本 KERNELDIR
Unknown symbol __kmalloc include <linux/slab.h> 加头文件
Device or resource busy 设备节点被占用,或忘记 cdev_del lsof /dev/hello 后 kill
Unable to handle kernel NULL pointer dereference 空指针解引用 kgdb/kdump 定位

九、下一步往哪走?

  1. 同步/互斥:把环形缓冲区改成 kfifo,加自旋锁
  2. 中断/GPIO:在树莓派上点亮 LED,用 gpiod 新接口
  3. platform + 设备树:写 I²C 温湿度传感器驱动
  4. 调试工具进阶:ftrace, perf, eBPF

十、总结:你刚完成了"从 0 到 1"

✅ 会编译内核与模块

✅ 理解字符设备骨架:申请设备号→cdev_addfile_operations

✅ 掌握 insmod/rmmod/dmesg 调试闭环

✅ 会用 copy_to_user/copy_from_user 做内核-用户数据交换

内核世界的大门已推开,下一站:中断、DMA、设备树、电源管理......

保持好奇,保持敬畏,Happy Hacking!

相关推荐
无敌最俊朗@3 小时前
Linux 进程创建与控制详解
linux·运维·服务器
张红尘3 小时前
龙蜥OS8.10配置repo源使用RPM安装Redis8.2
linux·redis·操作系统
编程点滴3 小时前
前端项目从 Windows 到 Linux:构建失败的陷阱
linux·前端
小白银子4 小时前
零基础从头教学Linux(Day 43)
linux·运维·服务器·nginx
安审若无5 小时前
PMON failed to acquire latch 的报错及sqlplus / as sysdba 无法连接
linux·运维·数据库
9毫米的幻想5 小时前
【Linux系统】—— 环境变量
linux·服务器·c语言·c++
DARLING Zero two♡5 小时前
【Linux操作系统】简学深悟启示录:动静态库
linux·运维·服务器
web安全工具库6 小时前
Linux ls 命令进阶:从隐藏文件到递归显示,成为文件浏览大师
linux·运维·服务器·c语言·开发语言
我要成为c嘎嘎大王7 小时前
【Linux】进程的概念和状态
linux·运维·服务器