Linux内核学习(八):linux内核配置与模块

简介: Linux内核学习(八):linux内核配置与模块

Linux内核学习(八):linux内核配置与模块

内核的配置是开发的第一步,前面我kconfig那篇讲了内核这些配置怎么生成的。当时也提到了内核配置的一个图形化的界面,这里再来啰嗦几句。

本文内容全部来自《奔跑吧 linux内核》

1、内核配置工具

(1)make config

这是基于文本的一种传统的配置方式。它会为内核支持的每一个特性向用户提问,如果用户输入“y”,则把该特性编译进内核;如果输入“m”,则把该特性变成以模块;如果输入为“n”,则表示不编译该特性。

(2)make oldconfig

make oldconfig和make config很类似,也是基于文本的配置工具,只不过它是在现有的内核配置文件的基础上建立一个新的配置文件,在有新的配置选项时会向用户提问。

(3)make menuconfig

make menuconfig是一种基于文本模式的图形用户界面,用户可以通过移动光标来浏览内核支持的特性。

在那篇文章里写到了上述几种内核内置工具最终会在 Linux 内核源代码的根目录下生成一个隐藏文件,即.config文件,这个文件包含了内核的所有配置信息。下面是一个.config文件的例子。

.config 文件的每个配置选项都以“CONFIG_”字段开始,后面的 y 表示内核会把这个特性静态编译进内核,m表示这个特性会被编译成内核模块。

如果不需要编译到内核中,就要在前面使用“#”注释,并在后面用“is not set”来标识。

.config文件通常有几千行,每一行都通过手工输入显得不现实。

config太多,实际项目中如何生成这个.config文件呢?

(1)基于开发板

使用板级的配置文件一些芯片公司通常会提供基于某款 SoC 芯片的开发板,读者可以基于此开发板来快速开发产品原型。

芯片公司同时会提供板级开发板包,其中包含移植好的Linux内核。以ARM公司的Vexpress板子为例,该板子对应Linux内核的配置文件被放在arch/arm/configs目录中。如图3.3所示,arch/arm/configs目录下包含了众多的ARM板子的配置文件。

ARM Vexpress板子对应的config配置文件是vexpress_defconfig文件,可以通过下面命令来配置内核。

$ export ARCH=arm
$ export CROSS_COMPILE=arm-linux-gnueabi
$ make vexpress_defconfig

(2)使用系统的配置文件

当我们需要编译电脑中的Linux系统内核时,可以使用系统自带的config文件。

以优麒麟18.04系统为例,boot目录下面有一个config-4.15.0-22-generic文件。当我们想编译一个新内核(如 Linux 4.17 内核)时,可以通过如下命令来生成一个新的.config文件。

$ cd linux-4.17
$ cp /boot/config-4.15.0-22-generic ./.config

下面紧接着我们来看看内核模块

2、内核模块

Linux内核采用宏内核架构,即操作系统的大部分功能都在内核中实现,比如进程管理、内存管理、进程调度、设备管理等,并且都在特权模式下(内核空间)运行。

而与之相反的另一种流行的架构是微内核架构,它把操作系统最基本的功能放入内核中,而其他大部分的功能(如设备驱动等)都放到非特权模式下,这种架构有天生优越的动态扩展性。

Linux的这种宏内核可以理解为一个完全静态的内核,那如何实现运行时内核的动态扩展呢?

其实Linux内核在发展过程中很早就引入了内核模块这个机制,内核模块全称Loadable Kernel Module(LKM)。

在内核运行时加载一组目标代码来实现某个特定的功能,这样在实际使用Linux的过程中可以不需要重新编译内核代码来实现动态扩展。

Linux内核通过内核模块来实现动态添加和删除某个功能。

2.1、从一个内核模块开始

上面我们知道了模块的相关知识,现在我们来整一个模块注册进内核看看,体验一下这个流程。

a. 准备内核模块代码

