宏内核和微内核
继续前面第一章的知识,虽然有点啰嗦,既然啰嗦了就继续啰嗦下去吧,也是给第一章的内容增加解释。
我们知道内核如果按种类来划分的话,可以分为宏内核和微内核,微内核是一个比较先进的内核,我不知道是不是真的先进,但是 BSD 系统的设计就是想用微内核的思想来完成的,结果导致系统稳定性很差,不能达到正常使用的程度。
什么是微内核呢?
如果把CPU当成是皇帝的话,皇帝身边的大臣们就可以划分为各种进程和线程了,比如打印机是一个进程,微内核的思想就是其他进程之间不能直接通信,他们只能跟CPU中央处理器进程通信,然后中央进程再做调度处理,这样看起来非常完美,也避免了内核很多高耦合的代码。
什么是宏内核呢?
宏内核的设计就跟微内核有点相反,宏内核的子进程之间是可以互相通信的,各个模块之间是可以进行函数调用的,你可以观察下 Linux 内核模块,导出内核符号表后,可以在其他模块调用。
这样设计从理论角度来看,代码耦合度比较高,出问题的概率会增大,然后实力打脸的是,Linux 非常成功,耦合度高有弊端也有优点,其中优点得到了充分的诠释,再加上内核模块可以动态的加载和卸载,这简直不要太完美哦。
(可以点击放大)
这个图,如果你这样看,可能看不是很清楚,因为图片比较大,你需要点击放大了来看,就可以看到里面的端倪和差别了。
Monolithic kernel
是我们所说的宏内核了,里面的设备文件,驱动,进程间通信,进程间通信也可以叫做IPC,这些都是柔和在一个一个模式里面,这个模式叫做内核态,宏内核可以动态加载内核模块,当然了,也可以动态卸载内核模块,你看上面图片那个深红色的地方就是内核态。
Micro kernel
是我们说的微内核了,微内核就是中央集权统治的了,既然是中央集权统治,就应该精简,毕竟贵族的话,都想自己身份特殊点,所以里面就是黄色的往下了很多。
Hybrid kernel
是混合式内核,又是微内核,又是宏内核,windows的设计者就是这么厉害,你们俩都说自己最厉害,又都不退一步,可能你们没有发现,可能我才是最厉害的,所以有人说,Windows 是宏内核,嗯,这样说没什么问题,看看目录[c:\windows\system32\drivers]下面有很多驱动相关的,然后又有人跑出来说,Windows是微内核,嗯,这样说也没有什么不对。
Linux 环境安装
先说下我在第一章节说的,会在这里给大家一个资源链接,我其中包括ubuntu的下载链接,ubuntu如果运行不了的话,需要进入bios打开虚拟机选项,这个是比较干净的版本,里面的vim ,git什么的都还没有安装,大家打开后需要自己安装,我觉得如果是初学者的话,这样也是要经历的,了解系统的一个过程。
作为一个Linux的狂热分子,我设置的Linux账号密码就是[ Linux ],大家可要记住啊。
Linux 内核,可以运行在 x86 体系结构上,也可以运行在 ARM 体系结构上。我刚开始学习 Linux 的时候,非常迫切的想要在 ARM 上运行代码,经过这么多年的摸爬滚打,我终于发现那是一个非常玩笑的想法,Linux 内核博大精深,你学的是内核,是思想,我们后面会从是嵌入式Linux开发,安卓开发,openwrt开发,MIFI开发等等,用到的都是Linux内核,会有差别,但是原理性的东西不会差别太大。
所以我想自己给大家写书的时候,还是用 ubuntu 来介绍 Linux 内核。至于在在ARM开发板上运行,给大家说一个东西,叫做交叉编译链,什么是交叉编译链,本来windows是X86体系结构的,正常编译出来的文件,应该只能在X86体系结构上执行,但是有了交叉编译器之后呢,我可以在windows上编译ARM体系结构的固件。
反过来说,你学好了Linux内核,理清了其中的脉络结构,不管你是做安卓开发,还是嵌入式开发,以及其他衍生的Linux应用或者驱动开发,都会得心应手。
我安装好的 ubuntu 开发环境,有需要的可以下载,下载链接查看前面部分提到的 github 链接,本书的所有例程都上传到 github 上做备份,到前面链接上下载 ubuntu 虚拟机压缩包,下载后用 VMware 打开,可能会打开会有提示出错,重启电脑,进入 Bios 把虚拟机功能选项设置为 Enable,再重新进入系统就可以看到一个可以让你学习的 ubuntu 系统了。
Linux 内核模块可以做什么?
Linux 内核下面的代码几乎都是用内核模块完成开发的,我们换一个角度来看,内核模块看起来就像是一个应用程序的一段代码,这个点我觉得应该很容易理解,但是它有跟应用程序又不一样,我们知道内核空间和用户空间地址段是不一样的,内核模块编写需要非常谨慎,因为稍微不注意就会可能引起系统的崩溃,我这样说大家应该能理解吧,应用空间运行的应用程序有问题,崩溃了没关系,那只是它自己挂掉了,但是内核模块不一样,如果内核模块有调用它的地方,比如音频的 ALSA 驱动,如果崩溃了,那么整个系统看语音部分就不能工作了。
设备驱动使用内核模块编程,就比如一个触摸屏驱动,在设备模型章节我们好好聊下这个事情,但是我们这里就提一下,一个设备需要依赖一个驱动,没有驱动设备是不可能正常工作了,所以内核模块在系统下的驱动都是用内核模块编写的,Linux 内核下面有非常多的设备,当然就需要非常多的驱动。
文件系统驱动,说到设备文件不得不提一句,Linux 下的所有都可以看做是文件,不同的文件系统还有有一个统一的虚拟文件系统来管理,这些都是需要内核模块来完成的。
系统调用,用户空间的程序需要调用内核服务,就必须要使用系统调用,正常的系统调用比如关机,读写文件,如果你需要新建一个系统调用也是可以的,那就需要你这个大神自己写个内核模块。
我大概就知道这么多,可能还有些没写出来的,也是内核模块编程范围的,比如网络编程等。
Hello World 内核模块
学习 C 语言的时候,我们会写一个 Hello World 来证明一下自己,然后我们学习 C++ 的时候,我们也会写一个 Hello World 来证明一下自己,再然后我们会学习 java、python、还有其他很多很多的编程语言,但是最开始,你总是想写一个 Hello World 来证明一下自己。
我也很想知道为什么我们跟 Hello World 那么过不去,来个比喻,这就像我们习惯了喝热水一样,如果仔细去追究,好像喝热水并不能如何如何,为什么我们习惯喝热水,很大的原因是因为我们的妈妈想让我们喝热水,因为我们的妈妈觉得喝热水是一个好习惯,喝热水是可以强身健体的,喝热水包治百病。
再反回来看我们为什么会在刚开始学习一个东西的时候喜欢去写 Hello World ,因为你接触的很多人告诉你这样做,如果可以,你明明是不需要些 Hello World 的,而是写 Hello You 或者 Hello 等其他什么东西的。
说到这里,我还是要写一个 Hello World 来让自己入个门,进入那个深不可测的世界,聆听 Linux 教诲、体会Linux带给我们的快感。
Hello World 内核模块代码
#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); static int hello_init(void) { printk(KERN_ALERT "Hello, world\n"); return 0; } static void hello_exit(void) { printk(KERN_ALERT "Goodbye, cruel world\n"); } module_init(hello_init); module_exit(hello_exit); |
就是这简单的几行代码,我们来简单的分析一下,前面两行是包含头文件,这个跟我们学习 C 语言没有什么区别,有了头文件才能调用函数,就是这么简单。
第三行,MODULE_LICENSE 这个是声明模块的许可证说明的,比喻一下,我们开车,就必须有一个驾驶证,如果我们没有驾驶证,就会被交警叔叔给抓住,模块代码也一样,如果没有许可证声明,就会被内核给抓住
模块的许可证声明 从 2.4.10 内核版本开始,模块必须通过 MODULE_LICENSE 宏声明此模块的许可证,否则在加载此模块时,会收到内核被污染 “kernel tainted” 的警告。从 linux/module.h 文件中可以看到,被内核接受的有意义的许可证有
"GPL",
"GPL v2",
"GPL and additional rights",
"Dual BSD/GPL",
"Dual MPL/GPL",
"Proprietary"
在同时支持2.4与2.6内核的设备驱动中,模块可按如下方式声明自己的许可证。
MODULE_LICENSE(“GPL”);
然后下面就是声明的两个函数,函数里面调用了 printk 来打印内容,后面再有一个 module_init 和 module_exit ,望文生义,我们可以知道这两行代码是用来说明模块初始化和模块退出时调用的函数。
printk 内核打印和调试工具
内核里面是没有 C 库(C库是我们写C语言的那个库函数)的,大家刚开始学习 C 语言的时候,知道一个函数叫做 printf ,这个也是一个打印函数,但是这个打印函数需要 C 库的帮忙,内核里面需要没有C库但是需要自己的打印函数,这个函数就叫做 printk。
不知道大家想过没有,为什么内核模块里面可以直接调用printk函数,内核模块是属于内核的,printk也是属于内核的,如果内核函数在内核导出了内核符号表,那么这个函数可以被内核里面的其他地方使用,我又要惊叹一下这样的设计了。
KERN_ALERT 这个是一个宏定义,用来控制内核打印的等级,等级很重要,通过控制等级可以动态的控制日志的输出,Linux 经久不衰的原因也是因为有这么多优良的基因,而其中的这个基因肯定是包括 printk
关于内核的打印部分,我觉得后面可以起一个章节单独说明,调试打印还是非常有必要的。
编写Makefile编译HelloWorld模块
有源码还是不够的,就好像我们做饭,准备了很多食材,我们还需要一个锅来料理这些食材,源代码也是一样,它们也是码农的食材,但是也是需要料理的,所以就出现了很多烹饪手法,我们码农也接触到了很多编译器,而在 Linux 下最常用的 就是 GCC 编译器,使用GCC 就离不开 makefile。
我们给上面的 Hello World 模块编写一个 makefile 吧
ifneq ($(KERNELRELEASE),) obj-m := hello.o else PWD := $(shell pwd) KVER := $(shell uname -r) KDIR := /lib/modules/$(KVER)/build all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions endif |
很多人写好了Makefile 但是编译却不成功,因为里面还有一些需要注意的地方,特别是初学者,可能要被这几行代码调戏很长时间,其中需要注意的是要用4空格的Tab键,要不然很大概率会出现编译出错。
我们编译内核需要编译工具Makefile,还需要一个很重要的东西,就是内核库文件,我现在是自己安装了一个ubuntu系统,这个系统在安装的时候已经有了一个内核编译好的库文件,位置就是 KDIR 指定的位置,里面的东西大概如下。
HelloWorld 模块编译的时候,需要从这个库文件里面去找到它需要依赖的函数,头文件等等。
linux@ubuntu:/lib/modules/3.13.0-32-generic/build$ ls arch Documentation fs ipc kernel mm samples sound usr block drivers include Kbuild lib Module.symvers scripts tools virt crypto firmware init Kconfig Makefile net security ubuntu linux@ubuntu:/lib/modules/3.13.0-32-generic/build$ |
假设像这样的错误
linux@ubuntu:~/linux$ make Makefile:11: *** missing separator. Stop. |
我们编译出来的内核模块,是包含一定的信息的,我们可以使用命令来查看这些信息,当然了,你也可以用这个方法去查看别人编写的内核模块的信息,很多初学者写代码都是哐哐哐,编译通过,开心一下,运行成功了,开心一下,完全不去考虑其中的细节,查看内核模块信息就是其中的一个细节,使用命令和运行结果如下。
linux@ubuntu:~/linuxBook/HelloWorld$ modinfo hello.ko filename: hello.ko license: Dual BSD/GPL srcversion: 31FE72DA6A560C890FF9B3F depends: vermagic: 3.13.0-117-generic SMP mod_unload modversions linux@ubuntu:~/linuxBook/HelloWorld$ |
内核模块的加载和卸载
内核模块编译好了之后,在当前的路径下会生成一些产物,如下图
linux@ubuntu:~/linux$ ls hello.c hello.ko hello.mod.c hello.mod.o hello.o Makefile modules.order Module.symvers linux@ubuntu:~/linux$ |
这时候,我们就需要装载这个内核模块到内核里,Linux 内核里面有一个链表来维护在运行中的内核模块,我们装载内核模块,就是往这个链表插入我们的内核模块,像下面的图片一样,如果你想得极端一点,就有点像插队。
装载 HelloWorld 内核模块
linux@ubuntu:~/linux$ sudo insmod hello.ko [sudo] password for linux: linux@ubuntu:~/linux$ |
输入 sudo insmod hello.ko 后,需要输入 su 超级用户的密码,这个时候,我们理论上就会执行module_init ,会有一个打印出来,我们去哪里看这个打印了这样输入命令 dmesg |tail -5 查看后面 5 行日志。
linux@ubuntu:~/linux$ dmesg |tail -5 [ 1154.971812] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready [41991.859527] e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: None [41991.869932] IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready [41991.870367] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready [42035.102864] Hello, world linux@ubuntu:~/linux$ |
同样我们去卸载内核模块的时候,执行指令sudo rmmod hello.ko
linux@ubuntu:~/linux$ dmesg |tail -5 [41991.859527] e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: None [41991.869932] IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready [41991.870367] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready [42035.102864] Hello, world [42284.945557] Goodbye, cruel world linux@ubuntu:~/linux$ |
insmod 和rmmod 的流程图如下面的图片,我这个图片引用的是其他书籍里面的,有可能跟最新的内核有出入,但是大致不会相差太多,可以作为参考。
(内核模块加载的流程图)
Module_init
Linux 内核里面非常多的代码都是通过模块的方式加载的,但是每个模块的加载顺序都是不一样的,比如设备驱动,因为设备驱动是在系统启动完成后再进行加载的,属于偏后面一些,但是肯定有一些核心组件需要提前加载的(比如时钟,I2C core等),因为需要不同的加载顺序,就有了不同级别的xx_init级别。
在\include\linux\init.h里面定义了不同的级别函数,数字越低,代表的级别越高,越早加载。
/* * A "pure" initcall has no dependencies on anything else, and purely * initializes variables that couldn't be statically initialized. * * This only exists for built-in code, not for modules. * Keep main.c:initcall_level_names[] in sync. */ #define pure_initcall(fn) __define_initcall(fn, 0) #define core_initcall(fn) __define_initcall(fn, 1) #define core_initcall_sync(fn) __define_initcall(fn, 1s) #define postcore_initcall(fn) __define_initcall(fn, 2) #define postcore_initcall_sync(fn) __define_initcall(fn, 2s) #define arch_initcall(fn) __define_initcall(fn, 3) #define arch_initcall_sync(fn) __define_initcall(fn, 3s) #define subsys_initcall(fn) __define_initcall(fn, 4) #define subsys_initcall_sync(fn) __define_initcall(fn, 4s) #define fs_initcall(fn) __define_initcall(fn, 5) #define fs_initcall_sync(fn) __define_initcall(fn, 5s) #define rootfs_initcall(fn) __define_initcall(fn, rootfs) #define device_initcall(fn) __define_initcall(fn, 6) #define device_initcall_sync(fn) __define_initcall(fn, 6s) #define late_initcall(fn) __define_initcall(fn, 7) #define late_initcall_sync(fn) __define_initcall(fn, 7s) #define __initcall(fn) device_initcall(fn) #define __exitcall(fn) \ static exitcall_t __exitcall_##fn __exit_call = fn #define console_initcall(fn) \ static initcall_t __initcall_##fn \ __used __section(.con_initcall.init) = fn ...... /** * module_init() - driver initialization entry point * @x: function to be run at kernel boot time or module insertion * * module_init() will either be called during do_initcalls() (if * builtin) or at module insertion time (if a module). There can only * be one per module. */ #define module_init(x) __initcall(x); /** * module_exit() - driver exit entry point * @x: function to be run when driver is removed * * module_exit() will wrap the driver clean-up code * with cleanup_module() when used with rmmod when * the driver is a module. If the driver is statically * compiled into the kernel, module_exit() has no effect. * There can only be one per module. */ #define module_exit(x) __exitcall(x); |
可以看到 module_init 实际上是__define_initcall(fn, 6) 属于设备级别的初始化,我们之前一直在说Linux 内核支持动态模块加载,Linux内核的很多子系统都是通过模块化来设计的,既然有动态加载,那就必须有静态加载,如果需要内核静态加载的话,就必须把这些内核模块编译进内核里面,我们学习内核编译的时候,肯定还记得一个 [*]和[M],只要简单的配置一下,就决定了你这个模块是作为一个单独的模块还是作为内核的一个部分。
这个加载顺序我认为是非常有用的,我之前做项目的时候,我们同事有个需求,需要A驱动必须要在B驱动之前加载,而且尽可能早的加载,这时候,就用到了不同的优先级别的xx_init了。
Linux 内核模块的依赖安装(动态加载模块)
我好像在之前说过,Linux 内核是宏内核,内核模块之间是可以互相进行函数调用的,这样的耦合性是比较高的,所以呢,Linux 内核肯定是存在依赖关系的,就好像,我像喝水,然后我首先要找到一个杯子,然后还要找到饮水机,等等等等。
按照上面的命令,我们安装一个内核模块使用 insmod 命令,如果这个模块依赖另一个模块呢?
那就再需要用 insmod 安装另一个模块,这样看起来比较麻烦,所以就有了这个命令 modprobe,modprobe可以动态的安装内核模块,如果发现有依赖关系,就把需要依赖的内核模块也安装了。
提醒一下,初学者做这个的时候,肯定会遇到一些意想不到的问题,所以我也针对这些问题做了些总结,希望看完文章的同学不会踩坑。
Makefile 文件
$(info $(PWD)) $(info $(VER)) ifneq ($(KERNELRELEASE),) obj-m := hello.o else PWD := $(shell pwd) KVER := $(shell uname -r) KDIR := /lib/modules/$(KVER)/build all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules install: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules_install clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean endif |
源文件跟之前的HelloWorld一样,就不贴出来了,这里主要是多了一个 make install 编译,用来安装内核模块。
运行modprobe 之前,要先运行depmod -a来更新依赖文件
linux@ubuntu:/lib/modules$ uname -r 3.13.0-117-generic linux@ubuntu:/lib/modules$ cd /lib/modules/3.13.0-117-generic/ linux@ubuntu:/lib/modules/3.13.0-117-generic$ sudo depmod -a linux@ubuntu:/lib/modules/3.13.0-117-generic$ cat modules.dep |tail -2 kernel/lib/percpu_test.ko: extra/hello.ko: linux@ubuntu:/lib/modules/3.13.0-117-generic$ |
然后再执行 modprobe -v hello ,后面跟的是 hello 而不是 hello.ko ,如果你输入 hello.ko 的话,会一直提示找不到该模块。
linux@ubuntu:/lib/modules/3.13.0-117-generic$ modprobe -v hello linux@ubuntu:/lib/modules/3.13.0-117-generic$ dmesg|tail -2 [244664.594921] Goodbye, cruel world [245400.931435] Hello, world linux@ubuntu:/lib/modules/3.13.0-117-generic$ sudo modprobe -r hello linux@ubuntu:/lib/modules/3.13.0-117-generic$ dmesg|tail -2 [245400.931435] Hello, world [246484.772567] Goodbye, cruel world linux@ubuntu:/lib/modules/3.13.0-117-generic$ |
内核空间和用户空间
上面这两个名字有点拗口,也许可以这样说,内核地址和用户地址,内核态和用户态实际上就是通过地址来区分的。
我觉得作为初学者,把这个图片看明白,然后再把旁边的英文自己看明白,然后把他们吃透,多看几次,多理解几次,多问几次,这样之后再去看其他的内容。
如果把Linux 看作是一个一个家庭,一个家庭首先就需要有一个房子,这就是硬件设备,然后呢父母就是这个家庭的内核,孩子们就是这个家庭用户空间进程,父母需要为这个家庭支付房租,水电,买吃的,喝的,还有给孩子们煮饭,洗衣服等等。那孩子们做什么事情呢?孩子们就负责上学,负责玩,他们要吃了,就找父母去拿,想睡觉了,就去睡觉,总之,他们不是这个家庭的基础,如果内核空间没有了,这个家庭肯定就瓦解了,但是用户没有了,内核还是会存在,这就是我们所说的丁克家庭。
我举个例子,家庭里的小孩有一天想吃饭,好了,他把碗拿到桌子边上,敲着碗说,爸爸妈妈,爸爸妈妈,快给我吃饭,然后呢,CPU就需要跳转到内核态,让父母去给小孩准备食物,准备好了,小孩就可以吃饭了,这时候CPU又回到了用户态执行。
有一天,我很开心的在上网,然后又一张图片印入了我的眼帘,你应该猜到了,就是下面这张图片,这张图片非常完美的解释了用户空间和内核空间,作者给我们举得例子就是系统调用,系统调用了write的函数方法,在调用的过程中,CPU跳转到内核空间执行,函数执行结束后,又会跳转到用户空间执行。
构建自己的 Linux 内核
我们想学习 Linux 内核,一个最重要的条件是会构建自己的 Linux 内核,要不然就是只是一个空谈家,我的书籍也是希望给大家一个这样动手的条件,理论加实践相结合来渗透进入 Linux 内核,我在前面已经给大家提供了一个安装好的 ubuntu 系统,我们就用这个 ubuntu 开刀,扒出我们自己的 Linux 内核。
获取内核源码
sudo apt-cache search linux-source sudo apt-get install linux-source-3.2.0 |
第一句是查询 Linux 内核源码,就是说我们现在运行的这个 ubuntu的系统内核源码是哪个版本的,我们如果要开发自己的内核,必须要有完整的内核源码,我说的 ubuntu 指的是我上面给出链接的那个。
第二句是安装 Linux 内核源码,安装完后在 /usr/src 下面有有源码压缩包。
ubuntu 下的 Linux 内核源码位置
/usr/src |
我们安装了 ubuntu 下面就有了 linux 内核源码,但是这个源码需要我们自己解压出来,对应的应该是
linux@ubuntu:/usr/src$ ls linux-headers-3.13.0-117 linux-headers-3.13.0-32-generic linux-headers-3.13.0-117-generic linux-source-3.2.0 linux-headers-3.13.0-32 linux-source-3.2.0.tar.bz2 linux@ubuntu:/usr/src$ |
所以就是这个压缩包了
linux-source-3.2.0.tar.bz2 |
使用 make menuconfig 配置自己的内核
我们有了自己的源码,还需要一个 .config 来配置内核,世界上的硬件设备千千万万,我们就只有一个内核源码,他们最大的区别就是配置文件了,所以我们就需要一个配置文件,这一步也很简单。
使用 uname -a 查看内核版本号
root@ubuntu:/usr/src# uname -a Linux ubuntu 3.13.0-117-generic #164~precise1-Ubuntu SMP Mon Apr 10 16:16:25 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux root@ubuntu:/usr/src# |
把内核版本号下面的 .config 复制到内核源码下面
root@ubuntu:/usr/src# cp linux-headers-3.13.0-117-generic/.config linux-source-3.2.0/linux-source-3.2.0/ |
使用 make menuconfig 配置内核
这一步不要做任何修改,直接 exit 就可以了,可以执行看看配置的界面爽一把是可以的。
编译内核
执行 make bzImage -jN 编译内核,我截取了编译过程中的一些输出日志,这里的 N 可以指定并行编译的线程,但不限定是core的个数。
linux@ubuntu:/usr/src/linux-source-3.2.0/linux-source-3.2.0$sudo make bzImage -j6 [sudo] password for linux: CHK include/linux/version.h make[1]: Nothing to be done for `relocs'. CHK include/generated/utsrelease.h CALL scripts/checksyscalls.sh CHK include/generated/compile.h VDSOSYM arch/x86/vdso/vdso-syms.lds VDSOSYM arch/x86/vdso/vdso32-int80-syms.lds VDSOSYM arch/x86/vdso/vdso32-syscall-syms.lds VDSOSYM arch/x86/vdso/vdso32-sysenter-syms.lds VDSOSYM arch/x86/vdso/vdso32-syms.lds LD arch/x86/vdso/built-in.o LD arch/x86/built-in.o LD vmlinux.o MODPOST vmlinux.o GEN .version CHK include/generated/compile.h UPD include/generated/compile.h CC init/version.o LD init/built-in.o LD .tmp_vmlinux1 KSYM .tmp_kallsyms1.S AS .tmp_kallsyms1.o LD .tmp_vmlinux2 KSYM .tmp_kallsyms2.S AS .tmp_kallsyms2.o LD vmlinux SYSMAP System.map SYSMAP .tmp_System.map VOFFSET arch/x86/boot/voffset.h CC arch/x86/boot/version.o OBJCOPY arch/x86/boot/compressed/vmlinux.bin GZIP arch/x86/boot/compressed/vmlinux.bin.gz MKPIGGY arch/x86/boot/compressed/piggy.S AS arch/x86/boot/compressed/piggy.o LD arch/x86/boot/compressed/vmlinux ZOFFSET arch/x86/boot/zoffset.h OBJCOPY arch/x86/boot/vmlinux.bin AS arch/x86/boot/header.o LD arch/x86/boot/setup.elf OBJCOPY arch/x86/boot/setup.bin BUILD arch/x86/boot/bzImage Setup is 17052 bytes (padded to 17408 bytes). System is 4422 kB CRC e36fccb0 Kernel: arch/x86/boot/bzImage is ready (#2) linux@ubuntu:/usr/src/linux-source-3.2.0/linux-source-3.2.0$ |
编译 Linux 内核模块
我们执行 make modules - j4 编译内核模块,我们前面说了,Linux 内核下面都是按照模块化来编程的,我们需要把这些模块编译出来,然后再安装到现在运行的 ubuntu 系统上,就完成了Linux 内核的安装。
执行的时候有可能出现一些什么问题,找到 error 相关的 Log ,百度找一下答案就可以了,如下是我执行的结果
linux@ubuntu:/usr/src/linux-source-3.2.0/linux-source-3.2.0$ sudo make modules -j6 [sudo] password for linux: CHK include/linux/version.h make[1]: Nothing to be done for `relocs'. CHK include/generated/utsrelease.h CALL scripts/checksyscalls.sh Building modules, stage 2. MODPOST 2801 modules WARNING: modpost: Found 4 section mismatch(es). To see full details build your kernel with: 'make CONFIG_DEBUG_SECTION_MISMATCH=y' linux@ubuntu:/usr/src/linux-source-3.2.0/linux-source-3.2.0$ |
安装内核
先安装内核模块,使用这个骚命令make modules_install
$sudo make modules_install INSTALL /lib/firmware/keyspan_pda/keyspan_pda.fw INSTALL /lib/firmware/keyspan_pda/xircom_pgs.fw INSTALL /lib/firmware/cpia2/stv0672_vp4.bin INSTALL /lib/firmware/yam/1200.bin INSTALL /lib/firmware/yam/9600.bin DEPMOD 3.2.79 linux@ubuntu:/usr/src/linux-source-3.2.0/linux-source-3.2.0$ |
然后安装内核,使用这个命令make install
linux@ubuntu:/usr/src/linux-source-3.2.0/linux-source-3.2.0$ sudo make install sh /usr/src/linux-source-3.2.0/linux-source-3.2.0/arch/x86/boot/install.sh 3.2.79 arch/x86/boot/bzImage \ System.map "/boot" run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 3.2.79 /boot/vmlinuz-3.2.79 run-parts: executing /etc/kernel/postinst.d/initramfs-tools 3.2.79 /boot/vmlinuz-3.2.79 update-initramfs: Generating /boot/initrd.img-3.2.79 run-parts: executing /etc/kernel/postinst.d/pm-utils 3.2.79 /boot/vmlinuz-3.2.79 run-parts: executing /etc/kernel/postinst.d/update-notifier 3.2.79 /boot/vmlinuz-3.2.79 run-parts: executing /etc/kernel/postinst.d/zz-update-grub 3.2.79 /boot/vmlinuz-3.2.79 Generating grub.cfg ... Warning: Setting GRUB_TIMEOUT to a non-zero value when GRUB_HIDDEN_TIMEOUT is set is no longer supported. Found linux image: /boot/vmlinuz-3.13.0-117-generic Found initrd image: /boot/initrd.img-3.13.0-117-generic Found linux image: /boot/vmlinuz-3.13.0-32-generic Found initrd image: /boot/initrd.img-3.13.0-32-generic Found linux image: /boot/vmlinuz-3.2.79 Found initrd image: /boot/initrd.img-3.2.79 Found memtest86+ image: /boot/memtest86+.bin done linux@ubuntu:/usr/src/linux-source-3.2.0/linux-source-3.2.0$ |
查看新安装的内核
内核安装完成之后,我们怎么判断新安装的内核是自己编译出来的呢?这个操作非常关键,很多码农写了代码烧录后,发现执行的情况不如自己的预期,也没有去查看有没有烧录成功,就不断的修改代码排查,这个是非常低级的错误。
生成initrd.img文件,使用命令update-initramfs。
#cd /lib/modules/3.2.79 root@ubuntu:/lib/modules# update-initramfs -c -k 3.2.79 update-initramfs: Generating /boot/initrd.img-3.2.79 root@ubuntu:/lib/modules# |
使用 update-grub2 更新新的内核,这个命令会去查找当前存在的新内核,然后用新的内核去安装引导。
linux@ubuntu:/usr/src/linux-source-3.2.0/linux-source-3.2.0$ sudo update-grub2 Generating grub.cfg ... Warning: Setting GRUB_TIMEOUT to a non-zero value when GRUB_HIDDEN_TIMEOUT is set is no longer supported. Found linux image: /boot/vmlinuz-3.13.0-117-generic Found initrd image: /boot/initrd.img-3.13.0-117-generic Found linux image: /boot/vmlinuz-3.13.0-32-generic Found initrd image: /boot/initrd.img-3.13.0-32-generic Found linux image: /boot/vmlinuz-3.2.79 Found initrd image: /boot/initrd.img-3.2.79 Found memtest86+ image: /boot/memtest86+.bin done linux@ubuntu:/usr/src/linux-source-3.2.0/linux-source-3.2.0$ |
查看重启前运行的内核版本
linux@ubuntu:/usr/src/linux-source-3.2.0/linux-source-3.2.0$ uname -a Linux ubuntu 3.13.0-117-generic #164~precise1-Ubuntu SMP Mon Apr 10 16:16:25 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux |
重启系统 shutdown -r now
这时候重启,你应该是看不到我们的内核版本的,因为我们用的是 ubuntu ,还有一些诡异的地方需要设置,其中就需要设置的是 /etc/grub.d/00_header
vim /etc/grub.d/00_header 修改style="countdown"为style="menu",这样我们在开机的时候就可以选择我们系统里面的内核了,要不然就一直是一个默认的内核。
通过上面的骚操作之后,我们再重启,重启的过程中会提示我们选择内核版本,我们记得别选原来的内核版本了,选一个我们原来不认识的内核版本,然后自信的按下回车键,等待一下。
这时候,我们再来查看一下内核版本
root@ubuntu:~# uname -a Linux ubuntu 3.2.79 #4 SMP Wed Jun 12 19:33:11 PDT 2019 x86_64 x86_64 x86_64 GNU/Linux root@ubuntu:~# |
总结
内核模块是基础,是我们开发内核的基础,但是又因为基础的东西难度不是非常大,所以写的也比较轻松,不过学习的时候,不应该怠慢,该敲代码的时候,就敲代码,该看书的时候,就看看书,有理解不了的,就提下问题。扎实的基础就应该在学习的时候边动手边思考,加深自己的印象。
原创文章写起来非常不容易,辛苦是在所难免的,因为从后面开始,没一个章节的篇幅就增加很多,所以我会拆开来写,比如一个章节我发几次。
我的文章后面默认打开赞赏,原因是我的原创名字和赞赏账号一样,也是因为懒的原因,这样我只需要点击一下就可以填好原创的作者,打开赞赏不是想大家给我多少钱,也是想得到读者们的肯定,如果是学生还不如留着钱去追下妹子,大学美好时光来之不易,该浪荡的时候就不要浪费了,但是觉得不错,有帮助的,可以给个转发支持一下,感激不尽。
下一章节会讲内核调试的一些方法,包括GDB,/proc文件系统调试,还有加dump_stack等手段来调试等等
============================