code:
.. SPDX-License-Identifier: GPL-2.0

.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/loongarch/introduction.rst
:Translator: Huacai Chen <chenhuacai@loongson.cn>

=============
LoongArch介绍
=============

LoongArch是一种新的RISC ISA，在一定程度上类似于MIPS和RISC-V。LoongArch指令集
包括一个精简32位版（LA32R）、一个标准32位版（LA32S）、一个64位版（LA64）。
LoongArch定义了四个特权级（PLV0~PLV3），其中PLV0是最高特权级，用于内核；而PLV3
是最低特权级，用于应用程序。本文档介绍了LoongArch的寄存器、基础指令集、虚拟内
存以及其他一些主题。

寄存器
======

LoongArch的寄存器包括通用寄存器（GPRs）、浮点寄存器（FPRs）、向量寄存器（VRs）
和用于特权模式（PLV0）的控制状态寄存器（CSRs）。

通用寄存器
----------

LoongArch包括32个通用寄存器（ ``$r0`` ~ ``$r31`` ），LA32中每个寄存器为32位宽，
LA64中每个寄存器为64位宽。 ``$r0`` 的内容总是固定为0，而其他寄存器在体系结构层面
没有特殊功能。（ ``$r1`` 算是一个例外，在BL指令中固定用作链接返回寄存器。）

内核使用了一套LoongArch寄存器约定，定义在LoongArch ELF psABI规范中，详细描述参见
:ref:`参考文献 <loongarch-references-zh_CN>`:

================= =============== =================== ==========
寄存器名          别名            用途                跨调用保持
================= =============== =================== ==========
``$r0``           ``$zero``       常量0               不使用
``$r1``           ``$ra``         返回地址            否
``$r2``           ``$tp``         TLS/线程信息指针    不使用
``$r3``           ``$sp``         栈指针              是
``$r4``-``$r11``  ``$a0``-``$a7`` 参数寄存器          否
``$r4``-``$r5``   ``$v0``-``$v1`` 返回值              否
``$r12``-``$r20`` ``$t0``-``$t8`` 临时寄存器          否
``$r21``          ``$u0``         每CPU变量基地址     不使用
``$r22``          ``$fp``         帧指针              是
``$r23``-``$r31`` ``$s0``-``$s8`` 静态寄存器          是
================= =============== =================== ==========

.. note::
    注意： ``$r21`` 寄存器在ELF psABI中保留未使用，但是在Linux内核用于保
    存每CPU变量基地址。该寄存器没有ABI命名，不过在内核中称为 ``$u0`` 。在
    一些遗留代码中有时可能见到 ``$v0`` 和 ``$v1`` ，它们是 ``$a0`` 和
    ``$a1`` 的别名，属于已经废弃的用法。

浮点寄存器
----------

当系统中存在FPU时，LoongArch有32个浮点寄存器（ ``$f0`` ~ ``$f31`` ）。在LA64
的CPU核上，每个寄存器均为64位宽。

浮点寄存器的使用约定与LoongArch ELF psABI规范的描述相同：

================= ================== =================== ==========
寄存器名          别名               用途                跨调用保持
================= ================== =================== ==========
``$f0``-``$f7``   ``$fa0``-``$fa7``  参数寄存器          否
``$f0``-``$f1``   ``$fv0``-``$fv1``  返回值              否
``$f8``-``$f23``  ``$ft0``-``$ft15`` 临时寄存器          否
``$f24``-``$f31`` ``$fs0``-``$fs7``  静态寄存器          是
================= ================== =================== ==========

.. note::
    注意：在一些遗留代码中有时可能见到 ``$fv0`` 和 ``$fv1`` ，它们是
    ``$fa0`` 和 ``$fa1`` 的别名，属于已经废弃的用法。

向量寄存器
----------

LoongArch现有两种向量扩展：

- 128位向量扩展LSX（全称Loongson SIMD eXtention），
- 256位向量扩展LASX（全称Loongson Advanced SIMD eXtention）。

LSX使用 ``$v0`` ~ ``$v31`` 向量寄存器，而LASX则使用 ``$x0`` ~ ``$x31`` 。

浮点寄存器和向量寄存器是复用的，比如：在一个实现了LSX和LASX的核上， ``$x0`` 的
低128位与 ``$v0`` 共用， ``$v0`` 的低64位与 ``$f0`` 共用，其他寄存器依此类推。

控制状态寄存器
--------------

控制状态寄存器只能在特权模式（PLV0）下访问:

================= ==================================== ==========
地址              全称描述                             简称
================= ==================================== ==========
0x0               当前模式信息                         CRMD
0x1               异常前模式信息                       PRMD
0x2               扩展部件使能                         EUEN
0x3               杂项控制                             MISC
0x4               异常配置                             ECFG
0x5               异常状态                             ESTAT
0x6               异常返回地址                         ERA
0x7               出错(Faulting)虚拟地址               BADV
0x8               出错(Faulting)指令字                 BADI
0xC               异常入口地址                         EENTRY
0x10              TLB索引                              TLBIDX
0x11              TLB表项高位                          TLBEHI
0x12              TLB表项低位0                         TLBELO0
0x13              TLB表项低位1                         TLBELO1
0x18              地址空间标识符                       ASID
0x19              低半地址空间页全局目录基址           PGDL
0x1A              高半地址空间页全局目录基址           PGDH
0x1B              页全局目录基址                       PGD
0x1C              页表遍历控制低半部分                 PWCL
0x1D              页表遍历控制高半部分                 PWCH
0x1E              STLB页大小                           STLBPS
0x1F              缩减虚地址配置                       RVACFG
0x20              CPU编号                              CPUID
0x21              特权资源配置信息1                    PRCFG1
0x22              特权资源配置信息2                    PRCFG2
0x23              特权资源配置信息3                    PRCFG3
0x30+n (0≤n≤15)   数据保存寄存器                       SAVEn
0x40              定时器编号                           TID
0x41              定时器配置                           TCFG
0x42              定时器值                             TVAL
0x43              计时器补偿                           CNTC
0x44              定时器中断清除                       TICLR
0x60              LLBit相关控制                        LLBCTL
0x80              实现相关控制1                        IMPCTL1
0x81              实现相关控制2                        IMPCTL2
0x88              TLB重填异常入口地址                  TLBRENTRY
0x89              TLB重填异常出错(Faulting)虚地址      TLBRBADV
0x8A              TLB重填异常返回地址                  TLBRERA
0x8B              TLB重填异常数据保存                  TLBRSAVE
0x8C              TLB重填异常表项低位0                 TLBRELO0
0x8D              TLB重填异常表项低位1                 TLBRELO1
0x8E              TLB重填异常表项高位                  TLBEHI
0x8F              TLB重填异常前模式信息                TLBRPRMD
0x90              机器错误控制                         MERRCTL
0x91              机器错误信息1                        MERRINFO1
0x92              机器错误信息2                        MERRINFO2
0x93              机器错误异常入口地址                 MERRENTRY
0x94              机器错误异常返回地址                 MERRERA
0x95              机器错误异常数据保存                 MERRSAVE
0x98              高速缓存标签                         CTAG
0x180+n (0≤n≤3)   直接映射配置窗口n                    DMWn
0x200+2n (0≤n≤31) 性能监测配置n                        PMCFGn
0x201+2n (0≤n≤31) 性能监测计数器n                      PMCNTn
0x300             内存读写监视点整体控制               MWPC
0x301             内存读写监视点整体状态               MWPS
0x310+8n (0≤n≤7)  内存读写监视点n配置1                 MWPnCFG1
0x311+8n (0≤n≤7)  内存读写监视点n配置2                 MWPnCFG2
0x312+8n (0≤n≤7)  内存读写监视点n配置3                 MWPnCFG3
0x313+8n (0≤n≤7)  内存读写监视点n配置4                 MWPnCFG4
0x380             取指监视点整体控制                   FWPC
0x381             取指监视点整体状态                   FWPS
0x390+8n (0≤n≤7)  取指监视点n配置1                     FWPnCFG1
0x391+8n (0≤n≤7)  取指监视点n配置2                     FWPnCFG2
0x392+8n (0≤n≤7)  取指监视点n配置3                     FWPnCFG3
0x393+8n (0≤n≤7)  取指监视点n配置4                     FWPnCFG4
0x500             调试寄存器                           DBG
0x501             调试异常返回地址                     DERA
0x502             调试数据保存                         DSAVE
================= ==================================== ==========

ERA，TLBRERA，MERRERA和DERA有时也分别称为EPC，TLBREPC，MERREPC和DEPC。

基础指令集
==========

指令格式
--------

LoongArch的指令字长为32位，一共有9种基本指令格式（以及一些变体）:

=========== ==========================
格式名称    指令构成
=========== ==========================
2R          Opcode + Rj + Rd
3R          Opcode + Rk + Rj + Rd
4R          Opcode + Ra + Rk + Rj + Rd
2RI8        Opcode + I8 + Rj + Rd
2RI12       Opcode + I12 + Rj + Rd
2RI14       Opcode + I14 + Rj + Rd
2RI16       Opcode + I16 + Rj + Rd
1RI21       Opcode + I21L + Rj + I21H
I26         Opcode + I26L + I26H
=========== ==========================

Opcode是指令操作码，Rj和Rk是源操作数（寄存器），Rd是目标操作数（寄存器），Ra是
4R-type格式特有的附加操作数（寄存器）。I8/I12/I14/I16/I21/I26分别是8位/12位/14位/
16位/21位/26位的立即数。其中较长的21位和26位立即数在指令字中被分割为高位部分与低位
部分，所以你们在这里的格式描述中能够看到I21L/I21H和I26L/I26H这样带后缀的表述。

指令列表
--------

为了简便起见，我们在此只罗列一下指令名称（助记符），需要详细信息请阅读
:ref:`参考文献 <loongarch-references-zh_CN>` 中的文档。

1. 算术运算指令::

    ADD.W SUB.W ADDI.W ADD.D SUB.D ADDI.D
    SLT SLTU SLTI SLTUI
    AND OR NOR XOR ANDN ORN ANDI ORI XORI
    MUL.W MULH.W MULH.WU DIV.W DIV.WU MOD.W MOD.WU
    MUL.D MULH.D MULH.DU DIV.D DIV.DU MOD.D MOD.DU
    PCADDI PCADDU12I PCADDU18I
    LU12I.W LU32I.D LU52I.D ADDU16I.D

2. 移位运算指令::

    SLL.W SRL.W SRA.W ROTR.W SLLI.W SRLI.W SRAI.W ROTRI.W
    SLL.D SRL.D SRA.D ROTR.D SLLI.D SRLI.D SRAI.D ROTRI.D

3. 位域操作指令::

    EXT.W.B EXT.W.H CLO.W CLO.D SLZ.W CLZ.D CTO.W CTO.D CTZ.W CTZ.D
    BYTEPICK.W BYTEPICK.D BSTRINS.W BSTRINS.D BSTRPICK.W BSTRPICK.D
    REVB.2H REVB.4H REVB.2W REVB.D REVH.2W REVH.D BITREV.4B BITREV.8B BITREV.W BITREV.D
    MASKEQZ MASKNEZ

4. 分支转移指令::

    BEQ BNE BLT BGE BLTU BGEU BEQZ BNEZ B BL JIRL

5. 访存读写指令::

    LD.B LD.BU LD.H LD.HU LD.W LD.WU LD.D ST.B ST.H ST.W ST.D
    LDX.B LDX.BU LDX.H LDX.HU LDX.W LDX.WU LDX.D STX.B STX.H STX.W STX.D
    LDPTR.W LDPTR.D STPTR.W STPTR.D
    PRELD PRELDX

6. 原子操作指令::

    LL.W SC.W LL.D SC.D
    AMSWAP.W AMSWAP.D AMADD.W AMADD.D AMAND.W AMAND.D AMOR.W AMOR.D AMXOR.W AMXOR.D
    AMMAX.W AMMAX.D AMMIN.W AMMIN.D

7. 栅障指令::

    IBAR DBAR

8. 特殊指令::

    SYSCALL BREAK CPUCFG NOP IDLE ERTN(ERET) DBCL(DBGCALL) RDTIMEL.W RDTIMEH.W RDTIME.D
    ASRTLE.D ASRTGT.D

9. 特权指令::

    CSRRD CSRWR CSRXCHG
    IOCSRRD.B IOCSRRD.H IOCSRRD.W IOCSRRD.D IOCSRWR.B IOCSRWR.H IOCSRWR.W IOCSRWR.D
    CACOP TLBP(TLBSRCH) TLBRD TLBWR TLBFILL TLBCLR TLBFLUSH INVTLB LDDIR LDPTE

虚拟内存
========

LoongArch可以使用直接映射虚拟内存和分页映射虚拟内存。

直接映射虚拟内存通过CSR.DMWn（n=0~3）来进行配置，虚拟地址（VA）和物理地址（PA）
之间有简单的映射关系::

 VA = PA + 固定偏移

分页映射的虚拟地址（VA）和物理地址（PA）有任意的映射关系，这种关系记录在TLB和页
表中。LoongArch的TLB包括一个全相联的MTLB（Multiple Page Size TLB，多样页大小TLB）
和一个组相联的STLB（Single Page Size TLB，单一页大小TLB）。

缺省状态下，LA32的整个虚拟地址空间配置如下：

============ =========================== ===========================
区段名       地址范围                    属性
============ =========================== ===========================
``UVRANGE``  ``0x00000000 - 0x7FFFFFFF`` 分页映射, 可缓存, PLV0~3
``KPRANGE0`` ``0x80000000 - 0x9FFFFFFF`` 直接映射, 非缓存, PLV0
``KPRANGE1`` ``0xA0000000 - 0xBFFFFFFF`` 直接映射, 可缓存, PLV0
``KVRANGE``  ``0xC0000000 - 0xFFFFFFFF`` 分页映射, 可缓存, PLV0
============ =========================== ===========================

用户态（PLV3）只能访问UVRANGE，对于直接映射的KPRANGE0和KPRANGE1，将虚拟地址的第
30~31位清零就等于物理地址。例如：物理地址0x00001000对应的非缓存直接映射虚拟地址
是0x80001000，而其可缓存直接映射虚拟地址是0xA0001000。

缺省状态下，LA64的整个虚拟地址空间配置如下：

============ ====================== ==================================
区段名       地址范围               属性
============ ====================== ==================================
``XUVRANGE`` ``0x0000000000000000 - 分页映射, 可缓存, PLV0~3
             0x3FFFFFFFFFFFFFFF``
``XSPRANGE`` ``0x4000000000000000 - 直接映射, 可缓存 / 非缓存, PLV0
             0x7FFFFFFFFFFFFFFF``
``XKPRANGE`` ``0x8000000000000000 - 直接映射, 可缓存 / 非缓存, PLV0
             0xBFFFFFFFFFFFFFFF``
``XKVRANGE`` ``0xC000000000000000 - 分页映射, 可缓存, PLV0
             0xFFFFFFFFFFFFFFFF``
============ ====================== ==================================

用户态（PLV3）只能访问XUVRANGE，对于直接映射的XSPRANGE和XKPRANGE，将虚拟地址的第
60~63位清零就等于物理地址，而其缓存属性是通过虚拟地址的第60~61位配置的（0表示强序
非缓存，1表示一致可缓存，2表示弱序非缓存）。

目前，我们仅用XKPRANGE来进行直接映射，XSPRANGE保留给以后用。

此处给出一个直接映射的例子：物理地址0x00000000_00001000的强序非缓存直接映射虚拟地址
（在XKPRANGE中）是0x80000000_00001000，其一致可缓存直接映射虚拟地址（在XKPRANGE中）
是0x90000000_00001000，而其弱序非缓存直接映射虚拟地址（在XKPRANGE中）是0xA0000000_
00001000。

Loongson与LoongArch的关系
=========================

LoongArch是一种RISC指令集架构（ISA），不同于现存的任何一种ISA，而Loongson（即龙
芯）是一个处理器家族。龙芯包括三个系列：Loongson-1（龙芯1号）是32位处理器系列，
Loongson-2（龙芯2号）是低端64位处理器系列，而Loongson-3（龙芯3号）是高端64位处理
器系列。旧的龙芯处理器基于MIPS架构，而新的龙芯处理器基于LoongArch架构。以龙芯3号
为例：龙芯3A1000/3B1500/3A2000/3A3000/3A4000都是兼容MIPS的，而龙芯3A5000（以及将
来的型号）都是基于LoongArch的。

.. _loongarch-references-zh_CN:

参考文献
========

Loongson官方网站（龙芯中科技术股份有限公司）：

  http://www.loongson.cn/

Loongson与LoongArch的开发者网站（软件与文档资源）：

  http://www.loongnix.cn/

  https://github.com/loongson/

  https://loongson.github.io/LoongArch-Documentation/

LoongArch指令集架构的文档：

  https://github.com/loongson/LoongArch-Documentation/releases/latest/download/LoongArch-Vol1-v1.02-CN.pdf （中文版）

  https://github.com/loongson/LoongArch-Documentation/releases/latest/download/LoongArch-Vol1-v1.02-EN.pdf （英文版）

LoongArch的ELF psABI文档：

  https://github.com/loongson/LoongArch-Documentation/releases/latest/download/LoongArch-ELF-ABI-v2.00-CN.pdf （中文版）

  https://github.com/loongson/LoongArch-Documentation/releases/latest/download/LoongArch-ELF-ABI-v2.00-EN.pdf （英文版）

Loongson与LoongArch的Linux内核源码仓库：

  https://git.kernel.org/pub/scm/linux/kernel/git/chenhuacai/linux-loongson.git


code:
.. SPDX-License-Identifier: GPL-2.0

.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/loongarch/irq-chip-model.rst
:Translator: Huacai Chen <chenhuacai@loongson.cn>

==================================
LoongArch的IRQ芯片模型（层级关系）
==================================

目前，基于LoongArch的处理器（如龙芯3A5000）只能与LS7A芯片组配合工作。LoongArch计算机
中的中断控制器（即IRQ芯片）包括CPUINTC（CPU Core Interrupt Controller）、LIOINTC（
Legacy I/O Interrupt Controller）、EIOINTC（Extended I/O Interrupt Controller）、
HTVECINTC（Hyper-Transport Vector Interrupt Controller）、PCH-PIC（LS7A芯片组的主中
断控制器）、PCH-LPC（LS7A芯片组的LPC中断控制器）和PCH-MSI（MSI中断控制器）。

CPUINTC是一种CPU内部的每个核本地的中断控制器，LIOINTC/EIOINTC/HTVECINTC是CPU内部的
全局中断控制器（每个芯片一个，所有核共享），而PCH-PIC/PCH-LPC/PCH-MSI是CPU外部的中
断控制器（在配套芯片组里面）。这些中断控制器（或者说IRQ芯片）以一种层次树的组织形式
级联在一起，一共有两种层级关系模型（传统IRQ模型和扩展IRQ模型）。

传统IRQ模型
===========

在这种模型里面，IPI（Inter-Processor Interrupt）和CPU本地时钟中断直接发送到CPUINTC，
CPU串口（UARTs）中断发送到LIOINTC，而其他所有设备的中断则分别发送到所连接的PCH-PIC/
PCH-LPC/PCH-MSI，然后被HTVECINTC统一收集，再发送到LIOINTC，最后到达CPUINTC::

     +-----+     +---------+     +-------+
     | IPI | --> | CPUINTC | <-- | Timer |
     +-----+     +---------+     +-------+
                      ^
                      |
                 +---------+     +-------+
                 | LIOINTC | <-- | UARTs |
                 +---------+     +-------+
                      ^
                      |
                +-----------+
                | HTVECINTC |
                +-----------+
                 ^         ^
                 |         |
           +---------+ +---------+
           | PCH-PIC | | PCH-MSI |
           +---------+ +---------+
             ^     ^           ^
             |     |           |
     +---------+ +---------+ +---------+
     | PCH-LPC | | Devices | | Devices |
     +---------+ +---------+ +---------+
          ^
          |
     +---------+
     | Devices |
     +---------+

扩展IRQ模型
===========

在这种模型里面，IPI（Inter-Processor Interrupt）和CPU本地时钟中断直接发送到CPUINTC，
CPU串口（UARTs）中断发送到LIOINTC，而其他所有设备的中断则分别发送到所连接的PCH-PIC/
PCH-LPC/PCH-MSI，然后被EIOINTC统一收集，再直接到达CPUINTC::

          +-----+     +---------+     +-------+
          | IPI | --> | CPUINTC | <-- | Timer |
          +-----+     +---------+     +-------+
                       ^       ^
                       |       |
                +---------+ +---------+     +-------+
                | EIOINTC | | LIOINTC | <-- | UARTs |
                +---------+ +---------+     +-------+
                 ^       ^
                 |       |
          +---------+ +---------+
          | PCH-PIC | | PCH-MSI |
          +---------+ +---------+
            ^     ^           ^
            |     |           |
    +---------+ +---------+ +---------+
    | PCH-LPC | | Devices | | Devices |
    +---------+ +---------+ +---------+
         ^
         |
    +---------+
    | Devices |
    +---------+

ACPI相关的定义
==============

CPUINTC::

  ACPI_MADT_TYPE_CORE_PIC;
  struct acpi_madt_core_pic;
  enum acpi_madt_core_pic_version;

LIOINTC::

  ACPI_MADT_TYPE_LIO_PIC;
  struct acpi_madt_lio_pic;
  enum acpi_madt_lio_pic_version;

EIOINTC::

  ACPI_MADT_TYPE_EIO_PIC;
  struct acpi_madt_eio_pic;
  enum acpi_madt_eio_pic_version;

HTVECINTC::

  ACPI_MADT_TYPE_HT_PIC;
  struct acpi_madt_ht_pic;
  enum acpi_madt_ht_pic_version;

PCH-PIC::

  ACPI_MADT_TYPE_BIO_PIC;
  struct acpi_madt_bio_pic;
  enum acpi_madt_bio_pic_version;

PCH-MSI::

  ACPI_MADT_TYPE_MSI_PIC;
  struct acpi_madt_msi_pic;
  enum acpi_madt_msi_pic_version;

PCH-LPC::

  ACPI_MADT_TYPE_LPC_PIC;
  struct acpi_madt_lpc_pic;
  enum acpi_madt_lpc_pic_version;

参考文献
========

