Linux驱动之创建字符驱动——学习笔记(3)

384 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

怎么创建一个最基本的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_regionregister_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的使用。