如何编写一个简单的Linux驱动(二)——设备操作集file_operations
前期知识
前言
在上一篇文章中,我们学习了驱动的基本框架。这一章,我们会在上一章代码的基础上,继续对驱动的框架进行完善。要下载上一篇文章的全部代码,请点击这里。
1.字符设备的四个基本操作
驱动让用户程序具备操作硬件设备的能力,那么对硬件设备有哪些操作呢?在学习编程语言时,我们都学过对文件的操作,包括打开文件、关闭文件、读文件、写文件这四个基本操作。对于Linux来说,一切设备皆文件,所以对设备的基本操作也可以分为打开、关闭、读、写这四个。而对于设备(已字符设备为例),Linux提供了一个操作集合——file_operarions。file_operations是一个结构体,其原型如下。
1 struct file_operations { 2 struct module *owner; 3 loff_t (*llseek) (struct file *, loff_t, int); 4 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 5 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 6 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); 7 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); 8 int (*iterate) (struct file *, struct dir_context *); 9 int (*iterate_shared) (struct file *, struct dir_context *); 10 unsigned int (*poll) (struct file *, struct poll_table_struct *); 11 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); 12 long (*compat_ioctl) (struct file *, unsigned int, unsigned long); 13 int (*mmap) (struct file *, struct vm_area_struct *); 14 int (*open) (struct inode *, struct file *); 15 int (*flush) (struct file *, fl_owner_t id); 16 int (*release) (struct inode *, struct file *); 17 int (*fsync) (struct file *, loff_t, loff_t, int datasync); 18 int (*fasync) (int, struct file *, int); 19 int (*lock) (struct file *, int, struct file_lock *); 20 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 21 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 22 int (*check_flags)(int); 23 int (*flock) (struct file *, int, struct file_lock *); 24 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); 25 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); 26 int (*setlease)(struct file *, long, struct file_lock **, void **); 27 long (*fallocate)(struct file *file, int mode, loff_t offset, 28 loff_t len); 29 void (*show_fdinfo)(struct seq_file *m, struct file *f); 30 #ifndef CONFIG_MMU 31 unsigned (*mmap_capabilities)(struct file *); 32 #endif 33 ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, 34 loff_t, size_t, unsigned int); 35 int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, 36 u64); 37 ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, 38 u64); 39 }
要使用该结构体,需要包含头文件"linux/fs.h"。该结构体中的成员变量很多,但在本章中,我们只用到打开(open)、关闭(release)、读(read)、写(write)这四个成员变量,以及一个默认需要的所有者(owner)成员变量。
1 struct file_operations { 2 ... 3 struct module *owner; 4 int (*open) (struct inode *, struct file *); 5 int (*release) (struct inode *, struct file *); 6 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 7 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 8 ... 9 }
file_operations结构体的成员变量向应用程序提供一个对设备操作的接口,但是接口的具体操作需要我们自己来实现。打开上一章所写的驱动源代码"shanwuyan.c",定义一个"file_operations"类型的结构体,再定义四个函数"shanwuyan_open"、"shanwuyan_release"、"shanwuyan_read"、"shanwuyan_write",让file_operations结构体变量的成员变量初始化为这四个函数。
1 /*打开设备*/ 2 static int shanwuyan_open(struct inode *inode, struct file *filp) 3 { 4 return 0; 5 } 6 7 /*释放(关闭)设备*/ 8 static int shanwuyan_release(struct inode *inode, struct file *filp) 9 { 10 return 0; 11 } 12 13 /*读设备*/ 14 static ssize_t shanwuyan_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) 15 { 16 return 0; 17 } 18 19 /*写设备*/ 20 static ssize_t shanwuyan_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) 21 { 22 return 0; 23 } 24 25 static struct file_operations shanwuyan_fops = 26 { 27 .owner = THIS_MODULE, //默认 28 .open = shanwuyan_open, //打开设备 29 .release = shanwuyan_release, //关闭设备 30 .read = shanwuyan_read, //读设备 31 .write = shanwuyan_write, //写设备 32 };
这样,用户在使用库函数"open"打开设备时,就会调用函数"shanwuyan_open";用"close"函数关闭设备时,就会调用函数"shanwuyan_release";用"read"函数读设备时,就会调用函数"shanwuyan_read";用"write"函数写设备时,就会调用函数"shanwuyan_write"。为了让这四个函数的调用更直观地为程序员所观察,我们可以在这四个函数中添加打印语句,这样每次对设备进行操作的时候,程序员都能在终端观察到相应的信息,如下方代码。
1 /*打开设备*/ 2 static int shanwuyan_open(struct inode *inode, struct file *filp) 3 { 4 printk(KERN_EMERG "shanwuyan_open\r\n"); 5 return 0; 6 } 7 8 /*释放(关闭)设备*/ 9 static int shanwuyan_release(struct inode *inode, struct file *filp) 10 { 11 printk(KERN_EMERG "shanwuyan_close\r\n"); 12 return 0; 13 } 14 15 /*读设备*/ 16 static ssize_t shanwuyan_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) 17 { 18 printk(KERN_EMERG "shanwuyan_read\r\n"); 19 return 0; 20 } 21 22 /*写设备*/ 23 static ssize_t shanwuyan_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) 24 { 25 printk(KERN_EMERG "shanwuyan_write\r\n"); 26 return 0; 27 }
2.注册与注销字符设备
字符设备的注册是在入口函数"shanwuyan_init"中完成的,字符设备的注销是在出口函数"shanwuyan_exit"中完成的。在上一篇文章中,这两个函数的作用只是打印一行字符串,并没有注册和注销字符设备的功能。在本章,我们将完善这两个函数。
首先介绍一个函数"register_chrdev",函数原型如下。
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops); //major是主设备号,name是设备名,fops是字符设备操作集的地址
该函数的作用是注册字符设备,设备号为程序员给定的一个主设备号major,设备名为用户给定的一个字符串,字符操作集为上文中定义的结构体地址。如果函数该函数返回值为负数,说明设备注册失败,否则说明设备注册成功。
接下来介绍注销字符设备的函数"unregister_chrdev",该函数的原型如下。
static inline void unregister_chrdev(unsigned int major, const char *name); //major是主设备号,name是设备名
该函数的作用是注销字符设备。
打开开发板的系统终端,输入命令"cat /proc/devices"可以查看有哪些设备号已经被占用。经过查看,本系统的设备号"200"处于空闲状态,可以用来注册字符设备。
完善入口函数和出口函数,代码如下。
1 ... 2 #define SHANWUYAN_MAJOR 200 //程序员给定的主设备号 3 #define SHANWUYAN_NAME "shanwuyan" //程序员给定的设备名字符串 4 ... 5 static struct file_operations shanwuyan_fops = 6 { 7 ... 8 } //定义的字符设备操作集 9 static int __init shanwuyan_init(void) //驱动入口函数 10 { 11 int ret = 0; 12 13 ret = register_chrdev(SHANWUYAN_MAJOR, SHANWUYAN_NAME, &shanwuyan_fops); 14 if(ret < 0) 15 printk(KERN_EMERG "init failed\r\n"); //注册失败 16 else 17 printk(KERN_EMERG "shanwuyan_init\r\n");//注册成功 18 return 0; 19 } 20 static void __exit shanwuyan_exit(void) //驱动出口函数 21 { 22 unregister_chrdev(SHANWUYAN_MAJOR, SHANWUYAN_NAME); //注销字符设备 23 printk(KERN_EMERG "shanwuyan_exit\r\n"); 24 } 25 ...
这样,一个字符设备驱动的雏形就完成了。
3.编写应用程序
编写一个应用程序,包含对设备的打开、关闭、读和写的操作。源代码如下
1 //文件名为"shanwuyan_APP.c" 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <fcntl.h> 5 #include <stdio.h> 6 #include <unistd.h> 7 #include <stdlib.h> 8 #include <string.h> 9 10 /* 11 *argc:应用程序参数个数,包括应用程序本身 12 *argv[]:具体的参数内容,字符串形式 13 *./shanwuyan_APP <filename> <r:w> r表示读,w表示写 14 */ 15 int main(int argc, char *argv[]) 16 { 17 int ret = 0; 18 int fd = 0; 19 char *filename; 20 21 if(argc != 3) //共有三个参数 22 { 23 printf("Error usage!\r\n"); 24 return -1; 25 } 26 27 filename = argv[1]; //获取文件名称 28 29 fd = open(filename, O_RDWR); 30 if(fd < 0) 31 { 32 printf("cannot open file %s\r\n", filename); 33 return -1; 34 } 35 36 if(!strcmp(argv[2], "r")) //读设备 37 { 38 39 read(fd, NULL, 0); //只是使用读函数,但不读出数据 40 } 41 else if(!strcmp(argv[2], "w")) //写设备 42 { 43 write(fd, NULL, 0); //只是使用写函数,但并不向设备写数据 44 45 } 46 else 47 { 48 printf("ERROR usage!\r\n"); 49 } 50 51 /*关闭设备*/ 52 close(fd); 53 54 return 0; 55 }
4.应用
编译驱动文件,交叉编译应用程序,拷贝到开发板中,并加载驱动。
驱动加载完成后,使用命令"mknod /dev/shanwuyan c 200 0",在"/dev"目录下创建"shanwuyan"设备节点。其中参数"c"是指创建一个字符设备节点,200表示主设备号,0表示次设备号。然后使用ls命令查看是否创建成功。
分别输入命令"./shanwuyan_APP /dev/shanwuyan r"和命令"./shanwuyan_APP /dev/shanwuyan w",可以看到终端打印了如下信息。可以看到,应用程序打开设备、关闭设备、读设备、写设备的操作都有所体现。
在本章中,我们只是单纯得调用了read和write函数,但是并没有真正的读写数据。读写数据操作将在下一章中出现。
本章的全部代码在这里。