简介

在“编写模块_helloworld”中,创建了一个最小的module,但是里面没有实现任何功能,只是一个能安装能卸载的模块而已。而在篇文章中,增加module功能,创建一个可以交互的module,也就是将module做成一个设备节点,通过读写设备节点,与module进行交互,可以使用write向设备写数据,也可以通过read从设备读取数据。这个设备就像一个文件一样

感谢

Photo by Karolina Wv: https://www.pexels.com/photo/a-bison-on-a-grassy-field-6929327/

代码

https://github.com/liodegwin/kernel/tree/main/module/io

原理

基本流程分为两块,第一是创建一个设备节点,这样就可以在安装模块的时候,在/dev目录下出现一个代表设备的文件,从而让我们可以read和write,来与内核模块交互;第二是实现open,read,write等对设备的操作函数,比如,我write这个设备的时候,就把写的字符串保存下来,等到read的时候,再把存的那些字符给用户空间,这就是一个简单的交互了

对于创建设备节点,首先要注册驱动程序register_chrdev,然后是创建设备类class_create,最后是创建一个设备节点device_create

而对于open,read,write等对于设备的操作函数,就是实现file_operations结构体中的函数,基本是利用copy_to_user和copyb_from_user来在用户空间和内核空间传递数据

相关函数

注册字符设备驱动

所谓的注册字符设备驱动,就是让内核知道有这样的一个驱动,内核好管理,将驱动和它能被应用的设备联系起来,就是使用下面的这个函数register_chrdev

1
2
static inline int register_chrdev(unsigned int major,constchar* name,
const struct file_operations* fops)

https://elixir.bootlin.com/linux/v5.15.82/source/include/linux/fs.h#L2743

https://www.cnblogs.com/zhaobinyouth/p/6227644.html

入口参数

major:主设备号,一般写0,内核来分配,免得冲突

name:设备名称

fops:file_operations结构体,封装了对设备的操作

创建设备节点

创建设备类

创建设备节点,必须要先创建设备属于的类,在/sys/class中有所有的设备类,也可以自己增加,我们自己增加设备类,需要使用class_create宏,非常简单,只有两个参数

https://elixir.bootlin.com/linux/v5.15.82/source/include/linux/device/class.h#L273

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* class_create - create a struct class structure
* @owner: pointer to the module that is to "own" this struct class
* @name: pointer to a string for the name of this class.
*
* This is used to create a struct class pointer that can then be used
* in calls to device_create().
*
* Returns &struct class pointer on success, or ERR_PTR() on error.
*
* Note, the pointer created here is to be destroyed when finished by
* making a call to class_destroy().
*/
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})

入口参数

owner:一个module的指针,这个module拥有这个class,一般填THIS_MODULE

name:这个class的名字,自己起一个名字

返回值

成功的话,返回一个struct class的指针,失败的话返回一个错误指针,通过查错误码来看看是哪种错误发生了

典型代码

1
2
3
4
5
6
7
8
//create dev class
#include <linux/device/class.h>
static struct dev_class* dev_class;
dev_class = class_create(THIS_MODULE,"this_class");
if(IS_ERR(dev_class)){
long errno = PTR_ERR(dev_class);
printk("errno is %d , in file %s function %s line %d\n",errno, __FILE__, __FUNCTION__,__LINE__);
}

创建设备节点

使用device_create宏来创建设备节点,详细如下

https://elixir.bootlin.com/linux/v5.15.82/source/include/linux/device.h#L898

1
2
3
4
5
6
/*
* Easy functions for dynamically creating devices on the fly
*/
__printf(5, 6) struct device *
device_create(struct class *cls, struct device *parent, dev_t devt,
void *drvdata, const char *fmt, ...);

入口参数

cls:设备类指针

parent:父设备指针

devt:设备号(主次都有)

drvdata:私有数据

fmt:名字

返回值

struct device 的指针,拿到了一个设备的指针

典型代码

1
2
3
#include <linux/device.h>
#include <linux/kdev_t.h>
device_create(dev_class, NULL, MKDEV(major,0), NULL, "dev");

实验观察

安装模块

首先是执行make,然后有

1
2
3
[liode@liodePC:95:tmp]$ ls
drv_io.c drv_io.ko drv_io.mod drv_io.mod.c drv_io.mod.o drv_io.o drv_io_test.c Makefile modules.order Module.symvers test*
安装模块

观察到,出现了几个变化,首先是模块中多了一个drv_io,然后是sys/class中多了一个drv_io_class,最后是设备节点创建出来了,我们可以使用了,就是/dev/io_1

1
2
3
4
5
6
7
[liode@liodePC:96:tmp]$ sudo insmod drv_io.ko
[liode@liodePC:97:tmp]$ lsmod |grep drv_io
drv_io 16384 0
[liode@liodePC:98:tmp]$ ls /sys/class |grep drv
drv_io_class/
[liode@liodePC:99:tmp]$ ls /dev|grep dev
dev_io_1

设备节点的读写

1
2
3
4
5
[liode@liodePC:102:tmp]$ ./test -w hello
write 6 char to drv
[liode@liodePC:103:tmp]$ ./test -r
read 1000 char from drv
read : hello

观察到我们把hello字符串存到了内核中,然后也能正确的取出来,完全成功了

总结

对于设备节点的创建,主要是要了解三个函数:regiser_chrdrv,class_create,device_create,对于读写,就是自己实现open,read,write等对于文件操作的系统调用