新字符设备驱动
新字符设备驱动原理
首先是动态分配设备号dev_t
旧的驱动设备存在的问题
- 设备号是静态分配的,不能动态分配
- 设备号是固定的,不能动态改变
- 设备号是全局的,不能重复
- 设备号是静态的,不能动态添加或删除
- 而且十分浪费,比如说现在设led主设备号为200那么次设备号的区间就被浪费了。所以一个led只能有一个主设备号一个次设备号
因此现在都是动态分配设备号
c
int major;//主设备号
int minor;//次设备号
dev_t devid;//设备号
if(major == 0)
{
alloc_chrdev_region(&devid,0,1,"test");//申请设备号
//param:1.指向 dev_t 的指针,2.起始设备号这里从 0 开始,3.设备号数量,4.设备名
major = MAJOR(devid);//获取主设备号
minor = MINOR(devid);//获取次设备号
}else{
devid = MKDEV(major,0);//大部分次设备号都是0号
register_chrdev_region(devid,1,"test");//注册设备号
}
如果想要注销设备号
c
unregister_chrdev_region(devid,1);//注销设备号
//param:1.设备号,2.设备号数量
第二步是字符设备结构struct cdev
那么linux中的字符设备结构struct cdev是这样的
c
struct cdev{
struct kobject kobj;//kobject是linux中的对象模型,这里用来表示字符设备
struct module *owner;//模块指针
const struct file_operations *ops;//文件操作结构体指针
struct list_head list;//链表头
dev_t dev;//设备号
unsigned int count;//设备号数量
}
这个结构体当中我们要着重关注ops和dev这两个成员
ops是文件操作结构体指针,这里我们要实现自己的文件操作函数
dev是设备号,这里我们要注册自己的设备号,就是我们第一步在做的内容
首先定义一个cdev结构体变量
c
struct cdev test_cdev;
第三步是初始化cdev结构体
- 先定义一个struct file_operations
c
struct file_operations test_fops = {
.owner = THIS_MODULE,
.read = test_read,
.write = test_write,
.open = test_open,
.release = test_release,
};
//param:1.模块指针,2.读取函数指针,3.写入函数指针,4.打开函数指针,5.关闭函数指针
- dev_init 函数对test_cdev结构体进行初始化重要的成员ops
c
test_cdev.owner = THIS_MODULE;//模块指针
cdev_init(&test_cdev,&test_fops);//初始化cdev结构体
//param:1.指向 cdev 结构体的指针,2.指向 file_operations 结构体的指针
- dev_add 函数对test_cdev结构体进行初始化重要的成员dev
c
test_cdev.dev = devid;//设备号
cdev_add(&test_cdev,devid,1);//添加cdev结构体到系统
//param:1.指向 cdev 结构体的指针,2.设备号,3.设备号数量
如果要卸载驱动就用dev_del函数
c
cdev_del(&test_cdev);//删除cdev结构体
//param:1.指向 cdev 结构体的指针
以上三步做完后就是配置好了字符设备的结构体,下一步就是自动创建设备节点
自动创建设备节点
mdev机制
udev是一个用户程序,linux通过udev来实现设备文件的创建与删除,udev可以检测系统中的硬件设备状态,当有新的硬件设备插入时,udev会自动创建对应的设备文件,当硬件设备移除时,udev会自动删除对应的设备文件。比说如当用modprobe命令加载模块的时候就会自动在**/dev目录**下创建对应的设备文件,当用modprobe -r命令卸载模块的时候就会自动删除对应的设备文件。
就是用于实现热拔插的一个用户程序
因此class_create/device_create 让 udev 自动在 /dev 下创建设备节点
为什么要创建这样一个设备文件
因为linux是一个多用户多任务的操作系统,每个用户都有自己的权限,而设备文件是用户访问硬件的接口,所以每个用户都需要有自己的设备文件,而不是所有用户都共享一个设备文件。
- 不需要为每个用户单独创建设备文件。设备文件通常是系统级的一个节点,所有用户共享同一个 /dev/xxx 。
- 多用户访问的权限控制依靠设备文件的属主/属组和权限位,或通过 udev 规则统一管理,而不是为每个用户建一份。
class/device 指内核设备模型中的两个对象: struct class 和 struct device 。它们把你的设备以标准化方式呈现在 /sys (sysfs)中,并向用户空间发送热插拔事件(uevent)。
用户空间的 udev 监听这些事件,根据设备信息和规则自动在 /dev 下创建设备节点、设置权限、建立符号链接、做持久命名等。
具体过程
创建类: class_create(THIS_MODULE, "test") → 在 /sys/class/test/ 出现一个类目录。
创建设备: device_create(test_class, NULL, devid, NULL, "test") → 在 /sys/class/test/test/ 出现设备对象,携带 devid (主/次设备号),并触发 uevent 。
第一步创建和删除类
这里的类是什么意思
它是对一类设备的逻辑分组,出现在 /sys/class/<类名>/ 下,便于用户态(如 udev )发现和管理设备。
与字符设备的 cdev 配合使用: cdev 让内核知道你有一个字符设备; class/device 让用户态看到它并创建 /dev/xxx 节点。!!!
自动船舰设备结点的工作是在驱动程序的入口函数中完成的,要在cdev_add函数后添加相关代码
c
struct class * test_class = class_create(THIS_MODULE,"test");//创建类
//param:1.模块指针,2.类名
如果要删除类就用class_destroy函数
c
class_destroy(test_class);//删除类
//param:1.类指针
第二步创建设备节点
要放在驱动入口函数static int __init led_init(void)中
c
struct device *device = device_create(test_class,NULL,devid,NULL,"test");//创建设备节点
//param:1.类指针,2.父设备指针,3.设备号,4.额外数据,5.设备名
如果要删除设备节点就用device_destroy函数
c
device_destroy(test_class,devid);//删除设备节点
//param:1.类指针,2.设备号
第三步设置文件私有数据
每一个设备都有一些属性比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,我们最好将这些变量做成一个结构体。在编写驱动open函数的时候将设备结构体作为私有数据添加到设备文件中
c
/* 设备结构体 */
struct test_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct mutex lock;//可以添加一个互斥锁做并发处理
char data[100];//内核缓冲区与有效数据长度
size_t data_len;
};
struct test_dev testdev
//open函数
static int test_open(struct inode *inode,struct file *filp)
{
filp->private_data = &testdev;//将设备结构体添加到设备文件中
return 0;
}
open 是字符设备被用户态调用 open("/dev/xxx") 时,VFS 进入驱动的入口函数。
它是"每次打开"独有的,不同进程或多次 open 会得到不同的 filp ,这就支持了并发和多实例,可承载不同状态。
在open函数设置好私有数据以后,在write、read、close函数中就可以通过filp->private_data来获取设备结构体
总结一下上面两个大步骤
cdev_add(&cdev, devid, count) 是关键步骤:把 devid 映射到你的 file_operations ,也就是说,把设备号( dev_t ,含主/次设备号)注册到内核的字符设备表,并绑定到你的回调集合 struct file_operations。没有它打开节点也不会进入你的驱动。
device_create(test_class, NULL, devid, NULL, "test") 把设备对象注册到 /sys/class/test/ 并携带设备号,触发节点创建与事件。有 devtmpfs/udev 时,这个设备对象会触发在 /dev 下生成节点(通常名为你传的 "test" 或被 udev 规则重命名),该节点的设备号也会是 249:0 。

这里可以看到在/sys/class/chrdevbase/目录下有一个chrdevbase目录,这个目录就是我们创建的类,在这个目录下有一个chrdevbase设备对象,这个设备对象就是我们创建的设备节点,它的设备号就是249:0(主设备号|次设备号)
然后在/dev目录下也有一个chrdevbase设备节点,这个节点的设备号也就是249:0。所以说这个设备节点就是我们创建的设备文件,用户可以通过这个设备文件来访问我们的设备。这个是udev看到sys里的类和设备对象,然后根据类和设备对象创建出设备节点。
访问路径
- 用户打开 /dev/test → 读取节点的设备号 → 通过主设备号定位 cdev → 进入 fops->open/read/write/... , filp->private_data 拿到设备上下文。
对共享数据做一个并发的保护-添加一个互斥锁
在驱动的入口函数添加一个互斥锁的初始化
c
mutex_init(&testdev.lock);//初始化互斥锁
接下来就是对设备操作函数的编写
先是向设备read函数
c
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
/*
* 读设备:根据用户请求长度与当前有效数据长度计算实际拷贝字节数
* 注意:copy_to_user 返回的是"未拷贝字节数",非 0 表示发生错误(-EFAULT)
* 规范做法是返回实际成功拷贝的字节数,便于用户态依据返回值处理
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
struct chrdevbase_dev *dev = filp->private_data;
size_t to_copy;
int not_copied;
mutex_lock(&dev->lock);//加锁保护共享数据
to_copy = cnt;
if (to_copy > dev->data_len)//如果用户请求长度大于有效数据长度
to_copy = dev->data_len;//实际拷贝字节数为有效数据长度
not_copied = copy_to_user(buf, dev->data, to_copy);//将数据从内核缓冲区拷贝到用户空间
mutex_unlock(&dev->lock);//解锁保护共享数据
if (not_copied)
return -EFAULT;
return to_copy;
}
介绍一下copy_to_user这个函数
copy_to_user函数用于将数据从内核缓冲区拷贝到用户空间,它的原型为
c
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
//param:1.用户空间地址,2.内核空间地址,3.要拷贝的字节数
//return:返回未拷贝的字节数
向设备写write函数
c
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
/*
* 写设备:限定拷贝长度避免溢出,并补 '\0' 便于打印
* 注意:copy_from_user 返回"未拷贝字节数",非 0 表示失败(-EFAULT)
* 返回成功拷贝的字节数,便于用户态确认写入规模
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
struct chrdevbase_dev *dev = filp->private_data;
size_t to_copy;
int not_copied;
mutex_lock(&dev->lock);
to_copy = cnt;
if (to_copy > sizeof(dev->data) - 1)//如果用户请求长度大于内核缓冲区长度
to_copy = sizeof(dev->data) - 1;//实际拷贝字节数为内核缓冲区长度减1
not_copied = copy_from_user(dev->data, buf, to_copy);
if (!not_copied) {
dev->data[to_copy] = '\0';
dev->data_len = to_copy;
}
mutex_unlock(&dev->lock);
if (not_copied)
return -EFAULT;
printk("kernel recevdata:%s\r\n", dev->data);
return to_copy;
}
介绍一下copy_from_user这个函数
copy_from_user函数用于将数据从用户空间拷贝到内核缓冲区,它的原型为
c
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
//param:1.内核空间地址,2.用户空间地址,3.要拷贝的字节数
//return:返回未拷贝的字节数
最后是对驱动程序编写对应的应用测试程序
c
#include "stdio.h"
#include "unistd.h"//包含标准输入输出函数的头文件
#include "sys/types.h"//包含系统数据类型的头文件
#include "sys/stat.h"//包含文件状态结构体的头文件
#include "fcntl.h"//包含文件操作函数的头文件
#include "stdlib.h"
#include "string.h"
static char usrdata[] = {"usr data!"};
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, retvalue;//文件描述符,返回值
char *filename;//设备文件名
char readbuf[100], writebuf[100];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];//获取设备文件名
/* 打开驱动文件 */
fd = open(filename, O_RDWR);//打开设备文件,读写模式
if(fd < 0){
printf("Can't open file %s\r\n", filename);
return -1;
}
if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据:按返回长度打印 */
retvalue = read(fd, readbuf, 50);//从设备文件读取数据,最多读取50字节
if(retvalue < 0){
printf("read file %s failed!\r\n", filename);
}else{
if(retvalue > 0 && retvalue < (int)sizeof(readbuf)){
readbuf[retvalue] = '\0';
}
printf("read(%d) data:%s\r\n", retvalue, readbuf);
}
}
if(atoi(argv[2]) == 2){
/* 向设备驱动写数据:按返回长度确认写入规模 */
memcpy(writebuf, usrdata, sizeof(usrdata));
retvalue = write(fd, writebuf, 50);//向设备文件写入数据,最多写入50字节
if(retvalue < 0){
printf("write file %s failed!\r\n", filename);
}else{
printf("write(%d) done\r\n", retvalue);
}
}
/* 关闭设备 */
retvalue = close(fd);//关闭设备文件
if(retvalue < 0){
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}