第一章:字符设备驱动概述

大家好,我是老李。做Linux内核驱动开发十几年了,今天咱们聊聊字符设备驱动。

说实话,字符设备驱动是Linux驱动开发里最基础、也是最容易上手的一类。我当年刚入行时,第一个练手的项目就是写一个简单的字符设备驱动——让一个LED灯能通过应用程序控制亮灭。嗯,就是那种「Hello World」级别的驱动。

但你别小看它。字符设备驱动的框架,几乎贯穿了所有复杂驱动(比如USB、PCIe、I2C设备)的设计思路。说白了,搞懂了字符设备,你就拿到了Linux驱动开发的钥匙。

什么是字符设备?

字符设备,顾名思义,就是以字节流(字符)为单位进行数据传输的设备。你往设备文件里写一个字节,驱动就处理一个字节;你读一个字节,驱动就返回一个字节。

典型的字符设备有哪些?

  • 串口(UART):一个字节一个字节地收发数据
  • GPIO:控制引脚高低电平,本质上就是读写0或1
  • 鼠标、键盘:输入的是一个个事件数据包
  • /dev/null、/dev/random:虚拟字符设备

我个人习惯把字符设备想象成「水管」——数据像水流一样,从应用程序流到驱动,再从驱动流回应用程序。没有复杂的寻址,没有随机访问,就是简单的顺序读写。

核心特征:字符设备在/dev目录下对应一个设备文件,应用程序通过open、read、write、ioctl等系统调用与驱动交互。

字符设备 vs 块设备

很多初学者会问:字符设备和块设备到底有啥区别?我当年也困惑过。

简单来说:

对比项 字符设备 块设备
数据传输单位 字节(字符)流 块(通常512字节或更大)
访问方式 顺序访问为主 随机访问(支持seek)
缓存机制 通常无内核缓存 有页缓存(Page Cache)
典型设备 串口、GPIO、鼠标 硬盘、SSD、U盘
设备文件类型 c(character) b(block)

你想想看,硬盘为什么是块设备?因为操作系统需要以块为单位管理数据,还要做缓存、排序、合并等优化。而串口呢?你发一个字节,驱动就得立刻处理一个字节,没法缓存一堆再一起发——这就是字符设备的典型场景。

我的经验:判断一个设备该归为字符还是块,有个简单方法——看它是否需要「随机访问」。如果需要像文件一样跳来跳去读写,那就是块设备;如果只是顺序收发数据流,那就是字符设备。

驱动开发环境搭建

好了,理论说完了,咱们来点实际的。要写字符设备驱动,你得先把环境搭好。我见过太多新手卡在这一步——环境没配好,代码写再多也跑不起来。

1. 内核源码树

驱动是内核的一部分,编译驱动必须依赖内核源码树。说白了,你需要一份与目标系统匹配的内核源码。

有两种方式:

  • 方式一:下载完整内核源码
    # 以Linux 5.10为例
    wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.tar.xz
    tar -xf linux-5.10.tar.xz
    cd linux-5.10
    make defconfig
    make modules_prepare
  • 方式二:使用发行版提供的头文件包
    # Ubuntu/Debian
    sudo apt-get install linux-headers-$(uname -r)

我个人更推荐方式一,尤其是做嵌入式开发时。因为发行版的头文件包有时会裁剪掉一些必要的头文件,导致编译报错。我曾经被这个坑过——折腾了两天才发现是头文件不全。

注意:内核源码树的版本必须与你运行驱动的内核版本一致。否则编译出来的模块加载时会报「Invalid module format」错误。

2. 交叉编译链

如果你是在PC上开发,目标平台也是PC(x86),那直接用gcc就行。但如果你做嵌入式开发(比如ARM、RISC-V),就需要交叉编译链。

举个例子,我常用的ARM交叉编译链安装方式:

# 安装ARM交叉编译工具链
sudo apt-get install gcc-arm-linux-gnueabihf

# 验证安装
arm-linux-gnueabihf-gcc --version

然后在编译驱动时,指定交叉编译器和内核源码路径:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C /path/to/kernel M=$(PWD) modules

这里有个小细节:CROSS_COMPILE后面的横线不能丢。我刚开始写驱动时,经常漏掉这个横线,结果编译出来的模块架构不对,加载时直接报错。

3. 一个简单的Makefile模板

写字符设备驱动,Makefile是标配。下面这个模板我用了十年,基本没变过:

# Makefile for character device driver
obj-m := mychardev.o

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

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

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

如果你用交叉编译,只需要修改KERNELDIR和加上ARCHCROSS_COMPILE即可。

避坑指南:我曾经在Makefile里忘了加M=$(PWD),结果make命令把整个内核都编译了一遍……等了半小时才发现。记住,M=告诉内核构建系统:我只编译外部模块,别碰内核本体。

本章知识体系

为了让你更直观地理解本章内容,我画了一张结构图:

字符设备驱动概述 - 知识体系 什么是字符设备 字节流传输 顺序访问为主 典型设备:串口、GPIO 字符 vs 块设备 传输单位:字节 vs 块 缓存机制不同 设备文件类型:c vs b 环境搭建 内核源码树 交叉编译链 Makefile编写 核心目标:掌握字符设备驱动的基本概念与开发环境 为后续编写第一个字符设备驱动打下基础

这张图把本章的三个核心知识点串起来了。你从左往右看:先理解什么是字符设备,再对比它和块设备的区别,最后搭建好开发环境。三步走,稳得很。

写在最后

环境搭建这块,我建议你亲自动手做一遍。别光看,敲命令、编译、加载模块,每一步都自己走一遍。我当年学驱动开发时,光环境就搭了五六遍才彻底搞明白。

嗯,第一章就到这里。记住:字符设备驱动是Linux驱动开发的基石,打好基础,后面学起来就顺了。


专注资料整理