龙芯3A5000的文档：

  https://github.com/loongson/LoongArch-Documentation/releases/latest/download/Loongson-3A5000-usermanual-1.02-CN.pdf (中文版)

  https://github.com/loongson/LoongArch-Documentation/releases/latest/download/Loongson-3A5000-usermanual-1.02-EN.pdf (英文版)

龙芯LS7A芯片组的文档：

  https://github.com/loongson/LoongArch-Documentation/releases/latest/download/Loongson-7A1000-usermanual-2.00-CN.pdf (中文版)

  https://github.com/loongson/LoongArch-Documentation/releases/latest/download/Loongson-7A1000-usermanual-2.00-EN.pdf (英文版)

.. note::
    - CPUINTC：即《龙芯架构参考手册卷一》第7.4节所描述的CSR.ECFG/CSR.ESTAT寄存器及其
      中断控制逻辑；
    - LIOINTC：即《龙芯3A5000处理器使用手册》第11.1节所描述的“传统I/O中断”；
    - EIOINTC：即《龙芯3A5000处理器使用手册》第11.2节所描述的“扩展I/O中断”；
    - HTVECINTC：即《龙芯3A5000处理器使用手册》第14.3节所描述的“HyperTransport中断”；
    - PCH-PIC/PCH-MSI：即《龙芯7A1000桥片用户手册》第5章所描述的“中断控制器”；
    - PCH-LPC：即《龙芯7A1000桥片用户手册》第24.3节所描述的“LPC中断”。

repo_name: angery/linux


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/maintainer/configure-git.rst

:译者:

 吴想成 Wu XiangCheng <bobwxc@email.cn>

.. _configuregit_zh:

Git配置
=======

本章讲述了维护者级别的git配置。

Documentation/maintainer/pull-requests.rst 中使用的标记分支应使用开发人员的
GPG公钥进行签名。可以通过将 ``-u`` 标志传递给 ``git tag`` 来创建签名标记。
但是，由于 *通常* 对同一项目使用同一个密钥，因此可以设置::

	git config user.signingkey "keyname"

或者手动编辑你的 ``.git/config`` 或 ``~/.gitconfig`` 文件::

	[user]
		name = Jane Developer
		email = jd@domain.org
		signingkey = jd@domain.org

你可能需要告诉 ``git`` 去使用 ``gpg2``::

	[gpg]
		program = /path/to/gpg2

你可能也需要告诉 ``gpg`` 去使用哪个 ``tty`` （添加到你的shell rc文件中）::

	export GPG_TTY=$(tty)

创建链接到lore.kernel.org的提交
-------------------------------

http://lore.kernel.org 网站是所有涉及或影响内核开发的邮件列表的总存档。在这里
存储补丁存档是推荐的做法，当维护人员将补丁应用到子系统树时，最好提供一个指向
lore存档链接的标签，以便浏览提交历史的人可以找到某个更改背后的相关讨论和基本
原理。链接标签如下所示：

	Link: https://lore.kernel.org/r/<message-id>

通过在git中添加以下钩子，可以将此配置为在发布 ``git am`` 时自动执行：

.. code-block:: none

	$ git config am.messageid true
	$ cat >.git/hooks/applypatch-msg <<'EOF'
	#!/bin/sh
	. git-sh-setup
	perl -pi -e 's|^Message-Id:\s*<?([^>]+)>?$|Link: https://lore.kernel.org/r/$1|g;' "$1"
	test -x "$GIT_DIR/hooks/commit-msg" &&
		exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
	:
	EOF
	$ chmod a+x .git/hooks/applypatch-msg


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/maintainer/index.rst

==============
内核维护者手册
==============

本文档本是内核维护者手册的首页。
本手册还需要大量完善！请自由提出（和编写）本手册的补充内容。
*译注：指英文原版*

.. toctree::
   :maxdepth: 2

   configure-git
   rebasing-and-merging
   pull-requests
   maintainer-entry-profile
   modifying-patches


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/maintainer/maintainer-entry-profile.rst

:译者:

 吴想成 Wu XiangCheng <bobwxc@email.cn>

.. _maintainerentryprofile_zh:

维护者条目概要
==============

维护人员条目概要补充了顶层过程文档（提交补丁，提交驱动程序……），增加了子系
统/设备驱动程序本地习惯以及有关补丁提交生命周期的相关内容。贡献者使用此文档
来调整他们的期望和避免常见错误；维护人员可以使用这些信息超越子系统层面查看
是否有机会汇聚到通用实践中。

总览
----

提供了子系统如何操作的介绍。MAINTAINERS文件告诉了贡献者应发送某文件的补丁到哪，
但它没有传达其他子系统的本地基础设施和机制以协助开发。

请考虑以下问题：

- 当补丁被本地树接纳或合并到上游时是否有通知？
- 子系统是否使用patchwork实例？Patchwork状态变更是否有通知？
- 是否有任何机器人或CI基础设施监视列表，或子系统是否使用自动测试反馈以便把
  控接纳补丁？
- 被拉入-next的Git分支是哪个？
- 贡献者应针对哪个分支提交？
- 是否链接到其他维护者条目概要？例如一个设备驱动可能指向其父子系统的条目。
  这使得贡献者意识到某维护者可能对提交链中其他维护者负有的义务。

提交检查单补遗
--------------

列出强制性和咨询性标准，超出通用标准“提交检查表，以便维护者检查一个补丁是否
足够健康。例如：“通过checkpatch.pl，没有错误、没有警告。通过单元测试详见某处”。

提交检查单补遗还可以包括有关硬件规格状态的详细信息。例如，子系统接受补丁之前
是否需要考虑在某个修订版上发布的规范。

开发周期的关键日期
------------------

提交者常常会误以为补丁可以在合并窗口关闭之前的任何时间发送，且下一个-rc1时仍
可以。事实上，大多数补丁都需要在下一个合并窗口打开之前提前进入linux-next中。
向提交者澄清关键日期（以-rc发布周为标志）以明确什么时候补丁会被考虑合并以及
何时需要等待下一个-rc。

至少需要讲明：

- 最后一个可以提交新功能的-rc：
  针对下一个合并窗口的新功能提交应该在此点之前首次发布以供考虑。在此时间点
  之后提交的补丁应该明确他们的目标为下下个合并窗口，或者给出应加快进度被接受
  的充足理由。通常新特性贡献者的提交应出现在-rc5之前。

- 最后合并-rc：合并决策的最后期限。
  向贡献者指出尚未接受的补丁集需要等待下下个合并窗口。当然，维护者没有义务
  接受所有给定的补丁集，但是如果审阅在此时间点尚未结束，那么希望贡献者应该
  等待并在下一个合并窗口重新提交。

可选项：

- 开发基线分支的首个-rc，列在概述部分，视为已为新提交做好准备。

审阅节奏
--------

贡献者最担心的问题之一是：补丁集已发布却未收到反馈，应在多久后发送提醒。除了
指定在重新提交之前要等待多长时间，还可以指示更新的首选样式；例如，重新发送
整个系列，或私下发送提醒邮件。本节也可以列出本区域的代码审阅方式，以及获取
不能直接从维护者那里得到的反馈的方法。

现有概要
--------

这里列出了现有的维护人员条目概要；我们可能会想要在不久的将来做一些不同的事情。

.. toctree::
   :maxdepth: 1

   ../doc-guide/maintainer-profile
   ../../../nvdimm/maintainer-entry-profile
   ../../../riscv/patch-acceptance


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/maintainer/modifying-patches.rst

:译者:

 吴想成 Wu XiangCheng <bobwxc@email.cn>

.. _modifyingpatches_zh:

修改补丁
========

如果你是子系统或者分支的维护者，由于代码在你的和提交者的树中并不完全相同，
有时你需要稍微修改一下收到的补丁以合并它们。

如果你严格遵守开发者来源证书的规则（c），你应该要求提交者重做，但这完全是会
适得其反的时间、精力浪费。规则（b）允许你调整代码，但这样修改提交者的代码并
让他背书你的错误是非常不礼貌的。为解决此问题，建议在你之前最后一个
Signed-off-by标签和你的之间添加一行，以指示更改的性质。这没有强制性要求，最
好在描述前面加上你的邮件和/或姓名，用方括号括住整行，以明显指出你对最后一刻
的更改负责。例如::

        Signed-off-by: Random J Developer <random@developer.example.org>
        [lucky@maintainer.example.org: struct foo moved from foo.c to foo.h]
        Signed-off-by: Lucky K Maintainer <lucky@maintainer.example.org>

如果您维护着一个稳定的分支，并希望同时明确贡献、跟踪更改、合并修复，并保护
提交者免受责难，这种做法尤其有用。请注意，在任何情况下都不得更改作者的身份
（From头），因为它会在变更日志中显示。

向后移植（back-port）人员特别要注意：为了便于跟踪，请在提交消息的顶部（即主题行
之后）插入补丁的来源，这是一种常见而有用的做法。例如，我们可以在3.x稳定版本
中看到以下内容::

        Date:   Tue Oct 7 07:26:38 2014 -0400

        libata: Un-break ATA blacklist

        commit 1c40279960bcd7d52dbdf1d466b20d24b99176c8 upstream.

下面是一个旧的内核在某补丁被向后移植后会出现的::

        Date:   Tue May 13 22:12:27 2008 +0200

        wireless, airo: waitbusy() won't delay

        [backport of 2.6 commit b7acbdfbd1f277c1eb23f344f899cfa4cd0bf36a]

不管什么格式，这些信息都为人们跟踪你的树，以及试图解决你树中的错误的人提供了
有价值的帮助。


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/maintainer/pull-requests.rst

:译者:

 吴想成 Wu XiangCheng <bobwxc@email.cn>

.. _pullrequests_zh:

如何创建拉取请求
================

本章描述维护人员如何创建并向其他维护人员提交拉取请求。这对将更改从一个维护者
树转移到另一个维护者树非常有用。

本文档由Tobin C. Harding（当时他尚不是一名经验丰富的维护人员）编写，内容主要
来自Greg Kroah Hartman和Linus Torvalds在LKML上的评论。Jonathan Corbet和Mauro
Carvalho Chehab提出了一些建议和修改。错误不可避免，如有问题，请找Tobin C.
Harding <me@tobin.cc>。

原始邮件线程::

	https://lore.kernel.org/r/20171114110500.GA21175@kroah.com

创建分支
--------

首先，您需要将希望包含拉取请求里的所有更改都放在单独分支中。通常您将基于某开发
人员树的一个分支，一般是打算向其发送拉取请求的开发人员。

为了创建拉取请求，您必须首先标记刚刚创建的分支。建议您选择一个有意义的标记名，
以即使过了一段时间您和他人仍能理解的方式。在名称中包含源子系统和目标内核版本
的指示也是一个好的做法。

Greg提供了以下内容。对于一个含有drivers/char中混杂事项、将应用于4.15-rc1内核的
拉取请求，可以命名为 ``char-misc-4.15-rc1`` 。如果要在 ``char-misc-next`` 分支
上打上此标记，您可以使用以下命令::

	git tag -s char-misc-4.15-rc1 char-misc-next

这将在 ``char-misc-next`` 分支的最后一个提交上创建一个名为 ``char-misc-4.15-rc1``
的标记，并用您的gpg密钥签名（参见 Documentation/maintainer/configure-git.rst ）。

Linus只接受基于签名过的标记的拉取请求。其他维护者可能会有所不同。

当您运行上述命令时 ``git`` 会打开编辑器要求你描述一下这个标记。在本例中您需要
描述拉取请求，所以请概述一下包含的内容，为什么要合并，是否完成任何测试。所有
这些信息都将留在标记中，然后在维护者合并拉取请求时保留在合并提交中。所以把它
写好，它将永远留在内核中。

正如Linus所说::

	不管怎么样，至少对我来说，重要的是 *信息* 。我需要知道我在拉取什么、
	为什么我要拉取。我也希望将此消息用于合并消息，因此它不仅应该对我有
	意义，也应该可以成为一个有意义的历史记录。

	注意，如果拉取请求有一些不寻常的地方，请详细说明。如果你修改了并非
	由你维护的文件，请解释 **为什么** 。我总会在差异中看到的，如果你不
	提的话，我只会觉得分外可疑。当你在合并窗口后给我发新东西的时候，
	（甚至是比较重大的错误修复），不仅需要解释做了什么、为什么这么做，
	还请解释一下 **时间问题** 。为什么错过了合并窗口……

	我会看你写在拉取请求邮件和签名标记里面的内容，所以根据你的工作流，
	你可以在签名标记里面描述工作内容（也会自动放进拉取请求邮件），也
	可以只在标记里面放个占位符，稍后在你实际发给我拉取请求时描述工作内容。

	是的，我会编辑这些消息。部分因为我需要做一些琐碎的格式调整（整体缩进、
	括号等），也因为此消息可能对我有意义（描述了冲突或一些个人问题）而对
	合并提交信息上下文没啥意义，因此我需要尽力让它有意义起来。我也会
	修复一些拼写和语法错误，特别是非母语者（母语者也是;^）。但我也会删掉
	或增加一些内容。

			Linus

Greg给出了一个拉取请求的例子::

	Char/Misc patches for 4.15-rc1

	Here is the big char/misc patch set for the 4.15-rc1 merge window.
	Contained in here is the normal set of new functions added to all
	of these crazy drivers, as well as the following brand new
	subsystems:
		- time_travel_controller: Finally a set of drivers for the
		  latest time travel bus architecture that provides i/o to
		  the CPU before it asked for it, allowing uninterrupted
		  processing
		- relativity_shifters: due to the affect that the
		  time_travel_controllers have on the overall system, there
		  was a need for a new set of relativity shifter drivers to
		  accommodate the newly formed black holes that would
		  threaten to suck CPUs into them.  This subsystem handles
		  this in a way to successfully neutralize the problems.
		  There is a Kconfig option to force these to be enabled
		  when needed, so problems should not occur.

	All of these patches have been successfully tested in the latest
	linux-next releases, and the original problems that it found have
	all been resolved (apologies to anyone living near Canberra for the
	lack of the Kconfig options in the earlier versions of the
	linux-next tree creations.)

	Signed-off-by: Your-name-here <your_email@domain>

此标记消息格式就像一个git提交。顶部有一行“总结标题”， 一定要在下面sign-off。

现在您已经有了一个本地签名标记，您需要将它推送到可以被拉取的位置::

	git push origin char-misc-4.15-rc1

创建拉取请求
------------

最后要做的是创建拉取请求消息。可以使用 ``git request-pull`` 命令让 ``git``
为你做这件事，但它需要确定你想拉取什么，以及拉取针对的基础（显示正确的拉取
更改和变更状态）。以下命令将生成一个拉取请求::

	git request-pull master git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/char-misc.git/ char-misc-4.15-rc1

引用Greg的话::

	此命令要求git比较从“char-misc-4.15-rc1”标记位置到“master”分支头（上述
	例子中指向了我从Linus的树分叉的地方，通常是-rc发布）的差异，并去使用
	git:// 协议拉取。如果你希望使用 https:// 协议，也可以用在这里（但是请
	注意，部分人由于防火墙问题没法用https协议拉取）。

	如果char-misc-4.15-rc1标记没有出现在我要求拉取的仓库中，git会提醒
	它不在那里，所以记得推送到公开地方。

	“git request-pull”会包含git树的地址和需要拉取的特定标记，以及标记
	描述全文（详尽描述标记）。同时它也会创建此拉取请求的差异状态和单个
	提交的缩短日志。

Linus回复说他倾向于 ``git://`` 协议。其他维护者可能有不同的偏好。另外，请注意
如果你创建的拉取请求没有签名标记， ``https://`` 可能是更好的选择。完整的讨论
请看原邮件。

提交拉取请求
------------

拉取请求的提交方式与普通补丁相同。向维护人员发送内联电子邮件并抄送LKML以及
任何必要特定子系统的列表。对Linus的拉取请求通常有如下主题行::

	[GIT PULL] <subsystem> changes for v4.15-rc1


code:
.. SPDX-License-Identifier: GPL-2.0

.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/maintainer/rebasing-and-merging.rst

:译者:

 吴想成 Wu XiangCheng <bobwxc@email.cn>

==========
变基与合并
==========

一般来说，维护子系统需要熟悉Git源代码管理系统。Git是一个功能强大的工具，有
很多功能；就像这类工具常出现的情况一样，使用这些功能的方法有对有错。本文档
特别介绍了变基与合并的用法。维护者经常在错误使用这些工具时遇到麻烦，但避免
问题实际上并不那么困难。

总的来说，需要注意的一点是：与许多其他项目不同，内核社区并不害怕在其开发历史
中看到合并提交。事实上，考虑到该项目的规模，避免合并几乎是不可能的。维护者会
在希望避免合并时遇到一些问题，而过于频繁的合并也会带来另一些问题。

变基
====

“变基（Rebase）”是更改存储库中一系列提交的历史记录的过程。有两种不同型的操作
都被称为变基，因为这两种操作都使用 ``git rebase`` 命令，但它们之间存在显著
差异：

 - 更改一系列补丁的父提交（起始提交）。例如，变基操作可以将基于上一内核版本
   的一个补丁集重建到当前版本上。在下面的讨论中，我们将此操作称为“变根”。

 - 通过修复（或删除）损坏的提交、添加补丁、添加标记以更改一系列补丁的历史，
   来提交变更日志或更改已应用提交的顺序。在下文中，这种类型的操作称为“历史
   修改”

术语“变基”将用于指代上述两种操作。如果使用得当，变基可以产生更清晰、更整洁的
开发历史；如果使用不当，它可能会模糊历史并引入错误。

以下一些经验法则可以帮助开发者避免最糟糕的变基风险：

 - 已经发布到你私人系统之外世界的历史通常不应更改。其他人可能会拉取你的树
   的副本，然后基于它进行工作；修改你的树会给他们带来麻烦。如果工作需要变基，
   这通常是表明它还没有准备好提交到公共存储库的信号。

   但是，总有例外。有些树（linux-next是一个典型的例子）由于它们的需要经常
   变基，开发人员知道不要基于它们来工作。开发人员有时会公开一个不稳定的分支，
   供其他人或自动测试服务进行测试。如果您确实以这种方式公开了一个可能不稳定
   的分支，请确保潜在使用者知道不要基于它来工作。

 - 不要在包含由他人创建的历史的分支上变基。如果你从别的开发者的仓库拉取了变更，
   那你现在就成了他们历史记录的保管人。你不应该改变它，除了少数例外情况。例如
   树中有问题的提交必须显式恢复（即通过另一个补丁修复），而不是通过修改历史而
   消失。

 - 没有合理理由，不要对树变根。仅为了切换到更新的基或避免与上游储存库的合并
   通常不是合理理由。

 - 如果你必须对储存库进行变根，请不要随机选取一个提交作为新基。在发布节点之间
   内核通常处于一个相对不稳定的状态；基于其中某点进行开发会增加遇到意外错误的
   几率。当一系列补丁必须移动到新基时，请选择移动到一个稳定节点（例如-rc版本
   节点）。

 - 请知悉对补丁系列进行变根（或做明显的历史修改）会改变它们的开发环境，且很
   可能使做过的大部分测试失效。一般来说，变基后的补丁系列应当像新代码一样对
   待，并重新测试。

合并窗口麻烦的一个常见原因是，Linus收到了一个明显在拉取请求发送之前不久才变根
（通常是变根到随机的提交上）的补丁系列。这样一个系列被充分测试的可能性相对较
低，拉取请求被接受的几率也同样较低。

相反，如果变基仅限于私有树、提交基于一个通用的起点、且经过充分测试，则引起
麻烦的可能性就很低。

合并
====

内核开发过程中，合并是一个很常见的操作；5.1版本开发周期中有超过1126个合并
——差不多占了整体的9%。内核开发工作积累在100多个不同的子系统树中，每个
子系统树都可能包含多个主题分支；每个分支通常独立于其他分支进行开发。因此
在任何给定分支进入上游储存库之前，至少需要一次合并。

许多项目要求拉取请求中的分支基于当前主干，这样历史记录中就不会出现合并提交。
内核并不是这样；任何为了避免合并而重新对分支变基都很可能导致麻烦。

子系统维护人员发现他们必须进行两种类型的合并：从较低层级的子系统树和从其他
子系统树（同级树或主线）进行合并。这两种情况下要遵循的最佳实践是不同的。

合并较低层级树
--------------

较大的子系统往往有多个级别的维护人员，较低级别的维护人员向较高级别发送拉取
请求。合并这样的请求执行几乎肯定会生成一个合并提交；这也是应该的。实际上，
子系统维护人员可能希望在极少数快进合并情况下使用 ``-–no-ff`` 标志来强制添加
合并提交，以便记录合并的原因。 **任何** 类型的合并的变更日志必须说明
*为什么* 合并。对于较低级别的树，“为什么”通常是对该取所带来的变化的总结。

各级维护人员都应在他们的拉取请求上使用经签名的标签，上游维护人员应在拉取分支
时验证标签。不这样做会威胁整个开发过程的安全。

根据上面列出的规则，一旦您将其他人的历史记录合并到树中，您就不得对该分支进行
变基，即使您能够这样做。

合并同级树或上游树
------------------

虽然来自下游的合并是常见且不起眼的，但当需要将一个分支推向上游时，其中来自
其他树的合并往往是一个危险信号。这种合并需要仔细考虑并加以充分证明，否则后续
的拉取请求很可能会被拒绝。

想要将主分支合并到存储库中是很自然的；这种类型的合并通常被称为“反向合并”
。反向合并有助于确保与并行的开发没有冲突，并且通常会给人一种温暖、舒服的
感觉，即处于最新。但这种诱惑几乎总是应该避免的。

为什么呢？反向合并将搅乱你自己分支的开发历史。它们会大大增加你遇到来自社区
其他地方的错误的机会，且使你很难确保你所管理的工作稳定并准备好合入上游。
频繁的合并还可以掩盖树中开发过程中的问题；它们会隐藏与其他树的交互，而这些
交互不应该（经常）发生在管理良好的分支中。

也就是说，偶尔需要进行反向合并；当这种情况发生时，一定要在提交信息中记录
*为什么* 。同样，在一个众所周知的稳定点进行合并，而不是随机提交。即使这样，
你也不应该反向合并一棵比你的直接上游树更高层级的树；如果确实需要更高级别的
反向合并，应首先在上游树进行。

