From f2267f06cb1e88d34c2947120fb97697366bc0e7 Mon Sep 17 00:00:00 2001 From: Felix Lee Date: Wed, 20 Sep 2023 22:50:19 +0800 Subject: [PATCH] revise before chapter interrupt --- lkmpg_cn.tex | 309 ++++++++++++++++++++++++++------------------------- 1 file changed, 155 insertions(+), 154 deletions(-) diff --git a/lkmpg_cn.tex b/lkmpg_cn.tex index 5ece17e..2174396 100644 --- a/lkmpg_cn.tex +++ b/lkmpg_cn.tex @@ -1426,18 +1426,18 @@ HelloWorld! \chapter{sysfs: 同你的模块交互} \label{sec:sysfs} -\emph{sysfs} 允许你在用户空间,通过读或设置模块中的变量,来与正在运行的内核进行% -交互。调试进程时,或作为应用程序或脚本的一个接口,这很有用。你可以在你的系统上% +\emph{sysfs} 允许你在用户空间,通过读或设置模块中的变量,与正在运行的内核进行% +交互。出于调试目的,或作为应用程序或脚本的一个接口,这很有用。可以在你的系统上% 的 \verb|/sys| 目录下找到 sysfs 目录与文件。 \begin{codebash} ls -l /sys \end{codebash} -在文件系统中,对于 kobjects 的属性可以用普通文件的形式,被输出。Sysfs 将文件 I/O % -操作转发到为属性定义的方法,提供读取和写入内核属性的手段。 +在文件系统中,对于 kobjects 的属性可以通过普通文件的形式被输出。Sysfs 将文件 I/O % +操作转发到作为属性被定义的方法,这些方法提供读取和写入内核属性的手段。 -简单的属性定义: +一个属性定义非常简单: \begin{code} struct attribute { @@ -1466,10 +1466,11 @@ void device_remove_file(struct device *, const struct device_attribute *); \end{code} 要读或写属性,当声明属性时,\cpp|show()| 或 \cpp|store()| 方法必须被指定。对大% -多数情况 \src{include/linux/sysfs.h} 提供易于的宏(\cpp|__ATTR|,\cpp|__ATT_RO|,% -\cpp|__ATTR_WO|,等等。)以简化属性定义,同时令代码更精简与可读。 +多数情况 \src{include/linux/sysfs.h} 提供了易于被使用的宏(\cpp|__ATTR|,% +\cpp|__ATT_RO|,以及 \cpp|__ATTR_WO|,等等。)以简化属性定义,并让代码更为精简% +且更具可读性。 -一个 hello world 示例如下结出,该示例包含可由 sysfs 访问的一个变量的创建。 +一个 hello world 模块如下所示,其包含通过如下 sysfs 访问的一个变量的创建。 \samplec{examples/hello-sysfs.c} @@ -1480,7 +1481,7 @@ make sudo insmod hello-sysfs.ko \end{codebash} -检查模块的存在: +检查模块是否存在: \begin{codebash} sudo lsmod | grep hello_sysfs @@ -1492,7 +1493,7 @@ sudo lsmod | grep hello_sysfs cat /sys/kernel/mymodule/myvariable \end{codebash} -设置 \cpp|myvariable| 的值并检查它的改变。 +设置 \cpp|myvariable| 的值并检查它已被改变。 \begin{codebash} echo "32" > /sys/kernel/mymodule/myvariable @@ -1505,55 +1506,51 @@ cat /sys/kernel/mymodule/myvariable sudo rmmod hello_sysfs \end{codebash} -在上面的操作过程中,我们用一个简单 kobject 来在 sysfs 下创建一个目录,并与它的% -属性进行通讯。自从 Linux v2.6.0 \cpp|kobject| 数据结构初露峥嵘。它最初的目的是% -作为一种统一管理引用计数对象的内核代码的简单方法。经过一些任务的扩展之后,它现% -在成为将设备模型的大部分及其 sysfs 接口粘合在一起的粘合剂。对于更多关于 kobject % -与 sysfs 的信息,查看 \src{Documentation/driver-api/driver-model/driver.rst} 与% -\url{https://lwn.net/Articles/51437/}。 +在上面的情况中,我们用一个简单 kobject 来在 sysfs 下创建一个目录,并且与它的% +属性进行通讯。自从 Linux v2.6.0 \cpp|kobject| 数据结构初露峥嵘。设计它最初的意% +图是作为一种统一管理引用计数对象的,内核代码的简单方法。经过一些任务的扩展之后,% +它现在成为将设备模型的大部分及其 sysfs 接口粘合在一起的粘合剂。对于更多关于 % +kobject 与 sysfs 的信息,查看 \src{Documentation/driver-api/driver-model/driver.rst} % +与 \url{https://lwn.net/Articles/51437/}。 \chapter{与设备文件对话} \label{sec:device_files} -设备应该表示物理设备。大多数物理设备都用于输出和输入,因此内核中的设备驱动程序% -必须有某种机制来获取输出,以及,从进程发送到设备。通过以输出与写入为目标,对于% -设备文件的打开操作的做法,就象向一个文件写入数据。以下示例,通过 \cpp|device_write| % -而被执行。 +设备应该表示物理设备。大多数物理设备被用于输出及输入,因此对内核中的设备驱动来% +说,必须有一些机制,实现从得到输出,到从进程发送数据到设备。通过以输出与写入自% +身为目的,打开设备文件来做到,就象写入一个文件。在下面的示例,其通过 % +\cpp|device_write| 而被执行。 -大多数物理设备被用于输出及输入,因此对内核中的设备驱动来说,必须有一些机制,实% -现从得到输出,到从进程发送数据到设备。通过以输出与写入自身为目的,打开设备文件% -来做到,就象写入一个文件。在下面的示例,其通过 \cpp|device_write| 而被执行。 - -这样远远不够。想象你有一个串行端口被连接到一个 modem(甚至你有一个内部 modem,% +这样远远不够。想象你有一个串行端口被连接到一个 modem\,(甚至你有一个内部 modem,% 从 CPU 的视角看来,它依然被作为一个串行端口连接到一个 modem 来被执行,因此不必% 为你的想象力税赋犯难)。自然的做法是使用设备文件将内容写入 modem (或者 modem 命% 令,或者是通过电话线传送的数据)并从 modem 读取内容(或者是命令的应答,或者是通过% 电话线接收的数据)。然而,这留下了一个问题:当您需要与串行端口本身通信时该怎么办,% 例如配置发送和接收数据的速率。 -在 Unix 中答案是使用一个被称为 \cpp|ioctl|(输入输出控制的缩写) 的特殊函数。每个% -设备可拥有自己的 \cpp|ioctl| 命令,可以读取 ioctl(从一个进程发送信息到内核),或% -写入 ioctl(向进程返回信息),同时执行两者,或同时不执行两者。注意这里的读和写的% -角色又再次相反了,因在 ioctl 中的读是发送信息到内核,且写入是从内核接收数据。 +在 Unix 中答案是使用一个被称为 \cpp|ioctl|\,(\,输入输出控制的缩写) 的特殊函数。% +每个设备可拥有自己的 \cpp|ioctl| 命令,可以读取 ioctl\,(\,从一个进程发送信息到% +内核\,),或写入 ioctl\,(\,向进程返回信息\,),同时执行两者,或同时不执行两者。注% +意这里的读和写的角色又再次变反了,因在 ioctl 中的读是发送信息到内核,且写入是从% +内核接收数据。 ioctl 函数被调用时,使用三个参数:对应设备文件的文件说明符,ioctl 数字,还有一% -个参数,它是一个 long 类型,因此你可利用强制转换来传递任何数据。在这种方法中,% -你不能传递一个结构,但你可以传递一个指向结构的指针。 +个参数,它是一个 long 整数类型,因此你可利用强制类型转换来传递任何数据。在这种% +方法中,你不能传递一个结构,但你可以传递一个指向结构的指针。 这里有一个示例: \samplec{examples/ioctl.c} -你可以看到,在 \cpp|test_ioctl_ioctl()| 函数中,被称为 \cpp|cmd| 的参数被调用。% -它是一个 ioctl 数字。ioctl 数字编码主设备号,ioctl 的类型,命令,与参数类型。% -ioctl 数字在头文件中,通常通过一个宏调用(\cpp|_IO|,\cpp|__IOR|,\cpp|_IOW| 或 % -\cpp|_IOWR|,其依赖于类型)来创建。这个头文件然后应当同时被使用 ioctl 的程序(因% -此可以产生相应的 ioctl)包含,并且还被内核模块(因此它可理解相关操作)包含。在下面% -的示例中,头文件是 \verb|chardev.h| 并且程序在 \verb|userspace_ioctl.c| 文件中% -使用它。 +你可以看到,在 \cpp|test_ioctl_ioctl()| 函数中有一个参数被称为 \cpp|cmd|。% +它是一个 ioctl 数字。ioctl 数字对主设备号,ioctl 的类型,命令,与参数类型时行% +编码。ioctl 数字在头文件中,通常通过一个宏调用(\cpp|_IO|,\cpp|__IOR|,\cpp|_IOW| % +或 \cpp|_IOWR|,其依赖于类型)来创建。这个头文件之后应当同时被使用 ioctl 的程序(因% +此可以产生相应的 ioctl),以及内核模块(因此它可以理解它)包含。在下面的示例中,头% +文件是 \verb|chardev.h| 并且程序在 \verb|userspace_ioctl.c| 文件中使用它。 -如果你想在你自己的内核中使用 ioctl,最好接收一个官方 ioctl 参数,因此如果你意外% -收到其它人的不同 ioctl,或如果他们收到你的 ioctl,你将知道有地方出错了。更多信息,% -查看内核源代码树中 \src{Documentation/userspace-api/ioctl/ioctl-number.rst}。 +如果你想在自己的内核中使用 ioctl,最好接收一个官方 ioctl 参数,因为你如果意外% +收到其它人的不同 ioctl,或他们如果收到你的 ioctl,你将知道有地方出错了。更多信息,% +查看内核源代码树中 \src{Documentation/userspace-api/ioctl/ioctl-number.rst} 的内容。 另外,我们需要小心对共享资源的并发访问,将导致竞争条件产生。解决方法是使用原子 % Compare-And-Swap (CAS),我们在 \ref{sec:chardev_c} 中提到了它,用它来强制执行% @@ -1567,7 +1564,7 @@ Compare-And-Swap (CAS),我们在 \ref{sec:chardev_c} 中提到了它,用它 \chapter{系统调用} \label{sec:syscall} -到目前为止,我们所做的唯一的一件事情,就是使用定义良好的内核机制来注册 \verb|/proc| % +到目前为止,我们只作了一件事,就是用良好定义的内核机制来注册 \verb|/proc| % 文件和设备处理程序。如果你想做内核程序员认为值得做的事,例如编写设备驱动程序,% 那么这很好。但是,如果你想做一些不寻常的事情,以某种方式改变系统的行为,该怎么% 办?然后,你就只能靠自己了。 @@ -1575,56 +1572,56 @@ Compare-And-Swap (CAS),我们在 \ref{sec:chardev_c} 中提到了它,用它 如果您不明智地使用虚拟机,那么这就是内核编程可能变得危险的地方。在编写下面的示% 例时,我杀死了 \cpp|open()| 系统调用。这意味着我无法打开任何文件,无法运行任何% 程序,也无法关闭系统。我不得不重新启动虚拟机。没有重要的文件被消灭,但如果我在% -一些实时任务关键系统上执行此操作,那么这可能是一个可能的结果。要确保不要丢失任% -何文件,甚至在一个测试环境中,请在执行 \sh|insmod| 和 \sh|rmmod| 命令之前运行 % -\sh|sync| 命令。 +一些实时任务很关键的系统上执行此操作,那么这或许是一个可能的结果。要确保不要丢% +失任何文件,甚至在一个测试环境中,请在执行 \sh|insmod| 和 \sh|rmmod| 命令之前% +运行 \sh|sync| 命令。 忘记 \verb|/proc| 文件,忘记设备文件。它们只是次要的细节。浩瀚宇宙的细节。真正% -的进程到内核的通信机制,即所有进程都使用的机制,是 \emph{系统调用}。当进程向内% +的进程到内核的通信机制,即所有进程都使用的机制,是{\bf{系统调用}\/}。当进程向内% 核请求服务时(例如打开文件,或请求更多内存),这就是被用到的机制。如果你想以有趣% -的方式改变内核行为,这里就是你可以做到的地方。顺便说一句,如果你想查看程序使用% +的方式改变内核行为,这里就是你可以施展的地方。顺便说一句,如果你想查看程序使用% 哪个系统调用,请运行 \sh|strace |。 -总之,一个进程不被支持有能力去访问内核。它不能访问内核内存,并且它不能调用内核% -函数。CPU 的硬件增强这方面的功能(这就是为什么它被称为``保护模式''或``页面保护''% -的原因)。 +总之,无情的现实是,一个进程被废除了访问内核的能力。它不能访问内核内存,并且它% +不能调用内核函数。CPU 的硬件增强这方面的功能(这就是为什么它被称为``保护模式''或% +``页面保护''的原因)。 系统调用是对通用规则的一个例外。在进程用适当的值填充寄存器,然后调用一条特殊指% -令,该指令跳转到内核中先前定义的位置(当然,该位置可以由用户进程读取,但不能写% -入)。在 Intel CPU 上,这是通过中断 0x80 完成的。硬件知道,一旦你跳转到这个位置,% -你就不再运行受限用户模式下,而是作为操作系统内核运行,因此你可以做任何你想做的% -事情。 - +令,该指令跳转到内核中先前定义的位置(当然,该位置可以由用户进程读取,但不能被% +它们写入)。在 Intel CPU 上,这是通过中断 0x80 完成的。硬件知道,一旦你跳转到这个% +位置,你就不再运行于受限用户模式下,而是作为操作系统内核运行,因此你可以做任何你% +想做的事情。 进程在内核中可以跳转到的位置被称为 \verb|system_call|。该位置的例程检查系统调用% -号,它告诉内核进程请求什么服务。然后,它查看系统调用表(\cpp|sys_call_table|)以% -查看要调用的内核函数地址。然后它调用该函数,并在返回后执行一些系统检查,然后返% -回到进程(或者返回到另一个进程,如果进程时间用完)。如想读该代码,看 % -\verb|arch/$(architecture)/kernel/entry.S|,中 \cpp|ENTRY(system_call)|。% -行后的代码。 +号,该系统调用号告诉内核,进程请求什么服务。然后,它查看系统调用表 % +(\cpp|sys_call_table|\,)\,以查看要调用的内核函数地址。然后它调用该函数,并在返% +回后执行一些系统检查,然后返回到进程(或者返回到另一个进程,如进程耗尽 CPU 分配% +给它的占用时间片)。如想读这些代码,查看 \verb|arch/$(architecture)/kernel/entry.S| % +中 \cpp|ENTRY(system_call)| 行后的代码。 所以,如果我们想要改变某个系统调用的工作方式,我们需要做的就是编写自己的函数来% -实现它(通常是添加一点我们自己的代码,然后调用原来的函数)。然后改变 \cpp|sys_call_table| % -处的指针,指向我们的函数。因为我们可能被移除,并且我们不想让系统处于不稳定状态,% -所以这对于 \cpp|cleanup_module| 恢复表到它是初状态很重要。 +实现它\,(\,通常是添加一点我们自己的代码,然后调用原来的函数)。然后改变 % +\cpp|sys_call_table| 处的指针,指向我们的函数。因为我们的函数可能被移除,并且% +我们不想让系统处于不稳定状态,所以这对于让 \cpp|cleanup_module| 恢复系统调用表% +到最初状态来说,很重要。 -要修改 \cpp|sys_call_table| 的内容,我们需要考虑控制寄存器。一个控制寄存器是改变% +要修改 \cpp|sys_call_table| 的内容,我们需要斟酌控制寄存器。一个控制寄存器是改变% 或控制 CPU 一般行为的处理器寄存器。对于 x86 架构,\verb|cr0| 寄存器具有修改处理% -器基本操作的各种控制标志。在 \verb|cr0| 中的 \verb|WP| 标志代表写保护。一旦\verb|WP| % -标记被设置,处理器禁止未来试图对只读小节的数据的写入操作。因此,我们必须在更改 % +器基本操作的各种控制标志。在 \verb|cr0| 中的 \verb|WP| 标志代表写保护。一旦 % +\verb|WP| 标记被设置,处理器禁止之后试图对只读段的写入操作。因此,我们必须在更改 % \cpp|sys_call_table| 之前禁止 \verb|WP| 标记。因此 Linux v5.3 开始,\cpp|write_cr0| % -由于 \verb|cr0| 比特敏感,无法使用该函数,由于安全问题所锁定的位,攻击者可能会% -写入 CPU 控制器寄存器,以禁用写保护等 CPU 保护。因此,我们必须提供自定义汇编例% -程来绕过它。 +函数不能被使用,由于 \verb|cr0| 比特位的敏感性带来的安全问题,攻击者可能会%写入 CPU % +控制器寄存器,以禁用写保护等 CPU 保护。因此,我们必须提供自定义汇编例程来绕过它。 然而,\cpp|sys_call_table| 符号未导出以防止误用。但是获取符号的方法很少,手动符% -号查找和 \cpp|kallsyms_lookup_name|。这里我们使用两者取决于内核版本。 +号查找和 \cpp|kallsyms_lookup_name| 可用的两种方法。这里我们使用两者取决于内核% +版本。 由于 \textit{control-flow-integrity},这是一种防止攻击者重定向执行代码的技术,% -以确保间接调用到达预期地址并且返回地址不被更改。从 Linux v5.7 开始,内核为 x86 % -修补了一系列 \textit{control-flow-enforcement} (CET)。并且 GCC 的某些配置,例如% +以确保间接调用到达预期地址,并且返回地址不被更改。从 Linux v5.7 开始,内核为 x86 % +修补了一系列 \textit{control-flow-enforcement} (CET)。并且 GCC 的某些配置,例如 % Ubuntu Linux 中的 GCC 版本\,9\,和\,10,默认情况下会在内核中添加\,CET\, (\verb|-fcf-protection| % -选项)。使用该 GCC 在关闭 retpoline 时编译内核,可能会导致在内核中启用了 CET。% +选项)。使用该 GCC 在关闭 retpoline 选项时编译内核,可能会导致在内核中启用了 CET。% 你可以使用以下命令查看 \verb|-fcf-protection| 选项是否启用: \begin{verbnobox}[\fontsize{7pt}{7pt}\selectfont] @@ -1641,15 +1638,15 @@ COLLECT_GCC_OPTIONS='-v' '-Q' '-O2' '--help=target' '-mtune=generic' '-march=x86 \end{verbnobox} 但在内核中 CET 不应被打开,开启后可能中断 Kprobes 与 bfp。最后,CET 从 v5.11 开% -始被禁止。为保证手动符号查找有效,我们最多只能使用 v5.4。 +始被禁止。为保证手动符号查找有效,我们最多只能使用 v5.4 版本内核。 -不幸的是,自从 Linux v5.7 \cpp|kallsyms_lookup_name| 也不被输出,需要一定的技巧% -才能获取 \cpp|kallsyms_lookup_name| 的地址。如果 \cpp|CONFIG_KPROBES| 被打开,% -我们可以方便地通过 Kprobes 检索函数地址,从而动态地侵入特定的内核例程。Kprobes % -通过替换探测指令的第一个字节在函数入口处插入断点。当 CPU 到达断点时,寄存器被存% -储,控制权将传递给 Kprobes。它将保存的寄存器的地址和 Kprobe 结构传递给你定义的% -处理程序,然后执行它。Kprobes 可以通过符号名称或地址来注册。在符号名称中,地址% -将由内核处理。 +不幸的是,自从 Linux v5.7 开始 \cpp|kallsyms_lookup_name| 也不被输出,需要一定% +的技巧才能获取 \cpp|kallsyms_lookup_name| 的地址。如果 \cpp|CONFIG_KPROBES| 选% +项被打开,我们可以方便地通过 Kprobes 检索函数地址,从而动态地侵入特定的内核例程。% +Kprobes 通过替换探测指令的第一个字节在函数入口处插入断点。当 CPU 到达断点时,寄% +存器被存储,控制权将传递给 Kprobes。它将保存的寄存器的地址和 Kprobe 结构传递给% +你定义的处理程序,然后执行它。Kprobes 可以通过符号名称或地址来注册。在符号名称% +中,地址将由内核处理。 否则,从 \verb|/proc/kallsyms| 和 \verb|/boot/System.map| 指定 \cpp|sys_call_table| % 的地址进入 \cpp|sym| 范围。以下是 \verb|/proc/kallsyms| 的示例的用法: @@ -1667,9 +1664,10 @@ Randomization)。KASLR 在每次启动时,随机分配内核代码与数据的 件 \verb|/boot/System.map| 中列出的地址,将会偏移一些熵(随机值)。KASLR 的目的是% 防止攻击者对内核空间的攻击。没有 KASLR,攻击者可以非常容易地发现固定地址的攻击% 地址。那么攻击者就可以利用面向返回的编程方式,插入一些恶意代码,通过被篡改的指针% -来执行或接收目标数据。KASLR 治愈这种攻击,因为攻击者不能立即知道目标地址,但一% -个暴力破解攻击还可以实施攻击。如果在 \src{/proc/kallsyms} 一个符号的地址与大文件 % -\verb|/boot/System.map| 中的地址不同,KASLR 通过你系统运行的内核被启用。 +来执行或接收目标数据。KASLR 缓和此攻击的危害,因为攻击者不能立即知道目标地址,% +但一个暴力破解攻击还可以实施攻击。如果在 \src{/proc/kallsyms} 中,一个符号的地% +址与在文件 \verb|/boot/System.map| 中的地址不同,说明你使用的内核中KASLR 已经被% +打开。 \begin{verbatim} $ grep GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub @@ -1684,9 +1682,11 @@ ffffffff82000300 R sys_call_table $ sudo grep sys_call_table /proc/kallsyms ffffffff86400300 R sys_call_table \end{verbatim} -如果 \verb|KASLR| 被启用,每次我们启动时,我们必须小心从 \verb|/proc/kallsyms| % -的地址。为了使用来自 \verb|/boot/System.map| 的地址,确保 \verb|KASLR| 被禁用。% -在下次启动时,你可以添加 \verb|nokaslr| 内核启动参数来禁止 \verb|KASLR|: + +如果 \verb|KASLR| 选项被启用,每次我们启动时,来自 \verb|/proc/kallsyms| 的地址,% +必须被我们注意。为了使用来自 \verb|/boot/System.map| 的地址,确保 \verb|KASLR| % +被禁用。在下次启动时,你可以添加 \verb|nokaslr| 内核启动参数来禁止 \verb|KASLR|: + \begin{verbnobox}[\fontsize{8pt}{8pt}\selectfont] $ grep GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" @@ -1707,11 +1707,11 @@ $ sudo update-grub \item \href{https://lwn.net/Articles/569635/}{Kernel address space layout randomization} \end{itemize} -这里有源代码是这样一个内核模块的示例。我们想``监视''某个用户,每当用户打开一个% +这里的源代码是一个此种内核模块的示例。我们想``监视''某个用户,每当用户打开一个% 文件时,并且用 \cpp|pr_info()| 打印一条信息。为此,我们用自己命名为 \cpp|our_sys_openat| % -的函数替换系统调用。这个函数检查当前进程的 uid(用户的id),且如果它等于我们监控% -的用户的 uid,它调用 \cpp|pr_info()| 来显示被打开的文件的名字。然后,无论哪种方% -式,它都会用相同的参数调用原始的 \cpp|openat()| 函数,来实际上打开文件。 +的函数替换系统调用。这个函数检查当前进程的 uid\,(\,用户的id\,),并且,如果它等% +于我们监控的用户的 uid,它调用 \cpp|pr_info()| 来显示被打开的文件的名字。然后,% +无论哪种方式,它都会用相同的参数调用原始的 \cpp|openat()| 函数,来实际上打开文件。 \cpp|init_module| 函数在 \cpp|sys_call_table| 中替换适当位置并在一个变量中保存% 最初的指针。\cpp|cleanup_module| 函数使用那个变量来恢复所有数据到之前正常状态。% @@ -1731,9 +1731,9 @@ $ sudo update-grub 前它就不再指向 \cpp|A_openat|。不幸的是,\cpp|B_openat| 仍会尝试调用 \cpp|A_openat|,% 它已经不在内存中了,因此即使不删除 B,系统调用也会崩溃。 -请注意,所有相关问题使得系统调用窃取在生产使用中不可行。为了防止人们做潜在有害% -的事情,\cpp|sys_call_table| 不再被输出。这意味着,如果你想要做的不仅仅是这个示% -例的空运行,你将必须修补当前内核,才能拥有 \cpp|sys_call_table| 被输出。 +请注意,所有相关问题使得系统调用窃取的花招在生产使用中不可行。为了防止人们做潜% +在有害的事情,\cpp|sys_call_table| 不再被输出。这意味着,如果你想要做的不仅仅% +是这个示例的空运行,你将必须修补当前内核,才能拥有 \cpp|sys_call_table| 被输出。 \samplec{examples/syscall.c} @@ -1741,24 +1741,24 @@ $ sudo update-grub \label{sec:blocking_process_thread} \section{睡眠} \label{sec:sleep} -当某人请你帮忙,而你恰巧现在不能帮时,你会怎么做?如果你是一个人,并被另一个人% -打扰,你唯一能说的是:``\emph{不是现在,我正忙,走开!}''。但如果你有一个内核模块% -并且你被一个进程打扰,你还有另外一种可能性。你可以让该进程进入休眠状态,直到可以% -为其提供服务为止。毕竟,进程一直被内核置于睡眠状态并被唤醒(这就是多个进程在单个 % -CPU 上同时运行的方式)。 +当某人请你帮忙,而你恰巧现在腾不出手,你会怎么做?如果你是一个人,并被另一个人% +打扰,你唯一能说的是:``\emph{不是现在,我正忙,走开!}''。但如果你有一个内核模块,% +并且你被一个进程打扰,你还有另外一种应付的可能性。你可以让该进程进入休眠状态,% +直到可以为该进程提供服务为止。毕竟,进程一直被内核置于睡眠状态并被唤醒(这就是% +多个进程在单个 CPU 上同时运行的方式)。 -内核模块就是这种工作方式的一个示例。这个文件(叫作 \verb|/proc/sleep|)只能被单一% -进程在一个时刻被打开。如果该文件已经打开,内核模块称为 \cpp|wait_event_interruptible|。% -保持文件打开的最简单方法是使用以下命令打开: +此内核模块就是这种处理方式的一个示例。该文件\,(\,叫作 \verb|/proc/sleep|\,)\,% +只能被单一进程在一个时刻被打开。如果该文件已经打开,内核模块调用 % +\cpp|wait_event_interruptible|。保持文件打开的最简单方法是使用以下命令打开: \begin{codebash} tail -f \end{codebash} -函数修改一个任务(一个任务是内核数据结构,它保存有关进程及其所在系统调用的信息,% -如果有的话)的状态到 \cpp|TASK_INTERRUPTIBLE|,这意味着该任务在某时被唤醒之前,一% -直不会动行,并将该任务添加到 WaitQ,它是等候访问文件的任务的队列。然后,该函数% -调用调度程序将上下文切换到另一个进程,该进程对 CPU 有一定使用。 +函数修改一个 task (\,一个 task 是内核数据结构,它保存有关进程及其所在系统调用的% +信息,如果有的话\,)\,的状态到 \cpp|TASK_INTERRUPTIBLE|,这意味着该任务在某时被% +唤醒之前,一直不会动行,并将该任务添加到 WaitQ,它是等候访问文件的任务的队列。% +然后,该函数调用调度程序将上下文切换到另一个进程,该进程对 CPU 有一定使用。 当一个进程处理完文件时,它关闭文件,并且 \cpp|module_close| 被调用。这个函数唤% 醒在队列(没有机制只唤醒其中一个队列内的进程)中的所有进程。它然后返回,并且刚刚% @@ -1773,7 +1773,7 @@ CPU 的控制权转到其它进程。最终,在队列中的一个进程将通 当其它进程得到一片 CPU 占用时间片,它将看到该全局变量,并返回睡眠状态。 因此我们将使用 \sh|tail -f| 来保持在后台打开,同时尝试用另一个进程访问它(再次在% -后台,这样我们就不需要切换到不同的 vt)。一旦第一个后台进程被使用命令 kill \%1 % +后台,这样我们就不需要切换到不同的终端)。一旦第一个后台进程被使用命令 kill \%1 % 所杀死,第二个进程被唤醒,它能访问文件并最终终止。 为使得我们的生活更为有趣,\cpp|module_close| 不垄断唤醒等待访问文件的进程。一个% @@ -1819,7 +1819,7 @@ $ \section{完成} \label{sec:completion} -在一人有多线程的模块中,有时一件事应在另一件事之前发生。而不是使用 \sh|sleep| % +在一个拥有多线程的模块中,有时一件事应在另一件事之前发生。而不是使用 \sh|sleep| % 命令,内核有另一种方法可以做到这一点,它允许超时或中断发生。在下面的示例中,两% 个线程已经开始,但一个需要在另一个之前开始。 @@ -1829,30 +1829,32 @@ $ 状态被更新,并且 \cpp|wait_for_completion| 被飞轮线程使用,来确保它不会过早的% 开始。 -因此即使 \cpp|flywheel_thread| 首先启去吧,你应当注意到是否你加载这个模块并运行 % -\sh|shidmesg|,它总是首先发生,因为 flywheel 线程等待它执行完成。 +因此即使 \cpp|flywheel_thread| 首先启动,你应当注意到是否加载这个模块,并运行 % +\sh|shidmesg| 命令,该命令使得转动曲轴线程总是首先发生,因为 flywheel 线程会等% +待曲轴进程完成任务。 \cpp|wait_for_completion| 函数还有其它变体,包括超时或被中断,但这种基本机制足% 以满足许多常见情况,而不会增加很多复杂性。 \chapter{避免冲突和死锁} \label{sec:synchronization} -如果进程运行于不同的 CPU 上或在不同的线程中试图访问相同的内存位置,然后奇怪的事% -情可能发生,你的系统或许被锁住。要避免这样,可以使用各种类型的互斥内核函数。这% -些指示一段代码是否被``锁定''或``解锁'',以便不会发生同时尝试运行这些代码的状况。 +如果进程运行于不同的 CPU 上,或在不同的线程中,试图访问相同的内存位置,然后奇% +怪的事情可能发生,你的系统或许被锁住。要避免这样,可以使用各种类型的互斥内核% +函数。这些指示一段代码是否被``锁定''或``解锁'',以便不会发生同时尝试运行这些代% +码的状况。 \section{互斥} \label{sec:mutex} -你可使用内核互斥体(互斥),其使用方式与在用户态中部署它们的方式大致相同。在大多% -数情况下,这可能是避免碰撞所需的全部内容。 +你可使用内核互斥体(相互排斥),其使用方式与你在用户态中部署它们的方式大致相同。% +在大多数情况下,这可能是避免冲突所需的全部情况。 \samplec{examples/example_mutex.c} \section{自旋锁} \label{sec:spinlock} -顾名思义,自旋锁会锁定代码正在运行其上的 CPU 并占用其 100\% 的资源。因此你只该% -对运行时间可能不会超过几毫秒的代码段上使用自旋锁机制,因此从用户角度来看不会明% -显减慢任何速度。 +顾名思义,自旋锁会锁定代码到正在运行此代码的 CPU 之上,并占用其 100\% 的资源。% +因此你只该对运行时间可能不会超过几毫秒的代码段,使用自旋锁机制,因此从用户角度% +来看不会明显减慢任何速度。 在这里的示例是 \verb|``irq safe''|,这意味着如果在锁定期间发生中断,那么它们不% 会被遗忘,并且在解锁发生时将被激活,使用 \cpp|flags| 变量来保持它们的状状。 @@ -1895,9 +1897,9 @@ $ \label{sec:print_macros} \section{替换} % FIXME: cross-reference -在 \ref{sec:preparation} 小节,值得注意的是,X Window 系统和内核模块编程不利于% -集成。这在内核模块开发过程中还有效。然而,在实际场景中,需要将消息中断到发出模% -块加载命令的 tty(电传打字机)。 +在 \ref{sec:preparation} 节,值得注意的是,X Window 系统和内核模块编程不利于% +集成。这在内核模块开发过程中还有效。然而,在实际场景中,需要紧急将加模块加载% +命令产生的消息转发到tty\,(\,teletype\,)。 术语``tty''源于 \emph{teletype},它最初指的是用于 Unix 系统通信的组合键盘打印% 机。今天,它表示 Unix 程序使用的文本流抽象,包括物理终端,X 显示中的 xterm,以% @@ -1911,24 +1913,24 @@ $ \section{闪轹键盘LED} \label{sec:flash_kb_led} -在某些情况下,你可能想有一种更简单、更直接的方式与外部世界进行交流。闪烁的键盘 % -LED 可以是这样的解决方案:它是吸引注意力或显示状态条件的直接方法。每个硬件上都% -有键盘 LED,它们始终可见,不需要任何设置,并且与写入 tty 或文件相比,它们的使用% -相当简单,而且具有非侵入性。 - -从 v4.14 到 v4.15,计时器(timer) API 发生一系列改变以提高内存安全性。一个 \cpp|timer_list| % -结构区域中的一个缓存溢出,或许有能力覆盖 \cpp|function| 与 \cpp|data| 字段,为% -攻击者提供一种使用返回对象编程(ROP)来调用内核中的任意函数的方法。另外,回调的函% -数原型,包含 \cpp|unsigned long| 参数,将阻止任何类型检查工作。此外,函数原型为 % -\cpp|unsigned long| 参数可能是 \textit{control-flow integrity} 的前向边缘保护的% -障碍。因此,最好使用唯一的原型来与采用 \cpp|unsigned long| 参数的集群分开。 - -计时器回调应该传递一个指向 \cpp|timer_list| 结构的指针,而不是一个 \cpp|unsigned long| % -参数。然后,它包装所有回调所需信息,包括 \cpp|container_of| 宏,替换 \cpp|unsigned long| % -值。更多信息,请参阅: \href{https://lwn.net/Articles/735887/}{Improving the kernel timers API}. - -在 Linux v4.14 以前,\cpp|setup_timer| 被用来初始化定时器,并且 \cpp|timer_list| 结% -构看起来如下: +在某些情况下,你可能想有一种更简单、更直接的方式与外部世界进行交流。闪烁键盘 % +LED 可以是此需求的解决方案:它是吸引注意力或显示状态条件的直接方法。每个硬件上% +都有键盘 LED,它们始终可见,不需要任何设置,并且与写入 tty 或文件相比,它们的% +使用相当简单,而且具有非侵入性。 + +从 v4.14 到 v4.15,计时器(timer) API 发生一系列改变,以提高内存安全性。一个 % +\cpp|timer_list| 区域中的一个缓存溢出,或许可能覆盖 \cpp|function| 与 \cpp|data| % +字段,这为攻击者提供一种使用返回对象编程(ROP)来调用内核中的任意函数的方法。另外,% +回调的函数原型,包含 \cpp|unsigned long| 参数,将阻止任何类型检查工作。此外,% +函数原型为 \cpp|unsigned long| 参数可能是 \textit{control-flow integrity} 的前% +向边缘保护的障碍。因此,最好使用唯一的原型来与采用 \cpp|unsigned long| 参数的% +集群分开。计时器回调应该传递一个指向 \cpp|timer_list| 结构的指针,而不是一个 % +\cpp|unsigned long| 参数。然后,它包装所有回调所需信息,包括 \cpp|container_of| % +宏,替换 \cpp|unsigned long| 值。更多信息,请参阅: % +\href{https://lwn.net/Articles/735887/}{Improving the kernel timers API}. + +Linux v4.14 以前,\cpp|setup_timer| 被用来初始化定时器,并且 \cpp|timer_list| % +结构看起来如下: \begin{code} struct timer_list { unsigned long expires; @@ -1942,7 +1944,7 @@ void setup_timer(struct timer_list *timer, void (*callback)(unsigned long), unsigned long data); \end{code} -自从 Linux v4.14 开始,\cpp|timer_setup| 被采用,并且内核逐步转从结构 % +从 Linux v4.14 开始,\cpp|timer_setup| 被采用,并且内核逐步转从结构 % \cpp|setup_timer| 逐步转换到 \cpp|timer_setup|。API 变更的原因之一是需要与旧版% 本接口共存。此外,\cpp|timer_setup| 首先通过 \cpp|setup_timer| 被执行。 @@ -1969,10 +1971,10 @@ struct timer_list { 如果本章中的示例都不符合你的调试需求,可能还可以尝试一些其它技巧。有没有想过 % \cpp|CONFIG_LL_DEBUG| 是什么?在 \sh|make menuconfig| 中有什么好处?如果激活% 它,你将获得对串行端口的低级别访问权限。虽然这本身听起来不是很强大,但你可以% -修补 \src{kernel/printk.c} 或任何其它必要的系统调用来打印 ASCII 字符,从而可% -以跟踪代码通过串行线路执行的几乎所有操作。你发现自己将内核移植到一些新的和以% -前不受支持的体系结构,这通常是首先应该实现的事情之一。通过网络控制台登录也可% -能值的一试。 +对 \src{kernel/printk.c} 打补丁, 或任何其它必要的系统调用来打印 ASCII 字符,% +从而可以跟踪你代码在一条串行线路上做的所有事情。你发现自己将内核移植到一些新% +的和以前不受支持的体系结构,这通常是首先应该执行的事情之一。通过网络控制台登% +录也可能值的一试。 虽然你在这里看到了很多可用于帮助调试的内容,但仍有一些事情需要注意。调试几乎% 总是侵入性的。添加调试代码可以改变情况,足以使错误看起来消失。因此,你应该将% @@ -1981,9 +1983,9 @@ struct timer_list { \chapter{调度任务} \label{sec:scheduling_tasks} -运行任务有两种主要方式:tasklets 与工作队列。Tasklet 是一种安排单个函数运行的% -快速而简单的方法。例如,当从中断触发时,工作队列更复杂,但也更适合按顺序运行% -多个事物。 +运行任务有两种主要方式:tasklets 与工作队列(work queues)。Tasklet 是一种安排单% +个函数运行的快速而简单的方法。例如,当从中断触发时,工作队列更复杂,但也更适合% +按顺序运行多个事物。 \section{Tasklets} \label{sec:tasklet} @@ -2011,16 +2013,15 @@ Example tasklet ends 在最近的内核中,tasklet 可以被工作队列,定时器,或线程中断所替换。\footnote{线% 程中断的目标是将更多的工作推送到不同的线程,以便对一个中断的最小需要被减少,并% 因此处理该中断(在同一时刻不能处理其它任何中断)花费的时间被减少了。% -查看\url{https://lwn.net/Articles/302043/}。} - -虽然删除 tasklets 仍然是一个长期的目标,但当前内核包含一百多种 tasklet 的使用。% -现在开发人员正在继续进行 API 更改和宏 \cpp|DECLARE_TASKLET_OLD| 存在是为了兼容% -性。要了解更多信息,查看 \url{https://lwn.net/Articles/830964/}。 +查看\url{https://lwn.net/Articles/302043/}。}虽然删除 tasklets 仍然是一个长期% +目标,但当前内核包含一百多种 tasklet 的使用。开发人员正在继续进行 API 更% +改,并且宏 \cpp|DECLARE_TASKLET_OLD| 存在是为了兼容性。要了解更多信息,查看 % +\url{https://lwn.net/Articles/830964/}。 \section{工作队列} \label{sec:workqueue} -要将任务添加到调度程序,我们可以使用工作队列。然后,内核使用完全公平调度程序(% -CFS)来执行队列中的工作。 +要将任务添加到调度程序,我们可以使用工作队列。然后,内核使用完全公平调度\,(\,% +Completely Fair Scheduler 缩写为 CFS\,)\,来执行队列中的工作。 \samplec{examples/sched.c} -- 2.39.5