0   #include <linux/init.h>
1   #include <linux/module.h>
2
3   static int __init my_test_init(void)
4   {
5     printk("my first kernel module init\n");
6     return 0;
7   }
8
9   static void __exit my_test_exit(void)
10  {
11    printk("goodbye\n");
12  }
13
14  module_init(my_test_init);
15  module_exit(my_test_exit);
16
17  MODULE_LICENSE("GPL");
18  MODULE_AUTHOR("Ben Shushu");
19  MODULE_DESCRIPTION("my test kernel module");
20  MODULE_ALIAS("mytest");

这个简单的内核模块只有两个函数:

  • 一个是 my_test_init()函数,输出一句话“my first kernel module init”;
  • 另一个是my_test_exit()函数,输出“goodbye”。

麻雀虽小,五脏俱全,这是一个可以运行的内核模块。

第0行和第1行包含了两个Linux内核的头文件,其中<linux/init.h>头文件对应的是内核源代码的 include/linux/init.h 文件

在这个头文件中包含了第 14 行和第 15 行中的module_init()和 module_exit()函数的声明。

<linux/module.h>头文件对应的是内核源代码的include/linux/module.h文件,包含了第17~20行的MODULE_AUTHOR()这些宏的声明。

**第 14 行的 module_init()告诉内核这是该模块的入口。**内核在各个模块初始化时有一个优先级顺序。对于驱动模块来说,它的优先级不是特别高,而且内核把所有模块的初始化函数都存放在一个特别的段中来管理。

第15行的module_exit()宏告诉内核这个模块的退出函数是my_test_exit()。

第3~7行是该内核模块初始化函数,我们在这个例子中仅仅用printk输出函数往终端中输出一句话。printk是类似C语言库中的printf()输出函数,但是它增加了输出级别的支持。这个函数在内核模块被加载时运行.

(可以使用insmod命令来加载一个内核模块。)

第9~12行是该内核模块的退出函数,在这个例子中我们也仅仅用printk输出一句话,标记卸载了该模块

(可以使用rmmod命令卸载一个内核模块)

(insmod和rmmod两个的意思就是咱们可以不只是说说prink,可是直接干。)

第 17~20 行,MODULE_LICENSE()表示这个模块代码接受的软件许可协议。Linux 内核是一个使用GPL V2的开源项目,这要求所有使用和修改了Linux内核源代码的个人或者公司都有义务把修改后的源代码公开,也就是一个强制的开源协议,因此一般我们编写的驱动代码中都需要显式地申明和遵循这个协议。

MODULE_AUTHOR()用来描述该模块的作者信息,可以包括作者的姓名和邮箱等。

MODULE_DESCRIPTION()用来简单描述该模块的用途或者功能介绍。

**MODULE_ALIAS()为用户空间提供一个合适的别名。**下面我们来看如何编译这个内核模块。在优麒麟Linux上编译该内核模块,下面是编写内核模块的Makefile文件。

现在创建好了咱们的模块,下一步就是来编写内核模块的makefile文件

b. 内核模块的makefile文件

