一起分析Linux系统设计思想——05字符设备驱动框架剖析(二)

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

1 最简单的Linux驱动程序

我们从最简单的一个Linux驱动程序开始,这样更容易抓住核心。

1.1 内核态驱动程序

1.1.1 源码

闲言少续,上代码,先操作看结果,有个感性认识。后面两个小节会详细分析内核驱动模块的设计思想。

/* 最简单的内核态驱动程序:cdriver.c文件 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>

/* 定义设备文件打开函数 */
int cdriver_open(struct inode *inode, struct file *file)
{
    /* 什么也不做,只是打印(注意在内核态只能用printk,不能用printf)*/
    printk("cdriver open success!\n"); 
    return 0;
}
/* 定义设备文件操作接口对象,并初始化 */
struct file_operations cdriver_fops = { 
    .owner = THIS_MODULE, /*THIS_MODULE是编译器生成的,其实就是this指针,如果觉得抽象可以先不关注*/
    .open = cdriver_open, /*将我们定义的cdriver_open函数挂到open函数指针上*/
};
/* 定义模块初始化函数 */
int __init cdriver_init(void) /*__init是段属性,可以先忽略*/
{
    /* 这里是核心动作——注册。注册主设备号(110),设备名称(“cdriver")和
       设备文件操作符首地址到内核中。 */
    register_chrdev(110, "cdriver", &cdriver_fops);
    return 0;
}
/* 定义模块退出函数*/
void __exit cdriver_exit(void)
{
    unregister_chrdev(110, "cdriver"); /*取消注册*/
}
/* 下面这三行东西真正把驱动做到了“模块化” */
module_init(cdriver_init); /*将cdriver_init放到指定段*/
module_exit(cdriver_exit);
MODULE_LICENSE("GPL"); /*注册GPL*/

1.1.2 Makefile

源码写完了,接下来我们要把它编译成一个ko模块。

# 在任意路径下编译ko模块的Makefile(在宿主机中)

# 定义内核源码路径,因为ko的编译需要使用内核源码目录下的Makefile
# 进行编译,此Makefile属于子Makefile级别的。
KERN_DIR = /work/linux-2.6.22.6 # 注意需要把路径修改成你自己的

all:
        # 该命令是make modules命令的扩展,用来生成ko文件。
        # -C (ChangeDirectory)选项的作用是指将当前的工作目录转移到指定目录(KERN_DIR),并调用该目录顶层下的Makefile。
        # M=`pwd`选项(等价于$(shell pwd))指明在构造modules目标之前需要返回到当前目录生成obj-m指定的xxx.o目标模块。
        make -C $(KERN_DIR) M=`pwd` modules 
clean:
        make -C $(KERN_DIR) M=`pwd` modules clean #删除ko文件和生成ko文件时的中间文件
        rm -rf modules.order Module.symvers .temp_versions #清除其他文件
obj-m   += cdriver.o #指定依赖的目标名称,需要和同目录下的.c文件同名。

直接在当前目录下执行 make 就会生成 cdriver.ko 文件。

1.1.3 安装模块并创建设备节点

