字符设备驱动的基本框架

下一篇:《 字符设备号的静态 / 动态分配 》

目录

一、驱动入口/出口函数的指定与构建

1. 驱动入口 / 出口函数的指定

2. 驱动入口 / 出口函数的构建​

二、字符设备的注册 / 注销

三、file_operations 结构体——Linux 内核驱动操作函数集合

1. 设备的打开及关闭

2. 设备的读取与写入

3. 初始化 fops 成员变量

四、LICENSE 和作者信息

五、本文所述结构汇整


一、驱动入口/出口函数的指定与构建

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 结构体中的 openrelease 两个函数。先编写好函数本体:

/* 打开设备 */
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");

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值