前期知识html
1.如何编写一个简单的Linux驱动(一)——驱动的基本框架
linux
2.如何编写一个简单的Linux驱动(二)——设备操做集file_operations
git
前言程序员
在上一篇文章中,咱们编写设备驱动遇到了很多问题:github
(1) 注册设备时,设备号须要程序员给定,每次编写驱动时,程序员须要知道有哪些设备号是空闲的;框架
(2) 加载驱动后,须要用户使用mknod命令手动生成设备节点;函数
(3) 虽然用户程序调用了读写设备的函数,可是并无数据传输。spa
在本篇文章中,咱们会一次解决这三个问题。code
要下载上一篇文章所写的所有代码,请点击这里。htm
1.自定义一个设备结构体
为了方便,咱们本身定义一个结构体,用于描述咱们的设备,存放和设备有关的属性。打开上一篇文章所写的源代码文件,加入以下代码。
1 struct shanwuyan_dev 2 { 3 struct cdev c_dev; //字符设备 4 dev_t dev_id; //设备号 5 struct class *class; //类 6 struct device *device; //设备 7 int major; //主设备号 8 int minor; //次设备号 9 }; 10 11 struct shanwuyan_dev shanwuyan; //定义一个设备结构体
咱们对成员变量分别进行解析。
成员变量 | 描述 |
struct cdev c_dev | 这是一个字符设备结构体,在后文咱们再介绍 |
dev_t dev_id | 这是一个32位的数据,其中高12位表示主设备号,低20位表示次设备号,高低设备号组合在一块儿表示一个完整的设备号 |
struct class *class | 类,主要做用后文再介绍 |
struct device *device | 设备,主要做用后文再介绍 |
int major | 主设备号 |
int minor | 次设备号 |
接下来咱们要介绍三个宏函数"MAJOR"、"MINOR"、"MKDEV",它们的原型以下。
1 #define MINORBITS 20 2 #define MINORMASK ((1U << MINORBITS) - 1) 3 4 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) 5 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) 6 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
看起来很复杂,可是它们的功能很简单:"MAJOR"的做用是根据设备号获取主设备号,即设备号的高12位;"MINOR"的做用是根据设备号获取次设备号,即设备号的低20位;"MKDEV"的做用是把主设备号和次设备号合并成一个完整的设备号。
2.新的注册与注销字符设备的方法
在上一篇文章中,咱们使用"register_chrdev"函数来注册设备,使用"unregister_chrdev"函数来注销设备。这一组函数的缺点是:首先,主设备号须要用户给定;其次,使用该函数的话,设备会占据整个主设备号,对应的次设备号没法使用,形成设备号的浪费。为了克服以上缺点,咱们引入两组新的注册设备号的函数"register_chrdev_region"和"alloc_chrdev_region",这两个函数对应的注销设备号的函数都是"unregister_chrdev_region"。它们的函数原型以下。
1 //这些函数的声明都在linux/fs.h中 2 extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *); //第一个参数是设备号的地址,第二个参数是次设备号的起始号,第三个参数是要申请的个数,第四个参数是设备名称 3 extern int register_chrdev_region(dev_t, unsigned, const char *); //第一个参数是设备号,第二个参数是要申请的个数,第三个参数是设备名称 4 extern void unregister_chrdev_region(dev_t, unsigned); //第一个参数是设备号,第二个参数是申请的个数
若是用户给定了主设备号,可使用"register_chrdev_region"函数来让系统分配次设备号;若是用户未给定主设备号,可使用"alloc_chrdev_region"函数,由系统分配主设备号和次设备号。这两个函数在驱动的入口函数里调用,做初始化用。相应的,要在驱动出口函数中调用"unregister_chrdev_region"函数来注销设备号。以下方代码。
1 static int __init shanwuyan_init(void) //驱动入口函数 2 { 3 int ret = 0; 4 5 shanwuyan.major = 0; //主设备号设置为0,表示用户不给定主设备号,主次设备号都由系统分配 6 /*1.分配设备号*/ 7 if(shanwuyan.major) //若是给定了主设备号,则由系统分配次设备号 8 { 9 shanwuyan.dev_id = MKDEV(shanwuyan.major, 0); //把用户给的主设备号和0号次设备号合并成一个设备号 10 ret = register_chrdev_region(shanwuyan.dev_id, 1, SHANWUYAN_NAME); //由于咱们只考虑一个设备的状况,因此只分配一个设备号,即设备号0 11 } 12 else //若是没有给定主设备号,则主次设备号所有由系统分配 13 { 14 ret = alloc_chrdev_region(&(shanwuyan.dev_id), 0, 1, SHANWUYAN_NAME); //只考虑一个设备的状况 15 shanwuyan.major = MAJOR(shanwuyan.dev_id); //获取主设备号 16 shanwuyan.minor = MINOR(shanwuyan.dev_id); //获取次设备号 17 } 18 if(ret < 0) //设备号分配失败,则打印错误信息,而后返回 19 { 20 printk(KERN_EMERG "shanwuyan chrdev_region error!\r\n"); 21 return -EINVAL; 22 } 23 else //若是设备号分配成功,则打印设备的主次设备号 24 { 25 printk(KERN_EMERG "shanwuyan.major = %d, shanwuyan.minor = %d\r\n", shanwuyan.major, shanwuyan.minor); 26 } 27 28 29 return 0; 30 } 31 32 static void __exit shanwuyan_exit(void) //驱动出口函数 33 { 34 /*1.注销设备号*/ 35 unregister_chrdev_region(shanwuyan.dev_id, 1); 36 }
以上代码的功能是:入口函数实现由系统分配主次设备号,出口函数实现注销系统分配的设备号。
听起来这两组新的注册设备号的函数好处多多,可是它们却有一个致命的缺点,那就是只能实现分配设备号的功能,却没法像"register_chrdev"函数那样还能够把设备添加到内核中。为了把设备添加到内核,咱们就要引进字符设备结构体"struct cdev",这也是咱们文章开头的自定义结构体的第一个成员变量。该结构体的原型以下。
1 //该结构体原型在linux/cdev.h中,记得在驱动代码中包含进去 2 struct cdev { 3 struct kobject kobj; 4 struct module *owner; 5 const struct file_operations *ops; 6 struct list_head list; 7 dev_t dev; 8 unsigned int count; 9 };
在本文中,咱们只用到该结构体中的三个成员变量"struct module *owner"、"const struct file_operations *ops"、"dev_t dev",他们的描述以下。
成员变量 | 描述 |
struct module *owner |
通常取值为THIS_MODULE |
const struct file_operations *ops |
设备操做集file_operations的地址 |
dev_t dev |
就是设备号 |
接下来要介绍两个与该结构体相关的函数,"cdev_init"和"cdev_add",它们的原型以下。
1 void cdev_init(struct cdev *, const struct file_operations *); //第一个参数是struct cdev结构体变量的地址,第二个参数是字符设备操做集的地址 2 int cdev_add(struct cdev *, dev_t, unsigned); //第一个参数是struct cdev结构体变量的地址,第二个参数是设备号,第三个参数是要添加的数量
这两个函数的做用分别是初始化字符设备结构体和向内核添加字符设备。
向入口函数中添加代码,将字符设备注册到内核中,添加的代码以下。
1 static int __init shanwuyan_init(void) //驱动入口函数 2 { 3 int ret = 0; 4 5 /*1.分配设备号*/ 6 ... 7 8 /*2.向内核添加字符设备*/ 9 shanwuyan.c_dev.owner = THIS_MODULE; 10 cdev_init(&(shanwuyan.c_dev), &(shanwuyan_fops)); //初始化字符设备结构体 11 cdev_add(&(shanwuyan.c_dev), shanwuyan.dev_id, 1); //添加设备到内核 12 13 return 0; 14 }
这样,设备就注册成功了。
3.自动建立设备节点
要实现自动建立设备节点,咱们须要引进两个结构体,"struct class"和"struct device"。即,文章开头的自定义设备结构体中的成员变量"struct class *class"和"struct device *device"是用于实现自动生成设备节点的。这两个结构体的具体实现咱们先不做深刻了解,只须要了解如何在这里使用他们。咱们先引进四个关于这两个结构体的函数,"class_create"、"class_destroy"、"device_create"、"device_destroy",这些函数的做用分别是建立类、摧毁类、建立设备、摧毁设备。它们的原型以下。
1 //位于"linux/device.h"中,记得在驱动代码中包含进去 2 #define class_create(owner, name) \ //第一个参数是全部者(通常为THIS_MODULE),第二个参数是设备名称 3 ({ \ 4 static struct lock_class_key __key; \ 5 __class_create(owner, name, &__key); \ 6 }) 7 8 extern void class_destroy(struct class *cls); //参数是建立的类的地址 9 10 struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); //第一个参数是类的地址,第二个参数为父设备地址(通常为NULL),第三个参数为设备号,第四个参数为可能用到的数据(通常为NULL),第五个参数为设备名称 11 extern void device_destroy(struct class *cls, dev_t devt); //第一个参数为类的地址,第二个参数为设备号
为了实现自动建立设备节点,咱们要在入口函数中建立一个类,而后在类里建立一个设备。在出口函数中,也要相应地摧毁设备和类。代码以下。
1 static int __init shanwuyan_init(void) //驱动入口函数 2 { 3 int ret = 0; 4 5 /*1.分配设备号*/ 6 ... 7 8 /*2.向内核添加字符设备*/ 9 ... 10 11 /*3.自动建立设备节点*/ 12 shanwuyan.class = class_create(THIS_MODULE, SHANWUYAN_NAME); //建立类 13 shanwuyan.device = device_create(shanwuyan.class, NULL, shanwuyan.dev_id, NULL, SHANWUYAN_NAME); //建立设备,设备节点就自动生成了。正常状况下,要考虑类和设备建立失败的状况,为了简化代码,这里就不写了 14 15 return 0; 16 } 17 18 static void __exit shanwuyan_exit(void) //驱动出口函数 19 { 20 /*1.注销设备号*/ 21 ... 22 /*2.摧毁设备*/ 23 device_destroy(shanwuyan.class, shanwuyan.dev_id); 24 /*3.摧毁类*/ 25 class_destroy(shanwuyan.class); 26 }
在入口函数中,咱们先建立了类,后建立了设备,即有类才能有设备,因此在出口函数中,咱们要先把设备摧毁了,而后再摧毁类。
4.实现与用户程序的数据传输
上一篇文章中,file_operations的读写操做并无发挥真正的做用。在本文中,咱们改写一下驱动读写函数和用户程序代码,让设备和用户程序实现数据传输。
首先修改一下驱动程序的"shanwuyan_write"函数和"shanwuyan_read"函数,其中读函数的做用是向用户程序传输一个字符串,写函数的做用是接收用户程序发来的数据,并打印出来,代码以下。
1 /*读设备*/ 2 static ssize_t shanwuyan_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) 3 { 4 char device_data[] = "device data"; 5 copy_to_user(buf, device_data, sizeof(device_data)); //向用户程序传输设备数据 6 return 0; 7 } 8 9 /*写设备*/ 10 static ssize_t shanwuyan_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) 11 { 12 char user_data[50]; 13 copy_from_user(user_data, buf, count); //获取用户程序写到设备的数据 14 printk("device get data:%s\r\n", user_data); 15 return 0; 16 }
这里用到了两个函数,"copy_to_user"和"copy_from_user",做用分别是向用户程序传输数据和从用户程序接收数据。它们的原型以下。
1 //声明在文件linux/uaccess.h中,记得在驱动代码中包含进去 2 static __always_inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n) //第一个参数是目的地址,第二个参数是源地址,第三个参数是数据的size 3 static __always_inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n) //第一个参数是目的地址,第二个参数是源地址,第三个参数是数据的size
接下来改造用户程序,所有代码以下。
1 //源代码文件名为"shanwuyanAPP.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 *./shanwuyanAPP <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 char readbuf[50]; 21 char user_data[] = "user data"; 22 23 if(argc != 3) 24 { 25 printf("Error usage!\r\n"); 26 return -1; 27 } 28 29 filename = argv[1]; //获取文件名称 30 31 fd = open(filename, O_RDWR); 32 if(fd < 0) 33 { 34 printf("cannot open file %s\r\n", filename); 35 return -1; 36 } 37 /*读操做*/ 38 if(!strcmp(argv[2], "r")) 39 { 40 read(fd, readbuf, 50); 41 printf("user get data:%s\r\n", readbuf); 42 } 43 /*写操做*/ 44 else if(!strcmp(argv[2], "w")) 45 { 46 write(fd, user_data, 50); 47 } 48 else 49 { 50 printf("ERROR usage!\r\n"); 51 } 52 53 /*关闭操做*/ 54 ret = close(fd); 55 if(ret < 0) 56 { 57 printf("close file %s failed\r\n", filename); 58 } 59 60 return 0; 61 }
5.应用
编译驱动程序,交叉编译用户程序,拷贝到开发板中。
在终端输入命令"insmod shanwuyan.ko"加载驱动,能够看到系统分配的主次设备号分别为246和0.
在终端输入命令"ls /dev/shanwuyan",能够看到已经自动建立了设备节点"/dev/shanwuyan"。
在终端输入"./shanwuyanAPP /dev/shanwuyan r",让用户程序读设备,能够看到终端打印出了设备传递给用户程序的信息。
在终端输入"./shanwuyanAPP /dev/shanwuyan w",让用户程序写设备,能够看到终端打印出了用户程序传递给设备的信息。
本文的所有代码在这里。