第一章:字符设备驱动概述
大家好,我是老李。做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和加上ARCH、CROSS_COMPILE即可。
避坑指南:我曾经在Makefile里忘了加M=$(PWD),结果make命令把整个内核都编译了一遍……等了半小时才发现。记住,M=告诉内核构建系统:我只编译外部模块,别碰内核本体。
本章知识体系
为了让你更直观地理解本章内容,我画了一张结构图:
这张图把本章的三个核心知识点串起来了。你从左往右看:先理解什么是字符设备,再对比它和块设备的区别,最后搭建好开发环境。三步走,稳得很。
写在最后
环境搭建这块,我建议你亲自动手做一遍。别光看,敲命令、编译、加载模块,每一步都自己走一遍。我当年学驱动开发时,光环境就搭了五六遍才彻底搞明白。
嗯,第一章就到这里。记住:字符设备驱动是Linux驱动开发的基石,打好基础,后面学起来就顺了。