|
Hello World 模块
Linux 设备驱动通常以内核模块的形式出现,因此学习编写 Linux 内核模块是学习编写 Linux 设备驱动的先决条件。
我们从最简单的 hello world 模块开始来建立对内核模块的感性认识。下面我们来看这个示例。
hello world 模块源代码:
#include <linux/module.h>
MODULE_LICENSE(&#34;Dual BSD/GPL&#34;); // 告诉内核该模块采用的许可证
static int __init hello_init(void)
{
printk(KERN_EMERG &#34;hello world\n&#34;);
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_EMERG &#34;goodbye world\n&#34;);
}
module_init(hello_init); // 告诉内核该模块被装载时调用 hello_init()
module_exit(hello_exit); // 告诉内核该模块被移除时调用 hello_exit()hello world 模块 Makefile:
KVERS = $(shell uname -r)
# modules
obj-m += hello.o
# Specify flags for the module compilation
# EXTRA_CFLAG = -g -o0
build:modules
# -C选项指定内核源代码目录,M=选项指定模块源代码目录
modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) cleanhello world 模块的编译:
pi@raspberrypi:~/code/baibai_test/hello $ make
make -C /lib/modules/5.15.40-v7l/build M=/home/pi/code/baibai_test/hello modules
make[1]: Entering directory &#39;/home/pi/code/linux-rpi-5.15.y&#39;
CC [M] /home/pi/code/baibai_test/hello/hello.o
MODPOST /home/pi/code/baibai_test/hello/Module.symvers
CC [M] /home/pi/code/baibai_test/hello/hello.mod.o
LD [M] /home/pi/code/baibai_test/hello/hello.ko
make[1]: Leaving directory &#39;/home/pi/code/linux-rpi-5.15.y&#39;hello world 装载和卸载:
pi@raspberrypi:~/code/baibai_test/hello $ sudo insmod hello.ko
Message from syslogd@raspberrypi at Oct 18 03:17:41 ...
kernel:[ 4340.242938] hello world
pi@raspberrypi:~/code/baibai_test/hello $ sudo rmmod hello
Message from syslogd@raspberrypi at Oct 18 03:17:49 ...
kernel:[ 4348.772362] goodbye world内核模块对 linux 内核架构的作用
我们从前面的示例已经看到,编写一个内核模块倒是挺简单的(至少当它仅仅需要显示hello world的时候),但为什么 Linux 要搞出内核模块这个东东呢?下面我们尝试从设计的角度来推演一下这个问题。
从宏内核说起
众所周知 Linux Kernel 使用的是宏内核。宏内核相对应的是微内核。
如果仅讨论架构的优劣,宏内核肯定是不如微内核的。关于宏内核和微内核, Linus 和 微内核操作系统 Minix 的作者 Tanenbaum 有一个著名辩论,其中 Linus 自己也说:“True, linux is monolithic, and I agree that microkernels are nicer.”
那么为什么 Linus 还是为 Linux 选择了宏内核架构呢?我们还是从刚才那个著名辩论中来找答案,Linus 说道:
“Linux has very much been a hobby (but a serious one: the best type) for me: I get no money for it, and it&#39;s not even part of any of my studies in the university. I&#39;ve done it all on my own time, and on my own machine.”
“What you don&#39;t mention is that minix doesn&#39;t do the micro-kernel thing very well, and has problems with real multitasking (in the kernel).”
“I got my 386 last January, and linux was partly a project to teach me about it. Many things should have been done more portably if it would have been a real project. I&#39;m not making overly many excuses about it though: it was a design decision, and last april when I started the thing, I didn&#39;t think anybody would actually want to use it. ”
。。。。。。
(附:Linus vs Tanenbaum 辩论原文链接:http://choices.cs.illinois.edu/cache/Linus_vs_Tanenbaum.html )
内容比较多,我这里只摘了几句有代表性的话。总结来说,这里面并没有非常直白地说为什么选择宏内核架构,但我们能够看到几个关键信息,一是 Linus 最初开发 Linux 时没想投入太多时间,也没想着赚钱,也没想过会被其他人使用,仅仅就是因为自己有了台PC,想在上面跑个操作系统来玩;二是微内核架构虽好,但实现起来比宏内核复杂,Tanenbaum 的 Minix 就有不少实现上的问题,导致 Minix 最终体现出来的效果并不好。
所以,我们可以推断 Linus 为 Linux 选择宏内核架构很大程度是因为宏内核简单、能用、好实现。
宏内核的缺点
Linux 的发展显然是远远超出了最初的预期,我们来分析下随着规模的增大 Linux 的宏内核会遇到什么样的问题:
- 所有内核功能整体编译在一起,即使一开始不用的功能也要先编译进内核,即使过程中不会再使用的功能也必须一直保持在内核中,非常不灵活;如果面对提前难以预知的热插拔设备,几乎就没法实现,难不成每次新插一个未知设备就要重装系统?
- 每次增减、删除、修改某个功能都要重新编译、重新加载整个内核,而且内核越大,编译、加载也会越慢,对调试非常不友好。我只想调试1K代码,结果每次都要背上整个内核的包袱,这严重降低了调试效率。
- 内核镜像文件会比较大,而且整个内核镜像会被加载到内存中运行,会占用较多内存空间。
- C语言的函数默认是全局的,所以内核的函数默认可以在整个内核范围内被调用,调用关系难以控制,整个内核很容易演变成糟糕的大泥球(大泥球是一种著名的反架构模式,可参考:https://wenku.baidu.com/view/3d8d6702b7daa58da0116c175f0e7cd18425188a.html)。
。。。。。。
如何改善?
前面把缺点列出来了,其实这些都是宏内核天然的问题,而微内核天然就没有这些问题(当然微内核会有其它问题,但跟本文的内核模块没什么关系,所以不讨论)。那很自然就能想到,那就跟微内核学习嘛。
像微内核一样,把一些功能划分出来。但因为是宏内核,这些功能还是在内核态运行,但是逻辑上是独立的,可以单独编译、单独装载和卸载。这就是 Linux 的内核模块啦。
这样一来,上面4个缺点中的前面3个基本就解决了。第4个缺点也得到一部分消减,毕竟分了模块,模块和模块之间是有逻辑边界的。如果想进一步消减第4个缺点,那就要把函数的范围默认限定在模块内部,模块对外的函数需要显示声明。于是 Linux 又设计了一个机制:只有显示用 EXPORT_SYMBOL() 或 EXPORT_SYMBOL_GPL() 声明的函数才能被其它模块调用。这样第4个缺点也基本得到了解决。 |
|