\label{sec:kernelmod}
参与 Linux 内核模块的开发需要具备 C 编程语言基础,以及有创建用于进程执行的传统%
-程序的记录。这种追求深入研究了一个领域,其中不受监管的指针,如果被忽视,可能会%
-潜在地触发整个文件系统的彻底消除,进而导致需要完全重新启动系统的情况。
+程序的记录。这需追求深入研究了一个领域,其中如果忽视不受监管的指针,可能会%
+潜在地触发整个文件系统的彻底消除,进而导致需要完全重新启动系统的情况发生。
Linux 内核模块被精确地定义为,在内核需要时,能够在内核中动态加载和卸载的代码段。%
这些模块在不需要重新启动系统的情况下,能增强内核的能力。一个值得注意的例子是设%
Linux 发行版将命令 \sh|modprobe|, \sh|insmod| 与 \sh|depmod| 放入一个软件包中。%
-在 Ubuntu/Debian GNU/Linux 上,运行如带参数的命令:
+在 Ubuntu/Debian GNU/Linux 上,运行如下带参数的命令:
\begin{codebash}
sudo apt-get install build-essential kmod
\end{codebash}
\section{在我们开始之前}
\label{sec:preparation}
-å\9c¨æ\88\91们深å\85¥ç \94究代ç \81ä¹\8bå\89\8dï¼\8cæ\9c\89äº\9bæ\9d\90æ\96\99é\9c\80è¦\81注æ\84\8fã\80\82差导å\98å\9c¨äº\8eæ¯\8f个人ç\9a\84ç³»ç»\9fï¼\8cä¸\8e不同个体的%
-方法,这是明显的事实。这个成功编译并加载的庆典``hello world''程序的成就,可能有%
+å\9c¨æ\88\91们深å\85¥ç \94究代ç \81ä¹\8bå\89\8dï¼\8cæ\9c\89äº\9bæ\9d\90æ\96\99é\9c\80è¦\81注æ\84\8fã\80\82å·®å¼\82å\98å\9c¨äº\8eæ¯\8f个人ç\9a\84ç³»ç»\9fï¼\8cå\8f\8a不同个体的%
+方法,这是明显的事实。这个成功编译并加载的庆祝\,``hello world''\,程序的成就,可能有%
时会带来挑战。令人欣慰的是,我们克服了首次尝试中的最初障碍,这为后续工作铺平了道%
路,以使其平顺地进行。
\begin{enumerate}
\item 模块版本。
为一个内核编译的一个模块,如编译时内核版本与已启动内核不同时,模块将不%
- 能加载,除非在内核中打 \cpp|CONFIG_MODVERSIONS| 选项。模块版本在本指南%
- 稍后进行讨论。直到解决模块版本问题,本指南中的示例,如果运行于一个打开%
+ 能加载,除非在内核中打开 \cpp|CONFIG_MODVERSIONS| 选项。模块版本在本指南%
+ 稍后进行讨论。直到解决模块版本问题,本指南中的示例,如果运行在一个打开%
模块版本选项的内核上,可能不会正常工作。然而,大多数现有 Linux 发行版本%
的内核,都打开了模块版本选项。如果在加载模块时,由于版本错误提升了难度,%
考虑编译一个使用关闭模块版本选项的内核。
至附带默认内核配置,以支持 SecureBoot。在这种情况下,内核模块需要一个被%
签名的安全密钥。
- 如果失败,尝试插入你第一个``hello world''模块,这将产生消息 ``\emph{ERROR:%
+ 如果失败,尝试插入你第一个\,``hello world''\,模块,这将产生消息 ``\emph{ERROR:%
could not insert module}''。如果在命令 \sh|dmesg| 中产生是这个消息 %
\emph{Lockdown: insmod: unsigned module loading is restricted; see man %
kernel lockdown.7},解决问题最简单的方法,是在你的 PC 或笔记本电脑上的启%
apt-cache search linux-headers-`uname -r`
\end{codebash}
-这将告诉你什么内核头文件可用。那么如示例安装头文件:
+这将告诉你什么内核头文件可用。那么如下面示例来安装头文件:
\begin{codebash}
sudo apt-get install kmod linux-headers-5.4.0-80-generic
\end{codebash}
-在 Arch Linux 发行版的系统上,运行:
+在 Arch Linux 发行版的系统上,运行如下命令:
\begin{codebash}
sudo pacman -S linux-headers
\end{codebash}
-在 Fedora 发行版的系统上,运行:
+在 Fedora 发行版的系统上,运行如下命令:
\begin{codebash}
sudo dnf install kernel-devel kernel-headers
\end{codebash}
\label{sec:examples}
所有来自本文档的示例代码,都能从 \verb|examples| 子目录中找到。
-如果这里有任何编译错误,那么你可能需要一个更现代的内核版本,或需要安装相应的内%
+如果这里有任何编译错误,那么你可能需要一个更为现代的内核版本,或需要安装相应的内%
核头文件包。
\chapter{Hello World}
\section{最简单的模块}
\label{sec:org2d3e245}
大多数人通过某种"\emph{hello world}"示例来开始学习编程。我不清楚打破这个传统后%%
-会对人们会发生什么,但我认为不出头是更为安全的。我们将开启一系列 hello world 编%
-程旅程,来演示编写内核模块的基础的不同形态。
+会对人们会发生什么,但我深为认同``出头的椽子先烂''的古训。我们将开启一系列 hello %
+world 编程旅程,来演示编写内核模块的不同形态基础知识。
这里可能是最简单的模块。
\samplec{examples/hello-1.c}
现在你将需要一个 \verb|Makefile|。如果你拷贝并粘贴这些内容,并使用 \textit{tabs} %
-符而不是空格符来替换行首的缩进,修改已经被粘贴的内容。
+符而不是空格符来替换行首的缩进,修改已经被粘贴的内容\footnote{以遵从 Makefile 文%
+件编写规则,相关知识建议参阅 GNU Make 手册}。
\begin{code}
obj-m += hello-1.o
(毕竟 \verb|-C| 选项会被处理,无论怎样)。查看更多关于 \verb|CURDIR| 信息,阅读 %
\href{https://www.gnu.org/software/make/manual/make.html}{《GNU make 手册》}。
-最终,只直接运行 \verb|make|。
+最终,只要直接运行 \verb|make|。
\begin{codebash}
make
安全策略是 \verb|sudoers|。在 \verb|sudoers| 安全策略中,\verb|env_reset| 选项%
默认被打开,这个打开的选项会限制环境变量。尤其是,路径变量不会在用户环境中被保%
持,他们被设定为默认值(如了解更多信息,查看: \href{https://www.sudo.ws/docs/%
-man/sudoers.man/}{sudoers 手册})。你可以通过下面命令来查看环境变量设置:
+man/sudoers.man/}{sudoers 手册})。你可以通过下面命令\footnote{细心的读者,应能%
+区分出运行这两次命令时,所使用的不同身份。这需要读者熟悉 Unix 命令行运行环境。%
+}来查看环境变量设置:
\begin{verbatim}
$ sudo -s
echo $(PWD)
\end{code}
-然后,我们可以使用 \verb|-p| 标记,从 Makefile 中打印出环境变量的值。
+然后,我们可以使用 \verb|-p| 标记,从 Makefile 中打印出环境变量的值。%
\begin{verbatim}
$ make -p | grep PWD
}
\end{enumerate}
-如所有操作都如行云流水,那么你发现你已经有一个被编译的 \verb|hello-1.ko| 模块。
-你可使用下面命令来查看模块信息:
+如所有操作都似行云流水,那么你发现已拥有一个被编译的 \verb|hello-1.ko| 模块。你%
+可使用下面命令来查看模块信息:
\begin{codebash}
modinfo hello-1.ko
\end{codebash}
你现在知道关于创建,编译,安装与移除模块的基础。现在对模块如何工作做进一步说明。
模块至少需有两个函数:一个``开始''(初始化)函数,称为 \verb|init_module()|,
-当模块用 \verb|insmod| 命令被加载到内核中时被调用,另一个``结束''(清除)函数,称%
-为 \verb|cleanup_module()| 函数,录模块从内核被移除之前的时间点,其被调用。实际%
+当模块用 \verb|insmod| 命令被加载到内核中时刻被调用,另一个``结束''(清除)函数,%
+称为 \verb|cleanup_module()| 函数,当模块从内核被移除之前的时刻其被调用。实际%
上,该情况从内核 2.3.13 开始有所改变。你现在可以使用任何你喜欢的名称为模块的开%
始与结束函数命名,并且你将在 \ref{hello_n_goodbye} 节学习这样做。
事实上,新的方法是更令人偏爱的方法。
通常,\cpp|init_module()| 或者向内核注册来处理一些事务,或者用模块代码来替换内%
核函数之一(通常代码做些事务,并在之后调用最初被替换的函数)。%
\verb|cleanup_module()| 函数被假定,无论 \verb|init_module()| 函数做什么,在移%
-除模块时,该函数都做出相反的行为,只有这样模块可以被安全的卸载。
+除模块时,该函数都做出相反的行为,只有这样模块可以被安全卸载。
最后,每个内核模块需要引入 \verb|<linux/module.h>| 头文件。只有在我们需要宏扩展 %
\verb|pr_alert()| 日志级别时,我们被需要引入头文件 \verb|<linux/printk.h>|。你%
-将在小节 \ref{sec:printk} 中学习相关知识。
+将在 \ref{sec:printk} 节中学习相关知识。
\begin{enumerate}
- \item 关于代码风格的一点。
+ \item 强è°\83å\85³äº\8e代ç \81é£\8eæ ¼ç\9a\84ä¸\80ç\82¹ã\80\82
对每个开始内核编程的人来说,另一件不能立即察觉到的事,是在你的代码中使%
用 \textbf{制表符} 与 \textbf{非空格} 作为缩进。
它是内核编码的一种代码风格约定。
\label{sec:printk}
开始之前,这里有个宏 \cpp|printk|,常伴随着一个优先级,如 \cpp|KERN_INFO| %
或 \verb|KERN_DEBUG|。
- 最近这些也可用一系列打印宏,用缩略形式而被表达,如 \verb|pr_info| 和 %
+ 最近这些也可用一系列缩略形式进行表达,如 \verb|pr_info| 和 %
\verb|pr_debug|。这仅仅节省一些无脑的键盘敲击,并使用代码显得有些整洁。%
- 他们可以 \src{include/linux/printk.h} 中找到。花些时间通读可用的优先级%
- 宏的代码。
+ 它们可以从 \src{include/linux/printk.h} 中找到。花些时间通读可用的优先%
+ 级的宏代码。
\item 关于编译。
内核模块的编译与普通用户空间应用程序的编译,有些许不同。早先的内核版本%
- 需要我们留心这些配置,配置通常被存储在 Makefile 文件中。尽管分层被组组,%
- 许å¤\9aå\86\97ä½\99é\85\8dç½®å\9c¨å\90ç\9b®å½\95ç\9a\84 Makefile ä¸è¢«ç´¯ç§¯ï¼\8cè¿\99使å¾\97é\85\8dç½®å\86\85容é\9d\9e常åº\9e大并æ\9b´é\9a¾%
- 于维护。幸运地是,这里有一种新方法来做这些事,被称为 kbuild,并且对于外%
- 部可加载模块的编译过程,现在完全被集成到标准的内核编译机制中。要学习更%
- 多关于如何编译模块的知识,不是官方内核的一部分内容(如你在本指南找到的所%
- 有示例),查看文件 \src{Documentation/kbuild/modules.rst}。
+ 需要我们留心这些配置,配置通常被存储在 Makefile 文件中。尽管 Makefile %
+ 被å\88\86å±\82ç»\84ç»\84ï¼\8c许å¤\9aå\86\97ä½\99é\85\8dç½®å\9c¨å\90ç\9b®å½\95ç\9a\84 Makefile ä¸è¢«ç´¯ç§¯ï¼\8cè¿\99使å¾\97é\85\8dç½®å\86\85容é\9d\9e%
+ 常庞大并更难于维护。幸运地是,这里有一种新方法来做这些事,被称为 kbuild,%
+ 并且对于外部可加载模块的编译过程,现在完全被集成到标准的内核编译机制中。%
+ 要学习更多关于如何编译模块的知识,不是官方内核的一部分内容(如你在本指南%
+ æ\89¾å\88°ç\9a\84æ\89\80æ\9c\89示ä¾\8b)ï¼\8cæ\9f¥ç\9c\8bæ\96\87ä»¶ \src{Documentation/kbuild/modules.rst}ã\80\82
对内核模块来说,关于 Makefile 文件更细致的信息,可以从内核源代码中文件 %
\src{Documentation/kbuild/makefiles.rst} 中找到。确保读过该文件以及相关%
\section{Hello\,与\,Goodbye}
\label{hello_n_goodbye}
在早期内核版本中,你必须查看 \cpp|init_module| 与 \cpp|cleanup_module| 函数,如%
-同在第一个 hello world 示例,但近些天你可以使用 \cpp|module_init| 和 %
-\cpp|module_exit| 宏来任意命名这两个函数。这些宏在 \src{include/linux/module.h}%
- 中被定义。唯一需要注意的是,在调用这些宏之前,你的初始化与清除函数必须已经被%
+同在第一个 hello world 示例,但最近你可以使用 \cpp|module_init| 和 %
+\cpp|module_exit| 宏来任意命名这两个函数。这些宏在 \src{include/linux/module.h} %
+中被定义。唯一需要注意的是,在调用这些宏之前,你的初始化与清除函数必须已经被%
定义过,否则你将得到编译错误。这里有关于这个技术的一个示例:
\samplec{examples/hello-2.c}
\section{\_\_init 与 \_\_exit 宏}
\label{init_n_exit}
-\cpp|__init| å®\8få¼\95èµ· init å\87½æ\95°è¢«ä¸¢å¼\83ï¼\8cå¹¶ä¸\94ï¼\8c对äº\8eåµ\8cå\85¥驱动来说,一旦 init 函数结束,%
-其点用的内存会被释放,但这不适用于可加载内核。当你思考当 init 函数被调用时有问%
-题,这是完全有道理的。
+\cpp|__init| å®\8få¼\95èµ· init å\87½æ\95°è¢«ä¸¢å¼\83ï¼\8cå¹¶ä¸\94ï¼\8c对äº\8eå\86\85åµ\8c驱动来说,一旦 init 函数结束,%
+其占用的内存会被释放,但种情况这不适用于可加载内核。如果你思考当 init 函数被调%
+用,这会产生完美的场景。
-这里也有一个 \cpp|__initdata| 宏,其工作与 \cpp|__init| 但只初始化变量,而不是%
-函数。
+这里也有一个 \cpp|__initdata| 宏,其工作与 \cpp|__init| 相似,但它只初始化变量,%
+而不是初始化函数。
-\cpp|__exit| 宏在模块被编译进内核时,引起省略该函数,并且如 \cpp|__init|,对于%
-可加载的内核模块不产生效果。再次提示,当 cleanup 函数运行时,如果你考虑,这种情%
-å\86µæ\9b´ä¸ºå\90\88ç\90\86ï¼\9båµ\8cå\85¥å\86\85æ ¸ç\9a\84驱动不需要一个 cleanup 函数,然而,可加载模块需要该函数。
+\cpp|__exit| 宏在模块被编译进内核时,引起省略该函数,并且如同 \cpp|__init|,对于%
+可加载内核模块不产生效果。再次提示,当 cleanup 函数运行时,如果你想想,这种情%
+å\86µæ\9b´ä¸ºå\90\88ç\90\86ï¼\9aåµ\8cå\85¥å\86\85æ ¸驱动不需要一个 cleanup 函数,然而,可加载模块需要该函数。
-这些宏在 \src{include/linux/init.h} 文件中被定义,并负责来释放内核点用的内存。%
-当你启动内核并看到如 Freeing unused kernel memory:236k free,这正是内核释入内%
+这些宏在 \src{include/linux/init.h} 文件中被定义,并负责来释放内核占用的内存。%
+当你启动内核并看到如 Freeing unused kernel memory:236k free,这正是内核释放内%
存的内容。
\samplec{examples/hello-3.c}