简要了解 字符设备驱动框架 整个流程
文章目录
- 基本知识:
- 实际应用效果说明
- 参考资料
- 字符设备驱动框架
- 基本结构
- 关键数据结构 - 文件操作结构体(file_operations)
- struct module *owner
- ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
- ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
- long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
- int (*open) (struct inode *, struct file *);
- int (*release) (struct inode *, struct file *);
- 驱动实现步骤
- 驱动程序编写
- 源文件 chrdev_fops.c
- 编译程序 Makefile
- 测试程序 app.c
- 源码解读 strcmp
- 源码解读 argv[1]
- 测试
- 加载驱动 insmod chrdev_fops.ko dmesg | tail
- ls /sys/class/class_test/ - ls /dev/device_test
- ./app /dev/device_test
- 读写测试 write-read
- 总结
基本知识:
字符设备驱动是Linux设备驱动中最基本的一种类型,它提供了一种面向字节流的设备访问方式。前面我们了解了驱动中字符设备相关知识:
- 设备号申请:动态申请 alloc_chrdev_region
- 字符设备的注册:初始化字符设备 cdev_init;添加字符设备到内核:cdev_add
- 创建设备节点: 创建类:class_create ; 创建类下面的节点:device_create
具体如下:首先驱动向 Linux 内核进行设备号申请, 之后的字符设备
注册时, 会对申请的设备号进行使用。而 Linux 内核会将字符设备抽象成一个具体的 struct cdev
结构体, 该结构体记录了字符设备的字符设备号、 内核对象等信息, cdev_init(…)函数对结构体
进行初始化之后, cdev_add(…)函数将设备号和 cdev 结构体进行链接, 这时设备号才真正指向
了内核中注册的设备。 设备注册成功之后, 此时还不能对字符设备进行文件操作, 所以需要设
备节节点来充当内核和用户层通信的桥梁
实际应用效果说明
上面基础知识做了了解,最终字符设备注册到驱动里面了。那怎么进行驱动调用? Linux 体系下一切皆文件,这里最重要的核心就是结构体
struct file_operations ,struct file_operations 结构体就是把系统调用和驱
动程序关联起来的关键数据结构。 该结构体的每一个成员都对应着一个系统调用, 读取
file_operation 中相应的函数指针, 接着把控制权转交给函数, 从而完成了 Linux 设备驱动程序
的工作。
我们这里重点讨论的其实就是这个文件操作。 在这个基础上把 字符驱动框架流程在此梳理一遍。
参考资料
基础相关内容
驱动-设备号申请
驱动-注册字符设备
驱动-创建设备节点
字符设备驱动框架
基本结构
字符设备驱动的核心是struct cdev结构体,它代表一个字符设备:
#include <linux/cdev.h>struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;struct list_head list;dev_t dev;unsigned int count;
};
这个知识在字符设备注册的时候理解过,参考资料:驱动-注册字符设备
关键数据结构 - 文件操作结构体(file_operations)
这个知识在字符设备注册的时候理解过,参考资料:驱动-注册字符设备
file_operations 文件操作结构体,对文件的打开、关闭、读、写 指向都是在这个结构体里面定义的
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);int (*open) (struct inode *, struct file *);int (*release) (struct inode *, struct file *);// 其他操作...
};
struct module *owner
owner 是第一个 file_operations 成员, 它并不是一个操作, 而一个指向拥有该结构的模块
的指针, 避免正在操作时被卸载, 一般为初始化为 THIS_MODULES (在 <linux/module.h> 中定
义的宏)
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
read 函数指针用来从设备中同步读取数据, 读取成功返回读取的字节数。 与应用程序中的
read 函数对应
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
write 函数指针用来发送数据给设备. 写入成功返回写入的字节数。 与应用程序中的 write
函数对应。
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
unlocked_ioctl 函数指针提供对于设备的控制功能, 与应用程序中的 ioctl 函数对应。
int (*open) (struct inode *, struct file *);
open 函数指针用于打开设备,与应用程序中的 open 函数对应。
int (*release) (struct inode *, struct file *);
release 函数指针在 file 结构体释放时被调用
file_operations 文件操作集的部分常用函数就介绍完了, 填充了部分常用函数的
file_operations 结构体如下 所示
static struct file_operations cdev_fops_test = {
.owner = THIS_MODULE,//将 owner 字段指向本模块, 可以避免在模块的操作正在被使用时卸载该模块
.open = chrdev_open,//将 open 字段指向 chrdev_open(...)函数
.read = chrdev_read,//将 open 字段指向 chrdev_read(...)函数
.write = chrdev_write,//将 open 字段指向 chrdev_write(...)函数
.release = chrdev_release,//将 open 字段指向 chrdev_release(...)函数
};//定义 file_operations 结构体类型的变量 cdev_test_ops
驱动实现步骤
其实已经总结了步骤,唯一缺的就是如何系统如何调用驱动进而调用到内核的一个理解。
在字符设备号申请-字符设备注册-创建类和类下面的设备后,借助于文件结构体来实现即可,就是对 file_operations 结构体里面的 文件操作使用起来。
驱动程序编写
源文件 chrdev_fops.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>static dev_t dev_num;//定义dev_t类型(32位大小)的变量dev_num
static struct cdev cdev_test; //定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备static int chrdev_open(struct inode *inode, struct file *file)
{printk("This is chrdev_open \n");return 0;
}static ssize_t chrdev_read(struct file *file,char __user *buf, size_t size, loff_t *off)
{printk("This is chrdev_read \n");return 0;
}static ssize_t chrdev_write(struct file *file,const char __user *buf,size_t size,loff_t *off)
{printk("This is chrdev_write \n");return 0;
}
static int chrdev_release(struct inode *inode, struct file *file)
{return 0;
}static struct file_operations cdev_test_ops = {.owner=THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块.open = chrdev_open,//将open字段指向chrdev_open(...)函数.read = chrdev_read,//将open字段指向chrdev_read(...)函数.write = chrdev_write,//将open字段指向chrdev_write(...)函数.release = chrdev_release,//将open字段指向chrdev_release(...)函数
};//定义file_operations结构体类型的变量cdev_test_opsstruct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类static int __init chrdev_fops_init(void)//驱动入口函数
{int ret;//定义int类型的变量ret,用来判断函数返回值int major,minor;//定义int类型的主设备号major和次设备号minorret=alloc_chrdev_region(&dev_num,0,1,"chardev_num"); //通过动态方式进行设备号注册if(ret < 0){printk("alloc_chrdev_region is error\n");} printk("alloc_chrdev_region is ok\n");major=MAJOR(dev_num);//通过MAJOR()函数进行主设备号获取minor=MINOR(dev_num);//通过MINOR()函数进行次设备号获取printk("major is %d\n",major);printk("minor is %d\n",minor);使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体cdev_init(&cdev_test,&cdev_test_ops);cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块 ret= cdev_add(&cdev_test,dev_num,1);if(ret < 0 ){printk("cdev_add is error\n");}printk("cdev_add is ok\n");class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_testdevice_create(class_test,NULL,dev_num,NULL,"device_test");//使用device_create进行设备的创建,设备名称为device_testreturn 0;
}
static void __exit chrdev_fops_exit(void)//驱动出口函数
{cdev_del(&cdev_test);//使用cdev_del()函数进行字符设备的删除unregister_chrdev_region(dev_num,1);//释放字符驱动设备号 device_destroy(class_test,dev_num);//删除创建的设备class_destroy(class_test);//删除创建的类printk("module exit \n");}
module_init(chrdev_fops_init);//注册入口函数
module_exit(chrdev_fops_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("wang fang chen "); //作者信息
源码分析:这个源码测试程序和之前的 创建字符设备节点 源码程序 基本一致,唯一区别 在 文件结构体 file_operations 中添加了文件方法的实现:
.open = chrdev_open,//将open字段指向chrdev_open(...)函数.read = chrdev_read,//将open字段指向chrdev_read(...)函数.write = chrdev_write,//将open字段指向chrdev_write(...)函数.release = chrdev_release,//将open字段指向chrdev_release(...)函数
这也是我们要做的测试,系统程序执行 read、write、open、release 对应的就是驱动程序的具体实现,这里其实就是一个映射关系。
编译程序 Makefile
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
obj-m += chrdev_fops.o
KDIR :=/home/wfc123/Linux/rk356x_linux/kernel
PWD ?= $(shell pwd)
all:make -C $(KDIR) M=$(PWD) modulesclean:make -C $(KDIR) M=$(PWD) clean
测试程序 app.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main(int argc, char *argv[])
{int fd; // 定义 int 类型的文件描述符char buf[32]; // 定义读取缓冲区 buffd = open(argv[1], O_RDWR, 0666); // 调用 open 函数, 打开输入的第一个参数文件, 权限为可读可写if (fd < 0){printf("open is error\n");return -1;}printf("open is ok\n");/*如果第二个参数为 read, 条件成立, 调用 read 函数, 对文件进行读取*/if (!strcmp(argv[2], "read")){read(fd, buf, 32);}/*如果第二个参数为 write, 条件成立, 调用 write 函数, 对文件进行写入*/else if (!strcmp(argv[2], "write")){write(fd, "hello\n", 6);}close(fd); // 调用 close 函数, 对取消文件描述符到文件的映射return 0;
}
源码解读 strcmp
比较两个字符串 str1 和 str2,返回一个整数值表示比较结果:
- 返回值 < 0:str1 小于 str2(按字典顺序)。
- 返回值 = 0:str1 等于 str2。
- 返回值 > 0:str1 大于 str2
源码解读 argv[1]
fd = open(argv[1], O_RDWR, 0666) 这行代码中 argv[1] 传递的第一个参数,那应该就是设备节点。我们先看看实际测试怎样的
[root@topeet:/mnt/sdcard]# ./app /dev/device_test
open is ok
Segmentation fault
[root@topeet:/mnt/sdcard]# ./app /dev/device_test write
open is ok
[root@topeet:/mnt/sdcard]# ./app /dev/device_test read
open is ok
argv[] 参数是包括命令+参数的。 argv[0] 是程序指令,argv[1] 就是代表第一个参数的
相关知识参考:驱动传参实验
测试
加载驱动 insmod chrdev_fops.ko dmesg | tail
我们看看 创建的类、类下面的设备吧,看看: 通过查看
ls /sys/class/class_test/ - ls /dev/device_test
[root@topeet:/mnt/sdcard]# ls /sys/class/class_test/
device_test[root@topeet:/mnt/sdcard]# ls /dev/device_test
/dev/device_test
说明设备已经被创建出来了
./app /dev/device_test
哈哈哈,这个 内核打印 This is chrdev_open 不就是文件结构体 file_operations 中的 chrdev_open 调用日志打印吗?
static int chrdev_open(struct inode *inode, struct file *file)
{printk("This is chrdev_open \n");return 0;
}
读写测试 write-read
这里如open 函数调用一样,内核里面打印了相关 write 、read 的日志打印
总结
- 这里是对字符设备的一个总结,前面知识的进一步理解
- 字符设备最终通过 文件结构体 file_operations 来实现,系统和驱动之间文件操作方法的映射。