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,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);
}
@@ -0,0 +1,156 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/refcount-vs-atomic.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_refcount-vs-atomic:
=======================================
与atomic_t相比,refcount_t的API是这样的
=======================================
.. contents:: :local:
简介
====
refcount_t API的目标是为实现对象的引用计数器提供一个最小的API。虽然来自
lib/refcount.c的独立于架构的通用实现在下面使用了原子操作,但一些 ``refcount_*()``
和 ``atomic_*()`` 函数在内存顺序保证方面有很多不同。本文档概述了这些差异,并
提供了相应的例子,以帮助开发者根据这些内存顺序保证的变化来验证他们的代码。
本文档中使用的术语尽量遵循tools/memory-model/Documentation/explanation.txt
中定义的正式LKMM。
memory-barriers.txt和atomic_t.txt提供了更多关于内存顺序的背景,包括通用的
和针对原子操作的。
内存顺序的相关类型
==================
.. note:: 下面的部分只涵盖了本文使用的与原子操作和引用计数器有关的一些内存顺
序类型。如果想了解更广泛的情况,请查阅memory-barriers.txt文件。
在没有任何内存顺序保证的情况下(即完全无序),atomics和refcounters只提供原
子性和程序顺序(program order, po)关系(在同一个CPU上)。它保证每个
``atomic_* ()`` 和 ``refcount_*()`` 操作都是原子性的,指令在单个CPU上按程序
顺序执行。这是用READ_ONCE()/WRITE_ONCE()和比较并交换原语实现的。
强(完全)内存顺序保证在同一CPU上的所有较早加载和存储的指令(所有程序顺序较早
[po-earlier]指令)在执行任何程序顺序较后指令(po-later)之前完成。它还保证
同一CPU上储存的程序优先较早的指令和来自其他CPU传播的指令必须在该CPU执行任何
程序顺序较后指令之前传播到其他CPU(A-累积属性)。这是用smp_mb()实现的。
RELEASE内存顺序保证了在同一CPU上所有较早加载和存储的指令(所有程序顺序较早
指令)在此操作前完成。它还保证同一CPU上储存的程序优先较早的指令和来自其他CPU
传播的指令必须在释放(release)操作之前传播到所有其他CPU(A-累积属性)。这是用
smp_store_release()实现的。
ACQUIRE内存顺序保证了同一CPU上的所有后加载和存储的指令(所有程序顺序较后
指令)在获取(acquire)操作之后完成。它还保证在获取操作执行后,同一CPU上
储存的所有程序顺序较后指令必须传播到所有其他CPU。这是用
smp_acquire__after_ctrl_dep()实现的。
对Refcounters的控制依赖(取决于成功)保证了如果一个对象的引用被成功获得(引用计数
器的增量或增加行为发生了,函数返回true),那么进一步的存储是针对这个操作的命令。对存
储的控制依赖没有使用任何明确的屏障来实现,而是依赖于CPU不对存储进行猜测。这只是
一个单一的CPU关系,对其他CPU不提供任何保证。
函数的比较
==========
情况1) - 非 “读/修改/写”(RMW)操作
------------------------------------
函数变化:
* atomic_set() --> refcount_set()
* atomic_read() --> refcount_read()
内存顺序保证变化:
* none (两者都是完全无序的)
情况2) - 基于增量的操作,不返回任何值
--------------------------------------
函数变化:
* atomic_inc() --> refcount_inc()
* atomic_add() --> refcount_add()
内存顺序保证变化:
* none (两者都是完全无序的)
情况3) - 基于递减的RMW操作,没有返回值
---------------------------------------
函数变化:
* atomic_dec() --> refcount_dec()
内存顺序保证变化:
* 完全无序的 --> RELEASE顺序
情况4) - 基于增量的RMW操作,返回一个值
---------------------------------------
函数变化:
* atomic_inc_not_zero() --> refcount_inc_not_zero()
* 无原子性对应函数 --> refcount_add_not_zero()
内存顺序保证变化:
* 完全有序的 --> 控制依赖于存储的成功
.. note:: 此处 **假设** 了,必要的顺序是作为获得对象指针的结果而提供的。
情况 5) - 基于Dec/Sub递减的通用RMW操作,返回一个值
---------------------------------------------------
函数变化:
* atomic_dec_and_test() --> refcount_dec_and_test()
* atomic_sub_and_test() --> refcount_sub_and_test()
内存顺序保证变化:
* 完全有序的 --> RELEASE顺序 + 成功后ACQUIRE顺序
情况6)其他基于递减的RMW操作,返回一个值
----------------------------------------
函数变化:
* 无原子性对应函数 --> refcount_dec_if_one()
* ``atomic_add_unless(&var, -1, 1)`` --> ``refcount_dec_not_one(&var)``
内存顺序保证变化:
* 完全有序的 --> RELEASE顺序 + 控制依赖
.. note:: atomic_add_unless()只在执行成功时提供完整的顺序。
情况7)--基于锁的RMW
--------------------
函数变化:
* atomic_dec_and_lock() --> refcount_dec_and_lock()
* atomic_dec_and_mutex_lock() --> refcount_dec_and_mutex_lock()
内存顺序保证变化:
* 完全有序 --> RELEASE顺序 + 控制依赖 + 持有
@@ -0,0 +1,133 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/symbol-namespaces.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
.. _cn_symbol-namespaces.rst:
=================================
符号命名空间(Symbol Namespaces)
=================================
本文档描述了如何使用符号命名空间来构造通过EXPORT_SYMBOL()系列宏导出的内核内符号的导出面。
简介
====
符号命名空间已经被引入,作为构造内核内API的导出面的一种手段。它允许子系统维护者将
他们导出的符号划分进独立的命名空间。这对于文档的编写非常有用(想想SUBSYSTEM_DEBUG
命名空间),也可以限制一组符号在内核其他部分的使用。今后,使用导出到命名空间的符号
的模块必须导入命名空间。否则,内核将根据其配置,拒绝加载该模块或警告说缺少
导入。
如何定义符号命名空间
====================
符号可以用不同的方法导出到命名空间。所有这些都在改变 EXPORT_SYMBOL 和与之类似的那些宏
被检测到的方式,以创建 ksymtab 条目。
使用EXPORT_SYMBOL宏
-------------------
除了允许将内核符号导出到内核符号表的宏EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()之外,
这些宏的变体还可以将符号导出到某个命名空间:EXPORT_SYMBOL_NS() 和 EXPORT_SYMBOL_NS_GPL()。
它们需要一个额外的参数:命名空间(the namespace)。请注意,由于宏扩展,该参数需
要是一个预处理器符号。例如,要把符号 ``usb_stor_suspend`` 导出到命名空间 ``USB_STORAGE``,
请使用::
EXPORT_SYMBOL_NS(usb_stor_suspend, "USB_STORAGE");
相应的 ksymtab 条目结构体 ``kernel_symbol`` 将有相应的成员 ``命名空间`` 集。
导出时未指明命名空间的符号将指向 ``NULL`` 。如果没有定义命名空间,则默认没有。
``modpost`` 和kernel/module/main.c分别在构建时或模块加载时使用名称空间。
使用DEFAULT_SYMBOL_NAMESPACE定义
--------------------------------
为一个子系统的所有符号定义命名空间可能会非常冗长,并可能变得难以维护。因此,我
们提供了一个默认定义(DEFAULT_SYMBOL_NAMESPACE),如果设置了这个定义, 它将成
为所有没有指定命名空间的 EXPORT_SYMBOL() 和 EXPORT_SYMBOL_GPL() 宏扩展的默认
定义。
有多种方法来指定这个定义,使用哪种方法取决于子系统和维护者的喜好。第一种方法是在
子系统的 ``Makefile`` 中定义默认命名空间。例如,如果要将usb-common中定义的所有符号导
出到USB_COMMON命名空间,可以在drivers/usb/common/Makefile中添加这样一行::
ccflags-y += -DDEFAULT_SYMBOL_NAMESPACE='"USB_COMMON"'
这将影响所有 EXPORT_SYMBOL() 和 EXPORT_SYMBOL_GPL() 语句。当这个定义存在时,
用EXPORT_SYMBOL_NS()导出的符号仍然会被导出到作为命名空间参数传递的命名空间中,
因为这个参数优先于默认的符号命名空间。
定义默认命名空间的第二个选项是直接在编译单元中作为预处理声明。上面的例子就会变
成::
#undef DEFAULT_SYMBOL_NAMESPACE
#define DEFAULT_SYMBOL_NAMESPACE "USB_COMMON"
应置于相关编译单元中任何 EXPORT_SYMBOL 宏之前
如何使用命名空间中导出的符号
============================
为了使用被导出到命名空间的符号,内核模块需要明确地导入这些命名空间。
否则内核可能会拒绝加载该模块。模块代码需要使用宏MODULE_IMPORT_NS来
表示它所使用的命名空间的符号。例如,一个使用usb_stor_suspend符号的
模块,需要使用如下语句导入命名空间USB_STORAGE::
MODULE_IMPORT_NS("USB_STORAGE");
这将在模块中为每个导入的命名空间创建一个 ``modinfo`` 标签。这也顺带
使得可以用modinfo检查模块已导入的命名空间::
$ modinfo drivers/usb/storage/ums-karma.ko
[...]
import_ns: USB_STORAGE
[...]
建议将 MODULE_IMPORT_NS() 语句添加到靠近其他模块元数据定义的地方,
如 MODULE_AUTHOR() 或 MODULE_LICENSE() 。
加载使用命名空间符号的模块
==========================
在模块加载时(比如 ``insmod`` ),内核将检查每个从模块中引用的符号是否可
用,以及它可能被导出到的名字空间是否被模块导入。内核的默认行为是拒绝
加载那些没有指明足以导入的模块。此错误会被记录下来,并且加载将以
EINVAL方式失败。要允许加载不满足这个前提条件的模块,可以使用此配置选项:
设置 MODULE_ALLOW_MISSING_NAMESPACE_IMPORTS=y 将使加载不受影响,但会
发出警告。
自动创建MODULE_IMPORT_NS声明
============================
缺少命名空间的导入可以在构建时很容易被检测到。事实上,如果一个模块
使用了一个命名空间的符号而没有导入它,modpost会发出警告。
MODULE_IMPORT_NS()语句通常会被添加到一个明确的位置(和其他模块元
数据一起)。为了使模块作者(和子系统维护者)的生活更加轻松,我们提
供了一个脚本和make目标来修复丢失的导入。修复丢失的导入可以用::
$ make nsdeps
对模块作者来说,以下情况可能很典型::
- 编写依赖未导入命名空间的符号的代码
- ``make``
- 注意 ``modpost`` 的警告,提醒你有一个丢失的导入。
- 运行 ``make nsdeps``将导入添加到正确的代码位置。
对于引入命名空间的子系统维护者来说,其步骤非常相似。同样,make nsdeps最终将
为树内模块添加缺失的命名空间导入::
- 向命名空间转移或添加符号(例如,使用EXPORT_SYMBOL_NS())。
- `make e`(最好是用allmodconfig来覆盖所有的内核模块)。
- 注意 ``modpost`` 的警告,提醒你有一个丢失的导入。
- 运行 ``maknsdeps``将导入添加到正确的代码位置。
你也可以为外部模块的构建运行nsdeps。典型的用法是::
$ make -C <path_to_kernel_src> M=$PWD nsdeps
@@ -0,0 +1,285 @@
.. SPDX-License-Identifier: GPL-2.0+
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/this_cpu_ops.rst
:翻译:
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
:校译:
吴想成 Wu Xiangcheng <bobwxc@email.cn>
============
this_cpu操作
============
:作者: Christoph Lameter, 2014年8月4日
:作者: Pranith Kumar, 2014年8月2日
this_cpu操作是一种优化访问与当前执行处理器相关的每CPU变量的方法。这是通过使用段寄
存器(或专用寄存器,cpu在其中永久存储特定处理器的每CPU区域的起始)来完成的。
this_cpu操作将每CPU变量的偏移量添加到处理器特定的每CPU基址上,并将该操作编码到对
每CPU变量进行操作的指令中。
这意味着在偏移量的计算和对数据的操作之间不存在原子性问题。因此,没有必要禁用抢占
或中断来确保处理器在计算地址和数据操作之间不被改变。
读取-修改-写入操作特别值得关注。通常处理器具有特殊的低延迟指令,可以在没有典型同
步开销的情况下运行,但仍提供某种宽松的原子性保证。例如,x86可以执行RMW(读取,
修改,写入)指令,如同inc/dec/cmpxchg,而无需锁前缀和相关的延迟损失。
对没有锁前缀的变量的访问是不同步的,也不需要同步,因为我们处理的是当前执行的处理
器所特有的每CPU数据。只有当前的处理器可以访问该变量,因此系统中的其他处理器不存在
并发性问题。
请注意,远程处理器对每CPU区域的访问是特殊情况,可能会影响通过 ``this_cpu_*`` 的本
地RMW操作的性能和正确性(远程写操作)。
this_cpu操作的主要用途是优化计数器操作。
定义了以下具有隐含抢占保护的this_cpu()操作。可以使用这些操作而不用担心抢占和中断::
this_cpu_read(pcp)
this_cpu_write(pcp, val)
this_cpu_add(pcp, val)
this_cpu_and(pcp, val)
this_cpu_or(pcp, val)
this_cpu_add_return(pcp, val)
this_cpu_xchg(pcp, nval)
this_cpu_cmpxchg(pcp, oval, nval)
this_cpu_cmpxchg_double(pcp1, pcp2, oval1, oval2, nval1, nval2)
this_cpu_sub(pcp, val)
this_cpu_inc(pcp)
this_cpu_dec(pcp)
this_cpu_sub_return(pcp, val)
this_cpu_inc_return(pcp)
this_cpu_dec_return(pcp)
this_cpu操作的内部工作
----------------------
在x86上,fs:或gs:段寄存器包含每CPU区域的基址。这样就可以简单地使用段覆盖,将每CPU
相对地址重定位到处理器适当的每CPU区域。所以对每CPU基址的重定位是通过段寄存器前缀
在指令中编码完成的。
例如::
DEFINE_PER_CPU(int, x);
int z;
z = this_cpu_read(x);
产生的单指令为::
mov ax, gs:[x]
而不是像每CPU操作那样,先是一系列的地址计算,然后从该地址获取。在this_cpu_ops之前,
这样的序列还需要先禁用/启用抢占功能,以防止内核在计算过程中将线程移动到不同的处理
器上。
请思考下面this_cpu操作::
this_cpu_inc(x)
这将产生如下单指令(无锁前缀!)::
inc gs:[x]
而不是在没有段寄存器的情况下所需要的以下操作::
int *y;
int cpu;
cpu = get_cpu();
y = per_cpu_ptr(&x, cpu);
(*y)++;
put_cpu();
请注意,这些操作只能用于为特定处理器保留的每CPU数据。如果不在上下文代码中禁用抢占,
``this_cpu_inc()`` 将仅保证每CPU的某一个计数器被正确地递增,但不能保证操作系统不
会在this_cpu指令执行的前后直接移动该进程。一般来说,这意味着每个处理器的单个计数
器的值是没有意义的。所有每CPU计数器的总和才是唯一有意义的值。
每CPU变量的使用是出于性能的考虑。如果多个处理器同时处理相同的代码路径,可以避免缓
存行跳转。每个处理器都有自己的每CPU变量,因此不会发生并发缓存行更新。为这种优化必
须付出的代价是,当需要计数器的值时要将每CPU计数器相加。
特殊的操作
----------
::
y = this_cpu_ptr(&x)
使用每CPU变量的偏移量(&x!),并返回属于当前执行处理器的每CPU变量的地址。
``this_cpu_ptr`` 避免了通用 ``get_cpu``/``put_cpu`` 序列所需的多个步骤。没有可用
的处理器编号。相反,本地每CPU区域的偏移量只是简单地添加到每CPU偏移量上。
请注意,这个操作通常是在抢占被禁用后再在代码段中使用。然后该指针用来访问临界区中
的本地每CPU数据。当重新启用抢占时,此指针通常不再有用,因为它可能不再指向当前处理
器的每CPU数据。
每CPU变量和偏移量
-----------------
每CPU变量相对于每CPU区域的起始点是有偏移的。它们没有地址,尽管代码里看起来像有一
样。不能直接对偏移量解引用,必须用处理器每CPU区域基指针加上偏移量,以构成有效地址。
因此,在每CPU操作的上下文之外使用x或&x是无效的,这种行为通常会被当作一个空指针的
解引用来处理。
::
DEFINE_PER_CPU(int, x);
在每CPU操作的上下文中,上面表达式说明x是一个每CPU变量。大多数this_cpu操作都需要一
个cpu变量。
::
int __percpu *p = &x;
&x和p是每CPU变量的偏移量。 ``this_cpu_ptr()`` 使用每CPU变量的偏移量,这让它看起来
有点奇怪。
每CPU结构体字段的操作
---------------------
假设我们有一个每CPU结构::
struct s {
int n,m;
};
DEFINE_PER_CPU(struct s, p);
这些字段的操作非常简单::
this_cpu_inc(p.m)
z = this_cpu_cmpxchg(p.m, 0, 1);
如果我们有一个相对于结构体s的偏移量::
struct s __percpu *ps = &p;
this_cpu_dec(ps->m);
z = this_cpu_inc_return(ps->n);
如果我们后面不使用 ``this_cpu ops`` 来操作字段,则指针的计算可能需要使用
``this_cpu_ptr()``::
struct s *pp;
pp = this_cpu_ptr(&p);
pp->m--;
z = pp->n++;
this_cpu ops的变体
------------------
this_cpu的操作是中断安全的。一些架构不支持这些每CPU的本地操作。在这种情况下,该操
作必须被禁用中断的代码所取代,然后做那些保证是原子的操作,再重新启用中断。当然这
样做是很昂贵的。如果有其他原因导致调度器不能改变我们正在执行的处理器,那么就没有
理由禁用中断了。为此,我们提供了以下__this_cpu操作。
这些操作不能保证并发中断或抢占。如果在中断上下文中不使用每CPU变量并且调度程序无法
抢占,那么它们是安全的。如果在操作进行时仍有中断发生,并且中断也修改了变量,则无
法保证RMW操作是安全的::
__this_cpu_read(pcp)
__this_cpu_write(pcp, val)
__this_cpu_add(pcp, val)
__this_cpu_and(pcp, val)
__this_cpu_or(pcp, val)
__this_cpu_add_return(pcp, val)
__this_cpu_xchg(pcp, nval)
__this_cpu_cmpxchg(pcp, oval, nval)
__this_cpu_cmpxchg_double(pcp1, pcp2, oval1, oval2, nval1, nval2)
__this_cpu_sub(pcp, val)
__this_cpu_inc(pcp)
__this_cpu_dec(pcp)
__this_cpu_sub_return(pcp, val)
__this_cpu_inc_return(pcp)
__this_cpu_dec_return(pcp)
将增加x,并且不会回退到在无法通过地址重定位和同一指令中的读取-修改-写入操作实现原
子性的平台上禁用中断的代码。
&this_cpu_ptr(pp)->n 对比 this_cpu_ptr(&pp->n)
----------------------------------------------
第一个操作使用偏移量并形成一个地址,然后再加上n字段的偏移量。这可能会导致编译器产
生两条加法指令。
第二个操作先加上两个偏移量,然后进行重定位。恕我直言,第二种形式看起来更干净,而
且更容易与 ``()`` 结合。第二种形式也与 ``this_cpu_read()`` 和大家的使用方式一致。
远程访问每CPU数据
-----------------
每CPU数据结构被设计为由一个CPU独占使用。如果您按预期使用变量,则 ``this_cpu_ops()``
保证是 ``原子的`` ,因为没有其他CPU可以访问这些数据结构。
在某些特殊情况下,您可能需要远程访问每CPU数据结构。通常情况下,进行远程读访问是安
全的,这经常是为了统计计数器值。远程写访问可能会出现问题,因为this_cpu操作没有锁
语义。远程写可能会干扰this_cpu RMW操作。
除非绝对必要,否则强烈建议不要对每CPU数据结构进行远程写访问。请考虑使用IPI来唤醒
远程CPU,并对其每CPU区域进行更新。
要远程访问每CPU数据结构,通常使用 ``per_cpu_ptr()`` 函数::
DEFINE_PER_CPU(struct data, datap);
struct data *p = per_cpu_ptr(&datap, cpu);
这清楚地表明,我们正准备远程访问每CPU区域。
您还可以执行以下操作以将datap偏移量转换为地址::
struct data *p = this_cpu_ptr(&datap);
但是,将通过this_cpu_ptr计算的指针传递给其他cpu是不寻常的,应该避免。
远程访问通常只用于读取另一个cpu的每CPU数据状态。由于this_cpu操作宽松的同步要求,
写访问可能会导致奇特的问题。
下面的情况说明了写入操作的一些问题,由于两个每CPU变量共享一个缓存行,但宽松的同步
仅应用于更新缓存行的一个进程。
考虑以下示例::
struct test {
atomic_t a;
int b;
};
DEFINE_PER_CPU(struct test, onecacheline);
如果一个处理器远程更新字段 ``a`` ,而本地处理器将使用this_cpu ops来更新字段 ``b`` ,
会发生什么情况,这一点值得注意。应避免在同一缓存行内同时访问数据。此外,可能还需
要进行代价高昂的同步。在这种情况下,通常建议使用IPI,而不是远程写入另一个处理器的
每CPU区域。
即使在远程写很少的情况下,请记住远程写将从最有可能访问它的处理器中逐出缓存行。如
果处理器唤醒时发现每CPU区域缺少本地缓存行,其性能和唤醒时间将受到影响。
@@ -0,0 +1,229 @@
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/unaligned-memory-access.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
:校译:
时奎亮 <alexs@kernel.org>
.. _cn_core-api_unaligned-memory-access:
==============
非对齐内存访问
==============
:作者: Daniel Drake <dsd@gentoo.org>,
:作者: Johannes Berg <johannes@sipsolutions.net>
:感谢他们的帮助: Alan Cox, Avuton Olrich, Heikki Orsila, Jan Engelhardt,
Kyle McMartin, Kyle Moffett, Randy Dunlap, Robert Hancock, Uli Kunitz,
Vadim Lobanov
Linux运行在各种各样的架构上,这些架构在内存访问方面有不同的表现。本文介绍了一些
关于不对齐访问的细节,为什么你需要编写不引起不对齐访问的代码,以及如何编写这样的
代码
非对齐访问的定义
================
当你试图从一个不被N偶数整除的地址(即addr % N != 0)开始读取N字节的数据时,就
会发生无对齐内存访问。例如,从地址0x10004读取4个字节的数据是可以的,但从地址
0x10005读取4个字节的数据将是一个不对齐的内存访问。
上述内容可能看起来有点模糊,因为内存访问可以以不同的方式发生。这里的背景是在机器
码层面上:某些指令在内存中读取或写入一些字节(例如x86汇编中的movb、movw、movl)。
正如将变得清晰的那样,相对容易发现那些将编译为多字节内存访问指令的C语句,即在处理
u16、u32和u64等类型时。
自然对齐
========
上面提到的规则构成了我们所说的自然对齐。当访问N个字节的内存时,基础内存地址必须被
N平均分割,即addr % N == 0。
在编写代码时,假设目标架构有自然对齐的要求。
在现实中,只有少数架构在所有大小的内存访问上都要求自然对齐。然而,我们必须考虑所
有支持的架构;编写满足自然对齐要求的代码是实现完全可移植性的最简单方法。
为什么非对齐访问时坏事
======================
执行非对齐内存访问的效果因架构不同而不同。在这里写一整篇关于这些差异的文档是很容
易的;下面是对常见情况的总结:
- 一些架构能够透明地执行非对齐内存访问,但通常会有很大的性能代价。
- 当不对齐的访问发生时,一些架构会引发处理器异常。异常处理程序能够纠正不对齐的
访问,但要付出很大的性能代价。
- 一些架构在发生不对齐访问时,会引发处理器异常,但异常中并没有包含足够的信息来
纠正不对齐访问。
- 有些架构不能进行无对齐内存访问,但会默默地执行与请求不同的内存访问,从而导致
难以发现的微妙的代码错误!
从上文可以看出,如果你的代码导致不对齐的内存访问发生,那么你的代码在某些平台上将无
法正常工作,在其他平台上将导致性能问题。
不会导致非对齐访问的代码
========================
起初,上面的概念似乎有点难以与实际编码实践联系起来。毕竟,你对某些变量的内存地址没
有很大的控制权,等等。
幸运的是事情并不复杂,因为在大多数情况下,编译器会确保代码工作正常。例如,以下面的
结构体为例::
struct foo {
u16 field1;
u32 field2;
u8 field3;
};
让我们假设上述结构体的一个实例驻留在从地址0x10000开始的内存中。根据基本的理解,访问
field2会导致非对齐访问,这并不是不合理的。你会期望field2位于该结构体的2个字节的偏移
量,即地址0x10002,但该地址不能被4平均整除(注意,我们在这里读一个4字节的值)。
幸运的是,编译器理解对齐约束,所以在上述情况下,它会在field1和field2之间插入2个字节
的填充。因此,对于标准的结构体类型,你总是可以依靠编译器来填充结构体,以便对字段的访
问可以适当地对齐(假设你没有将字段定义不同长度的类型)。
同样,你也可以依靠编译器根据变量类型的大小,将变量和函数参数对齐到一个自然对齐的方案。
在这一点上,应该很清楚,访问单个字节(u8或char)永远不会导致无对齐访问,因为所有的内
存地址都可以被1均匀地整除。
在一个相关的话题上,考虑到上述因素,你可以观察到,你可以对结构体中的字段进行重新排序,
以便将字段放在不重排就会插入填充物的地方,从而减少结构体实例的整体常驻内存大小。上述
例子的最佳布局是::
struct foo {
u32 field2;
u16 field1;
u8 field3;
};
对于一个自然对齐方案,编译器只需要在结构的末尾添加一个字节的填充。添加这种填充是为了满
足这些结构的数组的对齐约束。
另一点值得一提的是在结构体类型上使用__attribute__((packed))。这个GCC特有的属性告诉编
译器永远不要在结构体中插入任何填充,当你想用C结构体来表示一些“off the wire”的固定排列
的数据时,这个属性很有用。
你可能会倾向于认为,在访问不满足架构对齐要求的字段时,使用这个属性很容易导致不对齐的访
问。然而,编译器也意识到了对齐的限制,并且会产生额外的指令来执行内存访问,以避免造成不
对齐的访问。当然,与non-packed的情况相比,额外的指令显然会造成性能上的损失,所以packed
属性应该只在避免结构填充很重要的时候使用。
导致非对齐访问的代码
====================
考虑到上述情况,让我们来看看一个现实生活中可能导致非对齐内存访问的函数的例子。下面这个
函数取自include/linux/etherdevice.h,是一个优化的例程,用于比较两个以太网MAC地址是否
相等::
bool ether_addr_equal(const u8 *addr1, const u8 *addr2)
{
#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
u32 fold = ((*(const u32 *)addr1) ^ (*(const u32 *)addr2)) |
((*(const u16 *)(addr1 + 4)) ^ (*(const u16 *)(addr2 + 4)));
return fold == 0;
#else
const u16 *a = (const u16 *)addr1;
const u16 *b = (const u16 *)addr2;
return ((a[0] ^ b[0]) | (a[1] ^ b[1]) | (a[2] ^ b[2])) == 0;
#endif
}
在上述函数中,当硬件具有高效的非对齐访问能力时,这段代码没有问题。但是当硬件不能在任意
边界上访问内存时,对a[0]的引用导致从地址addr1开始的2个字节(16位)被读取。
想一想,如果addr1是一个奇怪的地址,如0x10003,会发生什么?(提示:这将是一个非对齐访
问。)
尽管上述函数存在潜在的非对齐访问问题,但它还是被包含在内核中,但被理解为只在16位对齐
的地址上正常工作。调用者应该确保这种对齐方式或者根本不使用这个函数。这个不对齐的函数
仍然是有用的,因为它是在你能确保对齐的情况下的一个很好的优化,这在以太网网络环境中几
乎是一直如此。
下面是另一个可能导致非对齐访问的代码的例子::
void myfunc(u8 *data, u32 value)
{
[...]
*((u32 *) data) = cpu_to_le32(value);
[...]
}
每当数据参数指向的地址不被4均匀整除时,这段代码就会导致非对齐访问。
综上所述,你可能遇到非对齐访问问题的两种主要情况包括:
1. 将变量定义不同长度的类型
2. 指针运算后访问至少2个字节的数据
避免非对齐访问
==============
避免非对齐访问的最简单方法是使用<linux/unaligned.h>头文件提供的get_unaligned()和
put_unaligned()宏。
回到前面的一个可能导致非对齐访问的代码例子::
void myfunc(u8 *data, u32 value)
{
[...]
*((u32 *) data) = cpu_to_le32(value);
[...]
}
为了避免非对齐的内存访问,你可以将其改写如下::
void myfunc(u8 *data, u32 value)
{
[...]
value = cpu_to_le32(value);
put_unaligned(value, (u32 *) data);
[...]
}
get_unaligned()宏的工作原理与此类似。假设'data'是一个指向内存的指针,并且你希望避免
非对齐访问,其用法如下::
u32 value = get_unaligned((u32 *) data);
这些宏适用于任何长度的内存访问(不仅仅是上面例子中的32位)。请注意,与标准的对齐内存
访问相比,使用这些宏来访问非对齐内存可能会在性能上付出代价。
如果使用这些宏不方便,另一个选择是使用memcpy(),其中源或目标(或两者)的类型为u8*或
非对齐char*。由于这种操作的字节性质,避免了非对齐访问。
对齐 vs. 网络
=============
在需要对齐负载的架构上,网络要求IP头在四字节边界上对齐,以优化IP栈。对于普通的以太网
硬件,常数NET_IP_ALIGN被使用。在大多数架构上,这个常数的值是2,因为正常的以太网头是
14个字节,所以为了获得适当的对齐,需要DMA到一个可以表示为4*n+2的地址。一个值得注意的
例外是powerpc,它将NET_IP_ALIGN定义为0,因为DMA到未对齐的地址可能非常昂贵,与未对齐
的负载的成本相比相形见绌。
对于一些不能DMA到未对齐地址的以太网硬件,如4*n+2或非以太网硬件,这可能是一个问题,这
时需要将传入的帧复制到一个对齐的缓冲区。因为这在可以进行非对齐访问的架构上是不必要的,
所以可以使代码依赖于CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS,像这样::
#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
skb = original skb
#else
skb = copy skb
#endif
@@ -0,0 +1,92 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/union_find.rst
=============================
Linux中的并查集(Union-Find)
=============================
:日期: 2024年6月21日
:作者: Xavier <xavier_qy@163.com>
何为并查集,它有什么用?
------------------------
并查集是一种数据结构,用于处理一些不交集的合并及查询问题。并查集支持的主要操作:
初始化:将每个元素初始化为单独的集合,每个集合的初始父节点指向自身。
查询:查询某个元素属于哪个集合,通常是返回集合中的一个“代表元素”。这个操作是为
了判断两个元素是否在同一个集合之中。
合并:将两个集合合并为一个。
并查集作为一种用于维护集合(组)的数据结构,它通常用于解决一些离线查询、动态连通性和
图论等相关问题,同时也是用于计算最小生成树的克鲁斯克尔算法中的关键,由于最小生成树在
网络路由等场景下十分重要,并查集也得到了广泛的引用。此外,并查集在符号计算,寄存器分
配等方面也有应用。
空间复杂度: O(n),n为节点数。
时间复杂度:使用路径压缩可以减少查找操作的时间复杂度,使用按秩合并可以减少合并操作的
时间复杂度,使得并查集每个查询和合并操作的平均时间复杂度仅为O(α(n)),其中α(n)是反阿
克曼函数,可以粗略地认为并查集的操作有常数的时间复杂度。
本文档涵盖了对Linux并查集实现的使用方法。更多关于并查集的性质和实现的信息,参见:
维基百科并查集词条
https://en.wikipedia.org/wiki/Disjoint-set_data_structure
并查集的Linux实现
------------------
Linux的并查集实现在文件“lib/union_find.c”中。要使用它,需要
“#include <linux/union_find.h>”。
并查集的数据结构定义如下::
struct uf_node {
struct uf_node *parent;
unsigned int rank;
};
其中parent为当前节点的父节点,rank为当前树的高度,在合并时将rank小的节点接到rank大
的节点下面以增加平衡性。
初始化并查集
-------------
可以采用静态或初始化接口完成初始化操作。初始化时,parent 指针指向自身,rank 设置
为 0。
示例::
struct uf_node my_node = UF_INIT_NODE(my_node);
或
uf_node_init(&my_node);
查找并查集的根节点
------------------
主要用于判断两个并查集是否属于一个集合,如果根相同,那么他们就是一个集合。在查找过程中
会对路径进行压缩,提高后续查找效率。
示例::
int connected;
struct uf_node *root1 = uf_find(&node_1);
struct uf_node *root2 = uf_find(&node_2);
if (root1 == root2)
connected = 1;
else
connected = 0;
合并两个并查集
--------------
对于两个相交的并查集进行合并,会首先查找它们各自的根节点,然后根据根节点秩大小,将小的
节点连接到大的节点下面。
示例::
uf_union(&node_1, &node_2);
@@ -0,0 +1,313 @@
.. SPDX-License-Identifier: GPL-2.0+
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/watch_queue.rst
:翻译:
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
:校译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
吴想成 Wu Xiangcheng <bobwxc@email.cn>
============
通用通知机制
============
通用通知机制是建立在标准管道驱动之上的,它可以有效地将来自内核的通知消息拼接到用
户空间打开的管道中。这可以与以下方面结合使用::
* Key/keyring 通知
通知缓冲区可以通过以下方式启用:
“General setup”/“General notification queue”
(CONFIG_WATCH_QUEUE)
文档包含以下章节:
.. contents:: :local:
概述
====
该设施以一种特殊模式打开的管道形式出现,管道的内部环形缓冲区用于保存内核生成的消
息。然后通过read()读出这些消息。在此类管道上禁用拼接以及类似的操作,因为它们希望
在某些情况下将其添加的内容还原到环中-这可能最终会与通知消息重叠。
管道的所有者必须告诉内核它想通过该管道观察哪些源。只有连接到该管道上的源才会将消
息插入其中。请注意,一个源可能绑定到多个管道,并同时将消息插入到所有管道中。
还可以将过滤器放置在管道上,以便在不感兴趣时可以忽略某些源类型和子事件。
如果环中没有可用的插槽,或者没有预分配的消息缓冲区可用,则将丢弃消息。在这两种情
况下,read()都会在读取缓冲区中当前的最后一条消息后,将WATCH_META_LOSS_NOTIFICATION
插入到输出缓冲区中。
请注意,当生成一个通知时,内核不会等待消费者收集它,而是继续执行。这意味着可以在
持有自旋锁的同时生成通知,并且还可以保护内核不被用户空间故障无限期地阻碍。
消息结构
========
通知消息由一个简短的头部开始::
struct watch_notification {
__u32 type:24;
__u32 subtype:8;
__u32 info;
};
“type”表示通知记录的来源,“subtype”表示该来源的记录类型(见下文观测源章节)。该类
型也可以是“WATCH_TYPE_META”。这是一个由观测队列本身在内部生成的特殊记录类型。有两
个子类型:
* WATCH_META_REMOVAL_NOTIFICATION
* WATCH_META_LOSS_NOTIFICATION
第一个表示安装了观察的对象已被删除或销毁,第二个表示某些消息已丢失。
“info”表示一系列东西,包括:
* 消息的长度,以字节为单位,包括头(带有WATCH_INFO_LENGTH的掩码,并按
WATCH_INFO_LENGTH__SHIFT移位)。这表示记录的大小,可能在8到127字节之间。
* 观测ID(带有WATCH_INFO_ID掩码,并按WATCH_INFO_ID__SHIFT移位)。这表示观测的主
叫ID,可能在0到255之间。多个观测组可以共享一个队列,这提供了一种区分它们的方法。
* 特定类型的字段(WATCH_INFO_TYPE_INFO)。这是由通知生产者设置的,以指示类型和
子类型的某些特定含义。
除长度外,信息中的所有内容都可以用于过滤。
头部后面可以有补充信息。此格式是由类型和子类型决定的。
观测列表(通知源)API
=====================
“观测列表“是订阅通知源的观测者的列表。列表可以附加到对象(比如键或超级块),也可
以是全局的(比如对于设备事件)。从用户空间的角度来看,一个非全局的观测列表通常是
通过引用它所属的对象来引用的(比如使用KEYCTL_NOTIFY并给它一个密钥序列号来观测特定
的密钥)。
为了管理观测列表,提供了以下函数:
* ::
void init_watch_list(struct watch_list *wlist,
void (*release_watch)(struct watch *wlist));
初始化一个观测列表。 如果 ``release_watch`` 不是NULL,那么这表示当watch_list对
象被销毁时,应该调用函数来丢弃观测列表对被观测对象的任何引用。
* ``void remove_watch_list(struct watch_list *wlist);``
这将删除订阅watch_list的所有观测,并释放它们,然后销毁watch_list对象本身。
观测队列(通知输出)API
=======================
“观测队列”是由应用程序分配的用以记录通知的缓冲区,其工作原理完全隐藏在管道设备驱
动中,但必须获得对它的引用才能设置观测。可以通过以下方式进行管理:
* ``struct watch_queue *get_watch_queue(int fd);``
由于观测队列在内核中通过实现缓冲区的管道的文件描述符表示,用户空间必须通过系
统调用传递该文件描述符,这可以用于从系统调用中查找指向观测队列的不透明指针。
* ``void put_watch_queue(struct watch_queue *wqueue);``
该函数用以丢弃从 ``get_watch_queue()`` 获得的引用。
观测订阅API
===========
“观测”是观测列表上的订阅,表示观测队列,从而表示应写入通知记录的缓冲区。观测队列
对象还可以携带该对象的过滤规则,由用户空间设置。watch结构体的某些部分可以由驱动程
序设置::
struct watch {
union {
u32 info_id; /* 在info字段中进行OR运算的ID */
...
};
void *private; /* 被观测对象的私有数据 */
u64 id; /* 内部标识符 */
...
};
``info_id`` 值是从用户空间获得并按WATCH_INFO_ID__SHIFT移位的8位数字。当通知写入关
联的观测队列缓冲区时,这将与struct watch_notification::info的WATCH_INFO_ID字段进
行或运算。
``private`` 字段是与watch_list相关联的驱动程序数据,并由 ``watch_list::release_watch()``
函数清除。
``id`` 字段是源的ID。使用不同ID发布的通知将被忽略。
提供以下函数来管理观测:
* ``void init_watch(struct watch *watch, struct watch_queue *wqueue);``
初始化一个观测对象,把它的指针设置到观察队列中,使用适当的限制来避免死锁。
* ``int add_watch_to_object(struct watch *watch, struct watch_list *wlist);``
将观测订阅到观测列表(通知源)。watch结构体中的driver-settable字段必须在调用
它之前设置。
* ::
int remove_watch_from_object(struct watch_list *wlist,
struct watch_queue *wqueue,
u64 id, false);
从观测列表中删除一个观测,该观测必须与指定的观测队列(``wqueue``)和对象标识
符(``id``)匹配。通知(``WATCH_META_REMOVAL_NOTIFICATION``)被发送到观测队列
表示该观测已被删除。
* ``int remove_watch_from_object(struct watch_list *wlist, NULL, 0, true);``
从观测列表中删除所有观测。预计这将被称为销毁前的准备工作,届时新的观测将无法
访问观测列表。通知(``WATCH_META_REMOVAL_NOTIFICATION``)被发送到每个订阅观测
的观测队列,以表明该观测已被删除。
通知发布API
===========
要将通知发布到观测列表以便订阅的观测可以看到,应使用以下函数::
void post_watch_notification(struct watch_list *wlist,
struct watch_notification *n,
const struct cred *cred,
u64 id);
应预先设置通知格式,并应传入一个指向头部(``n``)的指针。通知可能大于此值,并且缓
冲槽为单位的大小在 ``n->info & WATCH_INFO_LENGTH`` 中注明。
``cred`` 结构体表示源(对象)的证书,并传递给LSM,例如SELinux,以允许或禁止根据该队
列(对象)的证书在每个单独队列中记录注释。
``id`` 是源对象ID(如密钥上的序列号)。只有设置相同ID的观测才能看到这个通知。
观测源
======
任何特定的缓冲区都可以从多个源获取信息。 这些源包括:
* WATCH_TYPE_KEY_NOTIFY
这种类型的通知表示密钥和密钥环的变化,包括密钥环内容或密钥属性的变化。
更多信息请参见Documentation/security/keys/core.rst。
事件过滤
========
当创建观测队列后,我们可以应用一组过滤器以限制接收的事件::
struct watch_notification_filter filter = {
...
};
ioctl(fd, IOC_WATCH_QUEUE_SET_FILTER, &filter)
过滤器的描述的类型变量是::
struct watch_notification_filter {
__u32 nr_filters;
__u32 __reserved;
struct watch_notification_type_filter filters[];
};
其中“nr_filters”表示filters[]数组中过滤器的数量,而“__reserved”应为0。
“filter”数组有以下类型的元素::
struct watch_notification_type_filter {
__u32 type;
__u32 info_filter;
__u32 info_mask;
__u32 subtype_filter[8];
};
其中:
* ``type`` 是过滤的事件类型,应类似于“WATCH_TYPE_KEY_NOTIFY”。
* ``info_filter`` 与 ``info_mask`` 充当通知记录的信息字段的过滤器,只有在以下情
况,通知才会写入缓冲区::
(watch.info & info_mask) == info_filter
例如,这可以用于忽略不在一个挂载树上的观测点的事件。
* ``subtype_filter`` 是一个位掩码,表示感兴趣的子类型。subtype_filter[0]的
bit[0]对应子类型0,bit[1]对应子类型1,以此类推。
若ioctl()的参数为NULL,则过滤器将被移除,并且来自观测源的所有事件都将通过。
用户空间代码示例
================
缓冲区的创建如下所示::
pipe2(fds, O_TMPFILE);
ioctl(fds[1], IOC_WATCH_QUEUE_SET_SIZE, 256);
它可以被设置成接收密钥环变化的通知::
keyctl(KEYCTL_WATCH_KEY, KEY_SPEC_SESSION_KEYRING, fds[1], 0x01);
然后,这些通知可以被如下方式所使用::
static void consumer(int rfd, struct watch_queue_buffer *buf)
{
unsigned char buffer[128];
ssize_t buf_len;
while (buf_len = read(rfd, buffer, sizeof(buffer)),
buf_len > 0
) {
void *p = buffer;
void *end = buffer + buf_len;
while (p < end) {
union {
struct watch_notification n;
unsigned char buf1[128];
} n;
size_t largest, len;
largest = end - p;
if (largest > 128)
largest = 128;
memcpy(&n, p, largest);
len = (n->info & WATCH_INFO_LENGTH) >>
WATCH_INFO_LENGTH__SHIFT;
if (len == 0 || len > largest)
return;
switch (n.n.type) {
case WATCH_TYPE_META:
got_meta(&n.n);
case WATCH_TYPE_KEY_NOTIFY:
saw_key_change(&n.n);
break;
}
p += len;
}
}
}
@@ -0,0 +1,696 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/workqueue.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
陈兴友 Xingyou Chen <rockrush@rockwork.org>
.. _cn_workqueue.rst:
========
工作队列
========
:日期: September, 2010
:作者: Tejun Heo <tj@kernel.org>
:作者: Florian Mickler <florian@mickler.org>
简介
====
在很多情况下,需要一个异步的程序执行环境,工作队列(wq)API是这种情况下
最常用的机制。
当需要这样一个异步执行上下文时,一个描述将要执行的函数的工作项(work,
即一个待执行的任务)被放在队列中。一个独立的线程作为异步执行环境。该队
列被称为workqueue,线程被称为工作者(worker,即执行这一队列的线程)。
当工作队列上有工作项时,工作者会一个接一个地执行与工作项相关的函数。当
工作队列中没有任何工作项时,工作者就会变得空闲。当一个新的工作项被排入
队列时,工作者又开始执行。
为什么要有并发管理工作队列?
===========================
在最初的wq实现中,多线程(MT)wq在每个CPU上有一个工作者线程,而单线程
(ST)wq在全系统有一个工作者线程。一个MT wq需要保持与CPU数量相同的工
作者数量。这些年来,内核增加了很多MT wq的用户,随着CPU核心数量的不断
增加,一些系统刚启动就达到了默认的32k PID的饱和空间。
尽管MT wq浪费了大量的资源,但所提供的并发性水平却不能令人满意。这个限
制在ST和MT wq中都有,只是在MT中没有那么严重。每个wq都保持着自己独立的
工作者池。一个MT wq只能为每个CPU提供一个执行环境,而一个ST wq则为整个
系统提供一个。工作项必须竞争这些非常有限的执行上下文,从而导致各种问题,
包括在单一执行上下文周围容易发生死锁。
(MT wq)所提供的并发性水平和资源使用之间的矛盾也迫使其用户做出不必要的权衡,比
如libata选择使用ST wq来轮询PIO,并接受一个不必要的限制,即没有两个轮
询PIO可以同时进行。由于MT wq并没有提供更好的并发性,需要更高层次的并
发性的用户,如async或fscache,不得不实现他们自己的线程池。
并发管理工作队列(cmwq)是对wq的重新实现,重点是以下目标。
* 保持与原始工作队列API的兼容性。
* 使用由所有wq共享的每CPU统一的工作者池,在不浪费大量资源的情况下按
* 需提供灵活的并发水平。
* 自动调节工作者池和并发水平,使API用户不需要担心这些细节。
设计
====
为了简化函数的异步执行,引入了一个新的抽象概念,即工作项。
一个工作项是一个简单的结构,它持有一个指向将被异步执行的函数的指针。
每当一个驱动程序或子系统希望一个函数被异步执行时,它必须建立一个指
向该函数的工作项,并在工作队列中排队等待该工作项。(就是挂到workqueue
队列里面去)
工作项可以在线程或BH(软中断)上下文中执行。
对于由线程执行的工作队列,被称为(内核)工作者([k]worker)的特殊
线程会依次执行其中的函数。如果没有工作项排队,工作者线程就会闲置。
这些工作者线程被管理在所谓的工作者池中。
cmwq设计区分了面向用户的工作队列,子系统和驱动程序在上面排队工作,
以及管理工作者池和处理排队工作项的后端机制。
每个可能的CPU都有两个工作者池,一个用于正常的工作项,另一个用于高
优先级的工作项,还有一些额外的工作者池,用于服务未绑定工作队列的工
作项目——这些后备池的数量是动态的。
BH工作队列使用相同的结构。然而,由于同一时间只可能有一个执行上下文,
不需要担心并发问题。每个CPU上的BH工作者池只包含一个用于表示BH执行
上下文的虚拟工作者。BH工作队列可以被看作软中断的便捷接口。
当他们认为合适的时候,子系统和驱动程序可以通过特殊的
``workqueue API`` 函数创建和排队工作项。他们可以通过在工作队列上
设置标志来影响工作项执行方式的某些方面,他们把工作项放在那里。这些
标志包括诸如CPU定位、并发限制、优先级等等。要获得详细的概述,请参
考下面的 ``alloc_workqueue()`` 的 API 描述。
当一个工作项被排入一个工作队列时,目标工作池将根据队列参数和工作队
列属性确定,并被附加到工作池的共享工作列表上。例如,除非特别重写,
否则一个绑定的工作队列的工作项将被排在与发起线程运行的CPU相关的普
通或高级工作工作者池的工作项列表中。
对于任何线程池的实施,管理并发水平(有多少执行上下文处于活动状
态)是一个重要问题。cmwq试图将并发保持在一个尽可能低且充足的
水平。最低水平是为了节省资源,而充足是为了使系统能被充分使用。
每个与实际CPU绑定的worker-pool通过钩住调度器来实现并发管理。每当
一个活动的工作者被唤醒或睡眠时,工作者池就会得到通知,并跟踪当前可
运行的工作者的数量。一般来说,工作项不会占用CPU并消耗很多周期。这
意味着保持足够的并发性以防止工作处理停滞应该是最优的。只要CPU上有
一个或多个可运行的工作者,工作者池就不会开始执行新的工作,但是,当
最后一个运行的工作者进入睡眠状态时,它会立即安排一个新的工作者,这
样CPU就不会在有待处理的工作项目时闲置。这允许在不损失执行带宽的情
况下使用最少的工作者。
除了kthreads的内存空间外,保留空闲的工作者并没有其他成本,所以cmwq
在杀死它们之前会保留一段时间的空闲。
对于非绑定的工作队列,后备池的数量是动态的。可以使用
``apply_workqueue_attrs()`` 为非绑定工作队列分配自定义属性,
workqueue将自动创建与属性相匹配的后备工作者池。调节并发水平的责任在
用户身上。也有一个标志可以将绑定的wq标记为忽略并发管理。
详情请参考API部分。
前进进度的保证依赖于当需要更多的执行上下文时可以创建工作者,这也是
通过使用救援工作者来保证的。所有可能在处理内存回收的代码路径上使用
的工作项都需要在wq上排队,wq上保留了一个救援工作者,以便在内存有压
力的情况下下执行。否则,工作者池就有可能出现死锁,等待执行上下文释
放出来。
应用程序编程接口 (API)
======================
``alloc_workqueue()`` 分配了一个wq。原来的 ``create_*workqueue()``
函数已被废弃,并计划删除。 ``alloc_workqueue()`` 需要三个
参数 - ``@name`` , ``@flags`` 和 ``@max_active`` 。
``@name`` 是wq的名称,如果有的话,也用作救援线程的名称。
一个wq不再管理执行资源,而是作为前进进度保证、刷新(flush)和
工作项属性的域。 ``@flags`` 和 ``@max_active`` 控制着工作
项如何被分配执行资源、安排和执行。
``flags``
---------
``WQ_BH``
BH工作队列可以被看作软中断的便捷接口。它总是每个CPU一份,
其中的各个工作项也会按在队列中的顺序,被所属CPU在软中断
上下文中执行。
BH工作队列的 ``max_active`` 值必须为0,且只能单独或和
``WQ_HIGHPRI`` 标志组合使用。
BH工作项不可以睡眠。像延迟排队、冲洗、取消等所有其他特性
都是支持的。
``WQ_UNBOUND``
排队到非绑定wq的工作项由特殊的工作者池提供服务,这些工作者不
绑定在任何特定的CPU上。这使得wq表现得像一个简单的执行环境提
供者,没有并发管理。非绑定工作者池试图尽快开始执行工作项。非
绑定的wq牺牲了局部性,但在以下情况下是有用的。
* 预计并发水平要求会有很大的波动,使用绑定的wq最终可能会在不
同的CPU上产生大量大部分未使用的工作者,因为发起线程在不同
的CPU上跳转。
* 长期运行的CPU密集型工作负载,可以由系统调度器更好地管理。
``WQ_FREEZABLE``
一个可冻结的wq参与了系统暂停操作的冻结阶段。wq上的工作项被
排空,在解冻之前没有新的工作项开始执行。
``WQ_MEM_RECLAIM``
所有可能在内存回收路径中使用的wq都必须设置这个标志。无论内
存压力如何,wq都能保证至少有一个执行上下文。
``WQ_HIGHPRI``
高优先级wq的工作项目被排到目标cpu的高优先级工作者池中。高
优先级的工作者池由具有较高级别的工作者线程提供服务。
请注意,普通工作者池和高优先级工作者池之间并不相互影响。他
们各自维护其独立的工作者池,并在其工作者之间实现并发管理。
``WQ_CPU_INTENSIVE``
CPU密集型wq的工作项对并发水平没有贡献。换句话说,可运行的
CPU密集型工作项不会阻止同一工作者池中的其他工作项开始执行。
这对于那些预计会占用CPU周期的绑定工作项很有用,这样它们的
执行就会受到系统调度器的监管。
尽管CPU密集型工作项不会对并发水平做出贡献,但它们的执行开
始仍然受到并发管理的管制,可运行的非CPU密集型工作项会延迟
CPU密集型工作项的执行。
这个标志对于未绑定的wq来说是没有意义的。
``max_active``
--------------
``@max_active`` 决定了每个CPU可以分配给wq的工作项的最大执行上
下文数量。例如,如果 ``@max_active`` 为16 ,每个CPU最多可以同
时执行16个wq的工作项。它总是每CPU属性,即便对于未绑定 wq。
``@max_active`` 的最大限制是512,当指定为0时使用的默认值是256。
这些值被选得足够高,所以它们不是限制性因素,同时会在失控情况下提供
保护。
一个wq的活动工作项的数量通常由wq的用户来调节,更具体地说,是由用
户在同一时间可以排列多少个工作项来调节。除非有特定的需求来控制活动
工作项的数量,否则建议指定 为"0"。
一些用户依赖于任意时刻最多只有一个工作项被执行,且各工作项被按队列中
顺序处理带来的严格执行顺序。``@max_active`` 为1和 ``WQ_UNBOUND``
的组合曾被用来实现这种行为,现在不用了。请使用
``alloc_ordered_workqueue()`` 。
执行场景示例
============
下面的示例执行场景试图说明cmwq在不同配置下的行为。
工作项w0、w1、w2被排到同一个CPU上的一个绑定的wq q0上。w0
消耗CPU 5ms,然后睡眠10ms,然后在完成之前再次消耗CPU 5ms。
忽略所有其他的任务、工作和处理开销,并假设简单的FIFO调度,
下面是一个高度简化的原始wq的可能事件序列的版本。::
TIME IN MSECS EVENT
0 w0 starts and burns CPU
5 w0 sleeps
15 w0 wakes up and burns CPU
20 w0 finishes
20 w1 starts and burns CPU
25 w1 sleeps
35 w1 wakes up and finishes
35 w2 starts and burns CPU
40 w2 sleeps
50 w2 wakes up and finishes
And with cmwq with ``@max_active`` >= 3, ::
TIME IN MSECS EVENT
0 w0 starts and burns CPU
5 w0 sleeps
5 w1 starts and burns CPU
10 w1 sleeps
10 w2 starts and burns CPU
15 w2 sleeps
15 w0 wakes up and burns CPU
20 w0 finishes
20 w1 wakes up and finishes
25 w2 wakes up and finishes
如果 ``@max_active`` == 2, ::
TIME IN MSECS EVENT
0 w0 starts and burns CPU
5 w0 sleeps
5 w1 starts and burns CPU
10 w1 sleeps
15 w0 wakes up and burns CPU
20 w0 finishes
20 w1 wakes up and finishes
20 w2 starts and burns CPU
25 w2 sleeps
35 w2 wakes up and finishes
现在,我们假设w1和w2被排到了不同的wq q1上,这个wq q1
有 ``WQ_CPU_INTENSIVE`` 设置::
TIME IN MSECS EVENT
0 w0 starts and burns CPU
5 w0 sleeps
5 w1 and w2 start and burn CPU
10 w1 sleeps
15 w2 sleeps
15 w0 wakes up and burns CPU
20 w0 finishes
20 w1 wakes up and finishes
25 w2 wakes up and finishes
指南
====
* 如果一个wq可能处理在内存回收期间使用的工作项目,请不
要忘记使用 ``WQ_MEM_RECLAIM`` 。每个设置了
``WQ_MEM_RECLAIM`` 的wq都有一个为其保留的执行环境。
如果在内存回收过程中使用的多个工作项之间存在依赖关系,
它们应该被排在不同的wq中,每个wq都有 ``WQ_MEM_RECLAIM`` 。
* 除非需要严格排序,否则没有必要使用ST wq。
* 除非有特殊需要,建议使用0作为@max_active。在大多数使用情
况下,并发水平通常保持在默认限制之下。
* 一个wq作为前进进度保证,``WQ_MEM_RECLAIM`` ,冲洗(flush)和工
作项属性的域。不涉及内存回收的工作项,不需要作为工作项组的一
部分被刷新,也不需要任何特殊属性,可以使用系统中的一个wq。使
用专用wq和系统wq在执行特性上没有区别。
* 除非工作项预计会消耗大量的CPU周期,否则使用绑定的wq通常是有
益的,因为wq操作和工作项执行中的定位水平提高了。
亲和性作用域
============
一个非绑定工作队列根据其亲和性作用域来对CPU进行分组以提高缓存
局部性。比如如果一个工作队列使用默认的“cache”亲和性作用域,
它将根据最后一级缓存的边界来分组处理器。这个工作队列上的工作项
将被分配给一个与发起CPU共用最后级缓存的处理器上的工作者。根据
``affinity_strict`` 的设置,工作者在启动后可能被允许移出
所在作用域,也可能不被允许。
工作队列目前支持以下亲和性作用域。
``default``
使用模块参数 ``workqueue.default_affinity_scope`` 指定
的作用域,该参数总是会被设为以下作用域中的一个。
``cpu``
CPU不被分组。一个CPU上发起的工作项会被同一CPU上的工作者执行。
这使非绑定工作队列表现得像是不含并发管理的每CPU工作队列。
``smt``
CPU被按SMT边界分组。这通常意味着每个物理CPU核上的各逻辑CPU会
被分进同一组。
``cache``
CPU被按缓存边界分组。采用哪个缓存边界由架构代码决定。很多情况
下会使用L3。这是默认的亲和性作用域。
``numa``
CPU被按NUMA边界分组。
``system``
所有CPU被放在同一组。工作队列不尝试在临近发起CPU的CPU上运行
工作项。
默认的亲和性作用域可以被模块参数 ``workqueue.default_affinity_scope``
修改,特定工作队列的亲和性作用域可以通过 ``apply_workqueue_attrs()``
被更改。
如果设置了 ``WQ_SYSFS`` ,工作队列会在它的 ``/sys/devices/virtual/workqueue/WQ_NAME/``
目录中有以下亲和性作用域相关的接口文件。
``affinity_scope``
读操作以查看当前的亲和性作用域。写操作用于更改设置。
当前作用域是默认值时,当前生效的作用域也可以被从这个文件中
读到(小括号内),例如 ``default (cache)`` 。
``affinity_strict``
默认值0表明亲和性作用域不是严格的。当一个工作项开始执行时,
工作队列尽量尝试使工作者处于亲和性作用域内,称为遣返。启动后,
调度器可以自由地将工作者调度到系统中任意它认为合适的地方去。
这使得在保留使用其他CPU(如果必需且有可用)能力的同时,
还能从作用域局部性上获益。
如果设置为1,作用域内的所有工作者将被保证总是处于作用域内。
这在跨亲和性作用域会导致如功耗、负载隔离等方面的潜在影响时
会有用。严格的NUMA作用域也可用于和旧版内核中工作队列的行为
保持一致。
亲和性作用域与性能
==================
如果非绑定工作队列的行为对绝大多数使用场景来说都是最优的,
不需要更多调节,就完美了。很不幸,在当前内核中,重度使用
工作队列时,需要在局部性和利用率间显式地作一个明显的权衡。
更高的局部性带来更高效率,也就是相同数量的CPU周期内可以做
更多工作。然而,如果发起者没能将工作项充分地分散在亲和性
作用域间,更高的局部性也可能带来更低的整体系统利用率。以下
dm-crypt 的性能测试清楚地阐明了这一取舍。
测试运行在一个12核24线程、4个L3缓存的处理器(AMD Ryzen
9 3900x)上。为保持一致性,关闭CPU超频。 ``/dev/dm-0``
是NVME SSD(三星 990 PRO)上创建,用 ``cryptsetup``
以默认配置打开的一个 dm-crypt 设备。
场景 1: 机器上遍布着有充足的发起者和工作量
------------------------------------------
使用命令:::
$ fio --filename=/dev/dm-0 --direct=1 --rw=randrw --bs=32k --ioengine=libaio \
--iodepth=64 --runtime=60 --numjobs=24 --time_based --group_reporting \
--name=iops-test-job --verify=sha512
这里有24个发起者,每个同时发起64个IO。 ``--verify=sha512``
使得 ``fio`` 每次生成和读回内容受发起者和 ``kcryptd``
间的执行局部性影响。下面是基于不同 ``kcryptd`` 的亲和性
作用域设置,各经过五次测试得到的读取带宽和CPU利用率数据。
.. list-table::
:widths: 16 20 20
:header-rows: 1
* - 亲和性
- 带宽 (MiBps)
- CPU利用率(%)
* - system
- 1159.40 ±1.34
- 99.31 ±0.02
* - cache
- 1166.40 ±0.89
- 99.34 ±0.01
* - cache (strict)
- 1166.00 ±0.71
- 99.35 ±0.01
在系统中分布着足够多发起者的情况下,不论严格与否,“cache”
没有表现得更差。三种配置均使整个机器达到饱和,但由于提高了
局部性,缓存相关的两种有0.6%的(带宽)提升。
场景 2: 更少发起者,足以达到饱和的工作量
----------------------------------------
使用命令:::
$ fio --filename=/dev/dm-0 --direct=1 --rw=randrw --bs=32k \
--ioengine=libaio --iodepth=64 --runtime=60 --numjobs=8 \
--time_based --group_reporting --name=iops-test-job --verify=sha512
与上一个场景唯一的区别是 ``--numjobs=8``。 发起者数量
减少为三分之一,但仍然有足以使系统达到饱和的工作总量。
.. list-table::
:widths: 16 20 20
:header-rows: 1
* - 亲和性
- 带宽 (MiBps)
- CPU利用率(%)
* - system
- 1155.40 ±0.89
- 97.41 ±0.05
* - cache
- 1154.40 ±1.14
- 96.15 ±0.09
* - cache (strict)
- 1112.00 ±4.64
- 93.26 ±0.35
这里有超过使系统达到饱和所需的工作量。“system”和“cache”
都接近但并未使机器完全饱和。“cache”消耗更少的CPU但更高的
效率使其得到和“system”相同的带宽。
八个发起者盘桓在四个L3缓存作用域间仍然允许“cache (strict)”
几乎使机器饱和,但缺少对工作的保持(不移到空闲处理器上)
开始带来3.7%的带宽损失。
场景 3: 更少发起者,不充足的工作量
----------------------------------
使用命令:::
$ fio --filename=/dev/dm-0 --direct=1 --rw=randrw --bs=32k \
--ioengine=libaio --iodepth=64 --runtime=60 --numjobs=4 \
--time_based --group_reporting --name=iops-test-job --verify=sha512
再次,唯一的区别是 ``--numjobs=4``。由于发起者减少到四个,
现在没有足以使系统饱和的工作量,带宽变得依赖于完成时延。
.. list-table::
:widths: 16 20 20
:header-rows: 1
* - 亲和性
- 带宽 (MiBps)
- CPU利用率(%)
* - system
- 993.60 ±1.82
- 75.49 ±0.06
* - cache
- 973.40 ±1.52
- 74.90 ±0.07
* - cache (strict)
- 828.20 ±4.49
- 66.84 ±0.29
现在,局部性和利用率间的权衡更清晰了。“cache”展示出相比
“system”2%的带宽损失,而“cache (strict)”跌到20%。
结论和建议
----------
在以上试验中,虽然一致并且也明显,但“cache”亲和性作用域
相比“system”的性能优势并不大。然而,这影响是依赖于作用域
间距离的,在更复杂的处理器拓扑下可能有更明显的影响。
虽然这些情形下缺少工作保持是有坏处的,但比“cache (strict)”
好多了,而且最大化工作队列利用率的需求也并不常见。因此,
“cache”是非绑定池的默认亲和性作用域。
* 由于不存在一个适用于大多数场景的选择,对于可能需要消耗
大量CPU的工作队列,建议通过 ``apply_workqueue_attrs()``
进行(专门)配置,并考虑是否启用 ``WQ_SYSFS``。
* 设置了严格“cpu”亲和性作用域的非绑定工作队列,它的行为与
``WQ_CPU_INTENSIVE`` 每CPU工作队列一样。后者没有真正
优势,而前者提供了大幅度的灵活性。
* 亲和性作用域是从Linux v6.5起引入的。为了模拟旧版行为,
可以使用严格的“numa”亲和性作用域。
* 不严格的亲和性作用域中,缺少工作保持大概缘于调度器。内核
为什么没能维护好大多数场景下的工作保持,把事情作对,还没有
理论上的解释。因此,未来调度器的改进可能会使我们不再需要
这些调节项。
检查配置
========
使用 tools/workqueue/wq_dump.py(drgn脚本) 来检查未
绑定CPU的亲和性配置,工作者池,以及工作队列如何映射到池上: ::
$ tools/workqueue/wq_dump.py
Affinity Scopes
===============
wq_unbound_cpumask=0000000f
CPU
nr_pods 4
pod_cpus [0]=00000001 [1]=00000002 [2]=00000004 [3]=00000008
pod_node [0]=0 [1]=0 [2]=1 [3]=1
cpu_pod [0]=0 [1]=1 [2]=2 [3]=3
SMT
nr_pods 4
pod_cpus [0]=00000001 [1]=00000002 [2]=00000004 [3]=00000008
pod_node [0]=0 [1]=0 [2]=1 [3]=1
cpu_pod [0]=0 [1]=1 [2]=2 [3]=3
CACHE (default)
nr_pods 2
pod_cpus [0]=00000003 [1]=0000000c
pod_node [0]=0 [1]=1
cpu_pod [0]=0 [1]=0 [2]=1 [3]=1
NUMA
nr_pods 2
pod_cpus [0]=00000003 [1]=0000000c
pod_node [0]=0 [1]=1
cpu_pod [0]=0 [1]=0 [2]=1 [3]=1
SYSTEM
nr_pods 1
pod_cpus [0]=0000000f
pod_node [0]=-1
cpu_pod [0]=0 [1]=0 [2]=0 [3]=0
Worker Pools
============
pool[00] ref= 1 nice= 0 idle/workers= 4/ 4 cpu= 0
pool[01] ref= 1 nice=-20 idle/workers= 2/ 2 cpu= 0
pool[02] ref= 1 nice= 0 idle/workers= 4/ 4 cpu= 1
pool[03] ref= 1 nice=-20 idle/workers= 2/ 2 cpu= 1
pool[04] ref= 1 nice= 0 idle/workers= 4/ 4 cpu= 2
pool[05] ref= 1 nice=-20 idle/workers= 2/ 2 cpu= 2
pool[06] ref= 1 nice= 0 idle/workers= 3/ 3 cpu= 3
pool[07] ref= 1 nice=-20 idle/workers= 2/ 2 cpu= 3
pool[08] ref=42 nice= 0 idle/workers= 6/ 6 cpus=0000000f
pool[09] ref=28 nice= 0 idle/workers= 3/ 3 cpus=00000003
pool[10] ref=28 nice= 0 idle/workers= 17/ 17 cpus=0000000c
pool[11] ref= 1 nice=-20 idle/workers= 1/ 1 cpus=0000000f
pool[12] ref= 2 nice=-20 idle/workers= 1/ 1 cpus=00000003
pool[13] ref= 2 nice=-20 idle/workers= 1/ 1 cpus=0000000c
Workqueue CPU -> pool
=====================
[ workqueue \ CPU 0 1 2 3 dfl]
events percpu 0 2 4 6
events_highpri percpu 1 3 5 7
events_long percpu 0 2 4 6
events_unbound unbound 9 9 10 10 8
events_freezable percpu 0 2 4 6
events_power_efficient percpu 0 2 4 6
events_freezable_power_ percpu 0 2 4 6
rcu_gp percpu 0 2 4 6
rcu_par_gp percpu 0 2 4 6
slub_flushwq percpu 0 2 4 6
netns ordered 8 8 8 8 8
...
参见命令的帮助消息以获取更多信息。
监视
====
使用 tools/workqueue/wq_monitor.py 来监视工作队列的运行: ::
$ tools/workqueue/wq_monitor.py events
total infl CPUtime CPUhog CMW/RPR mayday rescued
events 18545 0 6.1 0 5 - -
events_highpri 8 0 0.0 0 0 - -
events_long 3 0 0.0 0 0 - -
events_unbound 38306 0 0.1 - 7 - -
events_freezable 0 0 0.0 0 0 - -
events_power_efficient 29598 0 0.2 0 0 - -
events_freezable_power_ 10 0 0.0 0 0 - -
sock_diag_events 0 0 0.0 0 0 - -
total infl CPUtime CPUhog CMW/RPR mayday rescued
events 18548 0 6.1 0 5 - -
events_highpri 8 0 0.0 0 0 - -
events_long 3 0 0.0 0 0 - -
events_unbound 38322 0 0.1 - 7 - -
events_freezable 0 0 0.0 0 0 - -
events_power_efficient 29603 0 0.2 0 0 - -
events_freezable_power_ 10 0 0.0 0 0 - -
sock_diag_events 0 0 0.0 0 0 - -
...
参见命令的帮助消息以获取更多信息。
调试
====
因为工作函数是由通用的工作者线程执行的,所以需要一些手段来揭示一些行为不端的工作队列用户。
工作者线程在进程列表中显示为: ::
root 5671 0.0 0.0 0 0 ? S 12:07 0:00 [kworker/0:1]
root 5672 0.0 0.0 0 0 ? S 12:07 0:00 [kworker/1:2]
root 5673 0.0 0.0 0 0 ? S 12:12 0:00 [kworker/0:0]
root 5674 0.0 0.0 0 0 ? S 12:13 0:00 [kworker/1:0]
如果kworkers失控了(使用了太多的cpu),有两类可能的问题:
1. 正在迅速调度的事情
2. 一个消耗大量cpu周期的工作项。
第一个可以用追踪的方式进行跟踪: ::
$ echo workqueue:workqueue_queue_work > /sys/kernel/tracing/set_event
$ cat /sys/kernel/tracing/trace_pipe > out.txt
(wait a few secs)
如果有什么东西在工作队列上忙着做循环,它就会主导输出,可以用工作项函数确定违规者。
对于第二类问题,应该可以只检查违规工作者线程的堆栈跟踪。 ::
$ cat /proc/THE_OFFENDING_KWORKER/stack
工作项函数在堆栈追踪中应该是微不足道的。
不可重入条件
============
工作队列保证,如果在工作项排队后满足以下条件,则工作项不能重入:
1. 工作函数没有被改变。
2. 没有人将该工作项排到另一个工作队列中。
3. 该工作项尚未被重新启动。
换言之,如果上述条件成立,则保证在任何给定时间最多由一个系统范围内的工作程序执行
该工作项。
请注意,在self函数中将工作项重新排队(到同一队列)不会破坏这些条件,因此可以安全
地执行此操作。否则在破坏工作函数内部的条件时需要小心。
内核内联文档参考
================
该API在以下内核代码中:
include/linux/workqueue.h
kernel/workqueue.c
@@ -0,0 +1,373 @@
.. SPDX-License-Identifier: GPL-2.0+
.. include:: ../disclaimer-zh_CN.rst
:Original: Documentation/core-api/xarray.rst
:翻译:
司延腾 Yanteng Si <siyanteng@loongson.cn>
周彬彬 Binbin Zhou <zhoubinbin@loongson.cn>
:校译:
.. _cn_core-api_xarray:
======
XArray
======
:作者: Matthew Wilcox
概览
====
XArray是一个抽象的数据类型,它的行为就像一个非常大的指针数组。它满足了许多与哈
希或传统可调整大小的数组相同的需求。与哈希不同的是,它允许你以一种高效的缓存方
式合理地转到下一个或上一个条目。与可调整大小的数组相比,不需要复制数据或改变MMU
的映射来增加数组。与双链表相比,它的内存效率更高,可并行,对缓存更友好。它利用
RCU的优势来执行查找而不需要锁定。
当使用的索引是密集聚集的时候,XArray的实现是有效的;而哈希对象并使用哈希作为索引
将不会有好的表现。XArray对小的索引进行了优化,不过对大的索引仍有良好的性能。如果
你的索引可以大于 ``ULONG_MAX`` ,那么XArray就不适合你的数据类型。XArray最重要
的用户是页面高速缓存。
普通指针可以直接存储在XArray中。它们必须是4字节对齐的,这对任何从kmalloc()和
alloc_page()返回的指针来说都是如此。这对任意的用户空间指针和函数指针来说都不是
真的。你可以存储指向静态分配对象的指针,只要这些对象的对齐方式至少是4(字节)。
你也可以在XArray中存储0到 ``LONG_MAX`` 之间的整数。你必须首先使用xa_mk_value()
将其转换为一个条目。当你从XArray中检索一个条目时,你可以通过调用xa_is_value()检
查它是否是一个值条目,并通过调用xa_to_value()将它转换回一个整数。
一些用户希望对他们存储在XArray中的指针进行标记。你可以调用xa_tag_pointer()来创建
一个带有标签的条目,xa_untag_pointer()将一个有标签的条目转回一个无标签的指针,
xa_pointer_tag()来检索一个条目的标签。标签指针使用相同的位,用于区分值条目和普通
指针,所以你必须决定他们是否要在任何特定的XArray中存储值条目或标签指针。
XArray不支持存储IS_ERR()指针,因为有些指针与值条目或内部条目冲突。
XArray的一个不寻常的特点是能够创建占据一系列索引的条目。一旦存储到其中,查询该范围
内的任何索引将返回与查询该范围内任何其他索引相同的条目。存储到任何索引都会存储所有
的索引条目。多索引条目可以明确地分割成更小的条目,或者将其存储 ``NULL`` 到任何条目中
都会使XArray忘记范围。
普通API
=======
首先初始化一个XArray,对于静态分配的XArray可以用DEFINE_XARRAY(),对于动态分配的
XArray可以用xa_init()。一个新初始化的XArray在每个索引处都包含一个 ``NULL`` 指针。
然后你可以用xa_store()来设置条目,用xa_load()来获取条目。xa_store将用新的条目覆盖任
何条目,并返回存储在该索引的上一个条目。你可以使用xa_erase()来代替调用xa_store()的
``NULL`` 条目。一个从未被存储过的条目、一个被擦除的条目和一个最近被存储过 ``NULL`` 的
条目之间没有区别。
你可以通过使用xa_cmpxchg()有条件地替换一个索引中的条目。和cmpxchg()一样,它只有在该索
引的条目有 ‘旧‘ 值时才会成功。它也会返回该索引上的条目;如果它返回与传递的 ‘旧‘ 相同的条
目,那么xa_cmpxchg()就成功了。
如果你只想在某个索引的当前条目为 ``NULL`` 时将一个新条目存储到该索引,你可以使用xa_insert(),
如果该条目不是空的,则返回 ``-EBUSY`` 。
你可以通过调用xa_extract()将条目从XArray中复制到一个普通数组中。或者你可以通过调用
xa_for_each()、xa_for_each_start()或xa_for_each_range()来遍历XArray中的现有条目。你
可能更喜欢使用xa_find()或xa_find_after()来移动到XArray中的下一个当前条目。
调用xa_store_range()可以在一个索引范围内存储同一个条目。如果你这样做,其他的一些操作将以
一种稍微奇怪的方式进行。例如,在一个索引上标记条目可能会导致该条目在一些,但不是所有其他索
引上被标记。储存到一个索引中可能会导致由一些,但不是所有其他索引检索的条目发生变化。
有时你需要确保对xa_store()的后续调用将不需要分配内存。xa_reserve()函数将在指定索引处存储
一个保留条目。普通API的用户将看到这个条目包含 ``NULL`` 。如果你不需要使用保留的条目,你可
以调用xa_release()来删除这个未使用的条目。如果在此期间有其他用户存储到该条目,xa_release()
将不做任何事情;相反,如果你想让该条目变成 ``NULL`` ,你应该使用xa_erase()。在一个保留的条
目上使用xa_insert()将会失败。
如果数组中的所有条目都是 ``NULL`` ,xa_empty()函数将返回 ``true`` 。
最后,你可以通过调用xa_destroy()删除XArray中的所有条目。如果XArray的条目是指针,你可能希望
先释放这些条目。你可以通过使用xa_for_each()迭代器遍历XArray中所有存在的条目来实现这一目的。
搜索标记
--------
数组中的每个条目都有三个与之相关的位,称为标记。每个标记可以独立于其他标记被设置或清除。你可以
通过使用xa_for_each_marked()迭代器来迭代有标记的条目。
你可以通过使用xa_get_mark()来查询某个条目是否设置了标记。如果该条目不是 ``NULL`` ,你可以通过
使用xa_set_mark()来设置一个标记,并通过调用xa_clear_mark()来删除条目上的标记。你可以通过调用
xa_marked()来询问XArray中的任何条目是否有一个特定的标记被设置。从XArray中删除一个条目会导致与
该条目相关的所有标记被清除。
在一个多索引条目的任何索引上设置或清除标记将影响该条目所涵盖的所有索引。查询任何索引上的标记将返
回相同的结果。
没有办法对没有标记的条目进行迭代;数据结构不允许有效地实现这一点。目前没有迭代器来搜索比特的逻辑
组合(例如迭代所有同时设置了 ``XA_MARK_1`` 和 ``XA_MARK_2`` 的条目,或者迭代所有设置了
``XA_MARK_0`` 或 ``XA_MARK_2`` 的条目)。如果有用户需要,可以增加这些内容。
分配XArrays
-----------
如果你使用DEFINE_XARRAY_ALLOC()来定义XArray,或者通过向xa_init_flags()传递 ``XA_FLAGS_ALLOC``
来初始化它,XArray会改变以跟踪条目是否被使用。
你可以调用xa_alloc()将条目存储在XArray中一个未使用的索引上。如果你需要从中断上下文中修改数组,你
可以使用xa_alloc_bh()或xa_alloc_irq(),在分配ID的同时禁用中断。
使用xa_store()、xa_cmpxchg()或xa_insert()也将标记该条目为正在分配。与普通的XArray不同,存储 ``NULL``
将标记该条目为正在使用中,就像xa_reserve()。要释放一个条目,请使用xa_erase()(或者xa_release(),
如果你只想释放一个 ``NULL`` 的条目)。
默认情况下,最低的空闲条目从0开始分配。如果你想从1开始分配条目,使用DEFINE_XARRAY_ALLOC1()或
``XA_FLAGS_ALLOC1`` 会更有效。如果你想分配ID到一个最大值,然后绕回最低的空闲ID,你可以使用
xa_alloc_cyclic()。
你不能在分配的XArray中使用 ``XA_MARK_0`` ,因为这个标记是用来跟踪一个条目是否是空闲的。其他的
标记可以供你使用。
内存分配
--------
xa_store(), xa_cmpxchg(), xa_alloc(), xa_reserve()和xa_insert()函数接受一个gfp_t参数,以
防XArray需要分配内存来存储这个条目。如果该条目被删除,则不需要进行内存分配,指定的GFP标志将被忽
略。
没有内存可供分配是可能的,特别是如果你传递了一组限制性的GFP标志。在这种情况下,这些函数会返回一
个特殊的值,可以用xa_err()把它变成一个错误值。如果你不需要确切地知道哪个错误发生,使用xa_is_err()
会更有效一些。
锁
--
当使用普通API时,你不必担心锁的问题。XArray使用RCU和一个内部自旋锁来同步访问:
不需要锁:
* xa_empty()
* xa_marked()
采取RCU读锁:
* xa_load()
* xa_for_each()
* xa_for_each_start()
* xa_for_each_range()
* xa_find()
* xa_find_after()
* xa_extract()
* xa_get_mark()
内部使用xa_lock:
* xa_store()
* xa_store_bh()
* xa_store_irq()
* xa_insert()
* xa_insert_bh()
* xa_insert_irq()
* xa_erase()
* xa_erase_bh()
* xa_erase_irq()
* xa_cmpxchg()
* xa_cmpxchg_bh()
* xa_cmpxchg_irq()
* xa_store_range()
* xa_alloc()
* xa_alloc_bh()
* xa_alloc_irq()
* xa_reserve()
* xa_reserve_bh()
* xa_reserve_irq()
* xa_destroy()
* xa_set_mark()
* xa_clear_mark()
假设进入时持有xa_lock:
* __xa_store()
* __xa_insert()
* __xa_erase()
* __xa_cmpxchg()
* __xa_alloc()
* __xa_set_mark()
* __xa_clear_mark()
如果你想利用锁来保护你存储在XArray中的数据结构,你可以在调用xa_load()之前调用xa_lock(),然后在
调用xa_unlock()之前对你找到的对象进行一个引用计数。这将防止存储操作在查找对象和增加refcount期间
从数组中删除对象。你也可以使用RCU来避免解除对已释放内存的引用,但对这一点的解释已经超出了本文的范
围。
在修改数组时,XArray不会禁用中断或softirqs。从中断或softirq上下文中读取XArray是安全的,因为RCU锁
提供了足够的保护。
例如,如果你想在进程上下文中存储XArray中的条目,然后在softirq上下文中擦除它们,你可以这样做::
void foo_init(struct foo *foo)
{
xa_init_flags(&foo->array, XA_FLAGS_LOCK_BH);
}
int foo_store(struct foo *foo, unsigned long index, void *entry)
{
int err;
xa_lock_bh(&foo->array);
err = xa_err(__xa_store(&foo->array, index, entry, GFP_KERNEL));
if (!err)
foo->count++;
xa_unlock_bh(&foo->array);
return err;
}
/* foo_erase()只在软中断上下文中调用 */
void foo_erase(struct foo *foo, unsigned long index)
{
xa_lock(&foo->array);
__xa_erase(&foo->array, index);
foo->count--;
xa_unlock(&foo->array);
}
如果你要从中断或softirq上下文中修改XArray,你需要使用xa_init_flags()初始化数组,传递
``XA_FLAGS_LOCK_IRQ`` 或 ``XA_FLAGS_LOCK_BH`` (参数)。
上面的例子还显示了一个常见的模式,即希望在存储端扩展xa_lock的覆盖范围,以保护与数组相关的一些统计
数据。
与中断上下文共享XArray也是可能的,可以在中断处理程序和进程上下文中都使用xa_lock_irqsave(),或者
在进程上下文中使用xa_lock_irq(),在中断处理程序中使用xa_lock()。一些更常见的模式有一些辅助函数,
如xa_store_bh()、xa_store_irq()、xa_erase_bh()、xa_erase_irq()、xa_cmpxchg_bh() 和xa_cmpxchg_irq()。
有时你需要用一个mutex来保护对XArray的访问,因为这个锁在锁的层次结构中位于另一个mutex之上。这并不
意味着你有权使用像__xa_erase()这样的函数而不占用xa_lock;xa_lock是用来进行lockdep验证的,将来也
会用于其他用途。
__xa_set_mark() 和 __xa_clear_mark() 函数也适用于你查找一个条目并想原子化地设置或清除一个标记的
情况。在这种情况下,使用高级API可能更有效,因为它将使你免于走两次树。
高级API
=======
高级API提供了更多的灵活性和更好的性能,但代价是接口可能更难使用,保障措施更少。高级API没有为你加锁,
你需要在修改数组的时候使用xa_lock。在对数组进行只读操作时,你可以选择使用xa_lock或RCU锁。你可以在
同一个数组上混合使用高级和普通操作;事实上,普通API是以高级API的形式实现的。高级API只对具有GPL兼容
许可证的模块可用。
高级API是基于xa_state的。这是一个不透明的数据结构,你使用XA_STATE()宏在堆栈中声明。这个宏初始化了
xa_state,准备开始在XArray上移动。它被用作一个游标来保持在XArray中的位置,并让你把各种操作组合在一
起,而不必每次都从头开始。xa_state的内容受rcu_read_lock()或xas_lock()的保护。如果需要删除保护状态
和树的这些锁中的任何一个,你必须调用xas_pause()以便将来的调用不会依赖于状态中未受保护的部分。
xa_state也被用来存储错误(store errors)。你可以调用xas_error()来检索错误。所有的操作在进行之前都
会检查xa_state是否处于错误状态,所以你没有必要在每次调用之后检查错误;你可以连续进行多次调用,只在
方便的时候检查。目前XArray代码本身产生的错误只有 ``ENOMEM`` 和 ``EINVAL`` ,但它支持任意的错误,
以防你想自己调用xas_set_err()。
如果xa_state持有 ``ENOMEM`` 错误,调用xas_nomem()将尝试使用指定的gfp标志分配更多的内存,并将其缓
存在xa_state中供下一次尝试。这个想法是,你拿着xa_lock,尝试操作,然后放弃锁。该操作试图在持有锁的情
况下分配内存,但它更有可能失败。一旦你放弃了锁,xas_nomem()可以更努力地尝试分配更多内存。如果值得重
试该操作,它将返回 ``true`` (即出现了内存错误,分配了更多的内存)。如果它之前已经分配了内存,并且
该内存没有被使用,也没有错误(或者一些不是 ``ENOMEM`` 的错误),那么它将释放之前分配的内存。
内部条目
--------
XArray为它自己的目的保留了一些条目。这些条目从未通过正常的API暴露出来,但是当使用高级API时,有可能看
到它们。通常,处理它们的最好方法是把它们传递给xas_retry(),如果它返回 ``true`` ,就重试操作。
.. flat-table::
:widths: 1 1 6
* - 名称
- 检测
- 用途
* - Node
- xa_is_node()
- 一个XArray节点。 在使用多索引xa_state时可能是可见的。
* - Sibling
- xa_is_sibling()
- 一个多索引条目的非典型条目。该值表示该节点中的哪个槽有典型条目。
* - Retry
- xa_is_retry()
- 这个条目目前正在被一个拥有xa_lock的线程修改。在这个RCU周期结束时,包含该条目的节点可能会被释放。
你应该从数组的头部重新开始查找。
* - Zero
- xa_is_zero()
- Zero条目通过普通API显示为 ``NULL`` ,但在XArray中占有一个条目,可用于保留索引供将来使用。这是
通过为分配的条目分配XArrays来使用的,这些条目是 ``NULL`` 。
其他内部条目可能会在未来被添加。在可能的情况下,它们将由xas_retry()处理。
附加函数
--------
xas_create_range()函数分配了所有必要的内存来存储一个范围内的每一个条目。如果它不能分配内存,它将在
xa_state中设置ENOMEM。
你可以使用xas_init_marks()将一个条目上的标记重置为默认状态。这通常是清空所有标记,除非XArray被标记
为 ``XA_FLAGS_TRACK_FREE`` ,在这种情况下,标记0被设置,所有其他标记被清空。使用xas_store()将一个
条目替换为另一个条目不会重置该条目上的标记;如果你想重置标记,你应该明确地这样做。
xas_load()会尽可能地将xa_state移动到该条目附近。如果你知道xa_state已经移动到了该条目,并且需要检查
该条目是否有变化,你可以使用xas_reload()来保存一个函数调用。
如果你需要移动到XArray中的不同索引,可以调用xas_set()。这可以将光标重置到树的顶端,这通常会使下一个
操作将光标移动到树中想要的位置。如果你想移动到下一个或上一个索引,调用xas_next()或xas_prev()。设置
索引不会使光标在数组中移动,所以不需要锁,而移动到下一个或上一个索引则需要锁。
你可以使用xas_find()搜索下一个当前条目。这相当于xa_find()和xa_find_after();如果光标已经移动到了
一个条目,那么它将找到当前引用的条目之后的下一个条目。如果没有,它将返回xa_state索引处的条目。使用
xas_next_entry()而不是xas_find()来移动到下一个当前条目,在大多数情况下会节省一个函数调用,但代价
是发出更多内联代码。
xas_find_marked()函数也是如此。如果xa_state没有被移动过,它将返回xa_state的索引处的条目,如果它
被标记了。否则,它将返回xa_state所引用的条目之后的第一个被标记的条目。xas_next_marked()函数等同
于xas_next_entry()。
当使用xas_for_each()或xas_for_each_marked()在XArray的某个范围内进行迭代时,可能需要暂时停止迭代。
xas_pause()函数的存在就是为了这个目的。在你完成了必要的工作并希望恢复后,xa_state处于适当的状态,在
你最后处理的条目后继续迭代。如果你在迭代时禁用了中断,那么暂停迭代并在每一个 ``XA_CHECK_SCHED`` 条目
中重新启用中断是很好的做法。
xas_get_mark(), xas_set_mark()和xas_clear_mark()函数要求xa_state光标已经被移动到XArray中的适当位
置;如果你在之前调用了xas_pause()或xas_set(),它们将不会有任何作用。
你可以调用xas_set_update(),让XArray每次更新一个节点时都调用一个回调函数。这被页面缓存的workingset
代码用来维护其只包含阴影项的节点列表。
多索引条目
----------
XArray有能力将多个索引联系在一起,因此对一个索引的操作会影响到所有的索引。例如,存储到任何索引将改变
从任何索引检索的条目的值。在任何索引上设置或清除一个标记,都会在每个被绑在一起的索引上设置或清除该标
记。目前的实现只允许将2的整数倍的范围绑在一起;例如指数64-127可以绑在一起,但2-6不能。这可以节省大量
的内存;例如,将512个条目绑在一起可以节省4kB以上的内存。
你可以通过使用XA_STATE_ORDER()或xas_set_order(),然后调用xas_store()来创建一个多索引条目。用一个
多索引的xa_state调用xas_load()会把xa_state移动到树中的正确位置,但是返回值没有意义,有可能是一个内
部条目或 ``NULL`` ,即使在范围内有一个条目存储。调用xas_find_conflict()将返回该范围内的第一个条目,
如果该范围内没有条目,则返回 ``NULL`` 。xas_for_each_conflict()迭代器将遍历每个与指定范围重叠的条目。
如果xas_load()遇到一个多索引条目,xa_state中的xa_index将不会被改变。当遍历一个XArray或者调用xas_find()
时,如果初始索引在一个多索引条目的中间,它将不会被改变。随后的调用或迭代将把索引移到范围内的第一个索引。
每个条目只会被返回一次,不管它占据了多少个索引。
不支持使用xas_next()或xas_prev()来处理一个多索引的xa_state。在一个多索引的条目上使用这两个函数中的任
何一个都会显示出同级的条目;这些条目应该被调用者跳过。
在一个多索引条目的任何一个索引中存储 ``NULL`` ,将把每个索引的条目设置为 ``NULL`` ,并解除绑定。通过调
用xas_split_alloc(),在没有xa_lock的情况下,可以将一个多索引条目分割成占据较小范围的条目,然后再取锁并
调用xas_split()。
函数和结构体
============
该API在以下内核代码中:
include/linux/xarray.h
lib/xarray.c