导致合并相关问题最常见的原因之一是：在发送拉取请求之前维护者合并上游以解决
合并冲突。同样，这种诱惑很容易理解，但绝对应该避免。对于最终拉取请求来说
尤其如此：Linus坚信他更愿意看到合并冲突，而不是不必要的反向合并。看到冲突
可以让他了解潜在的问题所在。他做过很多合并（在5.1版本开发周期中是382次），
而且在解决冲突方面也很在行——通常比参与的开发人员要强。

那么，当他们的子系统分支和主线之间发生冲突时，维护人员应该怎么做呢？最重要
的一步是在拉取请求中提示Linus会发生冲突；如果啥都没说则表明您的分支可以正常
合入。对于特别困难的冲突，创建并推送一个 *独立* 分支来展示你将如何解决问题。
在拉取请求中提到该分支，但是请求本身应该针对未合并的分支。

即使不存在已知冲突，在发送拉取请求之前进行合并测试也是个好主意。它可能会提醒
您一些在linux-next树中没有发现的问题，并帮助您准确地理解您正在要求上游做什么。

合并上游树或另一个子系统树的另一个原因是解决依赖关系。这些依赖性问题有时确实
会发生，而且有时与另一棵树交叉合并是解决这些问题的最佳方法；同样，在这种情况
下，合并提交应该解释为什么要进行合并。花点时间把它做好；会有人阅读这些变更
日志。

然而依赖性问题通常表明需要改变方法。合并另一个子系统树以解决依赖性风险会带来
其他缺陷，几乎永远不应这样做。如果该子系统树无法被合到上游，那么它的任何问题
也都会阻碍你的树合并。更可取的选择包括与维护人员达成一致意见，在其中一个树中
同时进行两组更改；或者创建一个主题分支专门处理可以合并到两个树中的先决条件提交。
如果依赖关系与主要的基础结构更改相关，正确的解决方案可能是将依赖提交保留一个
开发周期，以便这些更改有时间在主线上稳定。

最后
====

在开发周期的开头合并主线是比较常见的，可以获取树中其他地方的更改和修复。同样，
这样的合并应该选择一个众所周知的发布点，而不是一些随机点。如果在合并窗口期间
上游分支已完全清空到主线中，则可以使用以下命令向前拉取它::

  git merge v5.2-rc1^0

“^0”使Git执行快进合并（在这种情况下这应该可以），从而避免多余的虚假合并提交。

上面列出的就是指导方针了。总是会有一些情况需要不同的解决方案，这些指导原则
不应阻止开发人员在需要时做正确的事情。但是，我们应该时刻考虑是否真的出现了
这样的需求，并准备好解释为什么需要做一些不寻常的事情。


code:
.. SPDX-License-Identifier: GPL-2.0

.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mips/booting.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

.. _cn_booting:

BMIPS设备树引导
------------------------

  一些bootloaders只支持在内核镜像开始地址处的单一入口点。而其它
  bootloaders将跳转到ELF的开始地址处。两种方案都支持的；因为
  CONFIG_BOOT_RAW=y and CONFIG_NO_EXCEPT_FILL=y, 所以第一条指令
  会立即跳转到kernel_entry()入口处执行。

  与arch/arm情况(b)类似，dt感知的引导加载程序需要设置以下寄存器:

         a0 : 0

         a1 : 0xffffffff

         a2 : RAM中指向设备树块的物理指针(在chapterII中定义)。
              设备树可以位于前512MB物理地址空间(0x00000000 -
              0x1fffffff)的任何位置，以64位边界对齐。

  传统bootloaders不会使用这样的约定，并且它们不传入DT块。
  在这种情况下，Linux将通过选中CONFIG_DT_*查找DTB。

  以上约定只在32位系统中定义，因为目前没有任何64位的BMIPS实现。


code:
.. SPDX-License-Identifier: GPL-2.0

.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mips/features.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

.. _cn_features:

.. kernel-feat:: $srctree/Documentation/features mips


code:
.. SPDX-License-Identifier: GPL-2.0

.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mips/index.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

===========================
MIPS特性文档
===========================

.. toctree::
   :maxdepth: 2
   :numbered:

   booting
   ingenic-tcu

   features

.. only::  subproject and html

   Indices
   =======

   * :ref:`genindex`


code:
.. SPDX-License-Identifier: GPL-2.0

.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mips/ingenic-tcu.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

.. _cn_ingenic-tcu:

===============================================
君正 JZ47xx SoC定时器/计数器硬件单元
===============================================

君正 JZ47xx SoC中的定时器/计数器单元(TCU)是一个多功能硬件块。它有多达
8个通道，可以用作计数器，计时器，或脉冲宽度调制器。

- JZ4725B, JZ4750, JZ4755 只有６个TCU通道。其它SoC都有８个通道。

- JZ4725B引入了一个独立的通道，称为操作系统计时器(OST)。这是一个32位可
  编程定时器。在JZ4760B及以上型号上，它是64位的。

- 每个TCU通道都有自己的时钟源，可以通过 TCSR 寄存器设置通道的父级时钟
  源（pclk、ext、rtc）、开关以及分频。

    - 看门狗和OST硬件模块在它们的寄存器空间中也有相同形式的TCSR寄存器。
    - 用于关闭/开启的 TCU 寄存器也可以关闭/开启看门狗和 OST 时钟。

- 每个TCU通道在两种模式的其中一种模式下运行：

    - 模式 TCU1：通道无法在睡眠模式下运行，但更易于操作。
    - 模式 TCU2：通道可以在睡眠模式下运行，但操作比 TCU1 通道复杂一些。

- 每个 TCU 通道的模式取决于使用的SoC：

    - 在最老的SoC（高于JZ4740），八个通道都运行在TCU1模式。
    - 在 JZ4725B，通道5运行在TCU2,其它通道则运行在TCU1。
    - 在最新的SoC（JZ4750及之后），通道1-2运行在TCU2，其它通道则运行
      在TCU1。

- 每个通道都可以生成中断。有些通道共享一条中断线，而有些没有，其在SoC型
  号之间的变更：

    - 在很老的SoC（JZ4740及更低），通道0和通道1有它们自己的中断线；通
      道2-7共享最后一条中断线。
    - 在 JZ4725B，通道0有它自己的中断线；通道1-5共享一条中断线；OST
      使用最后一条中断线。
    - 在比较新的SoC（JZ4750及以后），通道5有它自己的中断线；通
      道0-4和（如果是8通道）6-7全部共享一条中断线；OST使用最后一条中
      断线。

实现
====

TCU硬件的功能分布在多个驱动程序：

==============      ===================================
时钟                drivers/clk/ingenic/tcu.c
中断                drivers/irqchip/irq-ingenic-tcu.c
定时器              drivers/clocksource/ingenic-timer.c
OST                 drivers/clocksource/ingenic-ost.c
脉冲宽度调制器      drivers/pwm/pwm-jz4740.c
看门狗              drivers/watchdog/jz4740_wdt.c
==============      ===================================

因为可以从相同的寄存器控制属于不同驱动程序和框架的TCU的各种功能，所以
所有这些驱动程序都通过相同的控制总线通用接口访问它们的寄存器。

有关TCU驱动程序的设备树绑定的更多信息，请参阅:
Documentation/devicetree/bindings/timer/ingenic,tcu.yaml.


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mm/active_mm.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

=========
Active MM
=========

这是一封linux之父回复开发者的一封邮件，所以翻译时我尽量保持邮件格式的完整。

::

 List:       linux-kernel
 Subject:    Re: active_mm
 From:       Linus Torvalds <torvalds () transmeta ! com>
 Date:       1999-07-30 21:36:24

 因为我并不经常写解释，所以已经抄送到linux-kernel邮件列表，而当我做这些，
 且更多的人在阅读它们时，我觉得棒极了。

 1999年7月30日 星期五， David Mosberger 写道：
 >
 > 是否有一个简短的描述，说明task_struct中的
 >  "mm" 和 "active_mm"应该如何使用？ (如果
 > 这个问题在邮件列表中讨论过，我表示歉意--我刚
 > 刚度假回来，有一段时间没能关注linux-kernel了）。

 基本上，新的设定是：

  - 我们有“真实地址空间”和“匿名地址空间”。区别在于，匿名地址空间根本不关心用
    户级页表，所以当我们做上下文切换到匿名地址空间时，我们只是让以前的地址空间
    处于活动状态。

    一个“匿名地址空间”的明显用途是任何不需要任何用户映射的线程--所有的内核线
    程基本上都属于这一类，但即使是“真正的”线程也可以暂时说在一定时间内它们不
    会对用户空间感兴趣，调度器不妨试着避免在切换VM状态上浪费时间。目前只有老
    式的bdflush sync能做到这一点。

  - “tsk->mm” 指向 “真实地址空间”。对于一个匿名进程来说，tsk->mm将是NULL，
    其逻辑原因是匿名进程实际上根本就 “没有” 真正的地址空间。

  - 然而，我们显然需要跟踪我们为这样的匿名用户“偷用”了哪个地址空间。为此，我们
    有 “tsk->active_mm”，它显示了当前活动的地址空间是什么。

    规则是，对于一个有真实地址空间的进程（即tsk->mm是 non-NULL），active_mm
    显然必须与真实的mm相同。

    对于一个匿名进程，tsk->mm == NULL，而tsk->active_mm是匿名进程运行时
    “借用”的mm。当匿名进程被调度走时，借用的地址空间被返回并清除。

 为了支持所有这些，“struct mm_struct”现在有两个计数器：一个是 “mm_users”
 计数器，即有多少 “真正的地址空间用户”，另一个是 “mm_count”计数器，即 “lazy”
 用户（即匿名用户）的数量，如果有任何真正的用户，则加1。

 通常情况下，至少有一个真正的用户，但也可能是真正的用户在另一个CPU上退出，而
 一个lazy的用户仍在活动，所以你实际上得到的情况是，你有一个地址空间 **只**
 被lazy的用户使用。这通常是一个短暂的生命周期状态，因为一旦这个线程被安排给一
 个真正的线程，这个 “僵尸” mm就会被释放，因为 “mm_count”变成了零。

 另外，一个新的规则是，**没有人** 再把 “init_mm” 作为一个真正的MM了。
 “init_mm”应该被认为只是一个 “没有其他上下文时的lazy上下文”，事实上，它主
 要是在启动时使用，当时还没有真正的VM被创建。因此，用来检查的代码

   if (current->mm == &init_mm)

 一般来说，应该用

   if (!current->mm)

 取代上面的写法（这更有意义--测试基本上是 “我们是否有一个用户环境”，并且通常
 由缺页异常处理程序和类似的东西来完成）。

 总之，我刚才在ftp.kernel.org上放了一个pre-patch-2.3.13-1，因为它稍微改
 变了接口以适配alpha（谁会想到呢，但alpha体系结构上下文切换代码实际上最终是
 最丑陋的之一--不像其他架构的MM和寄存器状态是分开的，alpha的PALcode将两者
 连接起来，你需要同时切换两者）。

 (文档来源 http://marc.info/?l=linux-kernel&m=93337278602211&w=2)


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mm/balance.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

========
内存平衡
========

2000年1月开始，作者：Kanoj Sarcar <kanoj@sgi.com>

对于 !__GFP_HIGH 和 !__GFP_KSWAPD_RECLAIM 以及非 __GFP_IO 的分配，需要进行
内存平衡。

调用者避免回收的第一个原因是调用者由于持有自旋锁或处于中断环境中而无法睡眠。第二个
原因可能是，调用者愿意在不产生页面回收开销的情况下分配失败。这可能发生在有0阶回退
选项的机会主义高阶分配请求中。在这种情况下，调用者可能也希望避免唤醒kswapd。

__GFP_IO分配请求是为了防止文件系统死锁。

在没有非睡眠分配请求的情况下，做平衡似乎是有害的。页面回收可以被懒散地启动，也就是
说，只有在需要的时候（也就是区域的空闲内存为0），而不是让它成为一个主动的过程。

也就是说，内核应该尝试从直接映射池中满足对直接映射页的请求，而不是回退到dma池中，
这样就可以保持dma池为dma请求（不管是不是原子的）所填充。类似的争论也适用于高内存
和直接映射的页面。相反，如果有很多空闲的dma页，最好是通过从dma池中分配一个来满足
常规的内存请求，而不是产生常规区域平衡的开销。

在2.2中，只有当空闲页总数低于总内存的1/64时，才会启动内存平衡/页面回收。如果dma
和常规内存的比例合适，即使dma区完全空了，也很可能不会进行平衡。2.2已经在不同内存
大小的生产机器上运行，即使有这个问题存在，似乎也做得不错。在2.3中，由于HIGHMEM的
存在，这个问题变得更加严重。

在2.3中，区域平衡可以用两种方式之一来完成：根据区域的大小（可能是低级区域的大小），
我们可以在初始化阶段决定在平衡任何区域时应该争取多少空闲页。好的方面是，在平衡的时
候，我们不需要看低级区的大小，坏的方面是，我们可能会因为忽略低级区可能较低的使用率
而做过于频繁的平衡。另外，只要对分配程序稍作修改，就有可能将memclass()宏简化为一
个简单的等式。

另一个可能的解决方案是，我们只在一个区 **和** 其所有低级区的空闲内存低于该区及其
低级区总内存的1/64时进行平衡。这就解决了2.2的平衡问题，并尽可能地保持了与2.2行为
的接近。另外，平衡算法在各种架构上的工作方式也是一样的，这些架构有不同数量和类型的
内存区。如果我们想变得更花哨一点，我们可以在未来为不同区域的自由页面分配不同的权重。

请注意，如果普通区的大小与dma区相比是巨大的，那么在决定是否平衡普通区的时候，考虑
空闲的dma页就变得不那么重要了。那么第一个解决方案就变得更有吸引力。

所附的补丁实现了第二个解决方案。它还 “修复”了两个问题：首先，在低内存条件下，kswapd
被唤醒，就像2.2中的非睡眠分配。第二，HIGHMEM区也被平衡了，以便给replace_with_highmem()
一个争取获得HIGHMEM页的机会，同时确保HIGHMEM分配不会落回普通区。这也确保了HIGHMEM
页不会被泄露（例如，在一个HIGHMEM页在交换缓存中但没有被任何人使用的情况下）。

kswapd还需要知道它应该平衡哪些区。kswapd主要是在无法进行平衡的情况下需要的，可能
是因为所有的分配请求都来自中断上下文，而所有的进程上下文都在睡眠。对于2.3，
kswapd并不真正需要平衡高内存区，因为中断上下文并不请求高内存页。kswapd看zone
结构体中的zone_wake_kswapd字段来决定一个区是否需要平衡。

如果从进程内存和shm中偷取页面可以减轻该页面节点中任何区的内存压力，而该区的内存压力
已经低于其水位，则会进行偷取。

watemark[WMARK_MIN/WMARK_LOW/WMARK_HIGH]/low_on_memory/zone_wake_kswapd：
这些是每个区的字段，用于确定一个区何时需要平衡。当页面数低于水位[WMARK_MIN]时，
hysteric 的字段low_on_memory被设置。这个字段会一直被设置，直到空闲页数变成水位
[WMARK_HIGH]。当low_on_memory被设置时，页面分配请求将尝试释放该区域的一些页面（如果
请求中设置了GFP_WAIT）。与此相反的是，决定唤醒kswapd以释放一些区的页。这个决定不是基于
hysteresis 的，而是当空闲页的数量低于watermark[WMARK_LOW]时就会进行；在这种情况下，
zone_wake_kswapd也被设置。

我所听到的（超棒的）想法：

1. 动态经历应该影响平衡：可以跟踪一个区的失败请求的数量，并反馈到平衡方案中（jalvo@mbay.net）。

2. 实现一个类似于replace_with_highmem()的replace_with_regular()，以保留dma页面。
   (lkd@tantalophile.demon.co.uk)


code:
.. SPDX-License-Identifier: GPL-2.0

:Original: Documentation/mm/damon/api.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

=======
API参考
=======

内核空间的程序可以使用下面的API来使用DAMON的每个功能。你所需要做的就是引用 ``damon.h`` ，
它位于源代码树的include/linux/。

结构体
======

该API在以下内核代码中:

include/linux/damon.h

函数
====

该API在以下内核代码中:

mm/damon/core.c


code:
.. SPDX-License-Identifier: GPL-2.0

:Original: Documentation/mm/damon/design.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

====
设计
====

可配置的层
==========

DAMON提供了数据访问监控功能，同时使其准确性和开销可控。基本的访问监控需要依赖于目标地址空间
并为之优化的基元。另一方面，作为DAMON的核心，准确性和开销的权衡机制是在纯逻辑空间中。DAMON
将这两部分分离在不同的层中，并定义了它的接口，以允许各种低层次的基元实现与核心逻辑的配置。

由于这种分离的设计和可配置的接口，用户可以通过配置核心逻辑和适当的低级基元实现来扩展DAMON的
任何地址空间。如果没有提供合适的，用户可以自己实现基元。

例如，物理内存、虚拟内存、交换空间、那些特定的进程、NUMA节点、文件和支持的内存设备将被支持。
另外，如果某些架构或设备支持特殊的优化访问检查基元，这些基元将很容易被配置。

特定地址空间基元的参考实现
==========================

基本访问监测的低级基元被定义为两部分。:

1. 确定地址空间的监测目标地址范围
2. 目标空间中特定地址范围的访问检查。

DAMON目前为物理和虚拟地址空间提供了基元的实现。下面两个小节描述了这些工作的方式。

基于VMA的目标地址范围构造
-------------------------

这仅仅是针对虚拟地址空间基元的实现。对于物理地址空间，只是要求用户手动设置监控目标地址范围。

在进程的超级巨大的虚拟地址空间中，只有小部分被映射到物理内存并被访问。因此，跟踪未映射的地
址区域只是一种浪费。然而，由于DAMON可以使用自适应区域调整机制来处理一定程度的噪声，所以严
格来说，跟踪每一个映射并不是必须的，但在某些情况下甚至会产生很高的开销。也就是说，监测目标
内部过于巨大的未映射区域应该被移除，以不占用自适应机制的时间。

出于这个原因，这个实现将复杂的映射转换为三个不同的区域，覆盖地址空间的每个映射区域。这三个
区域之间的两个空隙是给定地址空间中两个最大的未映射区域。这两个最大的未映射区域是堆和最上面
的mmap()区域之间的间隙，以及在大多数情况下最下面的mmap()区域和堆之间的间隙。因为这些间隙
在通常的地址空间中是异常巨大的，排除这些间隙就足以做出合理的权衡。下面详细说明了这一点::

    <heap>
    <BIG UNMAPPED REGION 1>
    <uppermost mmap()-ed region>
    (small mmap()-ed regions and munmap()-ed regions)
    <lowermost mmap()-ed region>
    <BIG UNMAPPED REGION 2>
    <stack>

基于PTE访问位的访问检查
-----------------------

物理和虚拟地址空间的实现都使用PTE Accessed-bit进行基本访问检查。唯一的区别在于从地址中
找到相关的PTE访问位的方式。虚拟地址的实现是为该地址的目标任务查找页表，而物理地址的实现则
是查找与该地址有映射关系的每一个页表。通过这种方式，实现者找到并清除下一个采样目标地址的位，
并检查该位是否在一个采样周期后再次设置。这可能会干扰其他使用访问位的内核子系统，即空闲页跟
踪和回收逻辑。为了避免这种干扰，DAMON使其与空闲页面跟踪相互排斥，并使用 ``PG_idle`` 和
``PG_young`` 页面标志来解决与回收逻辑的冲突，就像空闲页面跟踪那样。

独立于地址空间的核心机制
========================

下面四个部分分别描述了DAMON的核心机制和五个监测属性，即 ``采样间隔`` 、 ``聚集间隔`` 、
``更新间隔`` 、 ``最小区域数`` 和 ``最大区域数`` 。

访问频率监测
------------

DAMON的输出显示了在给定的时间内哪些页面的访问频率是多少。访问频率的分辨率是通过设置
``采样间隔`` 和 ``聚集间隔`` 来控制的。详细地说，DAMON检查每个 ``采样间隔`` 对每
个页面的访问，并将结果汇总。换句话说，计算每个页面的访问次数。在每个 ``聚合间隔`` 过
去后，DAMON调用先前由用户注册的回调函数，以便用户可以阅读聚合的结果，然后再清除这些结
果。这可以用以下简单的伪代码来描述::

    while monitoring_on:
        for page in monitoring_target:
            if accessed(page):
                nr_accesses[page] += 1
        if time() % aggregation_interval == 0:
            for callback in user_registered_callbacks:
                callback(monitoring_target, nr_accesses)
            for page in monitoring_target:
                nr_accesses[page] = 0
        sleep(sampling interval)

这种机制的监测开销将随着目标工作负载规模的增长而任意增加。

基于区域的抽样调查
------------------

为了避免开销的无限制增加，DAMON将假定具有相同访问频率的相邻页面归入一个区域。只要保持
这个假设（一个区域内的页面具有相同的访问频率），该区域内就只需要检查一个页面。因此，对
于每个 ``采样间隔`` ，DAMON在每个区域中随机挑选一个页面，等待一个 ``采样间隔`` ，检
查该页面是否同时被访问，如果被访问则增加该区域的访问频率。因此，监测开销是可以通过设置
区域的数量来控制的。DAMON允许用户设置最小和最大的区域数量来进行权衡。

然而，如果假设没有得到保证，这个方案就不能保持输出的质量。

适应性区域调整
--------------

即使最初的监测目标区域被很好地构建以满足假设（同一区域内的页面具有相似的访问频率），数
据访问模式也会被动态地改变。这将导致监测质量下降。为了尽可能地保持假设，DAMON根据每个
区域的访问频率自适应地进行合并和拆分。

对于每个 ``聚集区间`` ，它比较相邻区域的访问频率，如果频率差异较小，就合并这些区域。
然后，在它报告并清除每个区域的聚合接入频率后，如果区域总数不超过用户指定的最大区域数，
它将每个区域拆分为两个或三个区域。

通过这种方式，DAMON提供了其最佳的质量和最小的开销，同时保持了用户为其权衡设定的界限。

动态目标空间更新处理
--------------------

监测目标地址范围可以动态改变。例如，虚拟内存可以动态地被映射和解映射。物理内存可以被
热插拔。

由于在某些情况下变化可能相当频繁，DAMON允许监控操作检查动态变化，包括内存映射变化，
并仅在用户指定的时间间隔（ ``更新间隔`` ）中的每个时间段，将其应用于监控操作相关的
数据结构，如抽象的监控目标内存区。


code:
.. SPDX-License-Identifier: GPL-2.0

:Original: Documentation/mm/damon/faq.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

========
常见问题
========

为什么是一个新的子系统，而不是扩展perf或其他用户空间工具？
==========================================================

首先，因为它需要尽可能的轻量级，以便可以在线使用，所以应该避免任何不必要的开销，如内核-用户
空间的上下文切换成本。第二，DAMON的目标是被包括内核在内的其他程序所使用。因此，对特定工具
（如perf）的依赖性是不可取的。这就是DAMON在内核空间实现的两个最大的原因。

“闲置页面跟踪” 或 “perf mem” 可以替代DAMON吗？
==============================================

闲置页跟踪是物理地址空间访问检查的一个低层次的原始方法。“perf mem”也是类似的，尽管它可以
使用采样来减少开销。另一方面，DAMON是一个更高层次的框架，用于监控各种地址空间。它专注于内
存管理优化，并提供复杂的精度/开销处理机制。因此，“空闲页面跟踪” 和 “perf mem” 可以提供
DAMON输出的一个子集，但不能替代DAMON。

DAMON是否只支持虚拟内存？
=========================

不，DAMON的核心是独立于地址空间的。用户可以在DAMON核心上实现和配置特定地址空间的低级原始
部分，包括监测目标区域的构造和实际的访问检查。通过这种方式，DAMON用户可以用任何访问检查技
术来监测任何地址空间。

尽管如此，DAMON默认为虚拟内存和物理内存提供了基于vma/rmap跟踪和PTE访问位检查的地址空间
相关功能的实现，以供参考和方便使用。

我可以简单地监测页面的粒度吗？
==============================

是的，你可以通过设置 ``min_nr_regions`` 属性高于工作集大小除以页面大小的值来实现。
因为监视目标区域的大小被强制为 ``>=page size`` ，所以区域分割不会产生任何影响。


code:
.. SPDX-License-Identifier: GPL-2.0

:Original: Documentation/mm/damon/index.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

==========================
DAMON:数据访问监视器
==========================

DAMON是Linux内核的一个数据访问监控框架子系统。DAMON的核心机制使其成为
（该核心机制详见(Documentation/translations/zh_CN/mm/damon/design.rst)）

 - *准确度* （监测输出对DRAM级别的内存管理足够有用；但可能不适合CPU Cache级别），
 - *轻量级* （监控开销低到可以在线应用），以及
 - *可扩展* （无论目标工作负载的大小，开销的上限值都在恒定范围内）。

因此，利用这个框架，内核的内存管理机制可以做出高级决策。会导致高数据访问监控开销的实
验性内存管理优化工作可以再次进行。同时，在用户空间，有一些特殊工作负载的用户可以编写
个性化的应用程序，以便更好地了解和优化他们的工作负载和系统。

.. toctree::
   :maxdepth: 2

   faq
   design
   api


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mm/free_page_reporting.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

==========
空闲页报告
==========

空闲页报告是一个API，设备可以通过它来注册接收系统当前未使用的页面列表。这在虚拟
化的情况下是很有用的，客户机能够使用这些数据来通知管理器它不再使用内存中的某些页
面。

对于驱动，通常是气球驱动要使用这个功能，它将分配和初始化一个page_reporting_dev_info
结构体。它要填充的结构体中的字段是用于处理散点列表的 "report" 函数指针。它还必
须保证每次调用该函数时能处理至少相当于PAGE_REPORTING_CAPACITY的散点列表条目。
假设没有其他页面报告设备已经注册， 对page_reporting_register的调用将向报告框
架注册页面报告接口。

一旦注册，页面报告API将开始向驱动报告成批的页面。API将在接口被注册后2秒开始报告
页面，并在任何足够高的页面被释放之后2秒继续报告。

报告的页面将被存储在传递给报告函数的散列表中，最后一个条目的结束位被设置在条目
nent-1中。 当页面被报告函数处理时，分配器将无法访问它们。一旦报告函数完成，这些
页将被返回到它们所获得的自由区域。

在移除使用空闲页报告的驱动之前，有必要调用page_reporting_unregister，以移除
目前被空闲页报告使用的page_reporting_dev_info结构体。这样做将阻止进一步的报
告通过该接口发出。如果另一个驱动或同一驱动被注册，它就有可能恢复前一个驱动在报告
空闲页方面的工作。

Alexander Duyck, 2019年12月04日


code:
:Original: Documentation/mm/frontswap.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

=========
Frontswap
=========

Frontswap为交换页提供了一个 “transcendent memory” 的接口。在一些环境中，由
于交换页被保存在RAM（或类似RAM的设备）中，而不是交换磁盘，因此可以获得巨大的性能
节省（提高）。

.. _Transcendent memory in a nutshell: https://lwn.net/Articles/454795/

Frontswap之所以这么命名，是因为它可以被认为是与swap设备的“back”存储相反。存
储器被认为是一个同步并发安全的面向页面的“伪RAM设备”，符合transcendent memory
（如Xen的“tmem”，或内核内压缩内存，又称“zcache”，或未来的类似RAM的设备）的要
求；这个伪RAM设备不能被内核直接访问或寻址，其大小未知且可能随时间变化。驱动程序通过
调用frontswap_register_ops将自己与frontswap链接起来，以适当地设置frontswap_ops
的功能，它提供的功能必须符合某些策略，如下所示:

一个 “init” 将设备准备好接收与指定的交换设备编号（又称“类型”）相关的frontswap
交换页。一个 “store” 将把该页复制到transcendent memory，并与该页的类型和偏移
量相关联。一个 “load” 将把该页，如果找到的话，从transcendent memory复制到内核
内存，但不会从transcendent memory中删除该页。一个 “invalidate_page” 将从
transcendent memory中删除该页，一个 “invalidate_area” 将删除所有与交换类型
相关的页（例如，像swapoff）并通知 “device” 拒绝进一步存储该交换类型。

一旦一个页面被成功存储，在该页面上的匹配加载通常会成功。因此，当内核发现自己处于需
要交换页面的情况时，它首先尝试使用frontswap。如果存储的结果是成功的，那么数据就已
经成功的保存到了transcendent memory中，并且避免了磁盘写入，如果后来再读回数据，
也避免了磁盘读取。如果存储返回失败，transcendent memory已经拒绝了该数据，且该页
可以像往常一样被写入交换空间。

请注意，如果一个页面被存储，而该页面已经存在于transcendent memory中（一个 “重复”
的存储），要么存储成功，数据被覆盖，要么存储失败，该页面被废止。这确保了旧的数据永远
不会从frontswap中获得。

如果配置正确，对frontswap的监控是通过 `/sys/kernel/debug/frontswap` 目录下的
debugfs完成的。frontswap的有效性可以通过以下方式测量（在所有交换设备中）:

``failed_stores``
	有多少次存储的尝试是失败的

``loads``
	尝试了多少次加载（应该全部成功）

``succ_stores``
	有多少次存储的尝试是成功的

``invalidates``
	尝试了多少次作废

后台实现可以提供额外的指标。

经常问到的问题
==============

* 价值在哪里?

当一个工作负载开始交换时，性能就会下降。Frontswap通过提供一个干净的、动态的接口来
读取和写入交换页到 “transcendent memory”，从而大大增加了许多这样的工作负载的性
能，否则内核是无法直接寻址的。当数据被转换为不同的形式和大小（比如压缩）或者被秘密
移动（对于一些类似RAM的设备来说，这可能对写平衡很有用）时，这个接口是理想的。交换
页（和被驱逐的页面缓存页）是这种比RAM慢但比磁盘快得多的“伪RAM设备”的一大用途。

Frontswap对内核的影响相当小，为各种系统配置中更动态、更灵活的RAM利用提供了巨大的
灵活性：

在单一内核的情况下，又称“zcache”，页面被压缩并存储在本地内存中，从而增加了可以安
全保存在RAM中的匿名页面总数。Zcache本质上是用压缩/解压缩的CPU周期换取更好的内存利
用率。Benchmarks测试显示，当内存压力较低时，几乎没有影响，而在高内存压力下的一些
工作负载上，则有明显的性能改善（25%以上）。

“RAMster” 在zcache的基础上增加了对集群系统的 “peer-to-peer” transcendent memory
的支持。Frontswap页面像zcache一样被本地压缩，但随后被“remotified” 到另一个系
统的RAM。这使得RAM可以根据需要动态地来回负载平衡，也就是说，当系统A超载时，它可以
交换到系统B，反之亦然。RAMster也可以被配置成一个内存服务器，因此集群中的许多服务器
可以根据需要动态地交换到配置有大量内存的单一服务器上......而不需要预先配置每个客户
有多少内存可用

在虚拟情况下，虚拟化的全部意义在于统计地将物理资源在多个虚拟机的不同需求之间进行复
用。对于RAM来说，这真的很难做到，而且在不改变内核的情况下，要做好这一点的努力基本上
是失败的（除了一些广为人知的特殊情况下的工作负载）。具体来说，Xen Transcendent Memory
后端允许管理器拥有的RAM “fallow”，不仅可以在多个虚拟机之间进行“time-shared”，
而且页面可以被压缩和重复利用，以优化RAM的利用率。当客户操作系统被诱导交出未充分利用
的RAM时（如 “selfballooning”），突然出现的意外内存压力可能会导致交换；frontswap
允许这些页面被交换到管理器RAM中或从管理器RAM中交换（如果整体主机系统内存条件允许），
从而减轻计划外交换可能带来的可怕的性能影响。

一个KVM的实现正在进行中，并且已经被RFC'ed到lkml。而且，利用frontswap，对NVM作为
内存扩展技术的调查也在进行中。

* 当然，在某些情况下可能有性能上的优势，但frontswap的空间/时间开销是多少？

如果 CONFIG_FRONTSWAP 被禁用，每个 frontswap 钩子都会编译成空，唯一的开销是每
个 swapon'ed swap 设备的几个额外字节。如果 CONFIG_FRONTSWAP 被启用，但没有
frontswap的 “backend” 寄存器，每读或写一个交换页就会有一个额外的全局变量，而不
是零。如果 CONFIG_FRONTSWAP 被启用，并且有一个frontswap的backend寄存器，并且
后端每次 “store” 请求都失败（即尽管声称可能，但没有提供内存），CPU 的开销仍然可以
忽略不计 - 因为每次frontswap失败都是在交换页写到磁盘之前，系统很可能是 I/O 绑定
的，无论如何使用一小部分的 CPU 都是不相关的。

至于空间，如果CONFIG_FRONTSWAP被启用，并且有一个frontswap的backend注册，那么
每个交换设备的每个交换页都会被分配一个比特。这是在内核已经为每个交换设备的每个交换
页分配的8位（在2.6.34之前是16位）上增加的。(Hugh Dickins观察到，frontswap可能
会偷取现有的8个比特，但是我们以后再来担心这个小的优化问题)。对于标准的4K页面大小的
非常大的交换盘（这很罕见），这是每32GB交换盘1MB开销。

当交换页存储在transcendent memory中而不是写到磁盘上时，有一个副作用，即这可能会
产生更多的内存压力，有可能超过其他的优点。一个backend，比如zcache，必须实现策略
来仔细（但动态地）管理内存限制，以确保这种情况不会发生。

* 好吧，那就用内核骇客能理解的术语来快速概述一下这个frontswap补丁的作用如何？

我们假设在内核初始化过程中，一个frontswap 的 “backend” 已经注册了；这个注册表
明这个frontswap 的 “backend” 可以访问一些不被内核直接访问的“内存”。它到底提
供了多少内存是完全动态和随机的。

每当一个交换设备被交换时，就会调用frontswap_init()，把交换设备的编号（又称“类
型”）作为一个参数传给它。这就通知了frontswap，以期待 “store” 与该号码相关的交
换页的尝试。

每当交换子系统准备将一个页面写入交换设备时（参见swap_writepage()），就会调用
frontswap_store。Frontswap与frontswap backend协商，如果backend说它没有空
间，frontswap_store返回-1，内核就会照常把页换到交换设备上。注意，来自frontswap
backend的响应对内核来说是不可预测的；它可能选择从不接受一个页面，可能接受每九个
页面，也可能接受每一个页面。但是如果backend确实接受了一个页面，那么这个页面的数
据已经被复制并与类型和偏移量相关联了，而且backend保证了数据的持久性。在这种情况
下，frontswap在交换设备的“frontswap_map” 中设置了一个位，对应于交换设备上的
页面偏移量，否则它就会将数据写入该设备。

当交换子系统需要交换一个页面时（swap_readpage()），它首先调用frontswap_load()，
检查frontswap_map，看这个页面是否早先被frontswap backend接受。如果是，该页
的数据就会从frontswap后端填充，换入就完成了。如果不是，正常的交换代码将被执行，
以便从真正的交换设备上获得这一页的数据。

所以每次frontswap backend接受一个页面时，交换设备的读取和（可能）交换设备的写
入都被 “frontswap backend store” 和（可能）“frontswap backend loads”
所取代，这可能会快得多。

* frontswap不能被配置为一个 “特殊的” 交换设备，它的优先级要高于任何真正的交换
  设备（例如像zswap，或者可能是swap-over-nbd/NFS）？

首先，现有的交换子系统不允许有任何种类的交换层次结构。也许它可以被重写以适应层次
结构，但这将需要相当大的改变。即使它被重写，现有的交换子系统也使用了块I/O层，它
假定交换设备是固定大小的，其中的任何页面都是可线性寻址的。Frontswap几乎没有触
及现有的交换子系统，而是围绕着块I/O子系统的限制，提供了大量的灵活性和动态性。

例如，frontswap backend对任何交换页的接受是完全不可预测的。这对frontswap backend
的定义至关重要，因为它赋予了backend完全动态的决定权。在zcache中，人们无法预
先知道一个页面的可压缩性如何。可压缩性 “差” 的页面会被拒绝，而 “差” 本身也可
以根据当前的内存限制动态地定义。

此外，frontswap是完全同步的，而真正的交换设备，根据定义，是异步的，并且使用
块I/O。块I/O层不仅是不必要的，而且可能进行 “优化”，这对面向RAM的设备来说是
不合适的，包括将一些页面的写入延迟相当长的时间。同步是必须的，以确保后端的动
态性，并避免棘手的竞争条件，这将不必要地大大增加frontswap和/或块I/O子系统的
复杂性。也就是说，只有最初的 “store” 和 “load” 操作是需要同步的。一个独立
的异步线程可以自由地操作由frontswap存储的页面。例如，RAMster中的 “remotification”
线程使用标准的异步内核套接字，将压缩的frontswap页面移动到远程机器。同样，
KVM的客户方实现可以进行客户内压缩，并使用 “batched” hypercalls。

在虚拟化环境中，动态性允许管理程序（或主机操作系统）做“intelligent overcommit”。
例如，它可以选择只接受页面，直到主机交换可能即将发生，然后强迫客户机做他们
自己的交换。

transcendent memory规格的frontswap有一个坏处。因为任何 “store” 都可
能失败，所以必须在一个真正的交换设备上有一个真正的插槽来交换页面。因此，
frontswap必须作为每个交换设备的 “影子” 来实现，它有可能容纳交换设备可能
容纳的每一个页面，也有可能根本不容纳任何页面。这意味着frontswap不能包含比
swap设备总数更多的页面。例如，如果在某些安装上没有配置交换设备，frontswap
就没有用。无交换设备的便携式设备仍然可以使用frontswap，但是这种设备的
backend必须配置某种 “ghost” 交换设备，并确保它永远不会被使用。

* 为什么会有这种关于 “重复存储” 的奇怪定义？如果一个页面以前被成功地存储过，
  难道它不能总是被成功地覆盖吗？

几乎总是可以的，不，有时不能。考虑一个例子，数据被压缩了，原来的4K页面被压
缩到了1K。现在，有人试图用不可压缩的数据覆盖该页，因此会占用整个4K。但是
backend没有更多的空间了。在这种情况下，这个存储必须被拒绝。每当frontswap
拒绝一个会覆盖的存储时，它也必须使旧的数据作废，并确保它不再被访问。因为交
换子系统会把新的数据写到读交换设备上，这是确保一致性的正确做法。

* 为什么frontswap补丁会创建新的头文件swapfile.h？

frontswap代码依赖于一些swap子系统内部的数据结构，这些数据结构多年来一直
在静态和全局之间来回移动。这似乎是一个合理的妥协：将它们定义为全局，但在一
个新的包含文件中声明它们，该文件不被包含swap.h的大量源文件所包含。

Dan Magenheimer，最后更新于2012年4月9日


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mm/highmem.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

==========
高内存处理
==========

作者: Peter Zijlstra <a.p.zijlstra@chello.nl>

.. contents:: :local:

高内存是什么？
==============

当物理内存的大小接近或超过虚拟内存的最大大小时，就会使用高内存（highmem）。在这一点上，内
核不可能在任何时候都保持所有可用的物理内存的映射。这意味着内核需要开始使用它想访问的物理内
存的临时映射。

没有被永久映射覆盖的那部分（物理）内存就是我们所说的 "高内存"。对于这个边界的确切位置，有
各种架构上的限制。

例如，在i386架构中，我们选择将内核映射到每个进程的虚拟空间，这样我们就不必为内核的进入/退
出付出全部的TLB作废代价。这意味着可用的虚拟内存空间（i386上为4GiB）必须在用户和内核空间之
间进行划分。

使用这种方法的架构的传统分配方式是3:1，3GiB用于用户空间，顶部的1GiB用于内核空间。::

		+--------+ 0xffffffff
		| Kernel |
		+--------+ 0xc0000000
		|        |
		| User   |
		|        |
		+--------+ 0x00000000

这意味着内核在任何时候最多可以映射1GiB的物理内存，但是由于我们需要虚拟地址空间来做其他事
情--包括访问其余物理内存的临时映射--实际的直接映射通常会更少（通常在~896MiB左右）。

其他有mm上下文标签的TLB的架构可以有独立的内核和用户映射。然而，一些硬件（如一些ARM）在使
用mm上下文标签时，其虚拟空间有限。

临时虚拟映射
============

内核包含几种创建临时映射的方法。下面的列表按照使用的优先顺序显示了它们。

* kmap_local_page()。这个函数是用来要求短期映射的。它可以从任何上下文（包括中断）中调用，
  但是映射只能在获取它们的上下文中使用。

  在可行的情况下，这个函数应该比其他所有的函数优先使用。

  这些映射是线程本地和CPU本地的，这意味着映射只能从这个线程中访问，并且当映射处于活动状
  态时，该线程与CPU绑定。即使线程被抢占了（因为抢占永远不会被函数禁用），CPU也不能通过
  CPU-hotplug从系统中拔出，直到映射被处理掉。

  在本地的kmap区域中采取pagefaults是有效的，除非获取本地映射的上下文由于其他原因不允许
  这样做。

  kmap_local_page()总是返回一个有效的虚拟地址，并且假定kunmap_local()不会失败。

  嵌套kmap_local_page()和kmap_atomic()映射在一定程度上是允许的（最多到KMAP_TYPE_NR），
  但是它们的调用必须严格排序，因为映射的实现是基于堆栈的。关于如何管理嵌套映射的细节，
  请参见kmap_local_page() kdocs（包含在 "函数 "部分）。

* kmap_atomic().  这允许对单个页面进行非常短的时间映射。由于映射被限制在发布它的CPU上，
  它表现得很好，但发布的任务因此被要求留在该CPU上直到它完成，以免其他任务取代它的映射。

  kmap_atomic()也可以被中断上下文使用，因为它不睡眠，调用者也可能在调用kunmap_atomic()
  后才睡眠。

  内核中对kmap_atomic()的每次调用都会创建一个不可抢占的段，并禁用缺页异常。这可能是
  未预期延迟的来源之一。因此用户应该选择kmap_local_page()而不是kmap_atomic()。

  假设k[un]map_atomic()不会失败。

* kmap()。这应该被用来对单个页面进行短时间的映射，对抢占或迁移没有限制。它会带来开销，
  因为映射空间是受限制的，并且受到全局锁的保护，以实现同步。当不再需要映射时，必须用
  kunmap()释放该页被映射的地址。

  映射变化必须广播到所有CPU（核）上，kmap()还需要在kmap的池被回绕（TLB项用光了，需要从第
   一项复用）时进行全局TLB无效化，当映射空间被完全利用时，它可能会阻塞，直到有一个可用的
   槽出现。因此，kmap()只能从可抢占的上下文中调用。

  如果一个映射必须持续相对较长的时间，上述所有的工作都是必要的，但是内核中大部分的
  高内存映射都是短暂的，而且只在一个地方使用。这意味着在这种情况下，kmap()的成本大
  多被浪费了。kmap()并不是为长期映射而设计的，但是它已经朝着这个方向发展了，在较新
  的代码中强烈不鼓励使用它，前面的函数集应该是首选。

  在64位系统中，调用kmap_local_page()、kmap_atomic()和kmap()没有实际作用，因为64位
  地址空间足以永久映射所有物理内存页面。

* vmap()。这可以用来将多个物理页长期映射到一个连续的虚拟空间。它需要全局同步来解除
  映射。

临时映射的成本
==============

创建临时映射的代价可能相当高。体系架构必须操作内核的页表、数据TLB和/或MMU的寄存器。

如果CONFIG_HIGHMEM没有被设置，那么内核会尝试用一点计算来创建映射，将页面结构地址转换成
指向页面内容的指针，而不是去捣鼓映射。在这种情况下，解映射操作可能是一个空操作。

如果CONFIG_MMU没有被设置，那么就不可能有临时映射和高内存。在这种情况下，也将使用计算方法。

i386 PAE
========

在某些情况下，i386 架构将允许你在 32 位机器上安装多达 64GiB 的内存。但这有一些后果:

* Linux需要为系统中的每个页面建立一个页帧结构，而且页帧需要驻在永久映射中，这意味着：

* 你最多可以有896M/sizeof(struct page)页帧；由于页结构体是32字节的，所以最终会有
  112G的页；然而，内核需要在内存中存储更多的页帧......

* PAE使你的页表变大--这使系统变慢，因为更多的数据需要在TLB填充等方面被访问。一个好处
  是，PAE有更多的PTE位，可以提供像NX和PAT这样的高级功能。

一般的建议是，你不要在32位机器上使用超过8GiB的空间--尽管更多的空间可能对你和你的工作
量有用，但你几乎是靠你自己--不要指望内核开发者真的会很关心事情的进展情况。

函数
====

该API在以下内核代码中:

include/linux/highmem.h

include/linux/highmem-internal.h


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mm/hmm.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

==================
异构内存管理 (HMM)
==================

提供基础设施和帮助程序以将非常规内存（设备内存，如板上 GPU 内存）集成到常规内核路径中，其
基石是此类内存的专用struct page（请参阅本文档的第 5 至 7 节）。

HMM 还为 SVM（共享虚拟内存）提供了可选的帮助程序，即允许设备透明地访问与 CPU 一致的程序
地址，这意味着 CPU 上的任何有效指针也是该设备的有效指针。这对于简化高级异构计算的使用变得
必不可少，其中 GPU、DSP 或 FPGA 用于代表进程执行各种计算。

本文档分为以下部分：在第一部分中，我揭示了与使用特定于设备的内存分配器相关的问题。在第二
部分中，我揭示了许多平台固有的硬件限制。第三部分概述了 HMM 设计。第四部分解释了 CPU 页
表镜像的工作原理以及 HMM 在这种情况下的目的。第五部分处理内核中如何表示设备内存。最后，
最后一节介绍了一个新的迁移助手，它允许利用设备 DMA 引擎。

.. contents:: :local:

使用特定于设备的内存分配器的问题
================================

具有大量板载内存（几 GB）的设备（如 GPU）历来通过专用驱动程序特定 API 管理其内存。这会
造成设备驱动程序分配和管理的内存与常规应用程序内存（私有匿名、共享内存或常规文件支持内存）
之间的隔断。从这里开始，我将把这个方面称为分割的地址空间。我使用共享地址空间来指代相反的情况：
即，设备可以透明地使用任何应用程序内存区域。

分割的地址空间的发生是因为设备只能访问通过设备特定 API 分配的内存。这意味着从设备的角度来
看，程序中的所有内存对象并不平等，这使得依赖于广泛的库的大型程序变得复杂。

具体来说，这意味着想要利用像 GPU 这样的设备的代码需要在通用分配的内存（malloc、mmap
私有、mmap 共享）和通过设备驱动程序 API 分配的内存之间复制对象（这仍然以 mmap 结束，
但是是设备文件）。

对于平面数据集（数组、网格、图像……），这并不难实现，但对于复杂数据集（列表、树……），
很难做到正确。复制一个复杂的数据集需要重新映射其每个元素之间的所有指针关系。这很容易出错，
而且由于数据集和地址的重复，程序更难调试。

分割地址空间也意味着库不能透明地使用它们从核心程序或另一个库中获得的数据，因此每个库可能
不得不使用设备特定的内存分配器来重复其输入数据集。大型项目会因此受到影响，并因为各种内存
拷贝而浪费资源。

复制每个库的API以接受每个设备特定分配器分配的内存作为输入或输出，并不是一个可行的选择。
这将导致库入口点的组合爆炸。

最后，随着高级语言结构（在 C++ 中，当然也在其他语言中）的进步，编译器现在有可能在没有程
序员干预的情况下利用 GPU 和其他设备。某些编译器识别的模式仅适用于共享地址空间。对所有
其他模式，使用共享地址空间也更合理。

I/O 总线、设备内存特性
======================

由于一些限制，I/O 总线削弱了共享地址空间。大多数 I/O 总线只允许从设备到主内存的基本
内存访问；甚至缓存一致性通常是可选的。从 CPU 访问设备内存甚至更加有限。通常情况下，它
不是缓存一致的。

如果我们只考虑 PCIE 总线，那么设备可以访问主内存（通常通过 IOMMU）并与 CPU 缓存一
致。但是，它只允许设备对主存储器进行一组有限的原子操作。这在另一个方向上更糟：CPU
只能访问有限范围的设备内存，而不能对其执行原子操作。因此，从内核的角度来看，设备内存不
能被视为与常规内存等同。

另一个严重的因素是带宽有限（约 32GBytes/s，PCIE 4.0 和 16 通道）。这比最快的 GPU
内存 (1 TBytes/s) 慢 33 倍。最后一个限制是延迟。从设备访问主内存的延迟比设备访问自
己的内存时高一个数量级。

一些平台正在开发新的 I/O 总线或对 PCIE 的添加/修改以解决其中一些限制
（OpenCAPI、CCIX）。它们主要允许 CPU 和设备之间的双向缓存一致性，并允许架构支持的所
有原子操作。遗憾的是，并非所有平台都遵循这一趋势，并且一些主要架构没有针对这些问题的硬
件解决方案。

因此，为了使共享地址空间有意义，我们不仅必须允许设备访问任何内存，而且还必须允许任何内
存在设备使用时迁移到设备内存（在迁移时阻止 CPU 访问）。

共享地址空间和迁移
==================

HMM 打算提供两个主要功能。第一个是通过复制cpu页表到设备页表中来共享地址空间，因此对
于进程地址空间中的任何有效主内存地址，相同的地址指向相同的物理内存。

为了实现这一点，HMM 提供了一组帮助程序来填充设备页表，同时跟踪 CPU 页表更新。设备页表
更新不像 CPU 页表更新那么容易。要更新设备页表，您必须分配一个缓冲区（或使用预先分配的
缓冲区池）并在其中写入 GPU 特定命令以执行更新（取消映射、缓存失效和刷新等）。这不能通
过所有设备的通用代码来完成。因此，为什么HMM提供了帮助器，在把硬件的具体细节留给设备驱
动程序的同时，把一切可以考虑的因素都考虑进去了。

HMM 提供的第二种机制是一种新的 ZONE_DEVICE 内存，它允许为设备内存的每个页面分配一个
struct page。这些页面很特殊，因为 CPU 无法映射它们。然而，它们允许使用现有的迁移机
制将主内存迁移到设备内存，从 CPU 的角度来看，一切看起来都像是换出到磁盘的页面。使用
struct page可以与现有的 mm 机制进行最简单、最干净的集成。再次，HMM 仅提供帮助程序，
首先为设备内存热插拔新的 ZONE_DEVICE 内存，然后执行迁移。迁移内容和时间的策略决定留
给设备驱动程序。

请注意，任何 CPU 对设备页面的访问都会触发缺页异常并迁移回主内存。例如，当支持给定CPU
地址 A 的页面从主内存页面迁移到设备页面时，对地址 A 的任何 CPU 访问都会触发缺页异常
并启动向主内存的迁移。

凭借这两个特性，HMM 不仅允许设备镜像进程地址空间并保持 CPU 和设备页表同步，而且还通
过迁移设备正在使用的数据集部分来利用设备内存。

地址空间镜像实现和API
=====================

地址空间镜像的主要目标是允许将一定范围的 CPU 页表复制到一个设备页表中；HMM 有助于
保持两者同步。想要镜像进程地址空间的设备驱动程序必须从注册 mmu_interval_notifier
开始::

 int mmu_interval_notifier_insert(struct mmu_interval_notifier *interval_sub,
				  struct mm_struct *mm, unsigned long start,
				  unsigned long length,
				  const struct mmu_interval_notifier_ops *ops);

在 ops->invalidate() 回调期间，设备驱动程序必须对范围执行更新操作（将范围标记为只
读，或完全取消映射等）。设备必须在驱动程序回调返回之前完成更新。

当设备驱动程序想要填充一个虚拟地址范围时，它可以使用::

  int hmm_range_fault(struct hmm_range *range);

如果请求写访问，它将在丢失或只读条目上触发缺页异常（见下文）。缺页异常使用通用的 mm 缺
页异常代码路径，就像 CPU 缺页异常一样。

这两个函数都将 CPU 页表条目复制到它们的 pfns 数组参数中。该数组中的每个条目对应于虚拟
范围中的一个地址。HMM 提供了一组标志来帮助驱动程序识别特殊的 CPU 页表项。

在 sync_cpu_device_pagetables() 回调中锁定是驱动程序必须尊重的最重要的方面，以保
持事物正确同步。使用模式是::

 int driver_populate_range(...)
 {
      struct hmm_range range;
      ...

      range.notifier = &interval_sub;
      range.start = ...;
      range.end = ...;
      range.hmm_pfns = ...;

      if (!mmget_not_zero(interval_sub->notifier.mm))
          return -EFAULT;

 again:
      range.notifier_seq = mmu_interval_read_begin(&interval_sub);
      mmap_read_lock(mm);
      ret = hmm_range_fault(&range);
      if (ret) {
          mmap_read_unlock(mm);
          if (ret == -EBUSY)
                 goto again;
          return ret;
      }
      mmap_read_unlock(mm);

      take_lock(driver->update);
      if (mmu_interval_read_retry(&ni, range.notifier_seq) {
          release_lock(driver->update);
          goto again;
      }

      /* Use pfns array content to update device page table,
       * under the update lock */

      release_lock(driver->update);
      return 0;
 }

driver->update 锁与驱动程序在其 invalidate() 回调中使用的锁相同。该锁必须在调用
mmu_interval_read_retry() 之前保持，以避免与并发 CPU 页表更新发生任何竞争。

利用 default_flags 和 pfn_flags_mask
====================================

hmm_range 结构有 2 个字段，default_flags 和 pfn_flags_mask，它们指定整个范围
的故障或快照策略，而不必为 pfns 数组中的每个条目设置它们。

例如，如果设备驱动程序需要至少具有读取权限的范围的页面，它会设置::

    range->default_flags = HMM_PFN_REQ_FAULT;
    range->pfn_flags_mask = 0;

并如上所述调用 hmm_range_fault()。这将填充至少具有读取权限的范围内的所有页面。

现在假设驱动程序想要做同样的事情，除了它想要拥有写权限的范围内的一页。现在驱动程序设
置::

    range->default_flags = HMM_PFN_REQ_FAULT;
    range->pfn_flags_mask = HMM_PFN_REQ_WRITE;
    range->pfns[index_of_write] = HMM_PFN_REQ_WRITE;

有了这个，HMM 将在至少读取（即有效）的所有页面中异常，并且对于地址
== range->start + (index_of_write << PAGE_SHIFT) 它将异常写入权限，即，如果
CPU pte 没有设置写权限，那么HMM将调用handle_mm_fault()。

hmm_range_fault 完成后，标志位被设置为页表的当前状态，即 HMM_PFN_VALID | 如果页
面可写，将设置 HMM_PFN_WRITE。

从核心内核的角度表示和管理设备内存
==================================

尝试了几种不同的设计来支持设备内存。第一个使用特定于设备的数据结构来保存有关迁移内存
的信息，HMM 将自身挂接到 mm 代码的各个位置，以处理对设备内存支持的地址的任何访问。
事实证明，这最终复制了 struct page 的大部分字段，并且还需要更新许多内核代码路径才
能理解这种新的内存类型。

大多数内核代码路径从不尝试访问页面后面的内存，而只关心struct page的内容。正因为如此，
HMM 切换到直接使用 struct page 用于设备内存，这使得大多数内核代码路径不知道差异。
我们只需要确保没有人试图从 CPU 端映射这些页面。

移入和移出设备内存
==================

由于 CPU 无法直接访问设备内存，因此设备驱动程序必须使用硬件 DMA 或设备特定的加载/存
储指令来迁移数据。migrate_vma_setup()、migrate_vma_pages() 和
migrate_vma_finalize() 函数旨在使驱动程序更易于编写并集中跨驱动程序的通用代码。

在将页面迁移到设备私有内存之前，需要创建特殊的设备私有 ``struct page`` 。这些将用
作特殊的“交换”页表条目，以便 CPU 进程在尝试访问已迁移到设备专用内存的页面时会发生异常。

这些可以通过以下方式分配和释放::

    struct resource *res;
    struct dev_pagemap pagemap;

    res = request_free_mem_region(&iomem_resource, /* number of bytes */,
                                  "name of driver resource");
    pagemap.type = MEMORY_DEVICE_PRIVATE;
    pagemap.range.start = res->start;
    pagemap.range.end = res->end;
    pagemap.nr_range = 1;
    pagemap.ops = &device_devmem_ops;
    memremap_pages(&pagemap, numa_node_id());

    memunmap_pages(&pagemap);
    release_mem_region(pagemap.range.start, range_len(&pagemap.range));

还有devm_request_free_mem_region(), devm_memremap_pages(),
devm_memunmap_pages() 和 devm_release_mem_region() 当资源可以绑定到 ``struct device``.

整体迁移步骤类似于在系统内存中迁移 NUMA 页面(see :ref:`Page migration <page_migration>`) ，
但这些步骤分为设备驱动程序特定代码和共享公共代码:

1. ``mmap_read_lock()``

   设备驱动程序必须将 ``struct vm_area_struct`` 传递给migrate_vma_setup()，
   因此需要在迁移期间保留 mmap_read_lock() 或 mmap_write_lock()。

2. ``migrate_vma_setup(struct migrate_vma *args)``

   设备驱动初始化了 ``struct migrate_vma`` 的字段，并将该指针传递给
   migrate_vma_setup()。``args->flags`` 字段是用来过滤哪些源页面应该被迁移。
   例如，设置 ``MIGRATE_VMA_SELECT_SYSTEM`` 将只迁移系统内存，设置
   ``MIGRATE_VMA_SELECT_DEVICE_PRIVATE`` 将只迁移驻留在设备私有内存中的页
   面。如果后者被设置， ``args->pgmap_owner`` 字段被用来识别驱动所拥有的设备
   私有页。这就避免了试图迁移驻留在其他设备中的设备私有页。目前，只有匿名的私有VMA
   范围可以被迁移到系统内存和设备私有内存。

   migrate_vma_setup()所做的第一步是用 ``mmu_notifier_invalidate_range_start()``
   和 ``mmu_notifier_invalidate_range_end()`` 调用来遍历设备周围的页表，使
   其他设备的MMU无效，以便在 ``args->src`` 数组中填写要迁移的PFN。
   ``invalidate_range_start()`` 回调传递给一个``struct mmu_notifier_range`` ，
   其 ``event`` 字段设置为MMU_NOTIFY_MIGRATE， ``owner`` 字段设置为传递给
   migrate_vma_setup()的 ``args->pgmap_owner`` 字段。这允许设备驱动跳过无
   效化回调，只无效化那些实际正在迁移的设备私有MMU映射。这一点将在下一节详细解释。

   在遍历页表时，一个 ``pte_none()`` 或 ``is_zero_pfn()`` 条目导致一个有效
   的  “zero” PFN 存储在 ``args->src`` 阵列中。这让驱动分配设备私有内存并清
   除它，而不是复制一个零页。到系统内存或设备私有结构页的有效PTE条目将被
   ``lock_page()``锁定，与LRU隔离（如果系统内存和设备私有页不在LRU上），从进
   程中取消映射，并插入一个特殊的迁移PTE来代替原来的PTE。 migrate_vma_setup()
   还清除了 ``args->dst`` 数组。

3. 设备驱动程序分配目标页面并将源页面复制到目标页面。

   驱动程序检查每个 ``src`` 条目以查看该 ``MIGRATE_PFN_MIGRATE`` 位是否已
   设置并跳过未迁移的条目。设备驱动程序还可以通过不填充页面的 ``dst`` 数组来选
   择跳过页面迁移。

   然后，驱动程序分配一个设备私有 struct page 或一个系统内存页，用 ``lock_page()``
   锁定该页，并将 ``dst`` 数组条目填入::

     dst[i] = migrate_pfn(page_to_pfn(dpage));

   现在驱动程序知道这个页面正在被迁移，它可以使设备私有 MMU 映射无效并将设备私有
   内存复制到系统内存或另一个设备私有页面。由于核心 Linux 内核会处理 CPU 页表失
   效，因此设备驱动程序只需使其自己的 MMU 映射失效。

   驱动程序可以使用 ``migrate_pfn_to_page(src[i])`` 来获取源设备的
   ``struct page`` 面，并将源页面复制到目标设备上，如果指针为 ``NULL`` ，意
   味着源页面没有被填充到系统内存中，则清除目标设备的私有内存。

4. ``migrate_vma_pages()``

   这一步是实际“提交”迁移的地方。

   如果源页是 ``pte_none()`` 或 ``is_zero_pfn()`` 页，这时新分配的页会被插
   入到CPU的页表中。如果一个CPU线程在同一页面上发生异常，这可能会失败。然而，页
   表被锁定，只有一个新页会被插入。如果它失去了竞争，设备驱动将看到
   ``MIGRATE_PFN_MIGRATE`` 位被清除。

   如果源页被锁定、隔离等，源 ``struct page`` 信息现在被复制到目标
   ``struct page`` ，最终完成CPU端的迁移。

5. 设备驱动为仍在迁移的页面更新设备MMU页表，回滚未迁移的页面。

   如果 ``src`` 条目仍然有 ``MIGRATE_PFN_MIGRATE`` 位被设置，设备驱动可以
   更新设备MMU，如果 ``MIGRATE_PFN_WRITE`` 位被设置，则设置写启用位。

6. ``migrate_vma_finalize()``

   这一步用新页的页表项替换特殊的迁移页表项，并释放对源和目的 ``struct page``
   的引用。

7. ``mmap_read_unlock()``

   现在可以释放锁了。

独占访问存储器
==============

一些设备具有诸如原子PTE位的功能，可以用来实现对系统内存的原子访问。为了支持对一
个共享的虚拟内存页的原子操作，这样的设备需要对该页的访问是排他的，而不是来自CPU
的任何用户空间访问。  ``make_device_exclusive_range()`` 函数可以用来使一
个内存范围不能从用户空间访问。

这将用特殊的交换条目替换给定范围内的所有页的映射。任何试图访问交换条目的行为都会
导致一个异常，该异常会通过用原始映射替换该条目而得到恢复。驱动程序会被通知映射已
经被MMU通知器改变，之后它将不再有对该页的独占访问。独占访问被保证持续到驱动程序
放弃页面锁和页面引用为止，这时页面上的任何CPU异常都可以按所述进行。

内存 cgroup (memcg) 和 rss 统计
===============================

目前，设备内存被视为 rss 计数器中的任何常规页面（如果设备页面用于匿名，则为匿名，
如果设备页面用于文件支持页面，则为文件，如果设备页面用于共享内存，则为 shmem）。
这是为了保持现有应用程序的故意选择，这些应用程序可能在不知情的情况下开始使用设备
内存，运行不受影响。

一个缺点是 OOM 杀手可能会杀死使用大量设备内存而不是大量常规系统内存的应用程序，
因此不会释放太多系统内存。在决定以不同方式计算设备内存之前，我们希望收集更多关
于应用程序和系统在存在设备内存的情况下在内存压力下如何反应的实际经验。

对内存 cgroup 做出了相同的决定。设备内存页面根据相同的内存 cgroup 计算，常规
页面将被计算在内。这确实简化了进出设备内存的迁移。这也意味着从设备内存迁移回常规
内存不会失败，因为它会超过内存 cgroup 限制。一旦我们对设备内存的使用方式及其对
内存资源控制的影响有了更多的了解，我们可能会在后面重新考虑这个选择。

请注意，设备内存永远不能由设备驱动程序或通过 GUP 固定，因此此类内存在进程退出时
总是被释放的。或者在共享内存或文件支持内存的情况下，当删除最后一个引用时。


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mm/hugetlbfs_reserv.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

==============
Hugetlbfs 预留
==============

概述
====

:ref:`hugetlbpage` 中描述的巨页通常是预先分配给应用程序使用的。如果VMA指
示要使用巨页，这些巨页会在缺页异常时被实例化到任务的地址空间。如果在缺页异常
时没有巨页存在，任务就会被发送一个SIGBUS，并经常不高兴地死去。在加入巨页支
持后不久，人们决定，在mmap()时检测巨页的短缺情况会更好。这个想法是，如果
没有足够的巨页来覆盖映射，mmap()将失败。这首先是在mmap()时在代码中做一个
简单的检查，以确定是否有足够的空闲巨页来覆盖映射。就像内核中的大多数东西一
样，代码随着时间的推移而不断发展。然而，基本的想法是在mmap()时 “预留”
巨页，以确保巨页可以用于该映射中的缺页异常。下面的描述试图描述在v4.10内核
中是如何进行巨页预留处理的。

读者
====
这个描述主要是针对正在修改hugetlbfs代码的内核开发者。

数据结构
========

resv_huge_pages
	这是一个全局的（per-hstate）预留的巨页的计数。预留的巨页只对预留它们的任
	务可用。因此，一般可用的巨页的数量被计算为(``free_huge_pages - resv_huge_pages``)。
Reserve Map
	预留映射由以下结构体描述::

		struct resv_map {
			struct kref refs;
			spinlock_t lock;
			struct list_head regions;
			long adds_in_progress;
			struct list_head region_cache;
			long region_cache_count;
		};

	系统中每个巨页映射都有一个预留映射。resv_map中的regions列表描述了映射中的
	区域。一个区域被描述为::

		struct file_region {
			struct list_head link;
			long from;
			long to;
		};

	file_region结构体的 ‘from’ 和 ‘to’ 字段是进入映射的巨页索引。根据映射的类型，在
	reserv_map 中的一个区域可能表示该范围存在预留，或预留不存在。
Flags for MAP_PRIVATE Reservations
	这些被存储在预留的映射指针的底部。

	``#define HPAGE_RESV_OWNER    (1UL << 0)``
		表示该任务是与该映射相关的预留的所有者。
	``#define HPAGE_RESV_UNMAPPED (1UL << 1)``
		表示最初映射此范围（并创建储备）的任务由于COW失败而从该任务（子任务）中取消映
		射了一个页面。
Page Flags
	PagePrivate页面标志是用来指示在释放巨页时必须恢复巨页的预留。更多细节将在
	“释放巨页” 一节中讨论。

预留映射位置（私有或共享）
==========================

一个巨页映射或段要么是私有的，要么是共享的。如果是私有的，它通常只对一个地址空间
（任务）可用。如果是共享的，它可以被映射到多个地址空间（任务）。对于这两种类型的映射，
预留映射的位置和语义是明显不同的。位置的差异是：

- 对于私有映射，预留映射挂在VMA结构体上。具体来说，就是vma->vm_private_data。这个保
  留映射是在创建映射（mmap(MAP_PRIVATE)）时创建的。
- 对于共享映射，预留映射挂在inode上。具体来说，就是inode->i_mapping->private_data。
  由于共享映射总是由hugetlbfs文件系统中的文件支持，hugetlbfs代码确保每个节点包含一个预
  留映射。因此，预留映射在创建节点时被分配。

创建预留
========
当创建一个巨大的有页面支持的共享内存段（shmget(SHM_HUGETLB)）或通过mmap(MAP_HUGETLB)
创建一个映射时，就会创建预留。这些操作会导致对函数hugetlb_reserve_pages()的调用::

	int hugetlb_reserve_pages(struct inode *inode,
				  long from, long to,
				  struct vm_area_struct *vma,
				  vm_flags_t vm_flags)

hugetlb_reserve_pages()做的第一件事是检查在调用shmget()或mmap()时是否指定了NORESERVE
标志。如果指定了NORESERVE，那么这个函数立即返回，因为不需要预留。

参数'from'和'to'是映射或基础文件的巨页索引。对于shmget()，'from'总是0，'to'对应于段/映射
的长度。对于mmap()，offset参数可以用来指定进入底层文件的偏移量。在这种情况下，'from'和'to'
参数已经被这个偏移量所调整。

PRIVATE和SHARED映射之间的一个很大的区别是预留在预留映射中的表示方式。

- 对于共享映射，预留映射中的条目表示对应页面的预留存在或曾经存在。当预留被消耗时，预留映射不被
  修改。
- 对于私有映射，预留映射中没有条目表示相应页面存在预留。随着预留被消耗，条目被添加到预留映射中。
  因此，预留映射也可用于确定哪些预留已被消耗。

对于私有映射，hugetlb_reserve_pages()创建预留映射并将其挂在VMA结构体上。此外，
HPAGE_RESV_OWNER标志被设置，以表明该VMA拥有预留。

预留映射被查阅以确定当前映射/段需要多少巨页预留。对于私有映射，这始终是一个值（to - from）。
然而，对于共享映射来说，一些预留可能已经存在于(to - from)的范围内。关于如何实现这一点的细节，
请参见 :ref:`预留映射的修改 <resv_map_modifications>` 一节。

该映射可能与一个子池（subpool）相关联。如果是这样，将查询子池以确保有足够的空间用于映射。子池
有可能已经预留了可用于映射的预留空间。更多细节请参见 :ref: `子池预留 <sub_pool_resv>`
一节。

在咨询了预留映射和子池之后，就知道了需要的新预留数量。hugetlb_acct_memory()函数被调用以检查
并获取所要求的预留数量。hugetlb_acct_memory()调用到可能分配和调整剩余页数的函数。然而，在这
些函数中，代码只是检查以确保有足够的空闲的巨页来容纳预留。如果有的话，全局预留计数resv_huge_pages
会被调整，如下所示::

	if (resv_needed <= (resv_huge_pages - free_huge_pages))
		resv_huge_pages += resv_needed;

注意，在检查和调整这些计数器时，全局锁hugetlb_lock会被预留。

如果有足够的空闲的巨页，并且全局计数resv_huge_pages被调整，那么与映射相关的预留映射被修改以
反映预留。在共享映射的情况下，将存在一个file_region，包括'from'-'to'范围。对于私有映射，
不对预留映射进行修改，因为没有条目表示存在预留。

如果hugetlb_reserve_pages()成功，全局预留数和与映射相关的预留映射将根据需要被修改，以确保
在'from'-'to'范围内存在预留。

消耗预留/分配一个巨页
===========================

当与预留相关的巨页在相应的映射中被分配和实例化时，预留就被消耗了。该分配是在函数alloc_huge_page()
中进行的::

	struct page *alloc_huge_page(struct vm_area_struct *vma,
				     unsigned long addr, int avoid_reserve)

alloc_huge_page被传递给一个VMA指针和一个虚拟地址，因此它可以查阅预留映射以确定是否存在预留。
此外，alloc_huge_page需要一个参数avoid_reserve，该参数表示即使看起来已经为指定的地址预留了
预留，也不应该使用预留。avoid_reserve参数最常被用于写时拷贝和页面迁移的情况下，即现有页面的额
外拷贝被分配。

调用辅助函数vma_needs_reservation()来确定是否存在对映射(vma)中地址的预留。关于这个函数的详
细内容，请参见 :ref:`预留映射帮助函数 <resv_map_helpers>` 一节。从
vma_needs_reservation()返回的值通常为0或1。如果该地址存在预留，则为0，如果不存在预留，则为1。
如果不存在预留，并且有一个与映射相关联的子池，则查询子池以确定它是否包含预留。如果子池包含预留，
则可将其中一个用于该分配。然而，在任何情况下，avoid_reserve参数都会优先考虑为分配使用预留。在
确定预留是否存在并可用于分配后，调用dequeue_huge_page_vma()函数。这个函数需要两个与预留有关
的参数：

- avoid_reserve，这是传递给alloc_huge_page()的同一个值/参数。
- chg，尽管这个参数的类型是long，但只有0或1的值被传递给dequeue_huge_page_vma。如果该值为0，
  则表明存在预留（关于可能的问题，请参见 “预留和内存策略” 一节）。如果值
  为1，则表示不存在预留，如果可能的话，必须从全局空闲池中取出该页。

与VMA的内存策略相关的空闲列表被搜索到一个空闲页。如果找到了一个页面，当该页面从空闲列表中移除时，
free_huge_pages的值被递减。如果有一个与该页相关的预留，将进行以下调整::

	SetPagePrivate(page);	/* 表示分配这个页面消耗了一个预留，
				 * 如果遇到错误，以至于必须释放这个页面，预留将被
				 * 恢复。 */
	resv_huge_pages--;	/* 减少全局预留计数 */

注意，如果找不到满足VMA内存策略的巨页，将尝试使用伙伴分配器分配一个。这就带来了超出预留范围
的剩余巨页和超额分配的问题。即使分配了一个多余的页面，也会进行与上面一样的基于预留的调整:
SetPagePrivate(page) 和 resv_huge_pages--.

在获得一个新的巨页后，(page)->private被设置为与该页面相关的子池的值，如果它存在的话。当页
面被释放时，这将被用于子池的计数。

然后调用函数vma_commit_reservation()，根据预留的消耗情况调整预留映射。一般来说，这涉及
到确保页面在区域映射的file_region结构体中被表示。对于预留存在的共享映射，预留映射中的条目
已经存在，所以不做任何改变。然而，如果共享映射中没有预留，或者这是一个私有映射，则必须创建一
个新的条目。

注意，如果找不到满足VMA内存策略的巨页，将尝试使用伙伴分配器分配一个。这就带来了超出预留范围
的剩余巨页和过度分配的问题。即使分配了一个多余的页面，也会进行与上面一样的基于预留的调整。
SetPagePrivate(page)和resv_huge_pages-。

在获得一个新的巨页后，(page)->private被设置为与该页面相关的子池的值，如果它存在的话。当页
面被释放时，这将被用于子池的计数。

然后调用函数vma_commit_reservation()，根据预留的消耗情况调整预留映射。一般来说，这涉及
到确保页面在区域映射的file_region结构体中被表示。对于预留存在的共享映射，预留映射中的条目
已经存在，所以不做任何改变。然而，如果共享映射中没有预留，或者这是一个私有映射，则必须创建
一个新的条目。

在alloc_huge_page()开始调用vma_needs_reservation()和页面分配后调用
vma_commit_reservation()之间，预留映射有可能被改变。如果hugetlb_reserve_pages在共
享映射中为同一页面被调用，这将是可能的。在这种情况下，预留计数和子池空闲页计数会有一个偏差。
这种罕见的情况可以通过比较vma_needs_reservation和vma_commit_reservation的返回值来
识别。如果检测到这种竞争，子池和全局预留计数将被调整以进行补偿。关于这些函数的更多信息，请
参见 :ref:`预留映射帮助函数 <resv_map_helpers>` 一节。

实例化巨页
==========

在巨页分配之后，页面通常被添加到分配任务的页表中。在此之前，共享映射中的页面被添加到页面缓
存中，私有映射中的页面被添加到匿名反向映射中。在这两种情况下，PagePrivate标志被清除。因此，
当一个已经实例化的巨页被释放时，不会对全局预留计数（resv_huge_pages）进行调整。

释放巨页
========

巨页释放是由函数free_huge_page()执行的。这个函数是hugetlbfs复合页的析构器。因此，它只传
递一个指向页面结构体的指针。当一个巨页被释放时，可能需要进行预留计算。如果该页与包含保
留的子池相关联，或者该页在错误路径上被释放，必须恢复全局预留计数，就会出现这种情况。

page->private字段指向与该页相关的任何子池。如果PagePrivate标志被设置，它表明全局预留计数
应该被调整（关于如何设置这些标志的信息，请参见
:ref: `消耗预留/分配一个巨页 <consume_resv>` ）。

该函数首先调用hugepage_subpool_put_pages()来处理该页。如果这个函数返回一个0的值（不等于
传递的1的值），它表明预留与子池相关联，这个新释放的页面必须被用来保持子池预留的数量超过最小值。
因此，在这种情况下，全局resv_huge_pages计数器被递增。

如果页面中设置了PagePrivate标志，那么全局resv_huge_pages计数器将永远被递增。

子池预留
========

有一个结构体hstate与每个巨页尺寸相关联。hstate跟踪所有指定大小的巨页。一个子池代表一
个hstate中的页面子集，它与一个已挂载的hugetlbfs文件系统相关

当一个hugetlbfs文件系统被挂载时，可以指定min_size选项，它表示文件系统所需的最小的巨页数量。
如果指定了这个选项，与min_size相对应的巨页的数量将被预留给文件系统使用。这个数字在结构体
hugepage_subpool的min_hpages字段中被跟踪。在挂载时，hugetlb_acct_memory(min_hpages)
被调用以预留指定数量的巨页。如果它们不能被预留，挂载就会失败。

当从子池中获取或释放页面时，会调用hugepage_subpool_get/put_pages()函数。
hugepage_subpool_get/put_pages被传递给巨页数量，以此来调整子池的 “已用页面” 计数
（get为下降，put为上升）。通常情况下，如果子池中没有足够的页面，它们会返回与传递的相同的值或
一个错误。

然而，如果预留与子池相关联，可能会返回一个小于传递值的返回值。这个返回值表示必须进行的额外全局
池调整的数量。例如，假设一个子池包含3个预留的巨页，有人要求5个。与子池相关的3个预留页可以用来
满足部分请求。但是，必须从全局池中获得2个页面。为了向调用者转达这一信息，将返回值2。然后，调用
者要负责从全局池中获取另外两个页面。

COW和预留
==========

由于共享映射都指向并使用相同的底层页面，COW最大的预留问题是私有映射。在这种情况下，两个任务可
以指向同一个先前分配的页面。一个任务试图写到该页，所以必须分配一个新的页，以便每个任务都指向它
自己的页。

当该页最初被分配时，该页的预留被消耗了。当由于COW而试图分配一个新的页面时，有可能没有空闲的巨
页，分配会失败。

当最初创建私有映射时，通过设置所有者的预留映射指针中的HPAGE_RESV_OWNER位来标记映射的所有者。
由于所有者创建了映射，所有者拥有与映射相关的所有预留。因此，当一个写异常发生并且没有可用的页面
时，对预留的所有者和非所有者采取不同的行动。

在发生异常的任务不是所有者的情况下，异常将失败，该任务通常会收到一个SIGBUS。

如果所有者是发生异常的任务，我们希望它能够成功，因为它拥有原始的预留。为了达到这个目的，该页被
从非所有者任务中解映射出来。这样一来，唯一的引用就是来自拥有者的任务。此外，HPAGE_RESV_UNMAPPED
位被设置在非拥有任务的预留映射指针中。如果非拥有者任务后来在一个不存在的页面上发生异常，它可能
会收到一个SIGBUS。但是，映射/预留的原始拥有者的行为将与预期一致。

预留映射的修改
==============

以下低级函数用于对预留映射进行修改。通常情况下，这些函数不会被直接调用。而是调用一个预留映射辅
助函数，该函数调用这些低级函数中的一个。这些低级函数在源代码（mm/hugetlb.c）中得到了相当好的
记录。这些函数是::

	long region_chg(struct resv_map *resv, long f, long t);
	long region_add(struct resv_map *resv, long f, long t);
	void region_abort(struct resv_map *resv, long f, long t);
	long region_count(struct resv_map *resv, long f, long t);

在预留映射上的操作通常涉及两个操作:

1) region_chg()被调用来检查预留映射，并确定在指定的范围[f, t]内有多少页目前没有被代表。

   调用代码执行全局检查和分配，以确定是否有足够的巨页使操作成功。

2)
  a) 如果操作能够成功，regi_add()将被调用，以实际修改先前传递给regi_chg()的相同范围
     [f, t]的预留映射。
  b) 如果操作不能成功，region_abort被调用，在相同的范围[f, t]内中止操作。

