开发环境: windows + ubuntu18.04 + 迅为rk3568开发板
在linux中尽管没有明确规定要是用文件私有数据,但是在linux驱动源码中,却广泛使用,这极大体现了linux面向对象编程思想。在头文件的
struct file
结构体中预留了一个用于定义私有数据的数据域,结构体具体内容如下:
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
……
/* needed for tty driver, and maybe others */
<font size=6>void *private_data;</font>
……
} ;
使用私有数据的实质就是将结构体中的private_data
指针指向设备结构体。然后通过它可以将私有数据一路从 open
函数带到 read
、write
函数层层传入。一般是在 open 的时候赋值,
read
、write
时使用。open
函数中私有数据的使用如下所示:
struct device_test dev1;
// 打开设备函数
static int cdev_test_open(struct inode*inode,struct file*file)
{
// 将访问的设备设置成私有数据
file->private_data = &dev1;;
printk("cdev_test_open is ok\n");
return 0;
}
然后就可在read、write等函数使用struct device_test *test_dev=(struct device_test *)file->private_data;
来获取私有数据,即设备文件,再继续照常操作即可。
为了操作设备方面,还可以定义一个设备结构体用于存储设备信息及私有数据等,具体形式如下:
#define KBUFFSIZE 32 // 缓冲区大小
// 设备结构体
struct device_test{
dev_t dev_num; //设备号
int major; // 主设备号
int minor; // 次设备号
struct cdev cdev_test; // cdev
struct class *class; // 类
struct device *device; // 设备
char kbuff[KBUFFSIZE]; //缓冲区
};
该结构体的数据域取自于注册字符设备过程所需的变量。
使用文件私有数据的好处:
驱动源码
在下面的驱动中实现了使用一个字符驱动代码注册多个设备,同时各个设备都是通过私有数据进行单独访问。注册字符设备的详细教程可见此文章
同时为了减少代码的重复率,下面驱动源码中使用for
循环来实现驱动的注册与注销,总体思与注册单个字符设备是一致的,但是不同的是:
alloc_chrdev_region(&dev[0].dev_num, 0, CREATE_DEVICE_NUM, “alloc_name”);
需要注册子设备号的数量CREATE_DEVICE_NUM
发生了变化 ;#include
#include //初始化头文件
#include //最基本的文件,支持动态添加和卸载模块。
#include //注册杂项设备头文件
#include //注册设备节点的文件结构体
#include
#include
#define CREATE_DEVICE_NUM 2
#define KBUFFSIZE 32 // 缓冲区大小
// 设备结构体
struct device_test{
dev_t dev_num; //设备号
int major; // 主设备号
int minor; // 次设备号
struct cdev cdev_test; // cdev
struct class *class; // 类
struct device *device; // 设备
char kbuff[KBUFFSIZE]; //缓冲区
};
struct device_test dev[CREATE_DEVICE_NUM]; // 定义设备
char *deviceName[] = {"mydevice1","mydevice2","mydevice3","mydevice4"};
// 打开设备函数
static int cdev_test_open(struct inode*inode,struct file*file)
{
// 设置次设备号
int i;
for(i = 0;i<CREATE_DEVICE_NUM;++i)
dev[i].minor = i;
// 将访问的设备设置成私有数据
file->private_data = container_of(inode->i_cdev,struct device_test,cdev_test);
printk("cdev_test_open is ok\n");
return 0;
}
// 读取设备数据函数
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev = (struct device_test*)file->private_data;
if(copy_to_user(buf,test_dev->kbuff,strlen(test_dev->kbuff)) != 0)
{
printk("copy_from_user error\r\n"); // 应用数据传输到内核错误
return -1;
}
printk("read data from kernel:%s\r\n",test_dev->kbuff);
return 0;
}
//向设备写入数据函数
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev = (struct device_test*)file->private_data;
if(copy_from_user(test_dev->kbuff,buf,size) != 0)
{
printk("copy_from_user error\r\n"); // 应用数据传输到内核错误
return -1;
}
printk("write data to kernel: %s\r\n",test_dev->kbuff);
return 0;
}
// 释放设备(关闭设备)
static int cdev_test_release(struct inode *inode, struct file *file)
{
printk("This is cdev_test_release\r\n");
return 0;
}
/*设备操作函数,定义 file_operations 结构体类型的变量 cdev_test_fops*/
struct file_operations cdev_test_fops = {
.owner = THIS_MODULE, //将 owner 指向本模块,避免在模块的操作正在被使用时卸载该模块
.open = cdev_test_open, //将 open 字段指向 chrdev_open(...)函数
.read = cdev_test_read, //将 open 字段指向 chrdev_read(...)函数
.write = cdev_test_write, //将 open 字段指向 chrdev_write(...)函数
.release = cdev_test_release //将 open 字段指向 chrdev_release(...)函数
};
static int __init chr_fops_init(void) //驱动入口函数
{
/*注册字符设备驱动*/
int ret,i,num;
printk("------------------------------------\r\n");
/*1 创建设备号*/
//动态分配设备号
ret = alloc_chrdev_region(&dev[0].dev_num, 0, CREATE_DEVICE_NUM, "alloc_name");
if (ret < 0)
{
printk("alloc_chrdev_region error\r\n");
return -1;
}
for(i=0;i<CREATE_DEVICE_NUM;++i)
{
// 获取主从设备号
if(i == 0)
num=dev[i].dev_num;
else
num=dev[i-1].dev_num+1;
dev[i].major = MAJOR(num);
dev[i].minor = MINOR(num);
printk("number:%d major:%d minor:%d\r\n",num,dev[i].major,dev[i].minor);
// 初始化cdev
dev[i].cdev_test.owner = THIS_MODULE;
cdev_init(&dev[i].cdev_test,&cdev_test_fops);
// 添加cdev设备到内核
cdev_add(&dev[i].cdev_test,num,1);
// 创建类
dev[i].class = class_create(THIS_MODULE,deviceName[i]);
// 创建设备
dev[i].device = device_create(dev[i].class,NULL,num,NULL,deviceName[i]);
}
return 0;
}
// 注销字符设备
static void __exit chr_fops_exit(void) //驱动出口函数
{
int i,num;
for(i=0;i<CREATE_DEVICE_NUM;++i)
{
if(i == 0)
num=dev[i].dev_num;
else
num=dev[i-1].dev_num+1;
printk("number:%d \r\n",num);
//注销设备号
unregister_chrdev_region(num, 1);
//删除 cdev
cdev_del(&dev[i].cdev_test);
//删除设备
device_destroy(dev[i].class, num);
//删除类
class_destroy(dev[i].class);
}
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("[email protected]");
MODULE_DESCRIPTION("output hello when insmod ;output baibai when rmmod");
驱动中还涉及到三个与设备号相关的函数:
MAJOR
:通过设备号获取主设备号,即获取设备号的高12位;#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
MINOR
:通过设备号获取次设备号,即获取设备号的低20位;#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
MKDEV
:通过主设备号与此设备号获取设备号;#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
将驱动代码编译传输至开发板后,可使用insmod +驱动名.ko
加载驱动,至于加载结果查看方式:
lsmod
,可查看内核中驱动;ls /dev/设备名
,可查看是否有驱动的设备文件。测试源码
测试程序的思想是:
先通过设备名打开各个设备,然后再通过write
函数写入数据到内核(保存在结构体struct device_test
中的kbuff
数据域中,这个是在驱动中就设计好的),再通过read
函数读取出刚才写入的数据。
#include
#include
#include
#include
#include
#include
#define ARGCNUM 1
int main(int argc, char *argv[]) //主函数
{
if(argc != ARGCNUM)
{
printf("the number of your input is error\n");
return -1;
}
char *name[] = {"/dev/mydevice1","/dev/mydevice2"};
int fd[2];
for(int i=0;i<2;++i)
{
fd[i] = open(name[i], O_RDWR); //打开/dev/test 设备
if (fd[i] < 0)
printf("open [%s] error \n",name[i]);
}
write(fd[0],"hello1",sizeof("hello1")); // 设备写入数据
write(fd[1],"123",sizeof("123")); // 设备写入数据
char buf1[32]; //定义缓存区 buf1
read(fd[0],buf1,(sizeof(buf1)));
printf("buf1:%s\n",buf1);
memset(buf1,0,sizeof(buf1));//此处一定要有清空否则就一旦后面读取出数据短于前面就会出现显示错误
read(fd[1],buf1,(sizeof(buf1)));
printf("buf1:%s\n",buf1);
for(int i=0;i<2;++i)
close(fd[i]);
return 0;
}
在使用./测试程序名
,例如我的测试程序名为test
,命令就为./test
,测试结果如下:
此处需要注意: 每次读取完成数据后需要使用函数memset(buf1,0,sizeof(buf1));
清空数据buff
,否则下次读取数据时,一旦数据长度短于上次就会出现一部分上次读取的数据。例如,下图就是未进行清空后的执行结果: