第一章:驱动基础认知

大家好,我是这次课程的主讲。在正式开始之前,我想先聊聊一个很基础的问题——驱动程序到底是什么?

很多人刚接触驱动开发时,觉得这东西很神秘。其实说白了,驱动程序就是操作系统和硬件之间的翻译官。你想想看,操作系统不认识你的网卡、显卡、硬盘,它只知道怎么调度CPU、管理内存。那硬件怎么工作?谁来告诉操作系统「我这个设备能干啥」?嗯,就是驱动。

1.1 操作系统与硬件的关系

操作系统和硬件之间,是一种「分层协作」的关系。硬件在最底层,操作系统在中间层,应用程序在最上层。驱动就是连接操作系统和硬件的桥梁。

我刚开始做嵌入式开发时,遇到过一个问题:一块新的传感器芯片,插上后系统根本不识别。后来才发现,是内核里没有对应的驱动。操作系统只能通过驱动去访问硬件寄存器、读取状态、发送指令。没有驱动,硬件就是一块废铁。

这里有个关键点:操作系统不直接操作硬件。它通过驱动提供的接口来间接控制。比如你写一个read()函数,最终会调用驱动里的xxx_read()。这个调用链,就是驱动存在的意义。

核心理解:驱动是操作系统与硬件之间的契约。操作系统提供框架,驱动实现具体操作。

1.2 驱动分类:字符/块/网络设备

驱动按设备类型,主要分三类。我当年面试时,面试官第一个问题就是「说说字符设备和块设备的区别」。这个问题看似简单,但能看出你对驱动模型的理解深度。

类型 特点 典型设备 访问方式
字符设备 按字节流读写,顺序访问 串口、键盘、鼠标 open/read/write/close
块设备 按块读写,支持随机访问 硬盘、U盘、SD卡 通过文件系统挂载
网络设备 数据包收发,不通过文件节点 网卡、WiFi模块 socket接口

字符设备最简单,你打开一个设备文件,像读写普通文件一样操作它。块设备则复杂一些,因为它要支持文件系统。网络设备最特殊,它不暴露在/dev下,而是通过协议栈和socket打交道。

我记得有一次调试一个USB转串口驱动,发现数据总是丢字节。查了半天,原来是驱动里没有处理好缓冲区。字符设备虽然简单,但细节上很容易踩坑。

1.3 驱动开发环境搭建

环境搭建是入门的第一道坎。我见过太多人卡在这一步,编译不过、内核版本不对、模块加载失败……其实只要理清步骤,没那么难。

1.3.1 获取Linux内核源码

首先,你得有一份内核源码。我个人习惯从kernel.org下载稳定版,或者直接用发行版自带的源码包。

# 下载内核源码(以5.10 LTS为例)
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.100.tar.xz
tar -xf linux-5.10.100.tar.xz
cd linux-5.10.100

小技巧:如果你用的是Ubuntu,可以直接安装linux-source包,省去下载时间。但注意版本可能不是最新的。

1.3.2 配置内核

配置内核是编译前的关键步骤。你可以用当前系统的配置作为起点:

# 使用当前系统的配置
cp /boot/config-$(uname -r) .config
# 或者生成默认配置
make defconfig

这里有个坑:如果你直接make defconfig,可能会缺少你需要的驱动选项。我曾经编译完才发现网卡驱动没选上,又得重来一遍。建议用当前系统的配置,然后手动调整。

1.3.3 编译内核

编译内核需要一点时间,取决于你的CPU核心数。我建议用-j参数加速:

# 编译内核和模块
make -j$(nproc) bzImage
make -j$(nproc) modules

编译完成后,你会得到arch/x86/boot/bzImage这个内核镜像文件。模块则分散在各个目录下。

注意:编译内核时,确保你有足够的磁盘空间(至少10GB)。我遇到过磁盘写满导致编译失败的情况,很尴尬。

1.3.4 编写第一个驱动模块

环境搭好了,我们来写一个最简单的驱动——hello world。这不是花架子,它能验证你的开发环境是否正常。

// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Driver World!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Driver World!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple hello world driver");

对应的Makefile:

obj-m += hello.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

编译并加载:

make
sudo insmod hello.ko
sudo rmmod hello.ko
dmesg | tail

如果看到「Hello, Driver World!」和「Goodbye, Driver World!」,恭喜你,环境搭建成功了。

1.4 本章知识体系

为了让你更直观地理解本章内容,我画了一张图。它展示了驱动在整个系统中的位置,以及三类设备的关系。

应用程序 (User Space) 系统调用接口 (syscall) Linux 内核 (Kernel Space) VFS / 协议栈 / 内存管理 / 调度器 设备驱动 (Device Drivers) 硬件设备 (Hardware) 字符设备 块设备 / 网络设备

这张图很清楚地展示了:应用程序通过系统调用进入内核,内核再通过驱动访问硬件。驱动就是中间那一层「胶水」。三类设备在驱动层各有不同的实现方式,但最终都服务于上层应用。

本章要点回顾:

  • 驱动是操作系统与硬件之间的桥梁
  • 字符设备按字节流访问,块设备按块访问,网络设备通过socket通信
  • 搭建开发环境需要:内核源码 + 配置 + 编译 + 模块测试
  • 第一个驱动模块「hello world」能验证环境是否正常

嗯,第一章的内容就到这里。环境搭好了,后面我们才能动手写真正的驱动。下一章我们会深入字符设备驱动的实现细节,到时候会手把手带你写一个完整的驱动。

想要更多资料,联系可可