注意，这是一个两步的过程， region_add()和 region_abort()在事先调用 region_chg()后保证
成功。 region_chg()负责预先分配任何必要的数据结构以确保后续操作（特别是 region_add()）的
成功。

如上所述，region_chg()确定该范围内当前没有在映射中表示的页面的数量。region_add()返回添加
到映射中的范围内的页数。在大多数情况下， region_add() 的返回值与 region_chg() 的返回值相
同。然而，在共享映射的情况下，有可能在调用 region_chg() 和 region_add() 之间对预留映射进
行更改。在这种情况下，regi_add()的返回值将与regi_chg()的返回值不符。在这种情况下，全局计数
和子池计数很可能是不正确的，需要调整。检查这种情况并进行适当的调整是调用者的责任。

函数region_del()被调用以从预留映射中移除区域。
它通常在以下情况下被调用:

- 当hugetlbfs文件系统中的一个文件被删除时，该节点将被释放，预留映射也被释放。在释放预留映射
  之前，所有单独的file_region结构体必须被释放。在这种情况下，region_del的范围是[0, LONG_MAX]。
- 当一个hugetlbfs文件正在被截断时。在这种情况下，所有在新文件大小之后分配的页面必须被释放。
  此外，预留映射中任何超过新文件大小的file_region条目必须被删除。在这种情况下，region_del
  的范围是[new_end_of_file, LONG_MAX]。
