本文已参与「新人创作礼」活动,一起开启掘金创作之路。
怎么创建一个最基本的Linux驱动模板请看Linux驱动之简单入门——学习笔记(1)。
这篇文章讲一下怎么创建一个字符设备驱动,以及open、close、write、read等函数的使用。
一、知识点引入
1、file_operations 结构体
struct file_operations 这个结构体中的每个成员都对应一种系统调用。 用户进程利用系统调用对设备文件进行读写操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。(总结摘抄自《《嵌入式linux驱动程序设计从入门到精通》》)。
struct file_operations {
/* 模块所有者指针,一般初始化为THIS_MODULES
struct module *owner;
/* 用来修改文件当前的读写位置,返回新位置,loff_t为一个“长偏移量” */
loff_t (*llseek) (struct file *, loff_t, int);
/* 同步读取函数,读取成功返回读取的字节数,设置为NULL,调用时返回-EINVAL */
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
/* 同步写入操作 */
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
/* 异步读取操作,为NULL时全部通过read处理 */
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
/* 异步写入操作 */
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*iterate) (struct file *, struct dir_context *);
/* 判断目前是否可以对设备进行读写操作,字段为空时,设备会被认为既可读也可写 */
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/* 不使用BLK的文件系统,将使用此中函数指针替代ioctl */
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
/* 在64位系统上,32位的ioctl调用将使用此函数指针替代 */
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
/* 用于请求将设备内存映射到进程地址空间,如果无此方法,将返回-ENODEV */
int (*mmap) (struct file *, struct vm_area_struct *);
/* 打开设备的函数。如果为空,设备的打开操作永远成功,但系统不会通知驱动程序 */
int (*open) (struct inode *, struct file *);
/* 进程在关闭设备文件描述符时调用,执行未完成的操作 */
int (*flush) (struct file *, fl_owner_t id);
/* file结构释放时,将调用此指针函数,若release与open相同可设置为NULL */
int (*release) (struct inode *, struct file *);
/* 刷新待处理的数据,如果驱动程序没有实现,fsync调用将返回-EINVAL */
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
/* 异步的fsync函数 */
int (*aio_fsync) (struct kiocb *, int datasync);
/* 通知设备FANYNC标志发生变化,如果设备不支持异步通知,该字段可以为NULL */
int (*fasync) (int, struct file *, int);
/* 实现文件锁,设备驱动常不去实现此lock */
int (*lock) (struct file *, int, struct file_lock *);
/* 实现sendfile调用的另一部分,内核调用将其数据发送到对应文件,每次一个数据页 */
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
/* 在进程地址空间招到一个合适的位置,以便将底层设备中的内存段映射到该位置 */
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
/* 允许模块检查传递给fcntl(F_SETEL...)调用的标志 */
int (*check_flags)(int);
/* 实现文件锁 */
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
二、开始编程
1、实现的内容及流程
(1)要实现的内容
- 接着Linux驱动之简单入门——学习笔记(1)实现的最小模板继续编程。
- 将驱动模块注册为字符设备。
- 在/dev目录下创建该设备是应用层可以对其进行读写。
- 实现简单的读写函数指针并通过应用层程序进行测试。
(2)实现的流程
- 申请一个字符设备。
- 实现file_operations结构体中需要使用的函数,并把函数指针赋值给其相应的成员变量。
- 将字符设备与file_operations结构体进行绑定。
- 申请设备编号并注册到系统。
- 在/dev目录下创建设备。
- 编写应用层程序对该驱动设备进行读写操作。
(3)实现的目标
- 应用层通过open函数打开驱动设备。
- 应用层通过write函数写一个字符串给驱动,驱动接收到字符串并保存。
- 应用层通过read函数把驱动之前保存的字符串读到应用层。
- 应用层通过close函数关闭驱动设备。
2、驱动编程内容
(1)源码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
/*************************************************************************************************/
// 局部宏定义
/*************************************************************************************************/
#define EN_DEBUG 1 /* 调试信息开关 */
#if EN_DEBUG
#define PRINT(x...) printk(KERN_EMERG x) /* 提高打印等级 */
#else
#define PRINT(x...)
#endif
/*************************************************************************************************/
// 局部变量
/*************************************************************************************************/
static dev_t s_dev; /* 动态生成的设备编号 */
static struct cdev *s_cdev; /* 字符设备驱动结构体 */
static struct class *hello_class; /* 该驱动的类 */
static struct device *hello_device; /* 该驱动的设备 */
static char test_buff[128]; /* 测试缓冲区 */
/**************************************************************************************************
** 函数名称: hello_open
** 功能描述: 打开驱动文件时调用
** 输入参数: 无
** 输出参数: 无
** 返回参数: 无
**************************************************************************************************/
int hello_open(struct inode *inode, struct file *fp)
{
PRINT("[KERNEL]:%s ------ \n", __FUNCTION__);
return 0;
}
/**************************************************************************************************
** 函数名称: hello_release
** 功能描述: 释放驱动文件时调用
** 输入参数: 无
** 输出参数: 无
** 返回参数: 无
**************************************************************************************************/
int hello_release(struct inode *inode, struct file *fp)
{
PRINT("[KERNEL]:%s ------ \n", __FUNCTION__);
return 0;
}
/**************************************************************************************************
** 函数名称: hello_read
** 功能描述: 应用层调用read函数时同步调用此函数
** 输入参数: *buf:用户空间的数据,不可直接使用
** size:buf数据的长度
** 输出参数: 无
** 返回参数: 无
**************************************************************************************************/
ssize_t hello_read(struct file *fp, char __user *buf, size_t size, loff_t *loff)
{
strcat(test_buff, " from kernel!");
PRINT("[KERNEL]:%s test_buff = %s\n", __FUNCTION__, test_buff);
if (0 != copy_to_user(buf, test_buff, strlen(test_buff))) {
PRINT("[KERNEL]:ERROR: %s write error!\n\n", __FUNCTION__);
}
return 0;
}
/**************************************************************************************************
** 函数名称: hello_write
** 功能描述: 应用层调用write函数时同步调用此函数
** 输入参数: *buf:用户空间的数据,不可直接使用
** size:buf数据的长度
** 输出参数: 无
** 返回参数: 无
**************************************************************************************************/
ssize_t hello_write(struct file *fp, const char __user *buf, size_t size, loff_t *loff)
{
memset(test_buff, 0, sizeof(test_buff));
if (0 != copy_from_user(test_buff, buf, size)) {
PRINT("[KERNEL]:ERROR: %s write error!\n\n", __FUNCTION__);
}
PRINT("[KERNEL]:%s ------ test_buff = %s, size = %d\n", __FUNCTION__, test_buff, size);
return 0;
}
static struct c hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
}; /* 文件操作结构体 */
/**************************************************************************************************
** 函数名称: drv_init
** 功能: 驱动初始化函数,在加载时被调用
** 参数: 无
** 返回: 无
**************************************************************************************************/
static int __init drv_init(void)
{
PRINT("[KERNEL]:%s ------ \n", __FUNCTION__);
s_cdev = cdev_alloc(); /* 申请一个字符设备 */
cdev_init(s_cdev, &hello_fops); /* 初始化字符设备,与file_operations绑定 */
alloc_chrdev_region(&s_dev, 0, 1, "hello"); /* 动态申请一个设备编号 */
cdev_add(s_cdev, s_dev, 1); /* 添加一个字符设备到系统 */
hello_class = class_create(THIS_MODULE, "hello"); /* 将本模块创建一个类,并注册到内核 */
hello_device = device_create(hello_class, NULL, s_dev, NULL, "hello"); /* 创建设备并注册到内核 */
return 0;
}
/**************************************************************************************************
** 函数名称: drv_exit
** 功能描述: i2c驱动退出函数,在卸载时被调用
** 参数: 无
** 返回: 无
**************************************************************************************************/
static void __exit drv_exit(void)
{
PRINT("[KERNEL]:%s ------ \n", __FUNCTION__);
cdev_del(s_cdev); /* 从内核注销cdev设备对象 */
unregister_chrdev_region(s_dev, 1); /* 注销一个字符设备 */
device_destroy(hello_class, s_dev); /* 销毁设备 */
class_destroy(hello_class); /* 销毁类 */
}
module_init(drv_init); /* 模块初始化 */
module_exit(drv_exit); /* 模块卸载 */
MODULE_AUTHOR("hrx"); /* 模块作者 */
MODULE_DESCRIPTION("Linux Driver"); /* 模块描述 */
MODULE_VERSION("1.0.0"); /* 模块版本 */
MODULE_LICENSE("GPL"); /* 模块遵守的License */
(2)申请主设备号并添加到系统
1.cdev_alloc(系统函数)
从内存中分配一个cdev结构体。 在初始化时调用,实现在kernel\fs\char_dev.c中。
2. cdev_init(系统函数)
初始化 cdev 结构体并且将定义的 file_operations 结构体变量指针与 cdev 结构体绑定到一起。 在初始化时调用,实现在kernel\fs\char_dev.c中。
3.alloc_chrdev_region(系统函数)
动态向系统申请一个设备号(由第一个参数传出),如果要写死主设备号的话可以使用register_chrdev_region函数替代。 在初始化时调用,实现在kernel\fs\char_dev.c中。 参数(传出设备编号,次设备号的起始,次设备号的个数, /proc/devices目录下显示的名称)。 执行完这个函数之后就可以在开发板的 /proc/devices 中看到定义的驱动名称了(加载后)。
4.cdev_add(系统函数)
将cdev设备对象添加到内核。 在初始化时调用,实现在kernel\fs\char_dev.c中。
(3)注销主设备号并从系统删除
1.unregister_chrdev_region(系统函数)
与alloc_chrdev_region或register_chrdev_region函数相对,注销设备号。 在驱动卸载时调用,实现在kernel\fs\char_dev.c中。
2.cdev_del(系统函数)
与cdev_add函数相对,从内核注销cdev设备对象。 在驱动卸载时调用,实现在kernel\fs\char_dev.c中。
(4)将设备添加到/dev目录下
1.class_create(系统函数)
在 /sys/class 目录下创建一个本驱动的类的目录,并返回class对象。 在初始化时调用,实现在 kernel/drivers/base/class.c 中。
2.device_create(系统函数)
根据class_create生成的class指针以及主设备号、驱动名在 /dev 目录下创建驱动设备。 在初始化时调用,实现在 kernel/drivers/base/core.c 中。
(5)将设备从/dev目录下删除
1.device_destroy(系统函数)
与device_create函数相对,根据class_create生成的class指针以及主设备号将设备从 /dev 目录下删除。 在驱动卸载时调用,实现在 kernel/drivers/base/core.c 中。
2.class_destroy(系统函数)
与class_create函数相对,根据class_create生成的class指针将设备从 /sys/class 目录下删除。 在驱动卸载时调用,实现在 kernel/drivers/base/class.c 中。
(6)write和read函数指针的实现
1.hello_read
- 当用户层调用read函数时,此函数会被同步调用。
- __user修饰的*buf表示是用户空间地址,需要使用copy_to_user函数进行读操作。
- copy_to_user表示将数据从内核拷贝至应用层。
2.hello_write
- 当用户层调用write函数时,此函数会被同步调用。
- __user修饰的*buf表示是用户空间地址,需要使用copy_from_user函数进行写操作。
- copy_from_user表示将数据从应用层拷贝至内核。
3、应用编程内容
(1)源码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define DELAY50 usleep(50 * 1000); /* 延迟50ms */
int main(int argc,char **argv)
{
int ret; /* 读写返回值 */
int handle; /* 文件标识符 */
char str[128]; /* 测试用数组 */
handle = open("/dev/hello", O_RDWR); /* 打开驱动设备 */
if (handle == -1) {
printf("open /dev/hello fail!\n");
return 0;
}
memset(str, 0, sizeof(str));
sprintf(str, "%s", "ABCDEFG123456"); /* 填写测试的内容 */
DELAY50
ret = write(handle, str, strlen(str)); /* 把测试内容写给设备 */
DELAY50
printf("[user]:write %d byte,str = %s\n", ret, str);
DELAY50
memset(str, 0, sizeof(str));
ret = read(handle, str, sizeof(str)); /* 读取设备里的内容 */
DELAY50
printf("[user]:read %d byte, str = %s\n", ret, str);
DELAY50
close(handle);
return 0;
}
(2)交叉编译
参考:配置imx6交叉编译环境
三、测试
加载驱动hello.ko后执行hello应用层程序,结果如下。 根据结果可以看出在应用层分别调用了open、write、read、close等函数,驱动相应的函数也会被同步调用。
root@imx6qsabresd:/tmp# ./hello
[10649.796861] [KERNEL]:hello_open ------
[10649.900882] [KERNEL]:hello_write ------ test_buff = ABCDEFG123456, size = 13
[user]:write 13 byte,str = ABCDEFG123456
[10650.108593] [KERNEL]:hello_read test_buff = ABCDEFG123456 from kernel!
[user]:read 26 byte, str = ABCDEFG123456 from kernel!
[10650.315587] [KERNEL]:hello_release ------
写了太久了,下次有空再完善一下吧,后面继续研究下ioctl的使用。