侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

Linux 字符设备驱动框架详细介绍

linux  /  管理员 发布于 7年前   368

Linux 字符设备驱动框架

字符设备是Linux三大设备之一(另外两种是块设备,网络设备),字符设备就是字节流形式通讯的I/O设备,绝大部分设备都是字符设备,常见的字符设备包括鼠标、键盘、显示器、串口等等,当我们执行ls -l /dev的时候,就能看到大量的设备文件,c就是字符设备,b就是块设备,网络设备没有对应的设备文件。编写一个外部模块的字符设备驱动,除了要实现编写一个模块所需要的代码之外,还需要编写作为一个字符设备的代码。

驱动模型

Linux一切皆文件,那么作为一个设备文件,它的操作方法接口封装在struct file_operations,当我们写一个驱动的时候,一定要实现相应的接口,这样才能使这个驱动可用,Linux的内核中大量使用"注册+回调"机制进行驱动程序的编写,所谓注册回调,简单的理解,就是当我们open一个设备文件的时候,其实是通过VFS找到相应的inode,并执行此前创建这个设备文件时注册在inode中的open函数,其他函数也是如此,所以,为了让我们写的驱动能够正常的被应用程序操作,首先要做的就是实现相应的方法,然后再创建相应的设备文件。

#include <linux/cdev.h> //for struct cdev#include <linux/fs.h>  //for struct file#include <asm-generic/uaccess.h>  //for copy_to_user#include <linux/errno.h>      //for error number/* 准备操作方法集 *//* struct file_operations {  struct module *owner;  //THIS_MODULE    //读设备  ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  //写设备  ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);  //映射内核空间到用户空间  int (*mmap) (struct file *, struct vm_area_struct *);  //读写设备参数、读设备状态、控制设备  long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);  //打开设备  int (*open) (struct inode *, struct file *);  //关闭设备  int (*release) (struct inode *, struct file *);  //刷新设备  int (*flush) (struct file *, fl_owner_t id);  //文件定位  loff_t (*llseek) (struct file *, loff_t, int);  //异步通知  int (*fasync) (int, struct file *, int);  //POLL机制  unsigned int (*poll) (struct file *, struct poll_table_struct *);  。。。};*/ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset){  return 0;}struct file fops = {  .owner = THIS_MODULE,  .read = myread,  ...};/* 字符设备对象类型 */struct cdev {  //public    struct module *owner;        //模块所有者(THIS_MODULE),用于模块计数  const struct file_operations *ops; //操作方法集(分工:打开、关闭、读/写、...)  dev_t dev; //设备号(第一个)  unsigned int count;         //设备数量  //private  ...};static int __init chrdev_init(void){  ...  /* 构造cdev设备对象 */  struct cdev *cdev_alloc(void);  /* 初始化cdev设备对象 */  void cdev_init(struct cdev*, const struct file_opeartions*);  /* 为字符设备静态申请设备号 */  int register_chedev_region(dev_t from, unsigned count, const char* name);  /* 为字符设备动态申请主设备号 */  int alloc_chedev_region(dev_t* dev, unsigned baseminor, unsigned count, const char* name);  MKDEV(ma,mi)  //将主设备号和次设备号组合成设备号  MAJOR(dev)   //从dev_t数据中得到主设备号  MINOR(dev)   //从dev_t数据中得到次设备号  /* 注册字符设备对象cdev到内核 */  int cdev_add(struct cdev* , dev_t, unsigned);  ...}static void __exit chrdev_exit(void){  ...  /* 从内核注销cdev设备对象 */  void cdev_del(struct cdev* );  /* 从内核注销cdev设备对象 */  void cdev_put(stuct cdev *);  /* 回收设备号 */  void unregister_chrdev_region(dev_t from, unsigned count);  ...}

实现read,write

Linux下各个进程都有自己独立的进程空间,即使是将内核的数据映射到用户进程,该数据的PID也会自动转变为该用户进程的PID,由于这种机制的存在,我们不能直接将数据从内核空间和用户空间进行拷贝,而需要专门的拷贝数据函数/宏:

long copy_from_user(void *to, const void __user * from, unsigned long n)long copy_to_user(void __user *to, const void *from, unsigned long n)

这两个函数可以将内核空间的数据拷贝到回调该函数的用户进程的用户进程空间,有了这两个函数,内核中的read,write就可以实现内核空间和用户空间的数据拷贝。

ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset){  long ret = 0;  size = size > MAX_KBUF?MAX_KBUF:size;  if(copy_to_user(user_buf, kbuf,size)    return -EAGAIN;  }  return 0;}

实现ioctl

ioctl是Linux专门为用户层控制设备设计的系统调用接口,这个接口具有极大的灵活性,我们的设备打算让用户通过哪些命令实现哪些功能,都可以通过它来实现,ioctl在操作方法集中对应的函数指针是long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);,其中的命令和参数完全由驱动指定,Linux建议如图所示的方式定义ioctl()命令

设备类型    序列号     方向      数据尺寸
8bit             8bit           2bit      13/14bit

这里,设备类型字段为一个幻数,可以是0~0xff之间的数,内核中的"ioctl-number.txt"给出了一个推荐的和已经被使用的幻数(但是已经好久没人维护了),新设备驱动定义幻数的时候要避免与其冲突。命令码的方向字段为2bit,表示数据的传输方向,可能的值是:_IOC_NONE,_IOC_READ,_IOC_WRITE和_IOC_READ|_IOC_WRITE。命令码的数据字段表示涉及的用户数据的大小,这个成员的宽度依赖于体系结构,通常是13或14位。内核还定义了_IO(),_IOR(),_IOW(),_IOWR()这4个宏来辅助生成这种格式的命令。这几个宏的作用是根据传入的type(设备类型字段),nr(序列号字段)和size(数据长度字段)和宏名银行的方向字段移位组合生成命令码。内核中还预定义了一些I/O控制命令,如果某设备驱动中包含了与预定义命令一样的命令码,这些命令会被当做预定义命令被内核处理而不是被设备驱动处理,有如下4种:

  1. FIOCLEX:即file ioctl close on exec 对文件设置专用的标志,通知内核当exec()系统带哦用发生时自动关闭打开的文件
  2. FIONCLEX:即file ioctl not close on exec,清除由FIOCLEX设置的标志
  3. FIOQSIZE:获得一个文件或目录的大小,当用于设备文件时,返回一个ENOTTY错误
  4. FIONBIO:即file ioctl non-blocking I/O 这个调用修改flip->f_flags中的O_NONBLOCK标志

我们可以将驱动设计的命令包含在一个头文件中,记录用户程序和驱动程序的命令约定,下面是一个简单的例子

//mycmd.h...#include <asm/ioctl.h>#define CMDT 'A'#define KARG_SIZE 36struct karg{  int kval;  char kbuf[KARG_SIZE];};#define CMD_OFF _IO(CMDT,0)#define CMD_ON _IO(CMDT,1)#define CMD_R  _IOR(CMDT,2,struct karg)#define CMD_W  _IOW(CMDT,3,struct karg)...
//chrdev.cstatic long demo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){  static struct karg karg = {    .kval = 0,    .kbuf = {0},  };  struct karg *usr_arg;  switch(cmd){  case CMD_ON:    /* 开灯 */    break;  case CMD_OFF:    /* 关灯 */    break;  case CMD_R:    if(_IOC_SIZE(cmd) != sizeof(karg)){      return -EINVAL;    }    usr_arg = (struct karg *)arg;    if(copy_to_user(usr_arg, &karg, sizeof(karg))){      return -EAGAIN;    }    break;  case CMD_W:       if(_IOC_SIZE(cmd) != sizeof(karg)){      return -EINVAL;    }    usr_arg = (struct karg *)arg;    if(copy_from_user(&karg, usr_arg, sizeof(karg))){      return -EAGAIN;    }    break;  default:    ;  };  return 0;}

创建设备文件

