[个人总结] 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(结构体反向查找)。

相关推荐
陈让然2 小时前
VS Code新版本无法连接WSL ubuntu18.04
linux·运维·ubuntu
oMcLin2 小时前
如何在Oracle Linux 8.4上通过配置Oracle RAC集群,确保企业级数据库的高可用性与负载均衡?
linux·数据库·oracle
小杰帅气2 小时前
神秘的环境变量和进程地址空间
linux·运维·服务器
Vect__2 小时前
基于CSAPP对链接和库的理解
linux
胖咕噜的稞达鸭2 小时前
进程间的通信(1)(理解管道特性,匿名命名管道,进程池,systeam V共享内存是什么及优势)重点理解代码!
linux·运维·服务器·数据库
Coder个人博客2 小时前
Linux6.19-ARM64 boot Makefile子模块深入分析
linux·车载系统·系统架构·系统安全·鸿蒙系统
可爱又迷人的反派角色“yang”2 小时前
k8s(五)
linux·运维·docker·云原生·容器·kubernetes
爱吃生蚝的于勒2 小时前
【Linux】进程间通信之匿名管道
linux·运维·服务器·c语言·数据结构·c++·vim
好奇心害死薛猫2 小时前
飞牛OS开机自动挂载SMB
linux