将cdriver.ko文件放到嵌入式设备上。插一句,嵌入式开发阶段推荐使用nfs启动方式(具体操作方法可以参考我这篇文章:https://blog.csdn.net/weixin_44873133/article/details/109482297),这样的话就省的总是将程序或文件传来传去的,影响开发效率。

放好之后,按照下面的指令进行安装模块。

# 在嵌入式设备的终端上操作
/work # insmod cdriver.ko   #插入模块cdriver.ko到内核空间。
/work # lsmod               #查看当前插入的所有模块,以便确认我们安装成功。
Module                  Size  Used by    Not tainted
cdriver                 1664  0
/work # cat /proc/devices   #由于我们安装的是一个字符设备驱动,可以使用该指令查看设备情况。
Character devices: #标识是字符设备。
...
110 cdriver        #从这里可以看到我们的字符设备名称是"cdriver",主设备号是110。安装成功没毛病~
...

在上一篇文章中我们知道字符设备驱动被用户态程序访问是通过一个虚拟文件,又称为设备节点。在构建根文件系统的文章(https://albert-genius.blog.csdn.net/article/details/109349615)中也接触过这个概念了。

接下来我们就手动创建一下cdriver对应的虚拟文件。

# 在嵌入式设备的终端上操作
/work # mknod /dev/chrdriver c 110 0 #文件名称可以和设备名不同,但是主设备号一定要保持一致。
/work # ls -al /dev/chrdriver #查看创建结果,可以发现类型为c(char),主设备号为110,从设备号为0。
crw-r--r--    1 0        0        110,   0 Jan  1 00:13 /dev/chrdriver

1.2 用户态应用程序

1.2.1 源码

我们需要编写一个用户态应用程序来调用并验证内核模块的功能。

/* capp.c源文件 */
#include <stdio.h>
#include <fcntl.h>

int main(void)
{
    int fd = 0;
	
    /* 以读写方式打开我们创建的设备节点/dev/chrdriver */
    fd = open("/dev/chrdriver", O_RDWR); 
    if (fd < 0) { /*异常处理*/
        printf("open /dev/cdriver failed!\n");
        return -1; 
    }   
	/* 这里只是简单打印,在这里可以加入硬件操作 */
    printf("open /dev/cdriver success ! \n"); 
    
    (void)close(fd);    
    return 0;
}

1.2.2 编译和运行

编译比较简单,都不用写Makefile了,如下操作:

# 在宿主机上操作
albert@ubuntu:/work/nfsroot/minifs/work $ arm-linux-gcc -o capp_elf capp.c #编译
albert@ubuntu:/work/nfsroot/minifs/work $ ls
capp.c  capp_elf #目标成功生成

将capp_elf放在嵌入式设备上运行如下:

# 在嵌入式设备的终端中操作
/work # ./capp_elf            #运行应用程序
cdriver open success!         #这行打印是字符驱动模块内的打印
open /dev/cdriver success !   #这行打印是应用程序中的打印

2 Linux驱动分层思想

相信大部分TX已经可以熟练创建一个ko模块,并可以熟练地insmod了。但是背后的机制是什么,设计思想又是什么呢?

下面我们就来揭开这层神秘的面纱。

2.1 什么是分层

首先,我们先引入一个非常重要的概念——分层

那么分层思想的本质到底是什么呢?实质上就是下面这个公式:

分层 = 分类 + 单向依赖

分类就是按照软件差异进行代码分割, 模块化设计 就是分类的体现。既然还是一个系统,被分类后的各个模块之间就存在着不可割裂的联系,这种联系就是一种 依赖 关系,多个模块间的依赖关系就组成整个系统的 关系拓扑 。为防止关系过于复杂,我们往往会添加一些限制条件,比如说将依赖关系尽可能做成 单向 的。这样就会演变为分层的结构。

上面这个过程看起来并不复杂,复杂的是根据实际的业务需求或遇到的问题进行合理的分类和依赖关系设计——这也是架构师的重要工作之一。常见的分类手法有:将不同功能的模块进行分离;将稳定的和易变的模块进行分离等(这里只是简单提一下,后续在架构设计相关的专题中再详细说明)。

2.2 用分层思想剖析内核驱动工作原理

先上Linux系统分层示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W0LVl2W7-1605547401002)(05%E5%AD%97%E7%AC%A6%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8%E6%A1%86%E6%9E%B6%E5%89%96%E6%9E%90%EF%BC%88%E4%BA%8C%EF%BC%89.assets/image-20201117004124365.png)]

接下来我们通过这幅图来介绍什么是库函数,什么是系统调用,什么是用户空间,什么是内核空间,以及应用程序想要访问硬件的整个流程。

处理器在设计时为了保证系统的安全性,会设置特权模式和用户模式,操作系统作为最接近硬件的软件是会完美切合硬件设计的。因此,操作系统也会区分内核态和用户态,内核态工作在特权模式,用户态工作在用户模式。工作在内核态的进程可以访问到所有地址空间,工作在用户态的进程只可以访问到指定的一段地址空间,这段地址空间就叫做 用户空间 ,而它的补集就叫做 内核空间

用户程序想要访问硬件必须经过内核,有时也称为 陷入 内核。陷入内核,使用内核的动作就是 系统调用,也就是调用系统(反过来说貌似更直白哈)。系统调用也没有什么高级的,就是操作系统内核为了更好地服务于应用程序提供了一些列的系统API接口供应用程序调用,应用程序调用这些接口的动作就是系统调用了。

本来有这些系统调用接口也就满足应用程序的需求了,但是一些大牛闲着没事儿,封装了一些更好用的函数(这些函数有些调用了系统的API接口,有些没有)供应用开发者使用。这些函数族就是 C Library

接下来,我们再来看看我们刚刚写的最简单的Linux驱动程序在这个图的什么位置,以及它的工作原理。

我们写的cdriver.ko属于内核级的设备驱动模块,它其实可以直接操作硬件(后续我们将它演进成LED驱动时就更直观了)。为了向上统一接口,字符型设备都对应一个虚拟文件,这也是我们为什么需要 “mknod /dev/chrdriver c 110 0”,的原因。系统调用会为应用程序提供操作文件的统一接口,比如open、read、write等,C Library会进一步将这些接口封装成参数更简单的open、read、write等函数供应用程序开发人员使用。

上面的基础都打好了,就可以开开心心地写应用程序了。你看我们刚写的用户态的应用程序就调用一些C Library函数就可以像操作文件一样操作底层的驱动了,是不是很简单?

Tips:应用程序运行在用户空间,它会被MMU限制在某段内存中,是不被操作系统信任的(限制应用程序的某些权限),因此可以避免应用程序的错误导致整个系统崩溃。而驱动程序不同,是会被编译进内核的(静态或动态加载方式),是被内核信任的,驱动程序的错误很可能会导致整个系统崩溃。因此,开发Linnux内核驱动程序一定要加倍小心,异常处理变得极为重要。

最后,用一个词来概括整个过程——剥洋葱。

下一篇我们会继续分析一个ko内部的设计思想,敬请期待~~


恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿越临界点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值