一、设备与设备文件
在 Linux 中,内核通过驱动程序管理各种硬件设备或虚拟设备,例如:
- 串口控制器
- GPIO 控制器
- 随机数发生器
- 存储控制器
- 显示控制器等
用户空间程序不会直接操作硬件寄存器,而是通过内核提供的统一接口访问设备。
前面的章节我们讲过,就是 设备文件(device file),通常位于 /dev 目录下。
在开发板上执行:
ls -l /dev可以看到类似:
crw-rw-rw- 1 root root 1, 3 12月 9 19:02 null
crw-rw---- 1 root tty 4, 0 12月 9 19:02 tty0
brw-rw---- 1 root disk 179, 0 12月 9 19:02 mmcblk02
3
这些条目并不是普通的磁盘文件,而是“设备的访问入口”。 用户空间通过对这些设备文件进行 open / read / write / ioctl 等操作,从而间接访问实际设备。
二、字符设备与块设备的基本分类
从“访问方式”的角度,Linux 将设备大致分为两大类:
- 字符设备(Character Device)
- 块设备(Block Device)
在 /dev 中,可以通过 ls -l 输出的第 1 列第 1 个字符区分:
- 以
c开头的:字符设备(character) - 以
b开头的:块设备(block)
示例:
crw-rw-rw- 1 root root 1, 3 ... null # 字符设备
brw-rw---- 1 root disk 179, 0 ... mmcblk0 # 块设备2
1. 字符设备
字符设备(Character Device)以字节流的方式对外提供访问接口。其特点包括:
- 按字节(或字节序列)顺序读写数据,类似于流式 I/O。
- 通常不要求对齐到固定大小的数据块。
- 适用于各种控制类设备或流式数据设备。
典型例子:
- 串口终端:
/dev/ttyS0、/dev/ttyS1等 - 控制台:
/dev/console - 随机数设备:
/dev/random、/dev/urandom - 虚拟设备:
/dev/null、/dev/zero - GPIO / I2C / SPI 等控制接口在很多系统上也通过字符设备形式呈现
- 用户自定义的字符设备:如
/dev/mychar等
用户空间访问字符设备示例:
int fd = open("/dev/ttyS0", O_RDWR);
write(fd, buf, len);
read(fd, buf, len);
close(fd);2
3
4
在内核中,这类设备通常由字符设备驱动实现,通过一组回调函数(file_operations)响应上述系统调用。
2. 块设备
块设备(Block Device)以固定大小的数据块为单位进行访问,例如 512 字节、4 KB 等。其特点包括:
- 支持高效的随机访问和缓存机制。
- 主要用于各种持久化存储介质。
典型例子:
- eMMC / SD 卡:
/dev/mmcblk0、/dev/mmcblk0p1等 - U 盘、SATA/USB 磁盘:
/dev/sda、/dev/sdb - 压缩内存块设备:
/dev/zram0等
块设备通常并不会直接被应用程序以“原始块”形式频繁访问,而是:
- 在其上创建文件系统(如 ext4、vfat 等);
- 把该块设备挂载到某个目录;
- 通过普通文件读写接口访问文件系统中的文件。
块设备驱动与文件系统配合使用,相对复杂,暂不不重点展开。
三、为什么重点学习字符设备驱动
在 Linux 驱动开发中,存在多种类型的驱动:
- 字符设备驱动
- 块设备驱动
- 网络设备驱动
- 输入子系统驱动(键盘、触摸屏等)
- 声音子系统驱动(ALSA)
- 图形 / 显示驱动等
对于初学者而言,从字符设备驱动入门具有以下优势:
- 接口形式直观 字符设备使用的操作接口与普通文件类似,包括:
openreadwriteioctlclose等 这些接口在用户空间编程中也经常使用,便于理解。
- 调试方式简洁 字符设备一旦在
/dev下建立了节点,可以直接通过:echo/cathexdump- 简单的 C 测试程序 对设备进行读写操作,验证驱动逻辑。
- 适用范围广 很多简单或中等复杂度的外设,都可以通过字符设备模型进行封装,例如:
- GPIO 控制
- 简单的传感器
- LED 灯控制
- 虚拟测试设备等
因此,这一章节将围绕字符设备的基本概念和实现方式展开,包括:
- 设备号的申请与管理
- 字符设备框架(
cdev/miscdevice等) - 设备节点的创建(
/dev/mychar) - 用户应用与驱动之间的数据交互方式等。
四、接口:file_operations
用户空间在访问设备文件时,使用标准的系统调用:
open()/close()read()/write()ioctl()等
内核在接收到这些调用后,需要将其转交给具体设备驱动去处理。 这一过程在字符设备中,主要通过 struct file_operations 完成。
一个典型的字符设备驱动中,会定义类似如下的结构体:
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};2
3
4
5
6
7
8
含义可以简单理解为:
- 当用户空间对
/dev/mychar调用open()时,内核会调用驱动中的my_open()。 - 当调用
read()时,会进入my_read()。 - 当调用
write()时,会进入my_write()。 - 当调用
ioctl()时,会进入my_ioctl()。
因此,从内核实现角度看:
字符设备驱动 = 一组
**file_operations**回调函数 + 设备号注册 + 与**/dev**设备节点的绑定。