[个人总结] LDD3:3.字符驱动 - scull(4)

八、使用新设备

当 scull 驱动完成open/release/read/write四大核心方法与cdev注册、内存管理逻辑后,已经具备完整的可使用性,其核心特性是「作为一个基于内存的持久化数据缓存器,数据会驻留在内核内存中,直到被新数据覆盖或驱动卸载」,可通过常规的 Linux 命令和专用工具进行测试与验证。

8.1 scull 驱动的核心可用特性

在进行测试前,先明确 scull 设备的核心行为与特性,这是判断测试结果是否正常的依据:

  1. 数据持久化特性 :用户写入 scull 设备(/dev/scull0等)的数据,会驻留在内核由 scull 管理的内存中,除非被新数据覆盖、以只写模式重新打开(触发scull_trim清空)、驱动卸载,否则数据会一直保留(即使关闭设备文件、退出进程,数据也不会丢失);
  2. 内存限制特性:设备的存储长度仅受限于「系统可用物理 RAM 大小」,无人工设定的固定上限,写入数据越多,内核内存占用越大,直到系统内存耗尽;
  3. 数据传输特性:支持常规的读写操作,读写过程中会以「量子 + 量子集」为单位进行内存分配与数据存储,大块数据的读写会被拆分为多个量子的操作(驱动内部自动完成,对用户透明);
  4. 设备表现特性 :对用户层而言,scull 设备的行为类似一个「无格式的二进制文件」,可通过常规的文件操作命令(cp/dd/cat等)进行读写,无需专用工具。

8.2 scull 驱动的完整使用流程

8.2.1 编译 scull 驱动(内核模块编译)

  1. 准备完整的 scull 驱动代码(包含所有结构体定义、核心方法、模块入口 / 出口);

  2. 编写 Makefile(内核模块编译标准模板),示例如下: makefile

    复制代码
    # 内核源码目录(需替换为你的系统内核源码路径,或使用 /lib/modules/$(shell uname -r)/build)
    KERNELDIR ?= /usr/src/linux-headers-$(shell uname -r)
    # 当前目录
    PWD := $(shell pwd)
    
    # 编译模块
    modules:
        $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
    
    # 清理编译产物
    clean:
        $(MAKE) -C $(KERNELDIR) M=$(PWD) clean
    
    # 要编译的模块文件(替换为你的 scull 驱动源码文件名,无需 .c 后缀)
    obj-m += scull.o
  3. 执行 make 命令编译,编译成功后会生成 scull.ko(内核模块文件),这是可加载的驱动文件。

8.2.2 加载 scull 内核模块

  1. 执行 insmod scull.ko 命令加载模块(需要 root 权限,sudo insmod scull.ko);
    • 补充:若需要指定主设备号、量子大小等参数,可通过 insmod scull.ko scull_major=250 quantum=4096 传入(需驱动支持模块参数定义);
  2. 验证模块是否加载成功:
    • 执行 lsmod | grep scull,若能看到 scull 相关条目,说明模块加载成功;
    • 执行 cat /proc/devices,若能看到 scull 驱动的名称和主设备号(如 250 scull),说明设备号注册成功。

8.2.3 创建设备文件节点

scull 驱动注册后,内核已感知到设备,但用户层无法直接访问,需要创建「设备文件节点」(位于 /dev 目录下),通过 mknod 命令创建:

  1. 命令格式:mknod /dev/scull[0-3] c <主设备号> <次设备号>(需要 root 权限);

    • 说明:c 表示字符设备,主设备号 对应 /proc/devices 中看到的 scull 主设备号,次设备号 对应 scull 设备的索引(0~3);
  2. 示例(假设主设备号为 250): bash

    运行

    复制代码
    sudo mknod /dev/scull0 c 250 0
    sudo mknod /dev/scull1 c 250 1
    sudo mknod /dev/scull2 c 250 2
    sudo mknod /dev/scull3 c 250 3
  3. 验证设备文件:执行 ls -l /dev/scull*,若能看到创建的设备文件,且属性为 crw-r--r--(字符设备),说明创建成功。

8.2.4 测试 scull 设备

原文推荐使用 cpdd、输入 / 输出重定向等常规命令测试,无需编写专用测试程序,简单高效,以下是核心测试用例:

