前期知识

  1.如何编写一个简单的Linux驱动(一)——驱动的基本框架  

  2.如何编写一个简单的Linux驱动(二)——设备操作集file_operations

前言

  在上一篇文章中,我们编写设备驱动遇到了不少问题:

  (1) 注册设备时,设备号需要程序员给定,每次编写驱动时,程序员需要知道有哪些设备号是空闲的;

  (2) 加载驱动后,需要用户使用mknod命令手动生成设备节点;

  (3) 虽然用户程序调用了读写设备的函数,但是并没有数据传输。

  在本篇文章中,我们会一次解决这三个问题。

  要下载上一篇文章所写的全部代码,请点击这里

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 = ;
  4.  
  5. shanwuyan.major = ; //主设备号设置为0,表示用户不给定主设备号,主次设备号都由系统分配
  6. /*1.分配设备号*/
  7. if(shanwuyan.major) //如果给定了主设备号,则由系统分配次设备号
  8. {
  9. shanwuyan.dev_id = MKDEV(shanwuyan.major, ); //把用户给的主设备号和0号次设备号合并成一个设备号
  10. ret = register_chrdev_region(shanwuyan.dev_id, , SHANWUYAN_NAME); //因为我们只考虑一个设备的情况,所以只分配一个设备号,即设备号0
  11. }
  12. else //如果没有给定主设备号,则主次设备号全部由系统分配
  13. {
  14. ret = alloc_chrdev_region(&(shanwuyan.dev_id), , , SHANWUYAN_NAME); //只考虑一个设备的情况
  15. shanwuyan.major = MAJOR(shanwuyan.dev_id); //获取主设备号
  16. shanwuyan.minor = MINOR(shanwuyan.dev_id); //获取次设备号
  17. }
  18. if(ret < ) //设备号分配失败,则打印错误信息,然后返回
  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. return ;
  29. }
  30.  
  31. static void __exit shanwuyan_exit(void) //驱动出口函数
  32. {
  33. /*1.注销设备号*/
  34. unregister_chrdev_region(shanwuyan.dev_id, );
  35. }

  以上代码的功能是:入口函数实现由系统分配主次设备号,出口函数实现注销系统分配的设备号。

  听起来这两组新的注册设备号的函数好处多多,但是它们却有一个致命的缺点,那就是只能实现分配设备号的功能,却无法像"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",他们的描述如下。

成员变量 描述
  1. struct module *owner
一般取值为THIS_MODULE
  1. const struct file_operations *ops
设备操作集file_operations
  1. 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 = ;
  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, ); //添加设备到内核
  12.  
  13. return ;
  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 = ;
  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 ;
  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 ;
  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[];
  13. copy_from_user(user_data, buf, count); //获取用户程序写到设备的数据
  14. printk("device get data:%s\r\n", user_data);
  15. return ;
  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. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <stdio.h>
  5. #include <unistd.h>
  6. #include <stdlib.h>
  7. #include <string.h>
  8.  
  9. /*
  10. *argc:应用程序参数个数,包括应用程序本身
  11. *argv[]:具体的参数内容,字符串形式
  12. *./shanwuyanAPP <filename> <r:w> r表示读,w表示写
  13. */
  14. int main(int argc, char *argv[])
  15. {
  16. int ret = ;
  17. int fd = ;
  18. char *filename;
  19. char readbuf[];
  20. char user_data[] = "user data";
  21.  
  22. if(argc != )
  23. {
  24. printf("Error usage!\r\n");
  25. return -;
  26. }
  27.  
  28. filename = argv[]; //获取文件名称
  29.  
  30. fd = open(filename, O_RDWR);
  31. if(fd < )
  32. {
  33. printf("cannot open file %s\r\n", filename);
  34. return -;
  35. }
  36. /*读操作,从设备中读取字符串,并打印出来*/
  37. if(!strcmp(argv[], "r"))
  38. {
  39. read(fd, readbuf, );
  40. printf("user get data:%s\r\n", readbuf);
  41. }
  42. /*写操作,向设备写字符串*/
  43. else if(!strcmp(argv[], "w"))
  44. {
  45. write(fd, user_data, );
  46. }
  47. else
  48. {
  49. printf("ERROR usage!\r\n");
  50. }
  51.  
  52. /*关闭操作*/
  53. ret = close(fd);
  54. if(ret < )
  55. {
  56. printf("close file %s failed\r\n", filename);
  57. }
  58.  
  59. return ;
  60. }

5.应用

  编译驱动程序,交叉编译用户程序,拷贝到开发板中。

  在终端输入命令"insmod shanwuyan.ko"加载驱动,可以看到系统分配的主次设备号分别为246和0.

  在终端输入命令"ls /dev/shanwuyan",可以看到已经自动创建了设备节点"/dev/shanwuyan"。

  在终端输入"./shanwuyanAPP /dev/shanwuyan r",让用户程序读设备,可以看到终端打印出了设备传递给用户程序的信息。

  在终端输入"./shanwuyanAPP /dev/shanwuyan w",让用户程序写设备,可以看到终端打印出了用户程序传递给设备的信息。

  本文的全部代码在这里

