在 Linux 系统中,所有硬件设备都会被当作文件来管理。这些设备对应的特殊文件叫做设备节点,都存放在/dev文件夹里。它们的作用就像桥梁:一边连接着系统内部识别的硬件设备,另一边让应用程序能像操作普通文件一样读写设备数据。这样程序就能通过访问这些特殊文件,间接控制硬件完成操作。
一、SYSFS 文件系统
sysfs 是 Linux 内核提供的一个虚拟文件系统,通常挂载在 /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/
...2
3
4
二、自动创建设备文件的工具
Linux 新内核中,设备节点的创建主要借助于 udev(User 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_create 和 device_create 函数,就能让内核自动在 /dev/ 下生成设备文件,并配合 udev 正确识别。
例子(与驱动注册部分配合):
struct class *cls;
cls = class_create(THIS_MODULE, "myclass");
device_create(cls, NULL, devno, NULL, "mydev");2
3
这样会自动生成 /dev/mydev。
卸载驱动时记得清理:
device_destroy(cls, devno);
class_destroy(cls);2
五、创建设备节点实验
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2、源码解析
mydev_open 和 mydev_release 是 file_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_init 和 cdev_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_create 和 device_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
# 交叉编译器绝对路径前缀
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) clean2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4、Makefile讲解
看到这里,大家应该就比较熟悉了,和之前编写第一个驱动程序章节的Makefile几乎一摸一样!
这不过这里变为了 mydev_mk.o
CROSS_COMPILE:依旧是SDK中的编译器路径前缀。KDIR:依旧是内核源码目录。
5、编译
进入 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我们发现,此文件已经不存在了,也就是说驱动卸载的时候设备节点也被一同干掉了,我们在驱动卸载函数里面进行了一系列的操作:
- 删除设备节点
- 删除设备类
- 注销cdev
- 释放设备号
所以我们驱动退出之后,资源被回收的干干净净的。