测试用例 1:写入数据到 scull 设备(输出重定向 /dd)

  1. echo 重定向写入简单数据:

    复制代码
    echo "Hello, scull driver!" > /dev/scull0  # 普通用户可能无权限,需 sudo
  2. dd 写入大块数据(测试内存分配能力):

    复制代码
    # 从 /dev/zero 读取 100MB 数据,写入 /dev/scull0(bs=1M 表示块大小 1MB,count=100 表示 100 块)
    sudo dd if=/dev/zero of=/dev/scull0 bs=1M count=100

写入过程中,可观察系统内存变化(后续介绍 free 命令)。

测试用例 2:从 scull 设备读取数据(cat/cp/dd)

  1. cat 读取简单数据:

    复制代码
    cat /dev/scull0  # 读取 scull0 中的数据并输出到终端

若写入的是 Hello, scull driver!,终端会输出对应内容,说明 read/write 方法正常工作;

  1. cp 将 scull 设备数据拷贝到普通文件:

    复制代码
    sudo cp /dev/scull0 scull_data.txt  # 将 scull0 中的数据拷贝到 scull_data.txt
    cat scull_data.txt  # 验证拷贝结果

测试用例 3:验证数据持久化特性

  1. 写入数据后,关闭所有访问 scull 设备的进程,甚至退出当前终端;

  2. 重新执行 cat /dev/scull0,仍能读取到之前写入的数据,说明数据驻留在内核内存中,未随进程退出而丢失;

  3. 以只写模式重新打开设备(触发 scull_trim 清空):

    复制代码
    sudo dd if=/dev/null of=/dev/scull0  # 只写模式打开,清空数据
    cat /dev/scull0  # 无数据输出,说明数据已被清空

8.2.5 卸载 scull 驱动

  1. 先删除设备文件节点(可选,下次加载可复用):

    复制代码
    sudo rm /dev/scull0 /dev/scull1 /dev/scull2 /dev/scull3
  2. 执行 rmmod scull 命令卸载内核模块(需要 root 权限,sudo rmmod scull);

  3. 验证卸载结果:执行 lsmod | grep scull,无相关条目说明卸载成功;

  4. 卸载后,scull 管理的内核内存会被全部释放,写入的数据也会随之丢失。


8.3 核心工具详解:观察驱动运行状态与系统变化

原文提到了两个核心工具:free(观察内存变化)和 strace(跟踪系统调用),此外补充 dmesg(查看内核日志),这三个工具是驱动测试与调试的必备工具。

8.3.1 free ------ 观察系统内存变化(原文重点)

核心作用

scull 设备的数据存储在「内核空间内存」中,写入数据越多,内核内存占用越大,free 命令可实时观察系统内存的使用情况,验证 scull 驱动的内存分配行为。

使用方法

  1. 基本命令:free -h-h 表示人类可读格式,显示单位为 GB/MB,更直观);
  2. 关键输出字段解读:
    • total:系统总物理内存;
    • used:已使用的物理内存(包含内核内存、用户进程内存等);
    • free:空闲物理内存;
    • buff/cache:内核缓冲区和页面缓存(scull 分配的内存属于内核内存,会计入 usedbuff/cache);
  3. 测试步骤:
    • 写入前执行 free -h,记录空闲内存大小;
    • dd 写入大块数据到 scull 设备;
    • 写入后再次执行 free -h,可观察到 free 内存减少,usedbuff/cache 内存增加,说明 scull 成功分配了内核内存;
    • 清空 scull 数据或卸载驱动后,free 内存会恢复,说明 scull 成功释放了内核内存(无内存泄漏)。

8.3.2 strace ------ 跟踪系统调用

核心作用

strace 工具可跟踪用户程序执行过程中发出的所有「系统调用」(如 open/close/read/write/lseek 等),以及系统调用的返回值、参数等信息,用于验证 scull 驱动的「系统调用分发是否正常」「读写数据量是否符合预期」。

