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

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 36
struct 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.c
static 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 rootfs
OR

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

mount -t sysfs none sysfs /sys
mdev -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

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

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

(0)

相关推荐

  • 详解Linux驱动中,probe函数何时被调用

    最近看到linux的设备驱动模型,关于Kobject.Kset等还不是很清淅.看到了struct device_driver这个结构时,想到一个问题:它的初始化函数到底在哪里调用呢?以前搞PCI驱动时用pci驱动注册函数就可以调用它,搞s3c2410驱动时只要在mach-smdk2410.c中的struct platform_device *smdk2410_devices {}中加入设备也会调用.但从来就没有想过具体的驱动注册并调用probe的过程. 于是打开SourceInsight追踪了一

  • Linux内核模块和驱动的编写

    Linux内核是一个整体是结构,因此向内核添加任何东西,或者删除某些功能,都十分困难.为了解决这个问题引入了内核机制.从而可以动态的想内核中添加或者删除模块. 模块不被编译在内核中,因而控制了内核的大小.然而模块一旦被插入内核,他就和内核其他部分一样.这样一来就会曾家一部分系统开销.同时,如果模块出现问题,也许会带来系统的崩溃. 模块的实现机制: 启动时,由函数 void inti_modules() 来初始化模块,因为启动事很多时候没有模块.这个函数往往把内核自身当作一个虚模块. 如由系统需要

  • Ubuntu中为Android系统上编写Linux内核驱动程序实现方法

    在智能手机时代,每个品牌的手机都有自己的个性特点.正是依靠这种与众不同的个性来吸引用户,营造品牌凝聚力和用户忠城度,典型的代表非iphone莫属了.据统计,截止2011年5月,AppStore的应用软件数量达381062个,位居第一,而Android Market的应用软件数量达294738,紧随AppStore后面,并有望在8月份越过AppStore.随着Android系统逐步扩大市场占有率,终端设备的多样性亟需更多的移动开发人员的参与.据业内统计,Android研发人才缺口至少30万.目前,

  • 如何编写Linux设备驱动程序

    Linux是Unix操作系统的一种变种,在Linux下编写驱动程序的原理和思想完全类似于其他的Unix系统,但它dos或window环境下的驱动程序有很大的区别.在Linux环境下设计驱动程序,思想简洁,操作方便,功能也很强大,但是支持函数少,只能依赖kernel中的函数,有些常用的操作要自己来编写,而且调试也不方便.本人这几周来为实验室自行研制的一块多媒体卡编制了驱动程序,获得了一些经验,愿与Linux fans共享,有不当之处,请予指正. 以下的一些文字主要来源于khg,johnsonm的W

  • Ubuntu中为Android增加硬件抽象层(HAL)模块访问Linux内核驱动程序

    在Ubuntu Android简单介绍硬件抽象层(HAL)一文中,我们简要介绍了在Android系统为为硬件编写驱动程序的方法.简单来说,硬件驱动程序一方面分布在Linux内核中,另一方面分布在用户空间的硬件抽象层中.接着Ubuntu Android系统上编写Linux内核驱动程序实现方法一文中举例子说明了如何在Linux内核编写驱动程序.在这一篇文章中,我们将继续介绍Android系统硬件驱动程序的另一方面实现,即如何在硬件抽象层中增加硬件模块来和内核驱动程序交互.在这篇文章中,我们还将学习到

  • Ubuntu中为Android系统上实现内置C可执行程序测试Linux内核驱动程序

    在前一篇文章中,我们介绍了如何在Ubuntu上为Android系统编写Linux内核驱动程序.在这个名为hello的Linux内核驱动程序中,创建三个不同的文件节点来供用户空间访问,分别是传统的设备文件/dev/hello.proc系统文件/proc/hello和devfs系统属性文件/sys/class/hello/hello/val.进一步,还通过cat命令来直接访问/proc/hello和/sys/class/hello/hello/val文件来,以验证驱动程序的正确性.在这一篇文章里,我

  • Linux下如何安装Run文件格式NVIDIA显卡驱动

    本文给大家介绍的非常详细,具体详情请看下文吧. 开始安装首先修改/etc/inittab文件将: id:5:initdefault: 改为: id:3:initdefault: #vi /etc/inittab 然后重启电脑 系统进入字符模式并用root登录,随后运行NVIDIA-linux-x86-1.0-5336-pkg1.run # sh NVIDIA-linux-x86-1.0-5336-pkg1.run 然后根据提示作出选择,安装大概用时5分钟. 接着修改XF86Config文件把Dr

  • 在Debian系的Linux中编译并安装ixgbe驱动的教程

    Intel的10G网卡(比如,82598. 82599. x540)由ixgbe驱动支持.现代的Linux发行版已经带有了ixgbe驱动,通过可加载模块的方式使用.然而,有些情况你希望在你机器上的自己编译安装ixgbe驱动,比如,你想要体验ixbge驱动的最新特性时.同样,内核默认自带的ixgbe驱动中的一个问题是不允许你自定义驱动的参数.如果你想要一个完全定制的ixgbe驱动(比如 RSS.多队列.中断阈值等等),你需要手动从源码编译ixgbe驱动. 这里是如何在Ubuntu.Debian或者

  • Linux安装PHP MongoDB驱动

    PHP利于学习,使用广泛,主要适用于Web开发领域. MongoDB的主要目标是在键/值存储方式(提供了高性能和高度伸缩性)以及传统的RDBMS系统(丰富的功能)架起一座桥梁,集两者的优势于一身. 在php中使用mongodb你必须使用 mongodb 的 php驱动. 本文是小编在部署生产环境的时候简单记录. 1. 下载PHP的mongodb驱动安装包mongodb-1.1.9.tgz wget https://pecl.php.net/get/mongodb-1.1.9.tgz 2. 解压驱

  • linux 驱动编写之虚拟字符设备的编写实例详解

     linux 驱动编写 前言: 昨天我们说了一些简单模块编写方法,但是终归没有涉及到设备的编写内容,今天我们就可以了解一下相关方面的内容,并且用一个实例来说明在Linux上面设备是如何编写的.虽然我不是专门做linux驱动的,却也经常收到一些朋友们的来信.在信件中,很多做驱动的朋友对自己的工作不是很满意,认为自己的工作就是把代码拷贝来拷贝去,或者说是改来改去,没有什么技术含量.有这种想法的朋友不在少数,我想这主要还是因为他们对自己的工作缺少了解导致.如果有可能,我们可以问问自己这样几个问题: (

随机推荐