注:本文为 “Linux 内核模块 | 管理 / 分析 / 参数说明” 相关文章合辑。
未整理去重。
【Linux 驱动】Linux 内核模块
沧海一笑 - dj 于 2024-04-26 16:16:24 发布
1. 概述
在 Linux 系统中,设备驱动会以内核模块的形式出现,学习 Linux 内核模块编程是驱动开发的先决条件。第一次接触 Linux 内核模块,我们将围绕着“Linux 内核模块是什么”“Linux 内核模块的工作原理”以及“我们该怎么使用 Linux 内核模块”这样的思路一起走进 Linux 内核世界。大致分为四个部分:
- 内核模块的概念:内核模块是什么东西?为什么引入内核模块这一机制?
- 内核模块的原理:内核模块在内核中的加载、卸载过程,深入剖析内核模块如何导出符号。
- helloworld 实验:理解内核模块的代码框架和原理,写一个属于自己的模块,以及模块的使用方法等。
- 内核模块传参与符号共享实验:理解内核模块的参数模式、符号共享,验证内核模块的运行机制。
2. 什么是内核
内核是一个操作系统的核心,是基于硬件的第一层软件扩充,提供操作系统的最基本功能,是操作系统工作的基础,决定着整个操作系统的性能和稳定性。
内核按照体系结构分为两类:微内核(Micro Kernel)和宏内核(Monolithic Kernel)。在微内核架构中,内核只提供操作系统核心功能,如实现进程管理、存储器管理、进程间通信、I/O 设备管理等,而其他的应用层 IPC、文件系统功能、设备驱动模块则不被包含到内核功能中,属于微内核之外的模块,所以针对这些模块的修改不会影响到微内核的核心功能。微内核具有动态扩展性强的优点。Windows 操作系统、华为的鸿蒙操作系统就属于这类微内核架构。
而宏内核架构是将上述包括微内核以及微内核之外的应用层 IPC、文件系统功能、设备驱动模块都编译成一个整体。其优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。Linux 操作系统正是采用了宏内核结构。为了解决这一缺点,Linux 中引入了内核模块这一机制。

3. 内核模块机制引入
3.1 内核模块引入原因
Linux 是一个跨平台的操作系统,支持众多的设备,在 Linux 内核源码中有超过 50% 的代码都与设备驱动相关。Linux 为宏内核架构,如果开启所有的功能,内核就会变得十分臃肿。内核模块就是实现了某个功能的一段内核代码,在内核运行过程中,可以加载这部分代码到内核中,从而动态地增加了内核的功能。基于这种特性,我们进行设备驱动开发时,以内核模块的形式编写设备驱动,只需要编译相关的驱动代码即可,无需对整个内核进行编译。
3.2 内核模块引入好处
内核模块的引入不仅提高了系统的灵活性,对于开发人员来说更是提供了极大的方便。在设备驱动的开发过程中,我们可以随意将正在测试的驱动程序添加到内核中或者从内核中移除,每次修改内核模块的代码不需要重新启动内核。在开发板上,我们也不需要将内核模块程序,或者说设备驱动程序的 ELF 文件存放在开发板中,免去占用不必要的存储空间。当需要加载内核模块的时候,可以通过挂载 NFS 服务器,将存放在其他设备中的内核模块,加载到开发板上。在某些特定的场合,我们可以按照需要加载 / 卸载系统的内核模块,从而更好地为当前环境提供服务。
3.3 内核模块的定义和特点
了解了内核模块引入以及带来的诸多好处,我们可以在头脑中建立起对内核模块的初步认识,下面让我们给出内核模块的具体定义:内核模块全称 Loadable Kernel Module (LKM),是一种在内核运行时加载一组目标代码来实现某个特定功能的机制。
模块是具有独立功能的程序,它可以被单独编译,但不能独立运行,在运行时它被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不一样的。模块由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序和其他内核上层功能。因此内核模块具备如下特点:
- 模块本身不被编译入内核映像,这控制了内核的大小。
- 模块一旦被加载,它就和内核中的其他部分完全一样。
有了内核模块的概念,下面我们一起深入了解内核模块的工作机制吧。
4. 内核模块的工作机制
我们编写的内核模块,经过编译,最终形成以 .ko 为后缀的 ELF 文件。我们可以使用 file 命令来查看它。
deng@local:~/kernel/bak/5th/1module$ file test.ko
test.ko: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), BuildID [sha1]=ccde1e3a7d801e3320877ae06d126f2145e3fe46, with debug_info, not stripped
deng@local:~/kernel/bak/5th/1module$
那么这样的文件是如何被内核一步一步拿到并且很好地工作的呢?为了便于我们更好地理解内核模块的加载 / 卸载过程,可以先跟我一起学习 ELF 文件格式,了解 .ko 究竟是怎么一回事儿。再一同去看看内核源码,探究内核模块加载 / 卸载,以及符号导出的经过。
4.1 内核模块详细加载 / 卸载过程
ko 文件的文件格式
ko 文件在数据组织形式上是 ELF (Executable And Linking Format) 格式,是一种普通的可重定位目标文件。这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类。
ELF 文件格式的可能布局如下图。

