Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,139 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/PCI/acpi-info.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
=====================
PCI主桥的ACPI注意事项
=====================
一般的规则是,ACPI命名空间应该描述操作系统可能使用的所有东西,除非有其他方法让操作系
统找到它[1, 2]。
例如,没有标准的硬件机制来枚举PCI主桥,所以ACPI命名空间必须描述每个主桥、访问它
下面的PCI配置空间的方法、主桥转发到PCI的地址空间窗口(使用_CRS)以及传统的INTx
中断的路由(使用_PRT)。
在主桥下面的PCI设备,通常不需要通过ACPI描述。操作系统可以通过标准的PCI枚举机制来
发现它们,使用配置访问来发现和识别设备,并读取和测量它们的BAR。然而,如果ACPI为它们
提供电源管理或热插拔功能,或者如果设备有由平台中断控制器连接的INTx中断,需要一个_PRT
来描述这些连接,这种情况下ACPI可以描述PCI设备。
ACPI资源描述是通过ACPI命名空间中设备的_CRS对象完成的[2]。_CRS就像一个通用的PCI BAR:
操作系统可以读取_CRS并找出正在消耗的资源,即使它没有该设备的驱动程序[3]。这一点很重要,
因为它意味着一个旧的操作系统可以正确地工作,即使是在操作系统不知道的新设备的系统上。新设
备可能什么都不做,但操作系统至少可以确保没有资源与它们冲突。
像MCFG、HPET、ECDT等静态表,不是保留地址空间的机制。静态表是在操作系统在启动初期且在它
能够解析ACPI命名空间之前需要知道的东西。如果定义了一个新的表,即使旧的操作系统忽略了这
个表,它也需要正常运行。_CRS允许这样做,因为它是通用的,可以被旧的操作系统解析;而静态表
则不允许。
如果操作系统要管理一个通过ACPI描述的不可发现的设备,该设备将有一个特定的_HID/_CID,以
告诉操作系统与之绑定的驱动程序,并且_CRS告诉操作系统和驱动程序该设备的寄存器在哪里。
PCI主桥是PNP0A03或PNP0A08设备。它们的_CRS应该描述它们所消耗的所有地址空间。这包括它
们转发到PCI总线上的所有窗口,以及不转发到PCI的主桥本身的寄存器。主桥的寄存器包括次要/下
级总线寄存器,决定了桥下面的总线范围,窗口寄存器描述了桥洞,等等。这些都是设备相关的,非
架构相关的东西,所以PNP0A03/PNP0A08驱动可以管理它们的唯一方法是通过_PRS/_CRS/_SRS,
它包含了特定于设备的细节。主桥寄存器也包括ECAM空间,因为它是由主桥消耗的。
ACPI定义了一个Consumer/Producer位来区分桥寄存器(“Consumer”下文译作消费者)和
桥洞(“Producer”下文译作生产者)[4, 5],但是早期的BIOS没有正确使用这个位。其结果
是,目前的ACPI规范只为扩展地址空间描述符定义了消费者/生产者;在旧的QWord/Word/Word地
址空间描述符中,该位应该被忽略。因此,操作系统必须假定所有的QWord/Word/Word描述符都是
窗口。
在增加扩展地址空间描述符之前,消费者/生产者的失败意味着没有办法描述PNP0A03/PNP0A08设
备本身的桥寄存器。解决办法是在PNP0C02捕捉器中描述桥寄存器(包括ECAM空间)[6]。
除了ECAM之外,桥寄存器空间反正是特定于设备的,所以通用的PNP0A03/PNP0A08驱动程
序(pci_root.c)没有必要了解它。
新的架构应该能够在PNP0A03设备中使用“消费者”扩展地址空间描述符,用于桥寄存器,包括
ECAM,尽管对[6]的严格解释可能禁止这样做。旧的x86和ia64内核假定所有的地址空间描述
符,包括“消费者”扩展地址空间的描述符,都是窗口,所以在这些架构上以这种方式描述桥寄
存器是不安全的。
PNP0C02“主板”设备基本上是万能的。除了“不要将这些资源用于其他用途”之外,没有其他的编
程模型。因此,PNP0C02 _CRS应该声明ACPI命名空间中(1)没有被_CRS声明的任何其他设备对
象的地址空间,(2)不应该被OS分配给其他东西。
除非有一个标准的固件接口用于配置访问,例如ia64 SAL接口[7],否则PCIe规范要求使用增强
型配置访问方法(ECAM)。主桥消耗ECAM内存地址空间并将内存访问转换为PCI配置访问。该规范
定义了ECAM地址空间的布局和功能;只有地址空间的基础是特定于设备的。ACPI操作系统从静态
MCFG表或PNP0A03设备中的_CBA方法中了解基础地址。
MCFG表必须描述非热插拔主桥的ECAM空间[8]。由于MCFG是一个静态表,不能通过热插拔更新,
PNP0A03设备中的_CBA方法描述了可热插拔主桥的ECAM空间[9]。请注意,对于MCFG和_CBA,
基址总是对应于总线0,即使桥器下面的总线范围(通过_CRS报告)不从0开始。
[1] ACPI 6.2, sec 6.1:
对于任何在非枚举类型的总线上的设备(例如,ISA总线),OSPM会枚举设备的标识符,ACPI
系统固件必须为每个设备提供一个_HID对象...以使OSPM能够做到这一点。
[2] ACPI 6.2, sec 3.7:
操作系统枚举主板设备时,只需通过读取ACPI命名空间来寻找具有硬件ID的设备。
ACPI枚举的每个设备都包括ACPI命名空间中ACPI定义的对象,该对象报告设备可能占用的硬
件资源[_PRS],报告设备当前使用的资源[_CRS]的对象,以及配置这些资源的对象[_SRS]。
这些信息被即插即用操作系统(OSPM)用来配置设备。
[3] ACPI 6.2, sec 6.2:
OSPM使用设备配置对象来配置通过ACPI列举的设备的硬件资源。设备配置对象提供了关于当前
和可能的资源需求的信息,共享资源之间的关系,以及配置硬件资源的方法。
当OSPM枚举一个设备时,它调用_PRS来确定该设备的资源需求。它也可以调用_CRS来找到该设
备的当前资源设置。利用这些信息,即插即用系统决定设备应该消耗什么资源,并通过调用设备
的_SRS控制方法来设置这些资源。
在ACPI中,设备可以消耗资源(例如,传统的键盘),提供资源(例如,一个专有的PCI桥),
或者两者都做。除非另有规定,设备的资源被假定为来自设备层次结构中设备上方最近的匹配资
源。
[4] ACPI 6.2, sec 6.4.3.5.1, 2, 3, 4:
QWord/DWord/Word 地址空间描述符 (.1, .2, .3)
常规标志: Bit [0] 被忽略。
扩展地址空间描述符 (.4)
常规标志: Bit [0] 消费者/生产者:
* 1 – 这个设备消费这个资源
* 0 – 该设备生产和消费该资源
[5] ACPI 6.2, sec 19.6.43:
ResourceUsage指定内存范围是由这个设备(ResourceConsumer)消费还是传递给子设备
(ResourceProducer)。如果没有指定,那么就假定是ResourceConsumer。
[6] PCI Firmware 3.2, sec 4.1.2:
如果操作系统不能原生的懂得保留MMCFG区域,MMCFG区域必须由固件保留。在MCFG表中或通
过_CBA方法(见第4.1.3节)报告的地址范围必须通过声明主板资源来保留。对于大多数系统,
主板资源将出现在ACPI命名空间的根部(在_SB下),在一个节点的_HID为EISAID(PNP0C0
2),在这种情况下的资源不应该要求在根PCI总线的_CRS。这些资源可以选择在Int15 E820
或EFIGetMemoryMap中作为保留内存返回,但必须始终通过ACPI作为主板资源报告。
[7] PCI Express 4.0, sec 7.2.2:
对于PC兼容的系统,或者没有实现允许访问配置空间的处理器架构特定固件接口标准的系统,需
要使用本节中定义的ECAM。
[8] PCI Firmware 3.2, sec 4.1.2:
MCFG表是一个ACPI表,用于沟通的基础地址对应的非热的可移动的PCI段组范围内的PCI段组在
启动时提供给操作系统。这对PC兼容系统来说是必需的。
MCFG表仅用于沟通在启动时系统可用的PCI段组对应的基址。
[9] PCI Firmware 3.2, sec 4.1.3:
_CBA (Memory mapped Configuration Base Address) 控制方法是一个可选的ACPI对
象,用于返回热插拔主桥的64位内存映射的配置基址。_CBA 返回的基址是与处理器相关的地址。
_CBA 控制方法被评估为一个整数。
这个控制方法出现在主桥对象下。当_CBA方法出现在一个活动的主桥对象下时,操作系统会评
估这个结构,以确定内存映射的配置基址,对应于_CRS方法中指定的总线编号范围的PCI段组。
一个包含_CBA方法的ACPI命名空间对象也必须包含一个相应的_SEG方法。
@@ -0,0 +1,34 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/PCI/index.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
===================
Linux PCI总线子系统
===================
.. toctree::
:maxdepth: 2
:numbered:
pci
pciebus-howto
pci-iov-howto
msi-howto
sysfs-pci
acpi-info
Todolist:
* pci-error-recovery
* pcieaer-howto
* endpoint/index
* boot-interrupts
@@ -0,0 +1,244 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: <isonum.txt>
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/PCI/msi-howto.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
===========
MSI驱动指南
===========
:作者: Tom L Nguyen; Martine Silbermann; Matthew Wilcox
:版权: 2003, 2008 Intel Corporation
关于本指南
==========
本指南介绍了消息标记中断(MSI)的基本知识,使用MSI相对于传统中断机制的优势,如何
改变你的驱动程序以使用MSI或MSI-X,以及在设备不支持MSI时可以尝试的一些基本诊断方法。
什么是MSI?
==========
信息信号中断是指从设备写到一个特殊的地址,导致CPU接收到一个中断。
MSI能力首次在PCI 2.2中规定,后来在PCI 3.0中得到增强,允许对每个中断进行单独屏蔽。
MSI-X功能也随着PCI 3.0被引入。它比MSI支持每个设备更多的中断,并允许独立配置中断。
设备可以同时支持MSI和MSI-X,但一次只能启用一个。
为什么用MSI?
============
有三个原因可以说明为什么使用MSI比传统的基于针脚的中断有优势。
基于针脚的PCI中断通常在几个设备之间共享。为了支持这一点,内核必须调用每个与中断相
关的中断处理程序,这导致了整个系统性能的降低。MSI从不共享,所以这个问题不会出现。
当一个设备将数据写入内存,然后引发一个基于引脚的中断时,有可能在所有的数据到达内存
之前,中断就已经到达了(这在PCI-PCI桥后面的设备中变得更有可能)。为了确保所有的数
据已经到达内存中,中断处理程序必须在引发中断的设备上读取一个寄存器。PCI事务排序规
则要求所有的数据在返回寄存器的值之前到达内存。使用MSI可以避免这个问题,因为中断产
生的写入不能通过数据写入,所以当中断发生时,驱动程序知道所有的数据已经到达内存中。
PCI设备每个功能只能支持一个基于引脚的中断。通常情况下,驱动程序必须查询设备以找出
发生了什么事件,这就减慢了对常见情况的中断处理。有了MSI,设备可以支持更多的中断,
允许每个中断被专门用于不同的目的。一种可能的设计是给不经常发生的情况(如错误)提供
自己的中断,这使得驱动程序可以更有效地处理正常的中断处理路径。其他可能的设计包括给
网卡的每个数据包队列或存储控制器的每个端口提供一个中断。
如何使用MSI
===========
PCI设备被初始化为使用基于引脚的中断。设备驱动程序必须将设备设置为使用MSI或MSI-X。
并非所有的机器都能正确地支持MSI,对于这些机器,下面描述的API将简单地失败,设备将
继续使用基于引脚的中断。
加入内核对MSI的支持
-------------------
为了支持MSI或MSI-X,内核在构建时必须启用CONFIG_PCI_MSI选项。这个选项只在某些架
构上可用,而且它可能取决于其他一些选项的设置。例如,在x86上,你必须同时启用X86_UP_APIC
或SMP,才能看到CONFIG_PCI_MSI选项。
使用MSI
-------
大部分沉重的工作是在PCI层为驱动程序完成的。驱动程序只需要请求PCI层为这个设备设置
MSI功能。
要自动使用MSI或MSI-X中断向量,请使用以下函数::
int pci_alloc_irq_vectors(struct pci_dev *dev, unsigned int min_vecs,
unsigned int max_vecs, unsigned int flags);
它为一个PCI设备分配最多至max_vecs的中断向量。它返回分配的向量数量或一个负的错误。
如果设备对最小数量的向量有要求,驱动程序可以传递一个min_vecs参数,设置为这个限制,
如果PCI核不能满足最小数量的向量,将返回-ENOSPC。
flags参数用来指定设备和驱动程序可以使用哪种类型的中断(PCI_IRQ_INTX, PCI_IRQ_MSI,
PCI_IRQ_MSIX)。一个方便的短语(PCI_IRQ_ALL_TYPES)也可以用来要求任何可能的中断类型。
如果PCI_IRQ_AFFINITY标志被设置,pci_alloc_irq_vectors()将把中断分散到可用的CPU上。
要获得传递给require_irq()和free_irq()的Linux IRQ号码和向量,请使用以下函数::
int pci_irq_vector(struct pci_dev *dev, unsigned int nr);
在删除设备之前,应使用以下功能释放任何已分配的资源::
void pci_free_irq_vectors(struct pci_dev *dev);
如果一个设备同时支持MSI-X和MSI功能,这个API将优先使用MSI-X,而不是MSI。MSI-X支
持1到2048之间的任何数量的中断。相比之下,MSI被限制为最多32个中断(而且必须是2的幂)。
此外,MSI中断向量必须连续分配,所以系统可能无法为MSI分配像MSI-X那样多的向量。在一
些平台上,MSI中断必须全部针对同一组CPU,而MSI-X中断可以全部针对不同的CPU。
如果一个设备既不支持MSI-X,也不支持MSI,它就会退回到一个传统的IRQ向量。
MSI或MSI-X中断的典型用法是分配尽可能多的向量,可能达到设备支持的极限。如果nvec大于
设备支持的数量,它将自动被限制在支持的限度内,所以没有必要事先查询支持的向量的数量。::
nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_ALL_TYPES)
if (nvec < 0)
goto out_err;
如果一个驱动程序不能或不愿意处理可变数量的MSI中断,它可以要求一个特定数量的中断,将该
数量作为“min_vecs“和“max_vecs“参数传递给pci_alloc_irq_vectors()函数。::
ret = pci_alloc_irq_vectors(pdev, nvec, nvec, PCI_IRQ_ALL_TYPES);
if (ret < 0)
goto out_err;
上述请求类型的最臭名昭著的例子是为一个设备启用单一的MSI模式。它可以通过传递两个1作为
'min_vecs'和'max_vecs'来实现::
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES);
if (ret < 0)
goto out_err;
一些设备可能不支持使用传统的线路中断,在这种情况下,驱动程序可以指定只接受MSI或MSI-X。::
nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_MSI | PCI_IRQ_MSIX);
if (nvec < 0)
goto out_err;
传统API
-----------
以下用于启用和禁用MSI或MSI-X中断的旧API不应该在新代码中使用::
pci_enable_msi() /* deprecated */
pci_disable_msi() /* deprecated */
pci_enable_msix_range() /* deprecated */
pci_enable_msix_exact() /* deprecated */
pci_disable_msix() /* deprecated */
此外,还有一些API来提供支持的MSI或MSI-X向量的数量:pci_msi_vec_count()和
pci_msix_vec_count()。一般来说,应该避免使用这些方法,而是让pci_alloc_irq_vectors()
来限制向量的数量。如果你对向量的数量有合法的特殊用例,我们可能要重新审视这个决定,
并增加一个pci_nr_irq_vectors()助手,透明地处理MSI和MSI-X。
使用MSI时需要考虑的因素
-----------------------
自旋锁
~~~~~~
大多数设备驱动程序都有一个每的自旋锁,在中断处理程序中被占用。对于基于引脚的中断
或单一的MSI,没有必要禁用中断(Linux保证同一中断不会被重新输入)。如果一个设备
使用多个中断,驱动程序必须在锁被持有的时候禁用中断。如果设备发出一个不同的中断,
驱动程序将死锁,试图递归地获取自旋锁。这种死锁可以通过使用spin_lock_irqsave()
或spin_lock_irq()来避免,它们可以禁用本地中断并获取锁(见《不可靠的锁定指南》)。
如何判断一个设备上是否启用了MSI/MSI-X
-------------------------------------
使用“lspci -v“(以root身份)可能会显示一些具有“MSI“、“Message Signalled Interrupts“
或“MSI-X“功能的设备。这些功能中的每一个都有一个“启用“标志,后面是“+“(启用)
或“-“(禁用)。
MSI特性
=======
众所周知,一些PCI芯片组或设备不支持MSI。PCI协议栈提供了三种禁用MSI的方法:
1. 全局的
2. 在一个特定的桥后面的所有设备上
3. 在单一设备上
全局禁用MSI
-----------
一些主控芯片组根本无法正确支持MSI。如果我们幸运的话,制造商知道这一点,并在
ACPI FADT表中指明了它。在这种情况下,Linux会自动禁用MSI。有些板卡在表中没
有包括这一信息,因此我们必须自己检测它们。完整的列表可以在drivers/pci/quirks.c
中的quirk_disable_all_msi()函数附近找到。
如果你有一块有MSI问题的板子,你可以在内核命令行中传递pci=nomsi来禁用所有设
备上的MSI。你最好把问题报告给linux-pci@vger.kernel.org,包括完整的
“lspci -v“,这样我们就可以把这些怪癖添加到内核中。
禁用桥下的MSI
-------------
一些PCI桥接器不能在总线之间正确地路由MSI。在这种情况下,必须在桥接器后面的所
有设备上禁用MSI。
一些桥接器允许你通过改变PCI配置空间的一些位来启用MSI(特别是Hypertransport
芯片组,如nVidia nForce和Serverworks HT2000)。与主机芯片组一样,Linux大
多知道它们,如果可以的话,会自动启用MSI。如果你有一个Linux不知道的网桥,你可以
用你知道的任何方法在配置空间中启用MSI,然后通过以下方式在该网桥上启用MSI::
echo 1 > /sys/bus/pci/devices/$bridge/msi_bus
其中$bridge是你所启用的桥的PCI地址(例如0000:00:0e.0)。
要禁用MSI,请回显0而不是1。改变这个值应该谨慎进行,因为它可能会破坏这个桥下面所
有设备的中断处理。
同样,请通知 linux-pci@vger.kernel.org 任何需要特殊处理的桥。
在单一设备上关闭MSIs
--------------------
众所周知,有些设备的MSI实现是有问题的。通常情况下,这是在单个设备驱动程序中处理的,
但偶尔也有必要用一个古怪的方法来处理。一些驱动程序有一个选项可以禁用MSI的使用。虽然
这对驱动程序的作者来说是一个方便的变通办法,但这不是一个好的做法,不应该被模仿。
寻找设备上MSI被禁用的原因
-------------------------
从以上三个部分,你可以看到有许多原因导致MSI没有在某个设备上被启用。你的第一步应该是
仔细检查你的dmesg以确定你的机器是否启用了MSI。你还应该检查你的.config以确定你已经
启用了CONFIG_PCI_MSI。
然后,“lspci -t“给出一个设备上面的网列表。读取 ``/sys/bus/pci/devices/*/msi_bus``
将告诉你MSI是否被启用(1)或禁用(0)。如果在任何属于PCI根和设备之间的桥的msi_bus
文件中发现0,说明MSI被禁用。
也需要检查设备驱动程序,看它是否支持MSI。例如,它可能包含对带有PCI_IRQ_MSI或
PCI_IRQ_MSIX标志的pci_alloc_irq_vectors()的调用。
MSI(-X) APIs设备驱动程序列表
============================
PCI/MSI子系统有一个专门的C文件,用于其导出的设备驱动程序APIs - `drivers/pci/msi/api.c` 。
以下是导出的函数:
该API在以下内核代码中:
drivers/pci/msi/api.c
@@ -0,0 +1,169 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: <isonum.txt>
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/PCI/pci-iov-howto.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
.. _cn_pci-iov-howto:
==========================
PCI Express I/O 虚拟化指南
==========================
:版权: |copy| 2009 Intel Corporation
:作者: - Yu Zhao <yu.zhao@intel.com>
- Donald Dutile <ddutile@redhat.com>
概述
====
什么是SR-IOV
------------
单根I/O虚拟化(SR-IOV)是一种PCI Express扩展功能,它使一个物理设备显示为多个
虚拟设备。物理设备被称为物理功能(PF),而虚拟设备被称为虚拟功能(VF)。VF的分
配可以由PF通过封装在该功能中的寄存器动态控制。默认情况下,该功能未被启用,PF表
现为传统的PCIe设备。一旦开启,每个VF的PCI配置空间都可以通过自己的总线、设备和
功能编号(路由ID)来访问。而且每个VF也有PCI内存空间,用于映射其寄存器集。VF设
备驱动程序对寄存器集进行操作,这样它就可以发挥功能,并作为一个真正的现有PCI设备
出现。
使用指南
========
我怎样才能启用SR-IOV功能
------------------------
有多种方法可用于SR-IOV的启用。在第一种方法中,设备驱动(PF驱动)将通过SR-IOV
核心提供的API控制功能的启用和禁用。如果硬件具有SR-IOV能力,加载其PF驱动器将启
用它和与PF相关的所有VF。一些PF驱动需要设置一个模块参数,以确定要启用的VF的数量。
在第二种方法中,对sysfs文件sriov_numvfs的写入将启用和禁用与PCIe PF相关的VF。
这种方法实现了每个PF的VF启用/禁用值,而第一种方法则适用于同一设备的所有PF。此外,
PCI SRIOV核心支持确保启用/禁用操作是有效的,以减少同一检查在多个驱动程序中的重
复,例如,如果启用VF,检查numvfs == 0,确保numvfs <= totalvfs。
第二种方法是对新的/未来的VF设备的推荐方法。
我怎样才能使用虚拟功能
----------------------
在内核中,VF被视为热插拔的PCI设备,所以它们应该能够以与真正的PCI设备相同的方式
工作。VF需要的设备驱动与普通PCI设备的驱动相同。
开发者指南
==========
SR-IOV API
----------
用来开启SR-IOV功能:
(a) 对于第一种方法,在驱动程序中::
int pci_enable_sriov(struct pci_dev *dev, int nr_virtfn);
nr_virtfn'是要启用的VF的编号。
(b) 对于第二种方法,从sysfs::
echo 'nr_virtfn' > \
/sys/bus/pci/devices/<DOMAIN:BUS:DEVICE.FUNCTION>/sriov_numvfs
用来关闭SR-IOV功能:
(a) 对于第一种方法,在驱动程序中::
void pci_disable_sriov(struct pci_dev *dev);
(b) 对于第二种方法,从sysfs::
echo 0 > \
/sys/bus/pci/devices/<DOMAIN:BUS:DEVICE.FUNCTION>/sriov_numvfs
要想通过主机上的兼容驱动启用自动探测VF,在启用SR-IOV功能之前运行下面的命令。这
是默认的行为。
::
echo 1 > \
/sys/bus/pci/devices/<DOMAIN:BUS:DEVICE.FUNCTION>/sriov_drivers_autoprobe
要禁止主机上的兼容驱动自动探测VF,请在启用SR-IOV功能之前运行以下命令。更新这个
入口不会影响已经被探测的VF。
::
echo 0 > \
/sys/bus/pci/devices/<DOMAIN:BUS:DEVICE.FUNCTION>/sriov_drivers_autoprobe
用例
----
下面的代码演示了SR-IOV API的用法
::
static int dev_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
pci_enable_sriov(dev, NR_VIRTFN);
...
return 0;
}
static void dev_remove(struct pci_dev *dev)
{
pci_disable_sriov(dev);
...
}
static int dev_suspend(struct device *dev)
{
...
return 0;
}
static int dev_resume(struct device *dev)
{
...
return 0;
}
static void dev_shutdown(struct pci_dev *dev)
{
...
}
static int dev_sriov_configure(struct pci_dev *dev, int numvfs)
{
if (numvfs > 0) {
...
pci_enable_sriov(dev, numvfs);
...
return numvfs;
}
if (numvfs == 0) {
....
pci_disable_sriov(dev);
...
return 0;
}
}
static struct pci_driver dev_driver = {
.name = "SR-IOV Physical Function driver",
.id_table = dev_id_table,
.probe = dev_probe,
.remove = dev_remove,
.driver.pm = &dev_pm_ops
.shutdown = dev_shutdown,
.sriov_configure = dev_sriov_configure,
};
@@ -0,0 +1,514 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/PCI/pci.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
.. _cn_PCI_pci.rst:
===================
如何写Linux PCI驱动
===================
:作者: - Martin Mares <mj@ucw.cz>
- Grant Grundler <grundler@parisc-linux.org>
PCI的世界是巨大的,而且充满了(大多数是不愉快的)惊喜。由于每个CPU架构实现了不同
的芯片组,并且PCI设备有不同的要求(呃,“特性”),结果是Linux内核中的PCI支持并不
像人们希望的那样简单。这篇短文试图向所有潜在的驱动程序作者介绍PCI设备驱动程序的
Linux APIs。
更完整的资源是Jonathan Corbet、Alessandro Rubini和Greg Kroah-Hartman的
《Linux设备驱动程序》第三版。LDD3可以免费获得(在知识共享许可下),网址是:
https://lwn.net/Kernel/LDD3/。
然而,请记住,所有的文档都会受到“维护不及时”的影响。如果事情没有按照这里描述的那
样进行,请参考源代码。
请将有关Linux PCI API的问题/评论/补丁发送到“Linux PCI”
<linux-pci@atrey.karlin.mff.cuni.cz> 邮件列表。
PCI驱动的结构体
===============
PCI驱动通过pci_register_driver()在系统中“发现”PCI设备。实际上,它是反过来的。
当PCI通用代码发现一个新设备时,具有匹配“描述”的驱动程序将被通知。下面是这方面的细
节。
pci_register_driver()将大部分探测设备的工作留给了PCI层,并支持设备的在线插入/移
除[从而在一个驱动中支持可热插拔的PCI、CardBus和Express-Card]。 pci_register_driver()
调用需要传入一个函数指针表,从而决定了驱动的高层结构体。
一旦驱动探测到一个PCI设备并取得了所有权,驱动通常需要执行以下初始化:
- 启用设备
- 请求MMIO/IOP资源
- 设置DMA掩码大小(对于流式和一致的DMA)
- 分配和初始化共享控制数据(pci_allocate_coherent())
- 访问设备配置空间(如果需要)
- 注册IRQ处理程序(request_irq())
- 初始化非PCI(即芯片的LAN/SCSI/等部分)
- 启用DMA/处理引擎
当使用完设备后,也许需要卸载模块,驱动需要采取以下步骤:
- 禁用设备产生的IRQ
- 释放IRQ(free_irq())
- 停止所有DMA活动
- 释放DMA缓冲区(包括一致性和数据流式)
- 从其他子系统(例如scsi或netdev)上取消注册
- 释放MMIO/IOP资源
- 禁用设备
这些主题中的大部分都在下面的章节中有所涉及。其余的内容请参考LDD3或<linux/pci.h> 。
如果没有配置PCI子系统(没有设置 ``CONFIG_PCI`` ),下面描述的大多数PCI函数被定
义为内联函数,要么完全为空,要么只是返回一个适当的错误代码,以避免在驱动程序中出现
大量的 ``ifdef`` 。
调用pci_register_driver()
=========================
PCI设备驱动程序在初始化过程中调用 ``pci_register_driver()`` ,并提供一个指向
描述驱动程序的结构体的指针( ``struct pci_driver`` ):
该API在以下内核代码中:
include/linux/pci.h
pci_driver
ID表是一个由 ``struct pci_device_id`` 结构体成员组成的数组,以一个全零的成员
结束。一般来说,带有静态常数的定义是首选。
该API在以下内核代码中:
include/linux/mod_devicetable.h
pci_device_id
大多数驱动程序只需要 ``PCI_DEVICE()`` 或 ``PCI_DEVICE_CLASS()`` 来设置一个
pci_device_id表。
新的 ``PCI ID`` 可以在运行时被添加到设备驱动的 ``pci_ids`` 表中,如下所示::
echo "vendor device subvendor subdevice class class_mask driver_data" > \
/sys/bus/pci/drivers/{driver}/new_id
所有字段都以十六进制值传递(没有前置0x)。供应商和设备字段是强制性的,其他字段是可
选的。用户只需要传递必要的可选字段:
- subvendor和subdevice字段默认为PCI_ANY_ID (FFFFFFF)。
- class和classmask字段默认为0
- driver_data默认为0UL。
- override_only字段默认为0。
请注意, ``driver_data`` 必须与驱动程序中定义的任何一个 ``pci_device_id`` 条
目所使用的值相匹配。如果所有的 ``pci_device_id`` 成员都有一个非零的driver_data
值,这使得driver_data字段是强制性的。
一旦添加,驱动程序探测程序将被调用,以探测其(新更新的) ``pci_ids`` 列表中列出的
任何无人认领的PCI设备。
当驱动退出时,它只是调用 ``pci_unregister_driver()`` ,PCI层会自动调用驱动处理
的所有设备的移除钩子。
驱动程序功能/数据的“属性”
-------------------------
请在适当的地方标记初始化和清理函数(相应的宏在<linux/init.h>中定义):
====== ==============================================
__init 初始化代码。在驱动程序初始化后被抛弃。
__exit 退出代码。对于非模块化的驱动程序来说是忽略的。
====== ==============================================
关于何时/何地使用上述属性的提示:
- module_init()/module_exit()函数(以及所有仅由这些函数调用的初始化函数)应该被标记
- 为__init/__exit。
- 不要标记pci_driver结构体。
- 如果你不确定应该使用哪种标记,请不要标记一个函数。不标记函数比标记错误的函数更好。
如何手动搜索PCI设备
===================
PCI驱动最好有一个非常好的理由不使用 ``pci_register_driver()`` 接口来搜索PCI设备。
PCI设备被多个驱动程序控制的主要原因是一个PCI设备实现了几个不同的HW服务。例如,组合的
串行/并行端口/软盘控制器。
可以使用以下结构体进行手动搜索:
通过供应商和设备ID进行搜索::
struct pci_dev *dev = NULL;
while (dev = pci_get_device(VENDOR_ID, DEVICE_ID, dev))
configure_device(dev);
按类别ID搜索(以类似的方式迭代)::
pci_get_class(CLASS_ID, dev)
通过供应商/设备和子系统供应商/设备ID进行搜索::
pci_get_subsys(VENDOR_ID,DEVICE_ID, SUBSYS_VENDOR_ID, SUBSYS_DEVICE_ID, dev).
你可以使用常数 ``PCI_ANY_ID`` 作为 ``VENDOR_ID`` 或 ``DEVICE_ID`` 的通
配符替代。例如,这允许搜索来自一个特定供应商的任何设备。
这些函数是热拔插安全的。它们会增加它们所返回的 ``pci_dev`` 的参考计数。你最终
必须通过调用 ``pci_dev_put()`` 来减少这些设备上的参考计数(可能在模块卸载时)。
设备初始化步骤
==============
正如介绍中所指出的,大多数PCI驱动需要以下步骤进行设备初始化:
- 启用设备
- 请求MMIO/IOP资源
- 设置DMA掩码大小(对于流式和一致的DMA)
- 分配和初始化共享控制数据(pci_allocate_coherent())
- 访问设备配置空间(如果需要)
- 注册IRQ处理程序(request_irq())
- 初始化non-PCI(即芯片的LAN/SCSI/等部分)
- 启用DMA/处理引擎
驱动程序可以在任何时候访问PCI配置空间寄存器。(嗯,几乎如此。当运行BIST时,配置
空间可以消失......但这只会导致PCI总线主控中止,读取配置将返回垃圾值)。)
启用PCI设备
-----------
在接触任何设备寄存器之前,驱动程序需要通过调用 ``pci_enable_device()`` 启用
PCI设备。这将:
- 唤醒处于暂停状态的设备。
- 分配设备的I/O和内存区域(如果BIOS没有这样做)。
- 分配一个IRQ(如果BIOS没有)。
.. note::
pci_enable_device() 可能失败,检查返回值。
.. warning::
OS BUG:在启用这些资源之前,我们没有检查资源分配情况。如果我们在调用
之前调用pci_request_resources(),这个顺序会更合理。目前,当两个设备被分配
了相同的范围时,设备驱动无法检测到这个错误。这不是一个常见的问题,不太可能很快
得到修复。
这个问题之前已经讨论过了,但从2.6.19开始没有改变:
https://lore.kernel.org/r/20060302180025.GC28895@flint.arm.linux.org.uk/
pci_set_master()将通过设置PCI_COMMAND寄存器中的总线主控位来启用DMA。
``pci_clear_master()`` 将通过清除总线主控位来禁用DMA,它还修复了延迟计时器的
值,如果它被BIOS设置成假的。
如果PCI设备可以使用 ``PCI Memory-Write-Invalidate`` 事务,请调用 ``pci_set_mwi()`` 。
这将启用 ``Mem-Wr-Inval`` 的 ``PCI_COMMAND`` 位,也确保缓存行大小寄存器被正确设置。检
查 ``pci_set_mwi()`` 的返回值,因为不是所有的架构或芯片组都支持 ``Memory-Write-Invalidate`` 。
另外,如果 ``Mem-Wr-Inval`` 是好的,但不是必须的,可以调用 ``pci_try_set_mwi()`` ,让
系统尽最大努力来启用 ``Mem-Wr-Inval`` 。
请求MMIO/IOP资源
----------------
内存(MMIO)和I/O端口地址不应该直接从PCI设备配置空间中读取。使用 ``pci_dev`` 结构体
中的值,因为PCI “总线地址”可能已经被arch/chip-set特定的内核支持重新映射为“主机物理”
地址。
参见io_mapping函数,了解如何访问设备寄存器或设备内存。
设备驱动需要调用 ``pci_request_region()`` 来确认没有其他设备已经在使用相同的地址
资源。反之,驱动应该在调用 ``pci_disable_device()`` 之后调用 ``pci_release_region()`` 。
这个想法是为了防止两个设备在同一地址范围内发生冲突。
.. tip::
见上面的操作系统BUG注释。目前(2.6.19),驱动程序只能在调用pci_enable_device()
后确定MMIO和IO端口资源的可用性。
``pci_request_region()`` 的通用风格是 ``request_mem_region()`` (用于MMIO
范围)和 ``request_region()`` (用于IO端口范围)。对于那些不被 "正常 "PCI BAR描
述的地址资源,使用这些方法。
也请看下面的 ``pci_request_selected_regions()`` 。
设置DMA掩码大小
---------------
.. note::
如果下面有什么不明白的地方,请参考使用通用设备的动态DMA映射。本节只是提醒大家,
驱动程序需要说明设备的DMA功能,并不是DMA接口的权威来源。
虽然所有的驱动程序都应该明确指出PCI总线主控的DMA功能(如32位或64位),但对于流式
数据来说,具有超过32位总线主站功能的设备需要驱动程序通过调用带有适当参数的
``dma_set_mask()`` 来“注册”这种功能。一般来说,在系统RAM高于4G物理地址的情
况下,这允许更有效的DMA。
所有PCI-X和PCIe兼容设备的驱动程序必须调用 ``dma_set_mask()`` ,因为它们
是64位DMA设备。
同样,如果设备可以通过调用 ``dma_set_coherent_mask()`` 直接寻址到
4G物理地址以上的系统RAM中的“一致性内存”,那么驱动程序也必须“注册”这种功能。同
样,这包括所有PCI-X和PCIe兼容设备的驱动程序。许多64位“PCI”设备(在PCI-X之前)
和一些PCI-X设备对有效载荷(“流式”)数据具有64位DMA功能,但对控制(“一致性”)数
据则没有。
设置共享控制数据
----------------
一旦DMA掩码设置完毕,驱动程序就可以分配“一致的”(又称共享的)内存。参见使用通
用设备的动态DMA映射,了解DMA API的完整描述。本节只是提醒大家,需要在设备上启
用DMA之前完成。
初始化设备寄存器
----------------
一些驱动程序需要对特定的“功能”字段进行编程,或对其他“供应商专用”寄存器进行初始
化或重置。例如,清除挂起的中断。
注册IRQ处理函数
---------------
虽然调用 ``request_irq()`` 是这里描述的最后一步,但这往往只是初始化设备的另
一个中间步骤。这一步通常可以推迟到设备被打开使用时进行。
所有IRQ线的中断处理程序都应该用 ``IRQF_SHARED`` 注册,并使用devid将IRQ映射
到设备(记住,所有的PCI IRQ线都可以共享)。
``request_irq()`` 将把一个中断处理程序和设备句柄与一个中断号联系起来。历史上,
中断号码代表从PCI设备到中断控制器的IRQ线。在MSI和MSI-X中(更多内容见下文),中
断号是CPU的一个“向量”。
``request_irq()`` 也启用中断。在注册中断处理程序之前,请确保设备是静止的,并且
没有任何中断等待。
MSI和MSI-X是PCI功能。两者都是“消息信号中断”,通过向本地APIC的DMA写入来向CPU发
送中断。MSI和MSI-X的根本区别在于如何分配多个“向量”。MSI需要连续的向量块,而
MSI-X可以分配几个单独的向量。
在调用 ``request_irq()`` 之前,可以通过调用 ``pci_alloc_irq_vectors()``
的PCI_IRQ_MSI和/或PCI_IRQ_MSIX标志来启用MSI功能。这将导致PCI支持将CPU向量数
据编程到PCI设备功能寄存器中。许多架构、芯片组或BIOS不支持MSI或MSI-X,调用
``pci_alloc_irq_vectors`` 时只使用PCI_IRQ_MSI和PCI_IRQ_MSIX标志会失败,
所以尽量也要指定 ``PCI_IRQ_INTX`` 。
对MSI/MSI-X和传统INTx有不同中断处理程序的驱动程序应该在调用
``pci_alloc_irq_vectors`` 后根据 ``pci_dev``结构体中的 ``msi_enabled``
和 ``msix_enabled`` 标志选择正确的处理程序。
使用MSI有(至少)两个真正好的理由:
1) 根据定义,MSI是一个排他性的中断向量。这意味着中断处理程序不需要验证其设备是
否引起了中断。
2) MSI避免了DMA/IRQ竞争条件。到主机内存的DMA被保证在MSI交付时对主机CPU是可
见的。这对数据一致性和避
3) 免控制数据过期都很重要。这个保证允许驱动程序省略MMIO读取,以刷新DMA流。
参见drivers/infiniband/hw/mthca/或drivers/net/tg3.c了解MSI/MSI-X的使
用实例。
PCI设备关闭
===========
当一个PCI设备驱动程序被卸载时,需要执行以下大部分步骤:
- 禁用设备产生的IRQ
- 释放IRQ(free_irq())
- 停止所有DMA活动
- 释放DMA缓冲区(包括流式和一致的)
- 从其他子系统(例如scsi或netdev)上取消注册
- 禁用设备对MMIO/IO端口地址的响应
- 释放MMIO/IO端口资源
停止设备上的IRQ
---------------
如何做到这一点是针对芯片/设备的。如果不这样做,如果(也只有在)IRQ与另一个设备
共享,就会出现“尖叫中断”的可能性。
当共享的IRQ处理程序被“解钩”时,使用同一IRQ线的其余设备仍然需要启用该IRQ。因此,
如果“脱钩”的设备断言IRQ线,假设它是其余设备中的一个断言IRQ线,系统将作出反应。
由于其他设备都不会处理这个IRQ,系统将“挂起”,直到它决定这个IRQ不会被处理并屏蔽
这个IRQ(100,000次之后)。一旦共享的IRQ被屏蔽,其余设备将停止正常工作。这不是
一个好事情。
这是使用MSI或MSI-X的另一个原因,如果它可用的话。MSI和MSI-X被定义为独占中断,
因此不容易受到“尖叫中断”问题的影响。
释放IRQ
-------
一旦设备被静止(不再有IRQ),就可以调用free_irq()。这个函数将在任何待处理
的IRQ被处理后返回控制,从该IRQ上“解钩”驱动程序的IRQ处理程序,最后如果没有人
使用该IRQ,则释放它。
停止所有DMA活动
---------------
在试图取消分配DMA控制数据之前,停止所有的DMA操作是非常重要的。如果不这样做,
可能会导致内存损坏、挂起,在某些芯片组上还会导致硬崩溃。
在停止IRQ后停止DMA可以避免IRQ处理程序可能重新启动DMA引擎的竞争。
虽然这个步骤听起来很明显,也很琐碎,但过去有几个“成熟”的驱动程序没有做好这个
步骤。
释放DMA缓冲区
-------------
一旦DMA被停止,首先要清理流式DMA。即取消数据缓冲区的映射,如果有的话,将缓
冲区返回给“上游”所有者。
然后清理包含控制数据的“一致的”缓冲区。
关于取消映射接口的细节,请参见Documentation/core-api/dma-api.rst。
从其他子系统取消注册
--------------------
大多数低级别的PCI设备驱动程序支持其他一些子系统,如USB、ALSA、SCSI、NetDev、
Infiniband等。请确保你的驱动程序没有从其他子系统中丢失资源。如果发生这种情况,
典型的症状是当子系统试图调用已经卸载的驱动程序时,会出现Oops(恐慌)。
禁止设备对MMIO/IO端口地址做出响应
---------------------------------
io_unmap() MMIO或IO端口资源,然后调用pci_disable_device()。
这与pci_enable_device()对称相反。
在调用pci_disable_device()后不要访问设备寄存器。
释放MMIO/IO端口资源
-------------------
调用pci_release_region()来标记MMIO或IO端口范围为可用。
如果不这样做,通常会导致无法重新加载驱动程序。
如何访问PCI配置空间
===================
你可以使用 `pci_(read|write)_config_(byte|word|dword)` 来访问由
`struct pci_dev *` 表示的设备的配置空间。所有这些函数在成功时返回0,或者返回一个
错误代码( `PCIBIOS_...` ),这个错误代码可以通过pcibios_strerror翻译成文本字
符串。大多数驱动程序希望对有效的PCI设备的访问不会失败。
如果你没有可用的pci_dev结构体,你可以调用
`pci_bus_(read|write)_config_(byte|word|dword)` 来访问一个给定的设备和该总
线上的功能。
如果你访问配置头的标准部分的字段,请使用<linux/pci.h>中声明的位置和位的符号名称。
如果你需要访问扩展的PCI功能寄存器,只要为特定的功能调用pci_find_capability(),
它就会为你找到相应的寄存器块。
其它有趣的函数
==============
============================= =================================================
pci_get_domain_bus_and_slot() 找到与给定的域、总线和槽以及编号相对应的pci_dev。
如果找到该设备,它的引用计数就会增加。
pci_set_power_state() 设置PCI电源管理状态(0=D0 ... 3=D3
pci_find_capability() 在设备的功能列表中找到指定的功能
pci_resource_start() 返回一个给定的PCI区域的总线起始地址
pci_resource_end() 返回给定PCI区域的总线末端地址
pci_resource_len() 返回一个PCI区域的字节长度
pci_set_drvdata() 为一个pci_dev设置私有驱动数据指针
pci_get_drvdata() 返回一个pci_dev的私有驱动数据指针
pci_set_mwi() 启用设备内存写无效
pci_clear_mwi() 关闭设备内存写无效
============================= =================================================
杂项提示
========
当向用户显示PCI设备名称时(例如,当驱动程序想告诉用户它找到了什么卡时),请使
用pci_name(pci_dev)。
始终通过对pci_dev结构体的指针来引用PCI设备。所有的PCI层函数都使用这个标识,
它是唯一合理的标识。除了非常特殊的目的,不要使用总线/插槽/功能号————在有多个
主总线的系统上,它们的语义可能相当复杂。
不要试图在你的驱动程序中开启快速寻址周期写入功能。总线上的所有设备都需要有这样
的功能,所以这需要由平台和通用代码来处理,而不是由单个驱动程序来处理。
供应商和设备标识
================
不要在include/linux/pci_ids.h中添加新的设备或供应商ID,除非它们是在多个驱
动程序中共享。如果有需要的话,你可以在你的驱动程序中添加私有定义,或者直接使用
普通的十六进制常量。
设备ID是任意的十六进制数字(厂商控制),通常只在一个地方使用,即pci_device_id
表。
请务必提交新的供应商/设备ID到https://pci-ids.ucw.cz/。在
https://github.com/pciutils/pciids,有一个pci.ids文件的镜像。
过时的函数
==========
当你试图将一个旧的驱动程序移植到新的PCI接口时,你可能会遇到几个函数。它们不再存
在于内核中,因为它们与热插拔或PCI域或具有健全的锁不兼容。
================= ===================================
pci_find_device() 被pci_get_device()取代
pci_find_subsys() 被pci_get_subsys()取代
pci_find_slot() 被pci_get_domain_bus_and_slot()取代
pci_get_slot() 被pci_get_domain_bus_and_slot()取代
================= ===================================
另一种方法是传统的PCI设备驱动,即走PCI设备列表。这仍然是可能的,但不鼓励这样做。
MMIO空间和“写通知”
==================
将驱动程序从使用I/O端口空间转换为使用MMIO空间,通常需要一些额外的改变。具体来说,
需要处理“写通知”。许多驱动程序(如tg3,acenic,sym53c8xx_2)已经做了这个。I/O
端口空间保证写事务在CPU继续之前到达PCI设备。对MMIO空间的写入允许CPU在事务到达PCI
设备之前继续。HW weenies称这为“写通知”,因为在事务到达目的地之前,写的完成被“通知”
给CPU。
因此,对时间敏感的代码应该添加readl(),CPU在做其他工作之前应该等待。经典的“位脉冲”
序列对I/O端口空间很有效::
for (i = 8; --i; val >>= 1) {
outb(val & 1, ioport_reg); /* 置位 */
udelay(10);
}
对MMIO空间来说,同样的顺序应该是::
for (i = 8; --i; val >>= 1) {
writeb(val & 1, mmio_reg); /* 置位 */
readb(safe_mmio_reg); /* 刷新写通知 */
udelay(10);
}
重要的是, ``safe_mmio_reg`` 不能有任何干扰设备正确操作的副作用。
另一种需要注意的情况是在重置PCI设备时。使用PCI配置空间读数来刷新writeel()。如果预期
PCI设备不响应readl(),这将在所有平台上优雅地处理PCI主控器的中止。大多数x86平台将允许
MMIO读取主控中止(又称“软失败”),并返回垃圾(例如~0)。但许多RISC平台会崩溃(又称“硬失败”)。
@@ -0,0 +1,192 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: <isonum.txt>
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/PCI/pciebus-howto.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
.. _cn_pciebus-howto:
===========================
PCI Express端口总线驱动指南
===========================
:作者: Tom L Nguyen tom.l.nguyen@intel.com 11/03/2004
:版权: |copy| 2004 Intel Corporation
关于本指南
==========
本指南介绍了PCI Express端口总线驱动程序的基本知识,并提供了如何使服务驱
动程序在PCI Express端口总线驱动程序中注册/取消注册的介绍。
什么是PCI Express端口总线驱动程序
=================================
一个PCI Express端口是一个逻辑的PCI-PCI桥结构。有两种类型的PCI Express端
口:根端口和交换端口。根端口从PCI Express根综合体发起一个PCI Express链接,
交换端口将PCI Express链接连接到内部逻辑PCI总线。交换机端口,其二级总线代表
交换机的内部路由逻辑,被称为交换机的上行端口。交换机的下行端口是从交换机的内部
路由总线桥接到代表来自PCI Express交换机的下游PCI Express链接的总线。
一个PCI Express端口可以提供多达四个不同的功能,在本文中被称为服务,这取决于
其端口类型。PCI Express端口的服务包括本地热拔插支持(HP)、电源管理事件支持(PME)、
高级错误报告支持(AER)和虚拟通道支持(VC)。这些服务可以由一个复杂的驱动程序
处理,也可以单独分布并由相应的服务驱动程序处理。
为什么要使用PCI Express端口总线驱动程序?
=========================================
在现有的Linux内核中,Linux设备驱动模型允许一个物理设备只由一个驱动处理。
PCI Express端口是一个具有多个不同服务的PCI-PCI桥设备。为了保持一个干净和简
单的解决方案,每个服务都可以有自己的软件服务驱动。在这种情况下,几个服务驱动将
竞争一个PCI-PCI桥设备。例如,如果PCI Express根端口的本机热拔插服务驱动程序
首先被加载,它就会要求一个PCI-PCI桥根端口。因此,内核不会为该根端口加载其他服
务驱动。换句话说,使用当前的驱动模型,不可能让多个服务驱动同时加载并运行在
PCI-PCI桥设备上。
为了使多个服务驱动程序同时运行,需要有一个PCI Express端口总线驱动程序,它管
理所有填充的PCI Express端口,并根据需要将所有提供的服务请求分配给相应的服务
驱动程序。下面列出了使用PCI Express端口总线驱动程序的一些关键优势:
- 允许在一个PCI-PCI桥接端口设备上同时运行多个服务驱动。
- 允许以独立的分阶段方式实施服务驱动程序。
- 允许一个服务驱动程序在多个PCI-PCI桥接端口设备上运行。
- 管理和分配PCI-PCI桥接端口设备的资源给要求的服务驱动程序。
配置PCI Express端口总线驱动程序与服务驱动程序
=============================================
将PCI Express端口总线驱动支持纳入内核
-------------------------------------
包括PCI Express端口总线驱动程序取决于内核配置中是否包含PCI Express支持。当内核
中的PCI Express支持被启用时,内核将自动包含PCI Express端口总线驱动程序作为内核
驱动程序。
启用服务驱动支持
----------------
PCI设备驱动是基于Linux设备驱动模型实现的。所有的服务驱动都是PCI设备驱动。如上所述,
一旦内核加载了PCI Express端口总线驱动程序,就不可能再加载任何服务驱动程序。为了满
足PCI Express端口总线驱动程序模型,需要对现有的服务驱动程序进行一些最小的改变,其
对现有的服务驱动程序的功能没有影响。
服务驱动程序需要使用下面所示的两个API,将其服务注册到PCI Express端口总线驱动程
序中(见第5.2.1和5.2.2节)。在调用这些API之前,服务驱动程序必须初始化头文件
/include/linux/pcieport_if.h中的pcie_port_service_driver数据结构。如果不这
样做,将导致身份不匹配,从而使PCI Express端口总线驱动程序无法加载服务驱动程序。
pcie_port_service_register
~~~~~~~~~~~~~~~~~~~~~~~~~~
::
int pcie_port_service_register(struct pcie_port_service_driver *new)
这个API取代了Linux驱动模型的 pci_register_driver API。一个服务驱动应该总是在模
块启动时调用 pcie_port_service_register。请注意,在服务驱动被加载后,诸如
pci_enable_device(dev) 和 pci_set_master(dev) 的调用不再需要,因为这些调用由
PCI端口总线驱动执行。
pcie_port_service_unregister
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
void pcie_port_service_unregister(struct pcie_port_service_driver *new)
pcie_port_service_unregister取代了Linux驱动模型的pci_unregister_driver。当一
个模块退出时,它总是被服务驱动调用。
示例代码
~~~~~~~~
下面是服务驱动代码示例,用于初始化端口服务的驱动程序数据结构。
::
static struct pcie_port_service_id service_id[] = { {
.vendor = PCI_ANY_ID,
.device = PCI_ANY_ID,
.port_type = PCIE_RC_PORT,
.service_type = PCIE_PORT_SERVICE_AER,
}, { /* end: all zeroes */ }
};
static struct pcie_port_service_driver root_aerdrv = {
.name = (char *)device_name,
.id_table = service_id,
.probe = aerdrv_load,
.remove = aerdrv_unload,
.suspend = aerdrv_suspend,
.resume = aerdrv_resume,
};
下面是一个注册/取消注册服务驱动的示例代码。
::
static int __init aerdrv_service_init(void)
{
int retval = 0;
retval = pcie_port_service_register(&root_aerdrv);
if (!retval) {
/*
* FIX ME
*/
}
return retval;
}
static void __exit aerdrv_service_exit(void)
{
pcie_port_service_unregister(&root_aerdrv);
}
module_init(aerdrv_service_init);
module_exit(aerdrv_service_exit);
可能的资源冲突
==============
由于PCI-PCI桥接端口设备的所有服务驱动被允许同时运行,下面列出了一些可能的资源冲突和
建议的解决方案。
MSI 和 MSI-X 向量资源
---------------------
一旦设备上的MSI或MSI-X中断被启用,它就会一直保持这种模式,直到它们再次被禁用。由于同
一个PCI-PCI桥接端口的服务驱动程序共享同一个物理设备,如果一个单独的服务驱动程序启用或
禁用MSI/MSI-X模式,可能会导致不可预知的行为。
为了避免这种情况,所有的服务驱动程序都不允许在其设备上切换中断模式。PCI Express端口
总线驱动程序负责确定中断模式,这对服务驱动程序来说应该是透明的。服务驱动程序只需要知道
分配给结构体pcie_device的字段irq的向量IRQ,当PCI Express端口总线驱动程序探测每
个服务驱动程序时,它被传入。服务驱动应该使用(struct pcie_device*)dev->irq来调用
request_irq/free_irq。此外,中断模式被存储在struct pcie_device的interrupt_mode
字段中。
PCI内存/IO映射的区域
--------------------
PCI Express电源管理(PME)、高级错误报告(AER)、热插拔(HP)和虚拟通道(VC)的服务
驱动程序访问PCI Express端口的PCI配置空间。在所有情况下,访问的寄存器是相互独立的。这
个补丁假定所有的服务驱动程序都会表现良好,不会覆盖其他服务驱动程序的配置设置。
PCI配置寄存器
-------------
每个服务驱动都在自己的功能结构体上运行PCI配置操作,除了PCI Express功能结构体,其中根控制
寄存器和设备控制寄存器是在PME和AER之间共享。这个补丁假定所有的服务驱动都会表现良好,不会
覆盖其他服务驱动的配置设置。
@@ -0,0 +1,126 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/PCI/sysfs-pci.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
========================
通过sysfs访问PCI设备资源
========================
sysfs,通常挂载在/sys,在支持它的平台上提供对PCI资源的访问。例如,一个特定的总线可能看起
来像这样::
/sys/devices/pci0000:17
|-- 0000:17:00.0
| |-- class
| |-- config
| |-- device
| |-- enable
| |-- irq
| |-- local_cpus
| |-- remove
| |-- resource
| |-- resource0
| |-- resource1
| |-- resource2
| |-- revision
| |-- rom
| |-- subsystem_device
| |-- subsystem_vendor
| `-- vendor
`-- ...
最上面的元素描述了PCI域和总线号码。在这种情况下,域号是0000,总线号是17(两个值都是十六进制)。
这个总线在0号插槽中包含一个单一功能的设备。为了方便起见,我们复制了域和总线的编号。在设备目录
下有几个文件,每个文件都有自己的功能。
=================== =====================================================
文件 功能
=================== =====================================================
class PCI级别 (ascii, ro)
config PCI配置空间 (binary, rw)
device PCI设备 (ascii, ro)
enable 设备是否被启用 (ascii, rw)
irq IRQ编号 (ascii, ro)
local_cpus 临近CPU掩码(cpumask, ro)
remove 从内核的列表中删除设备 (ascii, wo)
resource PCI资源主机地址 (ascii, ro)
resource0..N PCI资源N,如果存在的话 (binary, mmap, rw\ [1]_)
resource0_wc..N_wc PCI WC映射资源N,如果可预取的话 (binary, mmap)
revision PCI修订版 (ascii, ro)
rom PCI ROM资源,如果存在的话 (binary, ro)
subsystem_device PCI子系统设备 (ascii, ro)
subsystem_vendor PCI子系统供应商 (ascii, ro)
vendor PCI供应商 (ascii, ro)
=================== =====================================================
::
ro - 只读文件
rw - 文件是可读和可写的
wo - 只写文件
mmap - 文件是可移动的
ascii - 文件包含ascii文本
binary - 文件包含二进制数据
cpumask - 文件包含一个cpumask类型的
.. [1] rw 仅适用于 IORESOURCE_IO(I/O 端口)区域
只读文件是信息性的,对它们的写入将被忽略,但 "rom "文件除外。可写文件可以用来在设备上执
行操作(例如,改变配置空间,分离设备)。 mmapable文件可以通过偏移量为0的文件的mmap获得,
可以用来从用户空间进行实际的设备编程。注意,有些平台不支持某些资源的mmapping,所以一定要
检查任何尝试的mmap的返回值。其中最值得注意的是I/O端口资源,它也提供读/写访问。
enable "文件提供了一个计数器,表明设备已经被启用了多少次。如果'enable'文件目前返回'4',
而一个'1'被呼入它,它将返回'5'。向它呼入一个'0'会减少计数。不过,即使它返回到0,一些初始
化可能也不会被逆转。
rom "文件很特别,因为它提供了对设备ROM文件的只读访问,如果有的话。然而,它在默认情况下是
禁用的,所以应用程序应该在尝试读取调用之前将字符串 "1 "写入该文件以启用它,并在访问之后将
"0 "写入该文件以禁用它。请注意,设备必须被启用,才能成功返回数据。如果驱动没有被绑定到设备
上,可以使用上面提到的 "enable "文件将其启用。
remove "文件是用来移除PCI设备的,通过向该文件写入一个非零的整数。这并不涉及任何形式的热插
拔功能,例如关闭设备的电源。该设备被从内核的PCI设备列表中移除,它的sysfs目录被移除,并且该
设备将被从任何连接到它的驱动程序中移除。移除PCI根总线是不允许的。
通过sysfs访问原有资源
---------------------
如果底层平台支持的话,传统的I/O端口和ISA内存资源也会在sysfs中提供。它们位于PCI类的层次结构
中,例如::
/sys/class/pci_bus/0000:17/
|-- bridge -> ../../../devices/pci0000:17
|-- cpuaffinity
|-- legacy_io
`-- legacy_mem
legacy_io文件是一个读/写文件,可以被应用程序用来做传统的端口I/O。应用程序应该打开该文件,寻
找所需的端口(例如0x3e8),并进行1、2或4字节的读或写。legacy_mem文件应该被mmapped,其偏移
量与所需的内存偏移量相对应,例如0xa0000用于VGA帧缓冲器。然后,应用程序可以简单地解除引用返回
的指针(当然是在检查了错误之后)来访问遗留内存空间。
支持新平台上的PCI访问
---------------------
为了支持上述的PCI资源映射,Linux平台代码最好定义ARCH_GENERIC_PCI_MMAP_RESOURCE并使用该
功能的通用实现。为了支持通过/proc/bus/pci中的文件实现mmap()的历史接口,平台也可以设置
HAVE_PCI_MMAP。
另外,设置了 HAVE_PCI_MMAP 的平台可以提供他们自己的 pci_mmap_page_range() 实现,而不是定
义 ARCH_GENERIC_PCI_MMAP_RESOURCE。
支持PCI资源的写组合映射的平台必须定义arch_can_pci_mmap_wc(),当写组合被允许时,在运行时应
评估为非零。支持I/O资源映射的平台同样定义arch_can_pci_mmap_io()。
遗留资源由HAVE_PCI_LEGACY定义保护。希望支持遗留功能的平台应该定义它并提供 pci_legacy_read,
pci_legacy_write 和 pci_mmap_legacy_page_range 函数。
@@ -0,0 +1,112 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/accounting/delay-accounting.rst
:Translator: Yang Yang <yang.yang29@zte.com.cn>
========
延时计数
========
任务在等待某些内核资源可用时,会造成延时。例如一个可运行的任务可能会等待
一个空闲CPU来运行。
基于每任务的延时计数功能度量由以下情况造成的任务延时:
a) 等待一个CPU(任务为可运行)
b) 完成由该任务发起的块I/O同步请求
c) 页面交换
d) 内存回收
e) 抖动
f) 直接规整
g) 写保护复制
并将这些统计信息通过taskstats接口提供给用户空间。
这些延时信息为适当的调整任务CPU优先级、io优先级、rss限制提供反馈。重要任务
长期延时,表示可能需要提高其相关优先级。
通过使用taskstats接口,本功能还可提供一个线程组(对应传统Unix进程)所有任务
(或线程)的总延时统计信息。此类汇总往往是需要的,由内核来完成更加高效。
用户空间的实体,特别是资源管理程序,可将延时统计信息汇总到任意组中。为实现
这一点,任务的延时统计信息在其生命周期内和退出时皆可获取,从而确保可进行
连续、完整的监控。
接口
----
延时计数使用taskstats接口,该接口由本目录另一个单独的文档详细描述。Taskstats
向用户态返回一个通用数据结构,对应每pid或每tgid的统计信息。延时计数功能填写
该数据结构的特定字段。见
include/uapi/linux/taskstats.h
其描述了延时计数相关字段。系统通常以计数器形式返回 CPU、同步块 I/O、交换、内存
回收、页缓存抖动、直接规整、写保护复制等的累积延时。
取任务某计数器两个连续读数的差值,将得到任务在该时间间隔内等待对应资源的总延时。
当任务退出时,内核会将包含每任务的统计信息发送给用户空间,而无需额外的命令。
若其为线程组最后一个退出的任务,内核还会发送每tgid的统计信息。更多详细信息见
taskstats接口的描述。
tools/accounting目录中的用户空间程序getdelays.c提供了一些简单的命令,用以显示
延时统计信息。其也是使用taskstats接口的示例。
用法
----
使用以下配置编译内核::
CONFIG_TASK_DELAY_ACCT=y
CONFIG_TASKSTATS=y
延时计数在启动时默认关闭。
若需开启,在启动参数中增加::
delayacct
本文后续的说明基于延时计数已开启。也可在系统运行时,使用sysctl的
kernel.task_delayacct进行开关。注意,只有在启用延时计数后启动的
任务才会有相关信息。
系统启动后,使用类似getdelays.c的工具获取任务或线程组(tgid)的延时信息。
getdelays命令的一般格式::
getdelays [-dilv] [-t tgid] [-p pid]
获取pid为10的任务从系统启动后的延时信息::
# ./getdelays -d -p 10
(输出信息和下例相似)
获取所有tgid为5的任务从系统启动后的总延时信息::
# ./getdelays -d -t 5
print delayacct stats ON
TGID 5
CPU count real total virtual total delay total delay average
8 7000000 6872122 3382277 0.423ms
IO count delay total delay average
0 0 0.000ms
SWAP count delay total delay average
0 0 0.000ms
RECLAIM count delay total delay average
0 0 0.000ms
THRASHING count delay total delay average
0 0 0.000ms
COMPACT count delay total delay average
0 0 0.000ms
WPCOPY count delay total delay average
0 0 0ms
获取pid为1的IO计数,它只和-p一起使用::
# ./getdelays -i -p 1
printing IO accounting
linuxrc: read=65536, write=0, cancelled_write=0
上面的命令与-v一起使用,可以获取更多调试信息。
@@ -0,0 +1,25 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/accounting/index.rst
:Translator: Yang Yang <yang.yang29@zte.com.cn>
.. _cn_accounting_index.rst:
====
计数
====
.. toctree::
:maxdepth: 1
delay-accounting
psi
taskstats
Todolist:
cgroupstats
taskstats-struct
@@ -0,0 +1,155 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/accounting/psi.rst
:Translator: Yang Yang <yang.yang29@zte.com.cn>
.. _cn_psi.rst:
=================
PSI——压力阻塞信息
=================
:日期: April, 2018
:作者: Johannes Weiner <hannes@cmpxchg.org>
当CPU、memory或IO设备处于竞争状态,业务负载会遭受时延毛刺、吞吐量降低,
及面临OOM的风险。
如果没有一种准确的方法度量系统竞争程度,则有两种后果:一种是用户过于节制,
未充分利用系统资源;另一种是过度使用,经常性面临业务中断的风险。
psi特性能够识别和量化资源竞争导致的业务中断,及其对复杂负载乃至整个系统在
时间上的影响。
准确度量因资源不足造成的生产力损失,有助于用户基于硬件调整业务负载,或基
于业务负载配置硬件。
psi能够实时的提供相关信息,因此系统可基于psi实现动态的负载管理。如实施
卸载、迁移、策略性的停止或杀死低优先级或可重启的批处理任务。
psi帮助用户实现硬件资源利用率的最大化。同时无需牺牲业务负载健康度,也无需
面临OOM等造成业务中断的风险。
压力接口
========
压力信息可通过/proc/pressure/ --cpu、memory、io文件分别获取。
CPU相关信息格式如下:
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
内存和IO相关信息如下:
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
some行代表至少有一个任务阻塞于特定资源的时间占比。
full行代表所有非idle任务同时阻塞于特定资源的时间占比。在这种状态下CPU资源
完全被浪费,相对于正常运行,业务负载由于耗费更多时间等待而受到严重影响。
由于此情况严重影响系统性能,因此清楚的识别本情况并与some行所代表的情况区分开,
将有助于分析及提升系统性能。这就是full独立于some行的原因。
avg代表阻塞时间占比(百分比),为最近10秒、60秒、300秒内的均值。这样我们
既可观察到短期事件的影响,也可看到中等及长时间内的趋势。total代表总阻塞
时间(单位微秒),可用于观察时延毛刺,这种毛刺可能在均值中无法体现。
监控压力门限
============
用户可注册触发器,通过poll()监控资源压力是否超过门限。
触发器定义:指定时间窗口期内累积阻塞时间的最大值。比如可定义500ms内积累
100ms阻塞,即触发一次唤醒事件。
触发器注册方法:用户打开代表特定资源的psi接口文件,写入门限、时间窗口的值。
所打开的文件描述符用于等待事件,可使用select()、poll()、epoll()。
写入信息的格式如下:
<some|full> <stall amount in us> <time window in us>
示例:向/proc/pressure/memory写入"some 150000 1000000"将新增触发器,将在
1秒内至少一个任务阻塞于内存的总时间超过150ms时触发。向/proc/pressure/io写入
"full 50000 1000000"将新增触发器,将在1秒内所有任务都阻塞于io的总时间超过50ms时触发。
触发器可针对多个psi度量值设置,同一个psi度量值可设置多个触发器。每个触发器需要
单独的文件描述符用于轮询,以区分于其他触发器。所以即使对于同一个psi接口文件,
每个触发器也需要单独的调用open()。
监控器在被监控资源进入阻塞状态时启动,在系统退出阻塞状态后停用。系统进入阻塞
状态后,监控psi增长的频率为每监控窗口刷新10次。
内核接受的窗口为500ms~10s,所以监控间隔为50ms~1s。设置窗口下限目的是为了
防止过于频繁的轮询。设置窗口上限的目的是因为窗口过长则无意义,此时查看
psi接口提供的均值即可。
监控器在激活后,至少在跟踪窗口期间将保持活动状态。以避免随着系统进入和退出
阻塞状态,监控器过于频繁的进入和退出活动状态。
用户态通知在监控窗口内会受到速率限制。当对应的文件描述符关闭,触发器会自动注销。
用户态监控器使用示例
====================
::
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <string.h>
#include <unistd.h>
/* 监控内存部分阻塞,监控时间窗口为1秒、阻塞门限为150毫秒。*/
int main() {
const char trig[] = "some 150000 1000000";
struct pollfd fds;
int n;
fds.fd = open("/proc/pressure/memory", O_RDWR | O_NONBLOCK);
if (fds.fd < 0) {
printf("/proc/pressure/memory open error: %s\n",
strerror(errno));
return 1;
}
fds.events = POLLPRI;
if (write(fds.fd, trig, strlen(trig) + 1) < 0) {
printf("/proc/pressure/memory write error: %s\n",
strerror(errno));
return 1;
}
printf("waiting for events...\n");
while (1) {
n = poll(&fds, 1, -1);
if (n < 0) {
printf("poll error: %s\n", strerror(errno));
return 1;
}
if (fds.revents & POLLERR) {
printf("got POLLERR, event source is gone\n");
return 0;
}
if (fds.revents & POLLPRI) {
printf("event triggered!\n");
} else {
printf("unknown event received: 0x%x\n", fds.revents);
return 1;
}
}
return 0;
}
Cgroup2接口
===========
对于CONFIG_CGROUP=y及挂载了cgroup2文件系统的系统,能够获取cgroups内任务的psi。
此场景下cgroupfs挂载点的子目录包含cpu.pressure、memory.pressure、io.pressure文件,
内容格式与/proc/pressure/下的文件相同。
可设置基于cgroup的psi监控器,方法与系统级psi监控器相同。
@@ -0,0 +1,145 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/accounting/taskstats.rst
:Translator: Yang Yang <yang.yang29@zte.com.cn>
================
每任务的统计接口
================
Taskstats是一个基于netlink的接口,用于从内核向用户空间发送每任务及每进程的
统计信息。
Taskstats设计目的:
- 在任务生命周期内和退出时高效的提供统计信息
- 统一不同计数子系统的接口
- 支持未来计数系统的扩展
术语
----
“pid”、“tid”、“任务”互换使用,用于描述由struct task_struct定义的标准
Linux任务。“每pid的统计数据”等价于“每任务的统计数据”。
“tgid”、“进程”、“线程组”互换使用,用于描述共享mm_struct的任务集,
也就是传统的Unix进程。尽管使用了tgid这个词,即使一个任务是线程组组长,
对它的处理也没有什么不同。只要一个进程还有任何归属它的任务,它就被认为
活着。
用法
----
为了在任务生命周期内获得统计信息,用户空间需打开一个单播的netlink套接字
(NETLINK_GENERIC族)然后发送指定pid或tgid的命令。响应消息中包含单个
任务的统计信息(若指定了pid)或进程所有任务汇总的统计信息(若指定了tgid)。
为了在任务退出时获取统计信息,用户空间的监听者发送一个指定cpu掩码的注册命令。
cpu掩码内的cpu上有任务退出时,每pid的统计信息将发送给注册成功的监听者。使用
cpu掩码可以限制一个监听者收到的数据,并有助于对netlink接口进行流量控制,后文
将进行更详细的解释。
如果正在退出的任务是线程组中最后一个退出的线程,额外一条包含每tgid统计信息的
记录也将发送给用户空间。后者包含线程组中所有线程(包括过去和现在)的每pid统计
信息总和。
getdelays.c是一个简单的示例,用以演示如何使用taskstats接口获取延迟统计信息。
用户可注册cpu掩码、发送命令和处理响应、监听每tid/tgid退出数据、将收到的数据
写入文件、通过增大接收缓冲区进行基本的流量控制。
接口
----
内核用户接口封装在include/linux/taskstats.h。
为避免本文档随着接口的演进而过期,本文仅给出当前版本的概要。当本文与taskstats.h
不一致时,以taskstats.h为准。
struct taskstats是每pid和每tgid数据共用的计数结构体。它是版本化的,可在内核新增
计数子系统时进行扩展。taskstats.h中定义了各字段及语义。
用户、内核空间的数据交换是属于NETLINK_GENERIC族的netlink消息,使用netlink属性
接口。消息格式如下::
+----------+- - -+-------------+-------------------+
| nlmsghdr | Pad | genlmsghdr | taskstats payload |
+----------+- - -+-------------+-------------------+
Taskstats载荷有三种类型:
1. 命令:由用户发送给内核。获取指定pid/tgid数据的命令包含一个类型为
TASKSTATS_CMD_ATTR_PID/TGID的属性,该属性包含u32的pid或tgid载荷。
pid/tgid指示用户空间要统计的任务/进程。
注册/注销获取指定cpu集上退出数据的命令包含一个类型为
TASKSTATS_CMD_ATTR_REGISTER/DEREGISTER_CPUMASK的属性,该属性包含cpu掩码载荷。
cpu掩码是以ascii码表示,用逗号分隔的cpu范围。例如若需监听1,2,3,5,7,8号cpu的
退出数据,cpu掩码表示为"1-3,5,7-8"。若用户空间在关闭监听套接字前忘了注销监听
的cpu集,随着时间的推移,内核会清理此监听集。但是,出于提效的目的,建议明确
执行注销。
2. 命令的应答:内核发出应答用户空间的命令。载荷有三类属性:
a) TASKSTATS_TYPE_AGGR_PID/TGID: 本属性不包含载荷,用以指示其后为被统计对象
的pig/tgid。
b) TASKSTATS_TYPE_PID/TGID:本属性的载荷为pig/tgid,其统计信息将被返回。
c) TASKSTATS_TYPE_STATS:本属性的载荷为一个struct taskstats实例。每pid和
每tgid统计信息共用该结构体。
3. 内核会在任务退出时发送新消息。其载荷包含一系列以下类型的属性:
a) TASKSTATS_TYPE_AGGR_PID:指示其后两个属性为pid+stats。
b) TASKSTATS_TYPE_PID:包含退出任务的pid。
c) TASKSTATS_TYPE_STATS:包含退出任务的每pid统计信息
d) TASKSTATS_TYPE_AGGR_TGID:指示其后两个属性为tgid+stats。
e) TASKSTATS_TYPE_TGID:包含任务所属进程的tgid
f) TASKSTATS_TYPE_STATS:包含退出任务所属进程的每tgid统计信息
每tgid的统计
------------
除了每任务的统计信息,taskstats还提供每进程的统计信息,因为资源管理通常以进程
粒度完成,并且仅在用户空间聚合任务统计信息效率低下且可能不准确(缺乏原子性)。
然而,除了每任务统计信息,在内核中维护每进程统计信息存在额外的时间和空间开销。
为解决此问题,taskstats代码将退出任务的统计信息累积到进程范围的数据结构中。
当进程最后一个任务退出时,累积的进程级数据也会发送到用户空间(与每任务数据一起)。
当用户查询每tgid数据时,内核将指定线程组中所有活动线程的统计信息相加,并添加到
该线程组的累积总数(含之前退出的线程)。
扩展taskstats
-------------
有两种方法可在未来修改内核扩展taskstats接口,以导出更多的每任务/进程统计信息:
1. 在现有struct taskstats末尾增加字段。该结构体中的版本号确保了向后兼容性。
用户空间将仅使用与其版本对应的结构体字段。
2. 定义单独的统计结构体并使用netlink属性接口返回对应的数据。由于用户空间独立
处理每个netlink属性,所以总是可以忽略其不理解类型的属性(因为使用了旧版本接口)。
在1.和2.之间进行选择,属于权衡灵活性和开销的问题。若仅需增加少数字段,那么1.是
首选方法,因为内核和用户空间无需承担处理新netlink属性的开销。但若新字段过多的
扩展现有结构体,导致不同的用户空间计数程序不必要的接收大型结构体,而对结构体
字段并不感兴趣,那么2.是值得的。
Taskstats的流量控制
-------------------
当退出任务数速率变大,监听者可能跟不上内核发送每tid/tgid退出数据的速率,而导致
数据丢失。taskstats结构体变大、cpu数量上升,都会导致这种可能性增加。
为避免统计信息丢失,用户空间应执行以下操作中至少一项:
- 增大监听者用于接收退出数据的netlink套接字接收缓存区。
- 创建更多的监听者,减少每个监听者监听的cpu数量。极端情况下可为每个cpu创建
一个监听者。用户还可考虑将监听者的cpu亲和性设置为监听cpu的子集,特别是当他们
仅监听一个cpu。
尽管采取了这些措施,若用户空间仍收到指示接收缓存区溢出的ENOBUFS错误消息,
则应采取其他措施处理数据丢失。
@@ -0,0 +1,291 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/README.rst
:译者:
吴想成 Wu XiangCheng <bobwxc@email.cn>
Linux内核6.x版本 <http://kernel.org/>
=========================================
以下是Linux版本6的发行注记。仔细阅读它们,
它们会告诉你这些都是什么,解释如何安装内核,以及遇到问题时该如何做。
什么是Linux?
---------------
Linux是Unix操作系统的克隆版本,由Linus Torvalds在一个松散的网络黑客
(Hacker,无贬义)团队的帮助下从头开始编写。它旨在实现兼容POSIX和
单一UNIX规范。
它具有在现代成熟的Unix中应当具有的所有功能,包括真正的多任务处理、虚拟内存、
共享库、按需加载、共享的写时拷贝(COW)可执行文件、恰当的内存管理以及包括
IPv4和IPv6在内的复合网络栈。
Linux在GNU通用公共许可证,版本2(GNU GPLv2)下分发,详见随附的COPYING文件。
它能在什么样的硬件上运行?
-----------------------------
虽然Linux最初是为32位的x86 PC机(386或更高版本)开发的,但今天它也能运行在
(至少)Compaq Alpha AXP、Sun SPARC与UltraSPARC、Motorola 68000、PowerPC、
PowerPC64、ARM、Hitachi SuperH、Cell、IBM S/390、MIPS、HP PA-RISC、Intel
IA-64、DEC VAX、AMD x86-64 Xtensa和ARC架构上。
Linux很容易移植到大多数通用的32位或64位体系架构,只要它们有一个分页内存管理
单元(PMMU)和一个移植的GNU C编译器(gcc;GNU Compiler Collection,GCC的一
部分)。Linux也被移植到许多没有PMMU的体系架构中,尽管功能显然受到了一定的
限制。
Linux也被移植到了其自己上。现在可以将内核作为用户空间应用程序运行——这被
称为用户模式Linux(UML)。
文档
-----
因特网上和书籍上都有大量的电子文档,既有Linux专属文档,也有与一般UNIX问题相关
的文档。我建议在任何Linux FTP站点上查找LDP(Linux文档项目)书籍的文档子目录。
本自述文件并不是关于系统的文档:有更好的可用资源。
- 因特网上和书籍上都有大量的(电子)文档,既有Linux专属文档,也有与普通
UNIX问题相关的文档。我建议在任何有LDP(Linux文档项目)书籍的Linux FTP
站点上查找文档子目录。本自述文件并不是关于系统的文档:有更好的可用资源。
- 文档/子目录中有各种自述文件:例如,这些文件通常包含一些特定驱动程序的
内核安装说明。请阅读
:ref:`Documentation/process/changes.rst <changes>` 文件,它包含了升级内核
可能会导致的问题的相关信息。
安装内核源代码
---------------
- 如果您要安装完整的源代码,请把内核tar档案包放在您有权限的目录中(例如您
的主目录)并将其解包::
xz -cd linux-6.x.tar.xz | tar xvf -
将“X”替换成最新内核的版本号。
【不要】使用 /usr/src/linux 目录!这里有一组库头文件使用的内核头文件
(通常是不完整的)。它们应该与库匹配,而不是被内核的变化搞得一团糟。
- 您还可以通过打补丁在6.x版本之间升级。补丁以xz格式分发。要通过打补丁进行
安装,请获取所有较新的补丁文件,进入内核源代码(linux-6.x)的目录并
执行::
xz -cd ../patch-6.x.xz | patch -p1
请【按顺序】替换所有大于当前源代码树版本的“x”,这样就可以了。您可能想要
删除备份文件(文件名类似xxx~ 或 xxx.orig),并确保没有失败的补丁(文件名
类似xxx# 或 xxx.rej)。如果有,不是你就是我犯了错误。
与6.x内核的补丁不同,6.x.y内核(也称为稳定版内核)的补丁不是增量的,而是
直接应用于基本的6.x内核。例如,如果您的基本内核是6.0,并且希望应用6.0.3
补丁,则不应先应用6.0.1和6.0.2的补丁。类似地,如果您运行的是6.0.2内核,
并且希望跳转到6.0.3,那么在应用6.0.3补丁之前,必须首先撤销6.0.2补丁
(即patch -R)。更多关于这方面的内容,请阅读
:ref:`Documentation/process/applying-patches.rst <applying_patches>` 。
或者,脚本 patch-kernel 可以用来自动化这个过程。它能确定当前内核版本并
应用找到的所有补丁::
linux/scripts/patch-kernel linux
上面命令中的第一个参数是内核源代码的位置。补丁是在当前目录应用的,但是
可以将另一个目录指定为第二个参数。
- 确保没有过时的 .o 文件和依赖项::
cd linux
make mrproper
现在您应该已经正确安装了源代码。
软件要求
---------
编译和运行6.x内核需要各种软件包的最新版本。请参考
:ref:`Documentation/process/changes.rst <changes>`
来了解最低版本要求以及如何升级软件包。请注意,使用过旧版本的这些包可能会
导致很难追踪的间接错误,因此不要以为在生成或操作过程中出现明显问题时可以
只更新包。
为内核建立目录
---------------
编译内核时,默认情况下所有输出文件都将与内核源代码放在一起。使用
``make O=output/dir`` 选项可以为输出文件(包括 .config)指定备用位置。
例如::
kernel source code: /usr/src/linux-6.x
build directory: /home/name/build/kernel
要配置和构建内核,请使用::
cd /usr/src/linux-6.x
make O=/home/name/build/kernel menuconfig
make O=/home/name/build/kernel
sudo make O=/home/name/build/kernel modules_install install
请注意:如果使用了 ``O=output/dir`` 选项,那么它必须用于make的所有调用。
配置内核
---------
即使只升级一个小版本,也不要跳过此步骤。每个版本中都会添加新的配置选项,
如果配置文件没有按预定设置,就会出现奇怪的问题。如果您想以最少的工作量
将现有配置升级到新版本,请使用 ``make oldconfig`` ,它只会询问您新配置
选项的答案。
- 其他配置命令包括::
"make config" 纯文本界面。
"make menuconfig" 基于文本的彩色菜单、选项列表和对话框。
"make nconfig" 增强的基于文本的彩色菜单。
"make xconfig" 基于Qt的配置工具。
"make gconfig" 基于GTK的配置工具。
"make oldconfig" 基于现有的 ./.config 文件选择所有选项,并询问
新配置选项。
"make olddefconfig"
类似上一个,但不询问直接将新选项设置为默认值。
"make defconfig" 根据体系架构,使用arch/$arch/defconfig或
arch/$arch/configs/${PLATFORM}_defconfig中的
默认选项值创建./.config文件。
"make ${PLATFORM}_defconfig"
使用arch/$arch/configs/${PLATFORM}_defconfig中
的默认选项值创建一个./.config文件。
用“make help”来获取您体系架构中所有可用平台的列表。
"make allyesconfig"
通过尽可能将选项值设置为“y”,创建一个
./.config文件。
"make allmodconfig"
通过尽可能将选项值设置为“m”,创建一个
./.config文件。
"make allnoconfig" 通过尽可能将选项值设置为“n”,创建一个
./.config文件。
"make randconfig" 通过随机设置选项值来创建./.config文件。
"make localmodconfig" 基于当前配置和加载的模块(lsmod)创建配置。禁用
已加载的模块不需要的任何模块选项。
要为另一台计算机创建localmodconfig,请将该计算机
的lsmod存储到一个文件中,并将其作为lsmod参数传入。
此外,通过在参数LMC_KEEP中指定模块的路径,可以将
模块保留在某些文件夹或kconfig文件中。
target$ lsmod > /tmp/mylsmod
target$ scp /tmp/mylsmod host:/tmp
host$ make LSMOD=/tmp/mylsmod \
LMC_KEEP="drivers/usb:drivers/gpu:fs" \
localmodconfig
上述方法在交叉编译时也适用。
"make localyesconfig" 与localmodconfig类似,只是它会将所有模块选项转换
为内置(=y)。你可以同时通过LMC_KEEP保留模块。
"make kvm_guest.config"
为kvm客户机内核支持启用其他选项。
"make xen.config" 为xen dom0客户机内核支持启用其他选项。
"make tinyconfig" 配置尽可能小的内核。
更多关于使用Linux内核配置工具的信息,见文档
Documentation/kbuild/kconfig.rst。
- ``make config`` 注意事项:
- 包含不必要的驱动程序会使内核变大,并且在某些情况下会导致问题:
探测不存在的控制器卡可能会混淆其他控制器。
- 如果存在协处理器,则编译了数学仿真的内核仍将使用协处理器:在
这种情况下,数学仿真永远不会被使用。内核会稍微大一点,但不管
是否有数学协处理器,都可以在不同的机器上工作。
- “kernel hacking”配置细节通常会导致更大或更慢的内核(或两者
兼而有之),甚至可以通过配置一些例程来主动尝试破坏坏代码以发现
内核问题,从而降低内核的稳定性(kmalloc())。因此,您可能应该
用于研究“开发”、“实验”或“调试”特性相关问题。
编译内核
---------
- 确保您至少有gcc 8.1可用。
有关更多信息,请参阅 :ref:`Documentation/process/changes.rst <changes>` 。
- 执行 ``make`` 来创建压缩内核映像。如果您安装了lilo以适配内核makefile,
那么也可以进行 ``make install`` ,但是您可能需要先检查特定的lilo设置。
实际安装必须以root身份执行,但任何正常构建都不需要。
无须徒然使用root身份。
- 如果您将内核的任何部分配置为模块,那么还必须执行 ``make modules_install`` 。
- 详细的内核编译/生成输出:
通常,内核构建系统在相当安静的模式下运行(但不是完全安静)。但是有时您或
其他内核开发人员需要看到编译、链接或其他命令的执行过程。为此,可使用
“verbose(详细)”构建模式。
向 ``make`` 命令传递 ``V=1`` 来实现,例如::
make V=1 all
如需构建系统也给出内个目标重建的愿意,请使用 ``V=2`` 。默认为 ``V=0`` 。
- 准备一个备份内核以防出错。对于开发版本尤其如此,因为每个新版本都包含
尚未调试的新代码。也要确保保留与该内核对应的模块的备份。如果要安装
与工作内核版本号相同的新内核,请在进行 ``make modules_install`` 安装
之前备份modules目录。
或者,在编译之前,使用内核配置选项“LOCALVERSION”向常规内核版本附加
一个唯一的后缀。LOCALVERSION可以在“General Setup”菜单中设置。
- 为了引导新内核,您需要将内核映像(例如编译后的
.../linux/arch/x86/boot/bzImage)复制到常规可引导内核的位置。
- 不再支持在没有LILO等启动装载程序帮助的情况下直接从软盘引导内核。
如果从硬盘引导Linux,很可能使用LILO,它使用/etc/lilo.conf文件中
指定的内核映像文件。内核映像文件通常是/vmlinuz、/boot/vmlinuz、
/bzImage或/boot/bzImage。使用新内核前,请保存旧映像的副本,并复制
新映像覆盖旧映像。然后您【必须重新运行LILO】来更新加载映射!否则,
将无法启动新的内核映像。
重新安装LILO通常需要运行/sbin/LILO。您可能希望编辑/etc/lilo.conf
文件为旧内核映像指定一个条目(例如/vmlinux.old)防止新的不能正常
工作。有关更多信息,请参阅LILO文档。
重新安装LILO之后,您应该就已经准备好了。关闭系统,重新启动,尽情
享受吧!
如果需要更改内核映像中的默认根设备、视频模式等,请在适当的地方使用
启动装载程序的引导选项。无需重新编译内核即可更改这些参数。
- 使用新内核重新启动并享受它吧。
若遇到问题
-----------
如果您发现了一些可能由于内核缺陷所导致的问题,请参阅:
Documentation/translations/zh_CN/admin-guide/reporting-issues.rst 。
想要理解内核错误报告,请参阅:
Documentation/translations/zh_CN/admin-guide/bug-hunting.rst 。
更多用GDB调试内核的信息,请参阅:
Documentation/translations/zh_CN/dev-tools/gdb-kernel-debugging.rst
和 Documentation/process/debugging/kgdb.rst 。
@@ -0,0 +1,293 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/bootconfig.rst
:译者: 吴想成 Wu XiangCheng <bobwxc@email.cn>
========
引导配置
========
:作者: Masami Hiramatsu <mhiramat@kernel.org>
概述
====
引导配置扩展了现有的内核命令行,以一种更有效率的方式在引导内核时进一步支持
键值数据。这允许管理员传递一份结构化关键字的配置文件。
配置文件语法
============
引导配置文件的语法采用非常简单的键值结构。每个关键字由点连接的单词组成,键
和值由 ``=`` 连接。值以分号( ``;`` )或换行符( ``\n`` )结尾。数组值中每
个元素由逗号( ``,`` )分隔。::
KEY[.WORD[...]] = VALUE[, VALUE2[...]][;]
与内核命令行语法不同,逗号和 ``=`` 周围允许有空格。
关键字只允许包含字母、数字、连字符( ``-`` )和下划线( ``_`` )。值可包含
可打印字符和空格,但分号( ``;`` )、换行符( ``\n`` )、逗号( ``,`` )、
井号( ``#`` )和右大括号( ``}`` )等分隔符除外。
如果你需要在值中使用这些分隔符,可以用双引号( ``"VALUE"`` )或单引号
( ``'VALUE'`` )括起来。注意,引号无法转义。
键的值可以为空或不存在。这些键用于检查该键是否存在(类似布尔值)。
键值语法
--------
引导配置文件语法允许用户通过大括号合并键名部分相同的关键字。例如::
foo.bar.baz = value1
foo.bar.qux.quux = value2
也可以写成::
foo.bar {
baz = value1
qux.quux = value2
}
或者更紧凑一些,写成::
foo.bar { baz = value1; qux.quux = value2 }
在这两种样式中,引导解析时相同的关键字都会自动合并。因此可以追加类似的树或
键值。
相同关键字的值
--------------
禁止两个或多个值或数组共享同一个关键字。例如::
foo = bar, baz
foo = qux # !错误! 我们不可以重定义相同的关键字
如果你想要更新值,必须显式使用覆盖操作符 ``:=`` 。例如::
foo = bar, baz
foo := qux
这样 ``foo`` 关键字的值就变成了 ``qux`` 。这对于通过添加(部分)自定义引导
配置来覆盖默认值非常有用,免于解析默认引导配置。
如果你想对现有关键字追加值作为数组成员,可以使用 ``+=`` 操作符。例如::
foo = bar, baz
foo += qux
这样, ``foo`` 关键字就同时拥有了 ``bar`` , ``baz`` 和 ``qux`` 。
此外,父关键字下可同时存在值和子关键字。
例如,下列配置是可行的。::
foo = value1
foo.bar = value2
foo := value3 # 这会更新foo的值。
注意,裸值不能直接放进结构化关键字中,必须在大括号外定义它。例如::
foo {
bar = value1
bar {
baz = value2
qux = value3
}
}
同时,关键字下值节点的顺序是固定的。如果值和子关键字同时存在,值永远是该关
键字的第一个子节点。因此如果用户先指定子关键字,如::
foo.bar = value1
foo = value2
则在程序(和/proc/bootconfig)中,它会按如下显示::
foo = value2
foo.bar = value1
注释
----
配置语法接受shell脚本风格的注释。注释以井号( ``#`` )开始,到换行符
( ``\n`` )结束。
::
# comment line
foo = value # value is set to foo.
bar = 1, # 1st element
2, # 2nd element
3 # 3rd element
会被解析为::
foo = value
bar = 1, 2, 3
注意你不能把注释放在值和分隔符( ``,`` 或 ``;`` )之间。如下配置语法是错误的::
key = 1 # comment
,2
/proc/bootconfig
================
/proc/bootconfig是引导配置的用户空间接口。与/proc/cmdline不同,此文件内容以
键值列表样式显示。
每个键值对一行,样式如下::
KEY[.WORDS...] = "[VALUE]"[,"VALUE2"...]
用引导配置引导内核
==================
用引导配置引导内核有两种方法:将引导配置附加到initrd镜像或直接嵌入内核中。
*initrd: initial RAM disk,初始内存磁盘*
将引导配置附加到initrd
----------------------
由于默认情况下引导配置文件是用initrd加载的,因此它将被添加到initrd(initramfs)
镜像文件的末尾,其中包含填充、大小、校验值和12字节幻数,如下所示::
[initrd][bootconfig][padding][size(le32)][checksum(le32)][#BOOTCONFIG\n]
大小和校验值为小端序存放的32位无符号值。
当引导配置被加到initrd镜像时,整个文件大小会对齐到4字节。空字符( ``\0`` )
会填补对齐空隙。因此 ``size`` 就是引导配置文件的长度+填充的字节。
Linux内核在内存中解码initrd镜像的最后部分以获取引导配置数据。由于这种“背负式”
的方法,只要引导加载器传递了正确的initrd文件大小,就无需更改或更新引导加载器
和内核镜像本身。如果引导加载器意外传递了更长的大小,内核将无法找到引导配置数
据。
Linux内核在tools/bootconfig下提供了 ``bootconfig`` 命令来完成此操作,管理员
可以用它从initrd镜像中删除或追加配置文件。你可以用以下命令来构建它::
# make -C tools/bootconfig
要向initrd镜像添加你的引导配置文件,请按如下命令操作(旧数据会自动移除)::
# tools/bootconfig/bootconfig -a your-config /boot/initrd.img-X.Y.Z
要从镜像中移除配置,可以使用-d选项::
# tools/bootconfig/bootconfig -d /boot/initrd.img-X.Y.Z
然后在内核命令行上添加 ``bootconfig`` 告诉内核去initrd文件末尾寻找内核配置。
将引导配置嵌入内核
------------------
如果你不能使用initrd,也可以通过Kconfig选项将引导配置文件嵌入内核中。在此情
况下,你需要用以下选项重新编译内核::
CONFIG_BOOT_CONFIG_EMBED=y
CONFIG_BOOT_CONFIG_EMBED_FILE="/引导配置/文件/的/路径"
``CONFIG_BOOT_CONFIG_EMBED_FILE`` 需要从源码树或对象树开始的引导配置文件的
绝对/相对路径。内核会将其嵌入作为默认引导配置。
与将引导配置附加到initrd一样,你也需要在内核命令行上添加 ``bootconfig`` 告诉
内核去启用内嵌的引导配置。
注意,即使你已经设置了此选项,仍可用附加到initrd的其他引导配置覆盖内嵌的引导
配置。
通过引导配置传递内核参数
========================
除了内核命令行,引导配置也可以用于传递内核参数。所有 ``kernel`` 关键字下的键
值对都将直接传递给内核命令行。此外, ``init`` 下的键值对将通过命令行传递给
init进程。参数按以下顺序与用户给定的内核命令行字符串相连,因此命令行参数可以
覆盖引导配置参数(这取决于子系统如何处理参数,但通常前面的参数将被后面的参数
覆盖)::
[bootconfig params][cmdline params] -- [bootconfig init params][cmdline init params]
如果引导配置文件给出的kernel/init参数是::
kernel {
root = 01234567-89ab-cdef-0123-456789abcd
}
init {
splash
}
这将被复制到内核命令行字符串中,如下所示::
root="01234567-89ab-cdef-0123-456789abcd" -- splash
如果用户给出的其他命令行是::
ro bootconfig -- quiet
则最后的内核命令行如下::
root="01234567-89ab-cdef-0123-456789abcd" ro bootconfig -- splash quiet
配置文件的限制
==============
当前最大的配置大小是32KB,关键字总数(不是键值条目)必须少于1024个节点。
注意:这不是条目数而是节点数,条目必须消耗超过2个节点(一个关键字和一个值)。
所以从理论上讲最多512个键值对。如果关键字平均包含3个单词,则可有256个键值对。
在大多数情况下,配置项的数量将少于100个条目,小于8KB,因此这应该足够了。如果
节点数超过1024,解析器将返回错误,即使文件大小小于32KB。(请注意,此最大尺寸
不包括填充的空字符。)
无论如何,因为 ``bootconfig`` 命令在附加启动配置到initrd映像时会验证它,用户
可以在引导之前注意到它。
引导配置API
===========
用户可以查询或遍历键值对,也可以查找(前缀)根关键字节点,并在查找该节点下的
键值。
如果您有一个关键字字符串,则可以直接使用 xbc_find_value() 查询该键的值。如果
你想知道引导配置里有哪些关键字,可以使用 xbc_for_each_key_value() 迭代键值对。
请注意,您需要使用 xbc_array_for_each_value() 访问数组的值,例如::
vnode = NULL;
xbc_find_value("key.word", &vnode);
if (vnode && xbc_node_is_array(vnode))
xbc_array_for_each_value(vnode, value) {
printk("%s ", value);
}
如果您想查找具有前缀字符串的键,可以使用 xbc_find_node() 通过前缀字符串查找
节点,然后用 xbc_node_for_each_key_value() 迭代前缀节点下的键。
但最典型的用法是获取前缀下的命名值或前缀下的命名数组,例如::
root = xbc_find_node("key.prefix");
value = xbc_node_find_value(root, "option", &vnode);
...
xbc_node_for_each_array_value(root, "array-option", value, anode) {
...
}
这将访问值“key.prefix.option”的值和“key.prefix.array-option”的数组。
锁是不需要的,因为在初始化之后配置只读。如果需要修改,必须复制所有数据和关键字。
函数与结构体
============
相关定义的kernel-doc参见:
- include/linux/bootconfig.h
- lib/bootconfig.c
@@ -0,0 +1,81 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: :doc:`../../../admin-guide/bug-bisect`
:译者:
吴想成 Wu XiangCheng <bobwxc@email.cn>
二分(bisect)缺陷
+++++++++++++++++++
(英文版)最后更新:2016年10月28日
引言
=====
始终尝试由来自kernel.org的源代码构建的最新内核。如果您没有信心这样做,请将
错误报告给您的发行版供应商,而不是内核开发人员。
找到缺陷(bug)并不总是那么容易,不过仍然得去找。如果你找不到它,不要放弃。
尽可能多的向相关维护人员报告您发现的信息。请参阅MAINTAINERS文件以了解您所
关注的子系统的维护人员。
在提交错误报告之前,请阅读“Documentation/admin-guide/reporting-issues.rst”。
设备未出现(Devices not appearing)
====================================
这通常是由udev/systemd引起的。在将其归咎于内核之前先检查一下。
查找导致缺陷的补丁
===================
使用 ``git`` 提供的工具可以很容易地找到缺陷,只要缺陷是可复现的。
操作步骤:
- 从git源代码构建内核
- 以此开始二分 [#f1]_::
$ git bisect start
- 标记损坏的变更集::
$ git bisect bad [commit]
- 标记正常工作的变更集::
$ git bisect good [commit]
- 重新构建内核并测试
- 使用以下任一与git bisect进行交互::
$ git bisect good
或::
$ git bisect bad
这取决于您测试的变更集上是否有缺陷
- 在一些交互之后,git bisect将给出可能导致缺陷的变更集。
- 例如,如果您知道当前版本有问题,而4.8版本是正常的,则可以执行以下操作::
$ git bisect start
$ git bisect bad # Current version is bad
$ git bisect good v4.8
.. [#f1] 您可以(可选地)在开始git bisect的时候提供good或bad参数
``git bisect start [BAD] [GOOD]``
如需进一步参考,请阅读:
- ``git-bisect`` 的手册页
- `Fighting regressions with git bisect(用git bisect解决回归)
<https://www.kernel.org/pub/software/scm/git/docs/git-bisect-lk2009.html>`_
- `Fully automated bisecting with "git bisect run"(使用git bisect run
来全自动二分) <https://lwn.net/Articles/317154>`_
- `Using Git bisect to figure out when brokenness was introduced
(使用Git二分来找出何时引入了错误) <http://webchick.net/node/99>`_
@@ -0,0 +1,331 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: :doc:`../../../admin-guide/bug-hunting`
:译者:
吴想成 Wu XiangCheng <bobwxc@email.cn>
追踪缺陷
=========
内核错误报告通常附带如下堆栈转储::
------------[ cut here ]------------
WARNING: CPU: 1 PID: 28102 at kernel/module.c:1108 module_put+0x57/0x70
Modules linked in: dvb_usb_gp8psk(-) dvb_usb dvb_core nvidia_drm(PO) nvidia_modeset(PO) snd_hda_codec_hdmi snd_hda_intel snd_hda_codec snd_hwdep snd_hda_core snd_pcm snd_timer snd soundcore nvidia(PO) [last unloaded: rc_core]
CPU: 1 PID: 28102 Comm: rmmod Tainted: P WC O 4.8.4-build.1 #1
Hardware name: MSI MS-7309/MS-7309, BIOS V1.12 02/23/2009
00000000 c12ba080 00000000 00000000 c103ed6a c1616014 00000001 00006dc6
c1615862 00000454 c109e8a7 c109e8a7 00000009 ffffffff 00000000 f13f6a10
f5f5a600 c103ee33 00000009 00000000 00000000 c109e8a7 f80ca4d0 c109f617
Call Trace:
[<c12ba080>] ? dump_stack+0x44/0x64
[<c103ed6a>] ? __warn+0xfa/0x120
[<c109e8a7>] ? module_put+0x57/0x70
[<c109e8a7>] ? module_put+0x57/0x70
[<c103ee33>] ? warn_slowpath_null+0x23/0x30
[<c109e8a7>] ? module_put+0x57/0x70
[<f80ca4d0>] ? gp8psk_fe_set_frontend+0x460/0x460 [dvb_usb_gp8psk]
[<c109f617>] ? symbol_put_addr+0x27/0x50
[<f80bc9ca>] ? dvb_usb_adapter_frontend_exit+0x3a/0x70 [dvb_usb]
[<f80bb3bf>] ? dvb_usb_exit+0x2f/0xd0 [dvb_usb]
[<c13d03bc>] ? usb_disable_endpoint+0x7c/0xb0
[<f80bb48a>] ? dvb_usb_device_exit+0x2a/0x50 [dvb_usb]
[<c13d2882>] ? usb_unbind_interface+0x62/0x250
[<c136b514>] ? __pm_runtime_idle+0x44/0x70
[<c13620d8>] ? __device_release_driver+0x78/0x120
[<c1362907>] ? driver_detach+0x87/0x90
[<c1361c48>] ? bus_remove_driver+0x38/0x90
[<c13d1c18>] ? usb_deregister+0x58/0xb0
[<c109fbb0>] ? SyS_delete_module+0x130/0x1f0
[<c1055654>] ? task_work_run+0x64/0x80
[<c1000fa5>] ? exit_to_usermode_loop+0x85/0x90
[<c10013f0>] ? do_fast_syscall_32+0x80/0x130
[<c1549f43>] ? sysenter_past_esp+0x40/0x6a
---[ end trace 6ebc60ef3981792f ]---
这样的堆栈跟踪提供了足够的信息来识别内核源代码中发生错误的那一行。根据问题的
严重性,它还可能包含 **“Oops”** 一词,比如::
BUG: unable to handle kernel NULL pointer dereference at (null)
IP: [<c06969d4>] iret_exc+0x7d0/0xa59
*pdpt = 000000002258a001 *pde = 0000000000000000
Oops: 0002 [#1] PREEMPT SMP
...
尽管有 **Oops** 或其他类型的堆栈跟踪,但通常需要找到出问题的行来识别和处理缺
陷。在本章中,我们将参考“Oops”来了解需要分析的各种堆栈跟踪。
如果内核是用 ``CONFIG_DEBUG_INFO`` 编译的,那么可以使用文件:
`scripts/decode_stacktrace.sh` 。
链接的模块
-----------
受到污染或正在加载/卸载的模块用“(…)”标记,污染标志在
`Documentation/admin-guide/tainted-kernels.rst` 文件中进行了描述,“正在被加
载”用“+”标注,“正在被卸载”用“-”标注。
Oops消息在哪?
---------------
通常,Oops文本由klogd从内核缓冲区读取,然后交给 ``syslogd`` ,后者将其写入
syslog文件,通常是 ``/var/log/messages`` (取决于 ``/etc/syslog.conf`` )。
在使用systemd的系统上,它也可以由 ``journald`` 守护进程存储,并通过运行
``journalctl`` 命令进行访问。
有时 ``klogd`` 会挂掉,这种情况下您可以运行 ``dmesg > file`` 从内核缓冲区
读取数据并保存它。或者您可以 ``cat /proc/kmsg > file`` ,但是您必须适时
中断以停止传输,因为 ``kmsg`` 是一个“永无止境的文件”。
如果机器严重崩溃,无法输入命令或磁盘不可用,那还有三个选项:
(1) 手动复制屏幕上的文本,并在机器重新启动后输入。很难受,但这是突然崩溃下
唯一的选择。或者你可以用数码相机拍下屏幕——虽然不那么好,但总比什么都没
有好。如果消息滚动超出控制台顶部,使用更高分辨率(例如 ``vga=791`` )
引导启动将允许您阅读更多文本。(警告:这需要 ``vesafb`` ,因此对“早期”
的Oppses没有帮助)
(2) 从串口终端启动(参见
:ref:`Documentation/admin-guide/serial-console.rst <serial_console>` ),
在另一台机器上运行调制解调器然后用你喜欢的通信程序捕获输出。
Minicom运行良好。
(3) 使用Kdump(参阅 Documentation/admin-guide/kdump/kdump.rst ),使用
Documentation/admin-guide/kdump/gdbmacros.txt 中的dmesg gdbmacro从旧内存
中提取内核环形缓冲区。
找到缺陷位置
-------------
如果你能指出缺陷在内核源代码中的位置,则报告缺陷的效果会非常好。这有两种方法。
通常来说使用 ``gdb`` 会比较容易,不过内核需要用调试信息来预编译。
gdb
^^^^
GNU 调试器(GNU debugger, ``gdb`` )是从 ``vmlinux`` 文件中找出OOPS的确切
文件和行号的最佳方法。
在使用 ``CONFIG_DEBUG_INFO`` 编译的内核上使用gdb效果最好。可通过运行以下命令
进行设置::
$ ./scripts/config -d COMPILE_TEST -e DEBUG_KERNEL -e DEBUG_INFO
在用 ``CONFIG_DEBUG_INFO`` 编译的内核上,你可以直接从OOPS复制EIP值::
EIP: 0060:[<c021e50e>] Not tainted VLI
并使用GDB来将其翻译成可读形式::
$ gdb vmlinux
(gdb) l *0xc021e50e
如果没有启用 ``CONFIG_DEBUG_INFO`` ,则使用OOPS的函数偏移::
EIP is at vt_ioctl+0xda8/0x1482
并在启用 ``CONFIG_DEBUG_INFO`` 的情况下重新编译内核::
$ ./scripts/config -d COMPILE_TEST -e DEBUG_KERNEL -e DEBUG_INFO
$ make vmlinux
$ gdb vmlinux
(gdb) l *vt_ioctl+0xda8
0x1888 is in vt_ioctl (drivers/tty/vt/vt_ioctl.c:293).
288 {
289 struct vc_data *vc = NULL;
290 int ret = 0;
291
292 console_lock();
293 if (VT_BUSY(vc_num))
294 ret = -EBUSY;
295 else if (vc_num)
296 vc = vc_deallocate(vc_num);
297 console_unlock();
或者若您想要更详细的显示::
(gdb) p vt_ioctl
$1 = {int (struct tty_struct *, unsigned int, unsigned long)} 0xae0 <vt_ioctl>
(gdb) l *0xae0+0xda8
您也可以使用对象文件作为替代::
$ make drivers/tty/
$ gdb drivers/tty/vt/vt_ioctl.o
(gdb) l *vt_ioctl+0xda8
如果你有调用跟踪,类似::
Call Trace:
[<ffffffff8802c8e9>] :jbd:log_wait_commit+0xa3/0xf5
[<ffffffff810482d9>] autoremove_wake_function+0x0/0x2e
[<ffffffff8802770b>] :jbd:journal_stop+0x1be/0x1ee
...
这表明问题可能在 :jbd: 模块中。您可以在gdb中加载该模块并列出相关代码::
$ gdb fs/jbd/jbd.ko
(gdb) l *log_wait_commit+0xa3
.. note::
您还可以对堆栈跟踪处的任何函数调用执行相同的操作,例如::
[<f80bc9ca>] ? dvb_usb_adapter_frontend_exit+0x3a/0x70 [dvb_usb]
上述调用发生的位置可以通过以下方式看到::
$ gdb drivers/media/usb/dvb-usb/dvb-usb.o
(gdb) l *dvb_usb_adapter_frontend_exit+0x3a
objdump
^^^^^^^^
要调试内核,请使用objdump并从崩溃输出中查找十六进制偏移,以找到有效的代码/汇
编行。如果没有调试符号,您将看到所示例程的汇编程序代码,但是如果内核有调试
符号,C代码也将可见(调试符号可以在内核配置菜单的hacking项中启用)。例如::
$ objdump -r -S -l --disassemble net/ipv4/tcp.o
.. note::
您需要处于内核树的顶层以便此获得您的C文件。
如果您无法访问源代码,仍然可以使用以下方法调试一些崩溃转储(如Dave Miller的
示例崩溃转储输出所示)::
EIP is at +0x14/0x4c0
...
Code: 44 24 04 e8 6f 05 00 00 e9 e8 fe ff ff 8d 76 00 8d bc 27 00 00
00 00 55 57 56 53 81 ec bc 00 00 00 8b ac 24 d0 00 00 00 8b 5d 08
<8b> 83 3c 01 00 00 89 44 24 14 8b 45 28 85 c0 89 44 24 18 0f 85
Put the bytes into a "foo.s" file like this:
.text
.globl foo
foo:
.byte .... /* bytes from Code: part of OOPS dump */
Compile it with "gcc -c -o foo.o foo.s" then look at the output of
"objdump --disassemble foo.o".
Output:
ip_queue_xmit:
push %ebp
push %edi
push %esi
push %ebx
sub $0xbc, %esp
mov 0xd0(%esp), %ebp ! %ebp = arg0 (skb)
mov 0x8(%ebp), %ebx ! %ebx = skb->sk
mov 0x13c(%ebx), %eax ! %eax = inet_sk(sk)->opt
`scripts/decodecode` 文件可以用来自动完成大部分工作,这取决于正在调试的CPU
体系结构。
报告缺陷
---------
一旦你通过定位缺陷找到了其发生的地方,你可以尝试自己修复它或者向上游报告它。
为了向上游报告,您应该找出用于开发受影响代码的邮件列表。这可以使用 ``get_maintainer.pl`` 。
例如,您在gspca的sonixj.c文件中发现一个缺陷,则可以通过以下方法找到它的维护者::
$ ./scripts/get_maintainer.pl -f drivers/media/usb/gspca/sonixj.c
Hans Verkuil <hverkuil@kernel.org> (odd fixer:GSPCA USB WEBCAM DRIVER,commit_signer:1/1=100%)
Mauro Carvalho Chehab <mchehab@kernel.org> (maintainer:MEDIA INPUT INFRASTRUCTURE (V4L/DVB),commit_signer:1/1=100%)
Tejun Heo <tj@kernel.org> (commit_signer:1/1=100%)
Bhaktipriya Shridhar <bhaktipriya96@gmail.com> (commit_signer:1/1=100%,authored:1/1=100%,added_lines:4/4=100%,removed_lines:9/9=100%)
linux-media@vger.kernel.org (open list:GSPCA USB WEBCAM DRIVER)
linux-kernel@vger.kernel.org (open list)
请注意它将指出:
- 最后接触源代码的开发人员(如果这是在git树中完成的)。在上面的例子中是Tejun
和Bhaktipriya(在这个特定的案例中,没有人真正参与这个文件的开发);
- 驱动维护人员(Hans Verkuil);
- 子系统维护人员(Mauro Carvalho Chehab);
- 驱动程序和/或子系统邮件列表(linux-media@vger.kernel.org);
- Linux内核邮件列表(linux-kernel@vger.kernel.org)。
通常,修复缺陷的最快方法是将它报告给用于开发相关代码的邮件列表(linux-media
ML),抄送驱动程序维护者(Hans)。
如果你完全不知道该把报告寄给谁,且 ``get_maintainer.pl`` 也没有提供任何有用
的信息,请发送到linux-kernel@vger.kernel.org。
感谢您的帮助,这使Linux尽可能稳定:-)
修复缺陷
---------
如果你懂得编程,你不仅可以通过报告错误来帮助我们,还可以提供一个解决方案。
毕竟,开源就是分享你的工作,你不想因为你的天才而被认可吗?
如果你决定这样做,请在制定解决方案后将其提交到上游。
请务必阅读
:ref:`Documentation/process/submitting-patches.rst <submittingpatches>` ,
以帮助您的代码被接受。
---------------------------------------------------------------------------
用 ``klogd`` 进行Oops跟踪的注意事项
------------------------------------
为了帮助Linus和其他内核开发人员, ``klogd`` 对保护故障的处理提供了大量支持。
为了完整支持地址解析,至少应该使用 ``sysklogd`` 包的1.3-pl3版本。
当发生保护故障时, ``klogd`` 守护进程会自动将内核日志消息中的重要地址转换为
它们的等效符号。然后通过 ``klogd`` 使用的任何报告机制来转发这个已翻译的内核
消息。保护错误消息可以直接从消息文件中剪切出来并转发给内核开发人员。
``klogd`` 执行两种类型的地址解析,静态翻译和动态翻译。静态翻译使用System.map
文件。为了进行静态转换, ``klogd`` 守护进程必须能够在守护进程初始化时找到系
统映射文件。有关 ``klogd`` 如何搜索映射文件的信息,请参见klogd手册页。
当使用内核可加载模块时,动态地址转换非常重要。由于内核模块的内存是从内核的
动态内存池中分配的,因此无论是模块的开头还是模块中的函数和符号都没有固定的
位置。
内核支持系统调用,允许程序确定加载哪些模块及其在内存中的位置。klogd守护进程
使用这些系统调用构建了一个符号表,可用于调试可加载内核模块中发生的保护错误。
klogd至少会提供产生保护故障的模块的名称。如果可加载模块的开发人员选择从模块
导出符号信息,则可能会有其他可用的符号信息。
由于内核模块环境可以是动态的,因此当模块环境发生变化时,必须有一种通知
``klogd`` 守护进程的机制。有一些可用的命令行选项允许klogd向当前正在执行的守
护进程发出信号示意应该刷新符号信息。有关更多信息,请参阅 ``klogd`` 手册页。
sysklogd发行版附带了一个补丁,它修改了 ``modules-2.0.0`` 包,以便在加载或
卸载模块时自动向klogd发送信号。应用此补丁基本上可无缝支持调试内核可加载模块
发生的保护故障。
以下是 ``klogd`` 处理的可加载模块中的保护故障示例::
Aug 29 09:51:01 blizard kernel: Unable to handle kernel paging request at virtual address f15e97cc
Aug 29 09:51:01 blizard kernel: current->tss.cr3 = 0062d000, %cr3 = 0062d000
Aug 29 09:51:01 blizard kernel: *pde = 00000000
Aug 29 09:51:01 blizard kernel: Oops: 0002
Aug 29 09:51:01 blizard kernel: CPU: 0
Aug 29 09:51:01 blizard kernel: EIP: 0010:[oops:_oops+16/3868]
Aug 29 09:51:01 blizard kernel: EFLAGS: 00010212
Aug 29 09:51:01 blizard kernel: eax: 315e97cc ebx: 003a6f80 ecx: 001be77b edx: 00237c0c
Aug 29 09:51:01 blizard kernel: esi: 00000000 edi: bffffdb3 ebp: 00589f90 esp: 00589f8c
Aug 29 09:51:01 blizard kernel: ds: 0018 es: 0018 fs: 002b gs: 002b ss: 0018
Aug 29 09:51:01 blizard kernel: Process oops_test (pid: 3374, process nr: 21, stackpage=00589000)
Aug 29 09:51:01 blizard kernel: Stack: 315e97cc 00589f98 0100b0b4 bffffed4 0012e38e 00240c64 003a6f80 00000001
Aug 29 09:51:01 blizard kernel: 00000000 00237810 bfffff00 0010a7fa 00000003 00000001 00000000 bfffff00
Aug 29 09:51:01 blizard kernel: bffffdb3 bffffed4 ffffffda 0000002b 0007002b 0000002b 0000002b 00000036
Aug 29 09:51:01 blizard kernel: Call Trace: [oops:_oops_ioctl+48/80] [_sys_ioctl+254/272] [_system_call+82/128]
Aug 29 09:51:01 blizard kernel: Code: c7 00 05 00 00 00 eb 08 90 90 90 90 90 90 90 90 89 ec 5d c3
@@ -0,0 +1,9 @@
清除 WARN_ONCE
--------------
WARN_ONCE / WARN_ON_ONCE / printk_once 仅仅打印一次消息.
echo 1 > /sys/kernel/debug/clear_warn_once
可以清除这种状态并且再次允许打印一次告警信息,这对于运行测试集后重现问题
很有用。
@@ -0,0 +1,105 @@
========
CPU 负载
========
Linux通过``/proc/stat``和``/proc/uptime``导出各种信息,用户空间工具
如top(1)使用这些信息计算系统花费在某个特定状态的平均时间。
例如:
$ iostat
Linux 2.6.18.3-exp (linmac) 02/20/2007
avg-cpu: %user %nice %system %iowait %steal %idle
10.01 0.00 2.92 5.44 0.00 81.63
...
这里系统认为在默认采样周期內有10.01%的时间工作在用户空间,2.92%的时
间用在系统空间,总体上有81.63%的时间是空闲的。
大多数情况下``/proc/stat``的信息几乎真实反映了系统信息,然而,由于内
核采集这些数据的方式/时间的特点,有时这些信息根本不可靠。
那么这些信息是如何被搜集的呢?每当时间中断触发时,内核查看此刻运行的
进程类型,并增加与此类型/状态进程对应的计数器的值。这种方法的问题是
在两次时间中断之间系统(进程)能够在多种状态之间切换多次,而计数器只
增加最后一种状态下的计数。
举例
---
假设系统有一个进程以如下方式周期性地占用cpu::
两个时钟中断之间的时间线
|-----------------------|
^ ^
|_ 开始运行 |
|_ 开始睡眠
(很快会被唤醒)
在上面的情况下,根据``/proc/stat``的信息(由于当系统处于空闲状态时,
时间中断经常会发生)系统的负载将会是0
大家能够想象内核的这种行为会发生在许多情况下,这将导致``/proc/stat``
中存在相当古怪的信息::
/* gcc -o hog smallhog.c */
#include <time.h>
#include <limits.h>
#include <signal.h>
#include <sys/time.h>
#define HIST 10
static volatile sig_atomic_t stop;
static void sighandler (int signr)
{
(void) signr;
stop = 1;
}
static unsigned long hog (unsigned long niters)
{
stop = 0;
while (!stop && --niters);
return niters;
}
int main (void)
{
int i;
struct itimerval it = { .it_interval = { .tv_sec = 0, .tv_usec = 1 },
.it_value = { .tv_sec = 0, .tv_usec = 1 } };
sigset_t set;
unsigned long v[HIST];
double tmp = 0.0;
unsigned long n;
signal (SIGALRM, &sighandler);
setitimer (ITIMER_REAL, &it, NULL);
hog (ULONG_MAX);
for (i = 0; i < HIST; ++i) v[i] = ULONG_MAX - hog (ULONG_MAX);
for (i = 0; i < HIST; ++i) tmp += v[i];
tmp /= HIST;
n = tmp - (tmp / 3.0);
sigemptyset (&set);
sigaddset (&set, SIGALRM);
for (;;) {
hog (n);
sigwait (&set, &i);
}
return 0;
}
参考
---
- https://lore.kernel.org/r/loom.20070212T063225-663@post.gmane.org
- Documentation/filesystems/proc.rst (1.8)
谢谢
---
Con Kolivas, Pavel Machek
@@ -0,0 +1,96 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/cputopology.rst
:翻译:
唐艺舟 Tang Yizhou <tangyeechou@gmail.com>
==========================
如何通过sysfs将CPU拓扑导出
==========================
CPU拓扑信息通过sysfs导出。显示的项(属性)和某些架构的/proc/cpuinfo输出相似。它们位于
/sys/devices/system/cpu/cpuX/topology/。请阅读ABI文件:
Documentation/ABI/stable/sysfs-devices-system-cpu。
drivers/base/topology.c是体系结构中性的,它导出了这些属性。然而,die、cluster、book、
draw这些层次结构相关的文件仅在体系结构提供了下文描述的宏的条件下被创建。
对于支持这个特性的体系结构,它必须在include/asm-XXX/topology.h中定义这些宏中的一部分::
#define topology_physical_package_id(cpu)
#define topology_die_id(cpu)
#define topology_cluster_id(cpu)
#define topology_core_id(cpu)
#define topology_book_id(cpu)
#define topology_drawer_id(cpu)
#define topology_sibling_cpumask(cpu)
#define topology_core_cpumask(cpu)
#define topology_cluster_cpumask(cpu)
#define topology_die_cpumask(cpu)
#define topology_book_cpumask(cpu)
#define topology_drawer_cpumask(cpu)
``**_id macros`` 的类型是int。
``**_cpumask macros`` 的类型是 ``(const) struct cpumask *`` 。后者和恰当的
``**_siblings`` sysfs属性对应(除了topology_sibling_cpumask(),它和thread_siblings
对应)。
为了在所有体系结构上保持一致,include/linux/topology.h提供了上述所有宏的默认定义,以防
它们未在include/asm-XXX/topology.h中定义:
1) topology_physical_package_id: -1
2) topology_die_id: -1
3) topology_cluster_id: -1
4) topology_core_id: 0
5) topology_book_id: -1
6) topology_drawer_id: -1
7) topology_sibling_cpumask: 仅入参CPU
8) topology_core_cpumask: 仅入参CPU
9) topology_cluster_cpumask: 仅入参CPU
10) topology_die_cpumask: 仅入参CPU
11) topology_book_cpumask: 仅入参CPU
12) topology_drawer_cpumask: 仅入参CPU
此外,CPU拓扑信息由/sys/devices/system/cpu提供,包含下述文件。输出对应的内部数据源放在
方括号("[]")中。
=========== ==================================================================
kernel_max: 内核配置允许的最大CPU下标值。[NR_CPUS-1]
offline: 由于热插拔移除或者超过内核允许的CPU上限(上文描述的kernel_max)
导致未上线的CPU。[~cpu_online_mask + cpus >= NR_CPUS]
online: 在线的CPU,可供调度使用。[cpu_online_mask]
possible: 已被分配资源的CPU,如果它们CPU实际存在,可以上线。
[cpu_possible_mask]
present: 被系统识别实际存在的CPU。[cpu_present_mask]
=========== ==================================================================
上述输出的格式和cpulist_parse()兼容[参见 <linux/cpumask.h>]。下面给些例子。
在本例中,系统中有64个CPU,但是CPU 32-63超过了kernel_max值,因为NR_CPUS配置项是32,
取值范围被限制为0..31。此外注意CPU2和4-31未上线,但是可以上线,因为它们同时存在于
present和possible::
kernel_max: 31
offline: 2,4-31,32-63
online: 0-1,3
possible: 0-31
present: 0-31
在本例中,NR_CPUS配置项是128,但内核启动时设置possible_cpus=144。系统中有4个CPU,
CPU2被手动设置下线(也是唯一一个可以上线的CPU)::
kernel_max: 127
offline: 2,4-127,128-143
online: 0-1,3
possible: 0-127
present: 0-3
阅读Documentation/core-api/cpu_hotplug.rst可了解开机参数possible_cpus=NUM,同时还
可以了解各种cpumask的信息。
@@ -0,0 +1,132 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: :doc:`../../../admin-guide/index`
:Translator: Alex Shi <alex.shi@linux.alibaba.com>
Linux 内核用户和管理员指南
==========================
下面是一组随时间添加到内核中的面向用户的文档的集合。到目前为止,还没有一个
整体的顺序或组织 - 这些材料不是一个单一的,连贯的文件!幸运的话,情况会随着
时间的推移而迅速改善。
这个初始部分包含总体信息,包括描述内核的README, 关于内核参数的文档等。
.. toctree::
:maxdepth: 1
README
Todolist:
* kernel-parameters
* devices
* sysctl/index
本节介绍CPU漏洞及其缓解措施。
Todolist:
* hw-vuln/index
下面的一组文档,针对的是试图跟踪问题和bug的用户。
.. toctree::
:maxdepth: 1
reporting-issues
reporting-regressions
bug-hunting
bug-bisect
tainted-kernels
init
Todolist:
* ramoops
* dynamic-debug-howto
* kdump/index
* perf/index
这是应用程序开发人员感兴趣的章节的开始。可以在这里找到涵盖内核ABI各个
方面的文档。
Todolist:
* sysfs-rules
本手册的其余部分包括各种指南,介绍如何根据您的喜好配置内核的特定行为。
.. toctree::
:maxdepth: 1
bootconfig
clearing-warn-once
cpu-load
cputopology
lockup-watchdogs
numastat
unicode
sysrq
mm/index
Todolist:
* acpi/index
* aoe/index
* auxdisplay/index
* bcache
* binderfs
* binfmt-misc
* blockdev/index
* braille-console
* btmrvl
* cgroup-v1/index
* cgroup-v2
* cifs/index
* dell_rbu
* device-mapper/index
* edid
* efi-stub
* ext4
* nfs/index
* gpio/index
* highuid
* hw_random
* initrd
* iostats
* java
* jfs
* kernel-per-CPU-kthreads
* laptops/index
* lcd-panel-cgram
* ldm
* LSM/index
* md
* media/index
* module-signing
* mono
* namespaces/index
* parport
* perf-security
* pm/index
* pnp
* rapidio
* ras
* rtc
* serial-console
* svga
* thunderbolt
* ufs
* vga-softcursor
* video-output
* xfs
.. only:: subproject and html
Indices
=======
* :ref:`genindex`
@@ -0,0 +1,54 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: :doc:`../../../admin-guide/init`
:译者:
吴想成 Wu XiangCheng <bobwxc@email.cn>
解释“No working init found.”启动挂起消息
=========================================
:作者:
Andreas Mohr <andi at lisas period de>
Cristian Souza <cristianmsbr at gmail period com>
本文档提供了加载初始化二进制(init binary)失败的一些高层级原因(大致按执行
顺序列出)。
1) **无法挂载根文件系统Unable to mount root FS** :请设置“debug”内核参数(在
引导加载程序bootloader配置文件或CONFIG_CMDLINE)以获取更详细的内核消息。
2) **初始化二进制不存在于根文件系统上init binary doesn't exist on rootfs** :
确保您的根文件系统类型正确(并且 ``root=`` 内核参数指向正确的分区);拥有
所需的驱动程序,例如SCSI或USB等存储硬件;文件系统(ext3、jffs2等)是内建的
(或者作为模块由initrd预加载)。
3) **控制台设备损坏Broken console device** : ``console= setup`` 中可能存在
冲突 --> 初始控制台不可用(initial console unavailable)。例如,由于串行
IRQ问题(如缺少基于中断的配置)导致的某些串行控制台不可靠。尝试使用不同的
``console= device`` 或像 ``netconsole=`` 。
4) **二进制存在但依赖项不可用Binary exists but dependencies not available** :
例如初始化二进制的必需库依赖项,像 ``/lib/ld-linux.so.2`` 丢失或损坏。使用
``readelf -d <INIT>|grep NEEDED`` 找出需要哪些库。
5) **无法加载二进制Binary cannot be loaded** :请确保二进制的体系结构与您的
硬件匹配。例如i386不匹配x86_64,或者尝试在ARM硬件上加载x86。如果您尝试在
此处加载非二进制文件(shell脚本?),您应该确保脚本在其工作头(shebang
header)行 ``#!/...`` 中指定能正常工作的解释器(包括其库依赖项)。在处理
脚本之前,最好先测试一个简单的非脚本二进制文件,比如 ``/bin/sh`` ,并确认
它能成功执行。要了解更多信息,请将代码添加到 ``init/main.c`` 以显示
kernel_execve()的返回值。
当您发现新的失败原因时,请扩展本解释(毕竟加载初始化二进制是一个 **关键** 且
艰难的过渡步骤,需要尽可能无痛地进行),然后向LKML提交一个补丁。
待办事项:
- 通过一个可以存储 ``kernel_execve()`` 结果值的结构体数组实现各种
``run_init_process()`` 调用,并在失败时通过迭代 **所有** 结果来记录一切
(非常重要的可用性修复)。
- 试着使实现本身在一般情况下更有帮助,例如在受影响的地方提供额外的错误消息。
@@ -0,0 +1,66 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/lockup-watchdogs.rst
:Translator: Hailong Liu <liu.hailong6@zte.com.cn>
.. _cn_lockup-watchdogs:
=================================================
Softlockup与hardlockup检测机制(又名:nmi_watchdog)
=================================================
Linux中内核实现了一种用以检测系统发生softlockup和hardlockup的看门狗机制。
Softlockup是一种会引发系统在内核态中一直循环超过20秒(详见下面“实现”小节)导致
其他任务没有机会得到运行的BUG。一旦检测到'softlockup'发生,默认情况下系统会打
印当前堆栈跟踪信息并进入锁定状态。也可配置使其在检测到'softlockup'后进入panic
状态;通过sysctl命令设置“kernel.softlockup_panic”、使用内核启动参数
“softlockup_panic”(详见Documentation/admin-guide/kernel-parameters.rst)以及使
能内核编译选项“BOOTPARAM_SOFTLOCKUP_PANIC”都可实现这种配置。
而'hardlockup'是一种会引发系统在内核态一直循环超过10秒钟(详见"实现"小节)导致其
他中断没有机会运行的缺陷。与'softlockup'情况类似,除了使用sysctl命令设置
'hardlockup_panic'、使能内核选项“BOOTPARAM_HARDLOCKUP_PANIC”以及使用内核参数
"nmi_watchdog"(详见:”Documentation/admin-guide/kernel-parameters.rst“)外,一旦检
测到'hardlockup'默认情况下系统打印当前堆栈跟踪信息,然后进入锁定状态。
这个panic选项也可以与panic_timeout结合使用(这个panic_timeout是通过稍具迷惑性的
sysctl命令"kernel.panic"来设置),使系统在panic指定时间后自动重启。
实现
====
Softlockup和hardlockup分别建立在hrtimer(高精度定时器)和perf两个子系统上而实现。
这也就意味着理论上任何架构只要实现了这两个子系统就支持这两种检测机制。
Hrtimer用于周期性产生中断并唤醒watchdog线程;NMI perf事件则以”watchdog_thresh“
(编译时默认初始化为10秒,也可通过”watchdog_thresh“这个sysctl接口来进行配置修改)
为间隔周期产生以检测 hardlockups。如果一个CPU在这个时间段内没有检测到hrtimer中
断发生,'hardlockup 检测器'(即NMI perf事件处理函数)将会视系统配置而选择产生内核
警告或者直接panic。
而watchdog线程本质上是一个高优先级内核线程,每调度一次就对时间戳进行一次更新。
如果时间戳在2*watchdog_thresh(这个是softlockup的触发门限)这段时间都未更新,那么
"softlocup 检测器"(内部hrtimer定时器回调函数)会将相关的调试信息打印到系统日志中,
然后如果系统配置了进入panic流程则进入panic,否则内核继续执行。
Hrtimer定时器的周期是2*watchdog_thresh/5,也就是说在hardlockup被触发前hrtimer有
2~3次机会产生时钟中断。
如上所述,内核相当于为系统管理员提供了一个可调节hrtimer定时器和perf事件周期长度
的调节旋钮。如何通过这个旋钮为特定使用场景配置一个合理的周期值要对lockups检测的
响应速度和lockups检测开销这二者之间进行权衡。
默认情况下所有在线cpu上都会运行一个watchdog线程。不过在内核配置了”NO_HZ_FULL“的
情况下watchdog线程默认只会运行在管家(housekeeping)cpu上,而”nohz_full“启动参数指
定的cpu上则不会有watchdog线程运行。试想,如果我们允许watchdog线程在”nohz_full“指
定的cpu上运行,这些cpu上必须得运行时钟定时器来激发watchdog线程调度;这样一来就会
使”nohz_full“保护用户程序免受内核干扰的功能失效。当然,副作用就是”nohz_full“指定
的cpu即使在内核产生了lockup问题我们也无法检测到。不过,至少我们可以允许watchdog
线程在管家(non-tickless)核上继续运行以便我们能继续正常的监测这些cpus上的lockups
事件。
不论哪种情况都可以通过sysctl命令kernel.watchdog_cpumask来对没有运行watchdog线程
的cpu集合进行调节。对于nohz_full而言,如果nohz_full cpu上有异常挂住的情况,通过
这种方式打开这些cpu上的watchdog进行调试可能会有所作用。
@@ -0,0 +1,29 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/mm/damon/index.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
============
监测数据访问
============
:doc:`DAMON </mm/damon/index>` 允许轻量级的数据访问监测。使用DAMON,
用户可以分析他们系统的内存访问模式,并优化它们。
.. toctree::
:maxdepth: 2
start
usage
reclaim
lru_sort
@@ -0,0 +1,263 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/mm/damon/lru_sort.rst
:翻译:
臧雷刚 Leigang Zang <zangleigang@hisilicon.com>
:校译:
==================
基于DAMON的LRU排序
==================
基于DAMON的LRU排序是一个静态的内核模块,旨在用于以主动的、轻量级的数据访问模型
为基础的页面优先级处理的LRU链表上,以使得LRU上的数据访问模型更为可信。
哪里需要主动的LRU排序
=====================
在一个大型系统中,以页为粒度的访问检测会有比较显著的开销,LRU通常不会主动去排序,
而是对部分特殊事件进行部分的、响应式的排序,例如:特殊的用户请求,系统调用或者
内存压力。这导致,在有些场景下,LRU不能够完美的作为一个可信的数据访问模型,比如
在内存压力下对目标内存进行回收。
因为DAMON能够尽可能准确的识别数据访问模型,同时只引起用户指定范围的开销,主动的
执行DAMON_LRU_SORT让LRU变得更为可信是有益的,而且这只需要较少和可控的开销。
这是如何工作的
==============
DAMON_LRU_SORT使用DAMON寻找热页(范围内的页面访问频率高于用户指定的阈值)和冷页
(范围内的页面在超过用户指定的时间无访问),并提高热页和降低冷页在LRU中的优先级。
为了避免在排序过程占用更多的CPU计算资源,可以设置一个CPU占用时间的约束值。在约
束下,分别提升或者降低更多的热页和冷页。系统管理员也可以配置三个内存水位以控制
在何种条件下自动激活或者停止这种机制。
冷热阈值和CPU约束的默认值是比较保守的。这意味着,在默认参数下,模块可以广泛且无
负作用的使用在常见环境中,同时在只消耗一小部分CPU时间的情况下,给有内存压力的系
统提供一定水平的冷热识别。
接口:模块参数
==============
使用此特性,你首先需要确认你的系统中运行的内核在编译时启用了
``CONFIG_DAMON_LRU_SORT=y``.
为了让系统管理员打开或者关闭并且调节指定的系统,DAMON_LRU_SORT设计了模块参数。
这意味着,你可以添加 ``damon_lru_sort.<parameter>=<value>`` 到内核的启动命令行
参数,或者在 ``/sys/modules/damon_lru_sort/parameters/<parameter>`` 写入正确的
值。
下边是每个参数的描述
enabled
-------
打开或者关闭DAMON_LRU_SORT.
你可以通过设置这个参数为 ``Y`` 来打开DAMON_LRU_SORT。设置为 ``N`` 关闭
DAMON_LRU_SORT。注意,在基于水位的激活的情况下,DAMON_LRU_SORT有可能不会真正去
监测或者做LRU排序。对这种情况,参考下方关于水位的描述。
commit_inputs
-------------
让DAMON_LRU_SORT再次读取输入参数,除了 ``enabled`` 。
在DAMON_LRU_SORT运行时,新的输入参数默认不会被应用。一旦这个参数被设置为 ``Y``
,DAMON_LRU_SORT会再次读取除了 ``enabled`` 之外的参数。读取完成后,这个参数会被
设置为 ``N`` 。如果在读取时发现有无效参数,DAMON_LRU_SORT会被关闭。
hot_thres_access_freq
---------------------
热点内存区域的访问频率阈值,千分比。
如果一个内存区域的访问频率大于等于这个值,DAMON_LRU_SORT把这个区域看作热区,并
在LRU上把这个区域标记为已访问,因些在内存压力下这部分内存不会被回收。默认为50%。
cold_min_age
------------
用于识别冷内存区域的时间阈值,单位是微秒。
如果一个内存区域在这个时间内未被访问过,DAMON_LRU_SORT把这个区域看作冷区,并在
LRU上把这个区域标记为未访问,因此在内存压力下这些内存会首先被回收。默认值为120
秒。
quota_ms
--------
尝试LRU链表排序的时间限制,单位是毫秒。
DAMON_LRU_SORT在一个时间窗口内(quota_reset_interval_ms)内最多尝试这么长时间来
对LRU进行排序。这个可以用来作为CPU计算资源的约束。如果值为0,则表示无限制。
默认10毫秒。
quota_reset_interval_ms
-----------------------
配额计时重置周期,毫秒。
配额计时重置周期。即,在quota_reset_interval_ms毫秒内,DAMON_LRU_SORT对LRU进行
排序不会超过quota_ms或者quota_sz。
默认1秒。
wmarks_interval
---------------
水位的检查周期,单位是微秒。
当DAMON_LRU_SORT使能但是由于水位而不活跃时检查水位前最小的等待时间。默认值5秒。
wmarks_high
-----------
空闲内存高水位,千分比。
如果空闲内存水位高于这个值,DAMON_LRU_SORT停止工作,不做任何事,除了周期性的检
查水位。默认200(20%)。
wmarks_mid
----------
空闲内存中间水位,千分比。
如果空闲内存水位在这个值与低水位之间,DAMON_LRU_SORT开始工作,开始检测并对LRU链
表进行排序。默认150(15%)。
wmarks_low
----------
空闲内存低水位,千分比。
如果空闲内存小于这个值,DAMON_LRU_SORT不再工作,不做任何事,除了周期性的检查水
线。默认50(5%)。
sample_interval
---------------
监测的采样周期,微秒。
DAMON对冷内存监测的采样周期。更多细节请参考DAMON文档 (:doc:`usage`) 。默认5
毫秒。
aggr_interval
-------------
监测的收集周期,微秒。
DAMON对冷内存进行收集的时间周期。更多细节请参考DAMON文档 (:doc:`usage`) 。默认
100毫秒。
min_nr_regions
--------------
最小监测区域数量。
对冷内存区域监测的最小数量。这个值可以作为监测质量的下限。不过,这个值设置的过
大会增加开销。更多细节请参考DAMON文档 (:doc:`usage`) 。默认值为10。
max_nr_regions
--------------
最大监测区域数量。
对冷内存区域监测的最大数量。这个值可以作为监测质量的上限。然而,这个值设置的过
低会导致监测结果变差。更多细节请参考DAMON文档 (:doc:`usage`) 。默认值为1000。
monitor_region_start
--------------------
目标内存区域的起始物理地址。
DAMON_LRU_SORT要处理的目标内存区域的起始物理地址。默认,使用系统最大内存。
monitor_region_end
------------------
目标内存区域的结束物理地址。
DAMON_LRU_SORT要处理的目标内存区域的结束物理地址。默认,使用系统最大内存。
kdamond_pid
-----------
DAMON线程的PID。
如果DAMON_LRU_SORT是使能的,这个表示任务线程的PID。其它情况为-1。
nr_lru_sort_tried_hot_regions
-----------------------------
被尝试进行LRU排序的热内存区域的数量。
bytes_lru_sort_tried_hot_regions
--------------------------------
被尝试进行LRU排序的热内存区域的大小(字节)。
nr_lru_sorted_hot_regions
-------------------------
成功进行LRU排序的热内存区域的数量。
bytes_lru_sorted_hot_regions
----------------------------
成功进行LRU排序的热内存区域的大小(字节)。
nr_hot_quota_exceeds
--------------------
热区域时间约束超过限制的次数。
nr_lru_sort_tried_cold_regions
------------------------------
被尝试进行LRU排序的冷内存区域的数量。
bytes_lru_sort_tried_cold_regions
---------------------------------
被尝试进行LRU排序的冷内存区域的大小(字节)。
nr_lru_sorted_cold_regions
--------------------------
成功进行LRU排序的冷内存区域的数量。
bytes_lru_sorted_cold_regions
-----------------------------
成功进行LRU排序的冷内存区域的大小(字节)。
nr_cold_quota_exceeds
---------------------
冷区域时间约束超过限制的次数。
Example
=======
如下是一个运行时的命令示例,使DAMON_LRU_SORT查找访问频率超过50%的区域并对其进行
LRU的优先级的提升,同时降低那些超过120秒无人访问的内存区域的优先级。优先级的处
理被限制在最多1%的CPU以避免DAMON_LRU_SORT消费过多CPU时间。在系统空闲内存超过50%
时DAMON_LRU_SORT停止工作,并在低于40%时重新开始工作。如果DAMON_RECLAIM没有取得
进展且空闲内存低于20%,再次让DAMON_LRU_SORT停止工作,以此回退到以LRU链表为基础
以页面为单位的内存回收上。 ::
# cd /sys/modules/damon_lru_sort/parameters
# echo 500 > hot_thres_access_freq
# echo 120000000 > cold_min_age
# echo 10 > quota_ms
# echo 1000 > quota_reset_interval_ms
# echo 500 > wmarks_high
# echo 400 > wmarks_mid
# echo 200 > wmarks_low
# echo Y > enabled
@@ -0,0 +1,228 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/mm/damon/reclaim.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
===============
基于DAMON的回收
===============
基于DAMON的回收(DAMON_RECLAIM)是一个静态的内核模块,旨在用于轻度内存压力下的主动和轻
量级的回收。它的目的不是取代基于LRU列表的页面回收,而是有选择地用于不同程度的内存压力和要
求。
哪些地方需要主动回收?
======================
在一般的内存超量使用(over-committed systems,虚拟化相关术语)的系统上,主动回收冷页
有助于节省内存和减少延迟高峰,这些延迟是由直接回收进程或kswapd的CPU消耗引起的,同时只产
生最小的性能下降 [1]_ [2]_ 。
基于空闲页报告 [3]_ 的内存过度承诺的虚拟化系统就是很好的例子。在这样的系统中,客户机
向主机报告他们的空闲内存,而主机则将报告的内存重新分配给其他客户。因此,系统的内存得到了充
分的利用。然而,客户可能不那么节省内存,主要是因为一些内核子系统和用户空间应用程序被设计为
使用尽可能多的内存。然后,客户机可能只向主机报告少量的内存是空闲的,导致系统的内存利用率下降。
在客户中运行主动回收可以缓解这个问题。
它是如何工作的?
================
DAMON_RECLAIM找到在特定时间内没有被访问的内存区域并分页。为了避免它在分页操作中消耗过多
的CPU,可以配置一个速度限制。在这个速度限制下,它首先分页出那些没有被访问过的内存区域。系
统管理员还可以配置在什么情况下这个方案应该自动激活和停用三个内存压力水位。
接口: 模块参数
==============
要使用这个功能,你首先要确保你的系统运行在一个以 ``CONFIG_DAMON_RECLAIM=y`` 构建的内
核上。
为了让系统管理员启用或禁用它,并为给定的系统进行调整,DAMON_RECLAIM利用了模块参数。也就
是说,你可以把 ``damon_reclaim.<parameter>=<value>`` 放在内核启动命令行上,或者把
适当的值写入 ``/sys/module/damon_reclaim/parameters/<parameter>`` 文件。
下面是每个参数的描述。
enabled
-------
启用或禁用DAMON_RECLAIM。
你可以通过把这个参数的值设置为 ``Y`` 来启用DAMON_RCLAIM,把它设置为 ``N`` 可以禁用
DAMON_RECLAIM。注意,由于基于水位的激活条件,DAMON_RECLAIM不能进行真正的监测和回收。
这一点请参考下面关于水位参数的描述。
min_age
-------
识别冷内存区域的时间阈值,单位是微秒。
如果一个内存区域在这个时间或更长的时间内没有被访问,DAMON_RECLAIM会将该区域识别为冷的,
并回收它。
默认为120秒。
quota_ms
--------
回收的时间限制,以毫秒为单位。
DAMON_RECLAIM 试图在一个时间窗口(quota_reset_interval_ms)内只使用到这个时间,以
尝试回收冷页。这可以用来限制DAMON_RECLAIM的CPU消耗。如果该值为零,则该限制被禁用。
默认为10ms。
quota_sz
--------
回收的内存大小限制,单位为字节。
DAMON_RECLAIM 收取在一个时间窗口(quota_reset_interval_ms)内试图回收的内存量,并
使其不超过这个限制。这可以用来限制CPU和IO的消耗。如果该值为零,则限制被禁用。
默认情况下是128 MiB。
quota_reset_interval_ms
-----------------------
时间/大小配额收取重置间隔,单位为毫秒。
时间(quota_ms)和大小(quota_sz)的配额的目标重置间隔。也就是说,DAMON_RECLAIM在
尝试回收‘不’超过quota_ms毫秒或quota_sz字节的内存。
默认为1秒。
wmarks_interval
---------------
当DAMON_RECLAIM被启用但由于其水位规则而不活跃时,在检查水位之前的最小等待时间。
wmarks_high
-----------
高水位的可用内存率(每千字节)。
如果系统的可用内存(以每千字节为单位)高于这个数值,DAMON_RECLAIM就会变得不活跃,所以
它什么也不做,只是定期检查水位。
wmarks_mid
----------
中间水位的可用内存率(每千字节)。
如果系统的空闲内存(以每千字节为单位)在这个和低水位线之间,DAMON_RECLAIM就会被激活,
因此开始监测和回收。
wmarks_low
----------
低水位的可用内存率(每千字节)。
如果系统的空闲内存(以每千字节为单位)低于这个数值,DAMON_RECLAIM就会变得不活跃,所以
它除了定期检查水位外什么都不做。在这种情况下,系统会退回到基于LRU列表的页面粒度回收逻辑。
sample_interval
---------------
监测的采样间隔,单位是微秒。
DAMON用于监测冷内存的采样间隔。更多细节请参考DAMON文档 (:doc:`usage`) 。
aggr_interval
-------------
监测的聚集间隔,单位是微秒。
DAMON对冷内存监测的聚集间隔。更多细节请参考DAMON文档 (:doc:`usage`)。
min_nr_regions
--------------
监测区域的最小数量。
DAMON用于冷内存监测的最小监测区域数。这可以用来设置监测质量的下限。但是,设
置的太高可能会导致监测开销的增加。更多细节请参考DAMON文档 (:doc:`usage`) 。
max_nr_regions
--------------
监测区域的最大数量。
DAMON用于冷内存监测的最大监测区域数。这可以用来设置监测开销的上限值。但是,
设置得太低可能会导致监测质量不好。更多细节请参考DAMON文档 (:doc:`usage`) 。
monitor_region_start
--------------------
目标内存区域的物理地址起点。
DAMON_RECLAIM将对其进行工作的内存区域的起始物理地址。也就是说,DAMON_RECLAIM
将在这个区域中找到冷的内存区域并进行回收。默认情况下,该区域使用最大系统内存区。
monitor_region_end
------------------
目标内存区域的结束物理地址。
DAMON_RECLAIM将对其进行工作的内存区域的末端物理地址。也就是说,DAMON_RECLAIM将
在这个区域内找到冷的内存区域并进行回收。默认情况下,该区域使用最大系统内存区。
kdamond_pid
-----------
DAMON线程的PID。
如果DAMON_RECLAIM被启用,这将成为工作线程的PID。否则,为-1。
nr_reclaim_tried_regions
------------------------
试图通过DAMON_RECLAIM回收的内存区域的数量。
bytes_reclaim_tried_regions
---------------------------
试图通过DAMON_RECLAIM回收的内存区域的总字节数。
nr_reclaimed_regions
--------------------
通过DAMON_RECLAIM成功回收的内存区域的数量。
bytes_reclaimed_regions
-----------------------
通过DAMON_RECLAIM成功回收的内存区域的总字节数。
nr_quota_exceeds
----------------
超过时间/空间配额限制的次数。
例子
====
下面的运行示例命令使DAMON_RECLAIM找到30秒或更长时间没有访问的内存区域并“回收”?
为了避免DAMON_RECLAIM在分页操作中消耗过多的CPU时间,回收被限制在每秒1GiB以内。
它还要求DAMON_RECLAIM在系统的可用内存率超过50%时不做任何事情,但如果它低于40%时
就开始真正的工作。如果DAMON_RECLAIM没有取得进展,因此空闲内存率低于20%,它会要求
DAMON_RECLAIM再次什么都不做,这样我们就可以退回到基于LRU列表的页面粒度回收了::
# cd /sys/module/damon_reclaim/parameters
# echo 30000000 > min_age
# echo $((1 * 1024 * 1024 * 1024)) > quota_sz
# echo 1000 > quota_reset_interval_ms
# echo 500 > wmarks_high
# echo 400 > wmarks_mid
# echo 200 > wmarks_low
# echo Y > enabled
.. [1] https://research.google/pubs/pub48551/
.. [2] https://lwn.net/Articles/787611/
.. [3] https://www.kernel.org/doc/html/latest/mm/free_page_reporting.html
@@ -0,0 +1,124 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/mm/damon/start.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
========
入门指南
========
本文通过演示DAMON的默认用户空间工具,简要地介绍了如何使用DAMON。请注意,为了简洁
起见,本文档只描述了它的部分功能。更多细节请参考该工具的使用文档。
`doc <https://github.com/damonitor/damo/blob/next/USAGE.md>`_ .
前提条件
========
内核
----
首先,你要确保你当前系统中跑的内核构建时选定了这个功能选项 ``CONFIG_DAMON_*=y``.
用户空间工具
------------
在演示中,我们将使用DAMON的默认用户空间工具,称为DAMON Operator(DAMO)。它可以在
https://github.com/damonitor/damo找到。下面的例子假设DAMO在你的$PATH上。当然,但
这并不是强制性的。
因为DAMO使用了DAMON的sysfs接口(详情请参考:doc:`usage`),你应该确保
:doc:`sysfs </filesystems/sysfs>` 被挂载。
记录数据访问模式
================
下面的命令记录了一个程序的内存访问模式,并将监测结果保存到文件中。 ::
$ git clone https://github.com/sjp38/masim
$ cd masim; make; ./masim ./configs/zigzag.cfg &
$ sudo damo record -o damon.data $(pidof masim)
命令的前两行下载了一个人工内存访问生成器程序并在后台运行。生成器将重复地逐一访问两个
100 MiB大小的内存区域。你可以用你的真实工作负载来代替它。最后一行要求 ``damo`` 将
访问模式记录在 ``damon.data`` 文件中。
将记录的模式可视化
==================
你可以在heatmap中直观地看到这种模式,显示哪个内存区域(X轴)何时被访问(Y轴)以及访
问的频率(数字)。::
$ sudo damo report heats --heatmap stdout
22222222222222222222222222222222222222211111111111111111111111111111111111111100
44444444444444444444444444444444444444434444444444444444444444444444444444443200
44444444444444444444444444444444444444433444444444444444444444444444444444444200
33333333333333333333333333333333333333344555555555555555555555555555555555555200
33333333333333333333333333333333333344444444444444444444444444444444444444444200
22222222222222222222222222222222222223355555555555555555555555555555555555555200
00000000000000000000000000000000000000288888888888888888888888888888888888888400
00000000000000000000000000000000000000288888888888888888888888888888888888888400
33333333333333333333333333333333333333355555555555555555555555555555555555555200
88888888888888888888888888888888888888600000000000000000000000000000000000000000
88888888888888888888888888888888888888600000000000000000000000000000000000000000
33333333333333333333333333333333333333444444444444444444444444444444444444443200
00000000000000000000000000000000000000288888888888888888888888888888888888888400
[...]
# access_frequency: 0 1 2 3 4 5 6 7 8 9
# x-axis: space (139728247021568-139728453431248: 196.848 MiB)
# y-axis: time (15256597248362-15326899978162: 1 m 10.303 s)
# resolution: 80x40 (2.461 MiB and 1.758 s for each character)
你也可以直观地看到工作集的大小分布,按大小排序。::
$ sudo damo report wss --range 0 101 10
# <percentile> <wss>
# target_id 18446632103789443072
# avr: 107.708 MiB
0 0 B | |
10 95.328 MiB |**************************** |
20 95.332 MiB |**************************** |
30 95.340 MiB |**************************** |
40 95.387 MiB |**************************** |
50 95.387 MiB |**************************** |
60 95.398 MiB |**************************** |
70 95.398 MiB |**************************** |
80 95.504 MiB |**************************** |
90 190.703 MiB |********************************************************* |
100 196.875 MiB |***********************************************************|
在上述命令中使用 ``--sortby`` 选项,可以显示工作集的大小是如何按时间顺序变化的。::
$ sudo damo report wss --range 0 101 10 --sortby time
# <percentile> <wss>
# target_id 18446632103789443072
# avr: 107.708 MiB
0 3.051 MiB | |
10 190.703 MiB |***********************************************************|
20 95.336 MiB |***************************** |
30 95.328 MiB |***************************** |
40 95.387 MiB |***************************** |
50 95.332 MiB |***************************** |
60 95.320 MiB |***************************** |
70 95.398 MiB |***************************** |
80 95.398 MiB |***************************** |
90 95.340 MiB |***************************** |
100 95.398 MiB |***************************** |
数据访问模式感知的内存管理
==========================
以下三个命令使每一个大小>=4K的内存区域在你的工作负载中没有被访问>=60秒,就会被换掉。 ::
$ echo "#min-size max-size min-acc max-acc min-age max-age action" > test_scheme
$ echo "4K max 0 0 60s max pageout" >> test_scheme
$ damo schemes -c test_scheme <pid of your workload>
@@ -0,0 +1,345 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/mm/damon/usage.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
========
详细用法
========
DAMON 为不同的用户提供了下面这些接口。
- *DAMON用户空间工具。*
`这 <https://github.com/damonitor/damo>`_ 为有这特权的人, 如系统管理员,希望有一个刚好
可以工作的人性化界面。
使用它,用户可以以人性化的方式使用DAMON的主要功能。不过,它可能不会为特殊情况进行高度调整。
它同时支持虚拟和物理地址空间的监测。更多细节,请参考它的 `使用文档
<https://github.com/damonitor/damo/blob/next/USAGE.md>`_。
- *sysfs接口。*
:ref:`这 <sysfs_interface>` 是为那些希望更高级的使用DAMON的特权用户空间程序员准备的。
使用它,用户可以通过读取和写入特殊的sysfs文件来使用DAMON的主要功能。因此,你可以编写和使
用你个性化的DAMON sysfs包装程序,代替你读/写sysfs文件。 `DAMON用户空间工具
<https://github.com/damonitor/damo>`_ 就是这种程序的一个例子 它同时支持虚拟和物理地址
空间的监测。
- *内核空间编程接口。*
:doc:`这 </mm/damon/api>` 这是为内核空间程序员准备的。使用它,用户可以通过为你编写内
核空间的DAMON应用程序,最灵活有效地利用DAMON的每一个功能。你甚至可以为各种地址空间扩展DAMON。
详细情况请参考接口 :doc:`文件 </mm/damon/api>`。
sysfs接口
=========
DAMON的sysfs接口是在定义 ``CONFIG_DAMON_SYSFS`` 时建立的。它在其sysfs目录下创建多
个目录和文件, ``<sysfs>/kernel/mm/damon/`` 。你可以通过对该目录下的文件进行写入和
读取来控制DAMON。
对于一个简短的例子,用户可以监测一个给定工作负载的虚拟地址空间,如下所示::
# cd /sys/kernel/mm/damon/admin/
# echo 1 > kdamonds/nr_kdamonds && echo 1 > kdamonds/0/contexts/nr_contexts
# echo vaddr > kdamonds/0/contexts/0/operations
# echo 1 > kdamonds/0/contexts/0/targets/nr_targets
# echo $(pidof <workload>) > kdamonds/0/contexts/0/targets/0/pid_target
# echo on > kdamonds/0/state
文件层次结构
------------
DAMON sysfs接口的文件层次结构如下图所示。在下图中,父子关系用缩进表示,每个目录有
``/`` 后缀,每个目录中的文件用逗号(",")分开。 ::
/sys/kernel/mm/damon/admin
│ kdamonds/nr_kdamonds
│ │ 0/state,pid
│ │ │ contexts/nr_contexts
│ │ │ │ 0/operations
│ │ │ │ │ monitoring_attrs/
│ │ │ │ │ │ intervals/sample_us,aggr_us,update_us
│ │ │ │ │ │ nr_regions/min,max
│ │ │ │ │ targets/nr_targets
│ │ │ │ │ │ 0/pid_target
│ │ │ │ │ │ │ regions/nr_regions
│ │ │ │ │ │ │ │ 0/start,end
│ │ │ │ │ │ │ │ ...
│ │ │ │ │ │ ...
│ │ │ │ │ schemes/nr_schemes
│ │ │ │ │ │ 0/action
│ │ │ │ │ │ │ access_pattern/
│ │ │ │ │ │ │ │ sz/min,max
│ │ │ │ │ │ │ │ nr_accesses/min,max
│ │ │ │ │ │ │ │ age/min,max
│ │ │ │ │ │ │ quotas/ms,bytes,reset_interval_ms
│ │ │ │ │ │ │ │ weights/sz_permil,nr_accesses_permil,age_permil
│ │ │ │ │ │ │ watermarks/metric,interval_us,high,mid,low
│ │ │ │ │ │ │ stats/nr_tried,sz_tried,nr_applied,sz_applied,qt_exceeds
│ │ │ │ │ │ │ tried_regions/
│ │ │ │ │ │ │ │ 0/start,end,nr_accesses,age
│ │ │ │ │ │ │ │ ...
│ │ │ │ │ │ ...
│ │ │ │ ...
│ │ ...
根
--
DAMON sysfs接口的根是 ``<sysfs>/kernel/mm/damon/`` ,它有一个名为 ``admin`` 的
目录。该目录包含特权用户空间程序控制DAMON的文件。拥有根权限的用户空间工具或deamons可以
使用这个目录。
kdamonds/
---------
与监测相关的信息包括请求规格和结果被称为DAMON上下文。DAMON用一个叫做kdamond的内核线程
执行每个上下文,多个kdamonds可以并行运行。
在 ``admin`` 目录下,有一个目录,即``kdamonds``,它有控制kdamonds的文件存在。在开始
时,这个目录只有一个文件,``nr_kdamonds``。向该文件写入一个数字(``N``),就会创建名为
``0`` 到 ``N-1`` 的子目录数量。每个目录代表每个kdamond。
kdamonds/<N>/
-------------
在每个kdamond目录中,存在两个文件(``state`` 和 ``pid`` )和一个目录( ``contexts`` )。
读取 ``state`` 时,如果kdamond当前正在运行,则返回 ``on`` ,如果没有运行则返回 ``off`` 。
写入 ``on`` 或 ``off`` 使kdamond处于状态。向 ``state`` 文件写 ``update_schemes_stats`` ,
更新kdamond的每个基于DAMON的操作方案的统计文件的内容。关于统计信息的细节,请参考
:ref:`stats section <sysfs_schemes_stats>`. 将 ``update_schemes_tried_regions`` 写到
``state`` 文件,为kdamond的每个基于DAMON的操作方案,更新基于DAMON的操作方案动作的尝试区域目录。
将`clear_schemes_tried_regions`写入`state`文件,清除kdamond的每个基于DAMON的操作方案的动作
尝试区域目录。 关于基于DAMON的操作方案动作尝试区域目录的细节,请参考:ref:tried_regions 部分
<sysfs_schemes_tried_regions>`。
如果状态为 ``on``,读取 ``pid`` 显示kdamond线程的pid。
``contexts`` 目录包含控制这个kdamond要执行的监测上下文的文件。
kdamonds/<N>/contexts/
----------------------
在开始时,这个目录只有一个文件,即 ``nr_contexts`` 。向该文件写入一个数字( ``N`` ),就会创
建名为``0`` 到 ``N-1`` 的子目录数量。每个目录代表每个监测背景。目前,每个kdamond只支持
一个上下文,所以只有 ``0`` 或 ``1`` 可以被写入文件。
contexts/<N>/
-------------
在每个上下文目录中,存在一个文件(``operations``)和三个目录(``monitoring_attrs``,
``targets``, 和 ``schemes``)。
DAMON支持多种类型的监测操作,包括对虚拟地址空间和物理地址空间的监测。你可以通过向文件
中写入以下关键词之一,并从文件中读取,来设置和获取DAMON将为上下文使用何种类型的监测操作。
- vaddr: 监测特定进程的虚拟地址空间
- paddr: 监视系统的物理地址空间
contexts/<N>/monitoring_attrs/
------------------------------
用于指定监测属性的文件,包括所需的监测质量和效率,都在 ``monitoring_attrs`` 目录中。
具体来说,这个目录下有两个目录,即 ``intervals`` 和 ``nr_regions`` 。
在 ``intervals`` 目录下,存在DAMON的采样间隔(``sample_us``)、聚集间隔(``aggr_us``)
和更新间隔(``update_us``)三个文件。你可以通过写入和读出这些文件来设置和获取微秒级的值。
在 ``nr_regions`` 目录下,有两个文件分别用于DAMON监测区域的下限和上限(``min`` 和 ``max`` ),
这两个文件控制着监测的开销。你可以通过向这些文件的写入和读出来设置和获取这些值。
关于间隔和监测区域范围的更多细节,请参考设计文件 (:doc:`/mm/damon/design`)。
contexts/<N>/targets/
---------------------
在开始时,这个目录只有一个文件 ``nr_targets`` 。向该文件写入一个数字(``N``),就可以创建
名为 ``0`` 到 ``N-1`` 的子目录的数量。每个目录代表每个监测目标。
targets/<N>/
------------
在每个目标目录中,存在一个文件(``pid_target``)和一个目录(``regions``)。
如果你把 ``vaddr`` 写到 ``contexts/<N>/operations`` 中,每个目标应该是一个进程。你
可以通过将进程的pid写到 ``pid_target`` 文件中来指定DAMON的进程。
targets/<N>/regions
-------------------
当使用 ``vaddr`` 监测操作集时( ``vaddr`` 被写入 ``contexts/<N>/operations`` 文
件),DAMON自动设置和更新监测目标区域,这样就可以覆盖目标进程的整个内存映射。然而,用户可
能希望将初始监测区域设置为特定的地址范围。
相反,当使用 ``paddr`` 监测操作集时,DAMON不会自动设置和更新监测目标区域( ``paddr``
被写入 ``contexts/<N>/operations`` 中)。因此,在这种情况下,用户应该自己设置监测目标
区域。
在这种情况下,用户可以按照自己的意愿明确设置初始监测目标区域,将适当的值写入该目录下的文件。
开始时,这个目录只有一个文件, ``nr_regions`` 。向该文件写入一个数字(``N``),就可以创
建名为 ``0`` 到 ``N-1`` 的子目录。每个目录代表每个初始监测目标区域。
regions/<N>/
------------
在每个区域目录中,你会发现两个文件( ``start`` 和 ``end`` )。你可以通过向文件写入
和从文件中读出,分别设置和获得初始监测目标区域的起始和结束地址。
每个区域不应该与其他区域重叠。 目录“N”的“结束”应等于或小于目录“N+1”的“开始”。
contexts/<N>/schemes/
---------------------
对于一版的基于DAMON的数据访问感知的内存管理优化,用户通常希望系统对特定访问模式的内存区
域应用内存管理操作。DAMON从用户那里接收这种形式化的操作方案,并将这些方案应用于目标内存
区域。用户可以通过读取和写入这个目录下的文件来获得和设置这些方案。
在开始时,这个目录只有一个文件,``nr_schemes``。向该文件写入一个数字(``N``),就可以
创建名为``0``到``N-1``的子目录的数量。每个目录代表每个基于DAMON的操作方案。
schemes/<N>/
------------
在每个方案目录中,存在五个目录(``access_pattern``、``quotas``、``watermarks``、
``stats`` 和 ``tried_regions``)和一个文件(``action``)。
``action`` 文件用于设置和获取你想应用于具有特定访问模式的内存区域的动作。可以写入文件
和从文件中读取的关键词及其含义如下。
- ``willneed``: 对有 ``MADV_WILLNEED`` 的区域调用 ``madvise()`` 。
- ``cold``: 对具有 ``MADV_COLD`` 的区域调用 ``madvise()`` 。
- ``pageout``: 为具有 ``MADV_PAGEOUT`` 的区域调用 ``madvise()`` 。
- ``hugepage``: 为带有 ``MADV_HUGEPAGE`` 的区域调用 ``madvise()`` 。
- ``nohugepage``: 为带有 ``MADV_NOHUGEPAGE`` 的区域调用 ``madvise()``。
- ``lru_prio``: 在其LRU列表上对区域进行优先排序。
- ``lru_deprio``: 对区域的LRU列表进行降低优先处理。
- ``stat``: 什么都不做,只计算统计数据
schemes/<N>/access_pattern/
---------------------------
每个基于DAMON的操作方案的目标访问模式由三个范围构成,包括以字节为单位的区域大小、每个
聚合区间的监测访问次数和区域年龄的聚合区间数。
在 ``access_pattern`` 目录下,存在三个目录( ``sz``, ``nr_accesses``, 和 ``age`` ),
每个目录有两个文件(``min`` 和 ``max`` )。你可以通过向 ``sz``, ``nr_accesses``, 和
``age`` 目录下的 ``min`` 和 ``max`` 文件分别写入和读取来设置和获取给定方案的访问模式。
schemes/<N>/quotas/
-------------------
每个 ``动作`` 的最佳 ``目标访问模式`` 取决于工作负载,所以不容易找到。更糟糕的是,将某些动作
的方案设置得过于激进会造成严重的开销。为了避免这种开销,用户可以为每个方案限制时间和大小配额。
具体来说,用户可以要求DAMON尽量只使用特定的时间(``时间配额``)来应用动作,并且在给定的时间间
隔(``重置间隔``)内,只对具有目标访问模式的内存区域应用动作,而不使用特定数量(``大小配额``)。
当预计超过配额限制时,DAMON会根据 ``目标访问模式`` 的大小、访问频率和年龄,对找到的内存区域
进行优先排序。为了进行个性化的优先排序,用户可以为这三个属性设置权重。
在 ``quotas`` 目录下,存在三个文件(``ms``, ``bytes``, ``reset_interval_ms``)和一个
目录(``weights``),其中有三个文件(``sz_permil``, ``nr_accesses_permil``, 和
``age_permil``)。
你可以设置以毫秒为单位的 ``时间配额`` ,以字节为单位的 ``大小配额`` ,以及以毫秒为单位的 ``重
置间隔`` ,分别向这三个文件写入数值。你还可以通过向 ``weights`` 目录下的三个文件写入数值来设
置大小、访问频率和年龄的优先权,单位为千分之一。
schemes/<N>/watermarks/
-----------------------
为了便于根据系统状态激活和停用每个方案,DAMON提供了一个称为水位的功能。该功能接收五个值,称为
``度量`` 、``间隔`` 、``高`` 、``中`` 、``低`` 。``度量值`` 是指可以测量的系统度量值,如
自由内存比率。如果系统的度量值 ``高`` 于memoent的高值或 ``低`` 于低值,则该方案被停用。如果
该值低于 ``中`` ,则该方案被激活。
在水位目录下,存在五个文件(``metric``, ``interval_us``,``high``, ``mid``, and ``low``)
用于设置每个值。你可以通过向这些文件的写入来分别设置和获取这五个值。
可以写入 ``metric`` 文件的关键词和含义如下。
- none: 忽略水位
- free_mem_rate: 系统的自由内存率(千分比)。
``interval`` 应以微秒为单位写入。
schemes/<N>/stats/
------------------
DAMON统计每个方案被尝试应用的区域的总数量和字节数,每个方案被成功应用的区域的两个数字,以及
超过配额限制的总数量。这些统计数据可用于在线分析或调整方案。
可以通过读取 ``stats`` 目录下的文件(``nr_tried``, ``sz_tried``, ``nr_applied``,
``sz_applied``, 和 ``qt_exceeds``))分别检索这些统计数据。这些文件不是实时更新的,所以
你应该要求DAMON sysfs接口通过在相关的 ``kdamonds/<N>/state`` 文件中写入一个特殊的关键字
``update_schemes_stats`` 来更新统计信息的文件内容。
schemes/<N>/tried_regions/
--------------------------
当一个特殊的关键字 ``update_schemes_tried_regions`` 被写入相关的 ``kdamonds/<N>/state``
文件时,DAMON会在这个目录下创建从 ``0`` 开始命名的整数目录。每个目录包含的文件暴露了关于每个
内存区域的详细信息,在下一个 :ref:`聚集区间 <sysfs_monitoring_attrs>`,相应的方案的 ``动作``
已经尝试在这个目录下应用。这些信息包括地址范围、``nr_accesses`` 以及区域的 ``年龄`` 。
当另一个特殊的关键字 ``clear_schemes_tried_regions`` 被写入相关的 ``kdamonds/<N>/state``
文件时,这些目录将被删除。
tried_regions/<N>/
------------------
在每个区域目录中,你会发现四个文件(``start``, ``end``, ``nr_accesses``, and ``age``)。
读取这些文件将显示相应的基于DAMON的操作方案 ``动作`` 试图应用的区域的开始和结束地址、``nr_accesses``
和 ``年龄`` 。
用例
~~~~
下面的命令应用了一个方案:”如果一个大小为[4KiB, 8KiB]的内存区域在[10, 20]的聚合时间间隔内
显示出每一个聚合时间间隔[0, 5]的访问量,请分页该区域。对于分页,每秒最多只能使用10ms,而且每
秒分页不能超过1GiB。在这一限制下,首先分页出具有较长年龄的内存区域。另外,每5秒钟检查一次系统
的可用内存率,当可用内存率低于50%时开始监测和分页,但如果可用内存率大于60%,或低于30%,则停
止监测。“ ::
# cd <sysfs>/kernel/mm/damon/admin
# # populate directories
# echo 1 > kdamonds/nr_kdamonds; echo 1 > kdamonds/0/contexts/nr_contexts;
# echo 1 > kdamonds/0/contexts/0/schemes/nr_schemes
# cd kdamonds/0/contexts/0/schemes/0
# # set the basic access pattern and the action
# echo 4096 > access_pattern/sz/min
# echo 8192 > access_pattern/sz/max
# echo 0 > access_pattern/nr_accesses/min
# echo 5 > access_pattern/nr_accesses/max
# echo 10 > access_pattern/age/min
# echo 20 > access_pattern/age/max
# echo pageout > action
# # set quotas
# echo 10 > quotas/ms
# echo $((1024*1024*1024)) > quotas/bytes
# echo 1000 > quotas/reset_interval_ms
# # set watermark
# echo free_mem_rate > watermarks/metric
# echo 5000000 > watermarks/interval_us
# echo 600 > watermarks/high
# echo 500 > watermarks/mid
# echo 300 > watermarks/low
请注意,我们强烈建议使用用户空间的工具,如 `damo <https://github.com/damonitor/damo>`_ ,
而不是像上面那样手动读写文件。以上只是一个例子。
监测结果的监测点
================
DAMON通过一个tracepoint ``damon:damon_aggregated`` 提供监测结果. 当监测开启时,你可
以记录追踪点事件,并使用追踪点支持工具如perf显示结果。比如说::
# echo on > monitor_on_DEPRECATED
# perf record -e damon:damon_aggregated &
# sleep 5
# kill 9 $(pidof perf)
# echo off > monitor_on_DEPRECATED
# perf script
@@ -0,0 +1,49 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/mm/index.rst
:翻译:
徐鑫 xu xin <xu.xin16@zte.com.cn>
========
内存管理
========
Linux内存管理子系统,顾名思义,是负责系统中的内存管理。它包括了虚拟内存与请求
分页的实现,内核内部结构和用户空间程序的内存分配、将文件映射到进程地址空间以
及许多其他很酷的事情。
Linux内存管理是一个具有许多可配置设置的复杂系统, 且这些设置中的大多数都可以通
过 ``/proc`` 文件系统获得,并且可以使用 ``sysctl`` 进行查询和调整。这些API接
口被描述在Documentation/admin-guide/sysctl/vm.rst文件和 `man 5 proc`_ 中。
.. _man 5 proc: http://man7.org/linux/man-pages/man5/proc.5.html
Linux内存管理有它自己的术语,如果你还不熟悉它,请考虑阅读下面参考:
Documentation/admin-guide/mm/concepts.rst.
在此目录下,我们详细描述了如何与Linux内存管理中的各种机制交互。
.. toctree::
:maxdepth: 1
damon/index
ksm
Todolist:
* concepts
* cma_debugfs
* hugetlbpage
* idle_page_tracking
* memory-hotplug
* nommu-mmap
* numa_memory_policy
* numaperf
* pagemap
* soft-dirty
* swap_numa
* transhuge
* userfaultfd
* zswap
@@ -0,0 +1,198 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/mm/ksm.rst
:翻译:
徐鑫 xu xin <xu.xin16@zte.com.cn>
============
内核同页合并
============
概述
====
KSM是一种能节省内存的数据去重功能,由CONFIG_KSM=y启用,并在2.6.32版本时被添
加到Linux内核。详见 ``mm/ksm.c`` 的实现,以及http://lwn.net/Articles/306704
和https://lwn.net/Articles/330589
KSM最初目的是为了与KVM(即著名的内核共享内存)一起使用而开发的,通过共享虚拟机
之间的公共数据,将更多虚拟机放入物理内存。但它对于任何会生成多个相同数据实例的
应用程序都是很有用的。
KSM的守护进程ksmd会定期扫描那些已注册的用户内存区域,查找内容相同的页面,这些
页面可以被单个写保护页面替换(如果进程以后想要更新其内容,将自动复制)。使用:
引用:`sysfs intraface <ksm_sysfs>` 接口来配置KSM守护程序在单个过程中所扫描的页
数以及两个过程之间的间隔时间。
KSM只合并匿名(私有)页面,从不合并页缓存(文件)页面。KSM的合并页面最初只能被
锁定在内核内存中,但现在可以就像其他用户页面一样被换出(但当它们被交换回来时共
享会被破坏: ksmd必须重新发现它们的身份并再次合并)。
以madvise控制KSM
================
KSM仅在特定的地址空间区域时运行,即应用程序通过使用如下所示的madvise(2)系统调
用来请求某块地址成为可能的合并候选者的地址空间::
int madvise(addr, length, MADV_MERGEABLE)
应用程序当然也可以通过调用::
int madvise(addr, length, MADV_UNMERGEABLE)
来取消该请求,并恢复为非共享页面:此时KSM将去除合并在该范围内的任何合并页。注意:
这个去除合并的调用可能突然需要的内存量超过实际可用的内存量-那么可能会出现EAGAIN
失败,但更可能会唤醒OOM killer。
如果KSM未被配置到正在运行的内核中,则madvise MADV_MERGEABLE 和 MADV_UNMERGEABLE
的调用只会以EINVAL 失败。如果正在运行的内核是用CONFIG_KSM=y方式构建的,那么这些
调用通常会成功:即使KSM守护程序当前没有运行,MADV_MERGEABLE 仍然会在KSM守护程序
启动时注册范围,即使该范围不能包含KSM实际可以合并的任何页面,即使MADV_UNMERGEABLE
应用于从未标记为MADV_MERGEABLE的范围。
如果一块内存区域必须被拆分为至少一个新的MADV_MERGEABLE区域或MADV_UNMERGEABLE区域,
当该进程将超过 ``vm.max_map_count`` 的设定,则madvise可能返回ENOMEM。(请参阅文档
Documentation/admin-guide/sysctl/vm.rst)。
与其他madvise调用一样,它们在用户地址空间的映射区域上使用:如果指定的范围包含未
映射的间隙(尽管在中间的映射区域工作),它们将报告ENOMEM,如果没有足够的内存用于
内部结构,则可能会因EAGAIN而失败。
KSM守护进程sysfs接口
====================
KSM守护进程可以由``/sys/kernel/mm/ksm/`` 中的sysfs文件控制,所有人都可以读取,但
只能由root用户写入。各接口解释如下:
pages_to_scan
ksmd进程进入睡眠前要扫描的页数。
例如, ``echo 100 > /sys/kernel/mm/ksm/pages_to_scan``
默认值:100(该值被选择用于演示目的)
sleep_millisecs
ksmd在下次扫描前应休眠多少毫秒
例如, ``echo 20 > /sys/kernel/mm/ksm/sleep_millisecs``
默认值:20(该值被选择用于演示目的)
merge_across_nodes
指定是否可以合并来自不同NUMA节点的页面。当设置为0时,ksm仅合并在物理上位
于同一NUMA节点的内存区域中的页面。这降低了访问共享页面的延迟。在有明显的
NUMA距离上,具有更多节点的系统可能受益于设置该值为0时的更低延迟。而对于
需要对内存使用量最小化的较小系统来说,设置该值为1(默认设置)则可能会受
益于更大共享页面。在决定使用哪种设置之前,您可能希望比较系统在每种设置下
的性能。 ``merge_across_nodes`` 仅当系统中没有ksm共享页面时,才能被更改设
置:首先将接口`run` 设置为2从而对页进行去合并,然后在修改
``merge_across_nodes`` 后再将‘run’又设置为1,以根据新设置来重新合并。
默认值:1(如早期的发布版本一样合并跨站点)
run
* 设置为0可停止ksmd运行,但保留合并页面,
* 设置为1可运行ksmd,例如, ``echo 1 > /sys/kernel/mm/ksm/run`` ,
* 设置为2可停止ksmd运行,并且对所有目前已合并的页进行去合并,但保留可合并
区域以供下次运行。
默认值:0(必须设置为1才能激活KSM,除非禁用了CONFIG_SYSFS)
use_zero_pages
指定是否应当特殊处理空页(即那些仅含zero的已分配页)。当该值设置为1时,
空页与内核零页合并,而不是像通常情况下那样空页自身彼此合并。这可以根据
工作负载的不同,在具有着色零页的架构上可以提高性能。启用此设置时应小心,
因为它可能会降低某些工作负载的KSM性能,比如,当待合并的候选页面的校验和
与空页面的校验和恰好匹配的时候。此设置可随时更改,仅对那些更改后再合并
的页面有效。
默认值:0(如同早期版本的KSM正常表现)
max_page_sharing
单个KSM页面允许的最大共享站点数。这将强制执行重复数据消除限制,以避免涉
及遍历共享KSM页面的虚拟映射的虚拟内存操作的高延迟。最小值为2,因为新创
建的KSM页面将至少有两个共享者。该值越高,KSM合并内存的速度越快,去重
因子也越高,但是对于任何给定的KSM页面,虚拟映射的最坏情况遍历的速度也会
越慢。减慢了这种遍历速度就意味着在交换、压缩、NUMA平衡和页面迁移期间,
某些虚拟内存操作将有更高的延迟,从而降低这些虚拟内存操作调用者的响应能力。
其他任务如果不涉及执行虚拟映射遍历的VM操作,其任务调度延迟不受此参数的影
响,因为这些遍历本身是调度友好的。
stable_node_chains_prune_millisecs
指定KSM检查特定页面的元数据的频率(即那些达到过时信息数据去重限制标准的
页面)单位是毫秒。较小的毫秒值将以更低的延迟来释放KSM元数据,但它们将使
ksmd在扫描期间使用更多CPU。如果还没有一个KSM页面达到 ``max_page_sharing``
标准,那就没有什么用。
KSM与MADV_MERGEABLE的工作有效性体现于 ``/sys/kernel/mm/ksm/`` 路径下的接口:
pages_shared
表示多少共享页正在被使用
pages_sharing
表示还有多少站点正在共享这些共享页,即节省了多少
pages_unshared
表示有多少页是唯一的,但被反复检查以进行合并
pages_volatile
表示有多少页因变化太快而无法放在tree中
full_scans
表示所有可合并区域已扫描多少次
stable_node_chains
达到 ``max_page_sharing`` 限制的KSM页数
stable_node_dups
重复的KSM页数
比值 ``pages_sharing/pages_shared`` 的最大值受限制于 ``max_page_sharing``
的设定。要想增加该比值,则相应地要增加 ``max_page_sharing`` 的值。
监测KSM的收益
=============
KSM可以通过合并相同的页面来节省内存,但也会消耗额外的内存,因为它需要生成一些rmap_items
来保存每个扫描页面的简要rmap信息。其中有些页面可能会被合并,但有些页面在被检查几次
后可能无法被合并,这些都是无益的内存消耗。
1) 如何确定KSM在全系统范围内是节省内存还是消耗内存?这里有一个简单的近似计算方法供参考::
general_profit =~ pages_sharing * sizeof(page) - (all_rmap_items) *
sizeof(rmap_item);
其中all_rmap_items可以通过对 ``pages_sharing`` 、 ``pages_shared`` 、 ``pages_unshared``
和 ``pages_volatile`` 的求和而轻松获得。
2) 单一进程中KSM的收益也可以通过以下近似的计算得到::
process_profit =~ ksm_merging_pages * sizeof(page) -
ksm_rmap_items * sizeof(rmap_item).
其中ksm_merging_pages显示在 ``/proc/<pid>/`` 目录下,而ksm_rmap_items
显示在 ``/proc/<pid>/ksm_stat`` 。
从应用的角度来看, ``ksm_rmap_items`` 和 ``ksm_merging_pages`` 的高比例意
味着不好的madvise-applied策略,所以开发者或管理员必须重新考虑如何改变madvis策
略。举个例子供参考,一个页面的大小通常是4K,而rmap_item的大小在32位CPU架构上分
别是32B,在64位CPU架构上是64B。所以如果 ``ksm_rmap_items/ksm_merging_pages``
的比例在64位CPU上超过64,或者在32位CPU上超过128,那么应用程序的madvise策略应
该被放弃,因为ksm收益大约为零或负值。
监控KSM事件
===========
在/proc/vmstat中有一些计数器,可以用来监控KSM事件。KSM可能有助于节省内存,这是
一种权衡,因为它可能会在KSM COW或复制中的交换上遭受延迟。这些事件可以帮助用户评估
是否或如何使用KSM。例如,如果cow_ksm增加得太快,用户可以减少madvise(, , MADV_MERGEABLE)
的范围。
cow_ksm
在每次KSM页面触发写时拷贝(COW)时都会被递增,当用户试图写入KSM页面时,
我们必须做一个拷贝。
ksm_swpin_copy
在换入时,每次KSM页被复制时都会被递增。请注意,KSM页在换入时可能会被复
制,因为do_swap_page()不能做所有的锁,而需要重组一个跨anon_vma的KSM页。
--
Izik Eidus,
Hugh Dickins, 2009年11月17日。
@@ -0,0 +1,48 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/numastat.rst
:Translator: Tao Zou <wodemia@linux.alibaba.com>
=======================
Numa策略命中/未命中统计
=======================
/sys/devices/system/node/node*/numastat
所有数据的单位都是页面。巨页有独立的计数器。
numa_hit、numa_miss和numa_foreign计数器反映了进程是否能够在他们偏好的节点上分配内存。
如果进程成功在偏好的节点上分配内存则在偏好的节点上增加numa_hit计数,否则在偏好的节点上增
加numa_foreign计数同时在实际内存分配的节点上增加numa_miss计数。
通常,偏好的节点是进程运行所在的CPU的本地节点,但是一些限制可以改变这一行为,比如内存策略,
因此同样有两个基于CPU本地节点的计数器。local_node和numa_hit类似,当在CPU所在的节点上分
配内存时增加local_node计数,other_node和numa_miss类似,当在CPU所在节点之外的其他节点
上成功分配内存时增加other_node计数。需要注意,没有和numa_foreign对应的计数器。
更多细节内容:
=============== ============================================================
numa_hit 一个进程想要从本节点分配内存并且成功。
numa_miss 一个进程想要从其他节点分配内存但是最终在本节点完成内存分配。
numa_foreign 一个进程想要在本节点分配内存但是最终在其他节点完成内存分配。
local_node 一个进程运行在本节点的CPU上并且从本节点上获得了内存。
other_node 一个进程运行在其他节点的CPU上但是在本节点上获得了内存。
interleave_hit 内存交叉分配策略下想要从本节点分配内存并且成功。
=============== ============================================================
你可以使用numactl软件包(http://oss.sgi.com/projects/libnuma/)中的numastat工具
来辅助阅读。需要注意,numastat工具目前只在有少量CPU的机器上运行良好。
需要注意,在包含无内存节点(一个节点有CPUs但是没有内存)的系统中numa_hit、numa_miss和
numa_foreign统计数据会被严重曲解。在当前的内核实现中,如果一个进程偏好一个无内存节点(即
进程正在该节点的一个本地CPU上运行),实际上会从距离最近的有内存节点中挑选一个作为偏好节点。
结果会导致相应的内存分配不会增加无内存节点上的numa_foreign计数器,并且会扭曲最近节点上的
numa_hit、numa_miss和numa_foreign统计数据。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,370 @@
.. SPDX-License-Identifier: (GPL-2.0+ OR CC-BY-4.0)
.. 【重分发信息参见本文件结尾】
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/reporting-regressions.rst
:译者:
吴想成 Wu XiangCheng <bobwxc@email.cn>
============
报告回归问题
============
“*我们拒绝出现回归*”是Linux内核开发的首要规则;Linux的发起者和领军开发者Linus
Torvalds立下了此规则并确保它被落实。
本文档描述了这条规则对用户的意义,以及Linux内核开发模型如何确保解决所有被报告
的回归;关于内核开发者如何处理的方面参见 Documentation/process/handling-regressions.rst 。
本文重点(亦即“太长不看”)
==========================
#. 如果某程序在原先的Linux内核上运行良好,但在较新版本上效果更差、或者根本不
能用,那么你就碰见回归问题了。注意,新内核需要使用类似配置编译;更多相关细
节参见下方。
#. 按照 Documentation/translations/zh_CN/admin-guide/reporting-issues.rst 中
所说的报告你的问题,该文档已经包含了所有关于回归的重要方面,为了方便起见也
复制到了下面。两个重点:在报告主题中使用“[REGRESSION]”开头并抄送或转发到
`回归邮件列表 <https://lore.kernel.org/regressions/>`_
(regressions@lists.linux.dev)。
#. 可选但是建议:在发送或转发报告时,指明该回归发生的起点,以便Linux内核回归
追踪机器人“regzbot”可以追踪此问题::
#regzbot introduced v5.13..v5.14-rc1
与用户相关的所有Linux内核回归细节
=================================
基本重点
--------
什么是“回归”以及什么是“无回归规则”?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
如果某程序/实例在原先的Linux内核上运行良好,但在较新版本上效果更差、或者根本
不能用,那么你就碰见回归问题了。“无回归规则”不允许出现这种情况。如果偶然发
生了,导致问题的开发者应当迅速修复问题。
也就是说,若Linux 5.13中的WiFi驱动程序运行良好,但是在5.14版本上却不能用、速
度明显变慢或出现错误,那就出现了回归。如果某正常工作的应用程序突然在新内核上
出现不稳定,这也是回归;这些问题可能是由于procfs、sysfs或Linux提供给用户空间
软件的许多其他接口之一的变化。但请记住,前述例子中的5.14需要使用类似于5.13的
配置构建。这可以用 ``make olddefconfig`` 实现,详细解释见下。
注意本节第一句话中的“实例”:即使开发者需要遵循“无回归”规则,但仍可自由地改
变内核的任何方面,甚至是导出到用户空间的API或ABI,只要别破坏现有的应用程序或
用例。
还需注意,“无回归”规则只限制内核提供给用户空间的接口。它不适用于内核内部接
口,比如一些外部开发的驱动程序用来插入钩子到内核的模块API。
如何报告回归?
~~~~~~~~~~~~~~
只需按照 Documentation/translations/zh_CN/admin-guide/reporting-issues.rst 中
所说的报告你的问题,该文档已经包含了要点。下面几点概述了一下只在回归中重要的
方面:
* 在检查可加入讨论的现有报告时,别忘了搜索 `Linux回归邮件列表
<https://lore.kernel.org/regressions/>`_ 和 `regzbot网页界面
<https://linux-regtracking.leemhuis.info/regzbot/>`_ 。
* 在报告主题的开头加上“[REGRESSION]”。
* 在你的报告中明确最后一个正常工作的内核版本和首个出问题的版本。如若可能,
用二分法尝试找出导致回归的变更,更多细节见下。
* 记得把报告发到Linux回归邮件列表(regressions@lists.linux.dev)。
* 如果通过邮件报告回归,请抄送回归列表。
* 如果你使用某些缺陷追踪器报告回归,请通过邮件转发已提交的报告到回归列表,
并抄送维护者以及出问题的相关子系统的邮件列表。
如果是稳定版或长期支持版系列(如v5.15.3…v5.15.5)的回归,请记得抄送
`Linux稳定版邮件列表 <https://lore.kernel.org/stable/>`_ (stable@vger.kernel.org)。
如果你成功地执行了二分,请抄送肇事提交的信息中所有签了“Signed-off-by:”的人。
在抄送你的报告到列表时,也请记得通知前述的Linux内核回归追踪机器人。只需在邮件
中包含如下片段::
#regzbot introduced: v5.13..v5.14-rc1
Regzbot会就将你的邮件视为在某个特定版本区间的回归报告。上例中即linux v5.13仍
然正常,而Linux 5.14-rc1是首个您遇到问题的版本。如果你执行了二分以查找导致回
归的提交,请使用指定肇事提交的id代替::
#regzbot introduced: 1f2e3d4c5d
添加这样的“regzbot命令”对你是有好处的,它会确保报告不会被忽略。如果你省略了
它,Linux内核的回归跟踪者会把你的回归告诉regzbot,只要你发送了一个副本到回归
邮件列表。但是回归跟踪者只有一个人,有时不得不休息或甚至偶尔享受可以远离电脑
的时光(听起来很疯狂)。因此,依赖此人手动将回归添加到 `已追踪且尚未解决的
Linux内核回归列表 <https://linux-regtracking.leemhuis.info/regzbot/>`_ 和
regzbot发送的每周回归报告,可能会出现延迟。 这样的延误会导致Linus Torvalds
在决定“继续开发还是发布新版本?”时忽略严重的回归。
真的修复了所有的回归吗?
~~~~~~~~~~~~~~~~~~~~~~~~
几乎所有都是,只要引起问题的变更(肇事提交)被可靠定位。也有些回归可以不用这
样,但通常是必须的。
谁需要找出回归的根本原因?
~~~~~~~~~~~~~~~~~~~~~~~~~~
受影响代码区域的开发者应该自行尝试定位问题所在。但仅靠他们的努力往往是不可
能做到的,很多问题只发生在开发者的无法接触的其他特定外部环境中——例如特定的
硬件平台、固件、Linux发行版、系统的配置或应用程序。这就是为什么最终往往是报
告者定位肇事提交;有时用户甚至需要再运行额外测试以查明确切的根本原因。开发
者应该提供建议和可能的帮助,以使普通用户更容易完成该流程。
如何找到罪魁祸首?
~~~~~~~~~~~~~~~~~~
如 Documentation/translations/zh_CN/admin-guide/reporting-issues.rst (简要)
和 Documentation/translations/zh_CN/admin-guide/bug-bisect.rst (详细)中所
述,执行二分。听起来工作量很大,但大部分情况下很快就能找到罪魁祸首。如果这很
困难或可靠地重现问题很耗时,请考虑与其他受影响的用户合作,一起缩小搜索范围。
当出现回归时我可以向谁寻求建议?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
发送邮件到回归邮件列表(regressions@lists.linux.dev)同时抄送Linux内核的回归
跟踪者(regressions@leemhuis.info);如果问题需要保密处理,可以省略列表。
关于回归的更多细节
------------------
“无回归规则”的目标是什么?
~~~~~~~~~~~~~~~~~~~~~~~~~~
用户应该放心升级内核版本,而不必担心有程序可能崩溃。这符合内核开发者的利益,
可以使更新有吸引力:他们不希望用户停留在停止维护或超过一年半的稳定/长期Linux
版本系列上。这也符合所有人的利益,因为 `那些系列可能含有已知的缺陷、安全问题
或其他后续版本已经修复的问题
<http://www.kroah.com/log/blog/2018/08/24/what-stable-kernel-should-i-use/>`_ 。
此外,内核开发者希望使用户测试最新的预发行版或常规发行版变得简单而有吸引力。
这同样符合所有人的利益,如果新版本出来后很快就有相关报告,会使追踪和修复问题
更容易。
实际中“无回归”规则真的可行吗?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这不是句玩笑话,请见Linux创建者和主要开发人员Linus Torvalds在邮件列表中的许
多发言,其中一些在 Documentation/process/handling-regressions.rst 中被引用。
此规则的例外情况极为罕见;之前当开发者认为某个特定的情况有必要援引例外时,
基本都被证明错了。
谁来确保“无回归”被落实?
~~~~~~~~~~~~~~~~~~~~~~~~
照看和支撑树的子系统维护者应该关心这一点——例如,Linus Torvalds之于主线,
Greg Kroah-Hartman等人之于各种稳定/长期系列。
他们都得到了别人的帮助,以确保回归报告不会被遗漏。其中之一是Thorsten
Leemhuis,他目前担任Linux内核的“回归跟踪者”;为了做好这项工作,他使用了
regzbot——Linux内核回归跟踪机器人。所以这就是为什么要抄送或转发你的报告到
回归邮件列表来通知这些人,已经最好在你的邮件中包含“regzbot命令”来立即追踪它。
回归通常多久能修复?
~~~~~~~~~~~~~~~~~~~~
开发者应该尽快修复任何被报告的回归,以提供及时为受影响的用户提供解决方案,并
防止更多用户遇到问题;然而,开发人员需要花足够的时间和注意力确保回归修复不会
造成额外的损害。
因此,答案取决于各种因素,如回归的影响、存在时长或出现于哪个Linux版本系列。
但最终,大多数的回归应该在两周内修复。
当问题可以通过升级某些软件解决时,是回归吗?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
基本都是。如果开发人员告诉您其他情况,请咨询上述回归跟踪者。
当新内核变慢或能耗增加,是回归吗?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
是的,但有一些差别。在微型基准测试中变慢5%不太可能被视为回归,除非它也会对
广泛基准测试的结果产生超过1%的影响。如果有疑问,请寻求建议。
当更新Linux时外部内核模块崩溃了,是回归吗?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
不,因为“无回归”规则仅限于Linux内核提供给用户空间的接口和服务。因此,它不包括
构建或运行外部开发的内核模块,因为它们在内核空间中运行与挂进内核使用的内部接
口偶尔会变化。
如何处理安全修复引起的回归?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在极为罕见的情况下,安全问题无法在不引起回归的情况下修复;这些修复都被放弃了,
因为它们终究会引起问题。幸运的是这种两难境地基本都可以避免,受影响区域的主要
开发者以及Linus Torvalds本人通常都会努力在不引入回归的情况下解决安全问题。
如果你仍然面临此种情况,请查看邮件列表档案是否有人尽力避免过回归。如果没有,
请报告它;如有疑问,请如上所述寻求建议。
当修复回归时不可避免会引入另一个,如何处理?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
很遗憾这种事确实会出现,但幸运的是并不经常出现;如果发生了,受影响代码区的资
深开发者应当调查该问题以找到避免回归的解决方法,至少避免它们的影响。如果你遇
到这样的情况,如上所述:检查之前的讨论是否有人已经尽了最大努力,如有疑问请寻
求建议。
小提示:如果人们在每个开发周期中定期给出主线预发布(即v5.15-rc1或-rc3)以供
测试,则可以避免这种情况。为了更好地解释,可以设想一个在Linux v5.14和v5.15-rc1
之间集成的更改,该更改导致了回归,但同时是应用于5.15-rc1的其他改进的强依赖。
如果有人在5.15发布之前就发现并报告了这个问题,那么所有更改都可以直接撤销,从
而解决回归问题。而就在几天或几周后,此解决方案变成了不可能,因为一些软件可能
已经开始依赖于后续更改之一:撤销所有更改将导致上述用户软件出现回归,这是不可
接受的。
若我所依赖的功能在数月前被移除了,是回归吗?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
是的,但如前节所述,通常很难修复此类回归。因此需要逐案处理。这也是定期测试主
线预发布对所有人有好处的另一个原因。
如果我似乎是唯一受影响的人,是否仍适用“无回归”规则?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
适用,但仅限于实际使用:Linux开发人员希望能够自由地取消那些只能在阁楼和博物
馆中找到的硬件的支持。
请注意,有时为了取得进展,不得不出现回归——后者也是防止Linux停滞不前所必需
的。因此如果回归所影响的用户很少,那么为了他们和其他人更大的利益,还是让事情
过去吧。尤其是存在某种规避回归的简单方法,例如更新一些软件或者使用专门为此目
的创建的内核参数。
回归规则是否也适用于staging树中的代码?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
不,参见 `适用于所有staging代码配置选项的帮助文本
<https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/staging/Kconfig>`_ ,
其早已声明::
请注意:这些驱动正在积极开发中,可能无法正常工作,并可能包含会在不久的
将来发生变化的用户接口。
虽然staging开发人员通常坚持“无回归”的原则,但有时为了取得进展也会违背它。这就
是为什么当staging树的WiFi驱动被基本推倒重来时,有些用户不得不处理回归(通常可
以忽略)。
为什么较新版本必须“使用相似配置编译”?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
因为Linux内核开发人员有时会集成已知的会导致回归的变更,但使它们成为可选的,并
在内核的默认配置下禁用它们。这一技巧允许进步,否则“无回归”规则将导致停滞。
例如,试想一个新的可以阻止恶意软件滥用某个内核的接口的安全特性,同时又需要满足
另一个很罕见的应用程序。上述的方法可使两方都满意:使用这些应用程序的人可以关闭
新的安全功能,而其他不会遇到麻烦的人可以启用它。
如何创建与旧内核相似的配置?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
用一个已知良好的内核启动机器,并用 ``make olddefconfig`` 配置新版的Linux。这
会让内核的构建脚本从正在运行的内核中摘录配置文件(“.config”文件),作为即将编
译的新版本的基础配置;同时将所有新的配置选项设为默认值,以禁用可能导致回归的
新功能。
如何报告在预编译的普通内核中发现的回归?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
您需要确保新的内核是用与旧版相似的配置编译(见上文),因为那些构建它们的人可
能启用了一些已知的与新内核不兼容的特性。如有疑问,请向内核的提供者报告问题并
寻求建议。
用“regzbot”追踪回归的更多信息
-----------------------------
什么是回归追踪?为啥我需要关心它?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
像“无回归”这样的规则需要有人来确保它们被遵守,否则会被有意/无意打破。历史证
明了这一点对于Linux内核开发也适用。这就是为什么Linux内核的回归跟踪者Thorsten
Leemhuis,,和另一些人尽力关注所有的回归直到他们解决。他们从未为此获得报酬,
因此这项工作是在尽最大努力的基础上完成的。
为什么/如何使用机器人追踪Linux内核回归?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
由于Linux内核开发过程的分布式和松散结构,完全手动跟踪回归已经被证明是相当困难
的。因此Linux内核的回归跟踪者开发了regzbot来促进这项工作,其长期目标是尽可能为
所有相关人员自动化回归跟踪。
Regzbot通过监视跟踪的回归报告的回复来工作。此外,它还查找用“Link:”标签引用这
些报告的补丁;对这些补丁的回复也会被跟踪。结合这些数据,可以很好地了解当前修
复过程的状态。
如何查看regzbot当前追踪的回归?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
参见 `regzbot在线 <https://linux-regtracking.leemhuis.info/regzbot/>`_ 。
何种问题可以由regzbot追踪?
~~~~~~~~~~~~~~~~~~~~~~~~~~~
该机器人只为了跟踪回归,因此请不要让regzbot涉及常规问题。但是对于Linux内核的
回归跟踪者来说,让regzbot跟踪严重问题也可以,如有关挂起、损坏数据或内部错误
(Panic、Oops、BUG()、warning…)的报告。
如何修改被追踪回归的相关信息?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在直接或间接回复报告邮件时使用“regzbot命令”即可。最简单的方法是:在“已发送”文
件夹或邮件列表存档中找到报告,然后使用邮件客户端的“全部回复”功能对其进行回复。
在该邮件中的独立段落中可使用以下命令之一(即使用空行将这些命令中的一个或多个与
其余邮件文本分隔开)。
* 更新回归引入起点,例如在执行二分之后::
#regzbot introduced: 1f2e3d4c5d
* 设置或更新标题::
#regzbot title: foo
* 监视讨论或bugzilla.kernel.org上有关讨论或修复的工单::
#regzbot monitor: https://lore.kernel.org/r/30th.anniversary.repost@klaava.Helsinki.FI/
#regzbot monitor: https://bugzilla.kernel.org/show_bug.cgi?id=123456789
* 标记一个有更多相关细节的地方,例如有关但主题不同的邮件列表帖子或缺陷追踪器中的工单::
#regzbot link: https://bugzilla.kernel.org/show_bug.cgi?id=123456789
* 标记回归已失效::
#regzbot invalid: wasn't a regression, problem has always existed
Regzbot还支持其他一些主要由开发人员或回归追踪人员使用的命令。命令的更多细节请
参考 `入门指南 <https://gitlab.com/knurd42/regzbot/-/blob/main/docs/getting_started.md>`_
和 `参考手册 <https://gitlab.com/knurd42/regzbot/-/blob/main/docs/reference.md>`_ 。
..
正文结束
..
如本文件开头所述,本文以GPL-2.0+或CC-BY-4.0许可发行。如您想仅在CC-BY-4.0许
可下重分发本文,请用“Linux内核开发者”作为作者,并用如下链接作为来源:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/plain/Documentation/translations/zh_CN/admin-guide/reporting-regressions.rst
..
注意:本RST文件内容只有在来自Linux内核源代码时是使用CC-BY-4.0许可的,因为经
过处理的版本(如经内核的构建系统)可能包含来自使用更严格许可证的文件的内容。
@@ -0,0 +1,280 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/sysrq.rst
:翻译:
黄军华 Junhua Huang <huang.junhua@zte.com.cn>
:校译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_admin-guide_sysrq:
Linux 魔法系统请求键骇客
========================
针对 sysrq.c 的文档说明
什么是魔法 SysRq 键?
~~~~~~~~~~~~~~~~~~~~~
它是一个你可以输入的具有魔法般的组合键。
无论内核在做什么,内核都会响应 SysRq 键的输入,除非内核完全卡死。
如何使能魔法 SysRq 键?
~~~~~~~~~~~~~~~~~~~~~~~
在配置内核时,我们需要设置 'Magic SysRq key (CONFIG_MAGIC_SYSRQ)' 为 'Y'。
当运行一个编译进 sysrq 功能的内核时,/proc/sys/kernel/sysrq 控制着被
SysRq 键调用的功能许可。这个文件的默认值由 CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE
配置符号设定,文件本身默认设置为 1。以下是 /proc/sys/kernel/sysrq 中可能的
值列表:
- 0 - 完全不使能 SysRq 键
- 1 - 使能 SysRq 键的全部功能
- >1 - 对于允许的 SysRq 键功能的比特掩码(参见下面更详细的功能描述)::
2 = 0x2 - 使能对控制台日志记录级别的控制
4 = 0x4 - 使能对键盘的控制 (SAK, unraw)
8 = 0x8 - 使能对进程的调试导出等
16 = 0x10 - 使能同步命令
32 = 0x20 - 使能重新挂载只读
64 = 0x40 - 使能对进程的信号操作 (term, kill, oom-kill)
128 = 0x80 - 允许重启、断电
256 = 0x100 - 允许让所有实时任务变普通任务
你可以通过如下命令把值设置到这个文件中::
echo "number" >/proc/sys/kernel/sysrq
这里被写入的 number 可以是 10 进制数,或者是带着 0x 前缀的 16 进制数。
CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE 必须是以 16 进制数写入。
注意,``/proc/sys/kernel/sysrq`` 的值只影响通过键盘触发 SySRq 的调用,对于
通过 ``/proc/sysrq-trigger`` 的任何操作调用都是允许的
(通过具有系统权限的用户)。
如何使用魔法 SysRq 键?
~~~~~~~~~~~~~~~~~~~~~~~
在 x86 架构上
你可以按下键盘组合键 `ALT-SysRq-<command key>`。
.. note::
一些键盘可能没有标识 'SySRq' 键。'SySRq' 键也被当做 'Print Screen'键。
同时有些键盘无法处理同时按下这么多键,因此你可以先按下键盘 `Alt` 键,
然后按下键盘 `SysRq` 键,再释放键盘 `SysRq` 键,之后按下键盘上命令键
`<command key>`,最后释放所有键。
在 SPARC 架构上
你可以按下键盘组合键 `ALT-STOP-<command key>` 。
在串行控制台(只针对 PC 类型的标准串口)
你可以发一个 ``BREAK`` ,然后在 5 秒内发送一个命令键,
发送 ``BREAK`` 两次将被翻译为一个正常的 BREAK 操作。
在 PowerPC 架构上
按下键盘组合键 `ALT - Print Screen` (或者 `F13`) - `<命令键>` 。
`Print Screen` (或者 `F13`) - `<命令键>` 或许也能实现。
在其他架构上
如果你知道其他架构的组合键,请告诉我,我可以把它们添加到这部分。
在所有架构上
写一个字符到 /proc/sysrq-trigger 文件,例如::
echo t > /proc/sysrq-trigger
这个命令键 `<command key>` 是区分大小写的。
什么是命令键?
~~~~~~~~~~~~~~
=========== ================================================================
命令键 功能
=========== ================================================================
``b`` 将立即重启系统,不会同步或者卸载磁盘。
``c`` 将执行系统 crash,如果配置了系统 crashdump,将执行 crashdump。
``d`` 显示所有持有的锁。
``e`` 发送 SIGTERM 信号给所有进程,除了 init 进程。
``f`` 将调用 oom killer 杀掉一个过度占用内存的进程,如果什么任务都没杀,
也不会 panic。
``g`` kgdb 使用(内核调试器)。
``h`` 将会显示帮助。(实际上除了这里列举的键,其他的都将显示帮助,
但是 ``h`` 容易记住):-)
``i`` 发送 SIGKILL 给所有进程,除了 init 进程。
``j`` 强制性的 “解冻它” - 用于被 FIFREEZE ioctl 操作冻住的文件系统。
``k`` 安全访问秘钥(SAK)杀掉在当前虚拟控制台的所有程序,注意:参考
下面 SAK 节重要论述。
``l`` 显示所有活动 cpu 的栈回溯。
``m`` 将导出当前内存信息到你的控制台。
``n`` 用于使所有实时任务变成普通任务。
``o`` 将关闭系统(如果配置和支持的话)。
``p`` 将导出当前寄存器和标志位到控制台。
``q`` 将导出每个 cpu 上所有已装备的高精度定时器(不是完整的
time_list 文件显示的 timers)和所有时钟事件设备的详细信息。
``r`` 关闭键盘的原始模式,设置为转换模式。
``s`` 将尝试同步所有的已挂载文件系统。
``t`` 将导出当前所有任务列表和它们的信息到控制台。
``u`` 将尝试重新挂载已挂载文件系统为只读。
``v`` 强制恢复帧缓存控制台。
``v`` 触发 ETM 缓存导出 [ARM 架构特有]
``w`` 导出处于不可中断状态(阻塞)的任务。
``x`` 在 ppc/powerpc 架构上用于 xmon 接口。
在 sparc64 架构上用于显示全局的 PMU(性能监控单元)寄存器。
在 MIPS 架构上导出所有的 tlb 条目。
``y`` 显示全局 cpu 寄存器 [SPARC-64 架构特有]
``z`` 导出 ftrace 缓存信息
``0``-``9`` 设置控制台日志级别,该级别控制什么样的内核信息将被打印到你的
控制台。(比如 ``0`` ,将使得只有紧急信息,像 PANICs or OOPSes
才能到你的控制台。)
=========== ================================================================
好了,我能用他们做什么呢?
~~~~~~~~~~~~~~~~~~~~~~~~~~
嗯,当你的 X 服务端或者 svgalib 程序崩溃,unraw(r) 非原始模式命令键是非常
方便的。
sak(k)(安全访问秘钥)在你尝试登陆的同时,又想确保当前控制台没有可以获取你的
密码的特洛伊木马程序运行时是有用的。它会杀掉给定控制台的所有程序,这样你
就可以确认当前的登陆提示程序是实际来自 init 进程的程序,而不是某些特洛伊
木马程序。
.. important::
在其实际的形式中,在兼容 C2 安全标准的系统上,它不是一个真正的 SAK,
它也不应该误认为此。
似乎其他人发现其可以作为(系统终端联机键)当你想退出一个程序,
同时不会让你切换控制台的方法。(比如,X 服务端或者 svgalib 程序)
``reboot(b)`` 是个好方法,当你不能关闭机器时,它等同于按下"复位"按钮。
``crash(c)`` 可以用于手动触发一个 crashdump,当系统卡住时。
注意当 crashdump 机制不可用时,这个只是触发一个内核 crash。
``sync(s)`` 在拔掉可移动介质之前,或者在使用不提供优雅关机的
救援 shell 之后很方便 -- 它将确保你的数据被安全地写入磁盘。注意,在你看到
屏幕上出现 "OK" 和 "Done" 之前,同步还没有发生。
``umount(u)`` 可以用来标记文件系统正常卸载,从正在运行的系统角度来看,它们将
被重新挂载为只读。这个重新挂载动作直到你看到 "OK" 和 "Done" 信息出现在屏幕上
才算完成。
日志级别 ``0`` - ``9`` 用于当你的控制台被大量的内核信息冲击,你不想看见的时候。
选择 ``0`` 将禁止除了最紧急的内核信息外的所有的内核信息输出到控制台。(但是如果
syslogd/klogd 进程是运行的,它们仍将被记录。)
``term(e)`` 和 ``kill(i)`` 用于当你有些有点失控的进程,你无法通过其他方式杀掉
它们的时候,特别是它正在创建其他进程。
"just thaw ``it(j)`` " 用于当你的系统由于一个 FIFREEZE ioctl 调用而产生的文件
系统冻结,而导致的不响应时。
有的时候 SysRq 键在使用它之后,看起来像是“卡住”了,我能做些什么?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这也会发生在我这,我发现轻敲键盘两侧的 shift、alt 和 control 键,然后再次敲击
一个无效的 SysRq 键序列可以解决问题。(比如,像键盘组合键 `alt-sysrq-z` )
切换到另一个虚拟控制台(键盘操作 `ALT+Fn` ),然后再切回来应该也有帮助。
我敲击了 SysRq 键,但像是什么都没发生,发生了什么错误?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
有一些键盘对于 SysRq 键设置了不同的键值,而不是提前定义的 99
(查看在 ``include/uapi/linux/input-event-codes.h`` 文件中 ``KEY_SYSRQ`` 的定义)
或者就根本没有 SysRq 键。在这些场景下,执行 ``showkey -s`` 命令来找到一个合适
的扫描码序列,然后使用 ``setkeycodes <sequence> 99`` 命令映射这个序列值到通用
的 SysRq 键编码上(比如 ``setkeycodes e05b 99`` )。最好将这个命令放在启动脚本
中。
哦,顺便说一句,你十秒钟不输入任何东西就将退出 “showkey”。
我想添加一个 SysRq 键事件到一个模块中,如何去做呢?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
为了注册一个基础函数到这个表中,首先你必须包含 ``include/linux/sysrq.h`` 头
文件,这个头文件定义了你所需要的所有东西。然后你必须创建一个 ``sysrq_key_op``
结构体,然后初始化它,使用如下内容,A) 你将使用的这个键的处理函数, B) 一个
help_msg 字符串,在 SysRq 键打印帮助信息时将打印出来,C) 一个 action_msg 字
符串,就在你的处理函数调用前打印出来。你的处理函数必须符合在 'sysrq.h' 文件中
的函数原型。
在 ``sysrq_key_op`` 结构体被创建后,你可以调用内核函数
``register_sysrq_key(int key, const struct sysrq_key_op *op_p);``,
该函数在表中的 'key' 对应位置内容是空的情况下,将通过 ``op_p`` 指针注册这个操作
函数到表中 'key' 对应位置上。在模块卸载的时候,你必须调用
``unregister_sysrq_key(int key, const struct sysrq_key_op *op_p)`` 函数,该函数
只有在当前该键对应的处理函数被注册到了 'key' 对应位置时,才会移除 'op_p' 指针
对应的键值操作函数。这是为了防止在你注册之后,该位置被改写的情况。
魔法 SysRq 键系统的工作原理是将键对应操作函数注册到键的操作查找表,
该表定义在 'drivers/tty/sysrq.c' 文件中。
该键表有许多在编译时候就注册进去的操作函数,但是是可变的。
并且有两个函数作为操作该表的接口被导出::
register_sysrq_key 和 unregister_sysrq_key.
当然,永远不要在表中留下无效指针,即,当你的模块存在调用 register_sysrq_key()
函数,它一定要调用 unregister_sysrq_key() 来清除它使用过的 SysRq 键表条目。
表中的空指针是安全的。:)
如果对于某种原因,在 handle_sysrq 调用的处理函数中,你认为有必要调用
handle_sysrq 函数时,你必须意识到当前你处于一个锁中(你同时也处于一个中断处理
函数中,这意味着不能睡眠)。所以这时你必须使用 ``__handle_sysrq_nolock`` 替代。
当我敲击一个 SysRq 组合键时,只有标题打印出现在控制台?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
SysRq 键的输出和所有其他控制台输出一样,受制于控制台日志级别控制。
这意味着,如果内核以发行版内核中常见的 "quiet" 方式启动,则输出可能不会出现在实际
的控制台上,即使它会出现在 dmesg 缓存中,也可以通过 dmesg 命令和 ``/proc/kmsg``
文件的消费访问到。作为一个特例,来自 sysrq 命令的标题行将被传递给所有控制台
使用者,就好像当前日志级别是最大的一样。如果只发出标题头,则几乎可以肯定内核日志
级别太低。如果你需要控制台上的输出,那么你将需要临时提高控制台日志级别,通过使用
键盘组合键 `alt-sysrq-8` 或者::
echo 8 > /proc/sysrq-trigger
在触发了你感兴趣的 SysRq 键命令后,记得恢复日志级别到正常情况。
我有很多问题时,可以请教谁?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
请教在内核邮件列表上的人,邮箱:
linux-kernel@vger.kernel.org
致谢
~~~~
- Mydraal <vulpyne@vulpyne.net> 撰写了该文件
- Adam Sulmicki <adam@cfar.umd.edu> 进行了更新
- Jeremy M. Dolan <jmd@turbogeek.org> 在 2001/01/28 10:15:59 进行了更新
- Crutcher Dunnavant <crutcher+kernel@datastacks.com> 添加键注册部分
@@ -0,0 +1,157 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: :doc:`../../../admin-guide/tainted-kernels`
:译者:
吴想成 Wu XiangCheng <bobwxc@email.cn>
受污染的内核
-------------
当发生一些在稍后调查问题时可能相关的事件时,内核会将自己标记为“受污染
(tainted)”的。不用太过担心,大多数情况下运行受污染的内核没有问题;这些信息
主要在有人想调查某个问题时才有意义的,因为问题的真正原因可能是导致内核受污染
的事件。这就是为什么来自受污染内核的缺陷报告常常被开发人员忽略,因此请尝试用
未受污染的内核重现问题。
请注意,即使在您消除导致污染的原因(亦即卸载专有内核模块)之后,内核仍将保持
污染状态,以表示内核仍然不可信。这也是为什么内核在注意到内部问题(“kernel
bug”)、可恢复错误(“kernel oops”)或不可恢复错误(“kernel panic”)时会打印
受污染状态,并将有关此的调试信息写入日志 ``dmesg`` 输出。也可以通过
``/proc/`` 中的文件在运行时检查受污染的状态。
BUG、Oops或Panics消息中的污染标志
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
在顶部以“CPU:”开头的一行中可以找到受污染的状态;内核是否受到污染和原因会显示
在进程ID(“PID:”)和触发事件命令的缩写名称(“Comm:”)之后::
BUG: unable to handle kernel NULL pointer dereference at 0000000000000000
Oops: 0002 [#1] SMP PTI
CPU: 0 PID: 4424 Comm: insmod Tainted: P W O 4.20.0-0.rc6.fc30 #1
Hardware name: Red Hat KVM, BIOS 0.5.1 01/01/2011
RIP: 0010:my_oops_init+0x13/0x1000 [kpanic]
[...]
如果内核在事件发生时没有被污染,您将在那里看到“Not-tainted:”;如果被污染,那
么它将是“Tainted:”以及字母或空格。在上面的例子中,它看起来是这样的::
Tainted: P W O
下表解释了这些字符的含义。在本例中,由于加载了专有模块( ``P`` ),出现了
警告( ``W`` ),并且加载了外部构建的模块( ``O`` ),所以内核早些时候受到
了污染。要解码其他字符,请使用下表。
解码运行时的污染状态
~~~~~~~~~~~~~~~~~~~~~
在运行时,您可以通过读取 ``cat /proc/sys/kernel/tainted`` 来查询受污染状态。
如果返回 ``0`` ,则内核没有受到污染;任何其他数字都表示受到污染的原因。解码
这个数字的最简单方法是使用脚本 ``tools/debugging/kernel-chktaint`` ,您的
发行版可能会将其作为名为 ``linux-tools`` 或 ``kernel-tools`` 的包的一部分提
供;如果没有,您可以从
`git.kernel.org <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/plain/tools/debugging/kernel-chktaint>`_
网站下载此脚本并用 ``sh kernel-chktaint`` 执行,它会在上面引用的日志中有类似
语句的机器上打印这样的内容::
Kernel is Tainted for following reasons:
* Proprietary module was loaded (#0)
* Kernel issued warning (#9)
* Externally-built ('out-of-tree') module was loaded (#12)
See Documentation/admin-guide/tainted-kernels.rst in the Linux kernel or
https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html for
a more details explanation of the various taint flags.
Raw taint value as int/string: 4609/'P W O '
你也可以试着自己解码这个数字。如果内核被污染的原因只有一个,那么这很简单,
在本例中您可以通过下表找到数字。如果你需要解码有多个原因的数字,因为它是一
个位域(bitfield),其中每个位表示一个特定类型的污染的存在或不存在,最好让
前面提到的脚本来处理。但是如果您需要快速看一下,可以使用这个shell命令来检查
设置了哪些位::
$ for i in $(seq 18); do echo $(($i-1)) $(($(cat /proc/sys/kernel/tainted)>>($i-1)&1));done
污染状态代码表
~~~~~~~~~~~~~~~
=== ===== ====== ========================================================
位 日志 数字 内核被污染的原因
=== ===== ====== ========================================================
0 G/P 1 已加载专用模块
1 _/F 2 模块被强制加载
2 _/S 4 内核运行在不合规范的系统上
3 _/R 8 模块被强制卸载
4 _/M 16 处理器报告了机器检测异常(MCE)
5 _/B 32 引用了错误的页或某些意外的页标志
6 _/U 64 用户空间应用程序请求的污染
7 _/D 128 内核最近死机了,即曾出现OOPS或BUG
8 _/A 256 ACPI表被用户覆盖
9 _/W 512 内核发出警告
10 _/C 1024 已加载staging驱动程序
11 _/I 2048 已应用平台固件缺陷的解决方案
12 _/O 4096 已加载外部构建(“树外”)模块
13 _/E 8192 已加载未签名的模块
14 _/L 16384 发生软锁定
15 _/K 32768 内核已实时打补丁
16 _/X 65536 备用污染,为发行版定义并使用
17 _/T 131072 内核是用结构随机化插件构建的
=== ===== ====== ========================================================
注:字符 ``_`` 表示空白,以便于阅读表。
污染的更详细解释
~~~~~~~~~~~~~~~~~
0) ``G`` 加载的所有模块都有GPL或兼容许可证, ``P`` 加载了任何专有模块。
没有MODULE_LICENSE(模块许可证)或MODULE_LICENSE未被insmod认可为GPL
兼容的模块被认为是专有的。
1) ``F`` 任何模块被 ``insmod -f`` 强制加载, ``' '`` 所有模块正常加载。
2) ``S`` 内核运行在不合规范的处理器或系统上:硬件已运行在不受支持的配置中,
因此无法保证正确执行。内核将被污染,例如:
- 在x86上:PAE是通过intel CPU(如Pentium M)上的forcepae强制执行的,这些
CPU不报告PAE,但可能有功能实现,SMP内核在非官方支持的SMP Athlon CPU上
运行,MSR被暴露到用户空间中。
- 在arm上:在某些CPU(如Keystone 2)上运行的内核,没有启用某些内核特性。
- 在arm64上:CPU之间存在不匹配的硬件特性,引导加载程序以不同的模式引导CPU。
- 某些驱动程序正在被用在不受支持的体系结构上(例如x86_64以外的其他系统
上的scsi/snic,非x86/x86_64/itanium上的scsi/ips,已经损坏了arm64上
irqchip/irq-gic的固件设置…)。
3) ``R`` 模块被 ``rmmod -f`` 强制卸载, ``' '`` 所有模块都正常卸载。
4) ``M`` 任何处理器报告了机器检测异常, ``' '`` 未发生机器检测异常。
5) ``B`` 页面释放函数发现错误的页面引用或某些意外的页面标志。这表示硬件问题
或内核错误;日志中应该有其他信息指示发生此污染的原因。
6) ``U`` 用户或用户应用程序特意请求设置受污染标志,否则应为 ``' '`` 。
7) ``D`` 内核最近死机了,即出现了OOPS或BUG。
8) ``A`` ACPI表被重写。
9) ``W`` 内核之前已发出过警告(尽管有些警告可能会设置更具体的污染标志)。
10) ``C`` 已加载staging驱动程序。
11) ``I`` 内核正在处理平台固件(BIOS或类似软件)中的严重错误。
12) ``O`` 已加载外部构建(“树外”)模块。
13) ``E`` 在支持模块签名的内核中加载了未签名的模块。
14) ``L`` 系统上先前发生过软锁定。
15) ``K`` 内核已经实时打了补丁。
16) ``X`` 备用污染,由Linux发行版定义和使用。
17) ``T`` 内核构建时使用了randstruct插件,它可以有意生成非常不寻常的内核结构
布局(甚至是性能病态的布局),这在调试时非常有用。于构建时设置。
@@ -0,0 +1,170 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/admin-guide/unicode.rst
:译者:
吴想成 Wu XiangCheng <bobwxc@email.cn>
Unicode(统一码)支持
======================
(英文版)上次更新:2005-01-17,版本号 1.4
此文档由H. Peter Anvin <unicode@lanana.org>管理,是Linux注册名称与编号管理局
(Linux Assigned Names And Numbers Authority,LANANA)项目的一部分。
现行版本请见:
http://www.lanana.org/docs/unicode/admin-guide/unicode.rst
简介
-----
Linux内核代码已被重写以使用Unicode来将字符映射到字体。下载一个Unicode到字体
(Unicode-to-font)表,八位字符集与UTF-8模式都将改用此字体来显示。
这微妙地改变了八位字符表的语义。现在的四个字符表是:
=============== =============================== ================
映射代号 映射名称 Escape代码 (G0)
=============== =============================== ================
LAT1_MAP Latin-1 (ISO 8859-1) ESC ( B
GRAF_MAP DEC VT100 pseudographics ESC ( 0
IBMPC_MAP IBM code page 437 ESC ( U
USER_MAP User defined ESC ( K
=============== =============================== ================
特别是 ESC ( U 不再是“直通字体”,因为字体可能与IBM字符集完全不同。
例如,即使加载了一个Latin-1字体,也允许使用块图形(block graphics)。
请注意,尽管这些代码与ISO 2022类似,但这些代码及其用途都与ISO 2022不匹配;
Linux有两个八位代码(G0和G1),而ISO 2022有四个七位代码(G0-G3)。
根据Unicode标准/ISO 10646,U+F000到U+F8FF被保留用于操作系统范围内的分配
(Unicode标准将其称为“团体区域(Corporate Zone)”,因为这对于Linux是不准确
的,所以我们称之为“Linux区域”)。选择U+F000作为起点,因为它允许直接映射
区域以2的大倍数开始(以防需要1024或2048个字符的字体)。这就留下U+E000到
U+EFFF作为最终用户区。
[v1.2]:Unicodes范围从U+F000到U+F7FF已经被硬编码为直接映射到加载的字体,
绕过了翻译表。用户定义的映射现在默认为U+F000到U+F0FF,模拟前述行为。实际上,
此范围可能较短;例如,vgacon只能处理256字符(U+F000..U+F0FF)或512字符
(U+F000..U+F1FF)字体。
Linux 区域中定义的实际字符
---------------------------
此外,还定义了Unicode 1.1.4中不存在的以下字符;这些字符由DEC VT图形映射使用。
[v1.2]此用法已过时,不应再使用;请参见下文。
====== ======================================
U+F800 DEC VT GRAPHICS HORIZONTAL LINE SCAN 1
U+F801 DEC VT GRAPHICS HORIZONTAL LINE SCAN 3
U+F803 DEC VT GRAPHICS HORIZONTAL LINE SCAN 7
U+F804 DEC VT GRAPHICS HORIZONTAL LINE SCAN 9
====== ======================================
DEC VT220使用6x10字符矩阵,这些字符在DEC VT图形字符集中形成一个平滑的过渡。
我省略了扫描5行,因为它也被用作块图形字符,因此被编码为U+2500 FORMS LIGHT
HORIZONTAL。
[v1.3]:这些字符已正式添加到Unicode 3.2.0中;它们在U+23BA、U+23BB、U+23BC、
U+23BD处添加。Linux现在使用新值。
[v1.2]:添加了以下字符来表示常见的键盘符号,这些符号不太可能被添加到Unicode
中,因为它们非常讨厌地取决于特定供应商。当然,这是糟糕设计的一个好例子。
====== ======================================
U+F810 KEYBOARD SYMBOL FLYING FLAG
U+F811 KEYBOARD SYMBOL PULLDOWN MENU
U+F812 KEYBOARD SYMBOL OPEN APPLE
U+F813 KEYBOARD SYMBOL SOLID APPLE
====== ======================================
克林贡(Klingon)语支持
------------------------
1996年,Linux是世界上第一个添加对人工语言克林贡支持的操作系统,克林贡是由
Marc Okrand为《星际迷航》电视连续剧创造的。这种编码后来被征募Unicode注册表
(ConScript Unicode Registry,CSUR)采用,并建议(但最终被拒绝)纳入Unicode
平面一。不过,它仍然是Linux区域中的Linux/CSUR私有分配。
这种编码已经得到克林贡语言研究所(Klingon Language Institute)的认可。
有关更多信息,请联系他们:
http://www.kli.org/
由于Linux CZ开头部分的字符大多是dingbats/symbols/forms类型,而且这是一种
语言,因此根据标准Unicode惯例,我将它放置在16单元的边界上。
.. note::
这个范围现在由征募Unicode注册表正式管理。规范性引用文件为:
https://www.evertype.com/standards/csur/klingon.html
克林贡语有一个26个字符的字母表,一个10位数的位置数字书写系统,从左到右
,从上到下书写。
克林贡字母的几种字形已经被提出。但是由于这组符号看起来始终是一致的,只有实际
的形状不同,因此按照标准Unicode惯例,这些差异被认为是字体变体。
====== =======================================================
U+F8D0 KLINGON LETTER A
U+F8D1 KLINGON LETTER B
U+F8D2 KLINGON LETTER CH
U+F8D3 KLINGON LETTER D
U+F8D4 KLINGON LETTER E
U+F8D5 KLINGON LETTER GH
U+F8D6 KLINGON LETTER H
U+F8D7 KLINGON LETTER I
U+F8D8 KLINGON LETTER J
U+F8D9 KLINGON LETTER L
U+F8DA KLINGON LETTER M
U+F8DB KLINGON LETTER N
U+F8DC KLINGON LETTER NG
U+F8DD KLINGON LETTER O
U+F8DE KLINGON LETTER P
U+F8DF KLINGON LETTER Q
- Written <q> in standard Okrand Latin transliteration
U+F8E0 KLINGON LETTER QH
- Written <Q> in standard Okrand Latin transliteration
U+F8E1 KLINGON LETTER R
U+F8E2 KLINGON LETTER S
U+F8E3 KLINGON LETTER T
U+F8E4 KLINGON LETTER TLH
U+F8E5 KLINGON LETTER U
U+F8E6 KLINGON LETTER V
U+F8E7 KLINGON LETTER W
U+F8E8 KLINGON LETTER Y
U+F8E9 KLINGON LETTER GLOTTAL STOP
U+F8F0 KLINGON DIGIT ZERO
U+F8F1 KLINGON DIGIT ONE
U+F8F2 KLINGON DIGIT TWO
U+F8F3 KLINGON DIGIT THREE
U+F8F4 KLINGON DIGIT FOUR
U+F8F5 KLINGON DIGIT FIVE
U+F8F6 KLINGON DIGIT SIX
U+F8F7 KLINGON DIGIT SEVEN
U+F8F8 KLINGON DIGIT EIGHT
U+F8F9 KLINGON DIGIT NINE
U+F8FD KLINGON COMMA
U+F8FE KLINGON FULL STOP
U+F8FF KLINGON SYMBOL FOR EMPIRE
====== =======================================================
其他虚构和人工字母
-------------------
自从分配了克林贡Linux Unicode块之后,John Cowan <jcowan@reutershealth.com>
和 Michael Everson <everson@evertype.com> 建立了一个虚构和人工字母的注册表。
征募Unicode注册表请访问:
https://www.evertype.com/standards/csur/
所使用的范围位于最终用户区域的低端,因此无法进行规范化分配,但建议希望对虚构
字母进行编码的人员使用这些代码,以实现互操作性。对于克林贡语,CSUR采用了Linux
编码。CSUR的人正在推动将Tengwar和Cirth添加到Unicode平面一;将克林贡添加到
Unicode平面一被拒绝,因此上述编码仍然是官方的。
@@ -0,0 +1,175 @@
Chinese translated version of Documentation/arch/arm/booting.rst
If you have any comment or update to the content, please contact the
original document maintainer directly. However, if you have a problem
communicating in English you can also ask the Chinese maintainer for
help. Contact the Chinese maintainer if this translation is outdated
or if there is a problem with the translation.
Maintainer: Russell King <linux@arm.linux.org.uk>
Chinese maintainer: Fu Wei <tekkamanninja@gmail.com>
---------------------------------------------------------------------
Documentation/arch/arm/booting.rst 的中文翻译
如果想评论或更新本文的内容,请直接联系原文档的维护者。如果你使用英文
交流有困难的话,也可以向中文版维护者求助。如果本翻译更新不及时或者翻
译存在问题,请联系中文版维护者。
英文版维护者: Russell King <linux@arm.linux.org.uk>
中文版维护者: 傅炜 Fu Wei <tekkamanninja@gmail.com>
中文版翻译者: 傅炜 Fu Wei <tekkamanninja@gmail.com>
中文版校译者: 傅炜 Fu Wei <tekkamanninja@gmail.com>
以下为正文
---------------------------------------------------------------------
启动 ARM Linux
==============
作者:Russell King
日期:2002年5月18日
以下文档适用于 2.4.18-rmk6 及以上版本。
为了启动 ARM Linux,你需要一个引导装载程序(boot loader),
它是一个在主内核启动前运行的一个小程序。引导装载程序需要初始化各种
设备,并最终调用 Linux 内核,将信息传递给内核。
从本质上讲,引导装载程序应提供(至少)以下功能:
1、设置和初始化 RAM。
2、初始化一个串口。
3、检测机器的类型(machine type)。
4、设置内核标签列表(tagged list)。
5、调用内核映像。
1、设置和初始化 RAM
-------------------
现有的引导加载程序: 强制
新开发的引导加载程序: 强制
引导装载程序应该找到并初始化系统中所有内核用于保持系统变量数据的 RAM。
这个操作的执行是设备依赖的。(它可能使用内部算法来自动定位和计算所有
RAM,或可能使用对这个设备已知的 RAM 信息,还可能使用任何引导装载程序
设计者想到的匹配方法。)
2、初始化一个串口
-----------------------------
现有的引导加载程序: 可选、建议
新开发的引导加载程序: 可选、建议
引导加载程序应该初始化并使能一个目标板上的串口。这允许内核串口驱动
自动检测哪个串口用于内核控制台。(一般用于调试或与目标板通信。)
作为替代方案,引导加载程序也可以通过标签列表传递相关的'console='
选项给内核以指定某个串口,而串口数据格式的选项在以下文档中描述:
Documentation/admin-guide/kernel-parameters.rst。
3、检测机器类型
--------------------------
现有的引导加载程序: 可选
新开发的引导加载程序: 强制
引导加载程序应该通过某些方式检测自身所处的机器类型。这是一个硬件
代码或通过查看所连接的硬件用某些算法得到,这些超出了本文档的范围。
引导加载程序最终必须能提供一个 MACH_TYPE_xxx 值给内核。
(详见 linux/arch/arm/tools/mach-types )。
4、设置启动数据
------------------
现有的引导加载程序: 可选、强烈建议
新开发的引导加载程序: 强制
引导加载程序必须提供标签列表或者 dtb 映像以传递配置数据给内核。启动
数据的物理地址通过寄存器 r2 传递给内核。
4a、设置内核标签列表
--------------------------------
bootloader 必须创建和初始化内核标签列表。一个有效的标签列表以
ATAG_CORE 标签开始,并以 ATAG_NONE 标签结束。ATAG_CORE 标签可以是
空的,也可以是非空。一个空 ATAG_CORE 标签其 size 域设置为
‘2’(0x00000002)。ATAG_NONE 标签的 size 域必须设置为零。
在列表中可以保存任意数量的标签。对于一个重复的标签是追加到之前标签
所携带的信息之后,还是会覆盖原来的信息,是未定义的。某些标签的行为
是前者,其他是后者。
bootloader 必须传递一个系统内存的位置和最小值,以及根文件系统位置。
因此,最小的标签列表如下所示:
+-----------+
基地址 -> | ATAG_CORE | |
+-----------+ |
| ATAG_MEM | | 地址增长方向
+-----------+ |
| ATAG_NONE | |
+-----------+ v
标签列表应该保存在系统的 RAM 中。
标签列表必须置于内核自解压和 initrd'bootp' 程序都不会覆盖的内存区。
建议放在 RAM 的头 16KiB 中。
4b、设置设备树
-------------------------
bootloader 必须以 64bit 地址对齐的形式加载一个设备树映像(dtb)到系统
RAM 中,并用启动数据初始化它。dtb 格式在文档
https://www.devicetree.org/specifications/ 中。内核将会在
dtb 物理地址处查找 dtb 魔数值(0xd00dfeed),以确定 dtb 是否已经代替
标签列表被传递进来。
bootloader 必须传递一个系统内存的位置和最小值,以及根文件系统位置。
dtb 必须置于内核自解压不会覆盖的内存区。建议将其放置于 RAM 的头 16KiB
中。但是不可将其放置于“0”物理地址处,因为内核认为:r2 中为 0,意味着
没有标签列表和 dtb 传递过来。
5、调用内核映像
---------------------------
现有的引导加载程序: 强制
新开发的引导加载程序: 强制
调用内核映像 zImage 有两个选择。如果 zImge 保存在 flash 中,且是为了
在 flash 中直接运行而被正确链接的。这样引导加载程序就可以在 flash 中
直接调用 zImage。
zImage 也可以被放在系统 RAM(任意位置)中被调用。注意:内核使用映像
基地址的前 16KB RAM 空间来保存页表。建议将映像置于 RAM 的 32KB 处。
对于以上任意一种情况,都必须符合以下启动状态:
- 停止所有 DMA 设备,这样内存数据就不会因为虚假网络包或磁盘数据而被破坏。
这可能可以节省你许多的调试时间。
- CPU 寄存器配置
r0 = 0,
r1 = (在上面 3 中获取的)机器类型码。
r2 = 标签列表在系统 RAM 中的物理地址,或
设备树块(dtb)在系统 RAM 中的物理地址
- CPU 模式
所有形式的中断必须被禁止 (IRQs 和 FIQs)
CPU 必须处于 SVC 模式。(对于 Angel 调试有特例存在)
- 缓存,MMUs
MMU 必须关闭。
指令缓存开启或关闭都可以。
数据缓存必须关闭。
- 引导加载程序应该通过直接跳转到内核映像的第一条指令来调用内核映像。
对于支持 ARM 指令集的 CPU,跳入内核入口时必须处在 ARM 状态,即使
对于 Thumb-2 内核也是如此。
对于仅支持 Thumb 指令集的 CPU,比如 Cortex-M 系列的 CPU,跳入
内核入口时必须处于 Thumb 状态。
@@ -0,0 +1,284 @@
Chinese translated version of Documentation/arch/arm/kernel_user_helpers.rst
If you have any comment or update to the content, please contact the
original document maintainer directly. However, if you have a problem
communicating in English you can also ask the Chinese maintainer for
help. Contact the Chinese maintainer if this translation is outdated
or if there is a problem with the translation.
Maintainer: Nicolas Pitre <nicolas.pitre@linaro.org>
Dave Martin <dave.martin@linaro.org>
Chinese maintainer: Fu Wei <tekkamanninja@gmail.com>
---------------------------------------------------------------------
Documentation/arch/arm/kernel_user_helpers.rst 的中文翻译
如果想评论或更新本文的内容,请直接联系原文档的维护者。如果你使用英文
交流有困难的话,也可以向中文版维护者求助。如果本翻译更新不及时或者翻
译存在问题,请联系中文版维护者。
英文版维护者: Nicolas Pitre <nicolas.pitre@linaro.org>
Dave Martin <dave.martin@linaro.org>
中文版维护者: 傅炜 Fu Wei <tekkamanninja@gmail.com>
中文版翻译者: 傅炜 Fu Wei <tekkamanninja@gmail.com>
中文版校译者: 宋冬生 Dongsheng Song <dongshneg.song@gmail.com>
傅炜 Fu Wei <tekkamanninja@gmail.com>
以下为正文
---------------------------------------------------------------------
内核提供的用户空间辅助代码
=========================
在内核内存空间的固定地址处,有一个由内核提供并可从用户空间访问的代码
段。它用于向用户空间提供因在许多 ARM CPU 中未实现的特性和/或指令而需
内核提供帮助的某些操作。这些代码直接在用户模式下执行的想法是为了获得
最佳效率,但那些与内核计数器联系过于紧密的部分,则被留给了用户库实现。
事实上,此代码甚至可能因不同的 CPU 而异,这取决于其可用的指令集或它
是否为 SMP 系统。换句话说,内核保留在不作出警告的情况下根据需要更改
这些代码的权利。只有本文档描述的入口及其结果是保证稳定的。
这与完全成熟的 VDSO 实现不同(但两者并不冲突),尽管如此,VDSO 可阻止
某些通过常量高效跳转到那些代码段的汇编技巧。且由于那些代码段在返回用户
代码前仅使用少量的代码周期,则一个 VDSO 间接远程调用将会在这些简单的
操作上增加一个可测量的开销。
在对那些拥有原生支持的新型处理器进行代码优化时,仅在已为其他操作使用
了类似的新增指令,而导致二进制结果已与早期 ARM 处理器不兼容的情况下,
用户空间才应绕过这些辅助代码,并在内联函数中实现这些操作(无论是通过
编译器在代码中直接放置,还是作为库函数调用实现的一部分)。也就是说,
如果你编译的代码不会为了其他目的使用新指令,则不要仅为了避免使用这些
内核辅助代码,导致二进制程序无法在早期处理器上运行。
新的辅助代码可能随着时间的推移而增加,所以新内核中的某些辅助代码在旧
内核中可能不存在。因此,程序必须在对任何辅助代码调用假设是安全之前,
检测 __kuser_helper_version 的值(见下文)。理想情况下,这种检测应该
只在进程启动时执行一次;如果内核版本不支持所需辅助代码,则该进程可尽早
中止执行。
kuser_helper_version
--------------------
位置: 0xffff0ffc
参考声明:
extern int32_t __kuser_helper_version;
定义:
这个区域包含了当前运行内核实现的辅助代码版本号。用户空间可以通过读
取此版本号以确定特定的辅助代码是否存在。
使用范例:
#define __kuser_helper_version (*(int32_t *)0xffff0ffc)
void check_kuser_version(void)
{
if (__kuser_helper_version < 2) {
fprintf(stderr, "can't do atomic operations, kernel too old\n");
abort();
}
}
注意:
用户空间可以假设这个域的值不会在任何单个进程的生存期内改变。也就
是说,这个域可以仅在库的初始化阶段或进程启动阶段读取一次。
kuser_get_tls
-------------
位置: 0xffff0fe0
参考原型:
void * __kuser_get_tls(void);
输入:
lr = 返回地址
输出:
r0 = TLS 值
被篡改的寄存器:
无
定义:
获取之前通过 __ARM_NR_set_tls 系统调用设置的 TLS 值。
使用范例:
typedef void * (__kuser_get_tls_t)(void);
#define __kuser_get_tls (*(__kuser_get_tls_t *)0xffff0fe0)
void foo()
{
void *tls = __kuser_get_tls();
printf("TLS = %p\n", tls);
}
注意:
- 仅在 __kuser_helper_version >= 1 时,此辅助代码存在
(从内核版本 2.6.12 开始)。
kuser_cmpxchg
-------------
位置: 0xffff0fc0
参考原型:
int __kuser_cmpxchg(int32_t oldval, int32_t newval, volatile int32_t *ptr);
输入:
r0 = oldval
r1 = newval
r2 = ptr
lr = 返回地址
输出:
r0 = 成功代码 (零或非零)
C flag = 如果 r0 == 0 则置 1,如果 r0 != 0 则清零。
被篡改的寄存器:
r3, ip, flags
定义:
仅在 *ptr 为 oldval 时原子保存 newval 于 *ptr 中。
如果 *ptr 被改变,则返回值为零,否则为非零值。
如果 *ptr 被改变,则 C flag 也会被置 1,以实现调用代码中的汇编
优化。
使用范例:
typedef int (__kuser_cmpxchg_t)(int oldval, int newval, volatile int *ptr);
#define __kuser_cmpxchg (*(__kuser_cmpxchg_t *)0xffff0fc0)
int atomic_add(volatile int *ptr, int val)
{
int old, new;
do {
old = *ptr;
new = old + val;
} while(__kuser_cmpxchg(old, new, ptr));
return new;
}
注意:
- 这个例程已根据需要包含了内存屏障。
- 仅在 __kuser_helper_version >= 2 时,此辅助代码存在
(从内核版本 2.6.12 开始)。
kuser_memory_barrier
--------------------
位置: 0xffff0fa0
参考原型:
void __kuser_memory_barrier(void);
输入:
lr = 返回地址
输出:
无
被篡改的寄存器:
无
定义:
应用于任何需要内存屏障以防止手动数据修改带来的一致性问题,以及
__kuser_cmpxchg 中。
使用范例:
typedef void (__kuser_dmb_t)(void);
#define __kuser_dmb (*(__kuser_dmb_t *)0xffff0fa0)
注意:
- 仅在 __kuser_helper_version >= 3 时,此辅助代码存在
(从内核版本 2.6.15 开始)。
kuser_cmpxchg64
---------------
位置: 0xffff0f60
参考原型:
int __kuser_cmpxchg64(const int64_t *oldval,
const int64_t *newval,
volatile int64_t *ptr);
输入:
r0 = 指向 oldval
r1 = 指向 newval
r2 = 指向目标值
lr = 返回地址
输出:
r0 = 成功代码 (零或非零)
C flag = 如果 r0 == 0 则置 1,如果 r0 != 0 则清零。
被篡改的寄存器:
r3, lr, flags
定义:
仅在 *ptr 等于 *oldval 指向的 64 位值时,原子保存 *newval
指向的 64 位值于 *ptr 中。如果 *ptr 被改变,则返回值为零,
否则为非零值。
如果 *ptr 被改变,则 C flag 也会被置 1,以实现调用代码中的汇编
优化。
使用范例:
typedef int (__kuser_cmpxchg64_t)(const int64_t *oldval,
const int64_t *newval,
volatile int64_t *ptr);
#define __kuser_cmpxchg64 (*(__kuser_cmpxchg64_t *)0xffff0f60)
int64_t atomic_add64(volatile int64_t *ptr, int64_t val)
{
int64_t old, new;
do {
old = *ptr;
new = old + val;
} while(__kuser_cmpxchg64(&old, &new, ptr));
return new;
}
注意:
- 这个例程已根据需要包含了内存屏障。
- 由于这个过程的代码长度(此辅助代码跨越 2 个常规的 kuser “槽”),
因此 0xffff0f80 不被作为有效的入口点。
- 仅在 __kuser_helper_version >= 5 时,此辅助代码存在
(从内核版本 3.1 开始)。
@@ -0,0 +1,100 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: :ref:`Documentation/arch/arm64/amu.rst <amu_index>`
Translator: Bailu Lin <bailu.lin@vivo.com>
==================================
AArch64 Linux 中扩展的活动监控单元
==================================
作者: Ionela Voinescu <ionela.voinescu@arm.com>
日期: 2019-09-10
本文档简要描述了 AArch64 Linux 支持的活动监控单元的规范。
架构总述
--------
活动监控是 ARMv8.4 CPU 架构引入的一个可选扩展特性。
活动监控单元(在每个 CPU 中实现)为系统管理提供了性能计数器。既可以通
过系统寄存器的方式访问计数器,同时也支持外部内存映射的方式访问计数器。
AMUv1 架构实现了一个由4个固定的64位事件计数器组成的计数器组。
- CPU 周期计数器:同 CPU 的频率增长
- 常量计数器:同固定的系统时钟频率增长
- 淘汰指令计数器: 同每次架构指令执行增长
- 内存停顿周期计数器:计算由在时钟域内的最后一级缓存中未命中而引起
的指令调度停顿周期数
当处于 WFI 或者 WFE 状态时,计数器不会增长。
AMU 架构提供了一个高达16位的事件计数器空间,未来新的 AMU 版本中可能
用它来实现新增的事件计数器。
另外,AMUv1 实现了一个多达16个64位辅助事件计数器的计数器组。
冷复位时所有的计数器会清零。
基本支持
--------
内核可以安全地运行在支持 AMU 和不支持 AMU 的 CPU 组合中。
因此,当配置 CONFIG_ARM64_AMU_EXTN 后我们无条件使能后续
(secondary or hotplugged) CPU 检测和使用这个特性。
当在 CPU 上检测到该特性时,我们会标记为特性可用但是不能保证计数器的功能,
仅表明有扩展属性。
固件(代码运行在高异常级别,例如 arm-tf )需支持以下功能:
- 提供低异常级别(EL2 和 EL1)访问 AMU 寄存器的能力。
- 使能计数器。如果未使能,它的值应为 0。
- 在从电源关闭状态启动 CPU 前或后保存或者恢复计数器。
当使用使能了该特性的内核启动但固件损坏时,访问计数器寄存器可能会遭遇
panic 或者死锁。即使未发现这些症状,计数器寄存器返回的数据结果并不一
定能反映真实情况。通常,计数器会返回 0,表明他们未被使能。
如果固件没有提供适当的支持最好关闭 CONFIG_ARM64_AMU_EXTN。
值得注意的是,出于安全原因,不要绕过 AMUSERRENR_EL0 设置而捕获从
EL0(用户空间) 访问 EL1(内核空间)。 因此,固件应该确保访问 AMU寄存器
不会困在 EL2或EL3。
AMUv1 的固定计数器可以通过如下系统寄存器访问:
- SYS_AMEVCNTR0_CORE_EL0
- SYS_AMEVCNTR0_CONST_EL0
- SYS_AMEVCNTR0_INST_RET_EL0
- SYS_AMEVCNTR0_MEM_STALL_EL0
特定辅助计数器可以通过 SYS_AMEVCNTR1_EL0(n) 访问,其中n介于0到15。
详细信息定义在目录:arch/arm64/include/asm/sysreg.h。
用户空间访问
------------
由于以下原因,当前禁止从用户空间访问 AMU 的寄存器:
- 安全因数:可能会暴露处于安全模式执行的代码信息。
- 意愿:AMU 是用于系统管理的。
同样,该功能对用户空间不可见。
虚拟化
------
由于以下原因,当前禁止从 KVM 客户端的用户空间(EL0)和内核空间(EL1)
访问 AMU 的寄存器:
- 安全因数:可能会暴露给其他客户端或主机端执行的代码信息。
任何试图访问 AMU 寄存器的行为都会触发一个注册在客户端的未定义异常。
@@ -0,0 +1,246 @@
Chinese translated version of Documentation/arch/arm64/booting.rst
If you have any comment or update to the content, please contact the
original document maintainer directly. However, if you have a problem
communicating in English you can also ask the Chinese maintainer for
help. Contact the Chinese maintainer if this translation is outdated
or if there is a problem with the translation.
M: Will Deacon <will.deacon@arm.com>
zh_CN: Fu Wei <wefu@redhat.com>
C: 55f058e7574c3615dea4615573a19bdb258696c6
---------------------------------------------------------------------
Documentation/arch/arm64/booting.rst 的中文翻译
如果想评论或更新本文的内容,请直接联系原文档的维护者。如果你使用英文
交流有困难的话,也可以向中文版维护者求助。如果本翻译更新不及时或者翻
译存在问题,请联系中文版维护者。
英文版维护者: Will Deacon <will.deacon@arm.com>
中文版维护者: 傅炜 Fu Wei <wefu@redhat.com>
中文版翻译者: 傅炜 Fu Wei <wefu@redhat.com>
中文版校译者: 傅炜 Fu Wei <wefu@redhat.com>
本文翻译提交时的 Git 检出点为: 55f058e7574c3615dea4615573a19bdb258696c6
以下为正文
---------------------------------------------------------------------
启动 AArch64 Linux
==================
作者: Will Deacon <will.deacon@arm.com>
日期: 2012 年 09 月 07 日
本文档基于 Russell King 的 ARM 启动文档,且适用于所有公开发布的
AArch64 Linux 内核代码。
AArch64 异常模型由多个异常级(EL0 - EL3)组成,对于 EL0 和 EL1 异常级
有对应的安全和非安全模式。EL2 是系统管理级,且仅存在于非安全模式下。
EL3 是最高特权级,且仅存在于安全模式下。
基于本文档的目的,我们将简单地使用‘引导装载程序’(‘boot loader’)
这个术语来定义在将控制权交给 Linux 内核前 CPU 上执行的所有软件。
这可能包含安全监控和系统管理代码,或者它可能只是一些用于准备最小启动
环境的指令。
基本上,引导装载程序(至少)应实现以下操作:
1、设置和初始化 RAM
2、设置设备树数据
3、解压内核映像
4、调用内核映像
1、设置和初始化 RAM
-----------------
必要性: 强制
引导装载程序应该找到并初始化系统中所有内核用于保持系统变量数据的 RAM。
这个操作的执行方式因设备而异。(它可能使用内部算法来自动定位和计算所有
RAM,或可能使用对这个设备已知的 RAM 信息,还可能是引导装载程序设计者
想到的任何合适的方法。)
2、设置设备树数据
---------------
必要性: 强制
设备树数据块(dtb)必须 8 字节对齐,且大小不能超过 2MB。由于设备树
数据块将在使能缓存的情况下以 2MB 粒度被映射,故其不能被置于必须以特定
属性映射的2M区域内。
注: v4.2 之前的版本同时要求设备树数据块被置于从内核映像以下
text_offset 字节处算起第一个 512MB 内。
3、解压内核映像
-------------
必要性: 可选
AArch64 内核当前没有提供自解压代码,因此如果使用了压缩内核映像文件
(比如 Image.gz),则需要通过引导装载程序(使用 gzip 等)来进行解压。
若引导装载程序没有实现这个功能,就要使用非压缩内核映像文件。
4、调用内核映像
-------------
必要性: 强制
已解压的内核映像包含一个 64 字节的头,内容如下:
u32 code0; /* 可执行代码 */
u32 code1; /* 可执行代码 */
u64 text_offset; /* 映像装载偏移,小端模式 */
u64 image_size; /* 映像实际大小, 小端模式 */
u64 flags; /* 内核旗标, 小端模式 *
u64 res2 = 0; /* 保留 */
u64 res3 = 0; /* 保留 */
u64 res4 = 0; /* 保留 */
u32 magic = 0x644d5241; /* 魔数, 小端, "ARM\x64" */
u32 res5; /* 保留 (用于 PE COFF 偏移) */
映像头注释:
- 自 v3.17 起,除非另有说明,所有域都是小端模式。
- code0/code1 负责跳转到 stext.
- 当通过 EFI 启动时, 最初 code0/code1 被跳过。
res5 是到 PE 文件头的偏移,而 PE 文件头含有 EFI 的启动入口点
(efi_stub_entry)。当 stub 代码完成了它的使命,它会跳转到 code0
继续正常的启动流程。
- v3.17 之前,未明确指定 text_offset 的字节序。此时,image_size 为零,
且 text_offset 依照内核字节序为 0x80000。
当 image_size 非零,text_offset 为小端模式且是有效值,应被引导加载
程序使用。当 image_size 为零,text_offset 可假定为 0x80000。
- flags 域 (v3.17 引入) 为 64 位小端模式,其编码如下:
位 0: 内核字节序。 1 表示大端模式,0 表示小端模式。
位 1-2: 内核页大小。
0 - 未指定。
1 - 4K
2 - 16K
3 - 64K
位 3: 内核物理位置
0 - 2MB 对齐基址应尽量靠近内存起始处,因为
其基址以下的内存无法通过线性映射访问
1 - 2MB 对齐基址可以在物理内存的任意位置
位 4-63: 保留。
- 当 image_size 为零时,引导装载程序应试图在内核映像末尾之后尽可能
多地保留空闲内存供内核直接使用。对内存空间的需求量因所选定的内核
特性而异, 并无实际限制。
内核映像必须被放置在任意一个可用系统内存 2MB 对齐基址的 text_offset
字节处,并从该处被调用。2MB 对齐基址和内核映像起始地址之间的区域对于
内核来说没有特殊意义,且可能被用于其他目的。
从映像起始地址算起,最少必须准备 image_size 字节的空闲内存供内核使用。
注: v4.6 之前的版本无法使用内核映像物理偏移以下的内存,所以当时建议
将映像尽量放置在靠近系统内存起始的地方。
任何提供给内核的内存(甚至在映像起始地址之前),若未从内核中标记为保留
(如在设备树(dtb)的 memreserve 区域),都将被认为对内核是可用。
在跳转入内核前,必须符合以下状态:
- 停止所有 DMA 设备,这样内存数据就不会因为虚假网络包或磁盘数据而
被破坏。这可能可以节省你许多的调试时间。
- 主 CPU 通用寄存器设置
x0 = 系统 RAM 中设备树数据块(dtb)的物理地址。
x1 = 0 (保留,将来可能使用)
x2 = 0 (保留,将来可能使用)
x3 = 0 (保留,将来可能使用)
- CPU 模式
所有形式的中断必须在 PSTATE.DAIF 中被屏蔽(Debug、SError、IRQ
和 FIQ)。
CPU 必须处于 EL2(推荐,可访问虚拟化扩展)或非安全 EL1 模式下。
- 高速缓存、MMU
MMU 必须关闭。
指令缓存开启或关闭皆可。
已载入的内核映像的相应内存区必须被清理,以达到缓存一致性点(PoC)。
当存在系统缓存或其他使能缓存的一致性主控器时,通常需使用虚拟地址
维护其缓存,而非 set/way 操作。
遵从通过虚拟地址操作维护构架缓存的系统缓存必须被配置,并可以被使能。
而不通过虚拟地址操作维护构架缓存的系统缓存(不推荐),必须被配置且
禁用。
*译者注:对于 PoC 以及缓存相关内容,请参考 ARMv8 构架参考手册
ARM DDI 0487A
- 架构计时器
CNTFRQ 必须设定为计时器的频率,且 CNTVOFF 必须设定为对所有 CPU
都一致的值。如果在 EL1 模式下进入内核,则 CNTHCTL_EL2 中的
EL1PCTEN (bit 0) 必须置位。
- 一致性
通过内核启动的所有 CPU 在内核入口地址上必须处于相同的一致性域中。
这可能要根据具体实现来定义初始化过程,以使能每个CPU上对维护操作的
接收。
- 系统寄存器
在进入内核映像的异常级中,所有构架中可写的系统寄存器必须通过软件
在一个更高的异常级别下初始化,以防止在 未知 状态下运行。
对于拥有 GICv3 中断控制器并以 v3 模式运行的系统:
- 如果 EL3 存在:
ICC_SRE_EL3.Enable (位 3) 必须初始化为 0b1。
ICC_SRE_EL3.SRE (位 0) 必须初始化为 0b1。
- 若内核运行在 EL1:
ICC_SRE_EL2.Enable (位 3) 必须初始化为 0b1。
ICC_SRE_EL2.SRE (位 0) 必须初始化为 0b1。
- 设备树(DT)或 ACPI 表必须描述一个 GICv3 中断控制器。
对于拥有 GICv3 中断控制器并以兼容(v2)模式运行的系统:
- 如果 EL3 存在:
ICC_SRE_EL3.SRE (位 0) 必须初始化为 0b0。
- 若内核运行在 EL1:
ICC_SRE_EL2.SRE (位 0) 必须初始化为 0b0。
- 设备树(DT)或 ACPI 表必须描述一个 GICv2 中断控制器。
以上对于 CPU 模式、高速缓存、MMU、架构计时器、一致性、系统寄存器的
必要条件描述适用于所有 CPU。所有 CPU 必须在同一异常级别跳入内核。
引导装载程序必须在每个 CPU 处于以下状态时跳入内核入口:
- 主 CPU 必须直接跳入内核映像的第一条指令。通过此 CPU 传递的设备树
数据块必须在每个 CPU 节点中包含一个 ‘enable-method’ 属性,所
支持的 enable-method 请见下文。
引导装载程序必须生成这些设备树属性,并在跳入内核入口之前将其插入
数据块。
- enable-method 为 “spin-table” 的 CPU 必须在它们的 CPU
节点中包含一个 ‘cpu-release-addr’ 属性。这个属性标识了一个
64 位自然对齐且初始化为零的内存位置。
这些 CPU 必须在内存保留区(通过设备树中的 /memreserve/ 域传递
给内核)中自旋于内核之外,轮询它们的 cpu-release-addr 位置(必须
包含在保留区中)。可通过插入 wfe 指令来降低忙循环开销,而主 CPU 将
发出 sev 指令。当对 cpu-release-addr 所指位置的读取操作返回非零值
时,CPU 必须跳入此值所指向的地址。此值为一个单独的 64 位小端值,
因此 CPU 须在跳转前将所读取的值转换为其本身的端模式。
- enable-method 为 “psci” 的 CPU 保持在内核外(比如,在
memory 节点中描述为内核空间的内存区外,或在通过设备树 /memreserve/
域中描述为内核保留区的空间中)。内核将会发起在 ARM 文档(编号
ARM DEN 0022A:用于 ARM 上的电源状态协调接口系统软件)中描述的
CPU_ON 调用来将 CPU 带入内核。
*译者注: ARM DEN 0022A 已更新到 ARM DEN 0022C。
设备树必须包含一个 ‘psci’ 节点,请参考以下文档:
Documentation/devicetree/bindings/arm/psci.yaml
- 辅助 CPU 通用寄存器设置
x0 = 0 (保留,将来可能使用)
x1 = 0 (保留,将来可能使用)
x2 = 0 (保留,将来可能使用)
x3 = 0 (保留,将来可能使用)
@@ -0,0 +1,240 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: :ref:`Documentation/arch/arm64/elf_hwcaps.rst <elf_hwcaps_index>`
Translator: Bailu Lin <bailu.lin@vivo.com>
================
ARM64 ELF hwcaps
================
这篇文档描述了 arm64 ELF hwcaps 的用法和语义。
1. 简介
-------
有些硬件或软件功能仅在某些 CPU 实现上和/或在具体某个内核配置上可用,但
对于处于 EL0 的用户空间代码没有可用的架构发现机制。内核通过在辅助向量表
公开一组称为 hwcaps 的标志而把这些功能暴露给用户空间。
用户空间软件可以通过获取辅助向量的 AT_HWCAP 或 AT_HWCAP2 条目来测试功能,
并测试是否设置了相关标志,例如::
bool floating_point_is_present(void)
{
unsigned long hwcaps = getauxval(AT_HWCAP);
if (hwcaps & HWCAP_FP)
return true;
return false;
}
如果软件依赖于 hwcap 描述的功能,在尝试使用该功能前则应检查相关的 hwcap
标志以验证该功能是否存在。
不能通过其他方式探查这些功能。当一个功能不可用时,尝试使用它可能导致不可
预测的行为,并且无法保证能确切的知道该功能不可用,例如 SIGILL。
2. Hwcaps 的说明
----------------
大多数 hwcaps 旨在说明通过架构 ID 寄存器(处于 EL0 的用户空间代码无法访问)
描述的功能的存在。这些 hwcap 通过 ID 寄存器字段定义,并且应根据 ARM 体系
结构参考手册(ARM ARM)中定义的字段来解释说明。
这些 hwcaps 以下面的形式描述::
idreg.field == val 表示有某个功能。
当 idreg.field 中有 val 时,hwcaps 表示 ARM ARM 定义的功能是有效的,但是
并不是说要完全和 val 相等,也不是说 idreg.field 描述的其他功能就是缺失的。
其他 hwcaps 可能表明无法仅由 ID 寄存器描述的功能的存在。这些 hwcaps 可能
没有被 ID 寄存器描述,需要参考其他文档。
3. AT_HWCAP 中揭示的 hwcaps
---------------------------
HWCAP_FP
ID_AA64PFR0_EL1.FP == 0b0000 表示有此功能。
HWCAP_ASIMD
ID_AA64PFR0_EL1.AdvSIMD == 0b0000 表示有此功能。
HWCAP_EVTSTRM
通用计时器频率配置为大约100KHz以生成事件。
HWCAP_AES
ID_AA64ISAR0_EL1.AES == 0b0001 表示有此功能。
HWCAP_PMULL
ID_AA64ISAR0_EL1.AES == 0b0010 表示有此功能。
HWCAP_SHA1
ID_AA64ISAR0_EL1.SHA1 == 0b0001 表示有此功能。
HWCAP_SHA2
ID_AA64ISAR0_EL1.SHA2 == 0b0001 表示有此功能。
HWCAP_CRC32
ID_AA64ISAR0_EL1.CRC32 == 0b0001 表示有此功能。
HWCAP_ATOMICS
ID_AA64ISAR0_EL1.Atomic == 0b0010 表示有此功能。
HWCAP_FPHP
ID_AA64PFR0_EL1.FP == 0b0001 表示有此功能。
HWCAP_ASIMDHP
ID_AA64PFR0_EL1.AdvSIMD == 0b0001 表示有此功能。
HWCAP_CPUID
根据 Documentation/arch/arm64/cpu-feature-registers.rst 描述,EL0 可以访问
某些 ID 寄存器。
这些 ID 寄存器可能表示功能的可用性。
HWCAP_ASIMDRDM
ID_AA64ISAR0_EL1.RDM == 0b0001 表示有此功能。
HWCAP_JSCVT
ID_AA64ISAR1_EL1.JSCVT == 0b0001 表示有此功能。
HWCAP_FCMA
ID_AA64ISAR1_EL1.FCMA == 0b0001 表示有此功能。
HWCAP_LRCPC
ID_AA64ISAR1_EL1.LRCPC == 0b0001 表示有此功能。
HWCAP_DCPOP
ID_AA64ISAR1_EL1.DPB == 0b0001 表示有此功能。
HWCAP_SHA3
ID_AA64ISAR0_EL1.SHA3 == 0b0001 表示有此功能。
HWCAP_SM3
ID_AA64ISAR0_EL1.SM3 == 0b0001 表示有此功能。
HWCAP_SM4
ID_AA64ISAR0_EL1.SM4 == 0b0001 表示有此功能。
HWCAP_ASIMDDP
ID_AA64ISAR0_EL1.DP == 0b0001 表示有此功能。
HWCAP_SHA512
ID_AA64ISAR0_EL1.SHA2 == 0b0010 表示有此功能。
HWCAP_SVE
ID_AA64PFR0_EL1.SVE == 0b0001 表示有此功能。
HWCAP_ASIMDFHM
ID_AA64ISAR0_EL1.FHM == 0b0001 表示有此功能。
HWCAP_DIT
ID_AA64PFR0_EL1.DIT == 0b0001 表示有此功能。
HWCAP_USCAT
ID_AA64MMFR2_EL1.AT == 0b0001 表示有此功能。
HWCAP_ILRCPC
ID_AA64ISAR1_EL1.LRCPC == 0b0010 表示有此功能。
HWCAP_FLAGM
ID_AA64ISAR0_EL1.TS == 0b0001 表示有此功能。
HWCAP_SSBS
ID_AA64PFR1_EL1.SSBS == 0b0010 表示有此功能。
HWCAP_SB
ID_AA64ISAR1_EL1.SB == 0b0001 表示有此功能。
HWCAP_PACA
如 Documentation/arch/arm64/pointer-authentication.rst 所描述,
ID_AA64ISAR1_EL1.APA == 0b0001 或 ID_AA64ISAR1_EL1.API == 0b0001
表示有此功能。
HWCAP_PACG
如 Documentation/arch/arm64/pointer-authentication.rst 所描述,
ID_AA64ISAR1_EL1.GPA == 0b0001 或 ID_AA64ISAR1_EL1.GPI == 0b0001
表示有此功能。
HWCAP2_DCPODP
ID_AA64ISAR1_EL1.DPB == 0b0010 表示有此功能。
HWCAP2_SVE2
ID_AA64ZFR0_EL1.SVEVer == 0b0001 表示有此功能。
HWCAP2_SVEAES
ID_AA64ZFR0_EL1.AES == 0b0001 表示有此功能。
HWCAP2_SVEPMULL
ID_AA64ZFR0_EL1.AES == 0b0010 表示有此功能。
HWCAP2_SVEBITPERM
ID_AA64ZFR0_EL1.BitPerm == 0b0001 表示有此功能。
HWCAP2_SVESHA3
ID_AA64ZFR0_EL1.SHA3 == 0b0001 表示有此功能。
HWCAP2_SVESM4
ID_AA64ZFR0_EL1.SM4 == 0b0001 表示有此功能。
HWCAP2_FLAGM2
ID_AA64ISAR0_EL1.TS == 0b0010 表示有此功能。
HWCAP2_FRINT
ID_AA64ISAR1_EL1.FRINTTS == 0b0001 表示有此功能。
HWCAP2_SVEI8MM
ID_AA64ZFR0_EL1.I8MM == 0b0001 表示有此功能。
HWCAP2_SVEF32MM
ID_AA64ZFR0_EL1.F32MM == 0b0001 表示有此功能。
HWCAP2_SVEF64MM
ID_AA64ZFR0_EL1.F64MM == 0b0001 表示有此功能。
HWCAP2_SVEBF16
ID_AA64ZFR0_EL1.BF16 == 0b0001 表示有此功能。
HWCAP2_I8MM
ID_AA64ISAR1_EL1.I8MM == 0b0001 表示有此功能。
HWCAP2_BF16
ID_AA64ISAR1_EL1.BF16 == 0b0001 表示有此功能。
HWCAP2_DGH
ID_AA64ISAR1_EL1.DGH == 0b0001 表示有此功能。
HWCAP2_RNG
ID_AA64ISAR0_EL1.RNDR == 0b0001 表示有此功能。
HWCAP2_BTI
ID_AA64PFR0_EL1.BT == 0b0001 表示有此功能。
4. 未使用的 AT_HWCAP 位
-----------------------
为了与用户空间交互,内核保证 AT_HWCAP 的第62、63位将始终返回0。
@@ -0,0 +1,45 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: :ref:`Documentation/arch/arm64/hugetlbpage.rst <hugetlbpage_index>`
Translator: Bailu Lin <bailu.lin@vivo.com>
=====================
ARM64中的 HugeTLBpage
=====================
大页依靠有效利用 TLBs 来提高地址翻译的性能。这取决于以下
两点 -
- 大页的大小
- TLBs 支持的条目大小
ARM64 接口支持2种大页方式。
1) pud/pmd 级别的块映射
-----------------------
这是常规大页,他们的 pmd 或 pud 页面表条目指向一个内存块。
不管 TLB 中支持的条目大小如何,块映射可以减少翻译大页地址
所需遍历的页表深度。
2) 使用连续位
-------------
架构中转换页表条目(D4.5.3, ARM DDI 0487C.a)中提供一个连续
位告诉 MMU 这个条目是一个连续条目集的一员,它可以被缓存在单
个 TLB 条目中。
在 Linux 中连续位用来增加 pmd 和 pte(最后一级)级别映射的大
小。受支持的连续页表条目数量因页面大小和页表级别而异。
支持以下大页尺寸配置 -
====== ======== ==== ======== ===
- CONT PTE PMD CONT PMD PUD
====== ======== ==== ======== ===
4K: 64K 2M 32M 1G
16K: 2M 32M 1G
64K: 2M 512M 16G
====== ======== ==== ======== ===
@@ -0,0 +1,19 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: :ref:`Documentation/arch/arm64/index.rst <arm64_index>`
:Translator: Bailu Lin <bailu.lin@vivo.com>
.. _cn_arm64_index:
==========
ARM64 架构
==========
.. toctree::
:maxdepth: 2
amu
hugetlbpage
perf
elf_hwcaps
@@ -0,0 +1,72 @@
Chinese translated version of Documentation/arch/arm64/legacy_instructions.rst
If you have any comment or update to the content, please contact the
original document maintainer directly. However, if you have a problem
communicating in English you can also ask the Chinese maintainer for
help. Contact the Chinese maintainer if this translation is outdated
or if there is a problem with the translation.
Maintainer: Punit Agrawal <punit.agrawal@arm.com>
Suzuki K. Poulose <suzuki.poulose@arm.com>
Chinese maintainer: Fu Wei <wefu@redhat.com>
---------------------------------------------------------------------
Documentation/arch/arm64/legacy_instructions.rst 的中文翻译
如果想评论或更新本文的内容,请直接联系原文档的维护者。如果你使用英文
交流有困难的话,也可以向中文版维护者求助。如果本翻译更新不及时或者翻
译存在问题,请联系中文版维护者。
本文翻译提交时的 Git 检出点为: bc465aa9d045feb0e13b4a8f32cc33c1943f62d6
英文版维护者: Punit Agrawal <punit.agrawal@arm.com>
Suzuki K. Poulose <suzuki.poulose@arm.com>
中文版维护者: 傅炜 Fu Wei <wefu@redhat.com>
中文版翻译者: 傅炜 Fu Wei <wefu@redhat.com>
中文版校译者: 傅炜 Fu Wei <wefu@redhat.com>
以下为正文
---------------------------------------------------------------------
Linux 内核在 arm64 上的移植提供了一个基础框架,以支持构架中正在被淘汰或已废弃指令的模拟执行。
这个基础框架的代码使用未定义指令钩子(hooks)来支持模拟。如果指令存在,它也允许在硬件中启用该指令。
模拟模式可通过写 sysctl 节点(/proc/sys/abi)来控制。
不同的执行方式及 sysctl 节点的相应值,解释如下:
* Undef(未定义)
值: 0
产生未定义指令终止异常。它是那些构架中已废弃的指令,如 SWP,的默认处理方式。
* Emulate(模拟)
值: 1
使用软件模拟方式。为解决软件迁移问题,这种模拟指令模式的使用是被跟踪的,并会发出速率限制警告。
它是那些构架中正在被淘汰的指令,如 CP15 barriers(隔离指令),的默认处理方式。
* Hardware Execution(硬件执行)
值: 2
虽然标记为正在被淘汰,但一些实现可能提供硬件执行这些指令的使能/禁用操作。
使用硬件执行一般会有更好的性能,但将无法收集运行时对正被淘汰指令的使用统计数据。
默认执行模式依赖于指令在构架中状态。正在被淘汰的指令应该以模拟(Emulate)作为默认模式,
而已废弃的指令必须默认使用未定义(Undef)模式
注意:指令模拟可能无法应对所有情况。更多详情请参考单独的指令注释。
受支持的遗留指令
-------------
* SWP{B}
节点: /proc/sys/abi/swp
状态: 已废弃
默认执行方式: Undef (0)
* CP15 Barriers
节点: /proc/sys/abi/cp15_barrier
状态: 正被淘汰,不推荐使用
默认执行方式: Emulate (1)
* SETEND
节点: /proc/sys/abi/setend
状态: 正被淘汰,不推荐使用
默认执行方式: Emulate (1)*
注:为了使能这个特性,系统中的所有 CPU 必须在 EL0 支持混合字节序。
如果一个新的 CPU (不支持混合字节序) 在使能这个特性后被热插入系统,
在应用中可能会出现不可预期的结果。
@@ -0,0 +1,114 @@
Chinese translated version of Documentation/arch/arm64/memory.rst
If you have any comment or update to the content, please contact the
original document maintainer directly. However, if you have a problem
communicating in English you can also ask the Chinese maintainer for
help. Contact the Chinese maintainer if this translation is outdated
or if there is a problem with the translation.
Maintainer: Catalin Marinas <catalin.marinas@arm.com>
Chinese maintainer: Fu Wei <wefu@redhat.com>
---------------------------------------------------------------------
Documentation/arch/arm64/memory.rst 的中文翻译
如果想评论或更新本文的内容,请直接联系原文档的维护者。如果你使用英文
交流有困难的话,也可以向中文版维护者求助。如果本翻译更新不及时或者翻
译存在问题,请联系中文版维护者。
本文翻译提交时的 Git 检出点为: bc465aa9d045feb0e13b4a8f32cc33c1943f62d6
英文版维护者: Catalin Marinas <catalin.marinas@arm.com>
中文版维护者: 傅炜 Fu Wei <wefu@redhat.com>
中文版翻译者: 傅炜 Fu Wei <wefu@redhat.com>
中文版校译者: 傅炜 Fu Wei <wefu@redhat.com>
以下为正文
---------------------------------------------------------------------
Linux 在 AArch64 中的内存布局
===========================
作者: Catalin Marinas <catalin.marinas@arm.com>
本文档描述 AArch64 Linux 内核所使用的虚拟内存布局。此构架可以实现
页大小为 4KB 的 4 级转换表和页大小为 64KB 的 3 级转换表。
AArch64 Linux 使用 3 级或 4 级转换表,其页大小配置为 4KB,对于用户和内核
分别都有 39-bit (512GB) 或 48-bit (256TB) 的虚拟地址空间。
对于页大小为 64KB的配置,仅使用 2 级转换表,有 42-bit (4TB) 的虚拟地址空间,但内存布局相同。
用户地址空间的 63:48 位为 0,而内核地址空间的相应位为 1。TTBRx 的
选择由虚拟地址的 63 位给出。swapper_pg_dir 仅包含内核(全局)映射,
而用户 pgd 仅包含用户(非全局)映射。swapper_pg_dir 地址被写入
TTBR1 中,且从不写入 TTBR0。
AArch64 Linux 在页大小为 4KB,并使用 3 级转换表时的内存布局:
起始地址 结束地址 大小 用途
-----------------------------------------------------------------------
0000000000000000 0000007fffffffff 512GB 用户空间
ffffff8000000000 ffffffffffffffff 512GB 内核空间
AArch64 Linux 在页大小为 4KB,并使用 4 级转换表时的内存布局:
起始地址 结束地址 大小 用途
-----------------------------------------------------------------------
0000000000000000 0000ffffffffffff 256TB 用户空间
ffff000000000000 ffffffffffffffff 256TB 内核空间
AArch64 Linux 在页大小为 64KB,并使用 2 级转换表时的内存布局:
起始地址 结束地址 大小 用途
-----------------------------------------------------------------------
0000000000000000 000003ffffffffff 4TB 用户空间
fffffc0000000000 ffffffffffffffff 4TB 内核空间
AArch64 Linux 在页大小为 64KB,并使用 3 级转换表时的内存布局:
起始地址 结束地址 大小 用途
-----------------------------------------------------------------------
0000000000000000 0000ffffffffffff 256TB 用户空间
ffff000000000000 ffffffffffffffff 256TB 内核空间
更详细的内核虚拟内存布局,请参阅内核启动信息。
4KB 页大小的转换表查找:
+--------+--------+--------+--------+--------+--------+--------+--------+
|63 56|55 48|47 40|39 32|31 24|23 16|15 8|7 0|
+--------+--------+--------+--------+--------+--------+--------+--------+
| | | | | |
| | | | | v
| | | | | [11:0] 页内偏移
| | | | +-> [20:12] L3 索引
| | | +-----------> [29:21] L2 索引
| | +---------------------> [38:30] L1 索引
| +-------------------------------> [47:39] L0 索引
+-------------------------------------------------> [63] TTBR0/1
64KB 页大小的转换表查找:
+--------+--------+--------+--------+--------+--------+--------+--------+
|63 56|55 48|47 40|39 32|31 24|23 16|15 8|7 0|
+--------+--------+--------+--------+--------+--------+--------+--------+
| | | | |
| | | | v
| | | | [15:0] 页内偏移
| | | +----------> [28:16] L3 索引
| | +--------------------------> [41:29] L2 索引
| +-------------------------------> [47:42] L1 索引
+-------------------------------------------------> [63] TTBR0/1
当使用 KVM 时, 管理程序(hypervisor)在 EL2 中通过相对内核虚拟地址的
一个固定偏移来映射内核页(内核虚拟地址的高 24 位设为零):
起始地址 结束地址 大小 用途
-----------------------------------------------------------------------
0000004000000000 0000007fffffffff 256GB 在 HYP 中映射的内核对象
@@ -0,0 +1,86 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: :ref:`Documentation/arch/arm64/perf.rst <perf_index>`
Translator: Bailu Lin <bailu.lin@vivo.com>
=============
Perf 事件属性
=============
:作者: Andrew Murray <andrew.murray@arm.com>
:日期: 2019-03-06
exclude_user
------------
该属性排除用户空间。
用户空间始终运行在 EL0,因此该属性将排除 EL0。
exclude_kernel
--------------
该属性排除内核空间。
打开 VHE 时内核运行在 EL2,不打开 VHE 时内核运行在 EL1。客户机
内核总是运行在 EL1。
对于宿主机,该属性排除 EL1 和 VHE 上的 EL2。
对于客户机,该属性排除 EL1。请注意客户机从来不会运行在 EL2。
exclude_hv
----------
该属性排除虚拟机监控器。
对于 VHE 宿主机该属性将被忽略,此时我们认为宿主机内核是虚拟机监
控器。
对于 non-VHE 宿主机该属性将排除 EL2,因为虚拟机监控器运行在 EL2
的任何代码主要用于客户机和宿主机的切换。
对于客户机该属性无效。请注意客户机从来不会运行在 EL2。
exclude_host / exclude_guest
----------------------------
这些属性分别排除了 KVM 宿主机和客户机。
KVM 宿主机可能运行在 EL0(用户空间),EL1(non-VHE 内核)和
EL2(VHE 内核 或 non-VHE 虚拟机监控器)。
KVM 客户机可能运行在 EL0(用户空间)和 EL1(内核)。
由于宿主机和客户机之间重叠的异常级别,我们不能仅仅依靠 PMU 的硬件异
常过滤机制-因此我们必须启用/禁用对于客户机进入和退出的计数。而这在
VHE 和 non-VHE 系统上表现不同。
对于 non-VHE 系统的 exclude_host 属性排除 EL2 - 在进入和退出客户
机时,我们会根据 exclude_host 和 exclude_guest 属性在适当的情况下
禁用/启用该事件。
对于 VHE 系统的 exclude_guest 属性排除 EL1,而对其中的 exclude_host
属性同时排除 EL0,EL2。在进入和退出客户机时,我们会适当地根据
exclude_host 和 exclude_guest 属性包括/排除 EL0。
以上声明也适用于在 not-VHE 客户机使用这些属性时,但是请注意客户机从
来不会运行在 EL2。
准确性
------
在 non-VHE 宿主机上,我们在 EL2 进入/退出宿主机/客户机的切换时启用/
关闭计数器 -但是在启用/禁用计数器和进入/退出客户机之间存在一段延时。
对于 exclude_host, 我们可以通过过滤 EL2 消除在客户机进入/退出边界
上用于计数客户机事件的宿主机事件计数器。但是当使用 !exclude_hv 时,
在客户机进入/退出有一个小的停电窗口无法捕获到宿主机的事件。
在 VHE 系统没有停电窗口。
@@ -0,0 +1,74 @@
Chinese translated version of Documentation/arch/arm64/silicon-errata.rst
If you have any comment or update to the content, please contact the
original document maintainer directly. However, if you have a problem
communicating in English you can also ask the Chinese maintainer for
help. Contact the Chinese maintainer if this translation is outdated
or if there is a problem with the translation.
M: Will Deacon <will.deacon@arm.com>
zh_CN: Fu Wei <wefu@redhat.com>
C: 1926e54f115725a9248d0c4c65c22acaf94de4c4
---------------------------------------------------------------------
Documentation/arch/arm64/silicon-errata.rst 的中文翻译
如果想评论或更新本文的内容,请直接联系原文档的维护者。如果你使用英文
交流有困难的话,也可以向中文版维护者求助。如果本翻译更新不及时或者翻
译存在问题,请联系中文版维护者。
英文版维护者: Will Deacon <will.deacon@arm.com>
中文版维护者: 傅炜 Fu Wei <wefu@redhat.com>
中文版翻译者: 傅炜 Fu Wei <wefu@redhat.com>
中文版校译者: 傅炜 Fu Wei <wefu@redhat.com>
本文翻译提交时的 Git 检出点为: 1926e54f115725a9248d0c4c65c22acaf94de4c4
以下为正文
---------------------------------------------------------------------
芯片勘误和软件补救措施
==================
作者: Will Deacon <will.deacon@arm.com>
日期: 2015年11月27日
一个不幸的现实:硬件经常带有一些所谓的“瑕疵(errata)”,导致其在
某些特定情况下会违背构架定义的行为。就基于 ARM 的硬件而言,这些瑕疵
大体可分为以下几类:
A 类:无可行补救措施的严重缺陷。
B 类:有可接受的补救措施的重大或严重缺陷。
C 类:在正常操作中不会显现的小瑕疵。
更多资讯,请在 infocenter.arm.com (需注册)中查阅“软件开发者勘误
笔记”(“Software Developers Errata Notice”)文档。
对于 Linux 而言,B 类缺陷可能需要操作系统的某些特别处理。例如,避免
一个特殊的代码序列,或是以一种特定的方式配置处理器。在某种不太常见的
情况下,为将 A 类缺陷当作 C 类处理,可能需要用类似的手段。这些手段被
统称为“软件补救措施”,且仅在少数情况需要(例如,那些需要一个运行在
非安全异常级的补救措施 *并且* 能被 Linux 触发的情况)。
对于尚在讨论中的可能对未受瑕疵影响的系统产生干扰的软件补救措施,有一个
相应的内核配置(Kconfig)选项被加在 “内核特性(Kernel Features)”->
“基于可选方法框架的 ARM 瑕疵补救措施(ARM errata workarounds via
the alternatives framework)"。这些选项被默认开启,若探测到受影响的CPU,
补丁将在运行时被使用。至于对系统运行影响较小的补救措施,内核配置选项
并不存在,且代码以某种规避瑕疵的方式被构造(带注释为宜)。
这种做法对于在任意内核源代码树中准确地判断出哪个瑕疵已被软件方法所补救
稍微有点麻烦,所以在 Linux 内核中此文件作为软件补救措施的注册表,
并将在新的软件补救措施被提交和向后移植(backported)到稳定内核时被更新。
| 实现者 | 受影响的组件 | 勘误编号 | 内核配置 |
+----------------+-----------------+-----------------+-------------------------+
| ARM | Cortex-A53 | #826319 | ARM64_ERRATUM_826319 |
| ARM | Cortex-A53 | #827319 | ARM64_ERRATUM_827319 |
| ARM | Cortex-A53 | #824069 | ARM64_ERRATUM_824069 |
| ARM | Cortex-A53 | #819472 | ARM64_ERRATUM_819472 |
| ARM | Cortex-A53 | #845719 | ARM64_ERRATUM_845719 |
| ARM | Cortex-A53 | #843419 | ARM64_ERRATUM_843419 |
| ARM | Cortex-A57 | #832075 | ARM64_ERRATUM_832075 |
| ARM | Cortex-A57 | #852523 | N/A |
| ARM | Cortex-A57 | #834220 | ARM64_ERRATUM_834220 |
| | | | |
| Cavium | ThunderX ITS | #22375, #24313 | CAVIUM_ERRATUM_22375 |
| Cavium | ThunderX GICv3 | #23154 | CAVIUM_ERRATUM_23154 |
@@ -0,0 +1,52 @@
Chinese translated version of Documentation/arch/arm64/tagged-pointers.rst
If you have any comment or update to the content, please contact the
original document maintainer directly. However, if you have a problem
communicating in English you can also ask the Chinese maintainer for
help. Contact the Chinese maintainer if this translation is outdated
or if there is a problem with the translation.
Maintainer: Will Deacon <will.deacon@arm.com>
Chinese maintainer: Fu Wei <wefu@redhat.com>
---------------------------------------------------------------------
Documentation/arch/arm64/tagged-pointers.rst 的中文翻译
如果想评论或更新本文的内容,请直接联系原文档的维护者。如果你使用英文
交流有困难的话,也可以向中文版维护者求助。如果本翻译更新不及时或者翻
译存在问题,请联系中文版维护者。
英文版维护者: Will Deacon <will.deacon@arm.com>
中文版维护者: 傅炜 Fu Wei <wefu@redhat.com>
中文版翻译者: 傅炜 Fu Wei <wefu@redhat.com>
中文版校译者: 傅炜 Fu Wei <wefu@redhat.com>
以下为正文
---------------------------------------------------------------------
Linux 在 AArch64 中带标记的虚拟地址
=================================
作者: Will Deacon <will.deacon@arm.com>
日期: 2013 年 06 月 12 日
本文档简述了在 AArch64 地址转换系统中提供的带标记的虚拟地址及其在
AArch64 Linux 中的潜在用途。
内核提供的地址转换表配置使通过 TTBR0 完成的虚拟地址转换(即用户空间
映射),其虚拟地址的最高 8 位(63:56)会被转换硬件所忽略。这种机制
让这些位可供应用程序自由使用,其注意事项如下:
(1) 内核要求所有传递到 EL1 的用户空间地址带有 0x00 标记。
这意味着任何携带用户空间虚拟地址的系统调用(syscall)
参数 *必须* 在陷入内核前使它们的最高字节被清零。
(2) 非零标记在传递信号时不被保存。这意味着在应用程序中利用了
标记的信号处理函数无法依赖 siginfo_t 的用户空间虚拟
地址所携带的包含其内部域信息的标记。此规则的一个例外是
当信号是在调试观察点的异常处理程序中产生的,此时标记的
信息将被保存。
(3) 当使用带标记的指针时需特别留心,因为仅对两个虚拟地址
的高字节,C 编译器很可能无法判断它们是不同的。
此构架会阻止对带标记的 PC 指针的利用,因此在异常返回时,其高字节
将被设置成一个为 “55” 的扩展符。
@@ -0,0 +1,28 @@
.. SPDX-License-Identifier: GPL-2.0
处理器体系结构
==============
以下文档提供了具体架构实现的编程细节。
.. toctree::
:maxdepth: 2
mips/index
arm64/index
../arch/riscv/index
openrisc/index
parisc/index
loongarch/index
TODOList:
* arm/index
* m68k/index
* nios2/index
* powerpc/index
* s390/index
* sh/index
* sparc/index
* x86/index
* xtensa/index
@@ -0,0 +1,48 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/loongarch/booting.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
====================
启动 Linux/LoongArch
====================
:作者: 司延腾 <siyanteng@loongson.cn>
:日期: 2022年11月18日
BootLoader传递给内核的信息
==========================
LoongArch支持ACPI和FDT启动,需要传递给内核的信息包括memmap、initrd、cmdline、可
选的ACPI/FDT表等。
内核在 `kernel_entry` 入口处被传递以下参数:
- a0 = efi_boot: `efi_boot` 是一个标志,表示这个启动环境是否完全符合UEFI
的要求。
- a1 = cmdline: `cmdline` 是一个指向内核命令行的指针。
- a2 = systemtable: `systemtable` 指向EFI的系统表,在这个阶段涉及的所有
指针都是物理地址。
Linux/LoongArch内核镜像文件头
=============================
内核镜像是EFI镜像。作为PE文件,它们有一个64字节的头部结构体,如下所示::
u32 MZ_MAGIC /* "MZ", MS-DOS 头 */
u32 res0 = 0 /* 保留 */
u64 kernel_entry /* 内核入口点 */
u64 _end - _text /* 内核镜像有效大小 */
u64 load_offset /* 加载内核镜像相对内存起始地址的偏移量 */
u64 res1 = 0 /* 保留 */
u64 res2 = 0 /* 保留 */
u64 res3 = 0 /* 保留 */
u32 LINUX_PE_MAGIC /* 魔术数 */
u32 pe_header - _head /* 到PE头的偏移量 */
@@ -0,0 +1,8 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/loongarch/features.rst
:Translator: Huacai Chen <chenhuacai@loongson.cn>
.. kernel-feat:: features loongarch
@@ -0,0 +1,27 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/loongarch/index.rst
:Translator: Huacai Chen <chenhuacai@loongson.cn>
=================
LoongArch体系结构
=================
.. toctree::
:maxdepth: 2
:numbered:
introduction
booting
irq-chip-model
features
.. only:: subproject and html
Indices
=======
* :ref:`genindex`
@@ -0,0 +1,353 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/loongarch/introduction.rst
:Translator: Huacai Chen <chenhuacai@loongson.cn>
=============
LoongArch介绍
=============
LoongArch是一种新的RISC ISA,在一定程度上类似于MIPS和RISC-V。LoongArch指令集
包括一个精简32位版(LA32R)、一个标准32位版(LA32S)、一个64位版(LA64)。
LoongArch定义了四个特权级(PLV0~PLV3),其中PLV0是最高特权级,用于内核;而PLV3
是最低特权级,用于应用程序。本文档介绍了LoongArch的寄存器、基础指令集、虚拟内
存以及其他一些主题。
寄存器
======
LoongArch的寄存器包括通用寄存器(GPRs)、浮点寄存器(FPRs)、向量寄存器(VRs)
和用于特权模式(PLV0)的控制状态寄存器(CSRs)。
通用寄存器
----------
LoongArch包括32个通用寄存器( ``$r0`` ~ ``$r31`` ),LA32中每个寄存器为32位宽,
LA64中每个寄存器为64位宽。 ``$r0`` 的内容总是固定为0,而其他寄存器在体系结构层面
没有特殊功能。( ``$r1`` 算是一个例外,在BL指令中固定用作链接返回寄存器。)
内核使用了一套LoongArch寄存器约定,定义在LoongArch ELF psABI规范中,详细描述参见
:ref:`参考文献 <loongarch-references-zh_CN>`:
================= =============== =================== ==========
寄存器名 别名 用途 跨调用保持
================= =============== =================== ==========
``$r0`` ``$zero`` 常量0 不使用
``$r1`` ``$ra`` 返回地址 否
``$r2`` ``$tp`` TLS/线程信息指针 不使用
``$r3`` ``$sp`` 栈指针 是
``$r4``-``$r11`` ``$a0``-``$a7`` 参数寄存器 否
``$r4``-``$r5`` ``$v0``-``$v1`` 返回值 否
``$r12``-``$r20`` ``$t0``-``$t8`` 临时寄存器 否
``$r21`` ``$u0`` 每CPU变量基地址 不使用
``$r22`` ``$fp`` 帧指针 是
``$r23``-``$r31`` ``$s0``-``$s8`` 静态寄存器 是
================= =============== =================== ==========
.. note::
注意: ``$r21`` 寄存器在ELF psABI中保留未使用,但是在Linux内核用于保
存每CPU变量基地址。该寄存器没有ABI命名,不过在内核中称为 ``$u0`` 。在
一些遗留代码中有时可能见到 ``$v0`` 和 ``$v1`` ,它们是 ``$a0`` 和
``$a1`` 的别名,属于已经废弃的用法。
浮点寄存器
----------
当系统中存在FPU时,LoongArch有32个浮点寄存器( ``$f0`` ~ ``$f31`` )。在LA64
的CPU核上,每个寄存器均为64位宽。
浮点寄存器的使用约定与LoongArch ELF psABI规范的描述相同:
================= ================== =================== ==========
寄存器名 别名 用途 跨调用保持
================= ================== =================== ==========
``$f0``-``$f7`` ``$fa0``-``$fa7`` 参数寄存器 否
``$f0``-``$f1`` ``$fv0``-``$fv1`` 返回值 否
``$f8``-``$f23`` ``$ft0``-``$ft15`` 临时寄存器 否
``$f24``-``$f31`` ``$fs0``-``$fs7`` 静态寄存器 是
================= ================== =================== ==========
.. note::
注意:在一些遗留代码中有时可能见到 ``$fv0`` 和 ``$fv1`` ,它们是
``$fa0`` 和 ``$fa1`` 的别名,属于已经废弃的用法。
向量寄存器
----------
LoongArch现有两种向量扩展:
- 128位向量扩展LSX(全称Loongson SIMD eXtention),
- 256位向量扩展LASX(全称Loongson Advanced SIMD eXtention)。
LSX使用 ``$v0`` ~ ``$v31`` 向量寄存器,而LASX则使用 ``$x0`` ~ ``$x31`` 。
浮点寄存器和向量寄存器是复用的,比如:在一个实现了LSX和LASX的核上, ``$x0`` 的
低128位与 ``$v0`` 共用, ``$v0`` 的低64位与 ``$f0`` 共用,其他寄存器依此类推。
控制状态寄存器
--------------
控制状态寄存器只能在特权模式(PLV0)下访问:
================= ==================================== ==========
地址 全称描述 简称
================= ==================================== ==========
0x0 当前模式信息 CRMD
0x1 异常前模式信息 PRMD
0x2 扩展部件使能 EUEN
0x3 杂项控制 MISC
0x4 异常配置 ECFG
0x5 异常状态 ESTAT
0x6 异常返回地址 ERA
0x7 出错(Faulting)虚拟地址 BADV
0x8 出错(Faulting)指令字 BADI
0xC 异常入口地址 EENTRY
0x10 TLB索引 TLBIDX
0x11 TLB表项高位 TLBEHI
0x12 TLB表项低位0 TLBELO0
0x13 TLB表项低位1 TLBELO1
0x18 地址空间标识符 ASID
0x19 低半地址空间页全局目录基址 PGDL
0x1A 高半地址空间页全局目录基址 PGDH
0x1B 页全局目录基址 PGD
0x1C 页表遍历控制低半部分 PWCL
0x1D 页表遍历控制高半部分 PWCH
0x1E STLB页大小 STLBPS
0x1F 缩减虚地址配置 RVACFG
0x20 CPU编号 CPUID
0x21 特权资源配置信息1 PRCFG1
0x22 特权资源配置信息2 PRCFG2
0x23 特权资源配置信息3 PRCFG3
0x30+n (0≤n≤15) 数据保存寄存器 SAVEn
0x40 定时器编号 TID
0x41 定时器配置 TCFG
0x42 定时器值 TVAL
0x43 计时器补偿 CNTC
0x44 定时器中断清除 TICLR
0x60 LLBit相关控制 LLBCTL
0x80 实现相关控制1 IMPCTL1
0x81 实现相关控制2 IMPCTL2
0x88 TLB重填异常入口地址 TLBRENTRY
0x89 TLB重填异常出错(Faulting)虚地址 TLBRBADV
0x8A TLB重填异常返回地址 TLBRERA
0x8B TLB重填异常数据保存 TLBRSAVE
0x8C TLB重填异常表项低位0 TLBRELO0
0x8D TLB重填异常表项低位1 TLBRELO1
0x8E TLB重填异常表项高位 TLBEHI
0x8F TLB重填异常前模式信息 TLBRPRMD
0x90 机器错误控制 MERRCTL
0x91 机器错误信息1 MERRINFO1
0x92 机器错误信息2 MERRINFO2
0x93 机器错误异常入口地址 MERRENTRY
0x94 机器错误异常返回地址 MERRERA
0x95 机器错误异常数据保存 MERRSAVE
0x98 高速缓存标签 CTAG
0x180+n (0≤n≤3) 直接映射配置窗口n DMWn
0x200+2n (0≤n≤31) 性能监测配置n PMCFGn
0x201+2n (0≤n≤31) 性能监测计数器n PMCNTn
0x300 内存读写监视点整体控制 MWPC
0x301 内存读写监视点整体状态 MWPS
0x310+8n (0≤n≤7) 内存读写监视点n配置1 MWPnCFG1
0x311+8n (0≤n≤7) 内存读写监视点n配置2 MWPnCFG2
0x312+8n (0≤n≤7) 内存读写监视点n配置3 MWPnCFG3
0x313+8n (0≤n≤7) 内存读写监视点n配置4 MWPnCFG4
0x380 取指监视点整体控制 FWPC
0x381 取指监视点整体状态 FWPS
0x390+8n (0≤n≤7) 取指监视点n配置1 FWPnCFG1
0x391+8n (0≤n≤7) 取指监视点n配置2 FWPnCFG2
0x392+8n (0≤n≤7) 取指监视点n配置3 FWPnCFG3
0x393+8n (0≤n≤7) 取指监视点n配置4 FWPnCFG4
0x500 调试寄存器 DBG
0x501 调试异常返回地址 DERA
0x502 调试数据保存 DSAVE
================= ==================================== ==========
ERA,TLBRERA,MERRERA和DERA有时也分别称为EPC,TLBREPC,MERREPC和DEPC。
基础指令集
==========
指令格式
--------
LoongArch的指令字长为32位,一共有9种基本指令格式(以及一些变体):
=========== ==========================
格式名称 指令构成
=========== ==========================
2R Opcode + Rj + Rd
3R Opcode + Rk + Rj + Rd
4R Opcode + Ra + Rk + Rj + Rd
2RI8 Opcode + I8 + Rj + Rd
2RI12 Opcode + I12 + Rj + Rd
2RI14 Opcode + I14 + Rj + Rd
2RI16 Opcode + I16 + Rj + Rd
1RI21 Opcode + I21L + Rj + I21H
I26 Opcode + I26L + I26H
=========== ==========================
Opcode是指令操作码,Rj和Rk是源操作数(寄存器),Rd是目标操作数(寄存器),Ra是
4R-type格式特有的附加操作数(寄存器)。I8/I12/I14/I16/I21/I26分别是8位/12位/14位/
16位/21位/26位的立即数。其中较长的21位和26位立即数在指令字中被分割为高位部分与低位
部分,所以你们在这里的格式描述中能够看到I21L/I21H和I26L/I26H这样带后缀的表述。
指令列表
--------
为了简便起见,我们在此只罗列一下指令名称(助记符),需要详细信息请阅读
:ref:`参考文献 <loongarch-references-zh_CN>` 中的文档。
1. 算术运算指令::
ADD.W SUB.W ADDI.W ADD.D SUB.D ADDI.D
SLT SLTU SLTI SLTUI
AND OR NOR XOR ANDN ORN ANDI ORI XORI
MUL.W MULH.W MULH.WU DIV.W DIV.WU MOD.W MOD.WU
MUL.D MULH.D MULH.DU DIV.D DIV.DU MOD.D MOD.DU
PCADDI PCADDU12I PCADDU18I
LU12I.W LU32I.D LU52I.D ADDU16I.D
2. 移位运算指令::
SLL.W SRL.W SRA.W ROTR.W SLLI.W SRLI.W SRAI.W ROTRI.W
SLL.D SRL.D SRA.D ROTR.D SLLI.D SRLI.D SRAI.D ROTRI.D
3. 位域操作指令::
EXT.W.B EXT.W.H CLO.W CLO.D SLZ.W CLZ.D CTO.W CTO.D CTZ.W CTZ.D
BYTEPICK.W BYTEPICK.D BSTRINS.W BSTRINS.D BSTRPICK.W BSTRPICK.D
REVB.2H REVB.4H REVB.2W REVB.D REVH.2W REVH.D BITREV.4B BITREV.8B BITREV.W BITREV.D
MASKEQZ MASKNEZ
4. 分支转移指令::
BEQ BNE BLT BGE BLTU BGEU BEQZ BNEZ B BL JIRL
5. 访存读写指令::
LD.B LD.BU LD.H LD.HU LD.W LD.WU LD.D ST.B ST.H ST.W ST.D
LDX.B LDX.BU LDX.H LDX.HU LDX.W LDX.WU LDX.D STX.B STX.H STX.W STX.D
LDPTR.W LDPTR.D STPTR.W STPTR.D
PRELD PRELDX
6. 原子操作指令::
LL.W SC.W LL.D SC.D
AMSWAP.W AMSWAP.D AMADD.W AMADD.D AMAND.W AMAND.D AMOR.W AMOR.D AMXOR.W AMXOR.D
AMMAX.W AMMAX.D AMMIN.W AMMIN.D
7. 栅障指令::
IBAR DBAR
8. 特殊指令::
SYSCALL BREAK CPUCFG NOP IDLE ERTN(ERET) DBCL(DBGCALL) RDTIMEL.W RDTIMEH.W RDTIME.D
ASRTLE.D ASRTGT.D
9. 特权指令::
CSRRD CSRWR CSRXCHG
IOCSRRD.B IOCSRRD.H IOCSRRD.W IOCSRRD.D IOCSRWR.B IOCSRWR.H IOCSRWR.W IOCSRWR.D
CACOP TLBP(TLBSRCH) TLBRD TLBWR TLBFILL TLBCLR TLBFLUSH INVTLB LDDIR LDPTE
虚拟内存
========
LoongArch可以使用直接映射虚拟内存和分页映射虚拟内存。
直接映射虚拟内存通过CSR.DMWn(n=0~3)来进行配置,虚拟地址(VA)和物理地址(PA)
之间有简单的映射关系::
VA = PA + 固定偏移
分页映射的虚拟地址(VA)和物理地址(PA)有任意的映射关系,这种关系记录在TLB和页
表中。LoongArch的TLB包括一个全相联的MTLB(Multiple Page Size TLB,多样页大小TLB)
和一个组相联的STLB(Single Page Size TLB,单一页大小TLB)。
缺省状态下,LA32的整个虚拟地址空间配置如下:
============ =========================== ===========================
区段名 地址范围 属性
============ =========================== ===========================
``UVRANGE`` ``0x00000000 - 0x7FFFFFFF`` 分页映射, 可缓存, PLV0~3
``KPRANGE0`` ``0x80000000 - 0x9FFFFFFF`` 直接映射, 非缓存, PLV0
``KPRANGE1`` ``0xA0000000 - 0xBFFFFFFF`` 直接映射, 可缓存, PLV0
``KVRANGE`` ``0xC0000000 - 0xFFFFFFFF`` 分页映射, 可缓存, PLV0
============ =========================== ===========================
用户态(PLV3)只能访问UVRANGE,对于直接映射的KPRANGE0和KPRANGE1,将虚拟地址的第
30~31位清零就等于物理地址。例如:物理地址0x00001000对应的非缓存直接映射虚拟地址
是0x80001000,而其可缓存直接映射虚拟地址是0xA0001000。
缺省状态下,LA64的整个虚拟地址空间配置如下:
============ ====================== ==================================
区段名 地址范围 属性
============ ====================== ==================================
``XUVRANGE`` ``0x0000000000000000 - 分页映射, 可缓存, PLV0~3
0x3FFFFFFFFFFFFFFF``
``XSPRANGE`` ``0x4000000000000000 - 直接映射, 可缓存 / 非缓存, PLV0
0x7FFFFFFFFFFFFFFF``
``XKPRANGE`` ``0x8000000000000000 - 直接映射, 可缓存 / 非缓存, PLV0
0xBFFFFFFFFFFFFFFF``
``XKVRANGE`` ``0xC000000000000000 - 分页映射, 可缓存, PLV0
0xFFFFFFFFFFFFFFFF``
============ ====================== ==================================
用户态(PLV3)只能访问XUVRANGE,对于直接映射的XSPRANGE和XKPRANGE,将虚拟地址的第
60~63位清零就等于物理地址,而其缓存属性是通过虚拟地址的第60~61位配置的(0表示强序
非缓存,1表示一致可缓存,2表示弱序非缓存)。
目前,我们仅用XKPRANGE来进行直接映射,XSPRANGE保留给以后用。
此处给出一个直接映射的例子:物理地址0x00000000_00001000的强序非缓存直接映射虚拟地址
(在XKPRANGE中)是0x80000000_00001000,其一致可缓存直接映射虚拟地址(在XKPRANGE中)
是0x90000000_00001000,而其弱序非缓存直接映射虚拟地址(在XKPRANGE中)是0xA0000000_
00001000。
Loongson与LoongArch的关系
=========================
LoongArch是一种RISC指令集架构(ISA),不同于现存的任何一种ISA,而Loongson(即龙
芯)是一个处理器家族。龙芯包括三个系列:Loongson-1(龙芯1号)是32位处理器系列,
Loongson-2(龙芯2号)是低端64位处理器系列,而Loongson-3(龙芯3号)是高端64位处理
器系列。旧的龙芯处理器基于MIPS架构,而新的龙芯处理器基于LoongArch架构。以龙芯3号
为例:龙芯3A1000/3B1500/3A2000/3A3000/3A4000都是兼容MIPS的,而龙芯3A5000(以及将
来的型号)都是基于LoongArch的。
.. _loongarch-references-zh_CN:
参考文献
========
Loongson官方网站(龙芯中科技术股份有限公司):
http://www.loongson.cn/
Loongson与LoongArch的开发者网站(软件与文档资源):
http://www.loongnix.cn/
https://github.com/loongson/
https://loongson.github.io/LoongArch-Documentation/
LoongArch指令集架构的文档:
https://github.com/loongson/LoongArch-Documentation/releases/latest/download/LoongArch-Vol1-v1.10-CN.pdf (中文版)
https://github.com/loongson/LoongArch-Documentation/releases/latest/download/LoongArch-Vol1-v1.10-EN.pdf (英文版)
LoongArch的ELF psABI文档:
https://github.com/loongson/LoongArch-Documentation/releases/latest/download/LoongArch-ELF-ABI-v2.01-CN.pdf (中文版)
https://github.com/loongson/LoongArch-Documentation/releases/latest/download/LoongArch-ELF-ABI-v2.01-EN.pdf (英文版)
Loongson与LoongArch的Linux内核源码仓库:
https://git.kernel.org/pub/scm/linux/kernel/git/chenhuacai/linux-loongson.git
@@ -0,0 +1,278 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/loongarch/irq-chip-model.rst
:Translator: Huacai Chen <chenhuacai@loongson.cn>
==================================
LoongArch的IRQ芯片模型(层级关系)
==================================
目前,基于LoongArch的处理器(如龙芯3A5000)只能与LS7A芯片组配合工作。LoongArch计算机
中的中断控制器(即IRQ芯片)包括CPUINTC(CPU Core Interrupt Controller)、LIOINTC(
Legacy I/O Interrupt Controller)、EIOINTC(Extended I/O Interrupt Controller)、
HTVECINTC(Hyper-Transport Vector Interrupt Controller)、PCH-PIC(LS7A芯片组的主中
断控制器)、PCH-LPC(LS7A芯片组的LPC中断控制器)和PCH-MSI(MSI中断控制器)。
CPUINTC是一种CPU内部的每个核本地的中断控制器,LIOINTC/EIOINTC/HTVECINTC是CPU内部的
全局中断控制器(每个芯片一个,所有核共享),而PCH-PIC/PCH-LPC/PCH-MSI是CPU外部的中
断控制器(在配套芯片组里面)。这些中断控制器(或者说IRQ芯片)以一种层次树的组织形式
级联在一起,一共有两种层级关系模型(传统IRQ模型和扩展IRQ模型)。
传统IRQ模型
===========
在这种模型里面,IPI(Inter-Processor Interrupt)和CPU本地时钟中断直接发送到CPUINTC,
CPU串口(UARTs)中断发送到LIOINTC,而其他所有设备的中断则分别发送到所连接的PCH-PIC/
PCH-LPC/PCH-MSI,然后被HTVECINTC统一收集,再发送到LIOINTC,最后到达CPUINTC::
+-----+ +---------+ +-------+
| IPI | --> | CPUINTC | <-- | Timer |
+-----+ +---------+ +-------+
^
|
+---------+ +-------+
| LIOINTC | <-- | UARTs |
+---------+ +-------+
^
|
+-----------+
| HTVECINTC |
+-----------+
^ ^
| |
+---------+ +---------+
| PCH-PIC | | PCH-MSI |
+---------+ +---------+
^ ^ ^
| | |
+---------+ +---------+ +---------+
| PCH-LPC | | Devices | | Devices |
+---------+ +---------+ +---------+
^
|
+---------+
| Devices |
+---------+
扩展IRQ模型
===========
在这种模型里面,IPI(Inter-Processor Interrupt)和CPU本地时钟中断直接发送到CPUINTC,
CPU串口(UARTs)中断发送到LIOINTC,而其他所有设备的中断则分别发送到所连接的PCH-PIC/
PCH-LPC/PCH-MSI,然后被EIOINTC统一收集,再直接到达CPUINTC::
+-----+ +---------+ +-------+
| IPI | --> | CPUINTC | <-- | Timer |
+-----+ +---------+ +-------+
^ ^
| |
+---------+ +---------+ +-------+
| EIOINTC | | LIOINTC | <-- | UARTs |
+---------+ +---------+ +-------+
^ ^
| |
+---------+ +---------+
| PCH-PIC | | PCH-MSI |
+---------+ +---------+
^ ^ ^
| | |
+---------+ +---------+ +---------+
| PCH-LPC | | Devices | | Devices |
+---------+ +---------+ +---------+
^
|
+---------+
| Devices |
+---------+
虚拟扩展IRQ模型
===============
在这种模型里面, IPI(Inter-Processor Interrupt) 和CPU本地时钟中断直接发送到CPUINTC,
CPU串口 (UARTs) 中断发送到PCH-PIC, 而其他所有设备的中断则分别发送到所连接的PCH_PIC/
PCH-MSI, 然后V-EIOINTC统一收集,再直接到达CPUINTC::
+-----+ +-------------------+ +-------+
| IPI |--> | CPUINTC(0-255vcpu)| <-- | Timer |
+-----+ +-------------------+ +-------+
^
|
+-----------+
| V-EIOINTC |
+-----------+
^ ^
| |
+---------+ +---------+
| PCH-PIC | | PCH-MSI |
+---------+ +---------+
^ ^ ^
| | |
+--------+ +---------+ +---------+
| UARTs | | Devices | | Devices |
+--------+ +---------+ +---------+
V-EIOINTC 是EIOINTC的扩展, 仅工作在虚拟机模式下, 中断经EIOINTC最多可个路由到
4个虚拟CPU. 但中断经V-EIOINTC最多可个路由到256个虚拟CPU.
传统的EIOINTC中断控制器,中断路由分为两个部分:8比特用于控制路由到哪个CPU,
4比特用于控制路由到特定CPU的哪个中断管脚。控制CPU路由的8比特前4比特用于控制
路由到哪个EIOINTC节点,后4比特用于控制此节点哪个CPU。中断路由在选择CPU路由
和CPU中断管脚路由时,使用bitmap编码方式而不是正常编码方式,所以对于一个
EIOINTC中断控制器节点,中断只能路由到CPU0 - CPU3,中断管脚IP0-IP3。
V-EIOINTC新增了两个寄存器,支持中断路由到更多CPU个和中断管脚。
V-EIOINTC功能寄存器
-------------------
功能寄存器是只读寄存器,用于显示V-EIOINTC支持的特性,目前两个支持两个特性
EXTIOI_HAS_INT_ENCODE 和 EXTIOI_HAS_CPU_ENCODE。
特性EXTIOI_HAS_INT_ENCODE是传统EIOINTC中断控制器的一个特性,如果此比特为1,
显示CPU中断管脚路由方式支持正常编码,而不是bitmap编码,所以中断可以路由到
管脚IP0 - IP15。
特性EXTIOI_HAS_CPU_ENCODE是V-EIOINTC新增特性,如果此比特为1,表示CPU路由
方式支持正常编码,而不是bitmap编码,所以中断可以路由到CPU0 - CPU255。
V-EIOINTC配置寄存器
-------------------
配置寄存器是可读写寄存器,为了兼容性考虑,如果不写此寄存器,中断路由采用
和传统EIOINTC相同的路由设置。如果对应比特设置为1,表示采用正常路由方式而
不是bitmap编码的路由方式。
高级扩展IRQ模型
===============
在这种模型里面,IPI(Inter-Processor Interrupt)和CPU本地时钟中断直接发送到CPUINTC,
CPU串口(UARTs)中断发送到LIOINTC,PCH-MSI中断发送到AVECINTC,而后通过AVECINTC直接
送达CPUINTC,而其他所有设备的中断则分别发送到所连接的PCH-PIC/PCH-LPC,然后由EIOINTC
统一收集,再直接到达CPUINTC::
+-----+ +-----------------------+ +-------+
| IPI | --> | CPUINTC | <-- | Timer |
+-----+ +-----------------------+ +-------+
^ ^ ^
| | |
+---------+ +----------+ +---------+ +-------+
| EIOINTC | | AVECINTC | | LIOINTC | <-- | UARTs |
+---------+ +----------+ +---------+ +-------+
^ ^
| |
+---------+ +---------+
| PCH-PIC | | PCH-MSI |
+---------+ +---------+
^ ^ ^
| | |
+---------+ +---------+ +---------+
| Devices | | PCH-LPC | | Devices |
+---------+ +---------+ +---------+
^
|
+---------+
| Devices |
+---------+
高级扩展IRQ模型 (带重定向)
==========================
在这种模型里面,IPI(Inter-Processor Interrupt)和CPU本地时钟中断直接发送到CPUINTC,
CPU串口(UARTs)中断发送到LIOINTC,PCH-MSI中断首先发送到REDIRECT模块,完成重定向后发
送到AVECINTC,而后通过AVECINTC直接送达CPUINTC,而其他所有设备的中断则分别发送到所连
接的PCH-PIC/PCH-LPC,然后由EIOINTC统一收集,再直接到达CPUINTC::
+-----+ +-----------------------+ +-------+
| IPI | --> | CPUINTC | <-- | Timer |
+-----+ +-----------------------+ +-------+
^ ^ ^
| | |
| +----------+ |
+---------+ | AVECINTC | +---------+ +-------+
| EIOINTC | +----------+ | LIOINTC | <-- | UARTs |
+---------+ | REDIRECT | +---------+ +-------+
^ +----------+
| ^
| |
+---------+ +---------+
| PCH-PIC | | PCH-MSI |
+---------+ +---------+
^ ^ ^
| | |
+---------+ +---------+ +---------+
| Devices | | PCH-LPC | | Devices |
+---------+ +---------+ +---------+
^
|
+---------+
| Devices |
+---------+
ACPI相关的定义
==============
CPUINTC::
ACPI_MADT_TYPE_CORE_PIC;
struct acpi_madt_core_pic;
enum acpi_madt_core_pic_version;
LIOINTC::
ACPI_MADT_TYPE_LIO_PIC;
struct acpi_madt_lio_pic;
enum acpi_madt_lio_pic_version;
EIOINTC::
ACPI_MADT_TYPE_EIO_PIC;
struct acpi_madt_eio_pic;
enum acpi_madt_eio_pic_version;
HTVECINTC::
ACPI_MADT_TYPE_HT_PIC;
struct acpi_madt_ht_pic;
enum acpi_madt_ht_pic_version;
PCH-PIC::
ACPI_MADT_TYPE_BIO_PIC;
struct acpi_madt_bio_pic;
enum acpi_madt_bio_pic_version;
PCH-MSI::
ACPI_MADT_TYPE_MSI_PIC;
struct acpi_madt_msi_pic;
enum acpi_madt_msi_pic_version;
PCH-LPC::
ACPI_MADT_TYPE_LPC_PIC;
struct acpi_madt_lpc_pic;
enum acpi_madt_lpc_pic_version;
参考文献
========
龙芯3A5000的文档:
https://github.com/loongson/LoongArch-Documentation/releases/latest/download/Loongson-3A5000-usermanual-1.02-CN.pdf (中文版)
https://github.com/loongson/LoongArch-Documentation/releases/latest/download/Loongson-3A5000-usermanual-1.02-EN.pdf (英文版)
龙芯LS7A芯片组的文档:
https://github.com/loongson/LoongArch-Documentation/releases/latest/download/Loongson-7A1000-usermanual-2.00-CN.pdf (中文版)
https://github.com/loongson/LoongArch-Documentation/releases/latest/download/Loongson-7A1000-usermanual-2.00-EN.pdf (英文版)
.. note::
- CPUINTC:即《龙芯架构参考手册卷一》第7.4节所描述的CSR.ECFG/CSR.ESTAT寄存器及其
中断控制逻辑;
- LIOINTC:即《龙芯3A5000处理器使用手册》第11.1节所描述的“传统I/O中断”;
- EIOINTC:即《龙芯3A5000处理器使用手册》第11.2节所描述的“扩展I/O中断”;
- HTVECINTC:即《龙芯3A5000处理器使用手册》第14.3节所描述的“HyperTransport中断”;
- PCH-PIC/PCH-MSI:即《龙芯7A1000桥片用户手册》第5章所描述的“中断控制器”;
- PCH-LPC:即《龙芯7A1000桥片用户手册》第24.3节所描述的“LPC中断”。
@@ -0,0 +1,34 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/mips/booting.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_booting:
BMIPS设备树引导
------------------------
一些bootloaders只支持在内核镜像开始地址处的单一入口点。而其它
bootloaders将跳转到ELF的开始地址处。两种方案都支持的;因为
CONFIG_BOOT_RAW=y and CONFIG_NO_EXCEPT_FILL=y, 所以第一条指令
会立即跳转到kernel_entry()入口处执行。
与arch/arm情况(b)类似,dt感知的引导加载程序需要设置以下寄存器:
a0 : 0
a1 : 0xffffffff
a2 : RAM中指向设备树块的物理指针(在chapterII中定义)。
设备树可以位于前512MB物理地址空间(0x00000000 -
0x1fffffff)的任何位置,以64位边界对齐。
传统bootloaders不会使用这样的约定,并且它们不传入DT块。
在这种情况下,Linux将通过选中CONFIG_DT_*查找DTB。
以上约定只在32位系统中定义,因为目前没有任何64位的BMIPS实现。
@@ -0,0 +1,13 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/mips/features.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_features:
.. kernel-feat:: features mips
@@ -0,0 +1,29 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/mips/index.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
===========================
MIPS特性文档
===========================
.. toctree::
:maxdepth: 2
:numbered:
booting
ingenic-tcu
features
.. only:: subproject and html
Indices
=======
* :ref:`genindex`
@@ -0,0 +1,72 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/mips/ingenic-tcu.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_ingenic-tcu:
===============================================
君正 JZ47xx SoC定时器/计数器硬件单元
===============================================
君正 JZ47xx SoC中的定时器/计数器单元(TCU)是一个多功能硬件块。它有多达
8个通道,可以用作计数器,计时器,或脉冲宽度调制器。
- JZ4725B, JZ4750, JZ4755 只有6个TCU通道。其它SoC都有8个通道。
- JZ4725B引入了一个独立的通道,称为操作系统计时器(OST)。这是一个32位可
编程定时器。在JZ4760B及以上型号上,它是64位的。
- 每个TCU通道都有自己的时钟源,可以通过 TCSR 寄存器设置通道的父级时钟
源(pclk、ext、rtc)、开关以及分频。
- 看门狗和OST硬件模块在它们的寄存器空间中也有相同形式的TCSR寄存器。
- 用于关闭/开启的 TCU 寄存器也可以关闭/开启看门狗和 OST 时钟。
- 每个TCU通道在两种模式的其中一种模式下运行:
- 模式 TCU1:通道无法在睡眠模式下运行,但更易于操作。
- 模式 TCU2:通道可以在睡眠模式下运行,但操作比 TCU1 通道复杂一些。
- 每个 TCU 通道的模式取决于使用的SoC:
- 在最老的SoC(高于JZ4740),八个通道都运行在TCU1模式。
- 在 JZ4725B,通道5运行在TCU2,其它通道则运行在TCU1。
- 在最新的SoC(JZ4750及之后),通道1-2运行在TCU2,其它通道则运行
在TCU1。
- 每个通道都可以生成中断。有些通道共享一条中断线,而有些没有,其在SoC型
号之间的变更:
- 在很老的SoC(JZ4740及更低),通道0和通道1有它们自己的中断线;通
道2-7共享最后一条中断线。
- 在 JZ4725B,通道0有它自己的中断线;通道1-5共享一条中断线;OST
使用最后一条中断线。
- 在比较新的SoC(JZ4750及以后),通道5有它自己的中断线;通
道0-4和(如果是8通道)6-7全部共享一条中断线;OST使用最后一条中
断线。
实现
====
TCU硬件的功能分布在多个驱动程序:
============== ===================================
时钟 drivers/clk/ingenic/tcu.c
中断 drivers/irqchip/irq-ingenic-tcu.c
定时器 drivers/clocksource/ingenic-timer.c
OST drivers/clocksource/ingenic-ost.c
脉冲宽度调制器 drivers/pwm/pwm-jz4740.c
看门狗 drivers/watchdog/jz4740_wdt.c
============== ===================================
因为可以从相同的寄存器控制属于不同驱动程序和框架的TCU的各种功能,所以
所有这些驱动程序都通过相同的控制总线通用接口访问它们的寄存器。
有关TCU驱动程序的设备树绑定的更多信息,请参阅:
Documentation/devicetree/bindings/timer/ingenic,tcu.yaml.
@@ -0,0 +1,32 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/openrisc/index.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_openrisc_index:
=================
OpenRISC 体系架构
=================
.. toctree::
:maxdepth: 2
openrisc_port
todo
Todolist:
features
.. only:: subproject and html
Indices
=======
* :ref:`genindex`
@@ -0,0 +1,127 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/openrisc/openrisc_port.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_openrisc_port:
==============
OpenRISC Linux
==============
这是Linux对OpenRISC类微处理器的移植;具体来说,最早移植目标是32位
OpenRISC 1000系列(或1k)。
关于OpenRISC处理器和正在进行中的开发的信息:
======= ==============================
网站 https://openrisc.io
邮箱 linux-openrisc@vger.kernel.org
======= ==============================
---------------------------------------------------------------------
OpenRISC工具链和Linux的构建指南
===============================
为了构建和运行Linux for OpenRISC,你至少需要一个基本的工具链,或许
还需要架构模拟器。 这里概述了准备就位这些部分的步骤。
1) 工具链
工具链二进制文件可以从openrisc.io或我们的github发布页面获得。不同
工具链的构建指南可以在openrisc.io或Stafford的工具链构建和发布脚本
中找到。
====== ==========================================================
二进制 https://github.com/stffrdhrn/or1k-toolchain-build/releases
工具链 https://openrisc.io/software
构建 https://github.com/stffrdhrn/or1k-toolchain-build
====== ==========================================================
2) 构建
像往常一样构建Linux内核::
make ARCH=openrisc CROSS_COMPILE="or1k-linux-" defconfig
make ARCH=openrisc CROSS_COMPILE="or1k-linux-"
3) 在FPGA上运行(可选)
OpenRISC社区通常使用FuseSoC来管理构建和编程SoC到FPGA中。 下面是用
OpenRISC SoC对De0 Nano开发板进行编程的一个例子。 在构建过程中,
FPGA RTL是从FuseSoC IP核库中下载的代码,并使用FPGA供应商工具构建。
二进制文件用openocd加载到电路板上。
::
git clone https://github.com/olofk/fusesoc
cd fusesoc
sudo pip install -e .
fusesoc init
fusesoc build de0_nano
fusesoc pgm de0_nano
openocd -f interface/altera-usb-blaster.cfg \
-f board/or1k_generic.cfg
telnet localhost 4444
> init
> halt; load_image vmlinux ; reset
4) 在模拟器上运行(可选)
QEMU是一个处理器仿真器,我们推荐它来模拟OpenRISC平台。 请按照QEMU网
站上的OpenRISC说明,让Linux在QEMU上运行。 你可以自己构建QEMU,但你的
Linux发行版可能提供了支持OpenRISC的二进制包。
============= ======================================================
qemu openrisc https://wiki.qemu.org/Documentation/Platforms/OpenRISC
============= ======================================================
---------------------------------------------------------------------
术语表
======
代码中使用了以下符号约定以将范围限制在几个特定处理器实现上:
========= =======================
openrisc: OpenRISC类型处理器
or1k: OpenRISC 1000系列处理器
or1200: OpenRISC 1200处理器
========= =======================
---------------------------------------------------------------------
历史
====
2003-11-18 Matjaz Breskvar (phoenix@bsemi.com)
将linux初步移植到OpenRISC或32架构。
所有的核心功能都实现了,并且可以使用。
2003-12-08 Matjaz Breskvar (phoenix@bsemi.com)
彻底改变TLB失误处理。
重写异常处理。
在默认的initrd中实现了sash-3.6的所有功能。
大幅改进的版本。
2004-04-10 Matjaz Breskvar (phoenix@bsemi.com)
大量的bug修复。
支持以太网,http和telnet服务器功能。
可以运行许多标准的linux应用程序。
2004-06-26 Matjaz Breskvar (phoenix@bsemi.com)
移植到2.6.x。
2004-11-30 Matjaz Breskvar (phoenix@bsemi.com)
大量的bug修复和增强功能。
增加了opencores framebuffer驱动。
2010-10-09 Jonas Bonn (jonas@southpole.se)
重大重写,使其与上游的Linux 2.6.36看齐。
@@ -0,0 +1,23 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/openrisc/todo.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_openrisc_todo.rst:
========
待办事项
========
OpenRISC Linux的移植已经完全投入使用,并且从 2.6.35 开始就一直在上游同步。
然而,还有一些剩余的项目需要在未来几个月内完成。 下面是一个即将进行调查的已知
不尽完美的项目列表,即我们的待办事项列表。
- 实现其余的DMA API……dma_map_sg等。
- 完成重命名清理工作……代码中提到了or32,这是架构的一个老名字。 我们
已经确定的名字是or1k,这个改变正在以缓慢积累的方式进行。 目前,or32相当
于or1k。
@@ -0,0 +1,45 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/parisc/debugging.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_parisc_debugging:
=================
调试PA-RISC
=================
好吧,这里有一些关于调试linux/parisc的较底层部分的信息。
1. 绝对地址
=====================
很多汇编代码目前运行在实模式下,这意味着会使用绝对地址,而不是像内核其他
部分那样使用虚拟地址。要将绝对地址转换为虚拟地址,你可以在System.map中查
找,添加__PAGE_OFFSET(目前是0x10000000)。
2. HPMCs
========
当实模式的代码试图访问不存在的内存时,会出现HPMC(high priority machine
check)而不是内核oops。若要调试HPMC,请尝试找到系统响应程序/请求程序地址。
系统请求程序地址应该与(某)处理器的HPA(I/O范围内的高地址)相匹配;系统响应程
序地址是实模式代码试图访问的地址。
系统响应程序地址的典型值是大于__PAGE_OFFSET (0x10000000)的地址,这意味着
在实模式试图访问它之前,虚拟地址没有被翻译成物理地址。
3. 有趣的Q位
============
某些非常关键的代码必须清除PSW中的Q位。当Q位被清除时,CPU不会更新中断处理
程序所读取的寄存器,以找出机器被中断的位置——所以如果你在清除Q位的指令和再
次设置Q位的RFI之间遇到中断,你不知道它到底发生在哪里。如果你幸运的话,IAOQ
会指向清除Q位的指令,如果你不幸运的话,它会指向任何地方。通常Q位的问题会
表现为无法解释的系统挂起或物理内存越界。
@@ -0,0 +1,31 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/parisc/index.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_parisc_index:
====================
PA-RISC体系架构
====================
.. toctree::
:maxdepth: 2
debugging
registers
Todolist:
features
.. only:: subproject and html
Indices
=======
* :ref:`genindex`
@@ -0,0 +1,156 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/parisc/registers.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_parisc_registers:
=========================
Linux/PA-RISC的寄存器用法
=========================
[ 用星号表示目前尚未实现的计划用途。 ]
ABI约定的通用寄存器
===================
控制寄存器
----------
============================ =================================
CR 0 (恢复计数器) 用于ptrace
CR 1-CR 7(无定义) 未使用
CR 8 (Protection ID) 每进程值*
CR 9, 12, 13 (PIDS) 未使用
CR10 (CCR) FPU延迟保存*
CR11 按照ABI的规定(SAR)
CR14 (中断向量) 初始化为 fault_vector
CR15 (EIEM) 所有位初始化为1*
CR16 (间隔计时器) 读取周期数/写入开始时间间隔计时器
CR17-CR22 中断参数
CR19 中断指令寄存器
CR20 中断空间寄存器
CR21 中断偏移量寄存器
CR22 中断 PSW
CR23 (EIRR) 读取未决中断/写入清除位
CR24 (TR 0) 内核空间页目录指针
CR25 (TR 1) 用户空间页目录指针
CR26 (TR 2) 不使用
CR27 (TR 3) 线程描述符指针
CR28 (TR 4) 不使用
CR29 (TR 5) 不使用
CR30 (TR 6) 当前 / 0
CR31 (TR 7) 临时寄存器,在不同地方使用
============================ =================================
空间寄存器(内核模式)
----------------------
======== ==============================
SR0 临时空间寄存器
SR4-SR7 设置为0
SR1 临时空间寄存器
SR2 内核不应该破坏它
SR3 用于用户空间访问(当前进程)
======== ==============================
空间寄存器(用户模式)
----------------------
======== ============================
SR0 临时空间寄存器
SR1 临时空间寄存器
SR2 保存Linux gateway page的空间
SR3 在内核中保存用户地址空间的值
SR4-SR7 定义了用户/内核的短地址空间
======== ============================
处理器状态字
------------
====================== ================================================
W (64位地址) 0
E (小尾端) 0
S (安全间隔计时器) 0
T (产生分支陷阱) 0
H (高特权级陷阱) 0
L (低特权级陷阱) 0
N (撤销下一条指令) 被C代码使用
X (数据存储中断禁用) 0
B (产生分支) 被C代码使用
C (代码地址转译) 1, 在执行实模式代码时为0
V (除法步长校正) 被C代码使用
M (HPMC 掩码) 0, 在执行HPMC操作*时为1
C/B (进/借 位) 被C代码使用
O (有序引用) 1*
F (性能监视器) 0
R (回收计数器陷阱) 0
Q (收集中断状态) 1 (在rfi之前的代码中为0)
P (保护标识符) 1*
D (数据地址转译) 1, 在执行实模式代码时为0
I (外部中断掩码) 由cli()/sti()宏使用。
====================== ================================================
“隐形”寄存器(影子寄存器)
---------------------------
============= ===================
PSW W 默认值 0
PSW E 默认值 0
影子寄存器 被中断处理代码使用
TOC启用位 1
============= ===================
----------------------------------------------------------
PA-RISC架构定义了7个寄存器作为“影子寄存器”。这些寄存器在
RETURN FROM INTERRUPTION AND RESTORE指令中使用,通过消
除中断处理程序中对一般寄存器(GR)的保存和恢复的需要来减
少状态保存和恢复时间。影子寄存器是GRs 1, 8, 9, 16, 17,
24和25。
-------------------------------------------------------------------------
寄存器使用说明,最初由John Marvin提供,并由Randolph Chung提供一些补充说明。
对于通用寄存器:
r1,r2,r19-r26,r28,r29 & r31可以在不保存它们的情况下被使用。当然,如果你
关心它们,在调用另一个程序之前,你也需要保存它们。上面的一些寄存器确实
有特殊的含义,你应该注意一下:
r1:
addil指令是硬性规定将其结果放在r1中,所以如果你使用这条指令要
注意这点。
r2:
这就是返回指针。一般来说,你不想使用它,因为你需要这个指针来返
回给你的调用者。然而,它与这组寄存器组合在一起,因为调用者不能
依赖你返回时的值是相同的,也就是说,你可以将r2复制到另一个寄存
器,并在作废r2后通过该寄存器返回,这应该不会给调用程序带来问题。
r19-r22:
这些通常被认为是临时寄存器。
请注意,在64位中它们是arg7-arg4。
r23-r26:
这些是arg3-arg0,也就是说,如果你不再关心传入的值,你可以使用
它们。
r28,r29:
这俩是ret0和ret1。它们是你传入返回值的地方。r28是主返回值。当返回
小结构体时,r29也可以用来将数据传回给调用程序。
r30:
栈指针
r31:
ble指令将返回指针放在这里。
r3-r18,r27,r30需要被保存和恢复。r3-r18只是一般用途的寄存器。
r27是数据指针,用来使对全局变量的引用更容易。r30是栈指针。
@@ -0,0 +1,69 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/riscv/boot-image-header.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_boot-image-header.rst:
==========================
RISC-V Linux启动镜像文件头
==========================
:Author: Atish Patra <atish.patra@wdc.com>
:Date: 20 May 2019
此文档仅描述RISC-V Linux 启动文件头的详情。
TODO:
写一个完整的启动指南。
在解压后的Linux内核镜像中存在以下64字节的文件头::
u32 code0; /* Executable code */
u32 code1; /* Executable code */
u64 text_offset; /* Image load offset, little endian */
u64 image_size; /* Effective Image size, little endian */
u64 flags; /* kernel flags, little endian */
u32 version; /* Version of this header */
u32 res1 = 0; /* Reserved */
u64 res2 = 0; /* Reserved */
u64 magic = 0x5643534952; /* Magic number, little endian, "RISCV" */
u32 magic2 = 0x05435352; /* Magic number 2, little endian, "RSC\x05" */
u32 res3; /* Reserved for PE COFF offset */
这种头格式与PE/COFF文件头兼容,并在很大程度上受到ARM64文件头的启发。因此,ARM64
和RISC-V文件头可以在未来合并为一个共同的头。
注意
====
- 将来也可以复用这个文件头,用来对RISC-V的EFI桩提供支持。为了使内核镜像如同一个
EFI应用程序一样加载,EFI规范中规定在内核镜像的开始需要PE/COFF镜像文件头。为了
支持EFI桩,应该用“MZ”魔术字符替换掉code0,并且res3(偏移量未0x3c)应指向PE/COFF
文件头的其余部分.
- 表示文件头版本号的Drop-bit位域
========== ==========
Bits 0:15 次要 版本
Bits 16:31 主要 版本
========== ==========
这保持了新旧版本之间的兼容性。
当前版本被定义为0.2。
- 从版本0.2开始,结构体成员“magic”就已经被弃用,在之后的版本中,可能会移除掉它。
最初,该成员应该与ARM64头的“magic”成员匹配,但遗憾的是并没有。
“magic2”成员代替“magic”成员与ARM64头相匹配。
- 在当前的文件头,标志位域只剩下了一个位。
===== ==============================
Bit 0 内核字节序。1 if BE, 0 if LE.
===== ==============================
- 对于引导加载程序加载内核映像来说,image_size成员对引导加载程序而言是必须的,否
则将引导失败。
@@ -0,0 +1,155 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/riscv/boot.rst
:翻译:
龙进 Jin Long <longjin@dragonos.org>
========================
RISC-V内核启动要求和限制
========================
:Author: Alexandre Ghiti <alexghiti@rivosinc.com>
:Date: 23 May 2023
这份文档描述了RISC-V内核对引导加载程序和固件的期望,以及任何开发者在接触
早期启动过程时必须牢记的约束。在这份文档中, ``早期启动过程`` 指的是在最
终虚拟映射设置之前运行的任何代码。
内核预加载的要求和限制
======================
RISC-V内核对引导加载程序和平台固件有以下要求:
寄存器状态
----------
RISC-V内核期望:
* ``$a0`` 应包含当前核心的hartid。
* ``$a1`` 应包含内存中设备树的地址。
CSR 寄存器状态
--------------
RISC-V内核期望:
* ``$satp = 0``: 如果存在MMU,必须将其禁用。
为常驻固件保留的内存
--------------------
RISC-V内核在直接映射中不能映射任何常驻内存或用PMPs保护的内存,
因此固件必须根据设备树规范 和/或 UEFI规范正确标记这些区域。
内核的位置
----------
RISC-V内核期望被放置在PMD边界(对于rv64为2MB对齐,对于rv32为4MB对齐)。
请注意,如果不是这样,EFI stub 将重定位内核。
硬件描述
--------
固件可以将设备树或ACPI表传递给RISC-V内核。
设备树可以直接从前一阶段通过$a1寄存器传递给内核,或者在使用UEFI启动时,
可以通过EFI配置表传递。
ACPI表通过EFI配置表传递给内核。在这种情况下,EFI stub 仍然会创建一个
小的设备树。请参阅下面的"EFI stub 和设备树"部分,了解这个设备树的详细
信息。
内核入口
--------
在SMP系统中,有两种方法可以进入内核:
- ``RISCV_BOOT_SPINWAIT``:固件在内核中释放所有的hart,一个hart赢
得抽奖并执行早期启动代码,而其他的hart则停在那里等待初始化完成。这种
方法主要用于支持没有SBI HSM扩展和M模式RISC-V内核的旧固件。
- ``有序启动``:固件只释放一个将执行初始化阶段的hart,然后使用SBI HSM
扩展启动所有其他的hart。有序启动方法是启动RISC-V内核的首选启动方法,
因为它可以支持CPU热插拔和kexec。
UEFI
----
UEFI 内存映射
~~~~~~~~~~~~~
使用UEFI启动时,RISC-V内核将只使用EFI内存映射来填充系统内存。
UEFI固件必须解析 ``/reserved-memory`` 设备树节点的子节点,并遵守设备
树规范,将这些子节点的属性( ``no-map`` 和 ``reusable`` )转换为其正
确的EFI等价物(参见设备树规范v0.4-rc1的"3.5.4/reserved-memory和
UEFI"部分)。
RISCV_EFI_BOOT_PROTOCOL
~~~~~~~~~~~~~~~~~~~~~~~
使用UEFI启动时,EFI stub 需要引导hartid以便将其传递给 ``$a1`` 中的
RISC-V内核。EFI stub使用以下方法之一获取引导hartid:
- ``RISCV_EFI_BOOT_PROTOCOL`` (**首选**)。
- ``boot-hartid`` 设备树子节点(**已弃用**)。
任何新的固件都必须实现 ``RISCV_EFI_BOOT_PROTOCOL``,因为基于设备树
的方法现已被弃用。
早期启动的要求和约束
====================
RISC-V内核的早期启动过程遵循以下约束:
EFI stub 和设备树
-----------------
使用UEFI启动时,EFI stub 会用与arm64相同的参数补充(或创建)设备树,
这些参数在Documentation/arch/arm/uefi.rst中的
"UEFI kernel supporton ARM"段落中有描述。
虚拟映射安装
------------
在RISC-V内核中,虚拟映射的安装分为两步进行:
1. ``setup_vm()`` 在 ``early_pg_dir`` 中安装一个临时的内核映射,这
允许发现系统内存。 此时只有内核文本/数据被映射。在建立这个映射时,
不能进行分配(因为系统内存还未知),所以``early_pg_dir``页表是静
态分配的(每个级别只使用一个表)。
2. ``setup_vm_final()`` 在 ``swapper_pg_dir`` 中创建最终的内核映
射,并利用发现的系统内存 创建线性映射。在建立这个映射时,内核可以
分配内存,但不能直接访问它(因为直接映射还不存在),所以它使用fixmap
区域的临时映射来访问新分配的页表级别。
为了让 ``virt_to_phys()`` 和 ``phys_to_virt()`` 能够正确地将直接
映射地址转换为物理地址,它们需要知道DRAM的起始位置。这发生在步骤1之后,
就在步骤2安装直接映射之前(参见arch/riscv/mm/init.c中的
``setup_bootmem()`` 函数)。在安装最终虚拟映射之前使用这些宏时必须
仔细检查。
通过fixmap进行设备树映射
------------------------
由于 ``reserved_mem`` 数组是用 ``setup_vm()`` 建立的虚拟地址初始化
的,并且与``setup_vm_final()``建立的映射一起使用,RISC-V内核使用
fixmap区域来映射设备树。这确保设备树可以通过两种虚拟映射访问。
Pre-MMU执行
-----------
在建立第一个虚拟映射之前,需要运行一些代码。这些包括第一个虚拟映射的安装本身,
早期替代方案的修补,以及内核命令行的早期解析。这些代码必须非常小心地编译,因为:
- ``-fno-pie``:这对于使用``-fPIE``的可重定位内核是必需的,否则,任何对
全局符号的访问都将通过 GOT进行,而GOT只是虚拟地重新定位。
- ``-mcmodel=medany``:任何对全局符号的访问都必须是PC相对的,以避免在设
置MMU之前发生任何重定位。
- *所有* 的仪表化功能也必须被禁用(包括KASAN,ftrace和其他)。
由于使用来自不同编译单元的符号需要用这些标志编译该单元,我们建议尽可能不要使用
外部符号。
@@ -0,0 +1,31 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/riscv/index.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_riscv_index:
===============
RISC-V 体系结构
===============
.. toctree::
:maxdepth: 1
boot
boot-image-header
vm-layout
patch-acceptance
.. only:: subproject and html
目录
====
* :ref:`genindex`
@@ -0,0 +1,33 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/riscv/patch-acceptance.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_riscv_patch-acceptance:
arch/riscv 开发者维护指南
=========================
概述
----
RISC-V指令集体系结构是公开开发的:
正在进行的草案可供所有人查看和测试实现。新模块或者扩展草案可能会在开发过程中发
生更改---有时以不兼容的方式对以前的草案进行更改。这种灵活性可能会给RISC-V Linux
维护者带来挑战。Linux开发过程更喜欢经过良好检查和测试的代码,而不是试验代码。我
们希望推广同样的规则到即将被内核合并的RISC-V相关代码。
附加的提交检查单
----------------
我们仅接受相关标准已经被RISC-V基金会标准为“已批准”或“已冻结”的扩展或模块的补丁。
(开发者当然可以维护自己的Linux内核树,其中包含所需代码扩展草案的代码。)
此外,RISC-V规范允许爱好者创建自己的自定义扩展。这些自定义拓展不需要通过RISC-V
基金会的任何审核或批准。为了避免将爱好者一些特别的RISC-V拓展添加进内核代码带来
的维护复杂性和对性能的潜在影响,我们将只接受RISC-V基金会正式冻结或批准的的扩展
补丁。(开发者当然可以维护自己的Linux内核树,其中包含他们想要的任何自定义扩展
的代码。)
@@ -0,0 +1,104 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/arch/riscv/vm-layout.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
Binbin Zhou <zhoubinbin@loongson.cn>
============================
RISC-V Linux上的虚拟内存布局
============================
:作者: Alexandre Ghiti <alex@ghiti.fr>
:日期: 12 February 2021
这份文件描述了RISC-V Linux内核使用的虚拟内存布局。
32位 RISC-V Linux 内核
======================
RISC-V Linux Kernel SV32
------------------------
TODO
64位 RISC-V Linux 内核
======================
RISC-V特权架构文档指出,64位地址 "必须使第63-48位值都等于第47位,否则将发生缺页异常。":这将虚
拟地址空间分成两半,中间有一个非常大的洞,下半部分是用户空间所在的地方,上半部分是RISC-V Linux
内核所在的地方。
RISC-V Linux Kernel SV39
------------------------
::
========================================================================================================================
开始地址 | 偏移 | 结束地址 | 大小 | 虚拟内存区域描述
========================================================================================================================
| | | |
0000000000000000 | 0 | 0000003fffffffff | 256 GB | 用户空间虚拟内存,每个内存管理器不同
__________________|____________|__________________|_________|___________________________________________________________
| | | |
0000004000000000 | +256 GB | ffffffbfffffffff | ~16M TB | ... 巨大的、几乎64位宽的直到内核映射的-256GB地方
| | | | 开始偏移的非经典虚拟内存地址空洞。
| | | |
__________________|____________|__________________|_________|___________________________________________________________
|
| 内核空间的虚拟内存,在所有进程之间共享:
____________________________________________________________|___________________________________________________________
| | | |
ffffffc6fee00000 | -228 GB | ffffffc6feffffff | 2 MB | fixmap
ffffffc6ff000000 | -228 GB | ffffffc6ffffffff | 16 MB | PCI io
ffffffc700000000 | -228 GB | ffffffc7ffffffff | 4 GB | vmemmap
ffffffc800000000 | -224 GB | ffffffd7ffffffff | 64 GB | vmalloc/ioremap space
ffffffd800000000 | -160 GB | fffffff6ffffffff | 124 GB | 直接映射所有物理内存
fffffff700000000 | -36 GB | fffffffeffffffff | 32 GB | kasan
__________________|____________|__________________|_________|____________________________________________________________
|
|
____________________________________________________________|____________________________________________________________
| | | |
ffffffff00000000 | -4 GB | ffffffff7fffffff | 2 GB | modules, BPF
ffffffff80000000 | -2 GB | ffffffffffffffff | 2 GB | kernel
__________________|____________|__________________|_________|____________________________________________________________
RISC-V Linux Kernel SV48
------------------------
::
========================================================================================================================
开始地址 | 偏移 | 结束地址 | 大小 | 虚拟内存区域描述
========================================================================================================================
| | | |
0000000000000000 | 0 | 00007fffffffffff | 128 TB | 用户空间虚拟内存,每个内存管理器不同
__________________|____________|__________________|_________|___________________________________________________________
| | | |
0000800000000000 | +128 TB | ffff7fffffffffff | ~16M TB | ... 巨大的、几乎64位宽的直到内核映射的-128TB地方
| | | | 开始偏移的非经典虚拟内存地址空洞。
| | | |
__________________|____________|__________________|_________|___________________________________________________________
|
| 内核空间的虚拟内存,在所有进程之间共享:
____________________________________________________________|___________________________________________________________
| | | |
ffff8d7ffee00000 | -114.5 TB | ffff8d7ffeffffff | 2 MB | fixmap
ffff8d7fff000000 | -114.5 TB | ffff8d7fffffffff | 16 MB | PCI io
ffff8d8000000000 | -114.5 TB | ffff8f7fffffffff | 2 TB | vmemmap
ffff8f8000000000 | -112.5 TB | ffffaf7fffffffff | 32 TB | vmalloc/ioremap space
ffffaf8000000000 | -80.5 TB | ffffef7fffffffff | 64 TB | 直接映射所有物理内存
ffffef8000000000 | -16.5 TB | fffffffeffffffff | 16.5 TB | kasan
__________________|____________|__________________|_________|____________________________________________________________
|
| 从此处开始,与39-bit布局相同:
____________________________________________________________|____________________________________________________________
| | | |
ffffffff00000000 | -4 GB | ffffffff7fffffff | 2 GB | modules, BPF
ffffffff80000000 | -2 GB | ffffffffffffffff | 2 GB | kernel
__________________|____________|__________________|_________|____________________________________________________________
@@ -0,0 +1,130 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/block/blk-mq.rst
:翻译:
柯子杰 kezijie <kezijie@leap-io-kernel.com>
:校译:
================================================
多队列块设备 I/O 排队机制 (blk-mq)
================================================
多队列块设备 I/O 排队机制提供了一组 API,使高速存储设备能够同时在多个队列中
处理并发的 I/O 请求并将其提交到块设备,从而实现极高的每秒输入/输出操作次数
(IOPS),充分发挥现代存储设备的并行能力。
介绍
====
背景
----
磁盘从 Linux 内核开发初期就已成为事实上的标准。块 I/O 子系统的目标是尽可能
为此类设备提供最佳性能,因为它们在进行随机访问时代价极高,性能瓶颈主要在机械
运动部件上,其速度远低于存储栈中其他任何层。其中一个软件优化例子是根据硬盘磁
头当前的位置重新排序读/写请求。
然而,随着固态硬盘和非易失性存储的发展,它们没有机械部件,也不存在随机访问代
码,并能够进行高速并行访问,存储栈的瓶颈从存储设备转移到了操作系统。为了充分
利用这些设备设计中的并行性,引入了多队列机制。
原来的设计只有一个队列来存储块设备 I/O 请求,并且只使用一个锁。由于缓存中的
脏数据和多处理器共享单锁的瓶颈,这种设计在 SMP 系统中扩展性不佳。当不同进程
(或同一进程在不同 CPU 上)同时执行块设备 I/O 时,该单队列模型还会出现严重
的拥塞问题。为了解决这些问题,blk-mq API 引入了多个队列,每个队列在本地 CPU
上拥有独立的入口点,从而消除了对全局锁的需求。关于其具体工作机制的更深入说明,
请参见下一节( `工作原理`_ )。
工作原理
--------
当用户空间执行对块设备的 I/O(例如读写文件)时,blk-mq 便会介入:它将存储和
管理发送到块设备的 I/O 请求,充当用户空间(文件系统,如果存在的话)与块设备驱
动之间的中间层。
blk-mq 由两组队列组成:软件暂存队列和硬件派发队列。当请求到达块层时,它会尝
试最短路径:直接发送到硬件队列。然而,有两种情况下可能不会这样做:如果该层有
IO 调度器或者是希望合并请求。在这两种情况下,请求将被发送到软件队列。
随后,在软件队列中的请求被处理后,请求会被放置到硬件队列。硬件队列是第二阶段
的队列,硬件可以直接访问并处理这些请求。然而,如果硬件没有足够的资源来接受更
多请求,blk-mq 会将请求放置在临时队列中,待硬件资源充足时再发送。
软件暂存队列
~~~~~~~~~~~~
在这些请求未直接发送到驱动时,块设备 I/O 子系统会将请求添加到软件暂存队列中
(由 struct blk_mq_ctx 表示)。一个请求可能包含一个或多个 BIO。它们通过 struct bio
数据结构到达块层。块层随后会基于这些 BIO 构建新的结构体 struct request,用于
与设备驱动通信。每个队列都有自己的锁,队列数量由每个 CPU 和每个 node 为基础
来决定。
暂存队列可用于合并相邻扇区的请求。例如,对扇区3-6、6-7、7-9的请求可以合并
为对扇区3-9的一个请求。即便 SSD 或 NVM 的随机访问和顺序访问响应时间相同,
合并顺序访问的请求仍可减少单独请求的数量。这种合并请求的技术称为 plugging。
此外,I/O 调度器还可以对请求进行重新排序以确保系统资源的公平性(例如防止某
个应用出现“饥饿”现象)或是提高 I/O 性能。
I/O 调度器
^^^^^^^^^^
块层实现了多种调度器,每种调度器都遵循一定启发式规则以提高 I/O 性能。它们是
“可插拔”的(plug and play),可在运行时通过 sysfs 选择。你可以在这里阅读更
多关于 Linux IO 调度器知识 `here
<https://www.kernel.org/doc/html/latest/block/index.html>`_。调度只发
生在同一队列内的请求之间,因此无法合并不同队列的请求,否则会造成缓存冲突并需
要为每个队列加锁。调度后,请求即可发送到硬件。可能选择的调度器之一是 NONE 调
度器,这是最直接的调度器:它只将请求放到进程所在的软件队列,不进行重新排序。
当设备开始处理硬件队列中的请求时(运行硬件队列),映射到该硬件队列的软件队列
会按映射顺序依次清空。
硬件派发队列
~~~~~~~~~~~~~
硬件队列(由 struct blk_mq_hw_ctx 表示)是设备驱动用来映射设备提交队列
(或设备 DMA 环缓存)的结构体,它是块层提交路径在底层设备驱动接管请求之前的
最后一个阶段。运行此队列时,块层会从相关软件队列中取出请求,并尝试派发到硬件。
如果请求无法直接发送到硬件,它们会被加入到请求的链表(``hctx->dispatch``) 中。
随后,当块层下次运行该队列时,会优先发送位于 ``dispatch`` 链表中的请求,
以确保那些最早准备好发送的请求能够得到公平调度。硬件队列的数量取决于硬件及
其设备驱动所支持的硬件上下文数,但不会超过系统的CPU核心数。在这个阶段不
会发生重新排序,每个软件队列都有一组硬件队列来用于提交请求。
.. note::
块层和设备协议都不保证请求完成顺序。此问题需由更高层处理,例如文件系统。
基于标识的完成机制
~~~~~~~~~~~~~~~~~~~
为了指示哪一个请求已经完成,每个请求都会被分配一个整数标识,该标识的取值范围
是从0到分发队列的大小。这个标识由块层生成,并在之后由设备驱动使用,从而避
免了为每个请求再单独创建冗余的标识符。当请求在驱动中完成时,驱动会将该标识返
回给块层,以通知该请求已完成。这样,块层就无需再进行线性搜索来确定是哪一个
I/O 请求完成了。
更多阅读
--------
- `Linux 块 I/O:多队列 SSD 并发访问简介 <http://kernel.dk/blk-mq.pdf>`_
- `NOOP 调度器 <https://en.wikipedia.org/wiki/Noop_scheduler>`_
- `Null 块设备驱动程序 <https://www.kernel.org/doc/html/latest/block/null_blk.html>`_
源代码
======
该API在以下内核代码中:
include/linux/blk-mq.h
block/blk-mq.c
@@ -0,0 +1,192 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/block/data-integrity.rst
:翻译:
柯子杰 kezijie <kezijie@leap-io-kernel.com>
:校译:
==========
数据完整性
==========
1. 引言
=======
现代文件系统对数据和元数据都进行了校验和保护以防止数据损坏。然而,这种损坏的
检测是在读取时才进行,这可能发生在数据写入数月之后。到那时,应用程序尝试写入
的原始数据很可能已经丢失。
解决方案是确保磁盘实际存储的内容就是应用程序想存储的。SCSI 协议族(如 SBC
数据完整性字段、SCC 保护提案)以及 SATA/T13(外部路径保护)最近新增的功能,
通过在 I/O 中附加完整性元数据的方式,试图解决这一问题。完整性元数据(在
SCSI 术语中称为保护信息)包括每个扇区的校验和,以及一个递增计数器,用于确保
各扇区按正确顺序被写入盘。在某些保护方案中,还能保证 I/O 写入磁盘的正确位置。
当前的存储控制器和设备实现了多种保护措施,例如校验和和数据清理。但这些技术通
常只在各自的独立域内工作,或最多仅在 I/O 路径的相邻节点之间发挥作用。DIF 及
其它数据完整性拓展有意思的点在于保护格式定义明确,I/O 路径上的每个节点都可以
验证 I/O 的完整性,如检测到损坏可直接拒绝。这不仅可以防止数据损坏,还能够隔
离故障点。
2. 数据完整性拓展
=================
如上所述,这些协议扩展只保护控制器与存储设备之间的路径。然而,许多控制器实际
上允许操作系统与完整性元数据(IMD)交互。我们一直与多家 FC/SAS HBA 厂商合作,
使保护信息能够在其控制器与操作系统之间传输。
SCSI 数据完整性字段通过在每个扇区后附加8字节的保护信息来实现。数据 + 完整
性元数据存储在磁盘的520字节扇区中。数据 + IMD 在控制器与目标设备之间传输
时是交错组合在一起的。T13 提案的方式类似。
由于操作系统处理520字节(甚至 4104 字节)扇区非常不便,我们联系了多家 HBA
厂商,并鼓励它们分离数据与完整性元数据的 scatter-gather lists。
控制器在写入时会将数据缓冲区和完整性元数据缓冲区的数据交错在一起,并在读取时
会拆分它们。这样,Linux 就能直接通过 DMA 将数据缓冲区传输到主机内存或从主机
内存读取,而无需修改页缓存。
此外,SCSI 与 SATA 规范要求的16位 CRC 校验在软件中计算代价较高。基准测试发
现,计算此校验在高负载情形下显著影响系统性能。一些控制器允许在操作系统接口处
使用轻量级校验。例如 Emulex 支持 TCP/IP 校验。操作系统提供的 IP 校验在写入
时会转换为16位 CRC,读取时则相反。这允许 Linux 或应用程序以极低的开销生成
完整性元数据(与软件 RAID5 相当)。
IP 校验在检测位错误方面比 CRC 弱,但关键在于数据缓冲区与完整性元数据缓冲区
的分离。只有这两个不同的缓冲区匹配,I/O 才能完成。
数据与完整性元数据缓冲区的分离以及校验选择被称为数据完整性扩展。由于这些扩展
超出了协议主体(T10、T13)的范围,Oracle 及其合作伙伴正尝试在存储网络行业协
会内对其进行标准化。
3. 内核变更
===========
Linux 中的数据完整性框架允许将保护信息固定到 I/O 上,并在支持该功能的控制器
之间发送和接收。
SCSI 和 SATA 中完整性扩展的优势在于,它们能够保护从应用程序到存储设备的整个
路径。然而,这同时也是最大的劣势。这意味着保护信息必须采用磁盘可以理解的格式。
通常,Linux/POSIX 应用程序并不关心所访问存储设备的具体细节。虚拟文件系统层
和块层会让硬件扇区大小和传输协议对应用程序完全透明。
然而,在准备发送到磁盘的保护信息时,就需要这种细节。因此,端到端保护方案的概
念实际上违反了层次结构。应用程序完全不应该知道它访问的是 SCSI 还是 SATA 磁盘。
Linux 中实现的数据完整性支持尝试将这些细节对应用程序隐藏。就应用程序(以及在
某种程度上内核)而言,完整性元数据是附加在 I/O 上的不透明信息。
当前实现允许块层自动为任何 I/O 生成保护信息。最终目标是将用户数据的完整性元
数据计算移至用户空间。内核中产生的元数据和其他 I/O 仍将使用自动生成接口。
一些存储设备允许为每个硬件扇区附加一个16位的标识值。这个标识空间的所有者是
块设备的所有者,也就是在多数情况下由文件系统掌控。文件系统可以利用这额外空间
按需为扇区附加标识。由于标识空间有限,块接口允许通过交错方式对更大的数据块标
识。这样,8*16位的信息可以附加到典型的 4KB 文件系统块上。
这也意味着诸如 fsck 和 mkfs 等应用程序需要能够从用户空间访问并操作这些标记。
为此,正在开发一个透传接口。
4. 块层实现细节
===============
4.1 Bio
--------
当启用 CONFIG_BLK_DEV_INTEGRITY 时,数据完整性补丁会在 struct bio 中添加
一个新字段。调用 bio_integrity(bio) 会返回一个指向 struct bip 的指针,该
结构体包含了该 bio 的完整性负载。本质上,bip 是一个精简版的 struct bio,其
中包含一个 bio_vec,用于保存完整性元数据以及所需的维护信息(bvec 池、向量计
数等)。
内核子系统可以通过调用 bio_integrity_alloc(bio) 来为某个 bio 启用数据完整
性保护。该函数会分配并附加一个 bip 到该 bio 上。
随后使用 bio_integrity_add_page() 将包含完整性元数据的单独页面附加到该 bio。
调用 bio_free() 会自动释放bip。
4.2 块设备
-----------
块设备可以在 queue_limits 结构中的 integrity 子结构中设置完整性信息。
对于分层块设备,需要选择一个适用于所有子设备的完整性配置文件。可以使用
queue_limits_stack_integrity() 来协助完成该操作。目前,DM 和 MD linear、
RAID0 和 RAID1 已受支持。而RAID4/5/6因涉及应用标签仍需额外的开发工作。
5.0 块层完整性API
==================
5.1 普通文件系统
-----------------
普通文件系统并不知道其下层块设备具备发送或接收完整性元数据的能力。
在执行写操作时,块层会在调用 submit_bio() 时自动生成完整性元数据。
在执行读操作时,I/O 完成后会触发完整性验证。
IMD 的生成与验证行为可以通过以下开关控制::
/sys/block/<bdev>/integrity/write_generate
and::
/sys/block/<bdev>/integrity/read_verify
flags.
5.2 具备完整性感知的文件系统
----------------------------
具备完整性感知能力的文件系统可以在准备 I/O 时附加完整性元数据,
并且如果底层块设备支持应用标签空间,也可以加以利用。
`bool bio_integrity_prep(bio);`
要为写操作生成完整性元数据或为读操作设置缓冲区,文件系统必须调用
bio_integrity_prep(bio)。
在调用此函数之前,必须先设置好 bio 的数据方向和起始扇区,并确
保该 bio 已经添加完所有的数据页。调用者需要自行保证,在 I/O 进行
期间 bio 不会被修改。如果由于某种原因准备失败,则应当以错误状态
完成该 bio。
5.3 传递已有的完整性元数据
--------------------------
能够自行生成完整性元数据或可以从用户空间传输完整性元数据的文件系统,
可以使用如下接口:
`struct bip * bio_integrity_alloc(bio, gfp_mask, nr_pages);`
为 bio 分配完整性负载并挂载到 bio 上。nr_pages 表示需要在
integrity bio_vec list 中存储多少页保护数据(类似 bio_alloc)。
完整性负载将在 bio_free() 被调用时释放。
`int bio_integrity_add_page(bio, page, len, offset);`
将包含完整性元数据的一页附加到已有的 bio 上。该 bio 必须已有 bip,
即必须先调用 bio_integrity_alloc()。对于写操作,页中的完整
性元数据必须采用目标设备可识别的格式,但有一个例外,当请求在 I/O 栈
中传递时,扇区号会被重新映射。这意味着通过此接口添加的页在 I/O 过程
中可能会被修改!完整性元数据中的第一个引用标签必须等于 bip->bip_sector。
只要 bip bio_vec array(nr_pages)有空间,就可以继续通过
bio_integrity_add_page()添加页。
当读操作完成后,附加的页将包含从存储设备接收到的完整性元数据。
接收方需要处理这些元数据,并在操作完成时验证数据完整性
----------------------------------------------------------------------
2007-12-24 Martin K. Petersen <martin.petersen@oracle.com>
@@ -0,0 +1,35 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/block/index.rst
:翻译:
柯子杰 ke zijie <kezijie@leap-io-kernel.com>
:校译:
=====
Block
=====
.. toctree::
:maxdepth: 1
blk-mq
data-integrity
TODOList:
* bfq-iosched
* biovecs
* cmdline-partition
* deadline-iosched
* inline-encryption
* ioprio
* kyber-iosched
* null_blk
* pr
* stat
* switching-sched
* writeback_cache_control
* ublk
@@ -0,0 +1,473 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/assoc_array.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
.. _cn_core-api_assoc_array:
==================
通用关联数组的实现
==================
简介
====
这个关联数组的实现是一个具有以下属性的对象容器:
1. 对象是不透明的指针。该实现不关心它们指向哪里(如果有的话)或它们指向什么(如果有的
话)。
.. note::
指向对象的指针 *必须* 在最小有效位为零。
2. 对象不需要包含供数组使用的链接块。这允许一个对象同时位于多个数组中。相反,数组是
由指向对象的元数据块组成的。
3. 对象需要索引键来定位它们在阵列中的位置。
4. 索引键必须是唯一的。插入一个与已经在数组中的且具有相同键值的对象将取代旧的对象。
5. 索引键可以是任何长度,也可以是不同的长度。
6. 索引键应该在早期就对长度进行编码,即在任何由于长度引起的变化出现之前。
7. 索引键可以包括一个哈希值,以便将对象分散到整个数组中。
8. 该数组可以迭代。对象不一定会按索引键的顺序出现。
9. 数组可以在被修改的时候进行迭代,只要RCU的读锁被迭代器持有。然而,请注意,在这种情
况下,一些对象可能会被看到不止一次。如果这是个问题,迭代器应该锁定以防止修改。然
而,除非删除,否则对象不会被错过。
10. 数组中的对象可以通过其索引键进行查询。
11. 当数组被修改时,对象可以被查询,前提是进行查询的线程持有RCU的读锁。
该实现在内部使用了一棵由16个指针节点组成的树,这些节点在每一层都由索引键的小数点进行索
引,其方式与基数树相同。为了提高内存效率,可以放置快捷键,以跳过本来是一系列单占节点的地
方。此外,节点将叶子对象指针打包到节点的空闲空间中,而不是做一个额外的分支,直到有对象
需要被添加到一个完整的节点中。
公用API
=======
公用API可以在 ``<linux/assoc_array.h>`` 中找到。关联数组的根是以下结构::
struct assoc_array {
...
};
该代码是通过启用 ``CONFIG_ASSOCIATIVE_ARRAY`` 来选择的,以::
./script/config -e ASSOCIATIVE_ARRAY
编辑脚本
--------
插入和删除功能会产生一个“编辑脚本”,以后可以应用这个脚本来实现更改,而不会造成 ``ENOMEM``
风险。这保留了将被安装在内部树中的预分配的元数据块,并跟踪应用脚本时将从树中删除的元数
据块。
在脚本应用后,这也被用来跟踪死块和死对象,以便以后可以释放它们。释放是在RCU宽限期过后
进行的--因此允许访问功能在RCU读锁下进行。
脚本在API之外显示为一个类型为::
struct assoc_array_edit;
有两个处理脚本的功能:
1. 应用一个编辑脚本::
void assoc_array_apply_edit(struct assoc_array_edit *edit);
这将执行编辑功能,插值各种写屏障,以允许在RCU读锁下的访问继续进行。然后,编辑脚本将被
传递给 ``call_rcu()`` ,以释放它和它所指向的任何死的东西。
2. Cancel an edit script::
void assoc_array_cancel_edit(struct assoc_array_edit *edit);
这将立即释放编辑脚本和所有预分配的内存。如果这是为了插入,新的对象不会被这个函数释放,
而是必须由调用者释放。
这些函数保证不会失败。
操作表
------
各种功能采用了一个操作表::
struct assoc_array_ops {
...
};
这指出了一些方法,所有这些方法都需要提供:
1. 从调用者数据中获取索引键的一个块::
unsigned long (*get_key_chunk)(const void *index_key, int level);
这应该返回一个由调用者提供的索引键的块,从level参数给出的 *比特* 位置开始。level参数将
是 ``ASSOC_ARRAY_KEY_CHUNK_SIZE`` 的倍数,该函数应返回 ``ASSOC_ARRAY_KEY_CHUNK_SIZE``
位。不可能出现错误。
2. 获取一个对象的索引键的一个块::
unsigned long (*get_object_key_chunk)(const void *object, int level);
和前面的函数一样,但是从数组中的一个对象而不是从调用者提供的索引键中获取数据。
3. 看看这是否是我们要找的对象::
bool (*compare_object)(const void *object, const void *index_key);
将对象与一个索引键进行比较,如果匹配则返回 ``true`` ,不匹配则返回 ``false`` 。
4. 对两个对象的索引键进行比较::
int (*diff_objects)(const void *object, const void *index_key);
返回指定对象的索引键与给定索引键不同的比特位置,如果它们相同,则返回-1。
5. 释放一个对象::
void (*free_object)(void *object);
释放指定的对象。注意,这可能是在调用 ``assoc_array_apply_edit()`` 后的一个RCU宽限期内
调用的,所以在模块卸载时可能需要 ``synchronize_rcu()`` 。
操控函数
--------
有一些函数用于操控关联数组:
1. 初始化一个关联数组::
void assoc_array_init(struct assoc_array *array);
这将初始化一个关联数组的基础结构。它不会失败。
2. 在一个关联数组中插入/替换一个对象::
struct assoc_array_edit *
assoc_array_insert(struct assoc_array *array,
const struct assoc_array_ops *ops,
const void *index_key,
void *object);
这将把给定的对象插入数组中。注意,指针的最小有效位必须是0,因为它被用来在内部标记指针的类
型。
如果该键已经存在一个对象,那么它将被新的对象所取代,旧的对象将被自动释放。
``index_key`` 参数应持有索引键信息,并在调用OPP表中的方法时传递给它们。
这个函数不对数组本身做任何改动,而是返回一个必须应用的编辑脚本。如果出现内存不足的错误,会
返回 ``-ENOMEM`` 。
调用者应专门锁定数组的其他修改器。
3. 从一个关联数组中删除一个对象::
struct assoc_array_edit *
assoc_array_delete(struct assoc_array *array,
const struct assoc_array_ops *ops,
const void *index_key);
这将从数组中删除一个符合指定数据的对象。
``index_key`` 参数应持有索引键信息,并在调用OPP表中的方法时传递给它们。
这个函数不对数组本身做任何改动,而是返回一个必须应用的编辑脚本。 ``-ENOMEM`` 在出现内存不足
的错误时返回。如果在数组中没有找到指定的对象,将返回 ``NULL`` 。
调用者应该对数组的其他修改者进行专门锁定。
4. 从一个关联数组中删除所有对象::
struct assoc_array_edit *
assoc_array_clear(struct assoc_array *array,
const struct assoc_array_ops *ops);
这个函数删除了一个关联数组中的所有对象,使其完全为空。
这个函数没有对数组本身做任何改动,而是返回一个必须应用的编辑脚本。如果出现内存不足
的错误,则返回 ``-ENOMEM`` 。
调用者应专门锁定数组的其他修改者。
5. 销毁一个关联数组,删除所有对象::
void assoc_array_destroy(struct assoc_array *array,
const struct assoc_array_ops *ops);
这将破坏关联数组的内容,使其完全为空。在这个函数销毁数组的同时,不允许另一个线程在RCU读锁
下遍历数组,因为在内存释放时不执行RCU延迟,这需要分配内存。
调用者应该专门针对数组的其他修改者和访问者进行锁定。
6. 垃圾回收一个关联数组::
int assoc_array_gc(struct assoc_array *array,
const struct assoc_array_ops *ops,
bool (*iterator)(void *object, void *iterator_data),
void *iterator_data);
这是对一个关联数组中的对象进行迭代,并将每个对象传递给 ``iterator()`` 。如果 ``iterator()`` 返回
true,该对象被保留。如果它返回 ``false`` ,该对象将被释放。如果 ``iterator()`` 函数返回 ``true`` ,它必须
在返回之前对该对象进行适当的 ``refcount`` 递增。
如果可能的话,内部树将被打包下来,作为迭代的一部分,以减少其中的节点数量。
``iterator_data`` 被直接传递给 ``iterator()`` ,否则会被函数忽略。
如果成功,该函数将返回 ``0`` ,如果没有足够的内存,则返回 ``-ENOMEM`` 。
在这个函数执行过程中,其他线程有可能在RCU读锁下迭代或搜索阵列。调用者应该专门针对数组的其他
修改者进行锁定。
访问函数
--------
有两个函数用于访问一个关联数组:
1. 遍历一个关联数组中的所有对象::
int assoc_array_iterate(const struct assoc_array *array,
int (*iterator)(const void *object,
void *iterator_data),
void *iterator_data);
这将数组中的每个对象传递给迭代器回调函数。 ``iterator_data`` 是该函数的私有数据。
在数组被修改的同时,可以在数组上使用这个方法,前提是RCU读锁被持有。在这种情况下,迭代函数有
可能两次看到某些对象。如果这是个问题,那么修改应该被锁定。然而,迭代算法不应该错过任何对象。
如果数组中没有对象,该函数将返回 ``0`` ,否则将返回最后一次调用的迭代器函数的结果。如果对迭代函数
的任何调用导致非零返回,迭代立即停止。
2. 在一个关联数组中寻找一个对象::
void *assoc_array_find(const struct assoc_array *array,
const struct assoc_array_ops *ops,
const void *index_key);
这将直接穿过数组的内部树,到达索引键所指定的对象。
这个函数可以在修改数组的同时用在数组上,前提是RCU读锁被持有。
如果找到对象,该函数将返回对象(并将 ``*_type`` 设置为对象的类型),如果没有找到对象,将返回 ``NULL`` 。
索引键形式
----------
索引键可以是任何形式的,但是由于算法没有被告知键有多长,所以强烈建议在任何由于长度而产生的变化
对比较产生影响之前,索引键应该很早就包括其长度。
这将导致具有不同长度键的叶子相互分散,而具有相同长度键的叶子则聚集在一起。
我们还建议索引键以键的其余部分的哈希值开始,以最大限度地提高整个键空间的散布情况。
分散性越好,内部树就越宽,越低。
分散性差并不是一个太大的问题,因为有快捷键,节点可以包含叶子和元数据指针的混合物。
索引键是以机器字的块状来读取的。每个块被细分为每层一个nibble(4比特),所以在32位CPU上这适合8层,
在64位CPU上适合16层。除非散布情况真的很差,否则不太可能有超过一个字的任何特定索引键需要被使用。
内部工作机制
============
关联数组数据结构有一个内部树。这个树由两种类型的元数据块构成:节点和快捷键。
一个节点是一个槽的数组。每个槽可以包含以下四种东西之一:
* 一个NULL的指针,表示槽是空的。
* 一个指向对象(叶子)的指针。
* 一个指向下一级节点的指针。
* 一个指向快捷键的指针。
基本的内部树形布局
------------------
暂时不考虑快捷键,节点形成一个多级树。索引键空间被树上的节点严格细分,节点出现在固定的层次上。例如::
Level: 0 1 2 3
=============== =============== =============== ===============
NODE D
NODE B NODE C +------>+---+
+------>+---+ +------>+---+ | | 0 |
NODE A | | 0 | | | 0 | | +---+
+---+ | +---+ | +---+ | : :
| 0 | | : : | : : | +---+
+---+ | +---+ | +---+ | | f |
| 1 |---+ | 3 |---+ | 7 |---+ +---+
+---+ +---+ +---+
: : : : | 8 |---+
+---+ +---+ +---+ | NODE E
| e |---+ | f | : : +------>+---+
+---+ | +---+ +---+ | 0 |
| f | | | f | +---+
+---+ | +---+ : :
| NODE F +---+
+------>+---+ | f |
| 0 | NODE G +---+
+---+ +------>+---+
: : | | 0 |
+---+ | +---+
| 6 |---+ : :
+---+ +---+
: : | f |
+---+ +---+
| f |
+---+
在上述例子中,有7个节点(A-G),每个节点有16个槽(0-f)。假设树上没有其他元数据节点,那么密钥空间
是这样划分的::
KEY PREFIX NODE
========== ====
137* D
138* E
13[0-69-f]* C
1[0-24-f]* B
e6* G
e[0-57-f]* F
[02-df]* A
因此,例如,具有以下示例索引键的键将在适当的节点中被找到::
INDEX KEY PREFIX NODE
=============== ======= ====
13694892892489 13 C
13795289025897 137 D
13889dde88793 138 E
138bbb89003093 138 E
1394879524789 12 C
1458952489 1 B
9431809de993ba - A
b4542910809cd - A
e5284310def98 e F
e68428974237 e6 G
e7fffcbd443 e F
f3842239082 - A
为了节省内存,如果一个节点可以容纳它的那部分键空间中的所有叶子,那么这个节点将有所有这些叶子,而不
会有任何元数据指针——即使其中一些叶子想在同一个槽中。
一个节点可以包含叶子和元数据指针的异质性混合。元数据指针必须在与它们的关键空间的细分相匹配的槽中。
叶子可以在任何没有被元数据指针占据的槽中。保证一个节点中没有一个叶子会与元数据指针占据的槽相匹配。
如果元数据指针在那里,任何键与元数据键前缀相匹配的叶必须在元数据指针指向的子树中。
在上面的索引键的例子列表中,节点A将包含::
SLOT CONTENT INDEX KEY (PREFIX)
==== =============== ==================
1 PTR TO NODE B 1*
any LEAF 9431809de993ba
any LEAF b4542910809cd
e PTR TO NODE F e*
any LEAF f3842239082
和节点B::
3 PTR TO NODE C 13*
any LEAF 1458952489
快捷键
---------
快捷键是跳过一块键空间的元数据记录。快捷键是一系列通过层次上升的单占节点的替代物。快捷键的存在是
为了节省内存和加快遍历速度。
树的根部有可能是一个快捷键——比如说,树至少包含17个节点,都有键前缀 ``1111`` 。插入算法将插入一个快捷键,
以单次跳过 ``1111`` 的键位,并到达第四层,在这里,这些键位实际上变得不同。
拆分和合并节点
------------------------------
每个节点的最大容量为16个叶子和元数据指针。如果插入算法发现它正试图将一个第17个对象插入到一个节点中,
该节点将被拆分,使得至少两个在该层有一个共同的关键段的叶子最终在一个单独的节点中,该共同的关键段的根
在该槽上。
如果一个完整的节点中的叶子和被插入的叶子足够相似,那么就会在树中插入一个快捷键。
当根植于某个节点的子树中的对象数量下降到16个或更少时,那么该子树将被合并成一个单独的节点——如果可能的
话,这将向根部扩散。
非递归式迭代
------------
每个节点和快捷键都包含一个指向其父节点的后置指针,以及该父节点中指向它的槽数。非递归迭代使用这些来
通过树的根部进行,前往父节点,槽N+1,以确保在没有堆栈的情况下取得进展。
然而,反向指针使得同时改变和迭代变得很棘手。
同时改变和迭代
--------------
有一些情况需要考虑:
1. 简单的插入/替换。这涉及到简单地将一个NULL或旧的匹配叶子的指针替换为屏障后的新叶子的指针。否则元数
据块不会改变。一个旧的叶子直到RCU宽限期过后才会被释放。
2. 简单删除。这只是涉及到清除一个旧的匹配叶子。元数据块不会有其他变化。旧的叶子直到RCU宽限期之后才会
被释放。
3. 插入,替换我们还没有进入的子树的一部分。这可能涉及到替换该子树的一部分——但这不会影响迭代,因为我们
还没有到达它的指针,而且祖先块也不会被替换(这些块的布局不会改变)。
4. 插入替换了我们正在处理的节点。这不是一个问题,因为我们已经通过了锚定指针,直到我们跟随后面的指针才
会切换到新的布局上——这时我们已经检查了被替换节点的叶子(在跟随任何元数据指针之前,我们会迭代一个节
点的所有叶子)。
然而,我们可能会重新看到一些叶子,这些叶子已经被分割成一个新的分支,而这个分支的位置比我们之前的位
置更远。
5. 插入替换了我们正在处理的依赖分支的节点。这不会影响到我们,直到我们跟随后面的指针。与(4)类似。
6. 删掉我们下面的一个分支。这不会影响我们,因为在我们看到新节点之前,回溯指针会让我们回到新节点的父节
点。整个崩溃的子树被扔掉了,没有任何变化——而且仍然会在同一个槽上生根,所以我们不应该第二次处理它,
因为我们会回到槽+1。
.. note::
在某些情况下,我们需要同时改变一个节点的父指针和父槽指针(比如说,我们在它之前插入了另一个节点,
并把它往上移了一层)。我们不能在不锁定读取的情况下这样做——所以我们必须同时替换该节点。
然而,当我们把一个快捷键改成一个节点时,这不是一个问题,因为快捷键只有一个槽,所以当向后遍
历一个槽时,不会使用父槽号。这意味着先改变槽位号是可以的——只要使用适当的屏障来确保父槽位号在后
退指针之后被读取。
过时的块和叶子在RCU宽限期过后会被释放,所以只要任何进行遍历或迭代的人持有RCU读锁,旧的上层建筑就不
应该在他们身上消失。
@@ -0,0 +1,49 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/boot-time-mm.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
时奎亮 <alexs@kernel.org>
.. _cn_core-api_boot-time-mm:
================
启动时的内存管理
================
系统初始化早期“正常”的内存管理由于没有设置完毕无法使用。但是内核仍然需要
为各种数据结构分配内存,例如物理页分配器。
一个叫做 ``memblock`` 的专用分配器执行启动时的内存管理。特定架构的初始化
必须在setup_arch()中设置它,并在mem_init()函数中移除它。
一旦早期的内存管理可用,它就为内存分配提供了各种函数和宏。分配请求可以指向
第一个(也可能是唯一的)节点或NUMA系统中的某个特定节点。有一些API变体在分
配失败时panic,也有一些不会panic的。
Memblock还提供了各种控制其自身行为的API。
Memblock概述
============
该API在以下内核代码中:
mm/memblock.c
函数和结构体
============
下面是关于memblock数据结构、函数和宏的描述。其中一些实际上是内部的,但由于
它们被记录下来,漏掉它们是很愚蠢的。此外,阅读内部函数的注释可以帮助理解引
擎盖下真正发生的事情。
该API在以下内核代码中:
include/linux/memblock.h
mm/memblock.c
@@ -0,0 +1,333 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/cachetlb.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
:校译:
吴想成 Wu XiangCheng <bobwxc@email.cn>
.. _cn_core-api_cachetlb:
======================
Linux下的缓存和TLB刷新
======================
:作者: David S. Miller <davem@redhat.com>
*译注:TLB,Translation Lookaside Buffer,页表缓存/变换旁查缓冲器*
本文描述了由Linux虚拟内存子系统调用的缓存/TLB刷新接口。它列举了每个接
口,描述了它的预期目的,以及接口被调用后的预期副作用。
下面描述的副作用是针对单处理器的实现,以及在单个处理器上发生的情况。若
为SMP,则只需将定义简单地扩展一下,使发生在某个特定接口的副作用扩展到系
统的所有处理器上。不要被这句话吓到,以为SMP的缓存/tlb刷新一定是很低
效的,事实上,这是一个可以进行很多优化的领域。例如,如果可以证明一个用
户地址空间从未在某个cpu上执行过(见mm_cpumask()),那么就不需要在该
cpu上对这个地址空间进行刷新。
首先是TLB刷新接口,因为它们是最简单的。在Linux下,TLB被抽象为cpu
用来缓存从软件页表获得的虚拟->物理地址转换的东西。这意味着,如果软件页
表发生变化,这个“TLB”缓存中就有可能出现过时(脏)的翻译。因此,当软件页表
发生变化时,内核会在页表发生 *变化后* 调用以下一种刷新方法:
1) ``void flush_tlb_all(void)``
最严格的刷新。在这个接口运行后,任何以前的页表修改都会对cpu可见。
这通常是在内核页表被改变时调用的,因为这种转换在本质上是“全局”的。
2) ``void flush_tlb_mm(struct mm_struct *mm)``
这个接口从TLB中刷新整个用户地址空间。在运行后,这个接口必须确保
以前对地址空间‘mm’的任何页表修改对cpu来说是可见的。也就是说,在
运行后,TLB中不会有‘mm’的页表项。
这个接口被用来处理整个地址空间的页表操作,比如在fork和exec过程
中发生的事情。
3) ``void flush_tlb_range(struct vm_area_struct *vma,
unsigned long start, unsigned long end)``
这里我们要从TLB中刷新一个特定范围的(用户)虚拟地址转换。在运行后,
这个接口必须确保以前对‘start’到‘end-1’范围内的地址空间‘vma->vm_mm’
的任何页表修改对cpu来说是可见的。也就是说,在运行后,TLB中不会有
‘mm’的页表项用于‘start’到‘end-1’范围内的虚拟地址。
“vma”是用于该区域的备份存储。主要是用于munmap()类型的操作。
提供这个接口是希望端口能够找到一个合适的有效方法来从TLB中删除多
个页面大小的转换,而不是让内核为每个可能被修改的页表项调用
flush_tlb_page(见下文)。
4) ``void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr)``
这一次我们需要从TLB中删除PAGE_SIZE大小的转换。‘vma’是Linux用来跟
踪进程的mmap区域的支持结构体,地址空间可以通过vma->vm_mm获得。另
外,可以通过测试(vma->vm_flags & VM_EXEC)来查看这个区域是否是
可执行的(因此在split-tlb类型的设置中可能在“指令TLB”中)。
在运行后,这个接口必须确保之前对用户虚拟地址“addr”的地址空间
“vma->vm_mm”的页表修改对cpu来说是可见的。也就是说,在运行后,TLB
中不会有虚拟地址‘addr’的‘vma->vm_mm’的页表项。
这主要是在故障处理时使用。
5) ``void update_mmu_cache(struct vm_area_struct *vma,
unsigned long address, pte_t *ptep)``
在每个缺页异常结束时,这个程序被调用,以告诉体系结构特定的代码,在
软件页表中,在地址空间“vma->vm_mm”的虚拟地址“地址”处,现在存在
一个翻译。
可以用它所选择的任何方式使用这个信息来进行移植。例如,它可以使用这
个事件来为软件管理的TLB配置预装TLB转换。目前sparc64移植就是这么干
的。
接下来,我们有缓存刷新接口。一般来说,当Linux将现有的虚拟->物理映射
改变为新的值时,其顺序将是以下形式之一::
1) flush_cache_mm(mm);
change_all_page_tables_of(mm);
flush_tlb_mm(mm);
2) flush_cache_range(vma, start, end);
change_range_of_page_tables(mm, start, end);
flush_tlb_range(vma, start, end);
3) flush_cache_page(vma, addr, pfn);
set_pte(pte_pointer, new_pte_val);
flush_tlb_page(vma, addr);
缓存级别的刷新将永远是第一位的,因为这允许我们正确处理那些缓存严格,
且在虚拟地址被从缓存中刷新时要求一个虚拟地址的虚拟->物理转换存在的系统。
HyperSparc cpu就是这样一个具有这种属性的cpu。
下面的缓存刷新程序只需要在特定的cpu需要的范围内处理缓存刷新。大多数
情况下,这些程序必须为cpu实现,这些cpu有虚拟索引的缓存,当虚拟->物
理转换被改变或移除时,必须被刷新。因此,例如,IA32处理器的物理索引
的物理标记的缓存没有必要实现这些接口,因为这些缓存是完全同步的,并
且不依赖于翻译信息。
下面逐个列出这些程序:
1) ``void flush_cache_mm(struct mm_struct *mm)``
这个接口将整个用户地址空间从高速缓存中刷掉。也就是说,在运行后,
将没有与‘mm’相关的缓存行。
这个接口被用来处理整个地址空间的页表操作,比如在退出和执行过程
中发生的事情。
2) ``void flush_cache_dup_mm(struct mm_struct *mm)``
这个接口将整个用户地址空间从高速缓存中刷新掉。也就是说,在运行
后,将没有与‘mm’相关的缓存行。
这个接口被用来处理整个地址空间的页表操作,比如在fork过程中发生
的事情。
这个选项与flush_cache_mm分开,以允许对VIPT缓存进行一些优化。
3) ``void flush_cache_range(struct vm_area_struct *vma,
unsigned long start, unsigned long end)``
在这里,我们要从缓存中刷新一个特定范围的(用户)虚拟地址。运行
后,在“start”到“end-1”范围内的虚拟地址的“vma->vm_mm”的缓存中
将没有页表项。
“vma”是被用于该区域的备份存储。主要是用于munmap()类型的操作。
提供这个接口是希望端口能够找到一个合适的有效方法来从缓存中删
除多个页面大小的区域, 而不是让内核为每个可能被修改的页表项调
用 flush_cache_page (见下文)。
4) ``void flush_cache_page(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn)``
这一次我们需要从缓存中删除一个PAGE_SIZE大小的区域。“vma”是
Linux用来跟踪进程的mmap区域的支持结构体,地址空间可以通过
vma->vm_mm获得。另外,我们可以通过测试(vma->vm_flags &
VM_EXEC)来查看这个区域是否是可执行的(因此在“Harvard”类
型的缓存布局中可能是在“指令缓存”中)。
“pfn”表示“addr”所对应的物理页框(通过PAGE_SHIFT左移这个
值来获得物理地址)。正是这个映射应该从缓存中删除。
在运行之后,对于虚拟地址‘addr’的‘vma->vm_mm’,在缓存中不会
有任何页表项,它被翻译成‘pfn’。
这主要是在故障处理过程中使用。
5) ``void flush_cache_kmaps(void)``
只有在平台使用高位内存的情况下才需要实现这个程序。它将在所有的
kmaps失效之前被调用。
运行后,内核虚拟地址范围PKMAP_ADDR(0)到PKMAP_ADDR(LAST_PKMAP)
的缓存中将没有页表项。
这个程序应该在asm/highmem.h中实现。
6) ``void flush_cache_vmap(unsigned long start, unsigned long end)``
``void flush_cache_vunmap(unsigned long start, unsigned long end)``
在这里,在这两个接口中,我们从缓存中刷新一个特定范围的(内核)
虚拟地址。运行后,在“start”到“end-1”范围内的虚拟地址的内核地
址空间的缓存中不会有页表项。
这两个程序中的第一个是在vmap_range()安装了页表项之后调用的。
第二个是在vunmap_range()删除页表项之前调用的。
还有一类cpu缓存问题,目前需要一套完全不同的接口来正确处理。最大
的问题是处理器的数据缓存中的虚拟别名。
.. note::
这段内容有些晦涩,为了减轻中文阅读压力,特作此译注。
别名(alias)属于缓存一致性问题,当不同的虚拟地址映射相同的
物理地址,而这些虚拟地址的index不同,此时就发生了别名现象(多
个虚拟地址被称为别名)。通俗点来说就是指同一个物理地址的数据被
加载到不同的cacheline中就会出现别名现象。
常见的解决方法有两种:第一种是硬件维护一致性,设计特定的cpu电
路来解决问题(例如设计为PIPT的cache);第二种是软件维护一致性,
就是下面介绍的sparc的解决方案——页面染色,涉及的技术细节太多,
译者不便展开,请读者自行查阅相关资料。
您的移植是否容易在其D-cache中出现虚拟别名?嗯,如果您的D-cache
是虚拟索引的,且cache大于PAGE_SIZE(页大小),并且不能防止同一
物理地址的多个cache行同时存在,您就会遇到这个问题。
如果你的D-cache有这个问题,首先正确定义asm/shmparam.h SHMLBA,
它基本上应该是你的虚拟寻址D-cache的大小(或者如果大小是可变的,
则是最大的可能大小)。这个设置将迫使SYSv IPC层只允许用户进程在
这个值的倍数的地址上对共享内存进行映射。
.. note::
这并不能解决共享mmaps的问题,请查看sparc64移植解决
这个问题的一个方法(特别是 SPARC_FLAG_MMAPSHARED)。
接下来,你必须解决所有其他情况下的D-cache别名问题。请记住这个事
实,对于一个给定的页面映射到某个用户地址空间,总是至少还有一个映
射,那就是内核在其线性映射中从PAGE_OFFSET开始。因此,一旦第一个
用户将一个给定的物理页映射到它的地址空间,就意味着D-cache的别名
问题有可能存在,因为内核已经将这个页映射到它的虚拟地址。
``void copy_user_page(void *to, void *from, unsigned long addr, struct page *page)``
``void clear_user_page(void *to, unsigned long addr, struct page *page)``
这两个程序在用户匿名或COW页中存储数据。它允许一个端口有效地
避免用户空间和内核之间的D-cache别名问题。
例如,一个端口可以在复制过程中把“from”和“to”暂时映射到内核
的虚拟地址上。这两个页面的虚拟地址的选择方式是,内核的加载/存
储指令发生在虚拟地址上,而这些虚拟地址与用户的页面映射是相同
的“颜色”。例如,Sparc64就使用这种技术。
“addr”参数告诉了用户最终要映射这个页面的虚拟地址,“page”参
数给出了一个指向目标页结构体的指针。
如果D-cache别名不是问题,这两个程序可以简单地直接调用
memcpy/memset而不做其他事情。
``void flush_dcache_page(struct page *page)``
任何时候,当内核写到一个页面缓存页,或者内核要从一个页面缓存
页中读出,并且这个页面的用户空间共享/可写映射可能存在时,
这个程序就会被调用。
.. note::
这个程序只需要为有可能被映射到用户进程的地址空间的
页面缓存调用。因此,例如,处理页面缓存中vfs符号链
接的VFS层代码根本不需要调用这个接口。
“内核写入页面缓存的页面”这句话的意思是,具体来说,内核执行存
储指令,在该页面的页面->虚拟映射处弄脏该页面的数据。在这里,通
过刷新的手段处理D-cache的别名是很重要的,以确保这些内核存储对
该页的用户空间映射是可见的。
推论的情况也同样重要,如果有用户对这个文件有共享+可写的映射,
我们必须确保内核对这些页面的读取会看到用户所做的最新的存储。
如果D-cache别名不是一个问题,这个程序可以简单地定义为该架构上
的nop。
在folio->flags (PG_arch_1)中有一个位是“架构私有”。内核保证,
对于分页缓存的页面,当这样的页面第一次进入分页缓存时,它将清除
这个位。
这使得这些接口可以更有效地被实现。如果目前没有用户进程映射这个
页面,它允许我们“推迟”(也许是无限期)实际的刷新过程。请看
sparc64的flush_dcache_page和update_mmu_cache实现,以了解如
何做到这一点。
这个想法是,首先在flush_dcache_page()时,如果page->mapping->i_mmap
是一个空树,只需标记架构私有页标志位。之后,在update_mmu_cache()
中,会对这个标志位进行检查,如果设置了,就进行刷新,并清除标志位。
.. important::
通常很重要的是,如果你推迟刷新,实际的刷新发生在同一个
CPU上,因为它将cpu存储到页面上,使其变脏。同样,请看
sparc64关于如何处理这个问题的例子。
``void flush_dcache_folio(struct folio *folio)``
该函数的调用情形与flush_dcache_page()相同。它允许架构针对刷新整个
folio页面进行优化,而不是一次刷新一页。
``void copy_to_user_page(struct vm_area_struct *vma, struct page *page,
unsigned long user_vaddr, void *dst, void *src, int len)``
``void copy_from_user_page(struct vm_area_struct *vma, struct page *page,
unsigned long user_vaddr, void *dst, void *src, int len)``
当内核需要复制任意的数据进出任意的用户页时(比如ptrace()),它将使
用这两个程序。
任何必要的缓存刷新或其他需要发生的一致性操作都应该在这里发生。如果
处理器的指令缓存没有对cpu存储进行窥探,那么你很可能需要为
copy_to_user_page()刷新指令缓存。
``void flush_anon_page(struct vm_area_struct *vma, struct page *page,
unsigned long vmaddr)``
当内核需要访问一个匿名页的内容时,它会调用这个函数(目前只有
get_user_pages())。注意:flush_dcache_page()故意对匿名页不起作
用。默认的实现是nop(对于所有相干的架构应该保持这样)。对于不一致性
的架构,它应该刷新vmaddr处的页面缓存。
``void flush_icache_range(unsigned long start, unsigned long end)``
当内核存储到它将执行的地址中时(例如在加载模块时),这个函数被调用。
如果icache不对存储进行窥探,那么这个程序将需要对其进行刷新。
``void flush_icache_page(struct vm_area_struct *vma, struct page *page)``
flush_icache_page的所有功能都可以在flush_dcache_page和update_mmu_cache
中实现。在未来,我们希望能够完全删除这个接口。
最后一类API是用于I/O到内核内特意设置的别名地址范围。这种别名是通过使用
vmap/vmalloc API设置的。由于内核I/O是通过物理页进行的,I/O子系统假定用户
映射和内核偏移映射是唯一的别名。这对vmap别名来说是不正确的,所以内核中任何
试图对vmap区域进行I/O的东西都必须手动管理一致性。它必须在做I/O之前刷新vmap
范围,并在I/O返回后使其失效。
``void flush_kernel_vmap_range(void *vaddr, int size)``
刷新vmap区域中指定的虚拟地址范围的内核缓存。这是为了确保内核在vmap范围
内修改的任何数据对物理页是可见的。这个设计是为了使这个区域可以安全地执
行I/O。注意,这个API并 *没有* 刷新该区域的偏移映射别名。
``void invalidate_kernel_vmap_range(void *vaddr, int size) invalidates``
在vmap区域的一个给定的虚拟地址范围的缓存,这可以防止处理器在物理页的I/O
发生时通过投机性地读取数据而使缓存变脏。这只对读入vmap区域的数据是必要的。
@@ -0,0 +1,210 @@
.. SPDX-License-Identifier: GPL-2.0+
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/circular-buffers.rst
:翻译:
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
:校译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
吴想成 Wu Xiangcheng <bobwxc@email.cn>
时奎亮 Alex Shi <alexs@kernel.org>
==========
环形缓冲区
==========
:作者: David Howells <dhowells@redhat.com>
:作者: Paul E. McKenney <paulmck@linux.ibm.com>
Linux 提供了许多可用于实现循环缓冲的特性。有两组这样的特性:
(1) 用于确定2次方大小的缓冲区信息的便利函数。
(2) 可以代替缓冲区中对象的生产者和消费者共享锁的内存屏障。
如下所述,要使用这些设施,只需要一个生产者和一个消费者。可以通过序列化来处理多个
生产者,并通过序列化来处理多个消费者。
.. Contents:
(*) 什么是环形缓冲区?
(*) 测量2次幂缓冲区
(*) 内存屏障与环形缓冲区的结合使用
- 生产者
- 消费者
(*) 延伸阅读
什么是环形缓冲区?
==================
首先,什么是环形缓冲区?环形缓冲区是具有固定的有限大小的缓冲区,它有两个索引:
(1) 'head'索引 - 生产者将元素插入缓冲区的位置。
(2) 'tail'索引 - 消费者在缓冲区中找到下一个元素的位置。
通常,当tail指针等于head指针时,表明缓冲区是空的;而当head指针比tail指针少一个时,
表明缓冲区是满的。
添加元素时,递增head索引;删除元素时,递增tail索引。tail索引不应该跳过head索引,
两个索引在到达缓冲区末端时都应该被赋值为0,从而允许海量的数据流过缓冲区。
通常情况下,元素都有相同的单元大小,但这并不是使用以下技术的严格要求。如果要在缓
冲区中包含多个元素或可变大小的元素,则索引可以增加超过1,前提是两个索引都没有超过
另一个。然而,实现者必须小心,因为超过一个单位大小的区域可能会覆盖缓冲区的末端并
且缓冲区会被分成两段。
测量2次幂缓冲区
===============
计算任意大小的环形缓冲区的占用或剩余容量通常是一个费时的操作,需要使用模(除法)
指令。但是如果缓冲区的大小为2次幂,则可以使用更快的按位与指令代替。
Linux提供了一组用于处理2次幂环形缓冲区的宏。可以通过以下方式使用::
#include <linux/circ_buf.h>
这些宏包括:
(#) 测量缓冲区的剩余容量::
CIRC_SPACE(head_index, tail_index, buffer_size);
返回缓冲区[1]中可插入元素的剩余空间大小。
(#) 测量缓冲区中的最大连续立即可用空间::
CIRC_SPACE_TO_END(head_index, tail_index, buffer_size);
返回缓冲区[1]中剩余的连续空间的大小,元素可以立即插入其中,而不必绕回到缓冲
区的开头。
(#) 测量缓冲区的使用数::
CIRC_CNT(head_index, tail_index, buffer_size);
返回当前占用缓冲区[2]的元素数量。
(#) 测量缓冲区的连续使用数::
CIRC_CNT_TO_END(head_index, tail_index, buffer_size);
返回可以从缓冲区中提取的连续元素[2]的数量,而不必绕回到缓冲区的开头。
这里的每一个宏名义上都会返回一个介于0和buffer_size-1之间的值,但是:
(1) CIRC_SPACE*()是为了在生产者中使用。对生产者来说,它们将返回一个下限,因为生
产者控制着head索引,但消费者可能仍然在另一个CPU上耗尽缓冲区并移动tail索引。
对消费者来说,它将显示一个上限,因为生产者可能正忙于耗尽空间。
(2) CIRC_CNT*()是为了在消费者中使用。对消费者来说,它们将返回一个下限,因为消费
者控制着tail索引,但生产者可能仍然在另一个CPU上填充缓冲区并移动head索引。
对于生产者,它将显示一个上限,因为消费者可能正忙于清空缓冲区。
(3) 对于第三方来说,生产者和消费者对索引的写入顺序是无法保证的,因为它们是独立的,
而且可能是在不同的CPU上进行的,所以在这种情况下的结果只是一种猜测,甚至可能
是错误的。
内存屏障与环形缓冲区的结合使用
==============================
通过将内存屏障与环形缓冲区结合使用,可以避免以下需求:
(1) 使用单个锁来控制对缓冲区两端的访问,从而允许同时填充和清空缓冲区;以及
(2) 使用原子计数器操作。
这有两个方面:填充缓冲区的生产者和清空缓冲区的消费者。在任何时候,只应有一个生产
者在填充缓冲区,同样的也只应有一个消费者在清空缓冲区,但双方可以同时操作。
生产者
------
生产者看起来像这样::
spin_lock(&producer_lock);
unsigned long head = buffer->head;
/* spin_unlock()和下一个spin_lock()提供必要的排序。 */
unsigned long tail = READ_ONCE(buffer->tail);
if (CIRC_SPACE(head, tail, buffer->size) >= 1) {
/* 添加一个元素到缓冲区 */
struct item *item = buffer[head];
produce_item(item);
smp_store_release(buffer->head,
(head + 1) & (buffer->size - 1));
/* wake_up()将确保在唤醒任何人之前提交head */
wake_up(consumer);
}
spin_unlock(&producer_lock);
这将表明CPU必须在head索引使其对消费者可用之前写入新项目的内容,同时CPU必须在唤醒
消费者之前写入修改后的head索引。
请注意,wake_up()并不保证任何形式的屏障,除非确实唤醒了某些东西。因此我们不能依靠
它来进行排序。但是数组中始终有一个元素留空,因此生产者必须产生两个元素,然后才可
能破坏消费者当前正在读取的元素。同时,消费者连续调用之间成对的解锁-加锁提供了索引
读取(指示消费者已清空给定元素)和生产者对该相同元素的写入之间的必要顺序。
消费者
------
消费者看起来像这样::
spin_lock(&consumer_lock);
/* 读取该索引处的内容之前,先读取索引 */
unsigned long head = smp_load_acquire(buffer->head);
unsigned long tail = buffer->tail;
if (CIRC_CNT(head, tail, buffer->size) >= 1) {
/* 从缓冲区中提取一个元素 */
struct item *item = buffer[tail];
consume_item(item);
/* 在递增tail之前完成对描述符的读取。 */
smp_store_release(buffer->tail,
(tail + 1) & (buffer->size - 1));
}
spin_unlock(&consumer_lock);
这表明CPU在读取新元素之前确保索引是最新的,然后在写入新的尾指针之前应确保CPU已完
成读取该元素,这将擦除该元素。
请注意,使用READ_ONCE()和smp_load_acquire()来读取反向(head)索引。这可以防止编译
器丢弃并重新加载其缓存值。如果您能确定反向(head)索引将仅使用一次,则这不是必须
的。smp_load_acquire()还可以强制CPU对后续的内存引用进行排序。类似地,两种算法都使
用smp_store_release()来写入线程的索引。这记录了我们正在写入可以并发读取的内容的事
实,以防止编译器破坏存储,并强制对以前的访问进行排序。
延伸阅读
========
关于Linux的内存屏障设施的描述,请查看Documentation/memory-barriers.txt。
@@ -0,0 +1,661 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/cpu_hotplug.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
:校译:
吴想成 Wu XiangCheng <bobwxc@email.cn>
.. _cn_core_api_cpu_hotplug:
=================
内核中的CPU热拔插
=================
:时间: 2021年9月
:作者: Sebastian Andrzej Siewior <bigeasy@linutronix.de>,
Rusty Russell <rusty@rustcorp.com.au>,
Srivatsa Vaddagiri <vatsa@in.ibm.com>,
Ashok Raj <ashok.raj@intel.com>,
Joel Schopp <jschopp@austin.ibm.com>,
Thomas Gleixner <tglx@kernel.org>
简介
====
现代系统架构的演进已经在处理器中引入了先进的错误报告和纠正能力。有一些OEM也支
持可热拔插的NUMA(Non Uniform Memory Access,非统一内存访问)硬件,其中物理
节点的插入和移除需要支持CPU热插拔。
这样的进步要求内核可用的CPU被移除,要么是出于配置的原因,要么是出于RAS的目的,
以保持一个不需要的CPU不在系统执行路径。因此需要在Linux内核中支持CPU热拔插。
CPU热拔插支持的一个更新颖的用途是它在SMP的暂停恢复支持中的应用。双核和超线程支
持使得即使是笔记本电脑也能运行不支持这些方法的SMP内核。
命令行开关
==========
``maxcpus=n``
限制启动时的CPU为 *n* 个。例如,如果你有四个CPU,使用 ``maxcpus=2`` 将只能启
动两个。你可以选择稍后让其他CPU上线。
``nr_cpus=n``
限制内核将支持的CPU总量。如果这里提供的数量低于实际可用的CPU数量,那么其他CPU
以后就不能上线了。
``possible_cpus=n``
这个选项设置 ``cpu_possible_mask`` 中的 ``possible_cpus`` 位。
这个选项只限于X86和S390架构。
``cpu0_hotplug``
允许关闭CPU0。
这个选项只限于X86架构。
CPU位图
=======
``cpu_possible_mask``
系统中可能可用CPU的位图。这是用来为per_cpu变量分配一些启动时的内存,这些变量
不会随着CPU的可用或移除而增加/减少。一旦在启动时的发现阶段被设置,该映射就是静态
的,也就是说,任何时候都不会增加或删除任何位。根据你的系统需求提前准确地调整它
可以节省一些启动时的内存。
``cpu_online_mask``
当前在线的所有CPU的位图。在一个CPU可用于内核调度并准备接收设备的中断后,它被
设置在 ``__cpu_up()`` 中。当使用 ``__cpu_disable()`` 关闭一个CPU时,它被清
空,在此之前,所有的操作系统服务包括中断都被迁移到另一个目标CPU。
``cpu_present_mask``
系统中当前存在的CPU的位图。它们并非全部在线。当物理热拔插被相关的子系统
(如ACPI)处理时,可以改变和添加新的位或从位图中删除,这取决于事件是
hot-add/hot-remove。目前还没有定死规定。典型的用法是在启动时启动拓扑结构,这时
热插拔被禁用。
你真的不需要操作任何系统的CPU映射。在大多数情况下,它们应该是只读的。当设置每个
CPU资源时,几乎总是使用 ``cpu_possible_mask`` 或 ``for_each_possible_cpu()``
来进行迭代。宏 ``for_each_cpu()`` 可以用来迭代一个自定义的CPU掩码。
不要使用 ``cpumask_t`` 以外的任何东西来表示CPU的位图。
使用CPU热拔插
=============
内核选项 *CONFIG_HOTPLUG_CPU* 需要被启用。它目前可用于多种架构,包括ARM、MIPS、
PowerPC和X86。配置是通过sysfs接口完成的::
$ ls -lh /sys/devices/system/cpu
total 0
drwxr-xr-x 9 root root 0 Dec 21 16:33 cpu0
drwxr-xr-x 9 root root 0 Dec 21 16:33 cpu1
drwxr-xr-x 9 root root 0 Dec 21 16:33 cpu2
drwxr-xr-x 9 root root 0 Dec 21 16:33 cpu3
drwxr-xr-x 9 root root 0 Dec 21 16:33 cpu4
drwxr-xr-x 9 root root 0 Dec 21 16:33 cpu5
drwxr-xr-x 9 root root 0 Dec 21 16:33 cpu6
drwxr-xr-x 9 root root 0 Dec 21 16:33 cpu7
drwxr-xr-x 2 root root 0 Dec 21 16:33 hotplug
-r--r--r-- 1 root root 4.0K Dec 21 16:33 offline
-r--r--r-- 1 root root 4.0K Dec 21 16:33 online
-r--r--r-- 1 root root 4.0K Dec 21 16:33 possible
-r--r--r-- 1 root root 4.0K Dec 21 16:33 present
文件 *offline* 、 *online* 、*possible* 、*present* 代表CPU掩码。每个CPU文件
夹包含一个 *online* 文件,控制逻辑上的开(1)和关(0)状态。要在逻辑上关闭CPU4::
$ echo 0 > /sys/devices/system/cpu/cpu4/online
smpboot: CPU 4 is now offline
一旦CPU被关闭,它将从 */proc/interrupts* 、*/proc/cpuinfo* 中被删除,也不应该
被 *top* 命令显示出来。要让CPU4重新上线::
$ echo 1 > /sys/devices/system/cpu/cpu4/online
smpboot: Booting Node 0 Processor 4 APIC 0x1
CPU又可以使用了。这应该对所有的CPU都有效。CPU0通常比较特殊,被排除在CPU热拔插之外。
在X86上,内核选项 *CONFIG_BOOTPARAM_HOTPLUG_CPU0* 必须被启用,以便能够关闭CPU0。
或者,可以使用内核命令选项 *cpu0_hotplug* 。CPU0的一些已知的依赖性:
* 从休眠/暂停中恢复。如果CPU0处于离线状态,休眠/暂停将失败。
* PIC中断。如果检测到PIC中断,CPU0就不能被移除。
如果你发现CPU0上有任何依赖性,请告知Fenghua Yu <fenghua.yu@intel.com>。
CPU的热拔插协作
===============
下线情况
--------
一旦CPU被逻辑关闭,注册的热插拔状态的清除回调将被调用,从 ``CPUHP_ONLINE`` 开始,到
``CPUHP_OFFLINE`` 状态结束。这包括:
* 如果任务因暂停操作而被冻结,那么 *cpuhp_tasks_frozen* 将被设置为true。
* 所有进程都会从这个将要离线的CPU迁移到新的CPU上。新的CPU是从每个进程的当前cpuset中
选择的,它可能是所有在线CPU的一个子集。
* 所有针对这个CPU的中断都被迁移到新的CPU上。
* 计时器也会被迁移到新的CPU上。
* 一旦所有的服务被迁移,内核会调用一个特定的例程 ``__cpu_disable()`` 来进行特定的清
理。
CPU热插拔API
============
CPU热拔插状态机
---------------
CPU热插拔使用一个从CPUHP_OFFLINE到CPUHP_ONLINE的线性状态空间的普通状态机。每个状态都
有一个startup和teardown的回调。
当一个CPU上线时,将按顺序调用startup回调,直到达到CPUHP_ONLINE状态。当设置状态的回调
或将实例添加到多实例状态时,也可以调用它们。
当一个CPU下线时,将按相反的顺序依次调用teardown回调,直到达到CPUHP_OFFLINE状态。当删
除状态的回调或从多实例状态中删除实例时,也可以调用它们。
如果某个使用场景只需要一个方向的热插拔操作回调(CPU上线或CPU下线),则在设置状态时,
可以将另一个不需要的回调设置为NULL。
状态空间被划分成三个阶段:
* PREPARE阶段
PREPARE阶段涵盖了从CPUHP_OFFLINE到CPUHP_BRINGUP_CPU之间的状态空间。
在该阶段中,startup回调在CPU上线操作启动CPU之前被调用,teardown回调在CPU下线操作使
CPU功能失效之后被调用。
这些回调是在控制CPU上调用的,因为它们显然不能在热插拔的CPU上运行,此时热插拔的CPU要
么还没有启动,要么已经功能失效。
startup回调用于设置CPU成功上线所需要的资源。teardown回调用于释放资源或在热插拔的CPU
功能失效后,将待处理的工作转移到在线的CPU上。
允许startup回调失败。如果回调失败,CPU上线操作被中止,CPU将再次被降到之前的状态(通
常是CPUHP_OFFLINE)。
本阶段中的teardown回调不允许失败。
* STARTING阶段
STARTING阶段涵盖了CPUHP_BRINGUP_CPU + 1到CPUHP_AP_ONLINE之间的状态空间。
该阶段中的startup回调是在早期CPU设置代码中的CPU上线操作期间,禁用中断的情况下在热拔
插的CPU上被调用。teardown回调是在CPU完全关闭前不久的CPU下线操作期间,禁用中断的情况
下在热拔插的CPU上被调用。
该阶段中的回调不允许失败。
回调用于低级别的硬件初始化/关机和核心子系统。
* ONLINE阶段
ONLINE阶段涵盖了CPUHP_AP_ONLINE + 1到CPUHP_ONLINE之间的状态空间。
该阶段中的startup回调是在CPU上线时在热插拔的CPU上调用的。teardown回调是在CPU下线操
作时在热插拔CPU上调用的。
回调是在每个CPU热插拔线程的上下文中调用的,该线程绑定在热插拔的CPU上。回调是在启用
中断和抢占的情况下调用的。
允许回调失败。如果回调失败,CPU热插拔操作被中止,CPU将恢复到之前的状态。
CPU 上线/下线操作
-----------------
一个成功的上线操作如下::
[CPUHP_OFFLINE]
[CPUHP_OFFLINE + 1]->startup() -> 成功
[CPUHP_OFFLINE + 2]->startup() -> 成功
[CPUHP_OFFLINE + 3] -> 略过,因为startup == NULL
...
[CPUHP_BRINGUP_CPU]->startup() -> 成功
=== PREPARE阶段结束
[CPUHP_BRINGUP_CPU + 1]->startup() -> 成功
...
[CPUHP_AP_ONLINE]->startup() -> 成功
=== STARTUP阶段结束
[CPUHP_AP_ONLINE + 1]->startup() -> 成功
...
[CPUHP_ONLINE - 1]->startup() -> 成功
[CPUHP_ONLINE]
一个成功的下线操作如下::
[CPUHP_ONLINE]
[CPUHP_ONLINE - 1]->teardown() -> 成功
...
[CPUHP_AP_ONLINE + 1]->teardown() -> 成功
=== STARTUP阶段开始
[CPUHP_AP_ONLINE]->teardown() -> 成功
...
[CPUHP_BRINGUP_ONLINE - 1]->teardown()
...
=== PREPARE阶段开始
[CPUHP_BRINGUP_CPU]->teardown()
[CPUHP_OFFLINE + 3]->teardown()
[CPUHP_OFFLINE + 2] -> 略过,因为teardown == NULL
[CPUHP_OFFLINE + 1]->teardown()
[CPUHP_OFFLINE]
一个失败的上线操作如下::
[CPUHP_OFFLINE]
[CPUHP_OFFLINE + 1]->startup() -> 成功
[CPUHP_OFFLINE + 2]->startup() -> 成功
[CPUHP_OFFLINE + 3] -> 略过,因为startup == NULL
...
[CPUHP_BRINGUP_CPU]->startup() -> 成功
=== PREPARE阶段结束
[CPUHP_BRINGUP_CPU + 1]->startup() -> 成功
...
[CPUHP_AP_ONLINE]->startup() -> 成功
=== STARTUP阶段结束
[CPUHP_AP_ONLINE + 1]->startup() -> 成功
---
[CPUHP_AP_ONLINE + N]->startup() -> 失败
[CPUHP_AP_ONLINE + (N - 1)]->teardown()
...
[CPUHP_AP_ONLINE + 1]->teardown()
=== STARTUP阶段开始
[CPUHP_AP_ONLINE]->teardown()
...
[CPUHP_BRINGUP_ONLINE - 1]->teardown()
...
=== PREPARE阶段开始
[CPUHP_BRINGUP_CPU]->teardown()
[CPUHP_OFFLINE + 3]->teardown()
[CPUHP_OFFLINE + 2] -> 略过,因为teardown == NULL
[CPUHP_OFFLINE + 1]->teardown()
[CPUHP_OFFLINE]
一个失败的下线操作如下::
[CPUHP_ONLINE]
[CPUHP_ONLINE - 1]->teardown() -> 成功
...
[CPUHP_ONLINE - N]->teardown() -> 失败
[CPUHP_ONLINE - (N - 1)]->startup()
...
[CPUHP_ONLINE - 1]->startup()
[CPUHP_ONLINE]
递归失败不能被合理地处理。
请看下面的例子,由于下线操作失败而导致的递归失败::
[CPUHP_ONLINE]
[CPUHP_ONLINE - 1]->teardown() -> 成功
...
[CPUHP_ONLINE - N]->teardown() -> 失败
[CPUHP_ONLINE - (N - 1)]->startup() -> 成功
[CPUHP_ONLINE - (N - 2)]->startup() -> 失败
CPU热插拔状态机在此停止,且不再尝试回滚,因为这可能会导致死循环::
[CPUHP_ONLINE - (N - 1)]->teardown() -> 成功
[CPUHP_ONLINE - N]->teardown() -> 失败
[CPUHP_ONLINE - (N - 1)]->startup() -> 成功
[CPUHP_ONLINE - (N - 2)]->startup() -> 失败
[CPUHP_ONLINE - (N - 1)]->teardown() -> 成功
[CPUHP_ONLINE - N]->teardown() -> 失败
周而复始,不断重复。在这种情况下,CPU留在该状态中::
[CPUHP_ONLINE - (N - 1)]
这至少可以让系统取得进展,让用户有机会进行调试,甚至解决这个问题。
分配一个状态
------------
有两种方式分配一个CPU热插拔状态:
* 静态分配
当子系统或驱动程序有相对于其他CPU热插拔状态的排序要求时,必须使用静态分配。例如,
在CPU上线操作期间,PERF核心startup回调必须在PERF驱动startup回调之前被调用。在CPU
下线操作中,驱动teardown回调必须在核心teardown回调之前调用。静态分配的状态由
cpuhp_state枚举中的常量描述,可以在include/linux/cpuhotplug.h中找到。
在适当的位置将状态插入枚举中,这样就满足了排序要求。状态常量必须被用于状态的设置
和移除。
当状态回调不是在运行时设置的,并且是kernel/cpu.c中CPU热插拔状态数组初始化的一部分
时,也需要静态分配。
* 动态分配
当对状态回调没有排序要求时,动态分配是首选方法。状态编号由setup函数分配,并在成功
后返回给调用者。
只有PREPARE和ONLINE阶段提供了一个动态分配范围。STARTING阶段则没有,因为该部分的大多
数回调都有明确的排序要求。
CPU热插拔状态的设置
-------------------
核心代码提供了以下函数用来设置状态:
* cpuhp_setup_state(state, name, startup, teardown)
* cpuhp_setup_state_nocalls(state, name, startup, teardown)
* cpuhp_setup_state_cpuslocked(state, name, startup, teardown)
* cpuhp_setup_state_nocalls_cpuslocked(state, name, startup, teardown)
对于一个驱动程序或子系统有多个实例,并且每个实例都需要调用相同的CPU hotplug状态回
调的情况,CPU hotplug核心提供多实例支持。与驱动程序特定的实例列表相比,其优势在于
与实例相关的函数完全针对CPU hotplug操作进行序列化,并在添加和删除时提供状态回调的
自动调用。要设置这样一个多实例状态,可以使用以下函数:
* cpuhp_setup_state_multi(state, name, startup, teardown)
@state参数要么是静态分配的状态,要么是动态分配状态(PUHP_PREPARE_DYN,CPUHP_ONLINE_DYN)
的常量之一, 具体取决于应该分配动态状态的状态阶段(PREPARE,ONLINE)。
@name参数用于sysfs输出和检测。命名惯例是"subsys:mode"或"subsys/driver:mode",
例如 "perf:mode"或"perf/x86:mode"。常见的mode名称有:
======== ============================================
prepare 对应PREPARE阶段中的状态
dead 对应PREPARE阶段中不提供startup回调的状态
starting 对应STARTING阶段中的状态
dying 对应STARTING阶段中不提供startup回调的状态
online 对应ONLINE阶段中的状态
offline 对应ONLINE阶段中不提供startup回调的状态
======== ============================================
由于@name参数只用于sysfs和检测,如果其他mode描述符比常见的描述符更好地描述状态的性质,
也可以使用。
@name参数的示例:"perf/online", "perf/x86:prepare", "RCU/tree:dying", "sched/waitempty"
@startup参数是一个指向回调的函数指针,在CPU上线操作时被调用。若应用不需要startup
回调,则将该指针设为NULL。
@teardown参数是一个指向回调的函数指针,在CPU下线操作时调用。若应用不需要teardown
回调,则将该指针设为NULL。
这些函数在处理已注册回调的方式上有所不同:
* cpuhp_setup_state_nocalls(), cpuhp_setup_state_nocalls_cpuslocked()和
cpuhp_setup_state_multi()只注册回调。
* cpuhp_setup_state()和cpuhp_setup_state_cpuslocked()注册回调,并对当前状态大于新
安装状态的所有在线CPU调用@startup回调(如果不是NULL)。根据状态阶段,回调要么在
当前的CPU上调用(PREPARE阶段),要么在CPU的热插拔线程中调用每个在线CPU(ONLINE阶段)。
如果CPU N的回调失败,那么CPU 0...N-1的teardown回调被调用以回滚操作。状态设置失败,
状态的回调没有被注册,在动态分配的情况下,分配的状态被释放。
状态设置和回调调用是针对CPU热拔插操作进行序列化的。如果设置函数必须从CPU热插拔的读
锁定区域调用,那么必须使用_cpuslocked()变体。这些函数不能在CPU热拔插回调中使用。
函数返回值:
======== ==========================================================
0 静态分配的状态设置成功
>0 动态分配的状态设置成功
返回的数值是被分配的状态编号。如果状态回调后来必须被移除,
例如模块移除,那么这个数值必须由调用者保存,并作为状态移
除函数的@state参数。对于多实例状态,动态分配的状态编号也
需要作为实例添加/删除操作的@state参数。
<0 操作失败
======== ==========================================================
移除CPU热拔插状态
-----------------
为了移除一个之前设置好的状态,提供了如下函数:
* cpuhp_remove_state(state)
* cpuhp_remove_state_nocalls(state)
* cpuhp_remove_state_nocalls_cpuslocked(state)
* cpuhp_remove_multi_state(state)
@state参数要么是静态分配的状态,要么是由cpuhp_setup_state*()在动态范围内分配
的状态编号。如果状态在动态范围内,则状态编号被释放,可再次进行动态分配。
这些函数在处理已注册回调的方式上有所不同:
* cpuhp_remove_state_nocalls(), cpuhp_remove_state_nocalls_cpuslocked()
和 cpuhp_remove_multi_state()只删除回调。
* cpuhp_remove_state()删除回调,并调用所有当前状态大于被删除状态的在线CPU的
teardown回调(如果不是NULL)。根据状态阶段,回调要么在当前的CPU上调用
(PREPARE阶段),要么在CPU的热插拔线程中调用每个在线CPU(ONLINE阶段)。
为了完成移除工作,teardown回调不能失败。
状态移除和回调调用是针对CPU热拔插操作进行序列化的。如果移除函数必须从CPU hotplug
读取锁定区域调用,那么必须使用_cpuslocked()变体。这些函数不能从CPU热插拔的回调中使用。
如果一个多实例的状态被移除,那么调用者必须先移除所有的实例。
多实例状态实例管理
------------------
一旦多实例状态被建立,实例就可以被添加到状态中:
* cpuhp_state_add_instance(state, node)
* cpuhp_state_add_instance_nocalls(state, node)
@state参数是一个静态分配的状态或由cpuhp_setup_state_multi()在动态范围内分配的状
态编号。
@node参数是一个指向hlist_node的指针,它被嵌入到实例的数据结构中。这个指针被交给
多实例状态的回调,可以被回调用来通过container_of()检索到实例。
这些函数在处理已注册回调的方式上有所不同:
* cpuhp_state_add_instance_nocalls()只将实例添加到多实例状态的节点列表中。
* cpuhp_state_add_instance()为所有当前状态大于@state的在线CPU添加实例并调用与
@state相关的startup回调(如果不是NULL)。该回调只对将要添加的实例进行调用。
根据状态阶段,回调要么在当前的CPU上调用(PREPARE阶段),要么在CPU的热插拔线
程中调用每个在线CPU(ONLINE阶段)。
如果CPU N的回调失败,那么CPU 0 ... N-1的teardown回调被调用以回滚操作,该函数
失败,实例不会被添加到多实例状态的节点列表中。
要从状态的节点列表中删除一个实例,可以使用这些函数:
* cpuhp_state_remove_instance(state, node)
* cpuhp_state_remove_instance_nocalls(state, node)
参数与上述cpuhp_state_add_instance*()变体相同。
这些函数在处理已注册回调的方式上有所不同:
* cpuhp_state_remove_instance_nocalls()只从状态的节点列表中删除实例。
* cpuhp_state_remove_instance()删除实例并调用与@state相关的回调(如果不是NULL),
用于所有当前状态大于@state的在线CPU。 该回调只对将要被移除的实例进行调用。
根据状态阶段,回调要么在当前的CPU上调用(PREPARE阶段),要么在CPU的热插拔
线程中调用每个在线CPU(ONLINE阶段)。
为了完成移除工作,teardown回调不能失败。
节点列表的添加/删除操作和回调调用是针对CPU热拔插操作进行序列化。这些函数不能在
CPU hotplug回调和CPU hotplug读取锁定区域内使用。
样例
----
在STARTING阶段设置和取消静态分配的状态,以获取上线和下线操作的通知::
ret = cpuhp_setup_state(CPUHP_SUBSYS_STARTING, "subsys:starting", subsys_cpu_starting, subsys_cpu_dying);
if (ret < 0)
return ret;
....
cpuhp_remove_state(CPUHP_SUBSYS_STARTING);
在ONLINE阶段设置和取消动态分配的状态,以获取下线操作的通知::
state = cpuhp_setup_state(CPUHP_ONLINE_DYN, "subsys:offline", NULL, subsys_cpu_offline);
if (state < 0)
return state;
....
cpuhp_remove_state(state);
在ONLINE阶段设置和取消动态分配的状态,以获取有关上线操作的通知,而无需调用回调::
state = cpuhp_setup_state_nocalls(CPUHP_ONLINE_DYN, "subsys:online", subsys_cpu_online, NULL);
if (state < 0)
return state;
....
cpuhp_remove_state_nocalls(state);
在ONLINE阶段设置、使用和取消动态分配的多实例状态,以获得上线和下线操作的通知::
state = cpuhp_setup_state_multi(CPUHP_ONLINE_DYN, "subsys:online", subsys_cpu_online, subsys_cpu_offline);
if (state < 0)
return state;
....
ret = cpuhp_state_add_instance(state, &inst1->node);
if (ret)
return ret;
....
ret = cpuhp_state_add_instance(state, &inst2->node);
if (ret)
return ret;
....
cpuhp_remove_instance(state, &inst1->node);
....
cpuhp_remove_instance(state, &inst2->node);
....
remove_multi_state(state);
测试热拔插状态
==============
验证自定义状态是否按预期工作的一个方法是关闭一个CPU,然后再把它上线。也可以把CPU放到某
些状态(例如 ``CPUHP_AP_ONLINE`` ),然后再回到 ``CPUHP_ONLINE`` 。这将模拟在
``CPUHP_AP_ONLINE`` 之后的一个状态出现错误,从而导致回滚到在线状态。
所有注册的状态都被列举在 ``/sys/devices/system/cpu/hotplug/states`` ::
$ tail /sys/devices/system/cpu/hotplug/states
138: mm/vmscan:online
139: mm/vmstat:online
140: lib/percpu_cnt:online
141: acpi/cpu-drv:online
142: base/cacheinfo:online
143: virtio/net:online
144: x86/mce:online
145: printk:online
168: sched:active
169: online
要将CPU4回滚到 ``lib/percpu_cnt:online`` ,再回到在线状态,只需发出::
$ cat /sys/devices/system/cpu/cpu4/hotplug/state
169
$ echo 140 > /sys/devices/system/cpu/cpu4/hotplug/target
$ cat /sys/devices/system/cpu/cpu4/hotplug/state
140
需要注意的是,状态140的清除回调已经被调用。现在重新上线::
$ echo 169 > /sys/devices/system/cpu/cpu4/hotplug/target
$ cat /sys/devices/system/cpu/cpu4/hotplug/state
169
启用追踪事件后,单个步骤也是可见的::
# TASK-PID CPU# TIMESTAMP FUNCTION
# | | | | |
bash-394 [001] 22.976: cpuhp_enter: cpu: 0004 target: 140 step: 169 (cpuhp_kick_ap_work)
cpuhp/4-31 [004] 22.977: cpuhp_enter: cpu: 0004 target: 140 step: 168 (sched_cpu_deactivate)
cpuhp/4-31 [004] 22.990: cpuhp_exit: cpu: 0004 state: 168 step: 168 ret: 0
cpuhp/4-31 [004] 22.991: cpuhp_enter: cpu: 0004 target: 140 step: 144 (mce_cpu_pre_down)
cpuhp/4-31 [004] 22.992: cpuhp_exit: cpu: 0004 state: 144 step: 144 ret: 0
cpuhp/4-31 [004] 22.993: cpuhp_multi_enter: cpu: 0004 target: 140 step: 143 (virtnet_cpu_down_prep)
cpuhp/4-31 [004] 22.994: cpuhp_exit: cpu: 0004 state: 143 step: 143 ret: 0
cpuhp/4-31 [004] 22.995: cpuhp_enter: cpu: 0004 target: 140 step: 142 (cacheinfo_cpu_pre_down)
cpuhp/4-31 [004] 22.996: cpuhp_exit: cpu: 0004 state: 142 step: 142 ret: 0
bash-394 [001] 22.997: cpuhp_exit: cpu: 0004 state: 140 step: 169 ret: 0
bash-394 [005] 95.540: cpuhp_enter: cpu: 0004 target: 169 step: 140 (cpuhp_kick_ap_work)
cpuhp/4-31 [004] 95.541: cpuhp_enter: cpu: 0004 target: 169 step: 141 (acpi_soft_cpu_online)
cpuhp/4-31 [004] 95.542: cpuhp_exit: cpu: 0004 state: 141 step: 141 ret: 0
cpuhp/4-31 [004] 95.543: cpuhp_enter: cpu: 0004 target: 169 step: 142 (cacheinfo_cpu_online)
cpuhp/4-31 [004] 95.544: cpuhp_exit: cpu: 0004 state: 142 step: 142 ret: 0
cpuhp/4-31 [004] 95.545: cpuhp_multi_enter: cpu: 0004 target: 169 step: 143 (virtnet_cpu_online)
cpuhp/4-31 [004] 95.546: cpuhp_exit: cpu: 0004 state: 143 step: 143 ret: 0
cpuhp/4-31 [004] 95.547: cpuhp_enter: cpu: 0004 target: 169 step: 144 (mce_cpu_online)
cpuhp/4-31 [004] 95.548: cpuhp_exit: cpu: 0004 state: 144 step: 144 ret: 0
cpuhp/4-31 [004] 95.549: cpuhp_enter: cpu: 0004 target: 169 step: 145 (console_cpu_notify)
cpuhp/4-31 [004] 95.550: cpuhp_exit: cpu: 0004 state: 145 step: 145 ret: 0
cpuhp/4-31 [004] 95.551: cpuhp_enter: cpu: 0004 target: 169 step: 168 (sched_cpu_activate)
cpuhp/4-31 [004] 95.552: cpuhp_exit: cpu: 0004 state: 168 step: 168 ret: 0
bash-394 [005] 95.553: cpuhp_exit: cpu: 0004 state: 169 step: 140 ret: 0
可以看到,CPU4一直下降到时间戳22.996,然后又上升到95.552。所有被调用的回调,
包括它们的返回代码都可以在跟踪中看到。
架构的要求
==========
需要具备以下功能和配置:
``CONFIG_HOTPLUG_CPU``
这个配置项需要在Kconfig中启用
``__cpu_up()``
调出一个cpu的架构接口
``__cpu_disable()``
关闭CPU的架构接口,在此程序返回后,内核不能再处理任何中断。这包括定时器的关闭。
``__cpu_die()``
这实际上是为了确保CPU的死亡。实际上,看看其他架构中实现CPU热拔插的一些示例代
码。对于那个特定的架构,处理器被从 ``idle()`` 循环中拿下来。 ``__cpu_die()``
通常会等待一些per_cpu状态的设置,以确保处理器的死亡例程被调用来保持活跃。
用户空间通知
============
在CPU成功上线或下线后,udev事件被发送。一个udev规则,比如::
SUBSYSTEM=="cpu", DRIVERS=="processor", DEVPATH=="/devices/system/cpu/*", RUN+="the_hotplug_receiver.sh"
将接收所有事件。一个像这样的脚本::
#!/bin/sh
if [ "${ACTION}" = "offline" ]
then
echo "CPU ${DEVPATH##*/} offline"
elif [ "${ACTION}" = "online" ]
then
echo "CPU ${DEVPATH##*/} online"
fi
可以进一步处理该事件。
内核内联文档参考
================
该API在以下内核代码中:
include/linux/cpuhotplug.h
@@ -0,0 +1,145 @@
.. SPDX-License-Identifier: GPL-2.0+
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/errseq.rst
:翻译:
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
:校译:
吴想成 Wu Xiangcheng <bobwxc@email.cn>
================
errseq_t数据类型
================
``errseq_t`` 是一种在一个地方记录错误的方法,并允许任意数量的 ``订阅者`` 判断自上
次采样点以来是否发生了变化。
最初的用例是跟踪文件同步系统调用( ``fsync``, ``fdatasync``, ``msync`` 和
``sync_file_range`` )的错误,但它也可以用于其他情况。
它被实现为一个无符号的32位值。低位被指定保存错误代码(在1和MAX_ERRNO之间)。高位
用作计数器。这里是用原子操作而不是锁来完成的,因此可以从任何上下文中调用这些函数。
请注意,如果频繁记录新错误,则存在冲突风险,因为我们用作计数器的位很少。
为了缓解这种情况,错误值和计数器之间的位被用作一个标志,以判断自记录新值以来是否
对该值进行了采样。这使我们能够避免在上次记录错误后没有人取样的情况下碰撞计数器。
因此,我们得到了一个类似这样的值:
+--------------------------------------+------+------------------------+
| 31..13 | 12 | 11..0 |
+--------------------------------------+------+------------------------+
| 计数器 | 标志 | 错误值 |
+--------------------------------------+------+------------------------+
总体思路是让 ``观察者`` 对errseq_t值进行采样,并将其保留为运行游标。该值稍后可用
于判断自采样完成后是否发生了任何新错误,并原子地记录检查时的状态。这使得我们能在
一个地方记录错误,然后有许多 ``观察者`` 可以判断自上次检查以来该值是否发生了变化。
新的errseq_t应始终清零。全零的errseq_t值是从未出现错误的特殊(但常见)情况。因此,
如果您希望知道自首次初始化以来是否曾经有过错误集,则全零值被用作 ``纪元`` 。
API的使用方法
=============
让我给你们讲一个关于员工drone的故事。现在,他总体上是个好员工,但公司有点...管理
繁重。他今天必须向77名主管汇报,明天 ``大老板`` 要从外地赶来,他肯定也会考验这个
可怜的家伙。
他们都把工作交给他去做---多到他都记不住谁交给他什么了,但这并不是什么大问题。主管
们只想知道他什么时候完成他们迄今为止交给他的所有工作,以及自从他们上次询问以来他
是否犯了任何错误。
他可能在他们实际上并没有交给他的工作上犯了错误,但他无法在那么详细的层面上记录事
情,他所能记得的只是他最近犯的错误。
下面是我们 ``worker_drone`` 的表达式::
struct worker_drone {
errseq_t wd_err; /* 用来记录错误 */
};
每天, ``worker_drone`` 都是以一张白纸开始的::
struct worker_drone wd;
wd.wd_err = (errseq_t)0;
主管们进来后对当天的工作进行初步了解。他们并不关心在他们观察开始之前发生的任何事
情::
struct supervisor {
errseq_t s_wd_err; /* wd_err的私有“游标” */
spinlock_t s_wd_err_lock; /* 保护s_wd_err */
}
struct supervisor su;
su.s_wd_err = errseq_sample(&wd.wd_err);
spin_lock_init(&su.s_wd_err_lock);
现在他们开始给他布置任务。每隔几分钟,他们就要求他完成迄今为止交给他的所有工作。
然后问他是否有犯任何错误::
spin_lock(&su.su_wd_err_lock);
err = errseq_check_and_advance(&wd.wd_err, &su.s_wd_err);
spin_unlock(&su.su_wd_err_lock);
到目前为止,它只是不断返回0。
现在,这家公司的老板非常吝啬,给了他不合格的设备来完成他的工作。偶尔设备会出现故
障,导致他犯错。他重重地叹了一口气,并把它记录下来::
errseq_set(&wd.wd_err, -EIO);
...然后继续工作。主管们最终会再次检查,他们在下次检查时都会发现这个错误。后续的调
用将返回0,直到记录下另一个错误,此时将向每个调用报告一次。
请注意,主管们无法知道他们犯了多少错误,只能知道自上次检查以来是否犯了一个错误,
以及记录的最新值。
偶尔,大老板会来抽查,要求员工为他做一次性的工作。他并不像主管们那样全职观察员工,
但他确实需要知道在他的工作处理过程中是否发生了错误。
他只需对员工当前的errseq_t进行采样,然后用它来判断后来是否发生了错误::
errseq_t since = errseq_sample(&wd.wd_err);
/* 提交一些工作,等待完成 */
err = errseq_check(&wd.wd_err, since);
由于他只是要在那个点之后丢弃 ``since`` ,所以他不需要在这里推进它。同时他也不需要
任何锁,因为它不能被其他人使用。
序列化更新errseq_t游标
======================
请注意,errseq_t API在check_and_advance_operation期间不保护errseq_t游标。只有典型
的错误代码是被原子化处理的。在多任务同时使用同一个errseq_t游标的情况下,对该游标
的更新进行序列化是很重要的。
如果不这样做,那么游标就有可能向后移动。在这种情况下,同一个错误可能被报告多次。
因此,通常先执行errseq_check检查是否有任何变化,然后在获取锁后才执行
errseq_check_and_advance。例如::
if (errseq_check(&wd.wd_err, READ_ONCE(su.s_wd_err)) {
/* su.s_wd_err被s_wd_err_lock保护 */
spin_lock(&su.s_wd_err_lock);
err = errseq_check_and_advance(&wd.wd_err, &su.s_wd_err);
spin_unlock(&su.s_wd_err_lock);
}
这就避免了自上次检查以来没有任何变化的常见情况下的自旋锁。
函数
====
该API在以下内核代码中:
lib/errseq.c
@@ -0,0 +1,109 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/genalloc.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
时奎亮 <alexs@kernel.org>
.. _cn_core-api_genalloc:
genalloc/genpool子系统
======================
内核中有许多内存分配子系统,每一个都是针对特定的需求。然而,有时候,内核开发者需
要为特定范围的特殊用途的内存实现一个新的分配器;通常这个内存位于某个设备上。该设
备的驱动程序的作者当然可以写一个小的分配器来完成工作,但这是让内核充满几十个测试
差劲的分配器的方法。早在2005年,Jes Sorensen从sym53c8xx_2驱动中提取了其中的一
个分配器,并将其作为一个通用模块发布,用于创建特设的内存分配器。这段代码在2.6.13
版本中被合并;此后它被大大地修改了。
.. _posted: https://lwn.net/Articles/125842/
使用这个分配器的代码应该包括<linux/genalloc.h>。这个动作从创建一个池开始,使用
一个:
该API在以下内核代码中:
lib/genalloc.c
对gen_pool_create()的调用将创建一个内存池。分配的粒度由min_alloc_order设置;它
是一个log-base-2(以2为底的对数)的数字,就像页面分配器使用的数字一样,但它指的是
字节而不是页面。因此,如果min_alloc_order被传递为3,那么所有的分配将是8字节的倍数。
增加min_alloc_order可以减少跟踪池中内存所需的内存。nid参数指定哪一个NUMA节点应该被
用于分配管家结构体;如果调用者不关心,它可以是-1。
“管理的”接口devm_gen_pool_create()将内存池与一个特定的设备联系起来。在其他方面,
当给定的设备被销毁时,它将自动清理内存池。
一个内存池池被关闭的方法是:
该API在以下内核代码中:
lib/genalloc.c
值得注意的是,如果在给定的内存池中仍有未完成的分配,这个函数将采取相当极端的步骤,调用
BUG(),使整个系统崩溃。你已经被警告了。
一个新创建的内存池没有内存可以分配。在这种状态下,它是相当无用的,所以首要任务之一通常
是向内存池里添加内存。这可以通过以下方式完成:
该API在以下内核代码中:
include/linux/genalloc.h
lib/genalloc.c
对gen_pool_add()的调用将把从地址(在内核的虚拟地址空间)开始的内存的大小字节放入
给定的池中,再次使用nid作为节点ID进行辅助内存分配。gen_pool_add_virt()变体将显式
物理地址与内存联系起来;只有在内存池被用于DMA分配时,这才是必要的。
从内存池中分配内存(并将其放回)的函数是:
该API在以下内核代码中:
include/linux/genalloc.h
lib/genalloc.c
正如人们所期望的,gen_pool_alloc()将从给定的池中分配size<字节。gen_pool_dma_alloc()
变量分配内存用于DMA操作,返回dma所指向的空间中的相关物理地址。这只有在内存是用
gen_pool_add_virt()添加的情况下才会起作用。请注意,这个函数偏离了genpool通常使用
无符号长值来表示内核地址的模式;它返回一个void * 来代替。
这一切看起来都比较简单;事实上,一些开发者显然认为这太简单了。毕竟,上面的接口没有提
供对分配函数如何选择返回哪块特定内存的控制。如果需要这样的控制,下面的函数将是有意义
的:
该API在以下内核代码中:
lib/genalloc.c
使用gen_pool_alloc_algo()进行的分配指定了一种用于选择要分配的内存的算法;默认算法可
以用gen_pool_set_algo()来设置。数据值被传递给算法;大多数算法会忽略它,但偶尔也会需
要它。当然,人们可以写一个特殊用途的算法,但是已经有一套公平的算法可用了:
- gen_pool_first_fit是一个简单的初配分配器;如果没有指定其他算法,这是默认算法。
- gen_pool_first_fit_align强迫分配有一个特定的对齐方式(通过genpool_data_align结
构中的数据传递)。
- gen_pool_first_fit_order_align 按照大小的顺序排列分配。例如,一个60字节的分配将
以64字节对齐。
- gen_pool_best_fit,正如人们所期望的,是一个简单的最佳匹配分配器。
- gen_pool_fixed_alloc在池中的一个特定偏移量(通过数据参数在genpool_data_fixed结
构中传递)进行分配。如果指定的内存不可用,则分配失败。
还有一些其他的函数,主要是为了查询内存池中的可用空间或迭代内存块等目的。然而,大多数
用户应该不需要以上描述的功能。如果幸运的话,对这个模块的广泛认识将有助于防止在未来编
写特殊用途的内存分配器。
该API在以下内核代码中:
lib/genalloc.c
@@ -0,0 +1,23 @@
.. SPDX-License-Identifier: GPL-2.0+
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/generic-radix-tree.rst
:翻译:
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
===================
通用基数树/稀疏数组
===================
通用基数树/稀疏数组的相关内容请见include/linux/generic-radix-tree.h文件中的
“DOC: Generic radix trees/sparse arrays”。
通用基数树函数
--------------
该API在以下内核代码中:
include/linux/generic-radix-tree.h
@@ -0,0 +1,409 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/genericirq.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
吴想成 Wu XiangCheng <bobwxc@email.cn>
.. include:: <isonum.txt>
.. _cn_core-api_genericirq:
================
Linux通用IRQ处理
================
:版权: |copy| 2005-2010: Thomas Gleixner
:版权: |copy| 2005-2006: Ingo Molnar
简介
====
通用中断处理层是为了给设备驱动程序提供一个完整的中断处理抽象(层)。它能够处
理所有不同类型的中断控制器硬件。设备驱动程序使用通用API函数来请求、启用、禁
用和释放中断。驱动程序不需要知道任何关于硬件处理中断的细节,所以它们可以在不同的
平台上使用而不需要修改代码。
本文档提供给那些希望在通用IRQ处理层的帮助下实现基于其架构的中断子系统的开发
者。
理论依据
========
Linux中中断处理的原始实现使用__do_IRQ()超级处理程序,它能够处理每种类型的
中断逻辑。
最初,Russell King确定了不同类型的处理程序,以便为Linux 2.5/2.6中的ARM中
断处理程序实现建立一个相当通用的集合。他区分了以下几种类型:
- 电平触发型
- 边沿触发型
- 简单型
在实现过程中,我们发现了另一种类型:
- 响应EOI(end of interrupt)型
在SMP的__do_IRQ()超级处理程序中,还需定义一种类型:
-  每cpu型(针对CPU SMP)
这种高层IRQ处理程序的拆分实现使我们能够为每个特定的中断类型优化中断处理的流
程。这减少了该特定代码路径的复杂性,并允许对特定类型进行优化处理。
最初的通用IRQ实现使用hw_interrupt_type结构体及其 ``->ack`` ``->end`` 等回
调来区分超级处理程序中的流控制。这导致了流逻辑和低级硬件逻辑的混合,也导致了
不必要的代码重复:例如i386中的 ``ioapic_level_irq`` 和 ``ioapic_edge_irq`` ,
这两个IRQ类型共享许多低级的细节,但有不同的流处理。
一个更自然的抽象是“irq流”和“芯片细节”的干净分离。
分析一些架构的IRQ子系统的实现可以发现,他们中的大多数可以使用一套通用的“irq
流”方法,只需要添加芯片级的特定代码。这种分离对于那些需要IRQ流本身而不需要芯
片细节的特定(子)架构也很有价值——以提供了一个更透明的IRQ子系统设计。
每个中断描述符都被分配给它自己的高层流程处理程序,这通常是一个通用的实现。(这
种高层次的流程处理程序的实现也使得提供解复用处理程序变得简单,这可以在各种架
构的嵌入式平台上找到。)
这种分离使得通用中断处理层更加灵活和可扩展。例如,一个(子)架构可以使用通用
的IRQ流实现“电平触发型”中断,并添加一个(子)架构特定的“边沿型”实现。
为了使向新模型的过渡更容易,并防止破坏现有实现,__do_IRQ()超级处理程序仍然
可用。这导致了一种暂时的双重性。随着时间的推移,新的模型应该在越来越多的架构中
被使用,因为它能使IRQ子系统更小更干净。它已经被废弃三年了,即将被删除。
已知的缺陷和假设
================
没有(但愿如此)。
抽象层
======
中断代码中主要有三个抽象层次:
1. 高级别的驱动API
2. 高级别的IRQ流处理器
3. 芯片级的硬件封装
中断控制流
----------
每个中断都由一个中断描述符结构体irq_desc来描述。中断是由一个“无符号整型”的数值来
引用的,它在描述符结构体数组中选择相应的中断描述符结构体。描述符结构体包含状态
信息和指向中断流方法和中断芯片结构的指针,这些都是分配给这个中断的。
每当中断触发时,低级架构代码通过调用desc->handle_irq()调用到通用中断代码中。
这个高层IRQ处理函数只使用由分配的芯片描述符结构体引用的desc->irq_data.chip
基元。
高级驱动程序API
---------------
高层驱动API由以下函数组成:
- request_irq()
- request_threaded_irq()
- free_irq()
- disable_irq()
- enable_irq()
- disable_irq_nosync() (SMP only)
- synchronize_irq() (SMP only)
- irq_set_irq_type()
- irq_set_irq_wake()
- irq_set_handler_data()
- irq_set_chip()
- irq_set_chip_data()
详见自动生成的函数文档。
.. note::
由于文档构建流程所限,中文文档中并没有引入自动生成的函数文档,所以请读者直接
阅读源码注释。
电平触发型IRQ流处理程序
-----------------------
通用层提供了一套预定义的irq-flow方法:
- handle_level_irq()
- handle_edge_irq()
- handle_fasteoi_irq()
- handle_simple_irq()
- handle_percpu_irq()
- handle_edge_eoi_irq()
- handle_bad_irq()
中断流处理程序(无论是预定义的还是架构特定的)由架构在启动期间或设备初始化期间分配给
特定中断。
默认流实现
~~~~~~~~~~
辅助函数
^^^^^^^^
辅助函数调用芯片基元,并被默认流实现所使用。以下是实现的辅助函数(简化摘录)::
default_enable(struct irq_data *data)
{
desc->irq_data.chip->irq_unmask(data);
}
default_disable(struct irq_data *data)
{
if (!delay_disable(data))
desc->irq_data.chip->irq_mask(data);
}
default_ack(struct irq_data *data)
{
chip->irq_ack(data);
}
default_mask_ack(struct irq_data *data)
{
if (chip->irq_mask_ack) {
chip->irq_mask_ack(data);
} else {
chip->irq_mask(data);
chip->irq_ack(data);
}
}
noop(struct irq_data *data))
{
}
默认流处理程序的实现
~~~~~~~~~~~~~~~~~~~~
电平触发型IRQ流处理器
^^^^^^^^^^^^^^^^^^^^^
handle_level_irq为电平触发型的中断提供了一个通用实现。
实现的控制流如下(简化摘录)::
desc->irq_data.chip->irq_mask_ack();
handle_irq_event(desc->action);
desc->irq_data.chip->irq_unmask();
默认的需回应IRQ流处理器
^^^^^^^^^^^^^^^^^^^^^^^
handle_fasteoi_irq为中断提供了一个通用的实现,它只需要在处理程序的末端有一个EOI。
实现的控制流如下(简化摘录)::
handle_irq_event(desc->action);
desc->irq_data.chip->irq_eoi();
默认的边沿触发型IRQ流处理器
^^^^^^^^^^^^^^^^^^^^^^^^^^^
handle_edge_irq为边沿触发型的中断提供了一个通用的实现。
实现的控制流如下(简化摘录)::
if (desc->status & running) {
desc->irq_data.chip->irq_mask_ack();
desc->status |= pending | masked;
return;
}
desc->irq_data.chip->irq_ack();
desc->status |= running;
do {
if (desc->status & masked)
desc->irq_data.chip->irq_unmask();
desc->status &= ~pending;
handle_irq_event(desc->action);
} while (status & pending);
desc->status &= ~running;
默认的简单型IRQ流处理器
^^^^^^^^^^^^^^^^^^^^^^^
handle_simple_irq提供了一个简单型中断的通用实现。
.. note::
简单型的流处理程序不调用任何处理程序/芯片基元。
实现的控制流程如下(简化摘录)::
handle_irq_event(desc->action);
默认的每CPU型流处理程序
^^^^^^^^^^^^^^^^^^^^^^^
handle_percpu_irq为每CPU型中断提供一个通用的实现。
每个CPU中断只在SMP上可用,该处理程序提供了一个没有锁的简化版本。
以下是控制流的实现(简化摘录)::
if (desc->irq_data.chip->irq_ack)
desc->irq_data.chip->irq_ack();
handle_irq_event(desc->action);
if (desc->irq_data.chip->irq_eoi)
desc->irq_data.chip->irq_eoi();
EOI边沿型IRQ流处理器
^^^^^^^^^^^^^^^^^^^^
handle_edge_eoi_irq提供了一个异常的边沿触发型处理程序,它只用于拯救powerpc/cell
上的一个严重失控的irq控制器。
坏的IRQ流处理器
^^^^^^^^^^^^^^^
handle_bad_irq用于处理没有真正分配处理程序的假中断。
特殊性和优化
~~~~~~~~~~~~
通用函数是为“干净”的架构和芯片设计的,它们没有平台特定的IRQ处理特殊性。如果一
个架构需要在“流”的层面上实现特殊性,那么它可以通过覆盖高层的IRQ-流处理程序来实
现。
延迟中断禁用
~~~~~~~~~~~~
每个中断可选择的功能是由Russell King在ARM中断实现中引入的,当调用disable_irq()
时,不会在硬件层面上屏蔽中断。中断保持启用状态,而在中断事件发生时在流处理器中被
屏蔽。这可以防止在硬件上丢失边沿中断,因为硬件上不存储边沿中断事件,而中断在硬件
级被禁用。当一个中断在IRQ_DISABLED标志被设置时到达,那么该中断在硬件层面被屏蔽,
IRQ_PENDING位被设置。当中断被enable_irq()重新启用时,将检查挂起位,如果它被设置,
中断将通过硬件或软件重发机制重新发送。(当你想使用延迟中断禁用功能,而你的硬件又不
能重新触发中断时,有必要启用CONFIG_HARDIRQS_SW_RESEND。) 延迟中断禁止功能是不可
配置的。
芯片级硬件封装
--------------
芯片级硬件描述符结构体 :c:type:`irq_chip` 包含了所有与芯片直接相关的功能,这些功
能可以被irq流实现所利用。
- ``irq_ack``
- ``irq_mask_ack`` - 可选的,建议使用的性能
- ``irq_mask``
- ``irq_unmask``
- ``irq_eoi`` - 可选的,EOI流处理程序需要
- ``irq_retrigger`` - 可选的
- ``irq_set_type`` - 可选的
- ``irq_set_wake`` - 可选的
这些基元的意思是严格意义上的:ack是指ACK,masking是指对IRQ线的屏蔽,等等。这取决
于流处理器如何使用这些基本的低级功能单元。
__do_IRQ入口点
==============
最初的实现__do_IRQ()是所有类型中断的替代入口点。它已经不存在了。
这个处理程序被证明不适合所有的中断硬件,因此被重新实现了边沿/级别/简单/超高速中断
的拆分功能。这不仅是一个功能优化。它也缩短了中断的代码路径。
在SMP上的锁
===========
芯片寄存器的锁定是由定义芯片基元的架构决定的。每个寄存器的结构通过desc->lock,由
通用层保护。
通用中断芯片
============
为了避免复制相同的IRQ芯片实现,核心提供了一个可配置的通用中断芯片实现。开发者在自
己实现相同的功能之前,应该仔细检查通用芯片是否符合他们的需求,并以稍微不同的方式实
现相同的功能。
该API在以下内核代码中:
kernel/irq/generic-chip.c
结构体
======
本章包含自动生成的结构体文档,这些结构体在通用IRQ层中使用。
该API在以下内核代码中:
include/linux/irq.h
include/linux/interrupt.h
提供的通用函数
==============
这一章包含了自动生成的内核API函数的文档,这些函数被导出。
该API在以下内核代码中:
kernel/irq/manage.c
kernel/irq/chip.c
提供的内部函数
==============
本章包含自动生成的内部函数的文档。
该API在以下内核代码中:
kernel/irq/irqdesc.c
kernel/irq/handle.c
kernel/irq/chip.c
鸣谢
====
感谢以下人士对本文档作出的贡献:
1. Thomas Gleixner tglx@kernel.org
2. Ingo Molnar mingo@elte.hu
@@ -0,0 +1,66 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/gfp_mask-from-fs-io.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
时奎亮 <alexs@kernel.org>
.. _cn_core-api_gfp_mask-from-fs-io:
============================
从FS/IO上下文中使用的GFP掩码
============================
:日期: 2018年5月
:作者: Michal Hocko <mhocko@kernel.org>
简介
====
文件系统和IO栈中的代码路径在分配内存时必须小心,以防止因直接调用FS或IO路径的内
存回收和阻塞已经持有的资源(例如锁--最常见的是用于事务上下文的锁)而造成递归死
锁。
避免这种死锁问题的传统方法是在调用分配器时,在gfp掩码中清除__GFP_FS和__GFP_IO
(注意后者意味着也要清除第一个)。GFP_NOFS和GFP_NOIO可以作为快捷方式使用。但事
实证明,上述方法导致了滥用,当限制性的gfp掩码被用于“万一”时,没有更深入的考虑,
这导致了问题,因为过度使用GFP_NOFS/GFP_NOIO会导致内存过度回收或其他内存回收的问
题。
新API
=====
从4.12开始,我们为NOFS和NOIO上下文提供了一个通用的作用域API,分别是
``memalloc_nofs_save`` , ``memalloc_nofs_restore`` 和 ``memalloc_noio_save`` ,
``memalloc_noio_restore`` ,允许从文件系统或I/O的角度将一个作用域标记为一个
关键部分。从该作用域的任何分配都将从给定的掩码中删除__GFP_FS和__GFP_IO,所以
没有内存分配可以追溯到FS/IO中。
该API在以下内核代码中:
include/linux/sched/mm.h
然后,FS/IO代码在任何与回收有关的关键部分开始之前简单地调用适当的保存函数
——例如,与回收上下文共享的锁或当事务上下文嵌套可能通过回收进行时。恢复函数
应该在关键部分结束时被调用。所有这一切最好都伴随着解释什么是回收上下文,以
方便维护。
请注意,保存/恢复函数的正确配对允许嵌套,所以从现有的NOIO或NOFS范围分别调
用 ``memalloc_noio_save`` 或 ``memalloc_noio_restore`` 是安全的。
那么__vmalloc(GFP_NOFS)呢?
===========================
vmalloc不支持GFP_NOFS语义,因为在分配器的深处有硬编码的GFP_KERNEL分配,要修
复这些分配是相当不容易的。这意味着用GFP_NOFS/GFP_NOIO调用 ``vmalloc`` 几乎
总是一个错误。好消息是,NOFS/NOIO语义可以通过范围API实现。
在理想的世界中,上层应该已经标记了危险的上下文,因此不需要特别的照顾, ``vmalloc``
的调用应该没有任何问题。有时,如果上下文不是很清楚,或者有叠加的违规行为,那么
推荐的方法是用范围API包装vmalloc,并加上注释来解释问题。
@@ -0,0 +1,80 @@
.. SPDX-License-Identifier: GPL-2.0+
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/idr.rst
:翻译:
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
:校译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
吴想成 Wu Xiangcheng <bobwxc@email.cn>
时奎亮 Alex Shi <alexs@kernel.org>
======
ID分配
======
:作者: Matthew Wilcox
概述
====
要解决的一个常见问题是分配标识符(IDs);它通常是标识事物的数字。比如包括文件描述
符、进程ID、网络协议中的数据包标识符、SCSI标记和设备实例编号。IDR和IDA为这个问题
提供了一个合理的解决方案,以避免每个人都自创。IDR提供将ID映射到指针的能力,而IDA
仅提供ID分配,因此内存效率更高。
IDR接口已经被废弃,请使用 ``XArray`` 。
IDR的用法
=========
首先初始化一个IDR,对于静态分配的IDR使用DEFINE_IDR(),或者对于动态分配的IDR使用
idr_init()。
您可以调用idr_alloc()来分配一个未使用的ID。通过调用idr_find()查询与该ID相关的指针,
并通过调用idr_remove()释放该ID。
如果需要更改与一个ID相关联的指针,可以调用idr_replace()。这样做的一个常见原因是通
过将 ``NULL`` 指针传递给分配函数来保留ID;用保留的ID初始化对象,最后将初始化的对
象插入IDR。
一些用户需要分配大于 ``INT_MAX`` 的ID。到目前为止,所有这些用户都满足 ``UINT_MAX``
的限制,他们使用idr_alloc_u32()。如果您需要超出u32的ID,我们将与您合作以满足您的
需求。
如果需要按顺序分配ID,可以使用idr_alloc_cyclic()。处理较大数量的ID时,IDR的效率会
降低,所以使用这个函数会有一点代价。
要对IDR使用的所有指针进行操作,您可以使用基于回调的idr_for_each()或迭代器样式的
idr_for_each_entry()。您可能需要使用idr_for_each_entry_continue()来继续迭代。如果
迭代器不符合您的需求,您也可以使用idr_get_next()。
当使用完IDR后,您可以调用idr_destroy()来释放IDR占用的内存。这并不会释放IDR指向的
对象;如果您想这样做,请使用其中一个迭代器来执行此操作。
您可以使用idr_is_empty()来查看当前是否分配了任何ID。
如果在从IDR分配一个新ID时需要带锁,您可能需要传递一组限制性的GFP标志,但这可能导
致IDR无法分配内存。为了解决该问题,您可以在获取锁之前调用idr_preload(),然后在分
配之后调用idr_preload_end()。
IDR同步的相关内容请见include/linux/idr.h文件中的“DOC: idr sync”。
IDA的用法
=========
IDA的用法的相关内容请见lib/idr.c文件中的“DOC: IDA description”。
函数和数据结构
==============
该API在以下内核代码中:
include/linux/idr.h
lib/idr.c
@@ -0,0 +1,148 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/index.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_core-api_index.rst:
===========
核心API文档
===========
这是核心内核API手册的首页。 非常感谢为本手册转换(和编写!)的文档!
核心实用程序
============
本节包含通用的和“核心中的核心”文档。 第一部分是 docbook 时期遗留下
来的大量 kerneldoc 信息;有朝一日,若有人有动力的话,应当把它们拆分
出来。
.. toctree::
:maxdepth: 1
kernel-api
printk-basics
printk-formats
workqueue
watch_queue
symbol-namespaces
数据结构和低级实用程序
======================
在整个内核中使用的函数库。
.. toctree::
:maxdepth: 1
kobject
kref
assoc_array
xarray
rbtree
idr
circular-buffers
generic-radix-tree
packing
this_cpu_ops
union_find
=======
Todolist:
timekeeping
errseq
并发原语
========
Linux如何让一切同时发生。 详情请参阅
:doc:`/locking/index`
.. toctree::
:maxdepth: 1
irq/index
refcount-vs-atomic
local_ops
padata
Todolist:
../RCU/index
低级硬件管理
============
缓存管理,CPU热插拔管理等。
.. toctree::
:maxdepth: 1
cachetlb
cpu_hotplug
genericirq
memory-hotplug
protection-keys
Todolist:
memory-hotplug
cpu_hotplug
genericirq
内存管理
========
如何在内核中分配和使用内存。请注意,在
:doc:`/mm/index` 中有更多的内存管理文档。
.. toctree::
:maxdepth: 1
memory-allocation
unaligned-memory-access
mm-api
genalloc
boot-time-mm
gfp_mask-from-fs-io
Todolist:
dma-api
dma-api-howto
dma-attributes
dma-isa-lpc
pin_user_pages
内核调试的接口
==============
Todolist:
debug-objects
tracepoint
debugging-via-ohci1394
其它文档
========
不适合放在其它地方或尚未归类的文件;
Todolist:
librs
.. only:: subproject and html
Indices
=======
* :ref:`genindex`
@@ -0,0 +1,26 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/core-api/irq/concepts.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_concepts.rst:
===========
什么是IRQ?
===========
IRQ (Interrupt ReQuest) 指来自设备的中断请求。
目前,它们可以通过一个引脚或通过一个数据包进入。
多个设备可以连接到同一个引脚,从而共享一个IRQ。
IRQ编号是用来描述硬件中断源的内核标识符。通常它是一个到全局irq_desc数组的索引,
但是除了在linux/interrupt.h中实现的之外,其它细节是体系结构特征相关的。
IRQ编号是对机器上可能的中断源的枚举。通常枚举的是系统中所有中断控制器的输入引脚
编号。在ISA(工业标准体系结构)的情况下所枚举的是两个i8259中断控制器的16个输入引脚。
体系结构可以给IRQ号赋予额外的含义,在涉及到硬件手动配置的情况下,我们鼓励这样做。
ISA IRQ是赋予这种额外含义的一个典型例子。
@@ -0,0 +1,22 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/core-api/irq/index.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_irq_index.rst:
====
IRQs
====
.. toctree::
:maxdepth: 1
concepts
irq-affinity
irq-domain
irqflags-tracing
@@ -0,0 +1,78 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/core-api/irq/irq-affinity.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_irq-affinity.rst:
==============
SMP IRQ 亲和性
==============
变更记录:
- 作者:最初由Ingo Molnar <mingo@redhat.com>开始撰写
- 后期更新维护: Max Krasnyansky <maxk@qualcomm.com>
/proc/irq/IRQ#/smp_affinity和/proc/irq/IRQ#/smp_affinity_list指定了哪些CPU能
够关联到一个给定的IRQ源,这两个文件包含了这些指定cpu的cpu位掩码(smp_affinity)和cpu列
表(smp_affinity_list)。它不允许关闭所有CPU, 同时如果IRQ控制器不支持中断请求亲和
(IRQ affinity),那么所有cpu的默认值将保持不变(即关联到所有CPU).
/proc/irq/default_smp_affinity指明了适用于所有非激活IRQ的默认亲和性掩码。一旦IRQ被
分配/激活,它的亲和位掩码将被设置为默认掩码。然后可以如上所述改变它。默认掩码是0xffffffff。
下面是一个先将IRQ44(eth1)限制在CPU0-3上,然后限制在CPU4-7上的例子(这是一个8CPU的SMP box)
::
[root@moon 44]# cd /proc/irq/44
[root@moon 44]# cat smp_affinity
ffffffff
[root@moon 44]# echo 0f > smp_affinity
[root@moon 44]# cat smp_affinity
0000000f
[root@moon 44]# ping -f h
PING hell (195.4.7.3): 56 data bytes
...
--- hell ping statistics ---
6029 packets transmitted, 6027 packets received, 0% packet loss
round-trip min/avg/max = 0.1/0.1/0.4 ms
[root@moon 44]# cat /proc/interrupts | grep 'CPU\|44:'
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
44: 1068 1785 1785 1783 0 0 0 0 IO-APIC-level eth1
从上面一行可以看出,IRQ44只传递给前四个处理器(0-3)。
现在让我们把这个IRQ限制在CPU(4-7)。
::
[root@moon 44]# echo f0 > smp_affinity
[root@moon 44]# cat smp_affinity
000000f0
[root@moon 44]# ping -f h
PING hell (195.4.7.3): 56 data bytes
..
--- hell ping statistics ---
2779 packets transmitted, 2777 packets received, 0% packet loss
round-trip min/avg/max = 0.1/0.5/585.4 ms
[root@moon 44]# cat /proc/interrupts | 'CPU\|44:'
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
44: 1068 1785 1785 1783 1784 1069 1070 1069 IO-APIC-level eth1
这次IRQ44只传递给最后四个处理器。
即CPU0-3的计数器没有变化。
下面是一个将相同的irq(44)限制在cpus 1024到1031的例子
::
[root@moon 44]# echo 1024-1031 > smp_affinity_list
[root@moon 44]# cat smp_affinity_list
1024-1031
需要注意的是,如果要用位掩码来做这件事,就需要32个为0的位掩码来追踪其相关的一个。
@@ -0,0 +1,233 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/core-api/irq/irq-domain.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
.. _cn_irq-domain.rst:
=======================
irq_domain 中断号映射库
=======================
目前Linux内核的设计使用了一个巨大的数字空间,每个独立的IRQ源都被分配了一个不
同的数字。
当只有一个中断控制器时,这很简单,但在有多个中断控制器的系统中,内核必须确保每
个中断控制器都能得到非重复的Linux IRQ号(数字)分配。
注册为唯一的irqchips的中断控制器编号呈现出上升的趋势:例如GPIO控制器等不同
种类的子驱动程序通过将其中断处理程序建模为irqchips,即实际上是级联中断控制器,
避免了重新实现与IRQ核心系统相同的回调机制。
在这里,中断号与硬件中断号离散了所有种类的对应关系:而在过去,IRQ号可以选择,
使它们与硬件IRQ线进入根中断控制器(即实际向CPU发射中断线的组件)相匹配,现
在这个编号仅仅是一个数字。
出于这个原因,我们需要一种机制将控制器本地中断号(即硬件irq编号)与Linux IRQ
号分开。
irq_alloc_desc*() 和 irq_free_desc*() API 提供了对irq号的分配,但它们不
提供任何对控制器本地IRQ(hwirq)号到Linux IRQ号空间的反向映射的支持。
irq_domain 库在 irq_alloc_desc*() API 的基础上增加了 hwirq 和 IRQ 号码
之间的映射。 相比于中断控制器驱动开放编码自己的反向映射方案,我们更喜欢用
irq_domain来管理映射。
irq_domain还实现了从抽象的irq_fwspec结构体到hwirq号的转换(到目前为止是
Device Tree和ACPI GSI),并且可以很容易地扩展以支持其它IRQ拓扑数据源。
irq_domain的用法
================
中断控制器驱动程序通过以下方式创建并注册一个irq_domain。调用
irq_domain_add_*() 或 irq_domain_create_*()函数之一(每个映射方法都有不
同的分配器函数,后面会详细介绍)。 函数成功后会返回一个指向irq_domain的指针。
调用者必须向分配器函数提供一个irq_domain_ops结构体。
在大多数情况下,irq_domain在开始时是空的,没有任何hwirq和IRQ号之间的映射。
通过调用irq_create_mapping()将映射添加到irq_domain中,该函数接受
irq_domain和一个hwirq号作为参数。 如果hwirq的映射还不存在,那么它将分配
一个新的Linux irq_desc,将其与hwirq关联起来,并调用.map()回调,这样驱动
程序就可以执行任何必要的硬件设置。
一旦建立了映射,可以通过多种方法检索或使用它:
- irq_resolve_mapping()返回一个指向给定域和hwirq号的irq_desc结构指针,
如果没有映射则返回NULL。
- irq_find_mapping()返回给定域和hwirq的Linux IRQ号,如果没有映射则返回0。
- generic_handle_domain_irq()处理一个由域和hwirq号描述的中断。
请注意,irq域的查找必须发生在与RCU读临界区兼容的上下文中。
在调用irq_find_mapping()之前,至少要调用一次irq_create_mapping()函数,
以免描述符不能被分配。
如果驱动程序有Linux的IRQ号或irq_data指针,并且需要知道相关的hwirq号(比
如在irq_chip回调中),那么可以直接从irq_data->hwirq中获得。
irq_domain映射的类型
====================
从hwirq到Linux irq的反向映射有几种机制,每种机制使用不同的分配函数。应该
使用哪种反向映射类型取决于用例。 下面介绍每一种反向映射类型:
线性映射
--------
::
irq_domain_create_linear()
线性反向映射维护了一个固定大小的表,该表以hwirq号为索引。 当一个hwirq被映射
时,会给hwirq分配一个irq_desc,并将irq号存储在表中。
当最大的hwirq号固定且数量相对较少时,线性图是一个很好的选择(~<256)。 这种
映射的优点是固定时间查找IRQ号,而且irq_descs只分配给在用的IRQ。 缺点是该表
必须尽可能大的hwirq号。
irq_domain_add_linear()和irq_domain_create_linear()在功能上是等价的,
除了第一个参数不同--前者接受一个Open Firmware特定的 'struct device_node' 而
后者接受一个更通用的抽象 'struct fwnode_handle' 。
大多数驱动应该使用线性映射
树状映射
--------
::
irq_domain_create_tree()
irq_domain维护着从hwirq号到Linux IRQ的radix的树状映射。 当一个hwirq被映射时,
一个irq_desc被分配,hwirq被用作radix树的查找键。
如果hwirq号可以非常大,树状映射是一个很好的选择,因为它不需要分配一个和最大hwirq
号一样大的表。 缺点是,hwirq到IRQ号的查找取决于表中有多少条目。
很少有驱动应该需要这个映射。
无映射
------
::
irq_domain_create_nomap()
当硬件中的hwirq号是可编程的时候,就可以采用无映射类型。 在这种情况下,最好将
Linux IRQ号编入硬件本身,这样就不需要映射了。 调用irq_create_direct_mapping()
会分配一个Linux IRQ号,并调用.map()回调,这样驱动就可以将Linux IRQ号编入硬件中。
大多数驱动程序无法使用此映射,现在它由CONFIG_IRQ_DOMAIN_NOMAP选项控制。
请不要引入此API的新用户。
传统映射类型
------------
::
irq_domain_create_simple()
irq_domain_create_legacy()
传统映射是已经为 hwirqs 分配了一系列 irq_descs 的驱动程序的特殊情况。 当驱动程
序不能立即转换为使用线性映射时,就会使用它。 例如,许多嵌入式系统板卡支持文件使用
一组用于IRQ号的定义(#define),这些定义被传递给struct设备注册。 在这种情况下,
不能动态分配Linux IRQ号,应该使用传统映射。
顾名思义,\*_legacy()系列函数已被废弃,只是为了方便对古老平台的支持而存在。
不应该增加新的用户。当\*_simple()系列函数的使用导致遗留行为时,他们也是如此。
传统映射假设已经为控制器分配了一个连续的IRQ号范围,并且可以通过向hwirq号添加一
个固定的偏移来计算IRQ号,反之亦然。 缺点是需要中断控制器管理IRQ分配,并且需要为每
个hwirq分配一个irq_desc,即使它没有被使用。
只有在必须支持固定的IRQ映射时,才应使用传统映射。 例如,ISA控制器将使用传统映射来
映射Linux IRQ 0-15,这样现有的ISA驱动程序就能得到正确的IRQ号。
大多数使用传统映射的用户应该使用irq_domain_add_simple()或
irq_domain_create_simple(),只有在系统提供IRQ范围时才会使用传统域,否则将使用
线性域映射。这个调用的语义是这样的:如果指定了一个IRQ范围,那么 描述符将被即时分配
给它,如果没有范围被分配,它将不会执行 irq_domain_add_linear() 或
irq_domain_create_linear(),这意味着 *no* irq 描述符将被分配。
一个简单域的典型用例是,irqchip供应商同时支持动态和静态IRQ分配。
为了避免最终出现使用线性域而没有描述符被分配的情况,确保使用简单域的驱动程序在任何
irq_find_mapping()之前调用irq_create_mapping()是非常重要的,因为后者实际上
将用于静态IRQ分配情况。
irq_domain_add_simple()和irq_domain_create_simple()以及
irq_domain_add_legacy()和irq_domain_create_legacy()在功能上是等价的,只
是第一个参数不同--前者接受Open Firmware特定的 'struct device_node' ,而后者
接受一个更通用的抽象 'struct fwnode_handle' 。
IRQ域层级结构
-------------
在某些架构上,可能有多个中断控制器参与将一个中断从设备传送到目标CPU。
让我们来看看x86平台上典型的中断传递路径吧
::
Device --> IOAPIC -> Interrupt remapping Controller -> Local APIC -> CPU
涉及到的中断控制器有三个:
1) IOAPIC 控制器
2) 中断重映射控制器
3) Local APIC 控制器
为了支持这样的硬件拓扑结构,使软件架构与硬件架构相匹配,为每个中断控制器建立一
个irq_domain数据结构,并将这些irq_domain组织成层次结构。
在建立irq_domain层次结构时,靠近设备的irq_domain为子域,靠近CPU的
irq_domain为父域。所以在上面的例子中,将建立如下的层次结构。
::
CPU Vector irq_domain (root irq_domain to manage CPU vectors)
^
|
Interrupt Remapping irq_domain (manage irq_remapping entries)
^
|
IOAPIC irq_domain (manage IOAPIC delivery entries/pins)
使用irq_domain层次结构的主要接口有四个:
1) irq_domain_alloc_irqs(): 分配IRQ描述符和与中断控制器相关的资源来传递这些中断。
2) irq_domain_free_irqs(): 释放IRQ描述符和与这些中断相关的中断控制器资源。
3) irq_domain_activate_irq(): 激活中断控制器硬件以传递中断。
4) irq_domain_deactivate_irq(): 停用中断控制器硬件,停止传递中断。
为了支持irq_domain层次结构,需要做如下修改:
1) 一个新的字段 'parent' 被添加到irq_domain结构中;它用于维护irq_domain的层次信息。
2) 一个新的字段 'parent_data' 被添加到irq_data结构中;它用于建立层次结构irq_data以
匹配irq_domain层次结构。irq_data用于存储irq_domain指针和硬件irq号。
3) 新的回调被添加到irq_domain_ops结构中,以支持层次结构的irq_domain操作。
在支持分层irq_domain和分层irq_data准备就绪后,为每个中断控制器建立一个irq_domain结
构,并为每个与IRQ相关联的irq_domain分配一个irq_data结构。现在我们可以再进一步支持堆
栈式(层次结构)的irq_chip。也就是说,一个irq_chip与层次结构中的每个irq_data相关联。
一个子irq_chip可以自己或通过与它的父irq_chip合作来实现一个所需的操作。
通过堆栈式的irq_chip,中断控制器驱动只需要处理自己管理的硬件,在需要的时候可以向其父
irq_chip请求服务。所以我们可以实现更简洁的软件架构。
为了让中断控制器驱动程序支持irq_domain层次结构,它需要做到以下几点:
1) 实现 irq_domain_ops.alloc 和 irq_domain_ops.free
2) 可选择地实现 irq_domain_ops.activate 和 irq_domain_ops.deactivate.
3) 可选择地实现一个irq_chip来管理中断控制器硬件。
4) 不需要实现irq_domain_ops.map和irq_domain_ops.unmap,它们在层次结构
irq_domain中是不用的。
irq_domain层次结构绝不是x86特有的,大量用于支持其他架构,如ARM、ARM64等。
调试功能
========
打开CONFIG_GENERIC_IRQ_DEBUGFS,可让IRQ子系统的大部分内部结构都在debugfs中暴露出来。
@@ -0,0 +1,47 @@
.. include:: ../../disclaimer-zh_CN.rst
:Original: Documentation/core-api/irq/irqflags-tracing.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_irqflags-tracing.rst:
=================
IRQ-flags状态追踪
=================
:Author: 最初由Ingo Molnar <mingo@redhat.com>开始撰写
“irq-flags tracing”(中断标志追踪)功能可以 “追踪” hardirq和softirq的状态,它让
感兴趣的子系统有机会了解到到内核中发生的每一个
hardirqs-off/hardirqs-on、softirqs-off/softirqs-on事件。
CONFIG_TRACE_IRQFLAGS_SUPPORT是通用锁调试代码提供的CONFIG_PROVE_SPIN_LOCKING
和CONFIG_PROVE_RW_LOCKING所需要的。否则将只有CONFIG_PROVE_MUTEX_LOCKING和
CONFIG_PROVE_RWSEM_LOCKING在一个架构上被提供--这些都是不在IRQ上下文中使用的
锁API。(rwsems的一个异常是可以解决的)
架构对这一点的支持当然不属于“微不足道”的范畴,因为很多低级的汇编代码都要处理irq-flags
的状态变化。但是一个架构可以以一种相当直接且无风险的方式启用irq-flags-tracing。
架构如果想支持这个,需要先做一些代码组织上的改变:
- 在他们的arch级Kconfig文件中添加并启用TRACE_IRQFLAGS_SUPPORT。
然后还需要做一些功能上的改变来实现对irq-flags-tracing的支持:
- 在低级入口代码中增加(构建条件)对trace_hardirqs_off()/trace_hardirqs_on()
函数的调用。锁验证器会密切关注 “real”的irq-flags是否与 “virtual”的irq-flags
状态相匹配,如果两者不匹配,则会发出警告(并关闭自己)。通常维护arch中
irq-flags-track的大部分时间都是在这种状态下度过的:看看lockdep的警告,试着
找出我们还没有搞定的汇编代码。修复并重复。一旦系统启动,并且在irq-flags跟踪功
能中没有出现lockdep警告的情况下,arch支持就完成了。
- 如果该架构有不可屏蔽的中断,那么需要通过lockdep_off()/lockdep_on()将这些中
断从irq跟踪[和锁验证]机制中排除。
一般来说,在一个架构中,不完整的irq-flags-tracing实现是没有风险的:lockdep
会检测到这一点,并将自己关闭。即锁验证器仍然可靠。应该不会因为irq-tracing的错
误而崩溃。(除非通过修改不该修改的条件来更改汇编或寄存器而破坏其他代码)
@@ -0,0 +1,378 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/kernel-api.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
.. _cn_kernel-api.rst:
============
Linux内核API
============
列表管理函数
============
该API在以下内核代码中:
include/linux/list.h
基本的C库函数
=============
在编写驱动程序时,一般不能使用C库中的例程。部分函数通常很有用,它们在
下面被列出。这些函数的行为可能会与ANSI定义的略有不同,这些偏差会在文中
注明。
字符串转换
----------
该API在以下内核代码中:
lib/vsprintf.c
include/linux/kernel.h
include/linux/kernel.h
lib/kstrtox.c
lib/string_helpers.c
字符串处理
----------
该API在以下内核代码中:
include/linux/fortify-string.h
lib/string.c
include/linux/string.h
mm/util.c
基本的内核库函数
================
Linux内核提供了很多实用的基本函数。
位运算
------
该API在以下内核代码中:
include/asm-generic/bitops/instrumented-atomic.h
include/asm-generic/bitops/instrumented-non-atomic.h
include/asm-generic/bitops/instrumented-lock.h
位图运算
--------
该API在以下内核代码中:
lib/bitmap.c
include/linux/bitmap.h
include/linux/bitmap.h
include/linux/bitmap.h
lib/bitmap.c
lib/bitmap.c
include/linux/bitmap.h
命令行解析
----------
该API在以下内核代码中:
lib/cmdline.c
排序
----
该API在以下内核代码中:
lib/sort.c
lib/list_sort.c
文本检索
--------
该API在以下内核代码中:
lib/textsearch.c
lib/textsearch.c
include/linux/textsearch.h
Linux中的CRC和数学函数
======================
算术溢出检查
------------
该API在以下内核代码中:
include/linux/overflow.h
CRC函数
-------
*译注:CRC,Cyclic Redundancy Check,循环冗余校验*
该API在以下内核代码中:
lib/crc4.c
lib/crc7.c
lib/crc8.c
lib/crc16.c
lib/crc32.c
lib/crc-ccitt.c
lib/crc-itu-t.c
基数为2的对数和幂函数
---------------------
该API在以下内核代码中:
include/linux/log2.h
整数幂函数
----------
该API在以下内核代码中:
lib/math/int_pow.c
lib/math/int_sqrt.c
除法函数
--------
该API在以下内核代码中:
include/asm-generic/div64.h
include/linux/math64.h
lib/math/gcd.c
UUID/GUID
---------
该API在以下内核代码中:
lib/uuid.c
内核IPC设备
===========
IPC实用程序
-----------
该API在以下内核代码中:
ipc/util.c
FIFO 缓冲区
===========
kfifo接口
---------
该API在以下内核代码中:
include/linux/kfifo.h
转发接口支持
============
转发接口支持旨在为工具和设备提供一种有效的机制,将大量数据从内核空间
转发到用户空间。
转发接口
--------
该API在以下内核代码中:
kernel/relay.c
kernel/relay.c
模块支持
========
模块加载
--------
该API在以下内核代码中:
kernel/module/kmod.c
模块接口支持
------------
更多信息请参阅kernel/module/目录下的文件。
硬件接口
========
该API在以下内核代码中:
kernel/dma.c
资源管理
--------
该API在以下内核代码中:
kernel/resource.c
kernel/resource.c
MTRR处理
--------
该API在以下内核代码中:
arch/x86/kernel/cpu/mtrr/mtrr.c
安全框架
========
该API在以下内核代码中:
security/security.c
security/inode.c
审计接口
========
该API在以下内核代码中:
kernel/audit.c
kernel/auditsc.c
kernel/auditfilter.c
核算框架
========
该API在以下内核代码中:
kernel/acct.c
块设备
======
该API在以下内核代码中:
include/linux/bio.h
block/blk-core.c
block/blk-core.c
block/blk-map.c
block/blk-sysfs.c
block/blk-settings.c
block/blk-flush.c
block/blk-lib.c
block/blk-integrity.c
kernel/trace/blktrace.c
block/genhd.c
block/genhd.c
字符设备
========
该API在以下内核代码中:
fs/char_dev.c
时钟框架
========
时钟框架定义了编程接口,以支持系统时钟树的软件管理。该框架广泛用于系统级芯片(SOC)平
台,以支持电源管理和各种可能需要自定义时钟速率的设备。请注意,这些 “时钟”与计时或实
时时钟(RTC)无关,它们都有单独的框架。这些:c:type: `struct clk <clk>` 实例可用于管理
各种时钟信号,例如一个96理例如96MHz的时钟信号,该信号可被用于总线或外设的数据交换,或以
其他方式触发系统硬件中的同步状态机转换。
通过明确的软件时钟门控来支持电源管理:未使用的时钟被禁用,因此系统不会因为改变不在使用
中的晶体管的状态而浪费电源。在某些系统中,这可能是由硬件时钟门控支持的,其中时钟被门控
而不在软件中被禁用。芯片的部分,在供电但没有时钟的情况下,可能会保留其最后的状态。这种
低功耗状态通常被称为*保留模式*。这种模式仍然会产生漏电流,特别是在电路几何结构较细的情
况下,但对于CMOS电路来说,电能主要是随着时钟翻转而被消耗的。
电源感知驱动程序只有在其管理的设备处于活动使用状态时才会启用时钟。此外,系统睡眠状态通
常根据哪些时钟域处于活动状态而有所不同:“待机”状态可能允许从多个活动域中唤醒,而
"mem"(暂停到RAM)状态可能需要更全面地关闭来自高速PLL和振荡器的时钟,从而限制了可能
的唤醒事件源的数量。驱动器的暂停方法可能需要注意目标睡眠状态的系统特定时钟约束。
一些平台支持可编程时钟发生器。这些可以被各种外部芯片使用,如其他CPU、多媒体编解码器以
及对接口时钟有严格要求的设备。
该API在以下内核代码中:
include/linux/clk.h
同步原语
========
读-复制-更新(RCU)
-------------------
该API在以下内核代码中:
include/linux/rcupdate.h
kernel/rcu/tree.c
kernel/rcu/tree_exp.h
kernel/rcu/update.c
include/linux/srcu.h
kernel/rcu/srcutree.c
include/linux/rculist_bl.h
include/linux/rculist.h
include/linux/rculist_nulls.h
include/linux/rcu_sync.h
kernel/rcu/sync.c
@@ -0,0 +1,379 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/kobject.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_core_api_kobject.rst:
=======================================================
关于kobjects、ksets和ktypes的一切你没想过需要了解的东西
=======================================================
:作者: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
:最后一次更新: December 19, 2007
根据Jon Corbet于2003年10月1日为lwn.net撰写的原创文章改编,网址是:
https://lwn.net/Articles/51437/
理解驱动模型和建立在其上的kobject抽象的部分的困难在于,没有明显的切入点。
处理kobjects需要理解一些不同的类型,所有这些类型都会相互引用。为了使事情
变得更简单,我们将多路并进,从模糊的术语开始,并逐渐增加细节。那么,先来
了解一些我们将要使用的术语的简明定义吧。
- 一个kobject是一个kobject结构体类型的对象。Kobjects有一个名字和一个
引用计数。一个kobject也有一个父指针(允许对象被排列成层次结构),一个
特定的类型,并且,通常在sysfs虚拟文件系统中表示。
Kobjects本身通常并不引人关注;相反它们常常被嵌入到其他包含真正引人注目
的代码的结构体中。
任何结构体都 **不应该** 有一个以上的kobject嵌入其中。如果有的话,对象的引用计
数肯定会被打乱,而且不正确,你的代码就会出现错误。所以不要这样做。
- ktype是嵌入一个kobject的对象的类型。每个嵌入kobject的结构体都需要一个
相应的ktype。ktype控制着kobject在被创建和销毁时的行为。
- 一个kset是一组kobjects。这些kobjects可以是相同的ktype或者属于不同的
ktype。kset是kobjects集合的基本容器类型。Ksets包含它们自己的kobjects,
但你可以安全地忽略这个实现细节,因为kset的核心代码会自动处理这个kobject。
当你看到一个下面全是其他目录的sysfs目录时,通常这些目录中的每一个都对应
于同一个kset中的一个kobject。
我们将研究如何创建和操作所有这些类型。将采取一种自下而上的方法,所以我们
将回到kobjects。
嵌入kobjects
=============
内核代码很少创建孤立的kobject,只有一个主要的例外,下面会解释。相反,
kobjects被用来控制对一个更大的、特定领域的对象的访问。为此,kobjects会被
嵌入到其他结构中。如果你习惯于用面向对象的术语来思考问题,那么kobjects可
以被看作是一个顶级的抽象类,其他的类都是从它派生出来的。一个kobject实现了
一系列的功能,这些功能本身并不特别有用,但在其他对象中却很好用。C语言不允
许直接表达继承,所以必须使用其他技术——比如结构体嵌入。
(对于那些熟悉内核链表实现的人来说,这类似于“list_head”结构本身很少有用,
但总是被嵌入到感兴趣的更大的对象中)。
例如, ``drivers/uio/uio.c`` 中的IO代码有一个结构体,定义了与uio设备相
关的内存区域::
struct uio_map {
struct kobject kobj;
struct uio_mem *mem;
};
如果你有一个uio_map结构体,找到其嵌入的kobject只是一个使用kobj成员的问题。
然而,与kobjects一起工作的代码往往会遇到相反的问题:给定一个结构体kobject
的指针,指向包含结构体的指针是什么?你必须避免使用一些技巧(比如假设
kobject在结构的开头),相反,你得使用container_of()宏,其可以在 ``<linux/kernel.h>``
中找到::
container_of(ptr, type, member)
其中:
* ``ptr`` 是一个指向嵌入kobject的指针,
* ``type`` 是包含结构体的类型,
* ``member`` 是 ``指针`` 所指向的结构体域的名称。
container_of()的返回值是一个指向相应容器类型的指针。因此,例如,一个嵌入到
uio_map结构 **中** 的kobject结构体的指针kp可以被转换为一个指向 **包含** uio_map
结构体的指针,方法是::
struct uio_map *u_map = container_of(kp, struct uio_map, kobj);
为了方便起见,程序员经常定义一个简单的宏,用于将kobject指针 **反推** 到包含
类型。在早期的 ``drivers/uio/uio.c`` 中正是如此,你可以在这里看到::
struct uio_map {
struct kobject kobj;
struct uio_mem *mem;
};
#define to_map(map) container_of(map, struct uio_map, kobj)
其中宏的参数“map”是一个指向有关的kobject结构体的指针。该宏随后被调用::
struct uio_map *map = to_map(kobj);
kobjects的初始化
================
当然,创建kobject的代码必须初始化该对象。一些内部字段是通过(强制)调用kobject_init()
来设置的::
void kobject_init(struct kobject *kobj, struct kobj_type *ktype);
ktype是正确创建kobject的必要条件,因为每个kobject都必须有一个相关的kobj_type。
在调用kobject_init()后,为了向sysfs注册kobject,必须调用函数kobject_add()::
int kobject_add(struct kobject *kobj, struct kobject *parent,
const char *fmt, ...);
这将正确设置kobject的父级和kobject的名称。如果该kobject要与一个特定的kset相关
联,在调用kobject_add()之前必须分配kobj->kset。如果kset与kobject相关联,则
kobject的父级可以在调用kobject_add()时被设置为NULL,则kobject的父级将是kset
本身。
由于kobject的名字是在它被添加到内核时设置的,所以kobject的名字不应该被直接操作。
如果你必须改变kobject的名字,请调用kobject_rename()::
int kobject_rename(struct kobject *kobj, const char *new_name);
kobject_rename()函数不会执行任何锁定操作,也不会对name进行可靠性检查,所以调用
者自己检查和串行化操作是明智的选择
有一个叫kobject_set_name()的函数,但那是历史遗产,正在被删除。如果你的代码需
要调用这个函数,那么它是不正确的,需要被修复。
要正确访问kobject的名称,请使用函数kobject_name()::
const char *kobject_name(const struct kobject * kobj);
有一个辅助函数可以同时初始化和添加kobject到内核中,令人惊讶的是,该函数被称为
kobject_init_and_add()::
int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,
struct kobject *parent, const char *fmt, ...);
参数与上面描述的单个kobject_init()和kobject_add()函数相同。
Uevents
=======
当一个kobject被注册到kobject核心后,你需要向全世界宣布它已经被创建了。这可以通
过调用kobject_uevent()来实现::
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
当kobject第一次被添加到内核时,使用 *KOBJ_ADD* 动作。这应该在该kobject的任
何属性或子对象被正确初始化后进行,因为当这个调用发生时,用户空间会立即开始寻
找它们。
当kobject从内核中移除时(关于如何做的细节在下面), **KOBJ_REMOVE** 的uevent
将由kobject核心自动创建,所以调用者不必担心手动操作。
引用计数
========
kobject的关键功能之一是作为它所嵌入的对象的一个引用计数器。只要对该对象的引用
存在,该对象(以及支持它的代码)就必须继续存在。用于操作kobject的引用计数的低
级函数是::
struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);
对kobject_get()的成功调用将增加kobject的引用计数器值并返回kobject的指针。
当引用被释放时,对kobject_put()的调用将递减引用计数值,并可能释放该对象。请注
意,kobject_init()将引用计数设置为1,所以设置kobject的代码最终需要kobject_put()
来释放该引用。
因为kobjects是动态的,所以它们不能以静态方式或在堆栈中声明,而总是以动态方式分
配。未来版本的内核将包含对静态创建的kobjects的运行时检查,并将警告开发者这种不
当的使用。
如果你使用struct kobject只是为了给你的结构体提供一个引用计数器,请使用struct kref
来代替;kobject是多余的。关于如何使用kref结构体的更多信息,请参见Linux内核源代
码树中的文件Documentation/core-api/kref.rst
创建“简单的”kobjects
====================
有时,开发者想要的只是在sysfs层次结构中创建一个简单的目录,而不必去搞那些复杂
的ksets、显示和存储函数,以及其他细节。这是一个应该创建单个kobject的例外。要
创建这样一个条目(即简单的目录),请使用函数::
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent);
这个函数将创建一个kobject,并将其放在sysfs中指定的父kobject下面的位置。要创
建与此kobject相关的简单属性,请使用::
int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
或者::
int sysfs_create_group(struct kobject *kobj, const struct attribute_group *grp);
这里使用的两种类型的属性,与已经用kobject_create_and_add()创建的kobject,
都可以是kobj_attribute类型,所以不需要创建特殊的自定义属性。
参见示例模块, ``samples/kobject/kobject-example.c`` ,以了解一个简单的
kobject和属性的实现。
ktypes和释放方法
================
以上讨论中还缺少一件重要的事情,那就是当一个kobject的引用次数达到零的时候
会发生什么。创建kobject的代码通常不知道何时会发生这种情况;首先,如果它知
道,那么使用kobject就没有什么意义。当sysfs被引入时,即使是可预测的对象生命
周期也会变得更加复杂,因为内核的其他部分可以获得在系统中注册的任何kobject
的引用。
最终的结果是,一个由kobject保护的结构体在其引用计数归零之前不能被释放。引
用计数不受创建kobject的代码的直接控制。因此,每当它的一个kobjects的最后一
个引用消失时,必须异步通知该代码。
一旦你通过kobject_add()注册了你的kobject,你绝对不能使用kfree()来直接释
放它。唯一安全的方法是使用kobject_put()。在kobject_init()之后总是使用
kobject_put()以避免错误的发生是一个很好的做法。
这个通知是通过kobject的release()方法完成的。通常这样的方法有如下形式::
void my_object_release(struct kobject *kobj)
{
struct my_object *mine = container_of(kobj, struct my_object, kobj);
/* Perform any additional cleanup on this object, then... */
kfree(mine);
}
有一点很重要:每个kobject都必须有一个release()方法,而且这个kobject必
须持续存在(处于一致的状态),直到这个方法被调用。如果这些约束条件没有
得到满足,那么代码就是有缺陷的。注意,如果你忘记提供release()方法,内
核会警告你。不要试图通过提供一个“空”的释放函数来摆脱这个警告。
如果你的清理函数只需要调用kfree(),那么你必须创建一个包装函数,该函数
使用container_of()来向上造型到正确的类型(如上面的例子所示),然后在整个
结构体上调用kfree()。
注意,kobject的名字在release函数中是可用的,但它不能在这个回调中被改
变。否则,在kobject核心中会出现内存泄漏,这让人很不爽。
有趣的是,release()方法并不存储在kobject本身;相反,它与ktype相关。
因此,让我们引入结构体kobj_type::
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops;
const struct attribute_group **default_groups;
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
void (*get_ownership)(struct kobject *kobj, kuid_t *uid, kgid_t *gid);
};
这个结构提用来描述一个特定类型的kobject(或者更正确地说,包含对象的
类型)。每个kobject都需要有一个相关的kobj_type结构;当你调用
kobject_init()或kobject_init_and_add()时必须指定一个指向该结构的
指针。
当然,kobj_type结构中的release字段是指向这种类型的kobject的release()
方法的一个指针。另外两个字段(sysfs_ops 和 default_groups)控制这种
类型的对象如何在 sysfs 中被表示;它们超出了本文的范围。
default_groups 指针是一个默认属性的列表,它将为任何用这个 ktype 注册
的 kobject 自动创建。
ksets
=====
一个kset仅仅是一个希望相互关联的kobjects的集合。没有限制它们必须是相
同的ktype,但是如果它们不是相同的,就要非常小心。
一个kset有以下功能:
- 它像是一个包含一组对象的袋子。一个kset可以被内核用来追踪“所有块
设备”或“所有PCI设备驱动”。
- kset也是sysfs中的一个子目录,与kset相关的kobjects可以在这里显示
出来。每个kset都包含一个kobject,它可以被设置为其他kobject的父对象;
sysfs层次结构的顶级目录就是以这种方式构建的。
- Ksets可以支持kobjects的 "热插拔",并影响uevent事件如何被报告给
用户空间。
在面向对象的术语中,“kset”是顶级的容器类;ksets包含它们自己的kobject,
但是这个kobject是由kset代码管理的,不应该被任何其他用户所操纵。
kset在一个标准的内核链表中保存它的子对象。Kobjects通过其kset字段指向其
包含的kset。在几乎所有的情况下,属于一个kset的kobjects在它们的父
对象中都有那个kset(或者,严格地说,它的嵌入kobject)。
由于kset中包含一个kobject,它应该总是被动态地创建,而不是静态地
或在堆栈中声明。要创建一个新的kset,请使用::
struct kset *kset_create_and_add(const char *name,
const struct kset_uevent_ops *uevent_ops,
struct kobject *parent_kobj);
当你完成对kset的处理后,调用::
void kset_unregister(struct kset *k);
来销毁它。这将从sysfs中删除该kset并递减其引用计数值。当引用计数
为零时,该kset将被释放。因为对该kset的其他引用可能仍然存在,
释放可能发生在kset_unregister()返回之后。
一个使用kset的例子可以在内核树中的 ``samples/kobject/kset-example.c``
文件中看到。
如果一个kset希望控制与它相关的kobjects的uevent操作,它可以使用
结构体kset_uevent_ops来处理它::
struct kset_uevent_ops {
int (* const filter)(struct kobject *kobj);
const char *(* const name)(struct kobject *kobj);
int (* const uevent)(struct kobject *kobj, struct kobj_uevent_env *env);
};
过滤器函数允许kset阻止一个特定kobject的uevent被发送到用户空间。
如果该函数返回0,该uevent将不会被发射出去。
name函数将被调用以覆盖uevent发送到用户空间的kset的默认名称。默
认情况下,该名称将与kset本身相同,但这个函数,如果存在,可以覆盖
该名称。
当uevent即将被发送至用户空间时,uevent函数将被调用,以允许更多
的环境变量被添加到uevent中。
有人可能会问,鉴于没有提出执行该功能的函数,究竟如何将一个kobject
添加到一个kset中。答案是这个任务是由kobject_add()处理的。当一个
kobject被传递给kobject_add()时,它的kset成员应该指向这个kobject
所属的kset。 kobject_add()将处理剩下的部分。
如果属于一个kset的kobject没有父kobject集,它将被添加到kset的目
录中。并非所有的kset成员都必须住在kset目录中。如果在添加kobject
之前分配了一个明确的父kobject,那么该kobject将被注册到kset中,
但是被添加到父kobject下面。
移除Kobject
===========
当一个kobject在kobject核心注册成功后,在代码使用完它时,必须将其
清理掉。要做到这一点,请调用kobject_put()。通过这样做,kobject核
心会自动清理这个kobject分配的所有内存。如果为这个对象发送了 ``KOBJ_ADD``
uevent,那么相应的 ``KOBJ_REMOVE`` uevent也将被发送,任何其他的
sysfs内务将被正确处理。
如果你需要分两次对kobject进行删除(比如说在你要销毁对象时无权睡眠),
那么调用kobject_del()将从sysfs中取消kobject的注册。这使得kobject
“不可见”,但它并没有被清理掉,而且该对象的引用计数仍然是一样的。在稍
后的时间调用kobject_put()来完成与该kobject相关的内存的清理。
kobject_del()可以用来放弃对父对象的引用,如果循环引用被构建的话。
在某些情况下,一个父对象引用一个子对象是有效的。循环引用必须通过明
确调用kobject_del()来打断,这样一个释放函数就会被调用,前一个循环
中的对象会相互释放。
示例代码出处
============
关于正确使用ksets和kobjects的更完整的例子,请参见示例程序
``samples/kobject/{kobject-example.c,kset-example.c}`` ,如果
您选择 ``CONFIG_SAMPLE_KOBJECT`` ,它们将被构建为可加载模块。
@@ -0,0 +1,311 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/kref.rst
翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
校译:
<此处请校译员签名(自愿),我将在下一个版本添加>
.. _cn_core_api_kref.rst:
=================================
为内核对象添加引用计数器(krefs)
=================================
:作者: Corey Minyard <minyard@acm.org>
:作者: Thomas Hellstrom <thellstrom@vmware.com>
其中很多内容都是从Greg Kroah-Hartman2004年关于krefs的OLS论文和演讲中摘
录的,可以在以下网址找到:
- http://www.kroah.com/linux/talks/ols_2004_kref_paper/Reprint-Kroah-Hartman-OLS2004.pdf
- http://www.kroah.com/linux/talks/ols_2004_kref_talk/
简介
====
krefs允许你为你的对象添加引用计数器。如果你有在多个地方使用和传递的对象,
而你没有refcounts,你的代码几乎肯定是坏的。如果你想要引用计数,krefs是个
好办法。
要使用kref,请在你的数据结构中添加一个,如::
struct my_data
{
.
.
struct kref refcount;
.
.
};
kref可以出现在数据结构体中的任何地方。
初始化
======
你必须在分配kref之后初始化它。 要做到这一点,可以这样调用kref_init::
struct my_data *data;
data = kmalloc_obj(*data);
if (!data)
return -ENOMEM;
kref_init(&data->refcount);
这将kref中的refcount设置为1。
Kref规则
========
一旦你有一个初始化的kref,你必须遵循以下规则:
1) 如果你对一个指针做了一个非临时性的拷贝,特别是如果它可以被传递给另一个执
行线程,你必须在传递之前用kref_get()增加refcount::
kref_get(&data->refcount);
如果你已经有了一个指向kref-ed结构体的有效指针(refcount不能为零),你
可以在没有锁的情况下这样做。
2) 当你完成对一个指针的处理时,你必须调用kref_put()::
kref_put(&data->refcount, data_release);
如果这是对该指针的最后一次引用,释放程序将被调用。如果代码从来没有尝试过
在没有已经持有有效指针的情况下获得一个kref-ed结构体的有效指针,那么在没
有锁的情况下这样做是安全的。
3) 如果代码试图获得对一个kref-ed结构体的引用,而不持有一个有效的指针,它必
须按顺序访问,在kref_put()期间不能发生kref_get(),并且该结构体在kref_get()
期间必须保持有效。
例如,如果你分配了一些数据,然后将其传递给另一个线程来处理::
void data_release(struct kref *ref)
{
struct my_data *data = container_of(ref, struct my_data, refcount);
kfree(data);
}
void more_data_handling(void *cb_data)
{
struct my_data *data = cb_data;
.
. do stuff with data here
.
kref_put(&data->refcount, data_release);
}
int my_data_handler(void)
{
int rv = 0;
struct my_data *data;
struct task_struct *task;
data = kmalloc_obj(*data);
if (!data)
return -ENOMEM;
kref_init(&data->refcount);
kref_get(&data->refcount);
task = kthread_run(more_data_handling, data, "more_data_handling");
if (task == ERR_PTR(-ENOMEM)) {
rv = -ENOMEM;
kref_put(&data->refcount, data_release);
goto out;
}
.
. do stuff with data here
.
out:
kref_put(&data->refcount, data_release);
return rv;
}
这样,两个线程处理数据的顺序并不重要,kref_put()处理知道数据不再被引用并释
放它。kref_get()不需要锁,因为我们已经有了一个有效的指针,我们拥有一个
refcount。put不需要锁,因为没有任何东西试图在没有持有指针的情况下获取数据。
在上面的例子中,kref_put()在成功和错误路径中都会被调用2次。这是必要的,因
为引用计数被kref_init()和kref_get()递增了2次。
请注意,规则1中的 "before "是非常重要的。你不应该做类似于::
task = kthread_run(more_data_handling, data, "more_data_handling");
if (task == ERR_PTR(-ENOMEM)) {
rv = -ENOMEM;
goto out;
} else
/* BAD BAD BAD - 在交接后得到 */
kref_get(&data->refcount);
不要以为你知道自己在做什么而使用上述构造。首先,你可能不知道自己在做什么。
其次,你可能知道自己在做什么(有些情况下涉及到锁,上述做法可能是合法的),
但其他不知道自己在做什么的人可能会改变代码或复制代码。这是很危险的作风。请
不要这样做。
在有些情况下,你可以优化get和put。例如,如果你已经完成了一个对象,并且给其
他对象排队,或者把它传递给其他对象,那么就没有理由先做一个get,然后再做一个
put::
/* 糟糕的额外获取(get)和输出(put) */
kref_get(&obj->ref);
enqueue(obj);
kref_put(&obj->ref, obj_cleanup);
只要做enqueue就可以了。 我们随时欢迎对这个问题的评论::
enqueue(obj);
/* 我们已经完成了对obj的处理,所以我们把我们的refcount传给了队列。
在这之后不要再碰obj了! */
最后一条规则(规则3)是最难处理的一条。例如,你有一个每个项目都被krefed的列表,
而你希望得到第一个项目。你不能只是从列表中抽出第一个项目,然后kref_get()它。
这违反了规则3,因为你还没有持有一个有效的指针。你必须添加一个mutex(或其他锁)。
比如说::
static DEFINE_MUTEX(mutex);
static LIST_HEAD(q);
struct my_data
{
struct kref refcount;
struct list_head link;
};
static struct my_data *get_entry()
{
struct my_data *entry = NULL;
mutex_lock(&mutex);
if (!list_empty(&q)) {
entry = container_of(q.next, struct my_data, link);
kref_get(&entry->refcount);
}
mutex_unlock(&mutex);
return entry;
}
static void release_entry(struct kref *ref)
{
struct my_data *entry = container_of(ref, struct my_data, refcount);
list_del(&entry->link);
kfree(entry);
}
static void put_entry(struct my_data *entry)
{
mutex_lock(&mutex);
kref_put(&entry->refcount, release_entry);
mutex_unlock(&mutex);
}
如果你不想在整个释放操作过程中持有锁,kref_put()的返回值是有用的。假设你不想在
上面的例子中在持有锁的情况下调用kfree()(因为这样做有点无意义)。你可以使用kref_put(),
如下所示::
static void release_entry(struct kref *ref)
{
/* 所有的工作都是在从kref_put()返回后完成的。*/
}
static void put_entry(struct my_data *entry)
{
mutex_lock(&mutex);
if (kref_put(&entry->refcount, release_entry)) {
list_del(&entry->link);
mutex_unlock(&mutex);
kfree(entry);
} else
mutex_unlock(&mutex);
}
如果你必须调用其他程序作为释放操作的一部分,而这些程序可能需要很长的时间,或者可
能要求相同的锁,那么这真的更有用。请注意,在释放例程中做所有的事情还是比较好的,
因为它比较整洁。
上面的例子也可以用kref_get_unless_zero()来优化,方法如下::
static struct my_data *get_entry()
{
struct my_data *entry = NULL;
mutex_lock(&mutex);
if (!list_empty(&q)) {
entry = container_of(q.next, struct my_data, link);
if (!kref_get_unless_zero(&entry->refcount))
entry = NULL;
}
mutex_unlock(&mutex);
return entry;
}
static void release_entry(struct kref *ref)
{
struct my_data *entry = container_of(ref, struct my_data, refcount);
mutex_lock(&mutex);
list_del(&entry->link);
mutex_unlock(&mutex);
kfree(entry);
}
static void put_entry(struct my_data *entry)
{
kref_put(&entry->refcount, release_entry);
}
这对于在put_entry()中移除kref_put()周围的mutex锁是很有用的,但是重要的是
kref_get_unless_zero被封装在查找表中的同一关键部分,否则kref_get_unless_zero
可能引用已经释放的内存。注意,在不检查其返回值的情况下使用kref_get_unless_zero
是非法的。如果你确信(已经有了一个有效的指针)kref_get_unless_zero()会返回true,
那么就用kref_get()代替。
Krefs和RCU
==========
函数kref_get_unless_zero也使得在上述例子中使用rcu锁进行查找成为可能::
struct my_data
{
struct rcu_head rhead;
.
struct kref refcount;
.
.
};
static struct my_data *get_entry_rcu()
{
struct my_data *entry = NULL;
rcu_read_lock();
if (!list_empty(&q)) {
entry = container_of(q.next, struct my_data, link);
if (!kref_get_unless_zero(&entry->refcount))
entry = NULL;
}
rcu_read_unlock();
return entry;
}
static void release_entry_rcu(struct kref *ref)
{
struct my_data *entry = container_of(ref, struct my_data, refcount);
mutex_lock(&mutex);
list_del_rcu(&entry->link);
mutex_unlock(&mutex);
kfree_rcu(entry, rhead);
}
static void put_entry(struct my_data *entry)
{
kref_put(&entry->refcount, release_entry_rcu);
}
但要注意的是,在调用release_entry_rcu后,结构kref成员需要在有效内存中保留一个rcu
宽限期。这可以通过使用上面的kfree_rcu(entry, rhead)来实现,或者在使用kfree之前
调用synchronize_rcu(),但注意synchronize_rcu()可能会睡眠相当长的时间。
@@ -0,0 +1,196 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/local_ops.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_local_ops:
========================
本地原子操作的语义和行为
========================
:作者: Mathieu Desnoyers
本文解释了本地原子操作的目的,如何为任何给定的架构实现这些操作,并说明了
如何正确使用这些操作。它还强调了在内存写入顺序很重要的情况下,跨CPU读取
这些本地变量时必须采取的预防措施。
.. note::
注意,基于 ``local_t`` 的操作不建议用于一般内核操作。请使用 ``this_cpu``
操作来代替使用,除非真的有特殊目的。大多数内核中使用的 ``local_t`` 已
经被 ``this_cpu`` 操作所取代。 ``this_cpu`` 操作在一条指令中结合了重
定位和类似 ``local_t`` 的语义,产生了更紧凑和更快的执行代码。
本地原子操作的目的
==================
本地原子操作的目的是提供快速和高度可重入的每CPU计数器。它们通过移除LOCK前
缀和通常需要在CPU间同步的内存屏障,将标准原子操作的性能成本降到最低。
在许多情况下,拥有快速的每CPU原子计数器是很有吸引力的:它不需要禁用中断来保护中
断处理程序,它允许在NMI(Non Maskable Interrupt)处理程序中使用连贯的计数器。
它对追踪目的和各种性能监测计数器特别有用。
本地原子操作只保证在拥有数据的CPU上的变量修改的原子性。因此,必须注意确保只
有一个CPU写到 ``local_t`` 的数据。这是通过使用每CPU的数据来实现的,并确
保我们在一个抢占式安全上下文中修改它。然而,从任何一个CPU读取 ``local_t``
数据都是允许的:这样它就会显得与所有者CPU的其他内存写入顺序不一致。
针对特定架构的实现
==================
这可以通过稍微修改标准的原子操作来实现:只有它们的UP变体必须被保留。这通常
意味着删除LOCK前缀(在i386和x86_64上)和任何SMP同步屏障。如果架构在SMP和
UP之间没有不同的行为,在你的架构的 ``local.h`` 中包括 ``asm-generic/local.h``
就足够了。
通过在一个结构体中嵌入一个 ``atomic_long_t`` , ``local_t`` 类型被定义为
一个不透明的 ``signed long`` 。这样做的目的是为了使从这个类型到
``long`` 的转换失败。该定义看起来像::
typedef struct { atomic_long_t a; } local_t;
使用本地原子操作时应遵循的规则
==============================
* 被本地操作触及的变量必须是每cpu的变量。
* *只有* 这些变量的CPU所有者才可以写入这些变量。
* 这个CPU可以从任何上下文(进程、中断、软中断、nmi...)中使用本地操作来更新
它的local_t变量。
* 当在进程上下文中使用本地操作时,必须禁用抢占(或中断),以确保进程在获得每
CPU变量和进行实际的本地操作之间不会被迁移到不同的CPU。
* 当在中断上下文中使用本地操作时,在主线内核上不需要特别注意,因为它们将在局
部CPU上运行,并且已经禁用了抢占。然而,我建议无论如何都要明确地禁用抢占,
以确保它在-rt内核上仍能正确工作。
* 读取本地cpu变量将提供该变量的当前拷贝。
* 对这些变量的读取可以从任何CPU进行,因为对 “ ``long`` ”,对齐的变量的更新
总是原子的。由于写入程序的CPU没有进行内存同步,所以在读取 *其他* cpu的变
量时,可以读取该变量的过期副本。
如何使用本地原子操作
====================
::
#include <linux/percpu.h>
#include <asm/local.h>
static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);
计数器
======
计数是在一个signed long的所有位上进行的。
在可抢占的上下文中,围绕本地原子操作使用 ``get_cpu_var()`` 和
``put_cpu_var()`` :它确保在对每个cpu变量进行写访问时,抢占被禁用。比如
说::
local_inc(&get_cpu_var(counters));
put_cpu_var(counters);
如果你已经在一个抢占安全上下文中,你可以使用 ``this_cpu_ptr()`` 代替::
local_inc(this_cpu_ptr(&counters));
读取计数器
==========
那些本地计数器可以从外部的CPU中读取,以求得计数的总和。请注意,local_read
所看到的跨CPU的数据必须被认为是相对于拥有该数据的CPU上发生的其他内存写入来
说不符合顺序的::
long sum = 0;
for_each_online_cpu(cpu)
sum += local_read(&per_cpu(counters, cpu));
如果你想使用远程local_read来同步CPU之间对资源的访问,必须在写入者和读取者
的CPU上分别使用显式的 ``smp_wmb()`` 和 ``smp_rmb()`` 内存屏障。如果你使
用 ``local_t`` 变量作为写在缓冲区中的字节的计数器,就会出现这种情况:在缓
冲区写和计数器增量之间应该有一个 ``smp_wmb()`` ,在计数器读和缓冲区读之间
也应有一个 ``smp_rmb()`` 。
下面是一个使用 ``local.h`` 实现每个cpu基本计数器的示例模块::
/* test-local.c
*
* Sample module for local.h usage.
*/
#include <asm/local.h>
#include <linux/module.h>
#include <linux/timer.h>
static DEFINE_PER_CPU(local_t, counters) = LOCAL_INIT(0);
static struct timer_list test_timer;
/* IPI called on each CPU. */
static void test_each(void *info)
{
/* Increment the counter from a non preemptible context */
printk("Increment on cpu %d\n", smp_processor_id());
local_inc(this_cpu_ptr(&counters));
/* This is what incrementing the variable would look like within a
* preemptible context (it disables preemption) :
*
* local_inc(&get_cpu_var(counters));
* put_cpu_var(counters);
*/
}
static void do_test_timer(unsigned long data)
{
int cpu;
/* Increment the counters */
on_each_cpu(test_each, NULL, 1);
/* Read all the counters */
printk("Counters read from CPU %d\n", smp_processor_id());
for_each_online_cpu(cpu) {
printk("Read : CPU %d, count %ld\n", cpu,
local_read(&per_cpu(counters, cpu)));
}
mod_timer(&test_timer, jiffies + 1000);
}
static int __init test_init(void)
{
/* initialize the timer that will increment the counter */
timer_setup(&test_timer, do_test_timer, 0);
mod_timer(&test_timer, jiffies + 1);
return 0;
}
static void __exit test_exit(void)
{
timer_shutdown_sync(&test_timer);
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mathieu Desnoyers");
MODULE_DESCRIPTION("Local Atomic Ops");
@@ -0,0 +1,138 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/memory-allocation.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
时奎亮 <alexs@kernel.org>
.. _cn_core-api_memory-allocation:
============
内存分配指南
============
Linux为内存分配提供了多种API。你可以使用 `kmalloc` 或 `kmem_cache_alloc`
系列分配小块内存,使用 `vmalloc` 及其派生产品分配大的几乎连续的区域,或者
你可以用 alloc_pages 直接向页面分配器请求页面。也可以使用更专业的分配器,
例如 `cma_alloc` 或 `zs_malloc` 。
大多数的内存分配API使用GFP标志来表达该内存应该如何分配。GFP的缩写代表
“(get free pages)获取空闲页”,是底层的内存分配功能。
(内存)分配API的多样性与众多的GFP标志相结合,使得“我应该如何分配内存?”这个问
题不那么容易回答,尽管很可能你应该使用
::
kzalloc(<size>, GFP_KERNEL);
当然,有些情况下必须使用其他分配API和不同的GFP标志。
获取空闲页标志
==============
GFP标志控制分配器的行为。它们告诉我们哪些内存区域可以被使用,分配器应该多努力寻
找空闲的内存,这些内存是否可以被用户空间访问等等。内存管理API为GFP标志和它们的
组合提供了参考文件,这里我们简要介绍一下它们的推荐用法:
* 大多数时候, ``GFP_KERNEL`` 是你需要的。内核数据结构的内存,DMA可用内存,inode
缓存,所有这些和其他许多分配类型都可以使用 ``GFP_KERNEL`` 。注意,使用 ``GFP_KERNEL``
意味着 ``GFP_RECLAIM`` ,这意味着在有内存压力的情况下可能会触发直接回收;调用上
下文必须允许睡眠。
* 如果分配是从一个原子上下文中进行的,例如中断处理程序,使用 ``GFP_NOWAIT`` 。这个
标志可以防止直接回收和IO或文件系统操作。因此,在内存压力下, ``GFP_NOWAIT`` 分配
可能会失败。有合理退路的分配应该使用 ``GFP_NOWARN`` 。
* 如果你认为访问保留内存区是合理的,并且除非分配成功,否则内核会有压力,你可以使用 ``GFP_ATOMIC`` 。
* 从用户空间触发的不可信任的分配应该是kmem核算的对象,必须设置 ``__GFP_ACCOUNT`` 位。
有一个方便的用于 ``GFP_KERNEL`` 分配的 ``GFP_KERNEL_ACCOUNT`` 快捷键,其应该被核
算。
* 用户空间的分配应该使用 ``GFP_USER`` 、 ``GFP_HIGHUSER`` 或 ``GFP_HIGHUSER_MOVABLE``
中的一个标志。标志名称越长,限制性越小。
``GFP_HIGHUSER_MOVABLE`` 不要求分配的内存将被内核直接访问,并意味着数据是可迁移的。
``GFP_HIGHUSER`` 意味着所分配的内存是不可迁移的,但也不要求它能被内核直接访问。举个
例子就是一个硬件分配内存,这些数据直接映射到用户空间,但没有寻址限制。
``GFP_USER`` 意味着分配的内存是不可迁移的,它必须被内核直接访问。
你可能会注意到,在现有的代码中,有相当多的分配指定了 ``GFP_NOIO`` 或 ``GFP_NOFS`` 。
从历史上看,它们被用来防止递归死锁,这种死锁是由直接内存回收调用到FS或IO路径以及对已
经持有的资源进行阻塞引起的。从4.12开始,解决这个问题的首选方法是使用新的范围API,即
:ref:`Documentation/core-api/gfp_mask-from-fs-io.rst <gfp_mask_from_fs_io>`.
其他传统的GFP标志是 ``GFP_DMA`` 和 ``GFP_DMA32`` 。它们用于确保分配的内存可以被寻
址能力有限的硬件访问。因此,除非你正在为一个有这种限制的设备编写驱动程序,否则要避免
使用这些标志。而且,即使是有限制的硬件,也最好使用dma_alloc* APIs。
GFP标志和回收行为
-----------------
内存分配可能会触发直接或后台回收,了解页面分配器将如何努力满足该请求或其他请求是非常
有用的。
* ``GFP_KERNEL & ~__GFP_RECLAIM`` - 乐观分配,完全不尝试释放内存。最轻量级的模
式,甚至不启动后台回收。应该小心使用,因为它可能会耗尽内存,而下一个用户可能会启
动更积极的回收。
* ``GFP_KERNEL & ~__GFP_DIRECT_RECLAIM`` (or ``GFP_NOWAIT`` ) - 乐观分配,不
试图从当前上下文中释放内存,但如果该区域低于低水位,可以唤醒kswapd来回收内存。可
以从原子上下文中使用,或者当请求是一个性能优化,并且有另一个慢速路径的回退。
* ``(GFP_KERNEL|__GFP_HIGH) & ~__GFP_DIRECT_RECLAIM`` (aka ``GFP_ATOMIC`` ) - 非
睡眠分配,有一个昂贵的回退,所以它可以访问某些部分的内存储备。通常从中断/底层上下
文中使用,有一个昂贵的慢速路径回退。
* ``GFP_KERNEL`` - 允许后台和直接回收,并使用默认的页面分配器行为。这意味着廉价
的分配请求基本上是不会失败的,但不能保证这种行为,所以失败必须由调用者适当检查(例
如,目前允许OOM杀手失败)。
* ``GFP_KERNEL | __GFP_NORETRY`` - 覆盖默认的分配器行为,所有的分配请求都会提前
失败,而不是导致破坏性的回收(在这个实现中是一轮的回收)。OOM杀手不被调用。
* ``GFP_KERNEL | __GFP_RETRY_MAYFAIL`` - 覆盖 **默认** 的分配器行为,所有分配请求都非
常努力。如果回收不能取得任何进展,该请求将失败。OOM杀手不会被触发。
* ``GFP_KERNEL | __GFP_NOFAIL`` - 覆盖默认的分配器行为,所有分配请求将无休止地循
环,直到成功。这可能真的很危险,特别是对于较大的需求。
选择内存分配器
==============
分配内存的最直接的方法是使用kmalloc()系列的函数。而且,为了安全起见,最好使用将内存
设置为零的例程,如kzalloc()。如果你需要为一个数组分配内存,有kmalloc_array()和kcalloc()
辅助程序。辅助程序struct_size()、array_size()和array3_size()可以用来安全地计算对
象的大小而不会溢出。
可以用 `kmalloc` 分配的块的最大尺寸是有限的。实际的限制取决于硬件和内核配置,但是对于
小于页面大小的对象,使用 `kmalloc` 是一个好的做法。
用 `kmalloc` 分配的块的地址至少要对齐到ARCH_KMALLOC_MINALIGN字节。对于2的幂的大小,
对齐方式也被保证为至少是各自的大小。
用kmalloc()分配的块可以用krealloc()调整大小。与kmalloc_array()类似:以krealloc_array()
的形式提供了一个用于调整数组大小的辅助工具。
对于大量的分配,你可以使用vmalloc()和vzalloc(),或者直接向页面分配器请求页面。由vmalloc
和相关函数分配的内存在物理上是不连续的。
如果你不确定分配的大小对 `kmalloc` 来说是否太大,可以使用kvmalloc()及其派生函数。它将尝
试用kmalloc分配内存,如果分配失败,将用 `vmalloc` 重新尝试。对于哪些GFP标志可以与 `kvmalloc`
一起使用是有限制的;请看kvmalloc_node()参考文档。注意, `kvmalloc` 可能会返回物理上不连
续的内存。
如果你需要分配许多相同的对象,你可以使用slab缓存分配器。在使用缓存之前,应该用
kmem_cache_create()或kmem_cache_create_usercopy()来设置缓存。如果缓存的一部分可能被复
制到用户空间,应该使用第二个函数。在缓存被创建后,kmem_cache_alloc()和它的封装可以从该缓
存中分配内存。
当分配的内存不再需要时,它必须被释放。你可以使用kvfree()来处理用 `kmalloc` 、 `vmalloc`
和 `kvmalloc` 分配的内存。slab缓存应该用kmem_cache_free()来释放。不要忘记用
kmem_cache_destroy()来销毁缓存。
@@ -0,0 +1,119 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/memory-hotplug.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
吴想成 Wu XiangCheng <bobwxc@email.cn>
.. _cn_core-api_memory-hotplug:
==========
内存热插拔
==========
内存热拔插事件通知器
====================
热插拔事件被发送到一个通知队列中。
在 ``include/linux/memory.h`` 中定义了六种类型的通知:
MEM_GOING_ONLINE
在新内存可用之前生成,以便能够为子系统处理内存做准备。页面分配器仍然无法从新
的内存中进行分配。
MEM_CANCEL_ONLINE
如果MEM_GOING_ONLINE失败,则生成。
MEM_ONLINE
当内存成功上线时产生。回调可以从新的内存中分配页面。
MEM_GOING_OFFLINE
在开始对内存进行下线处理时生成。从内存中的分配不再可能,但是一些要下线的内存
仍然在使用。回调可以用来释放一个子系统在指定内存块中已知的内存。
MEM_CANCEL_OFFLINE
如果MEM_GOING_OFFLINE失败,则生成。来自我们试图离线的内存块中的内存又可以使
用了。
MEM_OFFLINE
在内存下线完成后生成。
可以通过调用如下函数来注册一个回调程序:
hotplug_memory_notifier(callback_func, priority)
优先级数值较高的回调函数在数值较低的回调函数之前被调用。
一个回调函数必须有以下原型::
int callback_func(
struct notifier_block *self, unsigned long action, void *arg);
回调函数的第一个参数(self)是指向回调函数本身的通知器链块的一个指针。第二个参
数(action)是上述的事件类型之一。第三个参数(arg)传递一个指向
memory_notify结构体的指针::
struct memory_notify {
unsigned long start_pfn;
unsigned long nr_pages;
int status_change_nid;
}
- start_pfn是在线/离线内存的start_pfn。
- nr_pages是在线/离线内存的页数。
- status_change_nid是当nodemask的N_MEMORY被(将)设置/清除时设置的节点id。这
意味着一个新的(没上线的)节点通过联机获得新的内存,而一个节点失去了所有的内
存。如果这个值为-1,那么nodemask的状态就不会改变。
如果 status_changed_nid* >= 0,回调应该在必要时为节点创建/丢弃结构体。
回调程序应返回 ``include/linux/notifier.h`` 中定义的NOTIFY_DONE, NOTIFY_OK,
NOTIFY_BAD, NOTIFY_STOP中的一个值。
NOTIFY_DONE和NOTIFY_OK对进一步处理没有影响。
NOTIFY_BAD是作为对MEM_GOING_ONLINE、MEM_GOING_OFFLINE、MEM_ONLINE或MEM_OFFLINE
动作的回应,用于取消热插拔。它停止对通知队列的进一步处理。
NOTIFY_STOP停止对通知队列的进一步处理。
内部锁
======
当添加/删除使用内存块设备(即普通RAM)的内存时,device_hotplug_lock应该被保持
为:
- 针对在线/离线请求进行同步(例如,通过sysfs)。这样一来,内存块设备只有在内存
被完全添加后才能被用户空间访问(.online/.state属性)。而在删除内存时,我们知
道没有人在临界区。
- 与CPU热拔插或类似操作同步(例如ACPI和PPC相关操作)
特别是,在添加内存和用户空间试图以比预期更快的速度上线该内存时,有可能出现锁反转,
使用device_hotplug_lock可以避免此情况:
- device_online()将首先接受device_lock(),然后是mem_hotplug_lock。
- add_memory_resource()将首先使用mem_hotplug_lock,然后是device_lock()(在创
建设备时,在bus_add_device()期间)。
由于在使用device_lock()之前,设备对用户空间是可见的,这可能导致锁的反转。
内存的上线/下线应该通过device_online()/device_offline()完成————确保它与通过
sysfs进行的操作正确同步。建议持有device_hotplug_lock(例如,保护online_type)。
当添加/删除/上线/下线内存或者添加/删除异构或设备内存时,我们应该始终持有写模式的
mem_hotplug_lock,以序列化内存热插拔(例如访问全局/区域变量)。
此外,mem_hotplug_lock(与device_hotplug_lock相反)在读取模式下允许一个相当
有效的get_online_mems/put_online_mems实现,所以访问内存的代码可以防止该内存
消失。
@@ -0,0 +1,131 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/mm-api.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
:校译:
时奎亮<alexs@kernel.org>
.. _cn_core-api_mm-api:
============
内存管理APIs
============
API(Application Programming Interface,应用程序接口)
用户空间内存访问
================
该API在以下内核代码中:
arch/x86/include/asm/uaccess.h
arch/x86/lib/usercopy_32.c
mm/gup.c
.. _cn_mm-api-gfp-flags:
内存分配控制
============
该API在以下内核代码中:
include/linux/gfp_types.h
Slab缓存
========
此缓存非cpu片上缓存,请读者自行查阅资料。
该API在以下内核代码中:
include/linux/slab.h
mm/slab.c
mm/slab_common.c
mm/util.c
虚拟连续(内存页)映射
======================
该API在以下内核代码中:
mm/vmalloc.c
文件映射和页面缓存
==================
该API在以下内核代码中:
文件映射
--------
mm/filemap.c
预读
----
mm/readahead.c
回写
----
mm/page-writeback.c
截断
----
mm/truncate.c
include/linux/pagemap.h
内存池
======
该API在以下内核代码中:
mm/mempool.c
DMA池
=====
DMA(Direct Memory Access,直接存储器访问)
该API在以下内核代码中:
mm/dmapool.c
更多的内存管理函数
==================
该API在以下内核代码中:
mm/memory.c
mm/page_alloc.c
mm/mempolicy.c
include/linux/mm_types.h
include/linux/mm_inline.h
include/linux/page-flags.h
include/linux/mm.h
include/linux/page_ref.h
include/linux/mmzone.h
mm/util.c
@@ -0,0 +1,160 @@
.. SPDX-License-Identifier: GPL-2.0+
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/packing.rst
:翻译:
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
:校译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
吴想成 Wu Xiangcheng <bobwxc@email.cn>
时奎亮 Alex Shi <alexs@kernel.org>
========================
通用的位域打包和解包函数
========================
问题陈述
--------
使用硬件时,必须在几种与其交互的方法之间进行选择。
可以将指针映射到在硬件设备的内存区上精心设计的结构体,并将其字段作为结构成员(可
能声明为位域)访问。但是由于CPU和硬件设备之间潜在的字节顺序不匹配,以这种方式编写
代码会降低其可移植性。
此外,必须密切注意将硬件文档中的寄存器定义转换为结构的位域索引。此外,一些硬件
(通常是网络设备)倾向于以违反任何合理字边界(有时甚至是64位)的方式对其寄存器字
段进行分组。这就造成了不得不在结构中定义寄存器字段的“高”和“低”部分的不便。
结构域定义的更可靠的替代方法是通过移动适当数量的位来提取所需的字段。但这仍然不能
防止字节顺序不匹配,除非所有内存访问都是逐字节执行的。此外,代码很容易变得杂乱无
章,同时可能会在所需的许多位移操作中丢失一些高层次的想法。
许多驱动程序采用了位移的方法,然后试图用定制的宏来减少杂乱无章的东西,但更多的时
候,这些宏所采用的捷径依旧妨碍了代码真正的可移植性。
解决方案
--------
该API涉及2个基本操作:
- 将一个CPU可使用的数字打包到内存缓冲区中(具有硬件约束/特殊性)。
- 将内存缓冲区(具有硬件约束/特殊性)解压缩为一个CPU可使用的数字。
该API提供了对所述硬件约束和特殊性以及CPU字节序的抽象,因此这两者之间可能不匹配。
这些API函数的基本单元是u64。从CPU的角度来看,位63总是意味着字节7的位偏移量7,尽管
只是逻辑上的。问题是:我们将这个比特放在内存的什么位置?
以下示例介绍了打包u64字段的内存布局。打包缓冲区中的字节偏移量始终默认为0,1...7。
示例显示的是逻辑字节和位所在的位置。
1. 通常情况下(无特殊性),我们会这样做:
::
63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 39 38 37 36 35 34 33 32
7 6 5 4
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
3 2 1 0
也就是说,CPU可使用的u64的MSByte(7)位于内存偏移量0处,而u64的LSByte(0)位于内存偏移量7处。
这对应于大多数人认为的“大端”,其中位i对应于数字2^i。这在代码注释中也称为“逻辑”符号。
2. 如果设置了QUIRK_MSB_ON_THE_RIGHT,我们按如下方式操作:
::
56 57 58 59 60 61 62 63 48 49 50 51 52 53 54 55 40 41 42 43 44 45 46 47 32 33 34 35 36 37 38 39
7 6 5 4
24 25 26 27 28 29 30 31 16 17 18 19 20 21 22 23 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7
3 2 1 0
也就是说,QUIRK_MSB_ON_THE_RIGHT不会影响字节定位,但会反转字节内的位偏移量。
3. 如果设置了QUIRK_LITTLE_ENDIAN,我们按如下方式操作:
::
39 38 37 36 35 34 33 32 47 46 45 44 43 42 41 40 55 54 53 52 51 50 49 48 63 62 61 60 59 58 57 56
4 5 6 7
7 6 5 4 3 2 1 0 15 14 13 12 11 10 9 8 23 22 21 20 19 18 17 16 31 30 29 28 27 26 25 24
0 1 2 3
因此,QUIRK_LITTLE_ENDIAN意味着在内存区域内,每个4字节的字的每个字节都被放置在与
该字的边界相比的镜像位置。
4. 如果设置了QUIRK_MSB_ON_THE_RIGHT和QUIRK_LITTLE_ENDIAN,我们这样做:
::
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
4 5 6 7
0 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
0 1 2 3
5. 如果只设置了QUIRK_LSW32_IS_FIRST,我们这样做:
::
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
3 2 1 0
63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 39 38 37 36 35 34 33 32
7 6 5 4
在这种情况下,8字节内存区域解释如下:前4字节对应最不重要的4字节的字,后4字节对应
更重要的4字节的字。
6. 如果设置了QUIRK_LSW32_IS_FIRST和QUIRK_MSB_ON_THE_RIGHT,我们这样做:
::
24 25 26 27 28 29 30 31 16 17 18 19 20 21 22 23 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7
3 2 1 0
56 57 58 59 60 61 62 63 48 49 50 51 52 53 54 55 40 41 42 43 44 45 46 47 32 33 34 35 36 37 38 39
7 6 5 4
7. 如果设置了QUIRK_LSW32_IS_FIRST和QUIRK_LITTLE_ENDIAN,则如下所示:
::
7 6 5 4 3 2 1 0 15 14 13 12 11 10 9 8 23 22 21 20 19 18 17 16 31 30 29 28 27 26 25 24
0 1 2 3
39 38 37 36 35 34 33 32 47 46 45 44 43 42 41 40 55 54 53 52 51 50 49 48 63 62 61 60 59 58 57 56
4 5 6 7
8. 如果设置了QUIRK_LSW32_IS_FIRST,QUIRK_LITTLE_ENDIAN和QUIRK_MSB_ON_THE_RIGHT,
则如下所示:
::
0 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
0 1 2 3
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
4 5 6 7
我们总是认为我们的偏移量好像没有特殊性,然后在访问内存区域之前翻译它们。
预期用途
--------
选择使用该API的驱动程序首先需要确定上述3种quirk组合(共8种)中的哪一种与硬件文档
中描述的相匹配。然后,他们应该封装packing()函数,创建一个新的xxx_packing(),使用
适当的QUIRK_* one-hot 位集合来调用它。
packing()函数返回一个int类型的错误码,以防止程序员使用不正确的API。这些错误预计不
会在运行时发生,因此xxx_packing()返回void并简单地接受这些错误是合理的。它可以选择
转储栈或打印错误描述。
@@ -0,0 +1,161 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/padata.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_core_api_padata.rst:
==================
padata并行执行机制
==================
:日期: 2020年5月
Padata是一种机制,内核可以通过此机制将工作分散到多个CPU上并行完成,同时
可以选择保持它们的顺序。
它最初是为IPsec开发的,它需要在不对这些数据包重新排序的其前提下,为大量的数
据包进行加密和解密。这是目前padata的序列化作业支持的唯一用途。
Padata还支持多线程作业,将作业平均分割,同时在线程之间进行负载均衡和协调。
执行序列化作业
==============
初始化
------
使用padata执行序列化作业的第一步是建立一个padata_instance结构体,以全面
控制作业的运行方式::
#include <linux/padata.h>
struct padata_instance *padata_alloc(const char *name);
'name'即标识了这个实例。
然后,通过分配一个padata_shell来完成padata的初始化::
struct padata_shell *padata_alloc_shell(struct padata_instance *pinst);
一个padata_shell用于向padata提交一个作业,并允许一系列这样的作业被独立地
序列化。一个padata_instance可以有一个或多个padata_shell与之相关联,每个
都允许一系列独立的作业。
修改cpumasks
------------
用于运行作业的CPU可以通过两种方式改变,通过padata_set_cpumask()编程或通
过sysfs。前者的定义是::
int padata_set_cpumask(struct padata_instance *pinst, int cpumask_type,
cpumask_var_t cpumask);
这里cpumask_type是PADATA_CPU_PARALLEL(并行)或PADATA_CPU_SERIAL(串行)之一,其中并
行cpumask描述了哪些处理器将被用来并行执行提交给这个实例的作业,串行cpumask
定义了哪些处理器被允许用作串行化回调处理器。 cpumask指定了要使用的新cpumask。
一个实例的cpumasks可能有sysfs文件。例如,pcrypt的文件在
/sys/kernel/pcrypt/<instance-name>。在一个实例的目录中,有两个文件,parallel_cpumask
和serial_cpumask,任何一个cpumask都可以通过在文件中回显(echo)一个bitmask
来改变,比如说::
echo f > /sys/kernel/pcrypt/pencrypt/parallel_cpumask
读取其中一个文件会显示用户提供的cpumask,它可能与“可用”的cpumask不同。
Padata内部维护着两对cpumask,用户提供的cpumask和“可用的”cpumask(每一对由一个
并行和一个串行cpumask组成)。用户提供的cpumasks在实例分配时默认为所有可能的CPU,
并且可以如上所述进行更改。可用的cpumasks总是用户提供的cpumasks的一个子集,只包
含用户提供的掩码中的在线CPU;这些是padata实际使用的cpumasks。因此,向padata提
供一个包含离线CPU的cpumask是合法的。一旦用户提供的cpumask中的一个离线CPU上线,
padata就会使用它。
改变CPU掩码的操作代价很高,所以不应频繁更改。
运行一个作业
-------------
实际上向padata实例提交工作需要创建一个padata_priv结构体,它代表一个作业::
struct padata_priv {
/* Other stuff here... */
void (*parallel)(struct padata_priv *padata);
void (*serial)(struct padata_priv *padata);
};
这个结构体几乎肯定会被嵌入到一些针对要做的工作的大结构体中。它的大部分字段对
padata来说是私有的,但是这个结构在初始化时应该被清零,并且应该提供parallel()和
serial()函数。在完成工作的过程中,这些函数将被调用,我们马上就会遇到。
工作的提交是通过::
int padata_do_parallel(struct padata_shell *ps,
struct padata_priv *padata, int *cb_cpu);
ps和padata结构体必须如上所述进行设置;cb_cpu指向作业完成后用于最终回调的首选CPU;
它必须在当前实例的CPU掩码中(如果不是,cb_cpu指针将被更新为指向实际选择的CPU)。
padata_do_parallel()的返回值在成功时为0,表示工作正在进行中。-EBUSY意味着有人
在其他地方正在搞乱实例的CPU掩码,而当cb_cpu不在串行cpumask中、并行或串行cpumasks
中无在线CPU,或实例停止时,则会出现-EINVAL反馈。
每个提交给padata_do_parallel()的作业将依次传递给一个CPU上的上述parallel()函数
的一个调用,所以真正的并行是通过提交多个作业来实现的。parallel()在运行时禁用软
件中断,因此不能睡眠。parallel()函数把获得的padata_priv结构体指针作为其唯一的参
数;关于实际要做的工作的信息可能是通过使用container_of()找到封装结构体来获得的。
请注意,parallel()没有返回值;padata子系统假定parallel()将从此时开始负责这项工
作。作业不需要在这次调用中完成,但是,如果parallel()留下了未完成的工作,它应该准
备在前一个作业完成之前,被以新的作业再次调用
序列化作业
----------
当一个作业完成时,parallel()(或任何实际完成该工作的函数)应该通过调用通知padata此
事::
void padata_do_serial(struct padata_priv *padata);
在未来的某个时刻,padata_do_serial()将触发对padata_priv结构体中serial()函数的调
用。这个调用将发生在最初要求调用padata_do_parallel()的CPU上;它也是在本地软件中断
被禁用的情况下运行的。
请注意,这个调用可能会被推迟一段时间,因为padata代码会努力确保作业按照提交的顺序完
成。
销毁
----
清理一个padata实例时,可以预见的是调用两个free函数,这两个函数对应于分配的逆过程::
void padata_free_shell(struct padata_shell *ps);
void padata_free(struct padata_instance *pinst);
用户有责任确保在调用上述任何一项之前,所有未完成的工作都已完成。
运行多线程作业
==============
一个多线程作业有一个主线程和零个或多个辅助线程,主线程参与作业,然后等待所有辅助线
程完成。padata将作业分割成称为chunk的单元,其中chunk是一个线程在一次调用线程函数
中完成的作业片段。
用户必须做三件事来运行一个多线程作业。首先,通过定义一个padata_mt_job结构体来描述
作业,这在接口部分有解释。这包括一个指向线程函数的指针,padata每次将作业块分配给线
程时都会调用这个函数。然后,定义线程函数,它接受三个参数: ``start`` 、 ``end`` 和 ``arg`` ,
其中前两个参数限定了线程操作的范围,最后一个是指向作业共享状态的指针,如果有的话。
准备好共享状态,它通常被分配在主线程的堆栈中。最后,调用padata_do_multithreaded(),
它将在作业完成后返回。
接口
====
该API在以下内核代码中:
include/linux/padata.h
kernel/padata.c
@@ -0,0 +1,111 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/printk-basics.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
.. _cn_printk-basics.rst:
==================
使用printk记录消息
==================
printk()是Linux内核中最广为人知的函数之一。它是我们打印消息的标准工具,通常也是追踪和调试
的最基本方法。如果你熟悉printf(3),你就能够知道printk()是基于它的,尽管它在功能上有一些不
同之处:
- printk() 消息可以指定日志级别。
- 格式字符串虽然与C99基本兼容,但并不遵循完全相同的规范。它有一些扩展和一些限制(没
有 ``%n`` 或浮点转换指定符)。参见:ref: `如何正确地获得printk格式指定符<printk-specifiers>` 。
所有的printk()消息都会被打印到内核日志缓冲区,这是一个通过/dev/kmsg输出到用户空间的环
形缓冲区。读取它的通常方法是使用 ``dmesg`` 。
printk()的用法通常是这样的::
printk(KERN_INFO "Message: %s\n", arg);
其中 ``KERN_INFO`` 是日志级别(注意,它与格式字符串连在一起,日志级别不是一个单独的参数)。
可用的日志级别是:
+----------------+--------+-----------------------------------------------+
| 名称 | 字符串 | 别名函数 |
+================+========+===============================================+
| KERN_EMERG | "0" | pr_emerg() |
+----------------+--------+-----------------------------------------------+
| KERN_ALERT | "1" | pr_alert() |
+----------------+--------+-----------------------------------------------+
| KERN_CRIT | "2" | pr_crit() |
+----------------+--------+-----------------------------------------------+
| KERN_ERR | "3" | pr_err() |
+----------------+--------+-----------------------------------------------+
| KERN_WARNING | "4" | pr_warn() |
+----------------+--------+-----------------------------------------------+
| KERN_NOTICE | "5" | pr_notice() |
+----------------+--------+-----------------------------------------------+
| KERN_INFO | "6" | pr_info() |
+----------------+--------+-----------------------------------------------+
| KERN_DEBUG | "7" | pr_debug() and pr_devel() 若定义了DEBUG |
+----------------+--------+-----------------------------------------------+
| KERN_DEFAULT | "" | |
+----------------+--------+-----------------------------------------------+
| KERN_CONT | "c" | pr_cont() |
+----------------+--------+-----------------------------------------------+
日志级别指定了一条消息的重要性。内核根据日志级别和当前 *console_loglevel* (一个内核变量)决
定是否立即显示消息(将其打印到当前控制台)。如果消息的优先级比 *console_loglevel* 高(日志级
别值较低),消息将被打印到控制台。
如果省略了日志级别,则以 ``KERN_DEFAULT`` 级别打印消息。
你可以用以下方法检查当前的 *console_loglevel* ::
$ cat /proc/sys/kernel/printk
4 4 1 7
结果显示了 *current*, *default*, *minimum* 和 *boot-time-default* 日志级别
要改变当前的 console_loglevel,只需在 ``/proc/sys/kernel/printk`` 中写入所需的
级别。例如,要打印所有的消息到控制台上::
# echo 8 > /proc/sys/kernel/printk
另一种方式,使用 ``dmesg``::
# dmesg -n 5
设置 console_loglevel 打印 KERN_WARNING (4) 或更严重的消息到控制台。更多消息参
见 ``dmesg(1)`` 。
作为printk()的替代方案,你可以使用 ``pr_*()`` 别名来记录日志。这个系列的宏在宏名中
嵌入了日志级别。例如::
pr_info("Info message no. %d\n", msg_num);
打印 ``KERN_INFO`` 消息。
除了比等效的printk()调用更简洁之外,它们还可以通过pr_fmt()宏为格式字符串使用一个通用
的定义。例如,在源文件的顶部(在任何 ``#include`` 指令之前)定义这样的内容。::
#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__
会在该文件中的每一条 pr_*() 消息前加上发起该消息的模块和函数名称。
为了调试,还有两个有条件编译的宏:
pr_debug()和pr_devel(),除非定义了 ``DEBUG`` (或者在pr_debug()的情况下定义了
``CONFIG_DYNAMIC_DEBUG`` ),否则它们不会被编译。
函数接口
========
该API在以下内核代码中:
include/linux/printk.h
@@ -0,0 +1,597 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/printk-formats.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
.. _cn_printk-formats.rst:
==============================
如何获得正确的printk格式占位符
==============================
:作者: Randy Dunlap <rdunlap@infradead.org>
:作者: Andrew Murray <amurray@mpc-data.co.uk>
整数类型
========
::
若变量类型是Type,则使用printk格式占位符。
-------------------------------------------
char %d 或 %x
unsigned char %u 或 %x
short int %d 或 %x
unsigned short int %u 或 %x
int %d 或 %x
unsigned int %u 或 %x
long %ld 或 %lx
unsigned long %lu 或 %lx
long long %lld 或 %llx
unsigned long long %llu 或 %llx
size_t %zu 或 %zx
ssize_t %zd 或 %zx
s8 %d 或 %x
u8 %u 或 %x
s16 %d 或 %x
u16 %u 或 %x
s32 %d 或 %x
u32 %u 或 %x
s64 %lld 或 %llx
u64 %llu 或 %llx
如果 <type> 的大小依赖于配置选项 (例如 sector_t, blkcnt_t) 或其大小依赖于架构
(例如 tcflag_t),则使用其可能的最大类型的格式占位符并显式强制转换为它。
例如
::
printk("test: sector number/total blocks: %llu/%llu\n",
(unsigned long long)sector, (unsigned long long)blockcount);
提醒:sizeof()返回类型为size_t。
内核的printf不支持%n。显而易见,浮点格式(%e, %f, %g, %a)也不被识别。使用任何不
支持的占位符或长度限定符都会导致一个WARN并且终止vsnprintf()执行。
指针类型
========
一个原始指针值可以用%p打印,它将在打印前对地址进行哈希处理。内核也支持扩展占位符来打印
不同类型的指针。
一些扩展占位符会打印给定地址上的数据,而不是打印地址本身。在这种情况下,以下错误消息可能
会被打印出来,而不是无法访问的消息::
(null) data on plain NULL address
(efault) data on invalid address
(einval) invalid data on a valid address
普通指针
----------
::
%p abcdef12 or 00000000abcdef12
没有指定扩展名的指针(即没有修饰符的%p)被哈希(hash),以防止内核内存布局消息的泄露。这
样还有一个额外的好处,就是提供一个唯一的标识符。在64位机器上,前32位被清零。当没有足够的
熵进行散列处理时,内核将打印(ptrval)代替
如果可能的话,使用专门的修饰符,如%pS或%pB(如下所述),以避免打印一个必须事后解释的非哈
希地址。如果不可能,而且打印地址的目的是为调试提供更多的消息,使用%p,并在调试过程中
用 ``no_hash_pointers`` 参数启动内核,这将打印所有未修改的%p地址。如果你 *真的* 想知
道未修改的地址,请看下面的%px。
如果(也只有在)你将地址作为虚拟文件的内容打印出来,例如在procfs或sysfs中(使用
seq_printf(),而不是printk())由用户空间进程读取,使用下面描述的%pK修饰符,不
要用%p或%px。
错误指针
--------
::
%pe -ENOSPC
用于打印错误指针(即IS_ERR()为真的指针)的符号错误名。不知道符号名的错误值会以十进制打印,
而作为%pe参数传递的非ERR_PTR会被视为普通的%p。
符号/函数指针
-------------
::
%pS versatile_init+0x0/0x110
%ps versatile_init
%pSR versatile_init+0x9/0x110
(with __builtin_extract_return_addr() translation)
%pB prev_fn_of_versatile_init+0x88/0x88
``S`` 和 ``s`` 占位符用于打印符号格式的指针。它们的结果是符号名称带有(S)或不带有(s)偏移
量。如果禁用KALLSYMS,则打印符号地址。
``B`` 占位符的结果是带有偏移量的符号名,在打印堆栈回溯时应该使用。占位符将考虑编译器优化
的影响,当使用尾部调用并使用noreturn GCC属性标记时,可能会发生这种优化。
如果指针在一个模块内,模块名称和可选的构建ID将被打印在符号名称之后,并在说明符的末尾添加
一个额外的 ``b`` 。
::
%pS versatile_init+0x0/0x110 [module_name]
%pSb versatile_init+0x0/0x110 [module_name ed5019fdf5e53be37cb1ba7899292d7e143b259e]
%pSRb versatile_init+0x9/0x110 [module_name ed5019fdf5e53be37cb1ba7899292d7e143b259e]
(with __builtin_extract_return_addr() translation)
%pBb prev_fn_of_versatile_init+0x88/0x88 [module_name ed5019fdf5e53be37cb1ba7899292d7e143b259e]
来自BPF / tracing追踪的探查指针
----------------------------------
::
%pks kernel string
%pus user string
``k`` 和 ``u`` 指定符用于打印来自内核内存(k)或用户内存(u)的先前探测的内存。后面的 ``s`` 指
定符的结果是打印一个字符串。对于直接在常规的vsnprintf()中使用时,(k)和(u)注释被忽略,但是,当
在BPF的bpf_trace_printk()之外使用时,它会读取它所指向的内存,不会出现错误。
内核指针
--------
::
%pK 01234567 or 0123456789abcdef
用于打印应该对非特权用户隐藏的内核指针。%pK的行为取决于kptr_restrict sysctl——详见
Documentation/admin-guide/sysctl/kernel.rst。
未经修改的地址
--------------
::
%px 01234567 or 0123456789abcdef
对于打印指针,当你 *真的* 想打印地址时。在用%px打印指针之前,请考虑你是否泄露了内核内
存布局的敏感消息。%px在功能上等同于%lx(或%lu)。%px是首选,因为它在grep查找时更唯一。
如果将来我们需要修改内核处理打印指针的方式,我们将能更好地找到调用点。
在使用%px之前,请考虑使用%p并在调试过程中启用' ' no_hash_pointer ' '内核参数是否足
够(参见上面的%p描述)。%px的一个有效场景可能是在panic发生之前立即打印消息,这样无论如何
都可以防止任何敏感消息被利用,使用%px就不需要用no_hash_pointer来重现panic。
指针差异
--------
::
%td 2560
%tx a00
为了打印指针的差异,使用ptrdiff_t的%t修饰符。
例如::
printk("test: difference between pointers: %td\n", ptr2 - ptr1);
结构体资源(Resources)
-----------------------
::
%pr [mem 0x60000000-0x6fffffff flags 0x2200] or
[mem 0x0000000060000000-0x000000006fffffff flags 0x2200]
%pR [mem 0x60000000-0x6fffffff pref] or
[mem 0x0000000060000000-0x000000006fffffff pref]
用于打印结构体资源。 ``R`` 和 ``r`` 占位符的结果是打印出的资源带有(R)或不带有(r)解码标志
成员。
通过引用传递。
物理地址类型 phys_addr_t
------------------------
::
%pa[p] 0x01234567 or 0x0123456789abcdef
用于打印phys_addr_t类型(以及它的衍生物,如resource_size_t),该类型可以根据构建选项而
变化,无论CPU数据真实物理地址宽度如何。
通过引用传递。
DMA地址类型dma_addr_t
---------------------
::
%pad 0x01234567 or 0x0123456789abcdef
用于打印dma_addr_t类型,该类型可以根据构建选项而变化,而不考虑CPU数据路径的宽度。
通过引用传递。
原始缓冲区为转义字符串
----------------------
::
%*pE[achnops]
用于将原始缓冲区打印成转义字符串。对于以下缓冲区::
1b 62 20 5c 43 07 22 90 0d 5d
几个例子展示了如何进行转换(不包括两端的引号)。::
%*pE "\eb \C\a"\220\r]"
%*pEhp "\x1bb \C\x07"\x90\x0d]"
%*pEa "\e\142\040\\\103\a\042\220\r\135"
转换规则是根据可选的标志组合来应用的(详见:c:func:`string_escape_mem` 内核文档):
- a - ESCAPE_ANY
- c - ESCAPE_SPECIAL
- h - ESCAPE_HEX
- n - ESCAPE_NULL
- o - ESCAPE_OCTAL
- p - ESCAPE_NP
- s - ESCAPE_SPACE
默认情况下,使用 ESCAPE_ANY_NP。
ESCAPE_ANY_NP是许多情况下的明智选择,特别是对于打印SSID。
如果字段宽度被省略,那么将只转义1个字节。
原始缓冲区为十六进制字符串
--------------------------
::
%*ph 00 01 02 ... 3f
%*phC 00:01:02: ... :3f
%*phD 00-01-02- ... -3f
%*phN 000102 ... 3f
对于打印小的缓冲区(最长64个字节),可以用一定的分隔符作为一个
十六进制字符串。对于较大的缓冲区,可以考虑使用
:c:func:`print_hex_dump` 。
MAC/FDDI地址
------------
::
%pM 00:01:02:03:04:05
%pMR 05:04:03:02:01:00
%pMF 00-01-02-03-04-05
%pm 000102030405
%pmR 050403020100
用于打印以十六进制表示的6字节MAC/FDDI地址。 ``M`` 和 ``m`` 占位符导致打印的
地址有(M)或没有(m)字节分隔符。默认的字节分隔符是冒号(:)。
对于FDDI地址,可以在 ``M`` 占位符之后使用 ``F`` 说明,以使用破折号(——)分隔符
代替默认的分隔符。
对于蓝牙地址, ``R`` 占位符应使用在 ``M`` 占位符之后,以使用反转的字节顺序,适
合于以小尾端顺序的蓝牙地址的肉眼可见的解析。
通过引用传递。
IPv4地址
--------
::
%pI4 1.2.3.4
%pi4 001.002.003.004
%p[Ii]4[hnbl]
用于打印IPv4点分隔的十进制地址。 ``I4`` 和 ``i4`` 占位符的结果是打印的地址
有(i4)或没有(I4)前导零。
附加的 ``h`` 、 ``n`` 、 ``b`` 和 ``l`` 占位符分别用于指定主机、网络、大
尾端或小尾端地址。如果没有提供占位符,则使用默认的网络/大尾端顺序。
通过引用传递。
IPv6 地址
---------
::
%pI6 0001:0002:0003:0004:0005:0006:0007:0008
%pi6 00010002000300040005000600070008
%pI6c 1:2:3:4:5:6:7:8
用于打印IPv6网络顺序的16位十六进制地址。 ``I6`` 和 ``i6`` 占位符的结果是
打印的地址有(I6)或没有(i6)分号。始终使用前导零。
额外的 ``c`` 占位符可与 ``I`` 占位符一起使用,以打印压缩的IPv6地址,如
https://tools.ietf.org/html/rfc5952 所述
通过引用传递。
IPv4/IPv6地址(generic, with port, flowinfo, scope)
--------------------------------------------------
::
%pIS 1.2.3.4 or 0001:0002:0003:0004:0005:0006:0007:0008
%piS 001.002.003.004 or 00010002000300040005000600070008
%pISc 1.2.3.4 or 1:2:3:4:5:6:7:8
%pISpc 1.2.3.4:12345 or [1:2:3:4:5:6:7:8]:12345
%p[Ii]S[pfschnbl]
用于打印一个IP地址,不需要区分它的类型是AF_INET还是AF_INET6。一个指向有效结构
体sockaddr的指针,通过 ``IS`` 或 ``IS`` 指定,可以传递给这个格式占位符。
附加的 ``p`` 、 ``f`` 和 ``s`` 占位符用于指定port(IPv4, IPv6)、
flowinfo (IPv6)和sope(IPv6)。port有一个 ``:`` 前缀,flowinfo是 ``/`` 和
范围是 ``%`` ,每个后面都跟着实际的值。
对于IPv6地址,如果指定了额外的指定符 ``c`` ,则使用
https://tools.ietf.org/html/rfc5952 描述的压缩IPv6地址。
如https://tools.ietf.org/html/draft-ietf-6man-text-addr-representation-07
所建议的,IPv6地址由'[',']'包围,以防止出现额外的占位符 ``p`` , ``f`` 或 ``s`` 。
对于IPv4地址,也可以使用额外的 ``h`` , ``n`` , ``b`` 和 ``l`` 说
明符,但对于IPv6地址则忽略。
通过引用传递。
更多例子::
%pISfc 1.2.3.4 or [1:2:3:4:5:6:7:8]/123456789
%pISsc 1.2.3.4 or [1:2:3:4:5:6:7:8]%1234567890
%pISpfc 1.2.3.4:12345 or [1:2:3:4:5:6:7:8]:12345/123456789
UUID/GUID地址
-------------
::
%pUb 00010203-0405-0607-0809-0a0b0c0d0e0f
%pUB 00010203-0405-0607-0809-0A0B0C0D0E0F
%pUl 03020100-0504-0706-0809-0a0b0c0e0e0f
%pUL 03020100-0504-0706-0809-0A0B0C0E0E0F
用于打印16字节的UUID/GUIDs地址。附加的 ``l`` , ``L`` , ``b`` 和 ``B`` 占位符用
于指定小写(l)或大写(L)十六进制表示法中的小尾端顺序,以及小写(b)或大写(B)十六进制表
示法中的大尾端顺序。
如果没有使用额外的占位符,则将打印带有小写十六进制表示法的默认大端顺序。
通过引用传递。
目录项(dentry)的名称
----------------------
::
%pd{,2,3,4}
%pD{,2,3,4}
用于打印dentry名称;如果我们用 :c:func:`d_move` 和它比较,名称可能是新旧混合的,但
不会oops。 %pd dentry比较安全,其相当于我们以前用的%s dentry->d_name.name,%pd<n>打
印 ``n`` 最后的组件。 %pD对结构文件做同样的事情。
通过引用传递。
块设备(block_device)名称
--------------------------
::
%pg sda, sda1 or loop0p1
用于打印block_device指针的名称。
va_format结构体
---------------
::
%pV
用于打印结构体va_format。这些结构包含一个格式字符串
和va_list如下
::
struct va_format {
const char *fmt;
va_list *va;
};
实现 "递归vsnprintf"。
如果没有一些机制来验证格式字符串和va_list参数的正确性,请不要使用这个功能。
通过引用传递。
设备树节点
----------
::
%pOF[fnpPcCF]
用于打印设备树节点结构。默认行为相当于%pOFf。
- f - 设备节点全称
- n - 设备节点名
- p - 设备节点句柄
- P - 设备节点路径规范(名称+@单位)
- F - 设备节点标志
- c - 主要兼容字符串
- C - 全兼容字符串
当使用多个参数时,分隔符是':'。
例如
::
%pOF /foo/bar@0 - Node full name
%pOFf /foo/bar@0 - Same as above
%pOFfp /foo/bar@0:10 - Node full name + phandle
%pOFfcF /foo/bar@0:foo,device:--P- - Node full name +
major compatible string +
node flags
D - dynamic
d - detached
P - Populated
B - Populated bus
通过引用传递。
Fwnode handles
--------------
::
%pfw[fP]
用于打印fwnode_handles的消息。默认情况下是打印完整的节点名称,包括路径。
这些修饰符在功能上等同于上面的%pOF。
- f - 节点的全名,包括路径。
- P - 节点名称,包括地址(如果有的话)。
例如 (ACPI)
::
%pfwf \_SB.PCI0.CIO2.port@1.endpoint@0 - Full node name
%pfwP endpoint@0 - Node name
例如 (OF)
::
%pfwf /ocp@68000000/i2c@48072000/camera@10/port/endpoint - Full name
%pfwP endpoint - Node name
时间和日期
----------
::
%pt[RT] YYYY-mm-ddTHH:MM:SS
%pt[RT]s YYYY-mm-dd HH:MM:SS
%pt[RT]d YYYY-mm-dd
%pt[RT]t HH:MM:SS
%pt[RT][dt][r][s]
用于打印日期和时间::
R struct rtc_time structure
T time64_t type
以我们(人类)可读的格式。
默认情况下,年将以1900为单位递增,月将以1为单位递增。 使用%pt[RT]r (raw)
来抑制这种行为。
%pt[RT]s(空格)将覆盖ISO 8601的分隔符,在日期和时间之间使用''(空格)而
不是'T'(大写T)。当日期或时间被省略时,它不会有任何影响。
通过引用传递。
clk结构体
---------
::
%pC pll1
用于打印clk结构。%pC 打印时钟的名称(通用时钟框架)或唯一的32位
ID(传统时钟框架)。
通过引用传递。
位图及其衍生物,如cpumask和nodemask
-----------------------------------
::
%*pb 0779
%*pbl 0,3-6,8-10
对于打印位图(bitmap)及其派生的cpumask和nodemask,%*pb输出以字段宽度为位数的位图,
%*pbl输出以字段宽度为位数的范围列表。
字段宽度用值传递,位图用引用传递。可以使用辅助宏cpumask_pr_args()和
nodemask_pr_args()来方便打印cpumask和nodemask。
标志位字段,如页标志、gfp_flags
-------------------------------
::
%pGp 0x17ffffc0002036(referenced|uptodate|lru|active|private|node=0|zone=2|lastcpupid=0x1fffff)
%pGg GFP_USER|GFP_DMA32|GFP_NOWARN
%pGv read|exec|mayread|maywrite|mayexec|denywrite
将flags位字段打印为构造值的符号常量集合。标志的类型由第三个字符给出。目前支持的
是[p]age flags, [v]ma_flags(都期望 ``unsigned long *`` )和
[g]fp_flags(期望 ``gfp_t *`` )。标志名称和打印顺序取决于特定的类型。
注意,这种格式不应该直接用于跟踪点的:c:func:`TP_printk()` 部分。相反,应使
用 <trace/events/mmflags.h>中的show_*_flags()函数。
通过引用传递。
网络设备特性
------------
::
%pNF 0x000000000000c000
用于打印netdev_features_t。
通过引用传递。
V4L2和DRM FourCC代码(像素格式)
------------------------------
::
%p4cc
打印V4L2或DRM使用的FourCC代码,包括格式端序及其十六进制的数值。
通过引用传递。
例如::
%p4cc BG12 little-endian (0x32314742)
%p4cc Y10 little-endian (0x20303159)
%p4cc NV12 big-endian (0xb231564e)
谢谢
====
如果您添加了其他%p扩展,请在可行的情况下,用一个或多个测试用例扩展<lib/test_printf.c>。
谢谢你的合作和关注。
@@ -0,0 +1,99 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/protection-keys.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
吴想成 Wu XiangCheng <bobwxc@email.cn>
.. _cn_core-api_protection-keys:
============
内存保护密钥
============
用户空间的内存保护密钥(Memory Protection Keys for Userspace,PKU,亦
即PKEYs)是英特尔Skylake(及以后)“可扩展处理器”服务器CPU上的一项功能。
它将在未来的非服务器英特尔处理器和未来的AMD处理器中可用。
对于任何希望测试或使用该功能的人来说,它在亚马逊的EC2 C5实例中是可用的,
并且已知可以在那里使用Ubuntu 17.04镜像运行。
内存保护密钥提供了一种机制来执行基于页面的保护,但在应用程序改变保护域
时不需要修改页表。它的工作原理是在每个页表项中为“保护密钥”分配4个以
前被忽略的位,从而提供16个可能的密钥。
还有一个新的用户可访问寄存器(PKRU),为每个密钥提供两个单独的位(访
问禁止和写入禁止)。作为一个CPU寄存器,PKRU在本质上是线程本地的,可能
会给每个线程提供一套不同于其他线程的保护措施。
有两条新指令(RDPKRU/WRPKRU)用于读取和写入新的寄存器。该功能仅在64位
模式下可用,尽管物理地址扩展页表中理论上有空间。这些权限只在数据访问上
强制执行,对指令获取没有影响。
系统调用
========
有3个系统调用可以直接与pkeys进行交互::
int pkey_alloc(unsigned long flags, unsigned long init_access_rights)
int pkey_free(int pkey);
int pkey_mprotect(unsigned long start, size_t len,
unsigned long prot, int pkey);
在使用一个pkey之前,必须先用pkey_alloc()分配它。一个应用程序直接调用
WRPKRU指令,以改变一个密钥覆盖的内存的访问权限。在这个例子中,WRPKRU
被一个叫做pkey_set()的C函数所封装::
int real_prot = PROT_READ|PROT_WRITE;
pkey = pkey_alloc(0, PKEY_DISABLE_WRITE);
ptr = mmap(NULL, PAGE_SIZE, PROT_NONE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
ret = pkey_mprotect(ptr, PAGE_SIZE, real_prot, pkey);
... application runs here
现在,如果应用程序需要更新'ptr'处的数据,它可以获得访问权,进行更新,
然后取消其写访问权::
pkey_set(pkey, 0); // clear PKEY_DISABLE_WRITE
*ptr = foo; // assign something
pkey_set(pkey, PKEY_DISABLE_WRITE); // set PKEY_DISABLE_WRITE again
现在,当它释放内存时,它也将释放pkey,因为它不再被使用了::
munmap(ptr, PAGE_SIZE);
pkey_free(pkey);
.. note:: pkey_set()是RDPKRU和WRPKRU指令的一个封装器。在tools/testing/selftests/x86/protection_keys.c中可以找到一个实现实例。
tools/testing/selftests/x86/protection_keys.c.
行为
====
内核试图使保护密钥与普通的mprotect()的行为一致。例如,如果你这样做::
mprotect(ptr, size, PROT_NONE);
something(ptr);
这样做的时候,你可以期待保护密钥的相同效果::
pkey = pkey_alloc(0, PKEY_DISABLE_WRITE | PKEY_DISABLE_READ);
pkey_mprotect(ptr, size, PROT_READ|PROT_WRITE, pkey);
something(ptr);
无论something()是否是对'ptr'的直接访问,这都应该为真。
如::
*ptr = foo;
或者当内核代表应用程序进行访问时,比如read()::
read(fd, ptr, 1);
在这两种情况下,内核都会发送一个SIGSEGV,但当违反保护密钥时,si_code
将被设置为SEGV_PKERR,而当违反普通的mprotect()权限时,则是SEGV_ACCERR。
@@ -0,0 +1,391 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/rbtree.rst
:翻译:
唐艺舟 Tang Yizhou <tangyeechou@gmail.com>
=========================
Linux中的红黑树(rbtree)
=========================
:日期: 2007年1月18日
:作者: Rob Landley <rob@landley.net>
何为红黑树,它们有什么用?
--------------------------
红黑树是一种自平衡二叉搜索树,被用来存储可排序的键/值数据对。这与基数树(被用来高效
存储稀疏数组,因此使用长整型下标来插入/访问/删除结点)和哈希表(没有保持排序因而无法
容易地按序遍历,同时必须调节其大小和哈希函数,然而红黑树可以优雅地伸缩以便存储任意
数量的键)不同。
红黑树和AVL树类似,但在插入和删除时提供了更快的实时有界的最坏情况性能(分别最多两次
旋转和三次旋转,来平衡树),查询时间轻微变慢(但时间复杂度仍然是O(log n))。
引用Linux每周新闻(Linux Weekly News):
内核中有多处红黑树的使用案例。最后期限调度器和完全公平排队(CFQ)I/O调度器利用
红黑树跟踪请求;数据包CD/DVD驱动程序也是如此。高精度时钟代码使用一颗红黑树组织
未完成的定时器请求。ext3文件系统用红黑树跟踪目录项。虚拟内存区域(VMAs)、epoll
文件描述符、密码学密钥和在“分层令牌桶”调度器中的网络数据包都由红黑树跟踪。
本文档涵盖了对Linux红黑树实现的使用方法。更多关于红黑树的性质和实现的信息,参见:
Linux每周新闻关于红黑树的文章
https://lwn.net/Articles/184495/
维基百科红黑树词条
https://en.wikipedia.org/wiki/Red-black_tree
红黑树的Linux实现
-----------------
Linux的红黑树实现在文件“lib/rbtree.c”中。要使用它,需要“#include <linux/rbtree.h>”。
Linux的红黑树实现对速度进行了优化,因此比传统的实现少一个间接层(有更好的缓存局部性)。
每个rb_node结构体的实例嵌入在它管理的数据结构中,因此不需要靠指针来分离rb_node和它
管理的数据结构。用户应该编写他们自己的树搜索和插入函数,来调用已提供的红黑树函数,
而不是使用一个比较回调函数指针。加锁代码也留给红黑树的用户编写。
创建一颗红黑树
--------------
红黑树中的数据结点是包含rb_node结构体成员的结构体::
struct mytype {
struct rb_node node;
char *keystring;
};
当处理一个指向内嵌rb_node结构体的指针时,包住rb_node的结构体可用标准的container_of()
宏访问。此外,个体成员可直接用rb_entry(node, type, member)访问。
每颗红黑树的根是一个rb_root数据结构,它由以下方式初始化为空:
struct rb_root mytree = RB_ROOT;
在一颗红黑树中搜索值
--------------------
为你的树写一个搜索函数是相当简单的:从树根开始,比较每个值,然后根据需要继续前往左边或
右边的分支。
示例::
struct mytype *my_search(struct rb_root *root, char *string)
{
struct rb_node *node = root->rb_node;
while (node) {
struct mytype *data = container_of(node, struct mytype, node);
int result;
result = strcmp(string, data->keystring);
if (result < 0)
node = node->rb_left;
else if (result > 0)
node = node->rb_right;
else
return data;
}
return NULL;
}
在一颗红黑树中插入数据
----------------------
在树中插入数据的步骤包括:首先搜索插入新结点的位置,然后插入结点并对树再平衡
("recoloring")。
插入的搜索和上文的搜索不同,它要找到嫁接新结点的位置。新结点也需要一个指向它的父节点
的链接,以达到再平衡的目的。
示例::
int my_insert(struct rb_root *root, struct mytype *data)
{
struct rb_node **new = &(root->rb_node), *parent = NULL;
/* Figure out where to put new node */
while (*new) {
struct mytype *this = container_of(*new, struct mytype, node);
int result = strcmp(data->keystring, this->keystring);
parent = *new;
if (result < 0)
new = &((*new)->rb_left);
else if (result > 0)
new = &((*new)->rb_right);
else
return FALSE;
}
/* Add new node and rebalance tree. */
rb_link_node(&data->node, parent, new);
rb_insert_color(&data->node, root);
return TRUE;
}
在一颗红黑树中删除或替换已经存在的数据
--------------------------------------
若要从树中删除一个已经存在的结点,调用::
void rb_erase(struct rb_node *victim, struct rb_root *tree);
示例::
struct mytype *data = mysearch(&mytree, "walrus");
if (data) {
rb_erase(&data->node, &mytree);
myfree(data);
}
若要用一个新结点替换树中一个已经存在的键值相同的结点,调用::
void rb_replace_node(struct rb_node *old, struct rb_node *new,
struct rb_root *tree);
通过这种方式替换结点不会对树做重排序:如果新结点的键值和旧结点不同,红黑树可能被
破坏。
(按排序的顺序)遍历存储在红黑树中的元素
----------------------------------------
我们提供了四个函数,用于以排序的方式遍历一颗红黑树的内容。这些函数可以在任意红黑树
上工作,并且不需要被修改或包装(除非加锁的目的)::
struct rb_node *rb_first(struct rb_root *tree);
struct rb_node *rb_last(struct rb_root *tree);
struct rb_node *rb_next(struct rb_node *node);
struct rb_node *rb_prev(struct rb_node *node);
要开始迭代,需要使用一个指向树根的指针调用rb_first()或rb_last(),它将返回一个指向
树中第一个或最后一个元素所包含的节点结构的指针。要继续的话,可以在当前结点上调用
rb_next()或rb_prev()来获取下一个或上一个结点。当没有剩余的结点时,将返回NULL。
迭代器函数返回一个指向被嵌入的rb_node结构体的指针,由此,包住rb_node的结构体可用
标准的container_of()宏访问。此外,个体成员可直接用rb_entry(node, type, member)
访问。
示例::
struct rb_node *node;
for (node = rb_first(&mytree); node; node = rb_next(node))
printk("key=%s\n", rb_entry(node, struct mytype, node)->keystring);
带缓存的红黑树
--------------
计算最左边(最小的)结点是二叉搜索树的一个相当常见的任务,例如用于遍历,或用户根据
他们自己的逻辑依赖一个特定的顺序。为此,用户可以使用'struct rb_root_cached'来优化
时间复杂度为O(logN)的rb_first()的调用,以简单地获取指针,避免了潜在的昂贵的树迭代。
维护操作的额外运行时间开销可忽略,不过内存占用较大。
和rb_root结构体类似,带缓存的红黑树由以下方式初始化为空::
struct rb_root_cached mytree = RB_ROOT_CACHED;
带缓存的红黑树只是一个常规的rb_root,加上一个额外的指针来缓存最左边的节点。这使得
rb_root_cached可以存在于rb_root存在的任何地方,并且只需增加几个接口来支持带缓存的
树::
struct rb_node *rb_first_cached(struct rb_root_cached *tree);
void rb_insert_color_cached(struct rb_node *, struct rb_root_cached *, bool);
void rb_erase_cached(struct rb_node *node, struct rb_root_cached *);
操作和删除也有对应的带缓存的树的调用::
void rb_insert_augmented_cached(struct rb_node *node, struct rb_root_cached *,
bool, struct rb_augment_callbacks *);
void rb_erase_augmented_cached(struct rb_node *, struct rb_root_cached *,
struct rb_augment_callbacks *);
对增强型红黑树的支持
--------------------
增强型红黑树是一种在每个结点里存储了“一些”附加数据的红黑树,其中结点N的附加数据
必须是以N为根的子树中所有结点的内容的函数。它是建立在红黑树基础设施之上的可选特性。
想要使用这个特性的红黑树用户,插入和删除结点时必须调用增强型接口并提供增强型回调函数。
实现增强型红黑树操作的C文件必须包含<linux/rbtree_augmented.h>而不是<linux/rbtree.h>。
注意,linux/rbtree_augmented.h暴露了一些红黑树实现的细节而你不应依赖它们,请坚持
使用文档记录的API,并且不要在头文件中包含<linux/rbtree_augmented.h>,以最小化你的
用户意外地依赖这些实现细节的可能。
插入时,用户必须更新通往被插入节点的路径上的增强信息,然后像往常一样调用rb_link_node(),
然后是rb_augment_inserted()而不是平时的rb_insert_color()调用。如果
rb_augment_inserted()再平衡了红黑树,它将回调至一个用户提供的函数来更新受影响的
子树上的增强信息。
删除一个结点时,用户必须调用rb_erase_augmented()而不是rb_erase()。
rb_erase_augmented()回调至一个用户提供的函数来更新受影响的子树上的增强信息。
在两种情况下,回调都是通过rb_augment_callbacks结构体提供的。必须定义3个回调:
- 一个传播回调,它更新一个给定结点和它的祖先们的增强数据,直到一个给定的停止点
(如果是NULL,将更新一路更新到树根)。
- 一个复制回调,它将一颗给定子树的增强数据复制到一个新指定的子树树根。
- 一个树旋转回调,它将一颗给定的子树的增强值复制到新指定的子树树根上,并重新计算
先前的子树树根的增强值。
rb_erase_augmented()编译后的代码可能会内联传播、复制回调,这将导致函数体积更大,
因此每个增强型红黑树的用户应该只有一个rb_erase_augmented()的调用点,以限制编译后
的代码大小。
使用示例
^^^^^^^^
区间树是增强型红黑树的一个例子。参考Cormen,Leiserson,Rivest和Stein写的
《算法导论》。区间树的更多细节:
经典的红黑树只有一个键,它不能直接用来存储像[lo:hi]这样的区间范围,也不能快速查找
与新的lo:hi重叠的部分,或者查找是否有与新的lo:hi完全匹配的部分。
然而,红黑树可以被增强,以一种结构化的方式来存储这种区间范围,从而使高效的查找和
精确匹配成为可能。
这个存储在每个节点中的“额外信息”是其所有后代结点中的最大hi(max_hi)值。这个信息
可以保持在每个结点上,只需查看一下该结点和它的直系子结点们。这将被用于时间复杂度
为O(log n)的最低匹配查找(所有可能的匹配中最低的起始地址),就像这样::
struct interval_tree_node *
interval_tree_first_match(struct rb_root *root,
unsigned long start, unsigned long last)
{
struct interval_tree_node *node;
if (!root->rb_node)
return NULL;
node = rb_entry(root->rb_node, struct interval_tree_node, rb);
while (true) {
if (node->rb.rb_left) {
struct interval_tree_node *left =
rb_entry(node->rb.rb_left,
struct interval_tree_node, rb);
if (left->__subtree_last >= start) {
/*
* Some nodes in left subtree satisfy Cond2.
* Iterate to find the leftmost such node N.
* If it also satisfies Cond1, that's the match
* we are looking for. Otherwise, there is no
* matching interval as nodes to the right of N
* can't satisfy Cond1 either.
*/
node = left;
continue;
}
}
if (node->start <= last) { /* Cond1 */
if (node->last >= start) /* Cond2 */
return node; /* node is leftmost match */
if (node->rb.rb_right) {
node = rb_entry(node->rb.rb_right,
struct interval_tree_node, rb);
if (node->__subtree_last >= start)
continue;
}
}
return NULL; /* No match */
}
}
插入/删除是通过以下增强型回调来定义的::
static inline unsigned long
compute_subtree_last(struct interval_tree_node *node)
{
unsigned long max = node->last, subtree_last;
if (node->rb.rb_left) {
subtree_last = rb_entry(node->rb.rb_left,
struct interval_tree_node, rb)->__subtree_last;
if (max < subtree_last)
max = subtree_last;
}
if (node->rb.rb_right) {
subtree_last = rb_entry(node->rb.rb_right,
struct interval_tree_node, rb)->__subtree_last;
if (max < subtree_last)
max = subtree_last;
}
return max;
}
static void augment_propagate(struct rb_node *rb, struct rb_node *stop)
{
while (rb != stop) {
struct interval_tree_node *node =
rb_entry(rb, struct interval_tree_node, rb);
unsigned long subtree_last = compute_subtree_last(node);
if (node->__subtree_last == subtree_last)
break;
node->__subtree_last = subtree_last;
rb = rb_parent(&node->rb);
}
}
static void augment_copy(struct rb_node *rb_old, struct rb_node *rb_new)
{
struct interval_tree_node *old =
rb_entry(rb_old, struct interval_tree_node, rb);
struct interval_tree_node *new =
rb_entry(rb_new, struct interval_tree_node, rb);
new->__subtree_last = old->__subtree_last;
}
static void augment_rotate(struct rb_node *rb_old, struct rb_node *rb_new)
{
struct interval_tree_node *old =
rb_entry(rb_old, struct interval_tree_node, rb);
struct interval_tree_node *new =
rb_entry(rb_new, struct interval_tree_node, rb);
new->__subtree_last = old->__subtree_last;
old->__subtree_last = compute_subtree_last(old);
}
static const struct rb_augment_callbacks augment_callbacks = {
augment_propagate, augment_copy, augment_rotate
};
void interval_tree_insert(struct interval_tree_node *node,
struct rb_root *root)
{
struct rb_node **link = &root->rb_node, *rb_parent = NULL;
unsigned long start = node->start, last = node->last;
struct interval_tree_node *parent;
while (*link) {
rb_parent = *link;
parent = rb_entry(rb_parent, struct interval_tree_node, rb);
if (parent->__subtree_last < last)
parent->__subtree_last = last;
if (start < parent->start)
link = &parent->rb.rb_left;
else
link = &parent->rb.rb_right;
}
node->__subtree_last = last;
rb_link_node(&node->rb, rb_parent, link);
rb_insert_augmented(&node->rb, root, &augment_callbacks);
}
void interval_tree_remove(struct interval_tree_node *node,
struct rb_root *root)
{
rb_erase_augmented(&node->rb, root, &augment_callbacks);
}

Some files were not shown because too many files have changed in this diff Show More