如何编写一个简单的Linux驱动(二)——完善设备驱动的更多相关文章

  1. 如何编写一个简单的Linux驱动(二)——设备操作集file_operations

    前期知识 如何编写一个简单的Linux驱动(一)--驱动的基本框架 前言 在上一篇文章中,我们学习了驱动的基本框架.这一章,我们会在上一章代码的基础上,继续对驱动的框架进行完善.要下载上一篇文章的全部 ...

  2. 如何编写一个简单的Linux驱动(一)

    前言 最近在学习Linux驱动,记录下自己学习的历程. 驱动的基本框架 Linux驱动的基本框架包含两部分,“模块入口.出口的注册”和“模块入口.出口函数的实现”,如下方代码. static int ...

  3. 使用CEF(二)— 基于VS2019编写一个简单CEF样例

    使用CEF(二)- 基于VS2019编写一个简单CEF样例 在这一节中,本人将会在Windows下使用VS2019创建一个空白的C++Windows Desktop Application项目,逐步进 ...

  4. Linux内核分析第三周学习总结:构造一个简单的Linux系统MenuOS

    韩玉琪 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.Linux内 ...

  5. 20135202闫佳歆--week3 构造一个简单的Linux系统MenuOs--学习笔记

    此为个人学习笔记存档 week 3 构造一个简单的Linux系统MenuOs 复习: 计算机有三个法宝:存储程序计算机,函数调用堆栈,中断 操作系统有两把剑: 1.中断上下文的切换,保存现场和恢复现场 ...

  6. Linux内核分析-构造一个简单的Linux系统MenuOS

    构造一个简单的Linux系统MenuOS linux内核目录结构 arch目录包括了所有和体系结构相关的核心代码.它下面的每一个子目录都代表一种Linux支持的体系结构,例如i386就是Intel C ...

  7. 20135220谈愈敏Blog3_构造一个简单的Linux系统MenuOS

    构造一个简单的Linux系统MenuOS 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-1 ...

  8. Linux内核分析第三周——构造一个简单的Linux系统MenuOS

    构造一个简单的Linux系统MenuOS 李雪琦 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/UST ...

  9. Linux内核设计第三周——构造一个简单的Linux系统

    Linux内核设计第三周 ——构造一个简单的Linux系统 一.知识点总结 计算机三个法宝: 存储程序计算机 函数调用堆栈 中断 操作系统两把宝剑: 中断上下文的切换 进程上下文的切换 linux内核 ...

随机推荐

  1. 漏洞重温之XSS(上)

    漏洞简介 跨站脚本攻击(XSS)是指恶意攻击者往Web页面里插入恶意Script代码,当用户浏览页面之时,嵌入web网页中的script代码会被执行,从而达到恶意攻击用户的目的. XSS漏洞通常是通过 ...

  2. DB2根据报错代码查看表与字段信息

    select * from syscat.tables where tbspaceid='?' and tableid='?' select * from syscat.columns where t ...

  3. IOS 如何持久化自定义对象 2014-08-01 01:38

    如果持久话自定义对象 那么这个对象一定要遵循 NSCoding 协议 并实现编解码:然后再将编解码后的数据 NSKeyedArchiver 到NSData中   @interface NSKeyAnd ...

  4. Nginx和FastDfs完整配置过程

    借鉴(https://blog.csdn.net/qq_34301871/article/details/80060235) 1.unknown directive "ngx_fastdfs ...

  5. hook框架-frida简单使用模板以及frida相关接口

    一目录结构 ├── test.py #py脚本 └── test.js #js脚本 一.py脚本 test.py import frida import sys #连接设备app dev=frida. ...

  6. 基于python tkinter的点名小程序

    import datetimeimport jsonimport osimport randomimport tkinter as tkimport openpyxl # 花名册文件名excel_fi ...

  7. openstack-neutron-OVS agent分析

    参考链接: https://blog.csdn.net/sld880311/article/details/77978369 https://github.com/jffree/neutron-cod ...

  8. 小白学PyTorch 动态图与静态图的浅显理解

    文章来自公众号[机器学习炼丹术],回复"炼丹"即可获得海量学习资料哦! 目录 1 动态图的初步推导 2 动态图的叶子节点 3. grad_fn 4 静态图 本章节缕一缕PyTorc ...

  9. MySQL必知必会》正则表达式

    <MySQL必知必会>正则表达式 正则表达式 1.1.关键字 REGEXP 正则表达式的使用需要用到关键字 REGEXP . select prod_name from products ...

  10. 实用的git log用法

    git log可以很方便地查看日志,可以根据自己需要,将日志按照特定格式显示,或者输出某种格式. 最原始的输出样式: $ git log commit ca82a6dff817ec66f4434200 ...