在 Linux 上,sysctl 通过 /proc/sys 暴露一部分内核参数。它可以观察当前运行状态;有权限的写入则能在不重启的情况下改变状态。配置文件是另一层东西:它要求某个加载器在特定时刻执行赋值。混淆这两层,就会遇到常见故障:磁盘上的配置看起来正确,内核实际使用的却是另一个值。

本文限定 Linux、procps-ng 和 systemd。BSD、macOS 也提供名为 sysctl 的命令,但接口和持久化方式不同。在 macOS 上数出多少个键,不能作为 Linux procfs 接口的验证证据。

先确认正在读取哪个接口

对普通名称来说,kernel.ostype 对应 /proc/sys/kernel/ostype;procps 也接受斜杠分隔符。若名称本身含有字面意义的点号,应遵循工具的分隔规则,不能盲目把所有点替换成斜杠。可用键取决于内核配置、已加载模块和执行环境。不同机器的键集合不保证相同,同一台机器运行期间也可能变化。

procps 的普通输出是 key = value,-n 则只打印值。一次读取成功,说明调用者在当前环境看到了这个值,并不证明持久策略已经生效。有些值还会在观察期间变化。排查时只读取所需键,不能把所有读取都当成无副作用操作:手册明确说明,读取 vm.stat_refresh 会刷新每个 CPU 的统计,因此普通枚举会跳过它。

持久化由具体加载器完成

运行时赋值不会替你生成持久配置文件;反过来,保存配置片段也不会立即改变内核。讨论优先级之前,先确定系统在启动或重载时由哪个组件应用设置。

procps 的 --system 会先根据目录优先级选出文件:高优先级目录中的同名文件遮蔽低优先级副本。选中的片段随后按文件名的字典序应用,不按目录逐层决定最终值;最后再读取 /etc/sysctl.conf。systemd 的 sysctl.d 加载器也结合目录优先级和文件名排序,但不直接读取 /etc/sysctl.conf。发行版仍可能通过配置目录中的链接把这个文件纳入加载范围,因此要检查实际安装,不能只凭路径名称下结论。

这里存在两种不同覆盖:使用相同文件名可整体替代厂商文件;使用排序更靠后的不同文件名,可覆盖其中某个参数。两者都不能阻止另一个服务在加载完成之后再次写入同一参数。启动时赋值成功,与几小时后观察到的有效值,是需要分别记录的两件事。

不要混用不同实现的错误策略

systemd 的 sysctl.d 文档把变量不存在和权限失败列为调试级、非致命情况;其他赋值错误会在处理完其余设置后让服务失败。在某条赋值前加减号,则可忽略该条赋值的失败。这是 systemd 加载器的规则,不能移植成对每次 procps 命令退出状态的保证。

procps 的 -e 专门忽略未知键错误,不等于所有格式错误或被拒绝的写入都会成功。它的配置格式也支持用前导减号忽略某条赋值错误。把一个实现的选项和另一个实现的错误分类拼在一起,无法得到可移植的诊断结论;工具返回成功,也不应替代对目标值的明确核验。

坏掉的样子

依赖模块的参数,在启动加载器运行时可能尚不存在。应先检查模块是否可用,以及加载器当时的日志,再判断后来的读取结果是否暴露配置语法问题。另一个写入者也可能覆盖已经成功应用的值。把加载器名称、文件优先级、应用时间和后续有效值分开记录,才能区分没有应用与应用后又被改动。

容器又增加了一层边界:挂载方式、权限和命名空间都会影响可见性与写入范围。必须查询具体参数的文档;仅凭 net.* 这样的前缀,不能证明一次修改被隔离在容器内。不要为了让排查命令成功而临时扩大权限或重新挂载 procfs,否则诊断本身就改变了安全边界。

上下排列的卡片区分同名文件遮蔽与选中文件的排序;另一条文字链展示已加载值被后续写入者更改。加载器差异、只读探测范围和错误策略警告均明确保留。 静态说明原文;制作本图未执行文章实验。
加载器选择与排序先于运行时值,后续写入者仍可更改;原文检查只读观察。

快速验证

下面的只读练习提案要求 Linux、Python 3、procps-ng 4.0.4,且 procfs 可读;这一完整真实调用仍未执行。它通过两个接口比较一个普通参数,再检查故意不存在的键是否被拒绝。它不枚举未知处理器、不加载配置、不写参数,也不验证重启后的持久性。

import os
import subprocess
from pathlib import Path

def require_result(result, status, stdout, stderr):
    if (type(result.returncode) is not int or type(result.stdout) is not bytes
            or type(result.stderr) is not bytes
            or (result.returncode, result.stdout, result.stderr) != (status, stdout, stderr)):
        raise RuntimeError(f"unexpected sysctl result: {result!r}")

def read_sysctl(*args):
    return subprocess.run(
        ["sysctl", *args], capture_output=True, check=False, timeout=5,
        env={**os.environ, "LC_ALL": "C"})

require_result(read_sysctl("--version"), 0,
               b"sysctl from procps-ng 4.0.4\n", b"")
require_result(read_sysctl("kernel.ostype"), 0, b"kernel.ostype = Linux\n", b"")
require_result(read_sysctl("-n", "kernel.ostype"), 0, b"Linux\n", b"")
if Path("/proc/sys/kernel/ostype").read_bytes() != b"Linux\n":
    raise RuntimeError("unexpected procfs kernel type")
require_result(read_sysctl("-n", "kernel.xbcatm_audit_missing_key"), 1, b"",
               b"sysctl: cannot stat /proc/sys/kernel/xbcatm_audit_missing_key: "
               b"No such file or directory\n")
print("matching kernel type: Linux")
print("exact missing-key diagnostic accepted")

只有各项核验成功后,才应出现值一致及精确诊断通过的提示。负向对照固定使用 C 区域设置,要求退出码为 1、标准输出为空,并逐字匹配缺失路径的完整诊断,包括末尾换行。权限错误、工具缺失、超时、截断输出或意外成功均应失败,不能算未知键通过。其他 procps 版本或诊断格式需要单独审阅,不应放宽为子串匹配。纯响应测试并不验证 procfs 或真实命令;不要为了通过而扩大权限或改成写入。尤其要注意,sysctl --system 和 sysctl -p 会实际应用设置,并不是配置预览命令,因此不应放进只读检查中。

来源