- 当在一个hugetlbfs文件中打洞时。在这种情况下，巨页被一次次从文件的中间移除。当这些页被移除
  时，region_del()被调用以从预留映射中移除相应的条目。在这种情况下，region_del被传递的范
  围是[page_idx, page_idx + 1]。

在任何情况下，region_del()都会返回从预留映射中删除的页面数量。在非常罕见的情况下，region_del()
会失败。这只能发生在打洞的情况下，即它必须分割一个现有的file_region条目，而不能分配一个新的
结构体。在这种错误情况下，region_del()将返回-ENOMEM。这里的问题是，预留映射将显示对该页有
预留。然而，子池和全局预留计数将不反映该预留。为了处理这种情况，调用函数hugetlb_fix_reserve_counts()
来调整计数器，使其与不能被删除的预留映射条目相对应。

region_count()在解除私有巨页映射时被调用。在私有映射中，预留映射中没有条目表明存在一个预留。
因此，通过计算预留映射中的条目数，我们知道有多少预留被消耗了，有多少预留是未完成的
（Outstanding = (end - start) - region_count（resv, start, end））。由于映射正在消
失，子池和全局预留计数被未完成的预留数量所减去。

预留映射帮助函数
================

有几个辅助函数可以查询和修改预留映射。这些函数只对特定的巨页的预留感兴趣，所以它们只是传入一个
地址而不是一个范围。此外，它们还传入相关的VMA。从VMA中，可以确定映射的类型（私有或共享）和预留
映射的位置（inode或VMA）。这些函数只是调用 “预留映射的修改” 一节中描述的基础函数。然而，
它们确实考虑到了私有和共享映射的预留映射条目的 “相反” 含义，并向调用者隐藏了这个细节::

	long vma_needs_reservation(struct hstate *h,
				   struct vm_area_struct *vma,
				   unsigned long addr)

该函数为指定的页面调用 region_chg()。如果不存在预留，则返回1。如果存在预留，则返回0::

	long vma_commit_reservation(struct hstate *h,
				    struct vm_area_struct *vma,
				    unsigned long addr)

这将调用 region_add()，用于指定的页面。与region_chg和region_add的情况一样，该函数应在
先前调用的vma_needs_reservation后调用。它将为该页添加一个预留条目。如果预留被添加，它将
返回1，如果没有则返回0。返回值应与之前调用vma_needs_reservation的返回值进行比较。如果出
现意外的差异，说明在两次调用之间修改了预留映射::

	void vma_end_reservation(struct hstate *h,
				 struct vm_area_struct *vma,
				 unsigned long addr)

这将调用指定页面的 region_abort()。与region_chg和region_abort的情况一样，该函数应在
先前调用的vma_needs_reservation后被调用。它将中止/结束正在进行的预留添加操作::

	long vma_add_reservation(struct hstate *h,
				 struct vm_area_struct *vma,
				 unsigned long addr)

这是一个特殊的包装函数，有助于在错误路径上清理预留。它只从repare_reserve_on_error()函数
中调用。该函数与vma_needs_reservation一起使用，试图将一个预留添加到预留映射中。它考虑到
了私有和共享映射的不同预留映射语义。因此，region_add被调用用于共享映射（因为映射中的条目表
示预留），而region_del被调用用于私有映射（因为映射中没有条目表示预留）。关于在错误路径上需
要做什么的更多信息，请参见  “错误路径中的预留清理”  。

错误路径中的预留清理
====================

正如在:ref:`预留映射帮助函数<resv_map_helpers>` 一节中提到的，预留的修改分两步进行。首
先，在分配页面之前调用vma_needs_reservation。如果分配成功，则调用vma_commit_reservation。
如果不是，则调用vma_end_reservation。全局和子池的预留计数根据操作的成功或失败进行调整，
一切都很好。

此外，在一个巨页被实例化后，PagePrivate标志被清空，这样，当页面最终被释放时，计数是
正确的。

然而，有几种情况是，在一个巨页被分配后，但在它被实例化之前，就遇到了错误。在这种情况下，
页面分配已经消耗了预留，并进行了适当的子池、预留映射和全局计数调整。如果页面在这个时候被释放
（在实例化和清除PagePrivate之前），那么free_huge_page将增加全局预留计数。然而，预留映射
显示报留被消耗了。这种不一致的状态将导致预留的巨页的 “泄漏” 。全局预留计数将比它原本的要高，
并阻止分配一个预先分配的页面。

函数 restore_reserve_on_error() 试图处理这种情况。它有相当完善的文档。这个函数的目的
是将预留映射恢复到页面分配前的状态。通过这种方式，预留映射的状态将与页面释放后的全局预留计
数相对应。

函数restore_reserve_on_error本身在试图恢复预留映射条目时可能会遇到错误。在这种情况下，
它将简单地清除该页的PagePrivate标志。这样一来，当页面被释放时，全局预留计数将不会被递增。
然而，预留映射将继续看起来像预留被消耗了一样。一个页面仍然可以被分配到该地址，但它不会像最
初设想的那样使用一个预留页。

有一些代码（最明显的是userfaultfd）不能调用restore_reserve_on_error。在这种情况下，
它简单地修改了PagePrivate，以便在释放巨页时不会泄露预留。

预留和内存策略
==============
当git第一次被用来管理Linux代码时，每个节点的巨页列表就存在于hstate结构中。预留的概念是
在一段时间后加入的。当预留被添加时，没有尝试将内存策略考虑在内。虽然cpusets与内存策略不
完全相同，但hugetlb_acct_memory中的这个注释总结了预留和cpusets/内存策略之间的相互作
用::

	/*
	 * 当cpuset被配置时，它打破了严格的hugetlb页面预留，因为计数是在一个全局变量上完
	 * 成的。在有cpuset的情况下，这样的预留完全是垃圾，因为预留没有根据当前cpuset的
	 * 页面可用性来检查。在任务所在的cpuset中缺乏空闲的htlb页面时，应用程序仍然有可能
	 * 被内核OOM'ed。试图用cpuset来执行严格的计数几乎是不可能的（或者说太难看了），因
	 * 为cpuset太不稳定了，任务或内存节点可以在cpuset之间动态移动。与cpuset共享
	 * hugetlb映射的语义变化是不可取的。然而，为了预留一些语义，我们退回到检查当前空闲
	 * 页的可用性，作为一种最好的尝试，希望能将cpuset改变语义的影响降到最低。
	 */

添加巨页预留是为了防止在缺页异常时出现意外的页面分配失败（OOM）。然而，如果一个应用
程序使用cpusets或内存策略，就不能保证在所需的节点上有巨页可用。即使有足够数量的全局
预留，也是如此。

Hugetlbfs回归测试
=================

最完整的hugetlb测试集在libhugetlbfs仓库。如果你修改了任何hugetlb相关的代码，请使用
libhugetlbfs测试套件来检查回归情况。此外，如果你添加了任何新的hugetlb功能，请在
libhugetlbfs中添加适当的测试。

--
Mike Kravetz，2017年4月7日


code:
:Original: Documentation/mm/hwpoison.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

========
hwpoison
========

什么是hwpoison?
===============

即将推出的英特尔CPU支持从一些内存错误中恢复（ ``MCA恢复`` ）。这需要操作系统宣布
一个页面"poisoned"，杀死与之相关的进程，并避免在未来使用它。

这个补丁包在虚拟机中实现了必要的(编程)框架。

引用概述中的评论::

	高级机器的检查与处理。处理方法是损坏的页面被硬件报告，通常是由于2位ECC内
	存或高速缓存故障。

	这主要是针对在后台检测到的损坏的页面。当当前的CPU试图访问它时，当前运行的进程
	可以直接被杀死。因为还没有访问损坏的页面, 如果错误由于某种原因不能被处理，就可
	以安全地忽略它. 而不是用另外一个机器检查去处理它。

	处理不同状态的页面缓存页。这里棘手的部分是，相对于其他虚拟内存用户， 我们可以异
	步访问任何页面。因为内存故障可能随时随地发生，可能违反了他们的一些假设。这就是
	为什么这段代码必须非常小心。一般来说，它试图使用正常的锁规则，如获得标准锁，即使
	这意味着错误处理可能需要很长的时间。

	这里的一些操作有点低效，并且具有非线性的算法复杂性，因为数据结构没有针对这种情
	况进行优化。特别是从vma到进程的映射就是这种情况。由于这种情况大概率是罕见的，所
	以我们希望我们可以摆脱这种情况。

该代码由mm/memory-failure.c中的高级处理程序、一个新的页面poison位和虚拟机中的
各种检查组成，用来处理poison的页面。

现在主要目标是KVM客户机，但它适用于所有类型的应用程序。支持KVM需要最近的qemu-kvm
版本。