使用方法

  1. 基本命令格式:strace <要跟踪的命令>(需要 root 权限,sudo strace);

  2. 核心测试示例(跟踪 cp 命令读写 scull 设备):

    复制代码
    sudo strace cp /dev/scull0 scull_data.txt
  3. 关键输出解读(聚焦 scull 相关的系统调用):

    • open("/dev/scull0", O_RDONLY):跟踪 open 系统调用,查看是否成功打开 scull 设备(返回值为文件描述符,非负数表示成功);
    • read(3, "Hello, scull driver!", 4096):跟踪 read 系统调用,查看读取的文件描述符、缓冲区、请求字节数,以及返回值(成功读取的字节数);
    • write(1, "Hello, scull driver!", 21):跟踪 write 系统调用(将数据写入普通文件);
    • close(3):跟踪 close 系统调用;
  4. 核心验证点:

    • 观察 read/write 系统调用的返回值,是否与预期的数据量一致;
    • 观察大块数据读写时,是否会多次调用 read/write(对应 scull 驱动的「量子化」读写,每次传输一个量子或部分量子的数据);
    • 若驱动存在问题(如返回 -EFAULT),strace 会清晰地显示系统调用的错误返回值,便于定位问题。

8.3.3 dmesg ------ 查看内核日志

核心作用

scull 驱动中使用了 printk 输出日志(如 cdev_add 失败日志、调试信息等),这些日志不会输出到终端,而是存入内核日志缓冲区,dmesg 工具可查看这些内核日志,用于验证驱动的内部运行状态,排查内核态的问题。

使用方法

  1. 基本命令:
    • dmesg:查看所有内核日志;
    • dmesg | grep scull:过滤出 scull 驱动相关的日志(更高效);
    • dmesg -w:实时监控内核日志(类似 tail -f,驱动运行过程中实时输出日志);
  2. 核心验证点:
    • 查看驱动加载时的初始化日志,是否有错误信息(如设备号申请失败、cdev_add 失败);
    • 若在驱动中添加了调试用的 printk(如量子分配、数据拷贝的日志),可通过 dmesg 查看,验证驱动内部逻辑是否正常执行;
    • 驱动卸载时,查看是否有内存泄漏、资源未释放的相关日志(若有,需优化驱动的清理逻辑)。

8.4 额外调试技巧:在驱动中添加 printk 调试

原文提到「可增加一个 printk 在驱动的适当位置,观察大块数据读写中发生了什么」,这是内核驱动调试的最基础、最常用的方法,补充核心调试要点:

  1. printk 日志级别 :使用合适的日志级别(如 KERN_NOTICEKERN_INFOKERN_ERR),避免干扰内核正常日志输出,示例:

    复制代码
    // 调试量子分配
    printk(KERN_INFO "scull: allocate quantum, size: %d\n", dev->quantum);
    // 调试读写数据量
    printk(KERN_INFO "scull: read %lu bytes, offset: %lld\n", count, *offp);
  2. 关键位置添加 :在 read/write 方法的入口、数据拷贝前后、内存分配 / 释放前后添加 printk,跟踪核心流程;

  3. 避免频繁输出:大块数据读写时,若每次量子操作都输出日志,会导致内核日志刷屏,可添加条件判断(如每 100 个量子输出一次);

  4. 调试完成后移除 :正式的驱动代码应移除不必要的调试 printk,只保留关键的错误日志。


8.5 总结

  1. scull 驱动完成四大核心方法后可编译使用,核心特性是「内存持久化缓存,数据直到被覆盖或驱动卸载才丢失」;

  2. 完整使用流程:编译内核模块(Makefile)→ 加载模块(insmod)→ 创建设备文件(mknod)→ 测试设备(cp/dd/cat)→ 卸载模块(rmmod);

  3. mknod 创建设备文件的格式:mknod /dev/xxx c 主设备号 次设备号c 表示字符设备;

  4. free -h 用于观察系统内存变化,验证 scull 的内存分配与释放是否正常,无内存泄漏;

  5. strace 用于跟踪用户程序的系统调用,验证 read/write 的数据传输量与返回值是否符合预期;

  6. dmesg | grep scull 用于查看内核日志,排查驱动内部的初始化、错误等问题;

  7. 驱动调试的基础方法:在关键位置添加 printk,观察核心流程的执行情况;

  8. scull 设备对用户层透明,可通过常规文件操作命令测试,无需专用程序。


九、本章核心知识点 - 快速参考

