-
Notifications
You must be signed in to change notification settings - Fork 0
/
module_zh.tex
133 lines (112 loc) · 9.19 KB
/
module_zh.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
\section{Linux内核的架构}
Linux是宏内核\index{monolithic kernel},即整个内核是一个共用地址空间的二进制程序.
宏内核架构是相对于微内核而言的.
微内核架构基于消息传递,作为内核的二进制程序只有最基本的功能,其他的不同组件处于不同的地址空间,它们之间通过消息传递的方式来进行交互.
Linux内核采用宏内核的架构主要是出于性能方面的考虑.\cite{silberschatz2021operating}
同一地址空间的各个部分可以直接相互调用,没有消息传递的开销.
宏内核的架构不代表Linux内核不能采用模块化的设计,这得益于动态链接技术.
Linux内核由常驻内存的内核镜像 \lstinline{vmlinux}%
\index{kernel image} \index{v@\lstinline{vmlinux}|see {kernel image}}%
和动态加载的各种模块\index{kernel module}构成.
使用运行时加载的模块的好处有很多.
首先,这些模块在加载后就处于核心态,可以调用内核中的任何代码,访问硬件也没有限制,这可以使内核的设计者适度地划分各个功能之间的界限——避免功能之间耦合度过高的同时也不必在不同部分之间使用复杂的交互方法.
其次,动态加载模块可以在操作系统运行时更改内核,这对内核的开发有好处,因为不用编译整个内核并重启系统.
这也对节约资源有好处,因为可以只按需要加载内核模块,减少内存占用.
\begin{qbox}{为什么叫“镜像”?}
\lstinline{vmlinux} 被称为内核“镜像”(kernel image),是因为这个文件在加载操作系统时被用来在内存中创建内核的副本.
详见Image一词的含义\cite{imageWik70:online}.
\end{qbox}
要了解Linux如何实现模块化的宏内核架构,我们在案例分析中重点介绍两方面的内容:内核模块是如何构建的、内核模块是如何加载的.
\subsection{内核构建系统} \label{kbuild}
\begin{readsrcbox}{Kernel Build System}
内核的构建系统有关的文档在 \href{https://docs.kernel.org/kbuild/index.html}{\lstinline{Documentation/kbuild}} 目录.
可以参考顶层目录的\linuxsrc{Makefile},各个目录下的\lstinline{Makefile}、\lstinline{Kbuild}.
另外,\linuxsrc{script}目录下有用来链接 \lstinline{vmlinux}和有关构建模块的脚本.
\end{readsrcbox}
\begin{qbox}{如何找到某个子系统的有关文件和信息?}
\lstinline{Maintainer} 文件中列出了Linux内核各个子系统的维护者,同时它还包括有关项目的网页和涉及的文件等有用的信息.
\end{qbox}
Linux内核的构建系统\index{Kernel Build System}\index{Kbuild!see {Kernel Build System}}%
是一套基于GNU Make的递归式的构建系统,包括各个目录下的Makefile和为这些Makefile提供基础设施支持的其他文件,通常称为 \lstinline{kbuild}. \cite{LinuxKer71:online}
整个内核构建系统的目标主要就是内核镜像 \lstinline{vmlinux} 和可加载的模块.
各个Kbuild Makefile以向 \lstinline{obj-m} 和 \lstinline{obj-y} 变量增加内容的方式列出本目录应该构建的目标\index{build goal},
其中 \lstinline{obj-m} 变量中定义的目标会被构建成内核模块——后缀名为 \lstinline{.ko} 的一种ELF文件,而 \lstinline{obj-y} 变量中定义的目标会被 \lstinline{$(AR)}\footnote{一般就是GNU Binutils里面的ar,用于把多个 \lstinline{.o} 目标文件归档到一个文件中.} 合并到一个名为 \lstinline{built-in.a} 的库中,最后被链接进 \lstinline{vmlinux}.
顶层目录的Makefile递归地构建子目录.
有趣的是,内核的各个部分通常既可以编译到内核镜像中,也可以编译成可加载的内核模块,这是由目标是加入到 \lstinline{obj-m} 还是 \lstinline{obj-y} 来决定的.
例如Listing \ref{lst:obj-config}所示,当 \lstinline{CONFIG_BTRFS_FS}的值为y(Yes)时,BTRFS文件系统驱动——目标 \lstinline{btrfs.o}将作为内核内置的一部分编译;
而\lstinline{CONFIG_BTRFS_FS}的值为m(Module)时,它将编译成可加载的内核模块.
\begin{lstlisting}[language=make, caption=可配置的编译目标, label=lst:obj-config]
# fs/btrfs/Makefile
obj-$(CONFIG_BTRFS_FS) := btrfs.o
\end{lstlisting}
类似 \lstinline{CONFIG_BTRFS_FS} 的变量的定义来源于与内核构建系统一起工作的配置系统——Kconfig.
\index{Kconfig}%
开发者往往会通过 \lstinline{make *config} 来调用某种目录界面对将要构建的内核进行配置,选择要启用的功能并配置其中的参数.
这一步骤生成Kconfig语言的内核配置文件,为Kbuild提供各种变量,对构建出来的内核造成影响.
以Arch Linux为例,其官方的内核配置文件中有关上述BTRFS模块的\archlinuxconf{9619}{一行}为:
\lstinline{CONFIG_BTRFS_FS=m}.
这意味着Arch Linux 的 Linux 包所使用的内核中,BTRFS的支持是作为可加载的模块编译的.
内核构建系统的设计还允许在内核的代码树\index{out-of-tree module}外构建内核模块,只要安装了对应版本的头文件和构建脚本,Linux的使用者甚至可以在自己计算机的任何目录编写、构建和安装自己的内核模块.
同样以 Arch Linux 为例,要安装的是
\href{https://archlinux.org/packages/core/x86_64/linux-headers/}{linux-headers}.
编译自己的模块时,首先编写Kbuild文件,声明要编译的目标.
然后利用GNU Make构建前更改工作目录的功能,使用安装在系统中的内核构建系统来编译自己的模块,如 Listing \ref{lst:out-of-tree-make} 所示.
其中 \lstinline{/lib/modules/`uname -r`/build} 会被展开成当前运行内核的版本的构建目录,这个目录正是 linux-headers 包安装的.
\begin{lstlisting}[language=sh, caption=为本机的内核构建模块, label=lst:out-of-tree-make]
make -C /lib/modules/`uname -r`/build M=$PWD
\end{lstlisting}
\subsection{内核模块的加载} \label{loading modules}
\begin{readsrcbox}{加载模块}
模块在内核中的表示见 \linuxsrc{include/linux/module.h}.
加载模块的函数为 \linuxsrc{kernel/module.c} 中的 \lstinline{load_module}.
\lstinline{init_module} 是加载模块的系统调用,它把模块的内容从用户空间拷贝到内核,然后调用\lstinline{load_module}.
\end{readsrcbox}
在\ref{kbuild}中,我们了解到内核模块是ELF格式的二进制文件,加载模块就是要将这个文件加载到内核的地址空间中,
并做一系列准备工作,使该模块进入正常的工作状态、与内核的其他部分可以互相调用.
加载模块的步骤主要是:
\begin{enumerate}
\item 读ELF header,获取各个段的信息,如长度.
\item 确定内存布局,为模块分配内存.
\item 把各个段的内容转移到刚才分配的内存中.
\item 解析符号,把引用的符号重新变为直接指向变量的指针.
\item 重定位,调整代码中的地址使其与改变后的地址对应.
\item 传递参数,调用模块初始化的代码.
\end{enumerate}
动态的符号解析是模块之间互操作性的基础.
内核中不是可加载模块的部分遵循一般的C语言链接的规则,
而如果要暴露出可供模块使用的符号,
就要使用 \lstinline{export_symbol*} 这一类的宏来把该符号会被加入到几张特殊的符号表中. \cite{Unreliab5:online}
加载模块时,\lstinline{simplify_symbols} 函数会在这些表中找到被导出的符号.
如果要使用的符号来源于另一个模块,则这两个模块之间存在依赖关系,
内核会在每个模块的内存表示中记录其依赖的所有模块和依赖它的所有模块,
模块之间的依赖关系可以帮助确定卸载模块的顺序等.
内核是如何知道要加载哪些模块的呢?
模块的加载一般是由用户态的程序发起的.
Arch Linux的启动过程\cite{archboot:online}中,首先是bootloader加载vmlinux镜像,
这时内核的根文件系统是内存中的逻辑上的文件系统,
称为initramfs(initial RAM file system).\index{initramfs}
内核随后把磁盘上的initramfs镜像解压到该文件系统中,
这时就进入了“早期用户空间”阶段.\index{early userspace}
Arch Linux 的 init 系统采用的是systemd,\index{systemd}
systemd此时会启动一个名为 \lstinline{systemd-modules-load} 的服务
\footnote{位于\lstinline{/usr/lib/systemd/system/systemd-modules-load.service}},
该服务启动一个同名的程序,
来根据用户的配置文件加载内核启动早期所需要的模块.
当真正的根文件系统被挂载后,内核模块的自动加载就由systemd的udev系统\index{udev}负责,
udev根据设备的变化、事件的发生来自动加载和卸载所需要的内核模块,
以此为设备提供驱动程序和增改内核的功能.
\begin{notebox}
内核模块也可以手动管理.
\href{https://git.kernel.org/pub/scm/utils/kernel/kmod/kmod.git}{kmod}\index{kmod}
是Linux用户态的模块管理程序和库.
常用的命令有:
\begin{itemize}
\item 加载模块: \lstinline{modprobe 模块名}
\item 卸载模块: \lstinline{rmmod 模块名}
\item 显示依赖: \lstinline{modprobe --show-depends 模块名}
\end{itemize}
\end{notebox}
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "linux_zh"
%%% End: