注册字符设备需要首先向 Linux 内核申请设备号,接着使用 cdev_init()函数和 cdev_add()函数初始化并注册字符设备。字符设备注册成功之后,可以使用 class_create()函数device_create()函数实现自动创建设备节点。在此基础上,本文章继续对字符设备框架进行完善,主要内容为完善文件操作集中的成员。
一、文件操作集简介
struct file_operations 结构体又叫文件操作集,struct file_operations 结构体中的每一个成员都对应着一个操作函数,file_operations 结构体定义在内核源码/include/linux/fs.h 文件中,关键成员代码如下所示。
cpp
struct file_operations {
...
//read 函数指针用来从设备读取数据,读取成功返回读取的字节数。
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
struct module *owner;
//write 函数指针用来发送数据给设备. 写入成功返回写入的字节数。
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//unlocked_ioctl 函数指针主要提供对于设备的控制功能。也可以实现少量的数据传输
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
//open 函数指针用于打开设备。
int (*open) (struct inode *, struct file *);
//执行与 open 函数相反的操作
int (*release) (struct inode *, struct file *);
...
}
下面对关键成员进行介绍。
1.struct module *owner
owner 是一个指向拥有该结构的模块的指针,避免正在操作时被卸载,一般为初始化为 THIS_MODULES (在 <linux/module.h> 中定义的宏)
2.ssize_t (*read) (struct file *, char __user *, size_t, loff_t *)
read 函数指针用来从设备中同步读取数据,读取成功返回读取的字节数。当应用程序中使用read()函数操作设备节点时最终会执行驱动中 file_operations 结构体里指向的 read()函数。
3.ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)
write 函数指针用来发送数据给设备. 写入成功返回写入的字节数。当应用程序中使用write()函数操作设备节点时最终会执行驱动中 file_operations 结构体里指向的 write()函数。
4.long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long)
unlocked_ioctl 函数指针提供对于设备的控制功能,当应用程序中使用 ioctl()函数操作设备节点时最终会执行驱动中 file_operations 结构体里指向的 ioctl()函数。
5.int (*open) (struct inode *, struct file *)
open 函数指针用于打开设备,与应用程序中的 open 函数对应。当应用程序中使用 open()函数操作设备节点时最终会执行驱动中 file_operations 结构体里指向的 open()函数。
6.int (*release) (struct inode *, struct file *)
release 函数指针在 file 结构体释放时被调用。当应用程序中使用 close()函数操作设备节点时最终会执行驱动中 file_operations 结构体里指向的 release()函数。
下面代码填充了 file_operations 结构体中的常用成员:
cpp
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 结构体,并通过应用程序对字符设备
驱动进行文件操作实验。
二、实验程序的编写
1、驱动程序编写
采用自动申请设备号的方式进行设备号的申请,然后对获取的主设备号与次设备号进行打印,之后对字符设备进行注册,并填充 file_openration 结构体中的成员,最后自动创建设备节点。实验代码 chrdev_fops.c 代码如下所示:
cpp
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
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 dev_t dev_num;//定义 dev_t 类型变量 dev_num 来表示设备号
static struct cdev cdev_test;//定义 struct cdev 类型结构体变量 cdev_test,表示要注册的字符设备
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
static struct class *class_test;//定于 struct class *类型结构体变量 class_test,表示要创建的类
static int __init chrdev_fops_init(void)//驱动入口函数
{
int ret;//定义 int 类型的变量 ret,用来对函数返回值进行判断
int major,minor;//定义 int 类型的主设备号 major 和次设备号 minor
ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");//自动获取设备号,设备名 chrdev_name
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 minor is %d \n",major,minor);
cdev_init(&cdev_test,&cdev_fops_test);//使用 cdev_init()函数初始化 cdev_test 结构体,并链接到cdev_test_ops 结构体
cdev_test.owner = THIS_MODULE;//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
ret = cdev_add(&cdev_test,dev_num,1); //使用 cdev_add()函数进行字符设备的添加
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_test
device_create(class_test,NULL,dev_num,NULL,"device_test");//使用 device_create 进行设备的创建,设备名称为 device_test
return 0;
}
static void __exit chrdev_fops_exit(void)//驱动出口函数
{
device_destroy(class_test,dev_num);//删除创建的设备
class_destroy(class_test);//删除创建的类
cdev_del(&cdev_test);//删除添加的字符设备 cdev_test
unregister_chrdev_region(dev_num,1);//释放字符设备所申请的设备号
printk("module exit \n");
}
module_init(chrdev_fops_init);//注册入口函数
module_exit(chrdev_fops_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意 GPL 开源协议
MODULE_AUTHOR("moss");//作者信息
2、编写测试 APP
测试应用程序 app.c 内容代码如下所示:
cpp
#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];//定义读取缓冲区 buf
fd=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;
}
代码中第一个参数为要进行读写操作的设备节点,第二个参数为 read 时,对设备节点进行读操作,第二个参数为 write 时,对设备节点进行写操作。
三、运行测试
1、编译驱动程序
在 hrdev_fops.c 代码同一目录下创建 Makefile 文件,Makefile 文件如下:
cpp
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += chrdev_fops.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make 操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean 操作
回到到存放 chrdev_fops.c 和 Makefile 文件目录下,使用命令 make 编译驱动程序,编译完生成 chrdev_fops.ko 目标文件。
bash
make
2、编译应用程序
使用 aarch64-linux-gnu-gcc -o app app.c -static 命令交叉编译应用程序
bash
aarch64-linux-gnu-gcc -o app app.c -static
3、运行测试
开发板启动之后,使用 insmod chrdev_fops.ko 命令加载驱动程序,加载成功如图所示:

使用 ls /dev/device_test 代码检查设备节点 device_test 是否自动创建成功,创建成功如图所示:

使用./app /dev/device_test 命令运行应用程序对设备进行打开测试,如图所示:

在上图中可以看到"This is chrdev_open"和"open is ok"打印信息,说明应用程序运行成功,且调用了驱动程序中的 open()函数,而"Segmentation fault"错误打印是因为没有对第二个参数进传入,这里忽略即可,随后使用./app /dev/device_test read 命令对设备进行读测试,如图所示:
在上图中可以看到"This is chrdev_read"打印信息,证明驱动程序中的 read()函数被调 用了,然后使用./app /dev/device_test write 命令对设备进行写测试,如图所示:
在上图中可以看到"This is chrdev_write"打印信息,证明驱动程序中的 write()函数被调用了。 最后可以使用 rmmod chrdev_fops.ko 命令卸载驱动程序,如图所示:
通过字符设备驱动框架,应用程序通过设备节点可以与字符设备驱动进行交互,从而实现对硬件的操作。