9.1 设备号相关

#include <linux/types.h>

该头文件定义了设备号的核心类型及相关操作宏,是字符设备驱动的基础。

符号 / 宏 / 函数 核心解读与使用场景
dev_t 1. 内核中用于「唯一代表设备号」的核心数据类型(无符号整型,本质是 32 位或 64 位整数,其中高 12 位表示主设备号,低 20 位表示次设备号);2. 用途:存储设备的主、次设备号,作为设备注册、cdev 绑定的核心标识;3. 注意:不能直接手动拆分 / 拼接主、次设备号,必须使用内核提供的专用宏。
MAJOR(dev_t dev) 1. 提取设备号的「主设备号」的宏;2. 入参:dev_t 类型的完整设备号;3. 返回值:无符号整型的主设备号;4. 用途:从已有的 dev_t 中拆分出主设备号,用于设备识别、日志输出等。
MINOR(dev_t dev) 1. 提取设备号的「次设备号」的宏;2. 入参:dev_t 类型的完整设备号;3. 返回值:无符号整型的次设备号;4. 用途:从已有的 dev_t 中拆分出次设备号,用于区分同一主设备号下的不同设备(如 scull0~scull3)。
MKDEV(unsigned int major, unsigned int minor) 1. 从「主设备号」和「次设备号」拼接生成 dev_t 类型完整设备号的宏;2. 入参:major(主设备号)、minor(次设备号);3. 返回值:dev_t 类型的完整设备号;4. 用途:注册设备、绑定 cdev 前,构建合法的设备号标识。

9.2 文件系统与设备注册

#include <linux/fs.h>

该头文件是字符设备驱动的核心头文件,定义了设备注册 / 注销函数、三大核心数据结构,几乎所有字符设备驱动都必须包含。

(1)设备号分配与释放函数

函数 核心解读与使用场景
int register_chrdev_region(dev_t first, unsigned int count, char *name) 1. 「静态分配设备号」的函数(事先已知需要的主设备号时使用);2. 入参解读: - first:起始设备号(由 MKDEV 构建,指定起始主、次设备号); - count:需要分配的设备号数量(如 4 表示 scull0~scull3); - name:设备名称(会显示在 /proc/devices 中,用于标识设备);3. 返回值:成功返回 0,失败返回负的错误码;4. 适用场景:事先已预留好主设备号,无需动态分配,简单直接;5. 注意:若指定的设备号已被占用,函数会返回失败,需更换主设备号。
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name) 1. 「动态分配设备号」的函数(事先未知主设备号,推荐新驱动使用);2. 入参解读: - dev:输出参数,用于存放内核分配的「起始设备号」(驱动需提前定义 dev_t 变量,传入其地址); - firstminor:起始次设备号(一般设为 0); - count:需要分配的设备号数量; - name:设备名称(显示在 /proc/devices 中);3. 返回值:成功返回 0,失败返回负的错误码;4. 适用场景:新驱动开发,避免静态分配设备号的冲突问题,更具可移植性;5. 注意:分配成功后,可通过 MAJOR(*dev) 获取内核分配的主设备号,用于后续创建设备文件。
void unregister_chrdev_region(dev_t first, unsigned int count) 1. 释放已分配设备号的函数(无论静态 / 动态分配,卸载驱动时都必须调用);2. 入参:first(起始设备号)、count(设备号数量),必须与分配时的参数一致;3. 用途:释放设备号资源,避免设备号泄漏,确保其他驱动可复用该设备号;4. 注意:无返回值,调用前需确保设备号已成功分配。

(2)老旧设备注册 / 注销函数(不推荐新驱动使用)

函数 核心解读与使用场景
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops) 1. 2.6 内核之前的「老旧字符设备注册函数」,2.6 及以后内核提供兼容支持,但不推荐新代码使用 ;2. 入参:major(主设备号,设为 0 表示动态分配)、name(设备名称)、fops(文件操作结构体指针);3. 特点:无需手动操作 cdev 结构,简化了注册流程,但灵活性差,无法精细管理设备号;4. 适用场景:维护老旧驱动,新驱动应使用 cdev 相关函数。
int unregister_chrdev(unsigned int major, const char *name) 1. 恢复 register_chrdev 注册的设备资源,与老旧注册函数配套使用 ;2. 入参:majorname 必须与注册时的参数完全一致;3. 注意:新驱动使用 cdev_del 配合 unregister_chrdev_region 完成资源释放,无需使用该函数。

