大家好!作为一个刚入坑 Linux 驱动开发的初学者,我最近在疯狂啃书,敲代码找感觉,但是越学越感到世界是如此之大,但好在每天学到的新知识不会让我感到空虚。相信大家忘不了我们当初学习 C 语言是敲 Hello World 入门的,而现在学习 Linux 内核驱动,我们得先从编写最简单的内核模块入门。
这篇文章作为我的《Linux驱动开发》专栏的第二篇,我会分享一段带有传参功能的内核模块代码,详细分析其中的底层原理,并最终将它在板子上运行起来。
1 核心代码解析
先上代码,文件名是param.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_INFO 或 KERN_DEBUG,否则会干扰系统的关键错误报警。当然,这个例程中换用 KERN_INFO 是可以的,终端看不到我们可以去内核日志里面看。
2.2 __init 和 __exit
这两个宏可不仅仅是修饰符那么简单。
__init:它告诉编译器,这个函数只在模块初始化时使用。内核执行完它后,会立刻释放掉这部分代码占用的内存。在嵌入式设备这种内存匮乏的设备中,这是一个非常 nice 的设计。__exit:标记该函数只在模块卸载时调用。如果该模块被静态编译进了内核,而不是编译为.ko文件动态加载,这个函数甚至会被编译器直接优化掉,不会占用空间。
2.3 module_param
在用户态编程,我们习惯使用 main(int argc, char *argv[]) 传参,但在内核代码中,我们不能依赖 main 函数的参数,而是要借助 module_param 宏。
这个宏长下面这样:
module_param(name, type, perm)
它接收三个参数,下面一一介绍:
name:变量名,比如程序中的times。type:数据类型。charp代表字符指针char pointer,int代表整型,还有其他的如bool、long、short等等。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 如下:
#替换成你的开发板对应的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 网络文件系统,大家随意。
在板子上我们执行下面命令加载模块:
sudo insmod param.ko
第一次加载模块我们没有带参数,终端的内容也就是我们程序中默认的 who 和 times 的值。
然后我们卸载模块,再带上参数重新加载一下:
sudo rmmod param.ko
sudo insmod param.ko who="xlp" times=5
如图,可以看到初始化函数按照我们新传入的参数执行了。
到这里,我们本篇文章的主线任务就结束了,不知道大家还记得前面的 /sys 目录吗?后面的内容,我们继续就这个点简要的了解一下。
4. 探秘/sys文件系统
我们进入下面的目录:
/sys/module/param/parameters/
注意,param是编译后的模块名称,你当时怎么给文件命名的,现在就进哪个目录。
该目录的内容如下:
可以看到有两个文件,文件名分别就是我们程序中的变量名,访问权限也真的就是 -rw-r--r--,即0644。
我们查看一下文件的内容,发现正是我们加载模块时传进去的。
这里有一个问题请大家思考一下,如果我用echo修改这两个文件的值,终端还会打印出内容吗?
下面我们试试:
如图,我们先切换到 root 权限,然后分别修改 times 和 who 的内容,但最终终端并没有新的消息弹出来。
下面解释一下为什么。
道理其实也很简单,我们前面介绍module_init时就提到过,用它注册的初始化函数只会在模块加载时调用一次,而我们的 printk 循环打印逻辑在初始化函数中,在初始化完成之后它的内存就被回收了,此时 echo 修改的,仅仅是内核内存里留存的 who 变量和 times 变量的值而已。
5. 一个小坑
下面我要讲坑了,注意力请集中了。
上一章在讲修改 times 和 who 的值时,不知道大家有没有注意我是怎么修改的,我先使用sudo su切换到root权限再修改,而不是像下面这样:
sudo echo 10 > times
sudo echo "Linux" > who
先看看这样做的结果,我们再解释为什么:
可以看到,权限不足。
在你的大脑里,这句话的意思可能是:“以 root 权限执行 echo 10 > times 这个整体动作”。
但在 bash 的眼里,这句话被分成了两部分:
- bash 会首先看到
> times这个重定向符号。于是,bash 尝试以 当前登录用户也就是普通用户 cat 的身份,去打开并准备写入times这个文件。 - 如果文件打开成功,bash 才会以
root权限去执行sudo echo 3
而由于我们在代码里设置了参数的权限是 0644,也就是拥有者 root 可读写,其他人只能读,普通用户 cat 是没有写权限的。所以,在第一步时,bash 就直接报错拦截了,sudo 根本连执行的机会都没有。报错信息 -bash: times: Permission denied 前面的 -bash 就说明了这是 Shell 报的错,而不是 sudo 报的错。
大家可以去亲自尝试一下,在踩了这个坑之后,以后就不会再犯这种错误了。
6. 总结
通过这个简单的带参数的内核模块,我们其实明白了内核空间向用户空间暴露参数的机制,也就是/sys文件系统。
虽然还算不了真正意义上操作硬件的驱动,但是我想说的是,千里之行,始于足下。只要不断学习,把基础知识打牢,后面的路就会越走越宽,越走越快。
如果你也是正在学习 Linux 驱动的小白,欢迎评论区交流或者私信,大家一起进步。
最后,如果本文有帮助到你,希望能得到收藏和关注,我以后会持续更新这个专栏。
本文结束。