对于KVM的使用，需要一个新的信号类型，这样KVM就可以用适当的地址将机器检查注入到客户
机中。这在理论上也允许其他应用程序处理内存故障。我们的期望是，所有的应用程序都不要这
样做，但一些非常专业的应用程序可能会这样做。

故障恢复模式
============

有两种（实际上是三种）模式的内存故障恢复可以在。

vm.memory_failure_recovery sysctl 置零:
	所有的内存故障都会导致panic。请不要尝试恢复。

早期处理
	(可以在全局和每个进程中控制) 一旦检测到错误，立即向应用程序发送SIGBUS这允许
	应用程序以温和的方式处理内存错误（例如，放弃受影响的对象） 这是KVM qemu使用的
	模式。

推迟处理
	当应用程序运行到损坏的页面时，发送SIGBUS。这对不知道内存错误的应用程序来说是
	最好的，默认情况下注意一些页面总是被当作late kill处理。

用户控制
========

vm.memory_failure_recovery
	参阅 sysctl.txt

vm.memory_failure_early_kill
	全局启用early kill

PR_MCE_KILL
	设置early/late kill mode/revert 到系统默认值。

	arg1: PR_MCE_KILL_CLEAR:
		恢复到系统默认值
	arg1: PR_MCE_KILL_SET:
		arg2定义了线程特定模式

		PR_MCE_KILL_EARLY:
			Early kill
		PR_MCE_KILL_LATE:
			Late kill
		PR_MCE_KILL_DEFAULT
			使用系统全局默认值

	注意，如果你想有一个专门的线程代表进程处理SIGBUS(BUS_MCEERR_AO)，你应该在
	指定线程上调用prctl(PR_MCE_KILL_EARLY)。否则，SIGBUS将被发送到主线程。

PR_MCE_KILL_GET
	返回当前模式

测试
====

* madvise(MADV_HWPOISON, ....) (as root) - 在测试过程中Poison一个页面

* 通过debugfs ``/sys/kernel/debug/hwpoison/`` hwpoison-inject模块

  corrupt-pfn
	在PFN处注入hwpoison故障，并echoed到这个文件。这做了一些早期过滤，以避
	免在测试套件中损坏非预期页面。
  unpoison-pfn
	在PFN的Software-unpoison页面对应到这个文件。这样，一个页面可以再次被
	复用。这只对Linux注入的故障起作用，对真正的内存故障不起作用。

  注意这些注入接口并不稳定，可能会在不同的内核版本中发生变化

  corrupt-filter-dev-major, corrupt-filter-dev-minor
	只处理与块设备major/minor定义的文件系统相关的页面的内存故障。-1U是通
	配符值。这应该只用于人工注入的测试。

  corrupt-filter-memcg
	限制注入到memgroup拥有的页面。由memcg的inode号指定。

	Example::

		mkdir /sys/fs/cgroup/mem/hwpoison

	        usemem -m 100 -s 1000 &
		echo `jobs -p` > /sys/fs/cgroup/mem/hwpoison/tasks

		memcg_ino=$(ls -id /sys/fs/cgroup/mem/hwpoison | cut -f1 -d' ')
		echo $memcg_ino > /debug/hwpoison/corrupt-filter-memcg

		page-types -p `pidof init`   --hwpoison  # shall do nothing
		page-types -p `pidof usemem` --hwpoison  # poison its pages

  corrupt-filter-flags-mask, corrupt-filter-flags-value
	当指定时，只有在((page_flags & mask) == value)的情况下才会poison页面。
	这允许对许多种类的页面进行压力测试。page_flags与/proc/kpageflags中的相
	同。这些标志位在include/linux/kernel-page-flags.h中定义，并在
	Documentation/admin-guide/mm/pagemap.rst中记录。

* 架构特定的MCE注入器

  x86 有 mce-inject, mce-test

  在mce-test中的一些便携式hwpoison测试程序，见下文。

引用
====

http://halobates.de/mce-lc09-2.pdf
	09年LinuxCon的概述演讲

git://git.kernel.org/pub/scm/utils/cpu/mce/mce-test.git
	测试套件（在tsrc中的hwpoison特定可移植测试）。

git://git.kernel.org/pub/scm/utils/cpu/mce/mce-inject.git
	x86特定的注入器

限制
====
- 不是所有的页面类型都被支持，而且永远不会。大多数内核内部对象不能被恢
  复，目前只有LRU页。

---
Andi Kleen, 2009年10月


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mm/index.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

=================
Linux内存管理文档
=================

这是一份关于了解Linux的内存管理子系统的指南。如果你正在寻找关于简单分配内存的
建议，请参阅内存分配指南
(Documentation/translations/zh_CN/core-api/memory-allocation.rst)。
关于控制和调整的指南，请看管理指南
(Documentation/translations/zh_CN/admin-guide/mm/index.rst)。

.. toctree::
   :maxdepth: 1

   highmem

该处剩余文档待原始文档有内容后翻译。

遗留文档
========

这是一个关于Linux内存管理（MM）子系统内部的旧文档的集合，其中有不同层次的细节，
包括注释和邮件列表的回复，用于阐述数据结构和算法的描述。它应该被很好地整合到上述
结构化的文档中，如果它已经完成了它的使命，可以删除。

.. toctree::
   :maxdepth: 1

   active_mm
   balance
   damon/index
   free_page_reporting
   ksm
   frontswap
   hmm
   hwpoison
   hugetlbfs_reserv
   memory-model
   mmu_notifier
   numa
   overcommit-accounting
   page_frags
   page_migration
   page_owner
   page_table_check
   remap_file_pages
   split_page_table_lock
   vmalloced-kernel-stacks
   z3fold
   zsmalloc

TODOLIST:
* arch_pgtable_helpers
* free_page_reporting
* hugetlbfs_reserv
* slub
* transhuge
* unevictable-lru


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mm/ksm.rst

:翻译:

   徐鑫 xu xin <xu.xin16@zte.com.cn>

============
内核同页合并
============

KSM 是一种节省内存的数据去重功能，由CONFIG_KSM=y启用，并在2.6.32版本时被添加
到Linux内核。详见 ``mm/ksm.c`` 的实现，以及http://lwn.net/Articles/306704和
https://lwn.net/Articles/330589

KSM的用户空间的接口在Documentation/translations/zh_CN/admin-guide/mm/ksm.rst
文档中有描述。

设计
====

概述
----

概述内容请见mm/ksm.c文档中的“DOC: Overview”

逆映射
------
KSM维护着稳定树中的KSM页的逆映射信息。

当KSM页面的共享数小于 ``max_page_sharing`` 的虚拟内存区域(VMAs)时，则代表了
KSM页的稳定树其中的节点指向了一个ksm_rmap_item结构体类型的列表。同时，这个KSM页
的 ``page->mapping`` 指向了该稳定树节点。

如果共享数超过了阈值，KSM将给稳定树添加第二个维度。稳定树就变成链接一个或多
个稳定树"副本"的"链"。每个副本都保留KSM页的逆映射信息，其中 ``page->mapping``
指向该"副本"。

每个链以及链接到该链中的所有"副本"强制不变的是，它们代表了相同的写保护内存
内容，尽管任中一个"副本"是由同一片内存区的不同的KSM复制页所指向的。

这样一来，相比与无限的逆映射链表，稳定树的查找计算复杂性不受影响。但在稳定树
本身中不能有重复的KSM页面内容仍然是强制要求。

由 ``max_page_sharing`` 强制决定的数据去重限制是必要的，以此来避免虚拟内存
rmap链表变得过大。rmap的遍历具有O(N)的复杂度，其中N是共享页面的rmap_项（即
虚拟映射）的数量，而这个共享页面的节点数量又被 ``max_page_sharing`` 所限制。
因此，这有效地将线性O(N)计算复杂度从rmap遍历中分散到不同的KSM页面上。ksmd进
程在稳定节点"链"上的遍历也是O(N)，但这个N是稳定树"副本"的数量，而不是rmap项
的数量，因此它对ksmd性能没有显著影响。实际上，最佳稳定树"副本"的候选节点将
保留在"副本"列表的开头。

``max_page_sharing`` 的值设置得高了会促使更快的内存合并（因为将有更少的稳定
树副本排队进入稳定节点chain->hlist）和更高的数据去重系数，但代价是在交换、压
缩、NUMA平衡和页面迁移过程中可能导致KSM页的最大rmap遍历速度较慢。

``stable_node_dups/stable_node_chains`` 的比值还受 ``max_page_sharing`` 调控
的影响，高比值可能意味着稳定节点dup中存在碎片，这可以通过在ksmd中引入碎片算
法来解决，该算法将rmap项从一个稳定节点dup重定位到另一个稳定节点dup，以便释放
那些仅包含极少rmap项的稳定节点"dup"，但这可能会增加ksmd进程的CPU使用率，并可
能会减慢应用程序在KSM页面上的只读计算。

KSM会定期扫描稳定节点"链"中链接的所有稳定树"副本"，以便删减过时了的稳定节点。
这种扫描的频率由 ``stable_node_chains_prune_millisecs`` 这个sysfs 接口定义。

参考
====
内核代码请见mm/ksm.c。
涉及的函数(mm_slot  ksm_scan  stable_node  rmap_item)。


code:
.. SPDX-License-Identifier: GPL-2.0

:Original: Documentation/mm/memory-model.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

============
物理内存模型
============

系统中的物理内存可以用不同的方式进行寻址。最简单的情况是，物理内存从地址0开
始，跨越一个连续的范围，直到最大的地址。然而，这个范围可能包含CPU无法访问的
小孔隙。那么，在完全不同的地址可能有几个连续的范围。而且，别忘了NUMA，即不
同的内存库连接到不同的CPU。

Linux使用两种内存模型中的一种对这种多样性进行抽象。FLATMEM和SPARSEM。每
个架构都定义了它所支持的内存模型，默认的内存模型是什么，以及是否有可能手动
覆盖该默认值。

所有的内存模型都使用排列在一个或多个数组中的 `struct page` 来跟踪物理页
帧的状态。

无论选择哪种内存模型，物理页框号（PFN）和相应的 `struct page` 之间都存
在一对一的映射关系。

每个内存模型都定义了 :c:func:`pfn_to_page` 和 :c:func:`page_to_pfn`
帮助函数，允许从PFN到 `struct page` 的转换，反之亦然。

FLATMEM
=======

最简单的内存模型是FLATMEM。这个模型适用于非NUMA系统的连续或大部分连续的
物理内存。

在FLATMEM内存模型中，有一个全局的 `mem_map` 数组来映射整个物理内存。对
于大多数架构，孔隙在 `mem_map` 数组中都有条目。与孔洞相对应的 `struct page`
对象从未被完全初始化。

为了分配 `mem_map` 数组，架构特定的设置代码应该调用free_area_init()函数。
然而，在调用memblock_free_all()函数之前，映射数组是不能使用的，该函数
将所有的内存交给页分配器。

一个架构可能会释放 `mem_map` 数组中不包括实际物理页的部分。在这种情况下，特
定架构的 :c:func:`pfn_valid` 实现应该考虑到 `mem_map` 中的孔隙。

使用FLATMEM，PFN和 `struct page` 之间的转换是直接的。 `PFN - ARCH_PFN_OFFSET`
是 `mem_map` 数组的一个索引。

`ARCH_PFN_OFFSET` 定义了物理内存起始地址不同于0的系统的第一个页框号。

SPARSEMEM
=========

SPARSEMEM是Linux中最通用的内存模型，它是唯一支持若干高级功能的内存模型，
如物理内存的热插拔、非易失性内存设备的替代内存图和较大系统的内存图的延迟
初始化。

SPARSEMEM模型将物理内存显示为一个部分的集合。一个区段用mem_section结构
体表示，它包含 `section_mem_map` ，从逻辑上讲，它是一个指向 `struct page`
阵列的指针。然而，它被存储在一些其他的magic中，以帮助分区管理。区段的大小
和最大区段数是使用 `SECTION_SIZE_BITS` 和 `MAX_PHYSMEM_BITS` 常量
来指定的，这两个常量是由每个支持SPARSEMEM的架构定义的。 `MAX_PHYSMEM_BITS`
是一个架构所支持的物理地址的实际宽度，而 `SECTION_SIZE_BITS` 是一个任
意的值。

最大的段数表示为 `NR_MEM_SECTIONS` ，定义为

.. math::

   NR\_MEM\_SECTIONS = 2 ^ {(MAX\_PHYSMEM\_BITS - SECTION\_SIZE\_BITS)}

`mem_section` 对象被安排在一个叫做 `mem_sections` 的二维数组中。这个数组的
大小和位置取决于 `CONFIG_SPARSEM_EXTREME` 和可能的最大段数:

* 当 `CONFIG_SPARSEMEM_EXTREME` 被禁用时， `mem_sections` 数组是静态的，有
  `NR_MEM_SECTIONS` 行。每一行持有一个 `mem_section` 对象。
* 当 `CONFIG_SPARSEMEM_EXTREME` 被启用时， `mem_sections` 数组被动态分配。
  每一行包含价值 `PAGE_SIZE` 的 `mem_section` 对象，行数的计算是为了适应所有的
  内存区。

架构设置代码应该调用sparse_init()来初始化内存区和内存映射。

通过SPARSEMEM，有两种可能的方式将PFN转换为相应的 `struct page` --"classic sparse"和
 "sparse vmemmap"。选择是在构建时进行的，它由 `CONFIG_SPARSEMEM_VMEMMAP` 的
 值决定。

Classic sparse在page->flags中编码了一个页面的段号，并使用PFN的高位来访问映射该页
框的段。在一个区段内，PFN是指向页数组的索引。

Sparse vmemmapvmemmap使用虚拟映射的内存映射来优化pfn_to_page和page_to_pfn操
作。有一个全局的 `struct page *vmemmap` 指针，指向一个虚拟连续的 `struct page`
对象阵列。PFN是该数组的一个索引，`struct page` 从 `vmemmap` 的偏移量是该页的PFN。

为了使用vmemmap，一个架构必须保留一个虚拟地址的范围，以映射包含内存映射的物理页，并
确保 `vmemmap`指向该范围。此外，架构应该实现 :c:func:`vmemmap_populate` 方法，
它将分配物理内存并为虚拟内存映射创建页表。如果一个架构对vmemmap映射没有任何特殊要求，
它可以使用通用内存管理提供的默认 :c:func:`vmemmap_populate_basepages`。

虚拟映射的内存映射允许将持久性内存设备的 `struct page` 对象存储在这些设备上预先分
配的存储中。这种存储用vmem_altmap结构表示，最终通过一长串的函数调用传递给
vmemmap_populate()。vmemmap_populate()实现可以使用 `vmem_altmap` 和
:c:func:`vmemmap_alloc_block_buf` 助手来分配持久性内存设备上的内存映射。

ZONE_DEVICE
===========
`ZONE_DEVICE` 设施建立在 `SPARSEM_VMEMMAP` 之上，为设备驱动识别的物理地址范
围提供 `struct page` `mem_map` 服务。 `ZONE_DEVICE` 的 "设备" 方面与以下
事实有关：这些地址范围的页面对象从未被在线标记过，而且必须对设备进行引用，而不仅仅
是页面，以保持内存被“锁定”以便使用。 `ZONE_DEVICE` ，通过 :c:func:`devm_memremap_pages` ，
为给定的pfns范围执行足够的内存热插拔来开启 :c:func:`pfn_to_page`，
:c:func:`page_to_pfn`, ，和 :c:func:`get_user_pages` 服务。由于页面引
用计数永远不会低于1，所以页面永远不会被追踪为空闲内存，页面的 `struct list_head lru`
空间被重新利用，用于向映射该内存的主机设备/驱动程序进行反向引用。

虽然 `SPARSEMEM` 将内存作为一个区段的集合，可以选择收集并合成内存块，但
`ZONE_DEVICE` 用户需要更小的颗粒度来填充 `mem_map` 。鉴于 `ZONE_DEVICE`
内存从未被在线标记，因此它的内存范围从未通过sysfs内存热插拔api暴露在内存块边界
上。这个实现依赖于这种缺乏用户接口的约束，允许子段大小的内存范围被指定给
:c:func:`arch_add_memory` ，即内存热插拔的上半部分。子段支持允许2MB作为
:c:func:`devm_memremap_pages` 的跨架构通用对齐颗粒度。

`ZONE_DEVICE` 的用户是:

* pmem: 通过DAX映射将平台持久性内存作为直接I/O目标使用。

* hmm: 用 `->page_fault()` 和 `->page_free()` 事件回调扩展 `ZONE_DEVICE` ，
  以允许设备驱动程序协调与设备内存相关的内存管理事件，通常是GPU内存。参见Documentation/mm/hmm.rst。

* p2pdma: 创建 `struct page` 对象，允许PCI/E拓扑结构中的peer设备协调它们之间的
  直接DMA操作，即绕过主机内存。


code:
:Original: Documentation/mm/mmu_notifier.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

什么时候需要页表锁内通知？
==========================

当清除一个pte/pmd时，我们可以选择通过在页表锁下（通知版的\*_clear_flush调用
mmu_notifier_invalidate_range）通知事件。但这种通知并不是在所有情况下都需要的。

对于二级TLB（非CPU TLB），如IOMMU TLB或设备TLB（当设备使用类似ATS/PASID的东西让
IOMMU走CPU页表来访问进程的虚拟地址空间）。只有两种情况需要在清除pte/pmd时在持有页
表锁的同时通知这些二级TLB：

  A) 在mmu_notifier_invalidate_range_end()之前，支持页的地址被释放。
  B) 一个页表项被更新以指向一个新的页面（COW，零页上的写异常，__replace_page()，...）。

情况A很明显，你不想冒风险让设备写到一个现在可能被一些完全不同的任务使用的页面。

情况B更加微妙。为了正确起见，它需要按照以下序列发生:

  - 上页表锁
  - 清除页表项并通知 ([pmd/pte]p_huge_clear_flush_notify())
  - 设置页表项以指向新页

如果在设置新的pte/pmd值之前，清除页表项之后没有进行通知，那么你就会破坏设备的C11或
C++11等内存模型。

考虑以下情况（设备使用类似于ATS/PASID的功能）。

两个地址addrA和addrB，这样|addrA - addrB| >= PAGE_SIZE，我们假设它们是COW的
写保护（B的其他情况也适用）。

::

 [Time N] --------------------------------------------------------------------
 CPU-thread-0  {尝试写到addrA}
 CPU-thread-1  {尝试写到addrB}
 CPU-thread-2  {}
 CPU-thread-3  {}
 DEV-thread-0  {读取addrA并填充设备TLB}
 DEV-thread-2  {读取addrB并填充设备TLB}
 [Time N+1] ------------------------------------------------------------------
 CPU-thread-0  {COW_step0: {mmu_notifier_invalidate_range_start(addrA)}}
 CPU-thread-1  {COW_step0: {mmu_notifier_invalidate_range_start(addrB)}}
 CPU-thread-2  {}
 CPU-thread-3  {}
 DEV-thread-0  {}
 DEV-thread-2  {}
 [Time N+2] ------------------------------------------------------------------
 CPU-thread-0  {COW_step1: {更新页表以指向addrA的新页}}
 CPU-thread-1  {COW_step1: {更新页表以指向addrB的新页}}
 CPU-thread-2  {}
 CPU-thread-3  {}
 DEV-thread-0  {}
 DEV-thread-2  {}
 [Time N+3] ------------------------------------------------------------------
 CPU-thread-0  {preempted}
 CPU-thread-1  {preempted}
 CPU-thread-2  {写入addrA，这是对新页面的写入}
 CPU-thread-3  {}
 DEV-thread-0  {}
 DEV-thread-2  {}
 [Time N+3] ------------------------------------------------------------------
 CPU-thread-0  {preempted}
 CPU-thread-1  {preempted}
 CPU-thread-2  {}
 CPU-thread-3  {写入addrB，这是一个写入新页的过程}
 DEV-thread-0  {}
 DEV-thread-2  {}
 [Time N+4] ------------------------------------------------------------------
 CPU-thread-0  {preempted}
 CPU-thread-1  {COW_step3: {mmu_notifier_invalidate_range_end(addrB)}}
 CPU-thread-2  {}
 CPU-thread-3  {}
 DEV-thread-0  {}
 DEV-thread-2  {}
 [Time N+5] ------------------------------------------------------------------
 CPU-thread-0  {preempted}
 CPU-thread-1  {}
 CPU-thread-2  {}
 CPU-thread-3  {}
 DEV-thread-0  {从旧页中读取addrA}
 DEV-thread-2  {从新页面读取addrB}

所以在这里，因为在N+2的时候，清空页表项没有和通知一起作废二级TLB，设备在看到addrA的新值之前
就看到了addrB的新值。这就破坏了设备的总内存序。

当改变一个pte的写保护或指向一个新的具有相同内容的写保护页（KSM）时，将mmu_notifier_invalidate_range
调用延迟到页表锁外的mmu_notifier_invalidate_range_end()是可以的。即使做页表更新的线程
在释放页表锁后但在调用mmu_notifier_invalidate_range_end()前被抢占，也是如此。


code:
:Original: Documentation/mm/numa.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

始于1999年11月，作者： <kanoj@sgi.com>

==========================
何为非统一内存访问(NUMA)？
==========================

这个问题可以从几个视角来回答：硬件观点和Linux软件视角。

从硬件角度看，NUMA系统是一个由多个组件或装配组成的计算机平台，每个组件可能包含0个或更多的CPU、
本地内存和/或IO总线。为了简洁起见，并将这些物理组件/装配的硬件视角与软件抽象区分开来，我们在
本文中称这些组件/装配为“单元”。

每个“单元”都可以看作是系统的一个SMP[对称多处理器]子集——尽管独立的SMP系统所需的一些组件可能
不会在任何给定的单元上填充。NUMA系统的单元通过某种系统互连连接在一起——例如，交叉开关或点对点
链接是NUMA系统互连的常见类型。这两种类型的互连都可以聚合起来，以创建NUMA平台，其中的单元与其
他单元有多个距离。

对于Linux，感兴趣的NUMA平台主要是所谓的缓存相干NUMA--简称ccNUMA系统系统。在ccNUMA系统中，
所有的内存都是可见的，并且可以从连接到任何单元的任何CPU中访问，缓存一致性是由处理器缓存和/或
系统互连在硬件中处理。

