linux设备全解析

在 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 下找不到 eth0wlan0)。它是通过套接字(Socket)接口和特定的网络协议栈来访问的。
  • 数据单位:以"数据包"(Packet)为单位进行收发。
  • 管理工具 :使用 ifconfigip 命令查看,而不是 ls -l /dev
  • 常见例子
    • 以太网卡(Ethernet)。
    • Wi-Fi 网卡。
    • 回环接口(loopback)。

总结与对比

类型 对应 /dev 文件 访问单位 随机访问 典型例子
字符设备 字节 (Byte) 串口、鼠标、LED
块设备 块 (Block) 硬盘、U 盘
网络设备 数据包 (Packet) 网卡、Wi-Fi

补充:其他维度的分类

除了上述三大类,在深入学习 Linux 驱动时,你还会听到以下概念:

  1. 杂项设备 (Misc Device)
    • 本质上是字符设备。由于 Linux 主设备号资源有限,内核开发者把一些简单的字符设备统一归类为"杂项设备",它们共同占用主设备号 10,通过次设备号来区分。
  2. 伪设备 (Pseudo Device)
    • 不是真实的物理硬件,而是由内核软件模拟的设备,如 /dev/null、环回网卡、PTY(伪终端)。
  3. 总线设备模型 (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)

这是驱动的核心,定义了用户空间调用 openreadwrite 等系统调用时,内核具体执行什么函数。

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

  1. 创建类 (Class) :在 /sys/class/ 下创建一个目录。
  2. 创建设备 (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. 释放设备号

总结:完整流程图

  1. 分配设备号 (alloc_chrdev_region) --- 我有证件号了
  2. 初始化 cdev (cdev_init) --- 我具备了处理业务的能力 (fops)
  3. 注册 cdev (cdev_add) --- 我去政府部门挂了号
  4. 创建类 (class_create) --- 划分行业归属
  5. 创建设备节点 (device_create) --- 在街面上挂出了招牌 (/dev/xxx),用户可以进店了

补充:关于"老版本"注册接口

你可能在一些老教程里看到 register_chrdev(major, name, fops) 这个函数。

  • 优点:简单,一个函数搞定设备号和 cdev 注册。
  • 缺点:它会直接占用该主设备号下的所有 256 个次设备号,非常浪费资源。
  • 现状:现代工业级驱动通常不建议使用,除非是写非常简单的临时测试代码。


字符设备卸载全流程

字符设备驱动的卸载过程是注册过程的逆过程 。在 Linux 内核中,遵循 LIFO(后进先出) 的原则,即最后创建的资源应该最先被释放。

如果顺序错误,可能会导致内核崩溃(Oops),比如用户空间程序还在尝试访问一个已经不存在的设备节点。

以下是现代字符设备卸载的标准全流程:


1. 卸载流程图示

  1. 销毁设备节点 (device_destroy) ------ 撤掉招牌
  2. 销毁类 (class_destroy) ------ 注销行业归属
  3. 删除 cdev 结构体 (cdev_del) ------ 从内核名单剔除
  4. 释放设备号 (unregister_chrdev_region) ------ 归还证件号
  5. 清理硬件资源 (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);
  • 释放 GPIOgpio_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. 为什么顺序很重要?(注意事项)

  1. 先撤节点,再删 cdev

    如果先删除了 cdev 但没撤掉 /dev/xxx 节点,用户进程可能正好打开了这个节点。由于内核已经找不到对应的 fops,会导致不可预知的错误。

  2. 错误处理(Init 失败时的卸载)

    __init 函数中,如果某一步(比如 device_create)失败了,你必须通过 goto 语句跳转到对应的标签,按倒序把之前申请成功的资源释放掉。这叫"错误回滚"。

  3. 引用计数

    如果当前有进程正打开着该设备文件(/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卡)。
  • 文件系统 (逻辑层/管理者)

    • 它是建立在块设备之上的数据结构
    • 它负责把"文件名"映射到"块编号"上。
    • 它记录了:文件叫什么?存在哪些块里?谁有权读写?文件什么时候创建的?
    • 典型代表:ext4FAT32NTFSbtrfs

2. 它们是如何协同工作的?(层级关系)

当你执行 cp photo.jpg /mnt/usb/ 时,发生了以下过程:

  1. 用户层:你下达指令,操作对象是"文件名"。
  2. VFS (虚拟文件系统) :Linux 内核的中转站,它识别出你要往 /mnt/usb 写数据。
  3. 具体文件系统 (如 ext4)
    • 文件系统查表(Inode Table),找出一块空闲的"领地"。
    • 它决定:"把这个图片的数据放在第 500 到 800 号块吧。"
    • 它还会更新目录信息,记录 photo.jpg 在这里。
  4. 块设备驱动
    • 文件系统发令:"把这些数据写到块地址 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 的交换空间也是直接操作块设备的,没有文件系统。

总结

块设备提供"地皮",文件系统在上面"盖房子"。 用户通过"房子"(文件系统)来居住和存放物品,而不需要关心底下的"地皮"(块设备)具体是怎么排列的。

相关推荐
Jurio.1 小时前
当 AI 不再只是对话:Codex app 的自动化功能
运维·人工智能·ai·自动化·codex
shanql2 小时前
系统安装:安装Ubuntu 26.04 LTS
linux·ubuntu
红茶要加冰2 小时前
五、流程控制之循环
linux·运维·shell
fpcc2 小时前
Linux命令——lsof分析说明
linux·服务器
北京华盛恒辉软件开发公司12 小时前
大模型运维深远海漂浮式风电系统已融合人工智能AI软件平台
运维·人工智能
cui_ruicheng2 小时前
Linux网络编程(二):网络数据传输基本流程
linux·服务器·网络
怀旧,2 小时前
【Linux网络编程】15. Reactor 反应堆模式
linux·网络·php
jiayong233 小时前
Memory 写入、检索与纠错机制:让 Agent 记住,也让它忘对
java·服务器·网络·hermes
汪汪大队u3 小时前
从 Docker Compose 到 Kubernetes:物联网管理系统迁移实战(3)—— 两个运维坑
运维·docker·kubernetes