插入的设备模块,我们就可以使用cat /proc/devices命令查看当前系统注册的设备,但是我们还没有创建相应的设备文件,用户也就不能通过文件访问这个设备。设备文件的inode应该是包含了这个设备的设备号,操作方法集指针等信息,这样我们就可以通过设备文件找到相应的inode进而访问设备。创建设备文件的方法有两种,手动创建或自动创建,手动创建设备文件就是使用mknod /dev/xxx 设备类型 主设备号 次设备号的命令创建,所以首先需要使用cat /proc/devices查看设备的主设备号并通过源码找到设备的次设备号,需要注意的是,理论上设备文件可以放置在任何文件加夹,但是放到"/dev"才符合Linux的设备管理机制,这里面的devtmpfs是专门设计用来管理设备文件的文件系统。设备文件创建好之后就会和创建时指定的设备绑定,即使设备已经被卸载了,如要删除设备文件,只需要像删除普通文件一样rm即可。理论上模块名(lsmod),设备名(/proc/devices),设备文件名(/dev)并没有什么关系,完全可以不一样,但是原则上还是建议将三者进行统一,便于管理。

除了使用蹩脚的手动创建设备节点的方式,我们还可以在设备源码中使用相应的措施使设备一旦被加载就自动创建设备文件,自动创建设备文件需要我们在编译内核的时候或制作根文件系统的时候就好相应的配置:

Device Drivers --->    Generic Driver Options --->      [*]Maintain a devtmpfs filesystem to mount at /dev      [*] Automount devtmpfs at /dev,after the kernel mounted the rootfsOR

制作根文件系统的启动脚本写入

mount -t sysfs none sysfs /sysmdev -s //udev也行

有了这些准备,只需要导出相应的设备信息到"/sys"就可以按照我们的要求自动创建设备文件。内核给我们提供了相关的API

class_create(owner,name);struct device *device_create_vargs(struct class *cls, struct device *parent,dev_t devt, void *drvdata,const char *fmt, va_list vargs);void class_destroy(struct class *cls);  void device_destroy(struct class *cls, dev_t devt);

有了这几个函数,我们就可以在设备的xxx_init()和xxx_exit()中分别填写以下的代码就可以实现自动的创建删除设备文件

 /* 在/sys中导出设备类信息 */  cls = class_create(THIS_MODULE,DEV_NAME);  /* 在cls指向的类中创建一组(个)设备文件 */  for(i= minor;i<(minor+cnt);i++){    devp = device_create(cls,NULL,MKDEV(major,i),NULL,"%s%d",DEV_NAME,i);  } 
/* 在cls指向的类中删除一组(个)设备文件 */  for(i= minor;i<(minor+cnt);i++){    device_destroy(cls,MKDEV(major,i));  }  /* 在/sys中删除设备类信息 */  class_destroy(cls);       //一定要先卸载device再卸载class

完成了这些工作,一个简单的字符设备驱动就搭建完成了,现在就可以写一个用户程序进行测试了^ - ^

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!


  • 上一条:
    linux tar压缩排除某个文件夹的方法
    下一条:
    Linux系统命令中screen命令详解
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在Linux系统中使用Iptables实现流量转发功能流程步骤(0个评论)
    • vim学习笔记-入门级需要了解的一些快捷键(0个评论)
    • 在centos7系统中实现分区并格式化挂载一块硬盘到/data目录流程步骤(0个评论)
    • 在Linux系统种查看某一个进程所占用的内存命令(0个评论)
    • Linux中grep命令中的10种高级用法浅析(0个评论)
    • 近期文章
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • PHP 8.4 Alpha 1现已发布!(0个评论)
    • Laravel 11.15版本发布 - Eloquent Builder中添加的泛型(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-11
    • 2017-07
    • 2017-10
    • 2017-11
    • 2018-01
    • 2018-02
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2021-02
    • 2021-03
    • 2021-04
    • 2021-06
    • 2021-07
    • 2021-08
    • 2021-09
    • 2021-10
    • 2021-11
    • 2021-12
    • 2022-01
    • 2022-03
    • 2022-04
    • 2022-08
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-06
    • 2023-07
    • 2023-10
    • 2023-12
    • 2024-01
    • 2024-04
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客