文件开始处是一个 ELF 头部 (ELF Header),用来描述整个文件的组织,这些信息独立于处理器,也独立于文件中的其余内容。我们可以使用 readelf 工具查看 ELF 文件的头部详细信息。
deng@local:~/kernel/bak/5th/1module$ readelf -h test.ko
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: AArch64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 175504 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 27
deng@local:~/kernel/bak/5th/1module$
程序头部表 (Program Header Table) 是一个数组结构,它的每一个元素的数据结构如下:
- 每个数组元素表示一个“段”:包含一个或者多个“节区”,程序头部仅对于可执行文件和共享目标文件有意义。
- 其他信息:系统准备程序执行所必需的其他信息。
节区头部表 / 段表 (Section Header Table):ELF 文件中有很多各种各样的段,这个段表 (Section Header Table) 就是保存这些段的基本属性的结构,ELF 文件的段结构就是由段表决定的,编译器、链接器、装载器都是依靠段表来定位和访问各个段的属性的,包含了描述文件节区的信息。
ELF 头部中:
e_shoff:给出从文件头到节区头部表格的偏移字节数。e_shnum:给出表格中条目数目。e_shentsize:给出每个项目的字节数。
从这些信息中可以确切地定位节区的具体位置、长度。和程序头部表一样,每一项节区在节区头部表格中都存在着一项元素与它对应,因此可知,这个节区头部表格为一连续的空间,每一项元素为一结构体(思考这节开头的那张节区和节区头部的示意图)。
我们可以加上 -S 参数读取 ELF 文件的节区头部表的详细信息。
deng@local:~/kernel/bak/5th/1module$ readelf -S test.ko
There are 30 section headers, starting at offset 0x2ad90:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.gnu.bu [...] NOTE 0000000000000000 00000040
0000000000000024 0000000000000000 A 0 0 4
[ 2] .text PROGBITS 0000000000000000 00000064
0000000000000000 0000000000000000 AX 0 0 1
[ 3] .init.text PROGBITS 0000000000000000 00000068
0000000000000028 0000000000000000 AX 0 0 8
[ 4] .rela.init.text RELA 0000000000000000 000171a0
0000000000000030 0000000000000018 I 28 3 8
[ 5] .exit.text PROGBITS 0000000000000000 00000090
0000000000000020 0000000000000000 AX 0 0 8
[ 6] .rela.exit.text RELA 0000000000000000 000171d0
0000000000000030 0000000000000018 I 28 5 8
[ 7] .modinfo PROGBITS 0000000000000000 000000b0
0000000000000059 0000000000000000 A 0 0 8
[ 8] .rodata.str1.1 PROGBITS 0000000000000000 00000109
0000000000000021 0000000000000001 AMS 0 0 1
[ 9] .data PROGBITS 0000000000000000 0000012a
0000000000000000 0000000000000000 WA 0 0 1
[10] .gnu.linkonc [...] PROGBITS 0000000000000000 00000140
0000000000000300 0000000000000000 WA 0 0 64
[11] .rela.gnu.li [...] RELA 0000000000000000 00017200
0000000000000030 0000000000000018 I 28 10 8
[12] .bss NOBITS 0000000000000000 00000440
0000000000000000 0000000000000000 WA 0 0 1
[13] .debug_info PROGBITS 0000000000000000 00000440
000000000000c57a 0000000000000000 0 0 1
[14] .rela.debug_info RELA 0000000000000000 00017230
00000000000138f0 0000000000000018 I 28 13 8
[15] .debug_abbrev PROGBITS 0000000000000000 0000c9ba
00000000000006d5 0000000000000000 0 0 1
[16] .debug_aranges PROGBITS 0000000000000000 0000d08f
0000000000000060 0000000000000000 0 0 1
[17] .rela.debug_[...] RELA 0000000000000000 0002ab20
0000000000000060 0000000000000018 I 28 16 8
[18] .debug_ranges PROGBITS 0000000000000000 0000d0ef
0000000000000030 0000000000000000 0 0 1
[19] .rela.debug_[...] RELA 0000000000000000 0002ab80
0000000000000060 0000000000000018 I 28 18 8
[20] .debug_line PROGBITS 0000000000000000 0000d11f
0000000000000e34 0000000000000000 0 0 1
[21] .rela.debug_line RELA 0000000000000000 0002abe0
0000000000000030 0000000000000018 I 28 20 8
[22] .debug_str PROGBITS 0000000000000000 0000df53
0000000000008d1f 0000000000000001 MS 0 0 1
[23] .comment PROGBITS 0000000000000000 00016c72
000000000000005c 0000000000000001 MS 0 0 1
[24] .note.GNU-stack PROGBITS 0000000000000000 00016cce
0000000000000000 0000000000000000 0 0 1
[25] .debug_frame PROGBITS 0000000000000000 00016cd0
0000000000000060 0000000000000000 0 0 8
[26] .rela.debug_frame RELA 0000000000000000 0002ac10
0000000000000060 0000000000000018 I 28 25 8
[27] .shstrtab STRTAB 0000000000000000 0002ac70
000000000000011d 0000000000000000 0 0 1
[28] .symtab SYMTAB 0000000000000000 00016d30
00000000000003a8 0000000000000018 29 35 8
[29] .strtab STRTAB 0000000000000000 000170d8
00000000000000c1 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
deng@local:~/kernel/bak/5th/1module$
节区头部表中又包含了很多子表的信息,我们简单地来看两个。
重定位表
重定位表(.rel.text)位于段表之后,它的类型为(sh_type)为“SHT_REL”,即重定位表(Relocation Table)。链接器在处理目标文件时,必须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置,这些重定位信息都记录在 ELF 文件的重定位表里面。对于每个需要重定位的代码段或者数据段,都会有一个相应的重定位表。一个重定位表同时也是 ELF 的一个段,这个段的类型(sh_type)就是“SHT_REL”。
读取重定位表。
deng@local:~/kernel/bak/5th/1module$ readelf -r test.ko
重定位节 '.rela.init.text' at offset 0x171a0 contains 2 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
00000000000c 00260000011b R_AARCH64_CALL26 0000000000000000 printk + 0
000000000020 000600000101 R_AARCH64_ABS64 0000000000000000 .rodata.str1.1 + 0
重定位节 '.rela.exit.text' at offset 0x171d0 contains 2 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
00000000000c 00260000011b R_AARCH64_CALL26 0000000000000000 printk + 0
000000000018 000600000101 R_AARCH64_ABS64 0000000000000000 .rodata.str1.1 + 10
重定位节 '.rela.gnu.linkonce.this_module' at offset 0x17200 contains 2 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000170 002500000101 R_AARCH64_ABS64 0000000000000000 init_module + 0
0000000002e8 002400000101 R_AARCH64_ABS64 0000000000000000 cleanup_module + 0
重定位节 '.rela.debug_info' at offset 0x17230 contains 3338 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000006 000b00000102 R_AARCH64_ABS32 0000000000000000 .debug_abbrev + 0
00000000000c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 172c
000000000011 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 45a4
000000000015 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 330d
000000000019 000d00000102 R_AARCH64_ABS32 0000000000000000 .debug_ranges + 0
000000000025 000e00000102 R_AARCH64_ABS32 0000000000000000 .debug_line + 0
00000000002c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 39b9
000000000033 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 23e3
00000000003a 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3070
00000000003f 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 43bb
00000000004c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2049
000000000051 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3e7a
000000000063 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 44e5
000000000070 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 465a
00000000007a 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 4032
000000000087 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 16
00000000008c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 24
000000000099 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3bb9
0000000000f0 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + dc
000000000111 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 353a
000000000123 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3ea4
000000000138 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3e1a
000000000145 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3293
00000000014f 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 413d
00000000015a 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 259f
000000000165 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3be1
000000000170 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 257
00000000017b 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3005
000000000186 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3e69
0000000001a1 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2971
0000000001ac 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2dcb
0000000001b7 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 35fe
0000000001c2 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 16d3
0000000001cd 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 24a8
0000000001de 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2d2
0000000001e9 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3b95
0000000001f4 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 390b
0000000001ff 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 886
00000000020c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 1a56
000000000211 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2add
00000000021c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2043
000000000227 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3dd
000000000232 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + a82
字符串表
ELF 文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示比较困难,一种常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。一般字符串表在 ELF 文件中也以段的形式保存,常见的段名为 .strtab(String Table 字符串表)或者 .shstrtab(Section Header String Table 段字符串表)。
读取节区字符串表。
deng@local:~/kernel/bak/5th/1module$ readelf -p 28 test.ko
String dump of section '.symtab':
[ 218]
[ 270] "
[ 288] 6
[ 2a0] J
[ 2d0] _
[ 308] (
[ 318] j
[ 320] (
[ 330] {
[ 338] 1
[ 340] (
deng@local:~/kernel/bak/5th/1module$
ELF 文件格式相关的知识比较晦涩,我们只需要大概了解,有个初步印象即可,主要是为了理解内核模块的加载卸载以及符号导出,在后面提到相关名词不至于太陌生。
4.2 内核模块加载过程
我们了解了 .ko 内核模块文件的一些格式内容之后,我们可以知道内核模块其实也是一段经过特殊加工的代码,那么既然是加工过的代码,内核就可以利用到加工时留在内核模块里的信息,对内核模块进行利用。所以我们就可以接着了解内核模块的加载过程了。
首先 insmod 会通过文件系统将 .ko 模块读到用户空间的一块内存中,然后执行系统调用 sys_init_module() 解析模组。这时,内核在 vmalloc 区分配与 .ko 文件大小相同的内存来暂存 .ko 文件。暂存好之后解析 .ko 文件,将文件中的各个 section 分配到 init 段和 core 段,在 modules 区为 init 段和 core 段分配内存,并把对应的 section copy 到 modules 区最终的运行地址。经过 relocate 函数地址等操作后,就可以执行 .ko 的 init 操作了,这样就完成了一个 .ko 的加载流程。同时,init 段会被释放掉,仅留下 core 段来运行。
sys_init_module()(内核源码 /kernel/module.c)
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
err = may_init_module();
if (err)
return err;
pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
umod, len, uargs);
err = copy_module_from_user(umod, len, &info);
if (err)
return err;
return load_module(&info, uargs, 0);
}
- 第 14 行:通过
vmalloc在vmalloc区分配内存空间,将内核模块 copy 到此空间,info->hdr直接指向此空间首地址,也就是.ko的 ELF header。 - 第 18 行:然后通过
load_module()进行模块加载的核心处理,在这里完成了模块的搬移、重定向等艰苦的过程。
下面是 load_module() 的详细过程,代码已经被简化,主要包含 setup_load_info() 和 layout_and_allocate()。
load_module() 函数(内核源码 /kernel/module.c)
/* 分配并加载模块 */
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
{
struct module *mod;
long err = 0;
char *after_dashes;
...
err = setup_load_info(info, flags);
...
mod = layout_and_allocate(info, flags);
...
}
- 第 9 行:
setup_load_info()加载struct load_info和struct module,rewrite_section_headers,将每个 section 的sh_addr修改为当前镜像所在的内存地址,section 名称字符串表地址的获取方式是从 ELF 头中的e_shstrndx获取到节区头部字符串表的标号,找到对应 section 在 ELF 文件中的偏移,再加上 ELF 文件起始地址就得到了字符串表在内存中的地址。 - 第 11 行:在
layout_and_allocate()中,layout_sections()负责将 section 归类为core和init这两大类,为.ko的第二次搬移做准备。move_module()把.ko搬移到最终的运行地址。内核模块加载代码搬运过程到此就结束了。
内核模块要工作起来还得进行符号导出,后面内核模块导出符号小节讲解。
4.3 内核模块卸载过程
卸载过程相对加载比较简单,我们输入指令 rmmod,最终在系统内核中需要调用 sys_delete_module 进行实现。具体过程如下:先从用户空间传入需要卸载的模块名称,根据名称找到要卸载的模块指针,确保我们要卸载的模块没有被其他模块依赖,然后找到模块本身的 exit 函数实现卸载。代码如下。
内核模块卸载(内核源码 /kernel/module.c)
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
unsigned int, flags)
{
struct module *mod;
char name[MODULE_NAME_LEN];
int ret, forced = 0;
if (!capable(CAP_SYS_MODULE) || modules_disabled)
return -EPERM;
if (strncpy_from_user(name, name_user, MODULE_NAME_LEN - 1) < 0)
return -EFAULT;
name[MODULE_NAME_LEN - 1] = '\0';
audit_log_kern_module(name);
if (mutex_lock_interruptible(&module_mutex) != 0)
return -EINTR;
mod = find_module(name);
if (!mod) {
ret = -ENOENT;
goto out;
}
if (!list_empty(&mod->source_list)) {
ret = -EWOULDBLOCK;
goto out;
}
/* Doing init or already dying? */
if (mod->state != MODULE_STATE_LIVE) {
/* FIXME: if (force), slam module count damn the torpedoes */
pr_debug("%s already dying\n", mod->name);
ret = -EBUSY;
goto out;
}
if (mod->init && !mod->exit) {
forced = try_force_unload(flags);
if (!forced) {
/* This module can't be removed */
ret = -EBUSY;
goto out;
}
}
ret = try_stop_module(mod, flags, &forced);
if (ret != 0)
goto out;
mutex_unlock(&module_mutex);
/* Final destruction now no one is using it. */
if (mod->exit != NULL)
mod->exit();
blocking_notifier_call_chain(&module_notify_list, MODULE_STATE_GOING, mod);
klp_module_going(mod);
ftrace_release_mod(mod);
async_synchronize_full();
/* Store the name of the last unloaded module for diagnostic purposes */
strlcpy(last_unloaded_module, mod->name, sizeof(last_unloaded_module));
free_module(mod);
return 0;
out:
mutex_unlock(&module_mutex);
return ret;
}
- 第 8 行:确保有插入和删除模块不受限制的权利,并且模块没有被禁止插入或删除。
- 第 11 行:获得模块名字。
- 第 20 行:找到要卸载的模块指针。
- 第 26 行:有依赖的模块,需要先卸载它们。
- 第 39 行:检查模块的退出函数。
- 第 48 行:停止机器,使参考计数不能移动并禁用模块。
- 第 56 行:告诉通知链
module_notify_list上的监听者,模块状态变为MODULE_STATE_GOING。 - 第 60 行:等待所有异步函数调用完成。
5. 内核是如何导出符号的
符号是什么东西?我们为什么需要导出符号呢?内核模块如何导出符号呢?其他模块又是如何找到这些符号的呢?
这是这一小节讨论的知识,实际上,符号指的就是内核模块中使用 EXPORT_SYMBOL 声明的函数和变量。当模块被装入内核后,它所导出的符号都会记录在公共内核符号表中。在使用命令 insmod 加载模块后,模块就被连接到了内核,因此可以访问内核的共用符号。
通常情况下我们无需导出任何符号,但是如果其他模块想要从我们这个模块中获取某些方便的时候,就可以考虑使用导出符号为其提供服务。这被称为模块层叠技术。例如 msdos 文件系统依赖于由 fat 模块导出的符号;USB 输入设备模块层叠在 usbcore 和 input 模块之上。也就是我们可以将模块分为多个层,通过简化每一层来实现复杂的项目。
modprobe 是一个处理层叠模块的工具,它的功能相当于多次使用 insmod,除了装入指定模块外还同时装入指定模块所依赖的其他模块。
当我们要导出模块的时候,可以使用下面的宏:
EXPORT_SYMBOL(name)
EXPORT_SYMBOL_GPL(name) // name 为我们要导出的标志
符号必须在模块文件的全局部分导出,不能在函数中使用,_GPL 使得导出的模块只能被 GPL 许可的模块使用。编译我们的模块时,这两个宏会被拓展为一个特殊变量的声明,存放在 ELF 文件中。具体也就是存放在 ELF 文件的符号表中:
st_name:是符号名称在符号名称字符串表中的索引值st_value:是符号所在的内存地址st_size:是符号大小st_info:是符号类型和绑定信息st_shndx:表示符号所在 section
当 ELF 的符号表被加载到内核后,会执行 simplify_symbols 来遍历整个 ELF 文件符号表。根据 st_shndx 找到符号所在的 section 和 st_value 中符号在 section 中的偏移得到真正的内存地址。并最终将符号内存地址,符号名称指针存储到内核符号表中。
simplify_symbols 函数原型如下:
simplify_symbols 函数(内核源码 /kernel/module.c)
static int simplify_symbols(struct module *mod, const struct load_info *info)
函数参数和返回值如下:
- 参数:
mod:struct module类型结构体指针info:const struct load_info结构体指针
- 返回值:
ret:错误码
内核导出的符号表结构有两个字段,一个是符号在内存中的地址,一个是符号名称指针,符号名称被放在了 __ksymtab_strings 这个 section 中。以 EXPORT_SYMBOL 举例,符号会被放到名为 ___ksymtab 的 section 中。这个结构体我们要注意,它构成的表是导出符号表而不是通常意义上的符号表。
kernel_symbol 结构体(内核源码 /include/linux/export.h)
struct kernel_symbol {
unsigned long value;
const char *name;
};
value:符号在内存中的地址name:符号名称
其他的内核模块在寻找符号的时候会调用 resolve_symbol_wait 去内核和其他模块中通过符号名称寻址目标符号,resolve_symbol_wait 会调用 resolve_symbol,进而调用 find_symbol。找到了符号之后,把符号的实际地址赋值给符号表 sym[i].st_value = ksym->value。
find_symbol 函数(内核源码 /kernel/module.c)
/* 找到一个符号并将其连同(可选)crc 和(可选)拥有它的模块一起返回。需要禁用抢占或模块互斥。 */
const struct kernel_symbol *find_symbol(const char *name,
struct module **owner,
const s32 **crc,
bool gplok,
bool warn)
{
struct find_symbol_arg fsa;
fsa.name = name;
fsa.gplok = gplok;
fsa.warn = warn;
if (each_symbol_section(find_symbol_in_section, &fsa)) {
if (owner)
*owner = fsa.owner;
if (crc)
*crc = fsa.crc;
return fsa.sym;
}
pr_debug("Failed to find symbol %s\n", name);
return NULL;
}
EXPORT_SYMBOL_GPL(find_symbol);
- 第 14 行:在
each_symbol_section中,去查找了两个地方,一个是内核的导出符号表,即我们在将内核符号是如何导出的时候定义的全局变量,一个是遍历已经加载的内核模块,查找动作是在each_symbol_in_section中完成的。 - 第 25 行:导出符号标志
至此符号查找完毕,最后将所有 section 借助 ELF 文件的重定向表进行重定向,就能使用该符号了。
到这里内核就完成了内核模块的加载 / 卸载以及符号导出,可以查阅内核源码中/kernel/module.c。
Linux 内核模块管理:lsmod、insmod、rmmod、modinfo、modprobe、depmod 命令详解
posted @ 2020-04-18 21:10 韩晓萌
一、基本介绍
1、这些命令安装在 “kmod” 包中,系统通常已经安装了,如果没有安装请安装:
[root@localhost ]# rpm -ql kmod|grep sbin
/usr/sbin/depmod
/usr/sbin/insmod
/usr/sbin/lsmod
/usr/sbin/modinfo
/usr/sbin/modprobe
/usr/sbin/rmmod
/usr/sbin/weak-modules
2、CentOS 中所有与内核模块相关的文件都存放在 "/lib/modules/$(uname -r)/“下面(不管 32 位还是 64 位系统,都在 /lib/… 之下):
[root@localhost ~]# cd /lib/modules/$(uname -r)/
[root@localhost 3.10.0-123.el7.x86_64]# ls
build modules.builtin modules.modesetting source
extra modules.builtin.bin modules.networking updates
kernel modules.dep modules.order vdso
modules.alias modules.dep.bin modules.softdep
modules.alias.bin modules.devname modules.symbols
modules.block modules.drm modules.symbols.bin
二、命令介绍
1、lsmod:查看内核已加载的模块
[root@localhost ~]# lsmod|head -4
Module Size Used by
ip6table_filter 12815 0
ip6_tables 27025 1 ip6table_filter
iptable_filter 12810 0
2、modinfo:查看模块的基本信息
[root@localhost ~]# modinfo /lib/modules/3.10.0-123.el7.x86_64/kernel/fs/ext4/ext4.ko
filename: /lib/modules/3.10.0-123.el7.x86_64/kernel/fs/ext4/ext4.ko
license: GPL
description: Fourth Extended Filesystem
author: Remy Card, Stephen Tweedie, Andrew Morton, Andreas Dilger, Theodore Ts'o and others
alias: fs-ext4
alias: ext3
alias: fs-ext3
alias: ext2
alias: fs-ext2
srcversion: 7854620F0551D7F88A126F0
depends: mbcache,jbd2
intree: Y
vermagic: 3.10.0-123.el7.x86_64 SMP mod_unload modversions
signer: CentOS Linux kernel signing key
sig_key: BC:83:D0:FE:70:C6:2F:AB:1C:58:B4:EB:AA:95:E3:93:61:28:FC:F4
sig_hashalgo: sha256
3、insmod:将指定模块加载到内核,建议使用 modeprobe 命令
4、rmmod:将已加载模块从内核中移除,建议使用 modeprobe 命令
5、modprobe:加载或卸载内核模块,需要根据 modules.dep.bin 文件进行加载操作,可以自动解决模块间的依赖关系表
[root@localhost ~]# lsmod|grep ext4
[root@localhost ~]# modprobe ext4 #加载模块
[root@localhost ~]# lsmod|grep ext4
ext4 528957 0
mbcache 14958 1 ext4
jbd2 98341 1 ext4
[root@localhost ~]# modprobe -r ext4 #卸载模块
[root@localhost ~]# lsmod|grep ext4
6、depmod:查找 /lib/moduels/(uname -r)/ 中的所有模块并建立 modules.dep.bin 文件,该文件记录了模块位置及依赖关系
[root@localhost ~]# cd /lib/modules/$(uname -r)/
[root@localhost 3.10.0-123.el7.x86_64]# ls|grep dep
modules.dep
modules.dep.bin
modules.softdep
[root@localhost 3.10.0-123.el7.x86_64]# rm -rf modules.dep.bin
[root@localhost 3.10.0-123.el7.x86_64]# modprobe ext4
modprobe: FATAL: Module ext4 not found.
[root@localhost 3.10.0-123.el7.x86_64]# depmod -a #生成文件
[root@localhost 3.10.0-123.el7.x86_64]# modprobe ext4
[root@localhost 3.10.0-123.el7.x86_64]# lsmod|grep ext4
ext4 528957 0
mbcache 14958 1 ext4
jbd2 98341 1 ext4
[root@localhost 3.10.0-123.el7.x86_64]# ls|grep dep
modules.dep
modules.dep.bin
modules.softdep
posted @ 2020-04-18 21:10 韩晓萌
Linux内核模块分析(module_init宏)
阿基米东于 2016-05-17 03:25:13 发布
的模块化机制(module),但是module并不仅仅用于支撑驱动的加载和卸载。一个最简单的模块例子如下:
// filename: HelloWorld.c
#include <linux/module.h>
#include <linux/init.h>
static int hello_init(void)
{
printk(KERN_ALERT "Hello World\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Bye Bye World\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");
模块代码有两种运行方式,一是静态编译连接进内核,在系统启动过程中进行初始化;一是编译成可动态加载的module,通过insmod动态加载重定位到内核。这两种方式可以在Makefile中通过obj-y或obj-m选项进行选择。
而一旦可动态加载的模块目标代码(.ko)被加载重定位到内核,其作用域和静态链接的代码是完全等价的。所以这种运行方式的优点显而易见:
- 可根据系统需要运行动态加载模块,以扩充内核功能,不需要时将其卸载,以释放内存空间;
- 当需要修改内核功能时,只需编译相应模块,而不必重新编译整个内核。
因为这样的优点,在进行设备驱动开发时,基本上都是将其编译成可动态加载的模块。但是需要注意,有些模块必须要编译到内核,随内核一起运行,从不卸载,如 vfs、platform_bus等。
那么同样一份C代码如何实现这两种方式的呢?
答案就在于module_init宏!下面我们一起来分析module_init宏。(这里所用的Linux内核版本为3.10.10)
定位到Linux内核源码中的 include/linux/init.h,可以看到有如下代码:
#ifndef MODULE
// 省略
#define module_init(x) __initcall(x);
// 省略
#else
#define module_init(initfn) \
int init_module(void) __attribute__((alias(#initfn)));
// 省略
#endif
显然,MODULE 是由Makefile控制的。上面部分用于将模块静态编译连接进内核,下面部分用于编译可动态加载的模块。接下来我们对这两种情况进行分析。
方式一:#ifndef MODULE
代码梳理:
#define module_init(x) __initcall(x);
|
--> #define __initcall(fn) device_initcall(fn)
|
--> #define device_initcall(fn) __define_initcall(fn, 6)
|
--> #define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
即 module_init(hello_init) 展开为:
static initcall_t __initcall_hello_init6 __used \
__attribute__((__section__(".initcall6.init"))) = hello_init
这里的 initcall_t 是函数指针类型,如下:
typedef int (*initcall_t)(void);
GNU编译工具链支持用户自定义section,所以我们阅读Linux源码时,会发现大量使用如下一类用法:
__attribute__((__section__("section-name")))
__attribute__ 用来指定变量或结构位域的特殊属性,其后的双括弧中的内容是属性说明,它的语法格式为:__attribute__ ((attribute-list))。它有位置的约束,通常放于声明的尾部且“ ;” 之前。
这里的 attribute-list 为 __section__(".initcall6.init")。通常,编译器将生成的代码存放在.text段中。但有时可能需要其他的段,或者需要将某些函数、变量存放在特殊的段中,section 属性就是用来指定将一个函数、变量存放在特定的段中。
所以这里的意思就是:定义一个名为 __initcall_hello_init6 的函数指针变量,并初始化为 hello_init (指向 hello_init);并且该函数指针变量存放于 .initcall6.init 代码段中。
接下来,我们通过查看链接脚本( arch/$(ARCH)/kernel/vmlinux.lds.S)来了解 .initcall6.init 段。
可以看到,.init段中包含 INIT_CALLS,它定义在include/asm-generic/vmlinux.lds.h。INIT_CALLS 展开后可得:
#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
*(.initcallearly.init) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
VMLINUX_SYMBOL(__initcall_end) = .;
进一步展开为:
__initcall_start = .; \
*(.initcallearly.init) \
__initcall0_start = .; \
*(.initcall0.init) \
*(.initcall0s.init) \
// 省略1、2、3、4、5
__initcallrootfs_start = .; \
*(.initcallrootfs.init) \
*(.initcallrootfss.init) \
__initcall6_start = .; \
*(.initcall6.init) \
*(.initcall6s.init) \
__initcall7_start = .; \
*(.initcall7.init) \
*(.initcall7s.init) \
__initcall_end = .;
上面这些代码段最终在kernel.img中按先后顺序组织,也就决定了位于其中的一些函数的执行先后顺序(__initcall_hello_init6 位于 .initcall6.init 段中)。.init 或者 .initcalls 段的特点就是,当内核启动完毕后,这个段中的内存会被释放掉。这一点从内核启动信息可以看到:
Freeing unused kernel memory: 124K (80312000 - 80331000)
那么存放于 .initcall6.init 段中的 __initcall_hello_init6 是怎么样被调用的呢?我们看文件 init/main.c,代码梳理如下:
start_kernel
|
--> rest_init
|
--> kernel_thread
|
--> kernel_init
|
--> kernel_init_freeable
|
--> do_basic_setup
|
--> do_initcalls
|
--> do_initcall_level(level)
|
--> do_one_initcall(initcall_t fn)
kernel_init 这个函数是作为一个内核线程被调用的(该线程最后会启动第一个用户进程init)。
我们着重关注 do_initcalls 函数,如下:
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
函数 do_initcall_level 如下:
static void __init do_initcall_level(int level)
{
// 省略
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}
函数 do_one_initcall 如下:
int __init_or_module do_one_initcall(initcall_t fn)
{
int ret;
// 省略
ret = fn();
return ret;
}
initcall_levels 的定义如下:
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
initcall_levels[] 中的成员来自于 INIT_CALLS 的展开,如“__initcall0_start = .;”,这里的 __initcall0_start是一个变量,它跟代码里面定义的变量的作用是一样的,所以代码里面能够使用__initcall0_start。因此在 init/main.c 中可以通过 extern 的方法将这些变量引入,如下:
extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];
到这里基本上就明白了,在 do_initcalls 函数中会遍历 initcalls 段中的每一个函数指针,然后执行这个函数指针。因为编译器根据链接脚本的要求将各个函数指针链接到了指定的位置,所以可以放心地用 do_one_initcall(*fn) 来执行相关初始化函数。
我们例子中的 module_init(hello_init) 是 level6 的 initcalls 段,比较靠后调用,很多外设驱动都调用 module_init 宏,如果是静态编译连接进内核,则这些函数指针会按照编译先后顺序插入到 initcall6.init 段中,然后等待 do_initcalls 函数调用。
方式二:#else
相关代码:
#define module_init(initfn) \
static inline initcall_t __inittest(void) \
{ return initfn; } \
int init_module(void) __attribute__((alias(#initfn)));
__inittest 仅仅是为了检测定义的函数是否符合 initcall_t 类型,如果不是 __inittest 类型在编译时将会报错。所以真正的宏定义是:
#define module_init(initfn) \
int init_module(void) __attribute__((alias(#initfn)));
因此,用动态加载方式时,可以不使用 module_init 和 module_exit 宏,而直接定义 init_module 和 cleanup_module 函数,效果是一样的。
alias 属性是 gcc 的特有属性,将定义 init_module 为函数 initfn 的别名。所以 module_init(hello_init) 的作用就是定义一个变量名 init_module,其地址和 hello_init 是一样的。
上述例子编译可动态加载模块过程中,会自动产生 HelloWorld.mod.c 文件,内容如下:
#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>
MODULE_INFO(vermagic, VERMAGIC_STRING);
struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
static const char __module_depends[]
__used
__attribute__((section(".modinfo"))) =
"depends=";
可知,其定义了一个类型为 module 的全局变量 __this_module,成员 init 为 init_module(即 hello_init),且该变量链接到 .gnu.linkonce.this_module 段中。
编译后所得的 HelloWorld.ko 需要通过 insmod 将其加载进内核,由于 insmod 是 busybox 提供的用户层命令,所以我们需要阅读 busybox 源码。代码梳理如下:(文件 busybox/modutils/ insmod.c)
insmod_main
|
--> bb_init_module
|
--> init_module
而 init_module 定义如下:(文件 busybox/modutils/modutils.c)
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)
因此,该系统调用对应内核层的 sys_init_module 函数。
回到Linux内核源代码(kernel/module.c),代码梳理:
SYSCALL_DEFINE3(init_module, ...)
|
-->load_module
|
--> do_init_module(mod)
|
--> do_one_initcall(mod->init);
文件(include/linux/syscalls.h)中,有:
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
从而形成 sys_init_module 函数。
Linux内核模块管理
识途老码已于 2024-08-16 23:40:08 修改
内核模块保存的位置
参考: http://c.biancheng.net/view/1039.html
内核模块的保存位置在 /lib/modules/内核版本号/kernel/ 目录中
# 获取内核版本号
uname -a |awk '{print $3}'

modules.dep内核模块数据库文件
Linux中所有的内核模块都存放在 /lib/modules/$(uname -a |awk '{print $3}')/modules.dep 文件中,在安装模块时,依赖这个文件査找所有的模块,所以不需要指定模块所在位置的绝对路径,而且也依靠这个文件来解决模块的依赖性。
生成modules.dep(内核模块数据库文件)
modules.dep 文件如果不存在,可以手动执行depmod 命令来生成.depmod 命令会扫描系统中已有的内核模块,并生成modules.dep 文件.
insmod安装ko模块文件
# 安装ko模块文件
insmod /home/dpvs/dpdk-stable-17.11.2/build/kmod/igb_uio.ko
modprobe常用参数:
| 参数 | 解释 |
|---|---|
-a | 同时加载指定模块的所有依赖模块。 |
-r | 卸载指定的内核模块,并卸载其依赖的其他模块(如果没有其他模块依赖该模块)。 |
-l | 列出所有可用的内核模块。 |
-q | 静默模式,不显示任何输出信息。 |
-v | 详细模式,显示详细的输出信息。 |
-c | 检查模块依赖关系并显示任何错误信息。 |
-d | 指定一个用于加载内核模块的目录。 |
-n | 禁止自动加载模块。 |
-s | 禁止自动加载模块的符号版本检查。 |
查看内核模块
# 查看系统当前加载的模块
lsmod
# 查看指定模块的信息
# modinfo 模块名 例如:
modinfo autofs4

# 查看模块
lsmod | grep wireguard

加载内核模块
# 加载模块
modprobe 模块名

加载模块的依赖模块
## -a 参数表示同时加载指定模块的所有依赖模块
modprobe -a 模块名
配置内核模块在OS启动时自动加载
systemd-modules-load.service 从下列目录中读取文件,这些文件包含要在引导期间加载的静态列表中的内核模块。
相关目录:
| 目录路径 | 解释 |
|---|---|
/etc/modprobe.d/ | 用于在加载内核模块时指定参数或禁用模块。配置文件以 .conf 结尾,可用于设置模块选项或黑名单。 |
/etc/modules-load.d/ | 指定系统启动时应自动加载的内核模块。每个文件列出需要加载的模块名称,一行一个模块。 |
/run/modules-load.d/*.conf | 类似于 /etc/modules-load.d/,用于在系统运行时临时指定要加载的模块。由于 /run/ 是一个临时文件系统,配置在系统重启后会消失。 |
/usr/lib/modules-load.d/*.conf | 系统和软件包默认提供的模块加载配置。它们指定在系统启动时加载的内核模块,通常不建议用户手动修改。 |
开机自动加载内核模块示例
https://wiki.gentoo.org/wiki/WireGuard
/etc/modules-load.d/
# 在系统启动时自动加载virtio-net.ko
echo virtio-net >/etc/modules-load.d/virtio-net.conf
# 在系统启动时自动加载zfs内核模块
echo zfs >/etc/modules-load.d/zfs.conf
# 在系统启动时自动加载wireguard内核模块
echo wireguard >/etc/modules-load.d/wireguard.conf
# 在系统启动时自动加载tun内核模块
echo tun >/etc/modules-load.d/tun.conf
# 在系统启动时自动加载ip_gre内核模块
echo ip_gre >/etc/modules-load.d/ip_gre.conf
持久化配置内核模块的加载参数
/etc/modprobe.d
该目录的一些常见用途和作用:
在该目录中创建以 .conf 结尾的配置文件;
黑名单: 配置文件中使用blacklist关键字,指定要屏蔽的模块,以防止其加载。参数配置: 配置文件中使用options关键字,指定加载模块时要传递的参数值。这样可以灵活地配置模块的行为。别名设置: 配置文件并使用alias关键字,您可以定义模块的别名。
# 配置内核模块的加载参数
echo "options zram num_devices=1" >/etc/modprobe.d/zram.conf
删除内核模块
# 卸载模块
modprobe -r 模块名
常用Linux内核参数说明
识途老码已于 2024-12-17 21:35:28 修改
内核参数说明
参考:
- 常见的Linux系统内核网络参数及常见问题的解决方案_云服务器 ECS(ECS)-阿里云帮助中心
https://help.aliyun.com/zh/ecs/support/common-kernel-network-parameters-of-ecs-linux-instances-and-faq#h2--linux-
sysctl命令常用参数
sysctl命令能时动态地修改内核参数;
sysctl命令常用参数:
-
sysctl命令 – 配置系统内核参数 – Linux命令大全(手册)
https://www.linuxcool.com/sysctl -
sysctl 命令,Linux sysctl 命令详解:时动态地修改内核的运行参数 - Linux 命令搜索引擎
https://wangchujiang.com/linux-command/c/sysctl.html
# 查看所有内核参数配置
sysctl -a
# 从所有配置中过滤指定配置
sysctl -a | grep net.ipv4.tcp_ecn

# 加载所有的sysctl配置文件
sysctl --system
# 加载/etc/sysctl.conf默认配置文件
sysctl -p
# 加载指定sysctl配置文件
sysctl -p /etc/sysctl.d/diy.conf
高可用优化
-
linux内核参数注释与优化_功夫猫的技术博客_51CTO博客
https://blog.51cto.com/yangrong/1321594 -
CentOS(5.8/6.4)linux生产环境若干优化实战_老男孩linux培训的技术博客_51CTO博客
https://blog.51cto.com/oldboy/1336488
swap配置
- Swap分区详解-优快云博客
https://blog.youkuaiyun.com/omaidb/article/details/106674391
# 当剩余多少系统内存时,再使用swap
vm.swappiness=0
# 当剩余10%内存时再使用swap
vm.swappiness=10
内存分配模式
vm.overcommit_memory 决定了内核如何处理内存分配请求的策略
# 启发式过度分配(默认)
vm.overcommit_memory=0
# 总是过度分配
vm.overcommit_memory=1
# 从不过度分配,避免系统因过度分配内存而崩溃
vm.overcommit_memory=2
关闭OOM机制
参考: https://wangshangyou.com/linux-oom.html
# 开启OOM,进程瞬间占满内存
## 默认为0表示开启OOM,开启后会自动kill掉瞬间占满内存的进程
## 1 表示关闭,内存不足时,直接系统重启
echo "
# 关闭OOM
vm.panic_on_oom=1 " >> /etc/sysctl.conf
热生效
# 立即关闭OOM机制
sysctl -w vm.panic_on_oom=1
# 等价于
echo 1 >/proc/sys/vm/panic_on_oom
调高内存预留水位线

自动释放cache,防止oom
我之前遇到过程序去请求内存资源,但是cache把剩余内存打满且没释放导致请求内存失败,直接oom;
不检查物理内存是否够用
https://www.modb.pro/db/25985
值为0:内存不足时,启动 OOM killer。
值为1:内存不足时,有可能会触发 kernel panic(系统重启),也有可能启动 OOM killer。
值为2:内存不足时,表示强制触发 kernel panic,内核崩溃GG(系统重启)。内核不会对内存请求进行限制,允许物理内存+交换空间总和的分配。
echo "
# 自动释放cache防止oom
vm.overcommit_memory=2" >> /etc/sysctl.conf
# 热生效
echo 2 >/proc/sys/vm/overcommit_memory
同一用户同时可以添加的watch数目
- fs.inotify.max_user_watches默认值太小,导致too many open files - 技术颜良 - 博客园
https://www.cnblogs.com/cheyunhua/p/14325314.html
# 同一用户同时可以添加的watch数目
## 默认值8192
fs.inotify.max_user_watches=524288
# 防止容器数量增加导致fs.inotify.max_user_instances超过限制
# 默认128
fs.inotify.max_user_instances=8192
解除Linux进程数和线程数及文件打开数
允许开启的线程数量
# 加大允许开启的线程数量
vm.max_map_count=262144
系统中可同时打开的文件数
# 系统中可同时打开的最大文件数目
fs.file-max=52706963
# 单个进程可分配的最大文件数
fs.nr_open=52706963
# 查看系统中可同时打开的最大文件数
sysctl fs.file-max
# 查看单进程可打开的最大文件数
sysctl fs.nr_open


限制用户最大进程数和最大文件打开数限制
参考:
- limits.conf配置文件说明
https://cloud.tencent.com/developer/article/1403636
软限制`是`用户`可以`临时提高`的,而`硬限制`是`管理员`设置的`上限`,`不能被`用户或进程`改变
| 针对用户(*表示所有用户) | 限制方法 | 限制类型 | 限制的值 |
|---|---|---|---|
* 代表针对所有用户 | soft: 软限制 | noproc是最大进程数 | 11000 |
| * | hard: 硬限制 | nofile 是最大文件打开数 | 4100 |
| * | -为soft和hard全部限制 | noproc(最大进程数) | 1100 |
| * | - | nofile(最大文件打开数) | 4100 |
- 修改Linux最大文件描述符及ulimit相关使用 - 简书
https://www.jianshu.com/p/162e320cb352
os`的`openfile`值,建议加大。
配置文件在`/etc/security/limits.conf
# 所有用户最大内存锁定的软限制为未限制
* soft memlock unlimited
# 所有用户最大内存锁定的硬限制为未限制
* hard memlock unlimited
# 针对所有用户软限制最大进程数
* soft noproc 11000
# 针对所有用户硬限制最大进程数
* hard noproc 11000
# 针对所有用户软限制最大文件打开数
* soft nofile 65535
# 针对所有用户硬限制最大文件打开数
* hard nofile 65535
# 最大核心转储文件的大小为未限制
* soft core unlimited
* hard core unlimited
软硬一起限制
# *代表针对所有用户,-表示软硬都限制,memlock是最大内存锁定
* - memlock unlimited
# *代表针对所有用户,-表示软硬都限制,noproc是最大进程数
* - noproc 11000
# *代表针对所有用户,-表示软硬都限制,nofile是最大文件打开数
* - nofile 65535
# 最大核心转储文件的大小
* - core unlimited
使配置生效
重启系统或者重新登录用户才会让/etc/security/limits.conf 配置生效;
也可以通过重新加载PAM配置的方法让limits.conf配置热生效。
重新加载 PAM 配置可能会导致当前用户的会话中断或其他不可预测的结果。
# 重新加载 PAM(Pluggable Authentication Modules)配置,让limits.conf配置热生效
sudo systemctl restart systemd-logind
# 注销用户
logout
# 登录
ssh xxx

查看配置生效后的限制数量
# 查看用户进程能够打开的最大文件数
ulimit -n
# 查看用户最大进程数
ulimit -u
# 查看虚拟内存大小
ulimit -v
# 查看CPU时间限制
ulimit -t
# 查看最大核心转储文件的大小
ulimit -c
网络优化
提高网速
# 发送缓冲区(byte)
net.core.wmem_max=16777216
# 接收缓冲区(byte)
net.core.rmem_max=16777216
# 禁用 SYN Cookies,提高 TCP 连接的建立速度
## 不能防止 SYN 洪水攻击
net.ipv4.tcp_syncookies=0
# 最大 SYN 队列长度(默认128)
net.ipv4.tcp_max_syn_backlog=256
# FIN 超时时间
net.ipv4.tcp_fin_timeout=10
修改网络最大连接数
# 查看网络最大连接数
cat /proc/sys/net/core/somaxconn

# 默认网络最大连接数是128
net.core.somaxconn=1024
内核开启数据包转发功能
echo "
# 开启内核开启数据包转发
## 1为开启;0为关闭
net.ipv4.ip_forward=1 " >> /etc/sysctl.conf && sysctl -p
执行sysctl -p使内核配置生效
# 热生效
## 1为开启;0为关闭
echo 1 >/proc/sys/net/ipv4/ip_forward
将内核参数ip_forward设置为0以后,以下服务的功能可能会受影响.
-
iptables
-
firewalld
-
LVS
-
Keepalived
LVS和Keepalived需要NAT
启用arp代理
# 启用 ARP 代理(Proxy ARP)功能
net.ipv4.conf.all.proxy_arp=1
# 0:禁用 ARP 代理功能
# 1:启用 ARP 代理功能
# 2:只对连接到同一接口的主机进行代理
echo 1 >/proc/sys/net/ipv4/conf/all/proxy_arp
回应代理APR的数据包此网卡出去
# 回应代理ARP的数据包从接收到此代理ARP请求的网络接口出去
## 0:禁用在 PVLAN 中使用代理 ARP。
## 1:启用在 PVLAN 中使用代理 ARP
net.ipv4.conf.all.proxy_arp_pvlan=1
自动MTU
# 开启MTU黑洞检测,动态地调整数据包的大小
net.ipv4.tcp_mtu_probing=1
启用TCP窗口自动调整
TCP窗口大小可以动态地根据网络延迟和带宽进行调整,从而实现更高的数据传输效率。
启用TCP窗口缩放可以提高大带宽延迟产品网络的性能。这对于高速长距离网络连接非常有用。
# 启用TCP窗口缩放
net.ipv4.tcp_window_scaling=1
开启TCP低延时模式
启TCP小缓冲区功能,并设置TCP连接为低延迟模式。这有助于减少延迟和提高网络性能。
# 低延迟模式
## 在高吞吐量情况下,这个选项应该禁用
net.ipv4.tcp_low_latency=1
# 连接结束时保存TCP连接信息,能提高性能,但会导致无法进行网络分析
net.ipv4.tcp_no_metrics_save=1
# 设置TCP延迟确认时间为1毫秒(默认40ms)
## Linux内核2.6.39之后的版本已移除该参数
net.ipv4.tcp_delack_min=1
TCP快速重传和快速恢复算法
# RFC 1337规范中的TCP快速重传和快速恢复算法
net.ipv4.tcp_rfc1337=1
开启快速TCP
在TCP第一次握手时就传输数据。
谨慎启用TFO:https://candrews.integralblue.com/2019/03/the-sad-story-of-tcp-fast-open/
0 — 表示TFO未开启
1 — 表示TFO开启了,但是只对客户端有效
2 — 表示TFO开启了,但是只对服务器端有效
3 — 表示TFO开启了,同时对客户端和服务器端有效
# 开启快速tcp
net.ipv4.tcp_fastopen=3
# TCP Fast Acknowledgment(快速确认)功能
net.ipv4.tcp_fack=1
# 热生效
echo 3 > /proc/sys/net/ipv4/tcp_fastopen
开启SACK(减少重传)
TCP Selective Acknowledgment(SACK)是一种扩展TCP协议的机制,用于改善网络拥塞控制和数据传输可靠性。其主要作用如下:
-
减少重传:在传输数据时,如果发生数据包丢失,传统的TCP协议会重传丢失的数据包及其后续数据包。而使用SACK机制后,接收方可以告诉发送方哪些数据包已经接收到,从而发送方只需要重传丢失的部分。
-
改善网络拥塞控制:SACK机制可以帮助TCP协议更准确地判断网络拥塞情况。当发生网络拥塞时,SACK机制可以帮助TCP协议更快地进行拥塞控制,从而减少网络拥塞对数据传输的影响。
-
提高数据传输可靠性:SACK机制可以更准确地标记已经接收到的数据包,从而提高数据传输的可靠性。即使发生一些数据包的丢失,SACK机制也可以让TCP协议更快地恢复数据传输。
综上所述,TCP Selective Acknowledgment(SACK)机制可以提高TCP协议在网络中的表现,减少数据重传,改善网络拥塞控制,提高数据传输可靠性。
- 禁用TCP延迟确认
https://www.jianshu.com/p/f863e189aadf
: "减少重传"
# 启用 TIME_WAIT 状态的 TCP 连接的快速重用
net.ipv4.tcp_tw_reuse=1
# 启用 TIME_WAIT 状态的 TCP 连接的快速回收
net.ipv4.tw_recycle=1
# 减少段丢失时需要重传的段数目
## 默认为1
## 修改为0 ,表示 关闭checksum
# 开启SACK减少重传,改善丢包
# 启用选择性确认(Selective Acknowledgements)可以提高网络效率,尤其是在有丢包的情况下。
net.ipv4.tcp_sack=1
# 对sack的改进,能够检测不必要的重传
## 默认为1
net.ipv4.tcp_dsack=1
# 关闭checksum
# 参考:https://blog.51cto.com/u_15538975/10596897
net.ipv4.tcp_checksum=0
减少TCP发送KeepAlive消息间隔
# 设置 SYN+ACK 的重试次数。推荐值为 2。
## 默认值为5
net.ipv4.tcp_synack_retries = 2
# 设置SYN的重试次数。推荐值为2
## 默认值为6
net.ipv4.tcp_syn_retries = 2
# 当TCP连接队列溢出时,启用rst复位以通知客户端
net.ipv4.tcp_abort_on_overflow = 1
# 将TCP连接空闲时发送KeepAlive消息的时间间隔设置为300秒(5分钟)
## 默认是7200秒(2小时)
net.ipv4.tcp_keepalive_time = 50
# 设置TCP keepalive探测的次数,推荐值为 5。
## 默认值为9
net.ipv4.tcp_keepalive_probes = 5
# 设置TCP keepalive探测的时间间隔。推荐值为15
## 默认值为75
net.ipv4.tcp_keepalive_intvl = 15
允许TCP/IP堆栈接受其他网络接口的数据包,使用不同的L3设备
Linux4.4+开始支持该参数.
# 允许TCP/IP堆栈接受其他网络接口的数据包,使用不同的L3设备
## 对虚拟机可能有用
## 在一些负载平衡或故障转移场景下可能很有用
net.ipv4.udp_l3mdev_accept = 1
net.ipv4.tcp_l3mdev_accept = 1
减少孤儿连接重试次数
# 孤儿连接重试次数
## 默认值是0(8次),调整为3次
net.ipv4.tcp_orphan_retries = 3
防止TCP不回包
- Linux服务器收到SYN请求包没有回应ACK导致客户端无法建立TCP连接_syn 回 未知 ack-优快云博客
https://blog.youkuaiyun.com/chenlycly/article/details/80369408
# 快速重用处于TIME_WAIT状态的tcp连接--只适用于rhel7
## 1为开启;降低系统负载
## 0为关闭,防止TCP不回包
net.ipv4.tcp_tw_recycle = 1
# 精确TCP时间戳,能提高网络性能,同时增加资源开销
## 发送数据时,会将一个timestamp(表示发送时间)放在包里面
# 启用TCP时间戳可以提高网络的性能,尤其是在有重复数据包的情况下
## 0为关闭,降低系统负载
## 默认为1,1为开启,防止TCP不回包
net.ipv4.tcp_timestamps = 0
# 热生效
echo "0" > /proc/sys/net/ipv4/tcp_tw_recycle
# 可能没这个文件
echo "0" > /proc/sys/net/ipv4/tcp_timestamps
调整GRO/LRO/GSO/UFO/UDP校验和
# 开启GRO,将一组连续的 UDP 数据报片段合并为单个数据报
ethtool -K ethX gro on
# 开启LRO,将多个接收到的 UDP 数据报组合成一个更大的数据报
ethtool -K ethX lro on
# GSO是将TCP大包拆分成多个小包
ethtool -K eth0 gso on
# UFO是将UDP大包拆分成多个小包
ethtool -K eth0 ufo on
# 关闭UDP校验和计算
ethtool -K eth0 rx-checksumming off
调整网卡的RingBuffer(环形缓冲区)
# 查看网卡的RingBuffer(环形缓冲区)大小
ethtool -g <interface>

# 加大网卡的RingBuffer(环形缓冲区)
ethtool -G ens3 rx 4096 tx 4096
## 收到"netlink error: Operation not supported"错误消息;
## 这表示您的网络接口不支持通过ethtool命令调整RingBuffer大小。
启用BBR拥塞控制算法
结果让人失望,bbr2,bbr3 都不如 bbr1
# 加载tcp_bbr内核模块
modprobe tcp_bbr
# 在系统启动时自动加载tcp_bbr内核模块
echo tcp_bbr >/etc/modules-load.d/tcp_bbr.conf
# 查看tcp_bbr内核模块是否被加载
lsmod | grep bbr

# 查看支持的拥塞控制算法
sysctl net.ipv4.tcp_available_congestion_control

# 查看当前使用的拥塞控制算法
sysctl net.ipv4.tcp_congestion_control

启用BBR拥塞控制算法
# 指定Fair Queuing 公平队列调度器
net.core.default_qdisc=fq
# 启用BBR
net.ipv4.tcp_congestion_control=bbr
UDP优化
# 参考:<https://leohsiao.com/Linux/系统内核/内核.html>
# 指定 UDP 接收缓冲区的最小大小
## 1M==1048576
## 默认值 4096
net.ipv4.udp_rmem_min = 4096
# 指定 UDP 发送缓冲区的最小大小
## 1M==1048576
## 默认值 4096
net.ipv4.udp_wmem_min = 4096
# 调整 UDP 接收缓冲区的内存分配
## 最小值10M 默认值25M 最大值32M
## 默认值 18276 24371 36552
# net.ipv4.udp_mem = 10485760 26214400 33554432
net.ipv4.udp_mem = 4096 33554432 33554432
网络安全优化
常见配置:
https://eulixos.com/docs/2.0/SecHarden/内核参数.html#加固内核参数
禁用IPV6
- 开启或关闭IPv6_Alibaba Cloud Linux(Alinux)-阿里云帮助中心_
https://help.aliyun.com/zh/alinux/support/enable-or-disable-ipv6
echo "
# 关闭IPV6
## 1为开启;0为关闭
# 禁用系统中所有网络接口的 IPv6 功能
net.ipv6.conf.all.disable_ipv6=1
# 禁用默认网络接口的 IPv6 功能
## 默认网络接口是系统中除了 lo(回环接口)以外的所有网络接口
net.ipv6.conf.default.disable_ipv6=1
# 禁用回环接口 (lo) 的 IPv6 功能
net.ipv6.conf.lo.disable_ipv6=1" >> /etc/sysctl.conf
执行sysctl -p使内核配置生效
禁用指定网卡的IPV6
# 禁用指定网卡的IPV6
net.ipv6.conf.eth0.disable_ipv6=1
关闭IPV6内核模块的加载选项
# 指定内核模块的加载选项:关闭IPV6加载
echo "options ipv6 disable =1">/etc/modprobe.d/disable_ipv6.conf
热生效
# 热生效
## 1为开启‘禁用IPV6’;0为关闭‘禁用IPV6’
echo 1>/proc/sys/net/ipv6/conf/all/disable_ipv6
echo 1>/proc/sys/net/ipv6/conf/default/disable_ipv6
echo 1>/proc/sys/net/ipv6/conf/lo/disable_ipv6
禁止ping
echo "
# 禁ping
## 1为开启禁ping;0为关闭禁ping
net.ipv4.icmp_echo_ignore_all=1" >> /etc/sysctl.conf
执行sysctl -p使内核配置生效
0为允许ping(系统默认值),1为禁ping.
# 热生效
## 1为开启禁ping;0为关闭禁ping
echo "1" > /proc/sys/net/ipv4/icmp_echo_ignore_all
Centos7服务器优化脚本
参考:
- CentOS7一键优化安装脚本
https://www.toutiao.com/article/6956379818657186308/
via:
-
【Linux驱动】 Linux内核模块-优快云博客
https://dengjin.blog.youkuaiyun.com/article/details/138222373 -
Linux内核模块管理:lsmod、insmod、rmmod、modinfo、modprobe、depmod命令详解 - 韩晓萌 - 博客园
https://www.cnblogs.com/hanxiaomeng/p/12728104.html -
Linux内核模块分析(module_init宏)_#ifndef module-优快云博客
https://blog.youkuaiyun.com/lu_embedded/article/details/51432616 -
Linux内核模块管理指南-优快云博客
https://blog.youkuaiyun.com/omaidb/article/details/122718636 -
常用Linux内核参数说明_linux 内核参数-优快云博客
https://blog.youkuaiyun.com/omaidb/article/details/122719243
— -
内核模块 - Arch Linux
https://wiki.archlinuxcn.org/wiki/内核模块 -
Ubuntu Manpage: modules-load.d - 配置启动时加载哪些内核模块
https://manpages.ubuntu.com/manpages/focal/zh_CN/man5/modules-load.d.5.html -
modules-load.d
https://www.freedesktop.org/software/systemd/man/latest/modules-load.d.html
1446

被折叠的 条评论
为什么被折叠?



