下一篇:《 字符设备号的静态 / 动态分配 》 |
目录
三、file_operations 结构体——Linux 内核驱动操作函数集合
一、驱动入口/出口函数的指定与构建
1. 驱动入口 / 出口函数的指定
本类所需函数定义在 <linux/module.h> 下
首先需要声明驱动入口 (init) / 出口 (exit) 函数是什么,分别由以下两个函数完成:
#include <linux/module.h>
/* 指定驱动入口及出口函数 */
module_init(xxx_init); //将 xxx_init 函数指定为驱动入口函数
module_exit(xxx_exit); //将 xxx_exit 函数指定为驱动出口函数
这两个 module 函数在 <linux/init.h> 下定义好,所以调用一下头文件就可以直接使用。这两个函数一般视为驱动程序执行的开始,一般置于代码末尾(除去代码末尾声明的 LICENSE 信息和作者外),系统性的看驱动代码也是翻到最下面从这里开始看起的。
2. 驱动入口 / 出口函数的构建
本类所需函数定义在 <linux/init.h> 下
上一步已经指定了入口/出口函数,接下来就要构造指定函数的括号内 xxx_init 以及 xxx_exit 两个函数的本体。
入口函数一般设定为 int 类型,拥有返回值方便在后续的程序内编写验证代码,比较简单的便是验证return值是否为 0;出口函数一般设定为 void 型,一般只用实现简单的出口功能:
static int __init xxx_init(void) //驱动入口函数
{
/* 入口函数具体内容,要完成的功能 */
/* 注册字符设备驱动 */
return 0;
}
static void __exit xxx_exit(void) //驱动出口函数
{
/* 出口函数具体内容 */
/* 注销字符设备驱动 */
}
在这两个函数中,除了想要额外完成的功能外,各有一个必须完成的功能,那就是注册/注销字符设备,要在驱动模块加载完成后注册字符设备,卸载驱动模块时注销字符设备。这就是在入口/ 出口函数中留下的两个注释中所需编写代码得目的。
二、字符设备的注册 / 注销
本类所需函数定义在 <linux/init.h> 下
注册函数以及注销函数的原型如下:
/* 字符设备注册函数 */
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
/* 字符设备注销函数 */
static inline void unregister_chrdev(unsigned int major, const char *name)
注册函数中:
major : 主设备号;
name : 设备名,指向一串字符串;
fops : 结构体 file_operations 类型指针,指向设备的操作函数集合变量。
注销函数中:
major : 要注销设备对应的主设备号;
name : 要注销设备对应的设备名,指向一串字符串。
上一步提到,设备的注册与注销一般在驱动的入口及出口函数中进行,所以要将其写入 init 和 exit 函数:
#include <linux/init.h>
static struct file_operations test_fops; // file_operations 结构体,后文会写明
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
int ret = 0; //ret变量,验证设备注册是否成功
/* 注册字符设备驱动 */
ret = register_chrdev(200, "chrtest", &test_fops);
if (ret < 0) {
/* 字符设备注册失败,自行处理 */
}
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(200, "chrtest");
}
其中,在字符设备注册函数中,局部变量 ret 的设定,是为了验证字符设备是否注册成功,通过后面的 if 语句检验注册函数返回值,返回值 < 0 则为出错,通过 if 语句中的操作来处理错误。
三、file_operations 结构体——Linux 内核驱动操作函数集合
本类所需函数定义在 <linux/fs.h> 下
想要用户操纵驱动、设备,就在应用层调用一个函数,比如 open 函数,那么在驱动程序中也要有个驱动的 open 函数与之对应。类似的所有驱动函数的总集,就事先保存定义在了一个结构体里面,这个结构体就是 file_operations 结构体。这也就是上一步提到的,字符设备注册函数的参数中,*fops 结构体指针的指向。file_operations 的内容大体如下,只需看看了解:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
...
long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
其中有四个在字符设备驱动中比较基本常用的操作函数:open / release / read / write,这四个函数的功能分别为:打开设备 / 关闭设备 / 读取设备内存数据 / 数据写入设备内存。我们想要实现相对应的功能便需要实现响应的函数,即实现在 file_operations 结构体中相应的成员变量。//这里所谓“实现”的意思是要将编写的函数初始化给 file_operations 结构体中的成员变量。
1. 设备的打开及关闭
设备的打开和关闭是最基本的要求,几乎所有设备都需要提供打开、关闭的功能,否则后续对设备内部的操作便不必提了。而设备的打开与关闭需要我们实现 file_operations 结构体中的 open 和 release 两个函数。先编写好函数本体:
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp) //定义为 int 类型
{
/* 用户额外实现功能 */
return 0;
}
/* 关闭(释放)设备 */
static int chrtest_release(struct inode *inode, struct file *filp) //定义为 int 类型
{
/* 用户额外实现功能 */
return 0;
}
打开函数中:
inode : 传递给驱动的 inode;
filp : 设备文件, file 结构体有个叫做 private_data 的成员变量一般在 open 的时候将private_data 指向设备结构体;
return : 0,成功 / 其他,失败。
关闭函数中:
filp : 要关闭的设备文件(文件描述符);
return : 0,成功 / 其他,失败。
不同于之前两对函数:驱动入口 / 出口函数、设备注册 / 注销函数,每对的函数类型都是“进 int 出 void”,设备关闭函数也通常定义是 int 型,更方便检测返回值来验证是否成功。我想这是因为设备的关闭在使用中也同样十分重要,容易造成数据安全等方面的问题。
变好函数本体后,接下来的编写逻辑是通过结构体成员变量的初始化来将编好的函数“赋值”给 file_operations 结构体内对应变量,由于目前涉及的还有设备读 / 写操作函数,所以等到编写好这两个函数后一同进行初始化。
2. 设备的读取与写入
我们可以试着实现更多功能,比如对设备读写。假设这个设备控制着一段缓冲区(内存),那么应用程序就需要通过 read / write 函数对这段缓冲区进行读写操作,所以便要实 file_operations 中的 read 和 write 这两个函数。先编好函数本体:
/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
/* 用户实现功能 */
return 0;
}
/* 向设备写入 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
/* 用户实现功能 */
return 0;
}
读取函数中:
ssize_t : 有符号整形;
filp : 要打开的设备文件(文件描述符);
buf : 返回给用户空间的数据缓冲区;
cnt : 要读取的数据长度;
offt : 相对于文件首地址的偏移;
return : 读取的字节数,如果为负值,表示读取失败。
写入函数中:
ssize_t : 有符号整形;
filp : 设备文件,表示打开的文件描述符;
buf : 要写给设备写入的数据;
cnt : 要写入的数据长度;
offt : 相对于文件首地址的偏移;
return : 写入的字节数,如果为负值,表示写入失败。
借由这对函数便可以实现对设备的读写操作,完成比较辣鸡的基础自定义功能了(#^.^#),接下来就是上一步尾段提到的将所有操作函数初始化了,这样才可以实现它们对应的功能。
3. 初始化 fops 成员变量
之前两步里,我们编写了 chrtest_open / chrtest_read / chrtest_write / chrtest_release 四个函数,而这四个函数便是 chrtest 设备的 open / read / write / release 四个操作函数,所以我们需要继续将自己编写好的四个函数初始化“赋值”给之前构建好的 test_fops 结构体中的成员变量:
/* 设备操作函数结构体的初始化 */
static struct file_operations test_fops = {
.owner = THIS_MODULE, //比较固定的写法,表示是本内核模块,指定初始化
.open = chrtest_open, //open 操作为 chrtest_open 函数
.read = chrtest_read, //read 操作为 chrtest_read 函数
.write = chrtest_write, //write 操作为 chrtest_write 函数
.release = chrtest_release, //release 操作为 chrtest_release 函数
};
至此最简单基础的一个字符设备驱动程序内,需要“自己动脑”学习、编写的部分已经结束了。这里只是记录了大体的编写思路顺序以及框架,内部仍有很多地方需要填充,比如设备的创建、设备号的获取、函数内所需额外完成的内容等,需要后面继续慢慢学习摸索。而之所以强调需要“自己动脑”的部分结束,是因为对于基本的字符设备驱动框架来说还有一个“不需要动脑”按照固定格式写,但却仍很重要的片段。
四、LICENSE 和作者信息
一般地,我们要在代码最后加上这篇代码的相关信息,其中最重要的就包括 "LICENSE" 许可证、"AUTHOR" 作者,也可以加入对该程序的 "DESCRIPTION" 描述。这其中其他两项可以选择不添加,只有 LICENSE 是必须写上的,采用 GPL 协议,不然编译时会报错。其他两项可以选择不添加。
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Lepus57");
照样在双引号内填写后,我们最基本的字符设备驱动框架就搭建完成了,根据所需向其中填入对应逻辑代码即可完成。
五、本文所述结构汇整
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp) //定义为 int 类型
{
/* 用户额外实现功能 */
return 0;
}
/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
/* 用户实现功能 */
return 0;
}
/* 向设备写入 */
static ssize_t chrtest_write(struct file *filp, const chr __user *buf,
size_t cnt, loff *offt)
{
/* 用户实现功能 */
return 0;
}
/* 关闭(释放)设备 */
static int chrtest_release(struct inode *inode, struct file *filp) //定义为 int 类型
{
/* 用户额外实现功能 */
return 0;
}
/* 设备操作函数结构体的初始化 */
static struct file_operations test_fops = {
.owner = THIS_MODULE, //比较固定的写法,表示是本内核模块,指定初始化
.open = chrtest_open, //open 操作为 chrtest_open 函数
.read = chrtest_read, //read 操作为 chrtest_read 函数
.write = chrtest_write, //write 操作为 chrtest_write 函数
.release = chrtest_release, //release 操作为 chrtest_release 函数
};
/* 驱动入口函数 */
static int __init chrtest_init(void)
{
/* 入口函数具体内容 */
int ret = 0; //ret变量,验证设备注册是否成功
/* 注册字符设备驱动 */
ret = register_chrdev(200, "chrtest", &test_fops);
if (ret < 0) {
/* 字符设备注册失败,自行处理 */
}
return 0;
}
/* 驱动出口函数 */
static void __exit chrtest_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(200, "chrtest");
}
/* 指定驱动入口及出口函数 */
module_init(chrtest_init); //将 chrtest_init 函数指定为驱动入口函数
module_exit(chrtest_exit); //将 chrtest_exit 函数指定为驱动出口函数
/* 添加 LICENSE 和作者信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Lepus57");