【Linux驱动实战】:带参数的内核模块

大家好!作为一个刚入坑 Linux 驱动开发的初学者,我最近在疯狂啃书,敲代码找感觉,但是越学越感到世界是如此之大,但好在每天学到的新知识不会让我感到空虚。相信大家忘不了我们当初学习 C 语言是敲 Hello World 入门的,而现在学习 Linux 内核驱动,我们得先从编写最简单的内核模块入门。

这篇文章作为我的《Linux驱动开发》专栏的第二篇,我会分享一段带有传参功能的内核模块代码,详细分析其中的底层原理,并最终将它在板子上运行起来。


1 核心代码解析

先上代码,文件名是param.c

c 复制代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
​
static char* who = "world";
static int times = 1;
​
// 声明模块参数
module_param(who, charp, 0644);
module_param(times, int, 0644);
​
static int __init my_driver_init(void)
{
    int i;
    for(i=0; i<times; i++)
    {
        printk(KERN_EMERG "hello %s!  times = %d\n", who, i+1);
    }
    return 0;
}
​
static void __exit my_driver_exit(void)
{
    printk(KERN_EMERG "The module end!\n");
}
​
module_init(my_driver_init);
module_exit(my_driver_exit);
​
MODULE_LICENSE("GPL");

内核模块的一些知识点我在上一篇文章中已经讲了不少了,但是考虑到可能有新的读者朋友,有些点我可能简单地再提一下。

这段代码乍一看很简单,但是作为初学者,我们要知其然,更要知其所以然。下面我会讲一些值得深挖的知识点。


2. 代码详解

我们首先把需要注意的点逐个击破,然后再看代码的整体框架。

2.1 为什么用 printk 而不是 printf

大家可能会想,我们以往用 C 语言写的应用程序用的打印函数都是 printf 呀,为什么这里用的是 printk?其实这个问题本身中就藏着一个关键点,我们以往写的是应用程序,而应用程序是运行在用户空间的。我们现在写的模块运行在内核空间,所以不能用用户空间的printf,只能用内核专属的printk

这里我想借用《UNIX 环境高级编程》中的一张图,鉴于清晰度我重画了一下,但表达的意思是一样的,如下:

正如图中描述的那样,应用程序可以使用公用函数库,也可以使用系统调用。而内核中的程序是用不了公用函数库的,内核有自己专属的打印函数,也就是代码中的printk

要注意看 printk 中的 KERN_EMERG,这是 内核打印级别KERN_EMERG 是最高级别,代表系统崩溃前的紧急信息。

我这里用 KERN_EMERG 是为了确保打印信息能直接输出到开发板的终端上。但在实际的工程项目开发中,普通的提示信息应该使用 KERN_INFOKERN_DEBUG,否则会干扰系统的关键错误报警。当然,这个例程中换用 KERN_INFO 是可以的,终端看不到我们可以去内核日志里面看。

2.2 __init__exit

这两个宏可不仅仅是修饰符那么简单。

  • __init:它告诉编译器,这个函数只在模块初始化时使用。内核执行完它后,会立刻释放掉这部分代码占用的内存。在嵌入式设备这种内存匮乏的设备中,这是一个非常 nice 的设计。
  • __exit:标记该函数只在模块卸载时调用。如果该模块被静态编译进了内核,而不是编译为 .ko 文件动态加载,这个函数甚至会被编译器直接优化掉,不会占用空间。

2.3 module_param

在用户态编程,我们习惯使用 main(int argc, char *argv[]) 传参,但在内核代码中,我们不能依赖 main 函数的参数,而是要借助 module_param 宏。

这个宏长下面这样:

c 复制代码
module_param(name, type, perm)

它接收三个参数,下面一一介绍:

  • name:变量名,比如程序中的times
  • type:数据类型。charp 代表字符指针 char pointerint 代表整型,还有其他的如 boollongshort 等等。
  • perm:访问权限。

变量名和数据类型这两个参数都好理解,但是第三个参数 访问权限 我这里要详细讲讲:

大家也都看到了,代码中访问权限对应的参数填的0644,但是这个 0644 到底是什么意思呢?

其实这里的 0644 是八进制权限掩码,类似 Linux 系统的文件权限 rw-r--r--:意味着文件所有者可读可写,同组用户及其他用户只能读。

实际上,当模块被加载后,内核会在 /sys/module/param/parameters/ 目录下为这些参数生成对应的虚拟文件,这里的 param 是模块名称,这就意味着,我们甚至可以在模块运行期间,通过修改 /sys 目录下的文件来动态改变这些变量的值。 后面加载模块之后我会验证这一点。

2.4 别忘了 MODULE_LICENSE

如果你不加 MODULE_LICENSE("GPL"),模块虽然能编译过,但在加载时内核会无情地吐出一条警告:"module taints kernel"(该模块污染了内核)。Linux 内核是 GPL 开源协议的,内核极其鄙视 闭源 模块,加上这句不仅是遵循开源精神,也能让我们调用更多内核导出的底层 API。


3. 编译并运行

3.1 编译

Makefile 如下:

makefile 复制代码
#替换成你的开发板对应的Linux内核源码树的绝对路径,并且源码一定要是编译过的
KERNEL_DIR := /home/xlp/workspace/kernel
​
obj-m := param.o
​
all:
    make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules
​
clean:
    make -C $(KERNEL_DIR) M=$(PWD) clean
​

编写好 Makefile 之后,我们在存放 param.c 和 Makefile 的目录下执行 make ,如下:

如图,编译成功之后目录下多出了许多文件,其中大部分都是编译的中间产物,只有 param.ko 是我们需要的。

然后我们,就可以在板子上加载这个模块了。

3.2 运行

param.ko 从虚拟机传到板子上有很多方法,我用的 NFS 网络文件系统,大家随意。

在板子上我们执行下面命令加载模块:

bash 复制代码
sudo insmod param.ko

第一次加载模块我们没有带参数,终端的内容也就是我们程序中默认的 whotimes 的值。

然后我们卸载模块,再带上参数重新加载一下:

bash 复制代码
sudo rmmod param.ko
bash 复制代码
sudo insmod param.ko who="xlp" times=5

如图,可以看到初始化函数按照我们新传入的参数执行了。

到这里,我们本篇文章的主线任务就结束了,不知道大家还记得前面的 /sys 目录吗?后面的内容,我们继续就这个点简要的了解一下。


4. 探秘/sys文件系统

我们进入下面的目录:

bash 复制代码
/sys/module/param/parameters/

注意,param是编译后的模块名称,你当时怎么给文件命名的,现在就进哪个目录。

该目录的内容如下:

可以看到有两个文件,文件名分别就是我们程序中的变量名,访问权限也真的就是 -rw-r--r--,即0644

我们查看一下文件的内容,发现正是我们加载模块时传进去的。

这里有一个问题请大家思考一下,如果我用echo修改这两个文件的值,终端还会打印出内容吗?

下面我们试试:

如图,我们先切换到 root 权限,然后分别修改 timeswho 的内容,但最终终端并没有新的消息弹出来。

下面解释一下为什么。

道理其实也很简单,我们前面介绍module_init时就提到过,用它注册的初始化函数只会在模块加载时 调用一次,而我们的 printk 循环打印逻辑在初始化函数中,在初始化完成之后它的内存就被回收了,此时 echo 修改的,仅仅是内核内存里留存的 who 变量和 times 变量的值而已。


5. 一个小坑

下面我要讲坑了,注意力请集中了。

上一章在讲修改 timeswho 的值时,不知道大家有没有注意我是怎么修改的,我先使用sudo su切换到root权限再修改,而不是像下面这样:

bash 复制代码
sudo echo 10 > times
sudo echo "Linux" > who

先看看这样做的结果,我们再解释为什么:

可以看到,权限不足。

在你的大脑里,这句话的意思可能是:"以 root 权限执行 echo 10 > times 这个整体动作"。

但在 bash 的眼里,这句话被分成了两部分:

  1. bash 会首先看到 > times 这个重定向符号。于是,bash 尝试以 当前登录用户也就是普通用户 cat 的身份,去打开并准备写入 times 这个文件。
  2. 如果文件打开成功,bash 才会以 root 权限去执行 sudo echo 3

而由于我们在代码里设置了参数的权限是 0644,也就是拥有者 root 可读写,其他人只能读,普通用户 cat 是没有写权限的。所以,在第一步时,bash 就直接报错拦截了,sudo 根本连执行的机会都没有。报错信息 -bash: times: Permission denied 前面的 -bash 就说明了这是 Shell 报的错,而不是 sudo 报的错。

大家可以去亲自尝试一下,在踩了这个坑之后,以后就不会再犯这种错误了。


6. 总结

通过这个简单的带参数的内核模块,我们其实明白了内核空间向用户空间暴露参数的机制,也就是/sys文件系统。

虽然还算不了真正意义上操作硬件的驱动,但是我想说的是,千里之行,始于足下。只要不断学习,把基础知识打牢,后面的路就会越走越宽,越走越快。

如果你也是正在学习 Linux 驱动的小白,欢迎评论区交流或者私信,大家一起进步。

最后,如果本文有帮助到你,希望能得到收藏和关注,我以后会持续更新这个专栏。


本文结束。

相关推荐
木古古1820 小时前
搞一个高效的c/c++开发环境,工具VIm+自研vim插件+Shell脚本
linux·编辑器·vim
茫忙然21 小时前
U 盘搭建免驱 Linux 便携系统教程
linux·服务器
一起逃去看海吧1 天前
dify-03
java·linux·开发语言
fengyehongWorld1 天前
Linux 根据端口进行的相关查询
linux
lihao lihao1 天前
linux匿名管道
linux·运维·服务器
うちは止水1 天前
weston出图调试
linux·wayland·weston
STDD1 天前
Farming Simulator 25(模拟农场 25) Linux 专服搭建完全指南
linux·运维·javascript
好好风格1 天前
宝塔面板 HTTPS 端口证书不生效排查记录
linux·运维·nginx
用户2367829801681 天前
Linux pgrep 命令详解:按名称查找进程 PID 的高效方法
linux