(3)三大核心数据结构

数据结构 核心解读与使用场景
struct file_operations 1. 「字符设备驱动的方法集合」,存放驱动支持的所有设备操作函数(openreleasereadwrite 等);2. 用途:内核通过该结构体找到驱动的具体实现方法,完成系统调用到驱动方法的分发;3. 注意:结构体中的成员(函数指针)可根据驱动需求选择性实现,未实现的成员设为 NULL,内核会处理默认行为。
struct file 1. 代表「一个打开的文件」,由内核在 open 时创建,release 时销毁,每个打开的文件对应一个独立的 struct file 实例;2. 核心成员:private_data(驱动用于存放自定义设备结构体指针,跨方法传递数据)、f_flags(文件打开标志)、f_pos(文件偏移量,普通读写时由内核维护);3. 注意:该结构体属于内核空间,驱动可读写,但不能修改其只读成员。
struct inode 1. 代表「磁盘上的一个文件 / 设备文件」,描述文件的元数据(设备号、文件类型、权限等),一个文件只有一个 inode 结构体,与打开次数无关;2. 核心成员:i_cdev(指向该设备对应的 cdev 结构体,用于关联驱动)、i_rdev(设备号 dev_t 类型);3. 用途:open 方法中,通过 inode->i_cdev 找到驱动的 cdev 结构,进而获取自定义设备结构体。

9.3 cdev 设备管理

#include <linux/cdev.h>

该头文件定义了 cdev 结构体及相关操作函数,cdev 是 2.6 及以后内核中「字符设备的核心表示」,用于将设备号、文件操作方法、驱动资源绑定在一起。

函数 / 结构体 核心解读与使用场景
struct cdev 1. 内核中「字符设备的抽象」,用于管理字符设备的核心资源,每个字符设备对应一个 cdev 实例;2. 核心成员:ops(指向 struct file_operations,绑定驱动方法)、dev(设备号)、count(设备号数量);3. 用途:作为内核与驱动之间的桥梁,完成设备的注册与方法分发。
struct cdev *cdev_alloc(void) 1. 动态分配 cdev 结构体的函数;2. 返回值:成功返回 cdev 结构体指针,失败返回 NULL;3. 适用场景:驱动无需静态定义 cdev 变量,动态分配更灵活,适合多个设备的场景;4. 注意:分配后需调用 cdev_init 初始化,绑定 file_operations
void cdev_init(struct cdev *dev, struct file_operations *fops) 1. 初始化 cdev 结构体的函数(无论静态定义还是动态分配 cdev,都必须调用);2. 入参:devcdev 结构体指针)、fops(文件操作结构体指针);3. 用途:将 cdevfile_operations 绑定,初始化 cdev 的默认成员,为后续 cdev_add 做准备。
int cdev_add(struct cdev *dev, dev_t num, unsigned int count) 1. 将 cdev 结构体注册到内核的函数(驱动加载时调用,完成设备的最终注册);2. 入参解读: - dev:初始化完成的 cdev 结构体指针; - num:起始设备号(由 MKDEValloc_chrdev_region 获得); - count:设备号数量;3. 返回值:成功返回 0,失败返回负的错误码;4. 注意:注册成功后,内核才能识别该字符设备,用户层才能通过设备文件访问驱动。
void cdev_del(struct cdev *dev) 1. 从内核中注销 cdev 结构体的函数(驱动卸载时调用);2. 入参:cdev 结构体指针;3. 用途:释放 cdev 占用的内核资源,解除设备号与 cdev 的绑定;4. 注意:调用后需配合 unregister_chrdev_region 释放设备号,若 cdev 是动态分配的,还需调用 kfree 释放 cdev 本身。

9.4 结构体成员反向查找

#include <linux/kernel.h>

该头文件定义了 container_of 宏,是驱动中从「结构体成员指针」获取「容器结构体指针」的核心工具,也是 scull 驱动中的关键宏。

