字符设备驱动程序

Linux中,根据设备的类型可以分为三类:字符设备、块设备和网络设备。

字符设备:应用程序按字节/字符来读写数据,通常不支持随机存取。我们常用的键盘、串口都是字符设备。

块设备:应用程序可以随机访问设备数据。典型的块设备有硬盘、SD卡、闪存等,应用程序 可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块的倍数进行。

网络设备是一种特殊设备,它并不存在于/dev下面,主要用于网络数据的收发。

0x01 前置知识

用户使用open函数打开设备文件,做了些什么工作:

 (引自https://embed-linux-tutorial.readthedocs.io/zh_CN/latest/linux_driver/character_device.html

设备文件通常在开机启动时自动创建的,不过,我们仍然可以使用命令mknod来创建一个新的设备文件,命令的基本语法如下:

mknod 设备名 设备类型 主设备号 次设备号

当我们使用上述命令,创建了一个字符设备文件时,实际上就是创建了一个设备节点inode结构体,并且将该设备的设备编号记录在成员i_rdev,将成员f_op指针指向了def_chr_fops结构体。这就是mknod负责 的工作内容。

(之间的过程有点多,详细地可照着图看源码了解)总的来说用户调用open函数时,最终会调用file结构体中的f_op,即def_chr_fops。

在Linux内核中,使用结构体cdev来描述一个字符设备。函数chrdev_open最终将该文件结构体file的成员f_op替换成了cdev对应的ops成员,并执行ops结构体中的open函数。

我们使用对该文件描述符fd调用read、write函数,最终都会调用file结构体对应的函数,实际上也就是调用cdev结构体中ops结构体内的相关函数。

总结一下整个过程,当我们使用open函数,打开设备文件时,会根据该设备的文件的设备号找到相应的设备结构体,从而得到了操作该设备的方法。也就是说如果我们要添加一个新设备的话,我们需要提供一个设备号,一个设备结构体以及操作该设备的方法(file_operations结构体)。接下来,我们将介绍以上的三个内容。

0x02 设备驱动程序的编写

(引自哪我也不记得了)

 1)定义cdev设备

//第一种方式
static struct cdev chrdev;
//第二种方式
struct cdev *cdev_alloc(void);

2)分配/注销设备号

Linux的各种设备都以文件的形式存放在/dev目录下,为了管理这些设备,系统为各个设备进行编号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,如USB,tty等,次设备号用来区分同一类型的多个设备,如tty0,tty1……下图 列出了部分tty设备,他们的主设备号都是4,而不同的次设备号分别对应一个tty设备。

内核提供了一种数据类型:dev_t,用于记录设备编号,该数据类型实际上是一个无符号32位整型,其中的12位用于表示主设备号,剩余的20位则用于表示次设备号。

静态地为一个字符设备申请一个或多个设备编号

int register_chrdev_region(dev_t from, unsigned count, const char *name)

参数说明:

  • from:dev_t类型的变量,用于指定字符设备的起始设备号,如果要注册的设备号已经被其他的设备注册了,那么就会导致注册失败。
  • count:指定要申请的设备号个数,count的值不可以太大,否则会与下一个主设备号重叠。
  • name:用于指定该设备的名称,我们可以在/proc/devices中看到该设备。

动态分配设备编号

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

参数说明如下:

  • dev:指向dev_t类型数据的指针变量,用于存放分配到的设备编号的起始值;
  • baseminor:次设备号的起始值,通常情况下,设置为0;
  • count、name:同register_chrdev_region类型,用于指定需要分配的设备编号的个数以及设备的名称。
可通过cat /proc/devices查看内核分配的主设备号。
使用上两个函数分配设备号对应地注销函数(将设备编号还给内核)为:
void unregister_chrdev_region(dev_t from, unsigned count)

内核还提供了register_chrdev函数用于分配设备号。该函数是一个内联函数,它不仅支持静态申请设备号,也支持动态申请设备号,并将主设备号返回,函数原型如下所示。

static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{
    return __register_chrdev(major, 0, 256, name, fops);
}

参数说明:

  • major:用于指定要申请的字符设备的主设备号,等价于register_chrdev_region函数,当设置为0时,内核会自动分配一个未使用的主设备号。
  • name:用于指定字符设备的名称
  • fops:用于操作该设备的函数接口指针。

同一类字符设备会在内核中申请256个,若用不到,会造成资源浪费

注销函数:

static inline void unregister_chrdev(unsigned int major, const char *name)
{
  __unregister_chrdev(major, 0, 256, name);
}

3)初始化cdev

将cdev结构体与file_operations结构相关联

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
  • cdev:struct cdev类型的指针变量,指向需要关联的字符设备结构体;
  • fops:file_operations类型的结构体指针变量,一般将实现操作该设备的结构体file_operations结构体作为实参。

4)注册设备

cdev_add函数用于向内核的cdev_map散列表(管理当前系统中的所有字符设备)添加一个新的字符设备

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
  • p:struct cdev类型的指针,用于指定需要添加的字符设备;
  • dev:dev_t类型变量,用于指定设备的起始编号;
  • count:指定注册多少个设备。

5)file_operation *fops

自己编写一个字符设备驱动:https://tutorial.linux.doc.embedfire.com/zh_CN/latest/linux_driver/character_device.html

posted @ 2020-05-30 18:33  dx670  阅读(558)  评论(0编辑  收藏  举报