泰山派3M-RK3576-Linux内核驱动教程-Linux驱动基础-字符驱动设备-设备节点创建

Linux 系统中,所有硬件设备都会被当作文件来管理。这些设备对应的特殊文件叫做设备节点,都存放在/dev文件夹里。它们的作用就像桥梁:一边连接着系统内部识别的硬件设备,另一边让应用程序能像操作普通文件一样读写设备数据。这样程序就能通过访问这些特殊文件,间接控制硬件完成操作。

一、SYSFS 文件系统

sysfsLinux 内核提供的一个虚拟文件系统,通常挂载在 /sys 目录。它以目录和属性文件的形式,展示了系统中各类设备、内核模块、内核参数等信息。

像一扇窗口,让你不用深入内核代码即可直观看到所有硬件和驱动的情况。驱动注册时,如果用 class_create 等接口,会自动在 /sys/class/ 下生成相关的 class 目录(比如 /sys/class/myclass/),方便查看和自动生成 /dev/ 设备节点。

输入:

ls /sys/class/

输出内容会类似于:

android_usb/        gpio/         net/            rtc/        tty/
block/              hidraw/       power_supply/   sound/      usbmon/
drm/                input/        spidev/         thermal/    video4linux/
...

二、自动创建设备文件的工具

Linux 新内核中,设备节点的创建主要借助于 udevUser Device),它是一种用户空间的设备管理工具。udev 能自动监控内核发出的硬件变化事件(如插入或移除设备),并智能地在 /dev/ 目录下为所有类型的设备创建设备节点文件。

udev的优点:

  • 自动识别设备,自动创建设备文件;
  • 支持对设备文件命名和权限进行自定义规则设置;
  • 不必手动用 mknod 创建节点,方便又安全。

三、手动创建设备节点

虽然现在大多数情况下 udev 都会帮我们自动创建设备文件节点,但有时候为了测试或者内核配置特殊,我们可能需要自己手动创建节点。

可以使用命令:

sudo mknod /dev/设备名 [c|b] 主设备号 次设备号
  • 设备名:自己定义,比如 mydev
  • c:表示字符设备(b 表示块设备)
  • 主设备号、次设备号:与驱动程序中注册时保持一致

示例: 创建一个 mydev ,设定为主设备号为 240,次设备号为 0:

sudo mknod /dev/mydev c 240 0

还需要设置文件权限,例如:

sudo chmod 666 /dev/mydev

这样,所有用户都可以读写该设备。

危险操作:删除设备节点使用 sudo rm /dev/mydev

四、自动创建设备节点

推荐使用自动创建设备节点的方法。只需在驱动代码里调用内核提供的 class_createdevice_create 函数,就能让内核自动在 /dev/ 下生成设备文件,并配合 udev 正确识别。

例子(与驱动注册部分配合):

struct class *cls;
cls = class_create(THIS_MODULE, "myclass");
device_create(cls, NULL, devno, NULL, "mydev");

这样会自动生成 /dev/mydev

卸载驱动时记得清理:

device_destroy(cls, devno);
class_destroy(cls);

五、创建设备节点实验

1、源码编写

写一个简单的字符设备驱动,它在加载时自动创建设备节点 /dev/mydev

首先我们创建一个 04_mk_device_node/ 目录,并在其中创建一个 mydev_mk.c 源码文件,编写下面的驱动:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

static dev_t devno;
static struct cdev my_cdev;
static struct class *my_class;

// 设备操作函数
static int mydev_open(struct inode *inode, struct file *file) { return 0; }
static int mydev_release(struct inode *inode, struct file *file) { return 0; }

static struct file_operations mydev_fops = {
    .owner = THIS_MODULE,
    .open = mydev_open,
    .release = mydev_release,
};

static int __init mydev_init(void)
{
    int ret;
    // 自动分配设备号
    ret = alloc_chrdev_region(&devno, 0, 1, "mydev");
    if (ret < 0) return ret;

    // 注册cdev
    cdev_init(&my_cdev, &mydev_fops);
    cdev_add(&my_cdev, devno, 1);

    // 创建设备类和设备节点
    my_class = class_create(THIS_MODULE, "myclass");
    device_create(my_class, NULL, devno, NULL, "mydev");

    printk("mydev driver installed!\n");
    return 0;
}

static void __exit mydev_exit(void)
{
    device_destroy(my_class, devno);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(devno, 1);
    printk("mydev driver removed!\n");
}

module_init(mydev_init);
module_exit(mydev_exit);
MODULE_LICENSE("GPL");

2、源码解析

mydev_openmydev_releasefile_operations 必要参数:

  • open 是当你用 open("/dev/mydev") 这样的系统调用访问设备文件时,内核会自动调用这里定义的 mydev_open 函数。通常可以在这里做一些初始化工作,比如参数检查、分配缓冲区等。本例因为没有真正的实体设备,仅仅演示,所以直接 return 0; 即可。
  • release是当你关闭设备文件或程序退出时,内核会自动调用 mydev_release,进行清理和释放工作。这里同样只返回0,表示没有特殊操作。