核心解读与使用场景
container_of(pointer, type, field) 1. 从「结构体成员指针」反向获取「包含该成员的容器结构体指针」的宏,类型安全,无指针越界风险;2. 入参解读: - pointer:结构体成员的指针(如 inode->i_cdev,即 cdev 结构体指针); - type:容器结构体的类型(如 struct scull_dev,即自定义设备结构体); - field:成员在容器结构体中的字段名(如 cdev,即 struct scull_dev 中的 cdev 成员);3. 返回值:容器结构体的指针(如 struct scull_dev *);4. 核心用途:open 方法中,从 inode->i_cdev 找到 struct scull_dev 指针,进而访问驱动的核心资源(内存、信号量等);5. 注意:使用前需确保 pointer 是合法的成员指针,否则会返回无效指针,导致内核崩溃。

9.5 用户 / 内核空间数据拷贝

#include <asm/uaccess.h>

该头文件定义了用户空间与内核空间之间的数据拷贝函数,是 read/write 方法的核心,确保数据传输的安全性与正确性。

函数 核心解读与使用场景
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count) 1. 从「用户空间」拷贝数据到「内核空间」的函数,对应 write 方法;2. 入参解读: - to:内核空间缓冲区指针(如 scull 的量子内存指针); - from:用户空间缓冲区指针(write 方法的 buff 参数,带 __user 注解); - count:需要拷贝的字节数;3. 返回值:未成功拷贝的字节数 ,返回 0 表示拷贝成功,非 0 表示拷贝失败;4. 核心特性: - 自带用户空间指针有效性检查,无需手动检查; - 可睡眠,仅能在进程上下文(open/read/write 等)中使用;5. 错误处理:返回非 0 时,驱动应返回 -EFAULT 错误码给用户层。
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count) 1. 从「内核空间」拷贝数据到「用户空间」的函数,对应 read 方法;2. 入参解读: - to:用户空间缓冲区指针(read 方法的 buff 参数,带 __user 注解); - from:内核空间缓冲区指针(如 scull 的量子内存指针); - count:需要拷贝的字节数;3. 返回值:未成功拷贝的字节数 ,返回 0 表示拷贝成功,非 0 表示拷贝失败;4. 核心特性:与 copy_from_user 一致,自带指针检查、可睡眠;5. 错误处理:返回非 0 时,驱动应返回 -EFAULT 错误码给用户层。

9.6 总结

本章核心总结如下:

  1. 核心头文件:linux/types.h(设备号)、linux/fs.h(核心驱动)、linux/cdev.h(cdev 管理)、linux/kernel.hcontainer_of)、asm/uaccess.h(数据拷贝);

  2. 核心流程:设备号分配(静态 / 动态)→ cdev 初始化 / 注册 → 实现 file_operations 方法 → 驱动卸载时释放 cdev / 设备号;

  3. 核心安全原则:禁止直接解引用用户空间指针,数据拷贝必须使用 copy_to_user/copy_from_user

  4. 核心工具宏:MKDEV/MAJOR/MINOR(设备号操作)、container_of(结构体反向查找)。

相关推荐
Yana.nice1 小时前
openssl将证书从p7b转换为crt格式
java·linux
AI逐月1 小时前
tmux 常用命令总结:从入门到稳定使用的一篇实战博客
linux·服务器·ssh·php
小白跃升坊2 小时前
基于1Panel的AI运维
linux·运维·人工智能·ai大模型·教学·ai agent
跃渊Yuey2 小时前
【Linux】线程同步与互斥
linux·笔记
舰长1152 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络
zmjjdank1ng2 小时前
Linux 输出重定向
linux·运维
路由侠内网穿透.2 小时前
本地部署智能家居集成解决方案 ESPHome 并实现外部访问( Linux 版本)
linux·运维·服务器·网络协议·智能家居
VekiSon2 小时前
Linux内核驱动——基础概念与开发环境搭建
linux·运维·服务器·c语言·arm开发
zl_dfq3 小时前
Linux 之 【进程信号】(signal、kill、raise、abort、alarm、Core Dump核心转储机制)
linux
Ankie Wan3 小时前
cgroup(Control Group)是 Linux 内核提供的一种机制,用来“控制、限制、隔离、统计”进程对系统资源的使用。
linux·容器·cgroup·lxc