内存访问时间和有效的内存带宽取决于包含CPU的单元或进行内存访问的IO总线距离包含目标内存的单元
有多远。例如，连接到同一单元的CPU对内存的访问将比访问其他远程单元的内存经历更快的访问时间和
更高的带宽。 NUMA平台可以在任何给定单元上访问多种远程距离的（其他）单元。

平台供应商建立NUMA系统并不只是为了让软件开发人员的生活变得有趣。相反，这种架构是提供可扩展
内存带宽的一种手段。然而，为了实现可扩展的内存带宽，系统和应用软件必须安排大部分的内存引用
[cache misses]到“本地”内存——同一单元的内存，如果有的话——或者到最近的有内存的单元。

这就自然而然有了Linux软件对NUMA系统的视角:

Linux将系统的硬件资源划分为多个软件抽象，称为“节点”。Linux将节点映射到硬件平台的物理单元
上，对一些架构的细节进行了抽象。与物理单元一样，软件节点可能包含0或更多的CPU、内存和/或IO
总线。同样，对“较近”节点的内存访问——映射到较近单元的节点——通常会比对较远单元的访问经历更快
的访问时间和更高的有效带宽。

对于一些架构，如x86，Linux将“隐藏”任何代表没有内存连接的物理单元的节点，并将连接到该单元
的任何CPU重新分配到代表有内存的单元的节点上。因此，在这些架构上，我们不能假设Linux将所有
的CPU与一个给定的节点相关联，会看到相同的本地内存访问时间和带宽。

此外，对于某些架构，同样以x86为例，Linux支持对额外节点的仿真。对于NUMA仿真，Linux会将现
有的节点或者非NUMA平台的系统内存分割成多个节点。每个模拟的节点将管理底层单元物理内存的一部
分。NUMA仿真对于在非NUMA平台上测试NUMA内核和应用功能是非常有用的，当与cpusets一起使用时，
可以作为一种内存资源管理机制。[见 Documentation/admin-guide/cgroup-v1/cpusets.rst]

对于每个有内存的节点，Linux构建了一个独立的内存管理子系统，有自己的空闲页列表、使用中页列表、
使用统计和锁来调解访问。此外，Linux为每个内存区[DMA、DMA32、NORMAL、HIGH_MEMORY、MOVABLE
中的一个或多个]构建了一个有序的“区列表”。zonelist指定了当一个选定的区/节点不能满足分配请求
时要访问的区/节点。当一个区没有可用的内存来满足请求时，这种情况被称为“overflow 溢出”或
“fallback 回退”。

由于一些节点包含多个包含不同类型内存的区，Linux必须决定是否对区列表进行排序，使分配回退到不同
节点上的相同区类型，或同一节点上的不同区类型。这是一个重要的考虑因素，因为有些区，如DMA或DMA32，
代表了相对稀缺的资源。Linux选择了一个默认的Node ordered zonelist。这意味着在使用按NUMA距
离排序的远程节点之前，它会尝试回退到同一节点的其他分区。

默认情况下，Linux会尝试从执行请求的CPU被分配到的节点中满足内存分配请求。具体来说，Linux将试
图从请求来源的节点的适当分区列表中的第一个节点进行分配。这被称为“本地分配”。如果“本地”节点不能
满足请求，内核将检查所选分区列表中其他节点的区域，寻找列表中第一个能满足请求的区域。

本地分配将倾向于保持对分配的内存的后续访问 “本地”的底层物理资源和系统互连——只要内核代表其分配
一些内存的任务后来不从该内存迁移。Linux调度器知道平台的NUMA拓扑结构——体现在“调度域”数据结构
中[见 Documentation/scheduler/sched-domains.rst]——并且调度器试图尽量减少任务迁移到遥
远的调度域中。然而，调度器并没有直接考虑到任务的NUMA足迹。因此，在充分不平衡的情况下，任务可
以在节点之间迁移，远离其初始节点和内核数据结构。

系统管理员和应用程序设计者可以使用各种CPU亲和命令行接口，如taskset(1)和numactl(1)，以及程
序接口，如sched_setaffinity(2)，来限制任务的迁移，以改善NUMA定位。此外，人们可以使用
Linux NUMA内存策略修改内核的默认本地分配行为。 [见
:ref:`Documentation/admin-guide/mm/numa_memory_policy.rst <numa_memory_policy>`].

系统管理员可以使用控制组和CPUsets限制非特权用户在调度或NUMA命令和功能中可以指定的CPU和节点
的内存。 [见 Documentation/admin-guide/cgroup-v1/cpusets.rst]

在不隐藏无内存节点的架构上，Linux会在分区列表中只包括有内存的区域[节点]。这意味着对于一个无
内存的节点，“本地内存节点”——CPU节点的分区列表中的第一个区域的节点——将不是节点本身。相反，它
将是内核在建立分区列表时选择的离它最近的有内存的节点。所以，默认情况下，本地分配将由内核提供
最近的可用内存来完成。这是同一机制的结果，该机制允许这种分配在一个包含内存的节点溢出时回退到
其他附近的节点。

一些内核分配不希望或不能容忍这种分配回退行为。相反，他们想确保他们从指定的节点获得内存，或者
得到通知说该节点没有空闲内存。例如，当一个子系统分配每个CPU的内存资源时，通常是这种情况。

一个典型的分配模式是使用内核的numa_node_id()或CPU_to_node()函数获得“当前CPU”所在节点的
节点ID，然后只从返回的节点ID请求内存。当这样的分配失败时，请求的子系统可以恢复到它自己的回退
路径。板块内核内存分配器就是这样的一个例子。或者，子系统可以选择在分配失败时禁用或不启用自己。
内核分析子系统就是这样的一个例子。

如果架构支持——不隐藏无内存节点，那么连接到无内存节点的CPU将总是产生回退路径的开销，或者一些
子系统如果试图完全从无内存的节点分配内存，将无法初始化。为了透明地支持这种架构，内核子系统可
以使用numa_mem_id()或cpu_to_mem()函数来定位调用或指定CPU的“本地内存节点”。同样，这是同
一个节点，默认的本地页分配将从这个节点开始尝试。


code:
:Original: Documentation/mm/overcommit-accounting.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

==============
超量使用审计
==============

Linux内核支持下列超量使用处理模式

0
	启发式超量使用处理。拒绝明显的地址空间超量使用。用于一个典型的系统。
	它确保严重的疯狂分配失败，同时允许超量使用以减少swap的使用。在这种模式下，
	允许root分配稍多的内存。这是默认的。
1
	总是超量使用。适用于一些科学应用。经典的例子是使用稀疏数组的代码，只是依赖
	几乎完全由零页组成的虚拟内存

2
	不超量使用。系统提交的总地址空间不允许超过swap+一个可配置的物理RAM的数量
	（默认为50%）。根据你使用的数量，在大多数情况下，这意味着一个进程在访问页面时
	不会被杀死，但会在内存分配上收到相应的错误。

	对于那些想保证他们的内存分配在未来可用而又不需要初始化每一个页面的应用程序来说
	是很有用的。

超量使用策略是通过sysctl  `vm.overcommit_memory` 设置的。

可以通过 `vm.overcommit_ratio` （百分比）或 `vm.overcommit_kbytes` （绝对值）
来设置超限数量。这些只有在 `vm.overcommit_memory` 被设置为2时才有效果。

在 ``/proc/meminfo`` 中可以分别以CommitLimit和Committed_AS的形式查看当前
的超量使用和提交量。

陷阱
====

C语言的堆栈增长是一个隐含的mremap。如果你想得到绝对的保证，并在接近边缘的地方运行，
你 **必须** 为你认为你需要的最大尺寸的堆栈进行mmap。对于典型的堆栈使用来说，这并
不重要，但如果你真的非常关心的话，这就是一个值得关注的案例。

在模式2中，MAP_NORESERVE标志被忽略。

它是如何工作的
==============

超量使用是基于以下规则

对于文件映射
	| SHARED or READ-only	-	0 cost (该文件是映射而不是交换)
	| PRIVATE WRITABLE	-	每个实例的映射大小

对于匿名或者 ``/dev/zero`` 映射
	| SHARED			-	映射的大小
	| PRIVATE READ-only	-	0 cost (但作用不大)
	| PRIVATE WRITABLE	-	每个实例的映射大小

额外的计数
	| 通过mmap制作可写副本的页面
	| 从同一池中提取的shmfs内存

状态
====

*	我们核算mmap内存映射
*	我们核算mprotect在提交中的变化
*	我们核算mremap的大小变化
*	我们的审计 brk
*	审计munmap
*	我们在/proc中报告commit 状态
*	核对并检查分叉的情况
*	审查堆栈处理/执行中的构建
*	叙述SHMfs的情况
*	实现实际限制的执行

待续
====
*	ptrace 页计数（这很难）。


code:
:Original: Documentation/mm/page_frags.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

========
页面片段
========

一个页面片段是一个任意长度的任意偏移的内存区域，它位于一个0或更高阶的复合页面中。
该页中的多个碎片在该页的引用计数器中被单独计算。

page_frag函数，page_frag_alloc和page_frag_free，为页面片段提供了一个简单
的分配框架。这被网络堆栈和网络设备驱动使用，以提供一个内存的支持区域，作为
sk_buff->head使用，或者用于skb_shared_info的 “frags” 部分。

为了使用页面片段API，需要一个支持页面片段的缓冲区。这为碎片分配提供了一个中心点，
并允许多个调用使用一个缓存的页面。这样做的好处是可以避免对get_page的多次调用，
这在分配时开销可能会很大。然而，由于这种缓存的性质，要求任何对缓存的调用都要受到每
个CPU的限制，或者每个CPU的限制，并在执行碎片分配时强制禁止中断。

网络堆栈在每个CPU使用两个独立的缓存来处理碎片分配。netdev_alloc_cache被使用
netdev_alloc_frag和__netdev_alloc_skb调用的调用者使用。napi_alloc_cache
被调用__napi_alloc_frag和__napi_alloc_skb的调用者使用。这两个调用的主要区别是
它们可能被调用的环境。“netdev” 前缀的函数可以在任何上下文中使用，因为这些函数
将禁用中断，而 ”napi“ 前缀的函数只可以在softirq上下文中使用。

许多网络设备驱动程序使用类似的方法来分配页面片段，但页面片段是在环或描述符级别上
缓存的。为了实现这些情况，有必要提供一种拆解页面缓存的通用方法。出于这个原因，
__page_frag_cache_drain被实现了。它允许通过一次调用从一个页面释放多个引用。
这样做的好处是，它允许清理被添加到一个页面的多个引用，以避免每次分配都调用
get_page。

Alexander Duyck，2016年11月29日。


code:
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/mm/page_migration.rst

:翻译:

 司延腾 Yanteng Si <siyanteng@loongson.cn>

:校译:

========
页面迁移
========

页面迁移允许在进程运行时在NUMA系统的节点之间移动页面的物理位置。这意味着进程所看到的虚拟地
址并没有改变。然而，系统会重新安排这些页面的物理位置。

也可以参见 :ref: `<异构内存管理 (HMM)>` 以了解将页面迁移到设备私有内存或从设备私有内存中迁移。

页面迁移的主要目的是通过将页面移到访问该内存的进程所运行的处理器附近来减少内存访问的延迟。

页面迁移允许进程通过MF_MOVE和MF_MOVE_ALL选项手动重新定位其页面所在的节点，同时通过
mbind()设置一个新的内存策略。一个进程的页面也可以通过sys_migrate_pages()函数调用从另
一个进程重新定位。migrate_pages()函数调用接收两组节点，并将一个进程位于旧节点上的页面移
动到目标节点上。页面迁移功能由Andi Kleen的numactl包提供(需要0.9.3以上的版本，其仓库
地址https://github.com/numactl/numactl.git)。numactl提供了libnuma，它为页面迁移
提供了与其他NUMA功能类似的接口。执行 cat ``/proc/<pid>/numa_maps``  允许轻松查看进
程的页面位置。参见proc(5)手册中的numa_maps文档。

如果调度程序将一个进程重新安置到一个遥远的节点上的处理器，手动迁移是很有用的。批量调度程序
或管理员可以检测到这种情况，并将进程的页面移到新处理器附近。内核本身只提供手动的页迁移支持。
自动的页面迁移可以通过用户空间的进程移动页面来实现。一个特殊的函数调用 "move_pages" 允许
在一个进程中移动单个页面。例如，NUMA分析器可以获得一个显示频繁的节点外访问的日志，并可以使
用这个结果将页面移动到更有利的位置。

较大型的设备通常使用cpusets将系统分割成若干个节点。Paul Jackson为cpusets配备了当任务被
转移到另一个cpuset时移动页面的能力(见:ref:`CPUSETS <cpusets>`)。Cpusets允许进程定
位的自动化。如果一个任务被移到一个新的cpuset上，那么它的所有页面也会随之移动，这样进程的
性能就不会急剧下降。如果cpuset允许的内存节点发生变化，cpuset中的进程页也会被移动。

页面迁移允许为所有迁移技术保留一组节点中页面的相对位置，这将保留生成的特定内存分配模式即使
进程已被迁移。为了保留内存延迟，这一点是必要的。迁移后的进程将以类似的性能运行。

页面迁移分几个步骤进行。首先为那些试图从内核中使用migrate_pages()的进程做一个高层次的
描述（对于用户空间的使用，可以参考上面提到的Andi Kleen的numactl包），然后对低水平的细
节工作做一个低水平描述。

在内核中使用 migrate_pages()
============================

1. 从LRU中移除页面。

   要迁移的页面列表是通过扫描页面并把它们移到列表中来生成的。这是通过调用 isolate_lru_page()
   来完成的。调用isolate_lru_page()增加了对该页的引用，这样在页面迁移发生时它就不会
   消失。它还可以防止交换器或其他扫描器遇到该页。

2. 我们需要有一个new_page_t类型的函数，可以传递给migrate_pages()。这个函数应该计算
   出如何在给定的旧页面中分配正确的新页面。

3. migrate_pages()函数被调用，它试图进行迁移。它将调用该函数为每个被考虑迁移的页面分
   配新的页面。

migrate_pages()如何工作
=======================

migrate_pages()对它的页面列表进行了多次处理。如果当时对一个页面的所有引用都可以被移除，
那么这个页面就会被移动。该页已经通过isolate_lru_page()从LRU中移除，并且refcount被
增加，以便在页面迁移发生时不释放该页。

步骤:

1. 锁定要迁移的页面。

2. 确保回写已经完成。

3. 锁定我们要迁移到的新页面。锁定它是为了在迁移过程中立即阻止对这个（尚未更新的）页面的
   访问。

4. 所有对该页的页表引用都被转换为迁移条目。这就减少了一个页面的mapcount。如果产生的
   mapcount不是零，那么我们就不迁移该页。所有试图访问该页的用户空间进程现在将等待页
   面锁或者等待迁移页表项被移除。

5. i_pages的锁被持有。这将导致所有试图通过映射访问该页的进程在自旋锁上阻塞。

6. 检查该页的Refcount，如果还有引用，我们就退出。否则，我们知道我们是唯一引用这个页
   面的人。

7. 检查基数树，如果它不包含指向这个页面的指针，那么我们就退出，因为其他人修改了基数树。

8. 新的页面要用旧的页面的一些设置进行预处理，这样访问新的页面就会发现一个具有正确设置
   的页面。

9. 基数树被改变以指向新的页面。

10. 旧页的引用计数被删除，因为地址空间的引用已经消失。对新页的引用被建立，因为新页被
    地址空间引用。

11. i_pages锁被放弃。这样一来，在映射中的查找又变得可能了。进程将从在锁上自旋到在
    被锁的新页上睡眠。

12. 页面内容被复制到新的页面上。

13. 剩余的页面标志被复制到新的页面上。

14. 旧的页面标志被清除，以表明该页面不再提供任何信息。

15. 新页面上的回写队列被触发了。

16. 如果迁移条目被插入到页表中，那么就用真正的ptes替换它们。这样做将使那些尚未等待页
    锁的用户空间进程能够访问。

17. 页面锁从新旧页面上被撤销。等待页锁的进程将重做他们的缺页异常，并将到达新的页面。

18. 新的页面被移到LRU中，可以被交换器等再次扫描。

非LRU页面迁移
=============

尽管迁移最初的目的是为了减少NUMA的内存访问延迟，但压缩也使用迁移来创建高阶页面。

目前实现的问题是，它被设计为只迁移*LRU*页。然而，有一些潜在的非LRU页面可以在驱动中
被迁移，例如，zsmalloc，virtio-balloon页面。

对于virtio-balloon页面，迁移代码路径的某些部分已经被钩住，并添加了virtio-balloon
的特定函数来拦截迁移逻辑。这对一个驱动来说太特殊了，所以其他想让自己的页面可移动的驱
动就必须在迁移路径中添加自己的特定钩子。

为了克服这个问题，VM支持非LRU页面迁移，它为非LRU可移动页面提供了通用函数，而在迁移
路径中没有特定的驱动程序钩子。

如果一个驱动程序想让它的页面可移动，它应该定义三个函数，这些函数是
struct address_space_operations的函数指针。

1. ``bool (*isolate_page) (struct page *page, isolate_mode_t mode);``

   VM对驱动的isolate_page()函数的期望是，如果驱动成功隔离了该页，则返回*true*。
   返回true后，VM会将该页标记为PG_isolated，这样多个CPU的并发隔离就会跳过该
   页进行隔离。如果驱动程序不能隔离该页，它应该返回*false*。

   一旦页面被成功隔离，VM就会使用page.lru字段，因此驱动程序不应期望保留这些字段的值。

2. ``int (*migratepage) (struct address_space *mapping,``
|	``struct page *newpage, struct page *oldpage, enum migrate_mode);``

   隔离后，虚拟机用隔离的页面调用驱动的migratepage()。migratepage()的功能是将旧页
   的内容移动到新页，并设置struct page newpage的字段。请记住，如果你成功迁移了旧页
   并返回MIGRATEPAGE_SUCCESS，你应该通过page_lock下的__ClearPageMovable()向虚
   拟机表明旧页不再可移动。如果驱动暂时不能迁移该页，驱动可以返回-EAGAIN。在-EAGAIN
   时，VM会在短时间内重试页面迁移，因为VM将-EAGAIN理解为 "临时迁移失败"。在返回除
   -EAGAIN以外的任何错误时，VM将放弃页面迁移而不重试。

   在migratepage()函数中，驱动程序不应该接触page.lru字段。

3. ``void (*putback_page)(struct page *);``

   如果在隔离页上迁移失败，VM应该将隔离页返回给驱动，因此VM用隔离页调用驱动的
   putback_page()。在这个函数中，驱动应该把隔离页放回自己的数据结构中。

非LRU可移动页标志

   有两个页面标志用于支持非LRU可移动页面。

   * PG_movable

     驱动应该使用下面的函数来使页面在page_lock下可移动。::

	void __SetPageMovable(struct page *page, struct address_space *mapping)

     它需要address_space的参数来注册将被VM调用的migration family函数。确切地说，
     PG_movable不是struct page的一个真正的标志。相反，VM复用了page->mapping的低
     位来表示它::

	#define PAGE_MAPPING_MOVABLE 0x2
	page->mapping = page->mapping | PAGE_MAPPING_MOVABLE;

     所以驱动不应该直接访问page->mapping。相反，驱动应该使用page_mapping()，它可
     以在页面锁下屏蔽掉page->mapping的低2位，从而获得正确的struct address_space。

     对于非LRU可移动页面的测试，VM支持__PageMovable()函数。然而，它并不能保证识别
     非LRU可移动页面，因为page->mapping字段与struct page中的其他变量是统一的。如
     果驱动程序在被虚拟机隔离后释放了页面，尽管page->mapping设置了PAGE_MAPPING_MOVABLE，
     但它并没有一个稳定的值（看看__ClearPageMovable）。但是__PageMovable()在页
     面被隔离后，无论页面是LRU还是非LRU可移动的，调用它开销都很低，因为LRU页面在
     page->mapping中不可能有PAGE_MAPPING_MOVABLE设置。在用pfn扫描中的lock_page()
     进行更大开销的检查来选择受害者之前，它也很适合只是瞥一眼来测试非LRU可移动的页面。

     为了保证非LRU的可移动页面，VM提供了PageMovable()函数。与__PageMovable()不
     同，PageMovable()在lock_page()下验证page->mapping和
     mapping->a_ops->isolate_page。lock_page()可以防止突然破坏page->mapping。

     使用__SetPageMovable()的驱动应该在释放页面之前通过page_lock()下的
     __ClearMovablePage()清除该标志。

   * PG_isolated

     为了防止几个CPU同时进行隔离，VM在lock_page()下将隔离的页面标记为PG_isolated。
     因此，如果一个CPU遇到PG_isolated非LRU可移动页面，它可以跳过它。驱动程序不需要
     操作这个标志，因为VM会自动设置/清除它。请记住，如果驱动程序看到PG_isolated页，
     这意味着该页已经被VM隔离，所以它不应该碰page.lru字段。PG_isolated标志与
     PG_reclaim标志是同义的，所以驱动程序不应该为自己的目的使用PG_isolated。

监测迁移
========

以下事件（计数器）可用于监控页面迁移。

1. PGMIGRATE_SUCCESS: 正常的页面迁移成功。每个计数器意味着一个页面被迁移了。如果该
   页是一个非THP和非hugetlb页，那么这个计数器会增加1。如果该页面是一个THP或hugetlb
   页面，那么这个计数器会随着THP或hugetlb子页面的数量而增加。例如，迁移一个有4KB大小
   的基础页（子页）的2MB THP，将导致这个计数器增加512。

2. PGMIGRATE_FAIL: 正常的页面迁移失败。与上面PGMIGRATE_SUCCESS的计数规则相同：如
   果是THP或hugetlb，这个计数将被子页的数量增加。

3. THP_MIGRATION_SUCCESS: 一个THP被迁移而没有被分割。

4. THP_MIGRATION_FAIL: 一个THP不能被迁移，也不能被分割。

5. THP_MIGRATION_SPLIT: 一个THP被迁移了，但不是这样的：首先，这个THP必须被分割。
   在拆分之后，对它的子页面进行了迁移重试。

THP_MIGRATION_* 事件也会更新相应的PGMIGRATE_SUCCESS或PGMIGRATE_FAIL事件。
例如，一个THP迁移失败将导致THP_MIGRATION_FAIL和PGMIGRATE_FAIL增加。

Christoph Lameter，2006年5月8日。

Minchan Kim，2016年3月28日。