alloc_chrdev_region(&devno, 0, 1, "mydev");:自动分配设备号

  • devno:用于保存主设备号和次设备号(实际上是一个合成的 dev_t 类型值)。
  • 0:代表次设备号的起始编号为 0。
  • 1:代表一共需要多少连续的设备编号。这里为1,只分配了一个编号。
  • "mydev":给该设备号分配一个名字标签,方便在 /proc/devices 等内核日志和调试信息中查看。

举例说明:

  • 只做一个 /dev/mydev 节点时,写 1 就可以。
  • 想做多个(如 /dev/mydev0/dev/mydev1 等),可以写 0, 4,这样会依次分配四个次设备号,你可以实现一个“多设备实例”的驱动。

cdev_initcdev_add:注册和关联 cdev 对象

  • **cdev_init(&my_cdev, &mydev_fops);** 创建并初始化一个字符设备对象 my_cdev,把它和我们定义的设备操作集合 mydev_fops 联系起来。这样内核就知道打开这个设备文件时,该怎么调用你的驱动代码。
  • **cdev_add(&my_cdev, devno, 1);** 告诉内核:“我要注册 my_cdev 这个字符设备,设备号是 devno(一般就是 alloc 分配到的主+次号),设备数量是 1。” 成功后,内核对这个主/次设备号范围的用户请求(比如 open/read/write)都会转给你这个驱动来处理。

class_createdevice_create :自动生成设备文件节点

  • **class_create(THIS_MODULE, "myclass")**/sys/class/ 目录下创建一个名字为 myclass 的类别。它不仅为用户空间的 udev 管理机制提供识别信息,还会让用户在 /sys/class/myclass/ 看到相关节点,是驱动和系统自动化管理的桥梁。
  • **device_create(my_class, NULL, devno, NULL, "mydev")** 通过前面分配到的设备号 devno,在 /dev/ 下自动创建名为 /dev/mydev 的设备文件节点。 这样,普通用户和应用程序就可以用标准的文件操作访问硬件设备,不用再手动 mknod 创建节点。

卸载流程:

  • device_destroy:删除 /dev/mydev 节点。
  • class_destroy:注销设备类,删除 /sys/class/myclass/ 目录。
  • cdev_del:注销字符设备对象。
  • unregister_chrdev_region:注销之前分配的设备号。

正确释放资源是驱动开发的基本功,否则内核重载驱动、系统长期运行时容易产生“脏”环境甚至崩溃。

3、Makefile编写

我们在 04_mk_device_node/ 文件夹下创建一个 Makefile 文件,并在其中编写以下代码:

export ARCH=arm64

# 04.设备节点创建
export CROSS_COMPILE=/home/lckfb/TaishanPi-3-Linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-

# 和源文件名一致
obj-m += mydev_mk.o

# 内核源码目录
KDIR := /home/lckfb/TaishanPi-3-Linux/kernel-6.1
PWD ?= $(shell pwd)

all:
    make -C $(KDIR) M=$(PWD) modules

clean:
    make -C $(KDIR) M=$(PWD) clean

4、Makefile讲解

看到这里,大家应该就比较熟悉了,和之前编写第一个驱动程序章节的Makefile几乎一摸一样!

这不过这里变为了 mydev_mk.o

  • CROSS_COMPILE:依旧是SDK中的编译器路径前缀。
  • KDIR:依旧是内核源码目录。

5、编译

INFO

特别注意:我们编译内核驱动模块之前,一定要确保 kernel 已经被编译过了,因为编译内核模块的依赖文件有些需要在编译内核后生成。

如何编译内核:参考 Debian12编译内核

进入 04_mk_device_node/ 运行:

make

看到目录中生成了 mydev_mk.ko 就说明编译成功了:

六、驱动测试

mydev_mk.ko 复制到我们的开发板中( u盘 还是 SSH 都可以,前面有讲解如何传输文件),使用下面的命令挂载驱动:

sudo insmod mydev_mk.ko

然后我们使用 dmesg | grep -E 'mydev' 查看日志,寻找所有和 mydev 字样相关的日志,我们看到有相关日志,这个是我们在驱动中自己编写的 mydev driver installed! 语句,所以挂载成功了。

接下来我们使用 ls -l /dev/mydev 命令查看自动生成的设备文件:

这时我们看到 /dev/mydev 出现了,自动给我们分配的设备号为 511, 0

接下来我们使用下面的命令卸载掉,我们已经挂载的 mydev_mk 驱动,看看会发生什么:

sudo rmmod mydev_mk

日志显示我们已经卸载成功了,接下来我们查看下,之前的 /dev/mydev 文件是否还存在:

ls -l /dev/mydev

我们发现,此文件已经不存在了,也就是说驱动卸载的时候设备节点也被一同干掉了,我们在驱动卸载函数里面进行了一系列的操作:

  1. 删除设备节点
  2. 删除设备类
  3. 注销cdev
  4. 释放设备号

所以我们驱动退出之后,资源被回收的干干净净的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值