字符设备驱动程序
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类型,用于指定需要分配的设备编号的个数以及设备的名称。
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