0   BASEINCLUDE ?= /lib/modules/`uname -r`/build
1
2   mytest-objs := my_test.o
3   obj-m  :=  mytest.o
4
5   all :
6   $(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;
7
8   clean:
9   $(MAKE) -C $(BASEINCLUDE) SUBDIRS=$(PWD) clean;
10  rm -f *.ko;

第0行的****BASEINCLUDE指向正在运行Linux的内核编译目录,对于编译优麒麟Linux中运行的内核模块,我们需要指定到当前系统对应的内核中。一般来说,Linux系统的内核模块都会安装到/lib/modules这个目录下,通过“uname -r”命令可以找到对应的内核版本。

首先通过“uname -r”来查看当前系统的内核,比如我的系统里面装了4.15.0-20-generic的内核版本,这个内核版本的头文件放在/usr/src/linux-headers-4.15.0-20-generic目录中。

第2行表示该内核模块需要哪些目标文件,格式是:<模块名>-objs := <目标文件>.o

第3行表示要生成的模块。注意,模块名字不能和目标文件名相同。格式是: obj-m :=<模块名>.o

第5~6行表示要编译执行的动作。

第8~10行表示执行make clean需要的动作。

然后在终端中输入make命令来执行编译。$ make编译完成之后会生成mytest.ko文件。

我们可以通过file命令检查编译的模块是否正确,可以看到变成x86-64架构的ELF文件,说明已经编译成功了。

另外,也可以通过modinfo命令进一步做检查。

c.验证

接下来就可以在优麒麟Linux机器上验证我们的内核模块了

$sudo insmod mytest.ko  (安装模块)

你会发现没有输出,别着急,因为例子中的输出函数 printk()的默认输出等级,可以使用dmesg命令查看内核的打印信息。

$dmesg…
[258.575353] my first kernel module init

另外,你可以通过lsmod命令查看当前mytest模块是否已经被加载到系统中,它会显示模块之间的依赖关系。

加载模块之后,系统会在/sys/modules目录下新建一个目录,比如对于mytest模块会建一个名为mytest的目录。

figo@figo-OptiPlex-9020:/sys/module/mytest$ tree -a

如果需要卸载模块,可以通过rmmod命令来实现。

d. linux模块小结

模块加载函数:加载模块时,该函数会被自动执行,通常做一些初始化工作。

模块卸载函数:卸载模块时,该函数也会被自动执行,做一些清理工作。

模块许可声明:内核模块必须声明许可证,否则内核会发出被污染的警告。

模块参数:根据需求来添加,为可选项。

模块作者和描述声明:一般都需要完善这些信息。

模块导出符号:根据需求来添加,为可选项。

2. 模块参数传递

内核模块作为一个可扩展的动态模块,为Linux内核提供了灵活性。但是有时我们需要根据不同的应用场景给内核模块传递不同的参数,Linux内核提供一个宏来实现模块的参数传递。

#define module_param(name, type, perm)        \
module_param_named(name, name, type, perm)
#define MODULE_PARM_DESC(_parm, desc) \
__MODULE_INFO(parm, _parm, #_parm ":" desc)

module_param()宏由3个参数组成,name表示参数名,type表示参数类型,perm表示参数的读写等权限。

MODULE_PARM_DESC()宏为这个参数的简单说明,参数类型可以是byte、short、ushort、int、uint、long、ulong、char和bool等类型。

perm指定在sysfs中相应文件的访问权限,如设置为0表示不会出现在sysfs文件系统中;如设置成S_IRUGO(0444)可以被所有人读取,但是不能修改;如设置成S_IRUGO|S_IWUSR(0644),说明可以让root权限的用户修改这个参数。

(这里你可能有点懵,没事我们接着看看下面的栗子会好点)

这个例子定义了一个模块参数debug,类型是int,初始化值为1,权限访问为0644。

也就是说root权限用户可以修改这个值,这个参数的用途是打开调试信息。(参数说明的意思)

其实这是一个比较常用的内核调试方法,可以通过模块参数使用调试功能。通过debug=1 ,打开了调试功能。

下面这个例子定义了两个内核参数,一个是debug,另一个是静态全局变量mytest。

当通过“insmod mymodule.ko mytest=200”命令来加载模块时,可以看到终端里输出为:

(这里也可以安装)

还可以通过调试参数来关闭和打开调试信息。

在/sys/module/mymodule/parameters目录下面可以看到新增的两个参数。

具体更多地想法和疑惑,去实际操作一下会有深的体会哦

3、符号共享

我们在为一个设备编写驱动程序时,会把驱动按照功能分成好几个内核模块,这些内核模块之间有一些接口函数需要相互调用,这怎么实现呢?(锁一个驱动是很多模块的组合)

Linux内核为我们提供两个宏来解决这个问题。

EXPORT_SYMBOL( )
EXPORT_SYMBOL_GPL( )

EXPORT_SYMBOL()把函数或者符号对全部内核代码公开,也就是将一个函数以符号的方式导出给内核中的其他模块使用

EXPORT_SYMBOL_GPL()只能包含GPL许可的模块,内核核心的大部分模块导出来的符号都是使用GPL()这种形式的。如果要使用EXPORT_SYMBOL_GPL()导出函数,那么需要显式地通过模块申明为“GPL”,如MODULE_LICENSE(“GPL”)。

内核导出的符号表可以通过/proc/kallsyms来查看。

这些信息包含的内容:

  • 第1列显示的是该符号在内核地址空间的地址;
  • 第2列是符号属性,比如T表示该符号在text段中;
  • 第3列表示符号的字符串,也就是EXPORT_SYMBOL()导出来的符号;
  • 第4列显示哪些内核模块在使用这些符号。

相信到了这一步其实懵懵懂懂的应该具有一些能力去自己实现一些模块插到内核中了,想想自己竟然能改变内核了,有没有很兴奋。

目录
相关文章
|
10月前
|
安全 网络协议 Linux
深入理解Linux内核模块:加载机制、参数传递与实战开发
本文深入解析了Linux内核模块的加载机制、参数传递方式及实战开发技巧。内容涵盖模块基础概念、加载与卸载流程、生命周期管理、参数配置方法,并通过“Hello World”模块和字符设备驱动实例,带领读者逐步掌握模块开发技能。同时,介绍了调试手段、常见问题排查、开发规范及高级特性,如内核线程、模块间通信与性能优化策略。适合希望深入理解Linux内核机制、提升系统编程能力的技术人员阅读与实践。
902 1
|
8月前
|
存储 Linux 开发工具
Linux环境下使用Buildroot配置软件包
使用Buildroot可以大大简化嵌入式Linux系统的开发和维护工作,但它需要对Linux系统和交叉编译有深入的理解。通过上述步骤,可以有效地配置和定制软件包,为特定的嵌入式应用构建高效、稳定的系统。
925 11
|
10月前
|
监控 Ubuntu Linux
什么Linux,Linux内核及Linux操作系统
上面只是简单的介绍了一下Linux操作系统的几个核心组件,其实Linux的整体架构要复杂的多。单纯从Linux内核的角度,它要管理CPU、内存、网卡、硬盘和输入输出等设备,因此内核本身分为进程调度,内存管理,虚拟文件系统,网络接口等4个核心子系统。
1031 0
|
10月前
|
Web App开发 缓存 Rust
|
10月前
|
Ubuntu 安全 Linux
Ubuntu 发行版更新 Linux 内核,修复 17 个安全漏洞
本地攻击者可以利用上述漏洞,攻击 Ubuntu 22.10、Ubuntu 22.04、Ubuntu 20.04 LTS 发行版,导致拒绝服务(系统崩溃)或执行任意代码。
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
648 4
|
机器学习/深度学习 人工智能 负载均衡
深度解析:Linux内核调度策略的演变与优化
【5月更文挑战第30天】 随着计算技术的不断进步,操作系统的性能调优成为了提升计算机系统效率的关键。在众多操作系统中,Linux因其开源和高度可定制性而备受青睐。本文将深入剖析Linux操作系统的内核调度策略,追溯其历史演变过程,并重点探讨近年来为适应多核处理器和实时性要求而产生的调度策略优化。通过分析比较不同的调度算法,如CFS(完全公平调度器)、实时调度类和批处理作业的调度需求,本文旨在为系统管理员和开发者提供对Linux调度机制深层次理解,同时指出未来可能的发展趋势。
|
缓存 并行计算 Linux
深入解析Linux操作系统的内核优化策略
本文旨在探讨Linux操作系统内核的优化策略,包括内核参数调整、内存管理、CPU调度以及文件系统性能提升等方面。通过对这些关键领域的分析,我们可以理解如何有效地提高Linux系统的性能和稳定性,从而为用户提供更加流畅和高效的计算体验。
707 24
|
缓存 监控 网络协议
Linux操作系统的内核优化与实践####
本文旨在探讨Linux操作系统内核的优化策略与实际应用案例,深入分析内核参数调优、编译选项配置及实时性能监控的方法。通过具体实例讲解如何根据不同应用场景调整内核设置,以提升系统性能和稳定性,为系统管理员和技术爱好者提供实用的优化指南。 ####