在 Linux 操作系统中,从驱动开发和系统管理的角度来看,设备主要分为 三大基本类型。此外,还有一些特殊的分类方式。
1. 字符设备 (Character Device)
这是 Linux 中最常见、最简单的设备类型。
- 访问特点 :按字节流(Byte Stream)顺序访问,像管道或文件流一样。通常不支持随机跳转读取(即不能像硬盘那样直接定位到任意位置读取,虽然有些字符设备支持
lseek,但大多数不支持)。 - 设备文件 :位于
/dev目录下。 - 常见例子 :
- 串口(UART/TTY):数据按顺序一个接一个发送。
- 输入设备:键盘、鼠标、触摸屏。
- I2C/SPI 从设备:如传感器、EEPROM(虽然 EEPROM 有地址,但在 Linux 驱动架构中常作为字符设备处理)。
- 控制台 :
/dev/console。 - 虚拟设备 :
/dev/null(黑洞)、/dev/zero、/dev/random。
2. 块设备 (Block Device)
块设备主要用于存储数据。
- 访问特点 :数据以"块"(Block,如 512 字节或 4KB)为单位进行传输。它支持随机访问(Random Access),你可以自由地读取第 1 块,然后直接跳到第 100 块。
- 内核缓存:为了提高性能,块设备在内核中有一层复杂的缓存管理(Buffer Cache)和调度算法(I/O Scheduler)。
- 常见例子 :
- 硬盘(HDD)、固态硬盘(SSD)。
- SD 卡、TF 卡、U 盘。
- RAM Disk(内存盘)。
- 注意 :块设备上通常会挂载文件系统(如 ext4, FAT32)。
3. 网络设备 (Network Device)
网络设备在 Linux 中比较特殊,它不遵循"一切皆文件"的设计哲学。
- 访问特点 :它没有对应的设备文件(即
/dev下找不到eth0或wlan0)。它是通过套接字(Socket)接口和特定的网络协议栈来访问的。 - 数据单位:以"数据包"(Packet)为单位进行收发。
- 管理工具 :使用
ifconfig或ip命令查看,而不是ls -l /dev。 - 常见例子 :
- 以太网卡(Ethernet)。
- Wi-Fi 网卡。
- 回环接口(loopback)。
总结与对比
| 类型 | 对应 /dev 文件 | 访问单位 | 随机访问 | 典型例子 |
|---|---|---|---|---|
| 字符设备 | 是 | 字节 (Byte) | 否 | 串口、鼠标、LED |
| 块设备 | 是 | 块 (Block) | 是 | 硬盘、U 盘 |
| 网络设备 | 否 | 数据包 (Packet) | 否 | 网卡、Wi-Fi |
补充:其他维度的分类
除了上述三大类,在深入学习 Linux 驱动时,你还会听到以下概念:
- 杂项设备 (Misc Device) :
- 本质上是字符设备。由于 Linux 主设备号资源有限,内核开发者把一些简单的字符设备统一归类为"杂项设备",它们共同占用主设备号 10,通过次设备号来区分。
- 伪设备 (Pseudo Device) :
- 不是真实的物理硬件,而是由内核软件模拟的设备,如
/dev/null、环回网卡、PTY(伪终端)。
- 不是真实的物理硬件,而是由内核软件模拟的设备,如
- 总线设备模型 (Bus, Device, Driver) :
- 这是 Linux 内核管理硬件的架构思路。设备按照连接的总线分类,如 USB 设备 、PCI 设备 、Platform 设备(平台设备,通常指直接集成在 SoC 内部的外设)。
回答你之前的上下文:
当你谈论 I2C 子设备的 Device Tree 时,大部分 I2C 从设备在 Linux 内核中最终会被注册为字符设备 (例如通过 /dev/i2c-n 直接访问,或者由专门的驱动如 input 子系统封装)。
字符设备驱动的注册流程
在 Linux 内核中,字符设备驱动的注册流程经历了从"旧版本"到"新版本"的演变。现代驱动开发通常采用**"分配设备号 + 注册 cdev + 自动创建设备节点"**的标准流程。
以下是现代 Linux 字符设备驱动注册的详细步骤:
1. 确定设备号 (Major & Minor Number)
设备号分为主设备号 (标识驱动类型)和次设备号(标识同类设备中的某一个)。
- 动态分配(推荐) :使用
alloc_chrdev_region()。系统会自动找一个空闲的主设备号,避免冲突。 - 静态申请 :使用
register_chrdev_region()。前提是你明确知道哪个主设备号是空的。
c
dev_t dev_id;
alloc_chrdev_region(&dev_id, 0, 1, "my_char_dev"); // 从0号次设备开始,申请1个设备
int major = MAJOR(dev_id); // 获取主设备号
int minor = MINOR(dev_id); // 获取次设备号
2. 定义并实现文件操作集 (file_operations)
这是驱动的核心,定义了用户空间调用 open、read、write 等系统调用时,内核具体执行什么函数。
c
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_dev_open,
.read = my_dev_read,
.write = my_dev_write,
.release = my_dev_close,
};
3. 初始化并添加 cdev 结构体
struct cdev 是内核描述字符设备的对象。这一步将"设备号"与"操作集"绑定并告诉内核:一个新的字符设备诞生了。
c
struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops); // 初始化 cdev,绑定 fops
my_cdev.owner = THIS_MODULE;
cdev_add(&my_cdev, dev_id, 1); // 将 cdev 加入内核,正式生效
4. 自动创建设备节点 (Class & Device)
如果不做这一步,你必须手动在终端输入 mknod /dev/xxx c 主设备号 次设备号。现代驱动通过 udev(或嵌入式中的 mdev)机制自动创建 /dev/xxx。
- 创建类 (Class) :在
/sys/class/下创建一个目录。 - 创建设备 (Device) :在
/dev/下创建具体的节点。
c
struct class *my_class;
struct device *my_device;
my_class = class_create(THIS_MODULE, "my_class_name");
my_device = device_create(my_class, NULL, dev_id, NULL, "my_char_dev");
// 此时 /dev/my_char_dev 节点就自动生成了
5. 注销流程 (模块卸载时)
当执行 rmmod 时,必须按照注册的反向顺序释放资源,否则会导致内核崩溃或资源泄漏。
c
device_destroy(my_class, dev_id); // 1. 销毁设备节点
class_destroy(my_class); // 2. 销毁类
cdev_del(&my_cdev); // 3. 从内核删除 cdev
unregister_chrdev_region(dev_id, 1); // 4. 释放设备号
总结:完整流程图
- 分配设备号 (
alloc_chrdev_region) --- 我有证件号了 - 初始化 cdev (
cdev_init) --- 我具备了处理业务的能力 (fops) - 注册 cdev (
cdev_add) --- 我去政府部门挂了号 - 创建类 (
class_create) --- 划分行业归属 - 创建设备节点 (
device_create) --- 在街面上挂出了招牌 (/dev/xxx),用户可以进店了
补充:关于"老版本"注册接口
你可能在一些老教程里看到 register_chrdev(major, name, fops) 这个函数。
- 优点:简单,一个函数搞定设备号和 cdev 注册。
- 缺点:它会直接占用该主设备号下的所有 256 个次设备号,非常浪费资源。
- 现状:现代工业级驱动通常不建议使用,除非是写非常简单的临时测试代码。
字符设备卸载全流程
字符设备驱动的卸载过程是注册过程的逆过程 。在 Linux 内核中,遵循 LIFO(后进先出) 的原则,即最后创建的资源应该最先被释放。
如果顺序错误,可能会导致内核崩溃(Oops),比如用户空间程序还在尝试访问一个已经不存在的设备节点。
以下是现代字符设备卸载的标准全流程:
1. 卸载流程图示
- 销毁设备节点 (
device_destroy) ------ 撤掉招牌 - 销毁类 (
class_destroy) ------ 注销行业归属 - 删除 cdev 结构体 (
cdev_del) ------ 从内核名单剔除 - 释放设备号 (
unregister_chrdev_region) ------ 归还证件号 - 清理硬件资源 (
iounmap/free_irq等) ------ 关闭电源、拆除线缆
2. 详细步骤拆解
第一步:销毁设备节点 (device_destroy)
这一步会删除 /dev/ 目录下的设备文件。如果不先做这一步,用户可能在驱动注销过程中还能看到并尝试打开这个设备。
c
// 参数1:所属的类;参数2:设备号
device_destroy(my_class, dev_id);
第二步:销毁类 (class_destroy)
删除 /sys/class/ 目录下对应的文件夹。
c
class_destroy(my_class);
第三步:删除字符设备 (cdev_del)
这一步最关键。它会从内核的 cdev_map(字符设备散列表)中移除该设备。执行完这一步后,系统内核就不再认为该设备号对应你的 file_operations 了。
c
cdev_del(&my_cdev);
第四步:释放设备号 (unregister_chrdev_region)
将主设备号和次设备号还给内核,以便其他驱动可以使用。
c
unregister_chrdev_region(dev_id, 1);
第五步:硬件及内存清理(扫尾工作)
如果你的驱动在初始化时申请了内存、映射了寄存器或申请了中断,必须在这里释放:
- 取消 IO 映射 :
iounmap(reg_base); - 释放 GPIO :
gpio_free(gpio_num); - 释放中断 :
free_irq(irq_num, dev_id); - 释放内存 :
kfree(buffer);
3. 代码示例:标准的驱动退出函数
c
static void __exit my_driver_exit(void)
{
/* 1. 删除设备节点 */
device_destroy(my_class, dev_id);
/* 2. 销毁类 */
class_destroy(my_class);
/* 3. 删除 cdev 结构体 */
cdev_del(&my_cdev);
/* 4. 释放设备号 */
unregister_chrdev_region(dev_id, 1);
/* 5. 硬件清理:比如取消地址映射 */
if (reg_base) {
iounmap(reg_base);
}
printk(KERN_INFO "My char device unregistered successfully\n");
}
module_exit(my_driver_exit);
4. 为什么顺序很重要?(注意事项)
-
先撤节点,再删 cdev :
如果先删除了
cdev但没撤掉/dev/xxx节点,用户进程可能正好打开了这个节点。由于内核已经找不到对应的fops,会导致不可预知的错误。 -
错误处理(Init 失败时的卸载) :
在
__init函数中,如果某一步(比如device_create)失败了,你必须通过goto语句跳转到对应的标签,按倒序把之前申请成功的资源释放掉。这叫"错误回滚"。 -
引用计数 :
如果当前有进程正打开着该设备文件(
/dev/xxx),执行rmmod时:- 内核会将该模块的引用计数降为 0(如果允许强制卸载)。
- 卸载后,原本指向驱动函数的指针会变成野指针。现代内核通过模块引用计数(
THIS_MODULE)来尽量防止正在使用中的驱动被卸载。
总结
卸载的核心逻辑就是:"怎么来的,就怎么倒着回去"。注册时是从底层到应用层(设备号 -> cdev -> class -> device),卸载时就是从应用层到底层(device -> class -> cdev -> 设备号)。
块设备与文件系统的关系
块设备(Block Device)与文件系统(File System)的关系,可以用**"仓库"与"仓库管理员"**来类比。块设备是物理上的存储空间,而文件系统是管理这些空间的逻辑规则。
在 Linux 体系结构中,它们处于不同的层次,通过**挂载(Mount)**动作连接在一起。
1. 核心定位对比
-
块设备 (物理底层/原始数据):
- 它是一个线性的、连续的存储空间。
- 它只认"块编号"(比如:给我读第 1024 号块,每块 512 字节)。
- 它不知道什么是"文件名",也不知道什么是"目录",更不知道文件权限。
- 典型代表:
/dev/sda(硬盘)、/dev/mmcblk0(SD卡)。
-
文件系统 (逻辑层/管理者):
- 它是建立在块设备之上的数据结构。
- 它负责把"文件名"映射到"块编号"上。
- 它记录了:文件叫什么?存在哪些块里?谁有权读写?文件什么时候创建的?
- 典型代表:
ext4、FAT32、NTFS、btrfs。
2. 它们是如何协同工作的?(层级关系)
当你执行 cp photo.jpg /mnt/usb/ 时,发生了以下过程:
- 用户层:你下达指令,操作对象是"文件名"。
- VFS (虚拟文件系统) :Linux 内核的中转站,它识别出你要往
/mnt/usb写数据。 - 具体文件系统 (如 ext4) :
- 文件系统查表(Inode Table),找出一块空闲的"领地"。
- 它决定:"把这个图片的数据放在第 500 到 800 号块吧。"
- 它还会更新目录信息,记录
photo.jpg在这里。
- 块设备驱动 :
- 文件系统发令:"把这些数据写到块地址 500-800。"
- 驱动程序把电信号发给硬件(硬盘/闪存),完成物理存储。
3. 关键纽带:格式化与挂载
-
格式化 (Formatting/Make Filesystem):
- 本质是在块设备上**"画格子"并建立档案库**。
- 当你执行
mkfs.ext4 /dev/sda1时,就是在给原始的块设备贴上标签,划分出超级块(Superblock)、i节点区(Inode Table)和数据区。 - 没有格式化的块设备被称为"裸设备"(Raw Device),文件系统无法识别。
-
挂载 (Mounting):
- 这是将文件系统的根目录 与系统目录树中的某个点(挂载点)结合的过程。
- 通过挂载,原本孤立的块设备空间变成了
/home或/var下的可访问路径。
4. 形象类比:图书馆
- 块设备 = 图书馆的空书架。它只是一排排带编号的格子(块),你可以往里面塞书,但如果你不加管理,很快就找不到哪本书在哪。
- 文件系统 = 图书管理系统 。
- 它有一张索引卡(Inode),记录了《三国演义》放在 A 区 5 号柜(块地址)。
- 它有分类目录(Directory Structure),让你能通过"文学->名著->三国"找到书,而不需要记编号。
- 格式化 = 木匠进入空房间,把书架打好,并贴上索引标签。
5. 特殊情况:没有文件系统的块设备
块设备不一定非要配合文件系统使用:
- 数据库:某些大型数据库(如 Oracle)会直接读写"裸块设备",因为它们有自己专门的存储管理逻辑,不需要文件系统来"多此一举",从而追求更高的性能。
- 交换分区 (Swap):Linux 的交换空间也是直接操作块设备的,没有文件系统。
总结
块设备提供"地皮",文件系统在上面"盖房子"。 用户通过"房子"(文件系统)来居住和存放物品,而不需要关心底下的"地皮"(块设备)具体是怎么排列的。