虚拟内存申请先经过三种 overcommit 准入政策之一;后续物理需求仍受 cgroup、NUMA 及其他资源约束。

Linux overcommit 为虚拟内存承诺设定政策

Linux 的 overcommit 政策控制某些虚拟内存承诺是否可以获准,和分配常驻物理页、选择 OOM 牺牲进程、执行容器内存限制并不是同一件事。一次成功申请可能早于实际物理需求,但这不表示所有分配都没有成本,也不保证后续访问在任何资源约束下都能成功。分析时应把承诺、驻留和故障处理分别记录,而不是都称为“剩余内存”。 虚拟承诺不同于实际驻留 malloc 等分配器可能通过 brk 或 mmap 取得内存,也可能直接复用已有内存区,不发起新的系统调用。匿名页通常按需填入,但地址空间元数据、页表以及实际缺页过程仍然消耗资源。具体成本取决于应用申请什么、访问什么,不能把延迟分配简化成近乎免费地运行整个进程。 Committed_AS 是记账数量,不是所有进程常驻集之和,也不是当前内存压力读数。映射类型会影响成本:私有可写映射可能在页面尚未全部驻留时就形成承诺,普通共享或只读文件映射则采用不同规则。因此,虚拟大小很大的进程未必就是物理内存短缺的责任方,应同时查看实际使用和对应记账范围。 三种准入政策 模式零是默认启发式政策,拒绝明显超额的申请,同时允许超过简单常驻内存预算的分配。它的判断不等于为每个获准字节准备专属 RAM。模式一跳过 overcommit 准入检查,有助于某些稀疏工作负载,但地址空间上限、映射数量以及其他分配失败仍然存在,不能描述成任何荒谬请求都会被无条件接受。 模式二执行更严格的承诺记账。名义 CommitLimit 来自交换空间加上配置的内存额度:比例计算会扣除预留给 hugetlb 的内存,非零 overcommit_kbytes 则用绝对额度代替比例。内核保留量与调用上下文还会影响实际准入。因此,CommitLimit 与 Committed_AS 的差值是有用背景,不是保证任意调用者都能一次申请成功的最大值,也不是当前可用物理内存。 需要记账的分配如果无法纳入预算,可能立即返回 ENOMEM,使部分容量问题更早暴露,包括 fork 和后续改变映射权限所需的承诺。fork 记账与具体映射及写时复制义务有关,并不是简单把父进程常驻集乘二。有些应用有意保留稀疏地址空间,因此政策变化可能影响启动,即使它们预计不会触碰所有页面。 严格记账不是 OOM 免疫 模式二降低一类超额承诺风险,却不会覆盖 cgroup 内存限制、NUMA 约束、其他内核分配以及所有后续需求。任务仍可能遇到内存分配失败或 OOM 事件。不能承诺选了模式二,某个业务工作进程就绝不会被杀,更不能把这个模式当成服务可靠性的单一开关。 ...

十二月 18, 2027 · 2 分钟 · XB Field Notes
/work 目录分支到标记删除的旧条目、保留的新文件,以及由 x 规则递归保留的子树。

systemd-tmpfiles 执行创建与年龄策略

systemd-tmpfiles 用配置文件描述文件系统对象与维护规则。一条规则可以按指定模式创建目录,年龄规则则可以让旧条目符合清理条件,但只有命令实际执行对应操作,效果才会发生。这是一种有用的生命周期策略,不是连续监视器,也不是硬性存储配额。目录已经声明、清理命令曾经运行和磁盘空间始终有界,是三种不能互相替代的事实。 树形图描述原文特定的 d 与 x 年龄清理样例。x 排除不能覆盖显式 r 或 R 删除。执行清理、删除路径名与释放存储是不同事件;本轮绘图没有运行清理。 坏掉的样子 年龄不只是“创建文件多久了”,也不只是“多久没有修改内容”。文档中的默认规则考虑多个时间戳,包括访问与修改时间、文件状态变化时间,以及可获得的创建时间。某个参与判断的时间戳较新,就可能保留一个看起来 mtime 很老的条目。因此,为了测试而改写时间戳,也可能以意外方式改变结果;不能只观察其中一个字段就断定工具应该删除文件。 --create 和 --clean 执行不同操作。创建配置中的目录,不证明里面的旧内容已经清除。下面使用的 d 规则保留目录本身,同时让符合年龄条件的内容参与清理;其他类型有不同删除语义。不能因为都叫“清理”,就随意换成递归移除规则,否则可能把按条件删除变成完全不同的破坏性操作。 年龄清理中的 x 排除规则可以保留匹配目录及其内容,X 则只排除目录本身,没有相同的递归保护。它们也不会覆盖显式 r 或 R 删除规则。判断一次维护会删什么,必须同时阅读最终生效配置与执行操作,而不是只找到某条看起来写着“忽略”的记录,就认定其他规则都不可能影响该路径。 快速验证 在装有 systemd-tmpfiles 的一次性 Linux 环境中,以管理员身份运行下面的 Python 脚本。恢复文本记载的环境为 Debian 12、systemd 252.39,但原始运行日志不可用。本轮没有执行此样例;以下结果说明其预期断言,不能视为本轮实测。脚本传入一份明确配置和临时 --root,所有创建对象与删除目标都属于本次实验,不会使用当前系统平时执行的 tmpfiles 配置: ...

十二月 10, 2027 · 2 分钟 · XB Field Notes
三个编号命令形成有限的向前执行序列,大幅重启箭头从完成处返回第一条命令,说明新的激活可以重复工作。

systemd oneshot 完成的是一次激活

Type=oneshot 服务表达一段有限工作,启动命令成功结束后,本次启动才算完成。因此,它适合初始化步骤,以及依赖方需要等待任务结束的定时工作。oneshot 描述的是一次激活的进程模型,不会建立永久记录来阻止任务在重启、再次激活或机器重新开机后重跑。名字中的“一次”,不能直接推导为业务意义上的“永远只做一次”。 oneshot 完成的是一次激活。后续命令失败不会撤销先前副作用,保留的 active/exited 状态也可以没有存活主进程。三个命令和 PID 0 来自原文样例;本轮绘图没有执行服务。 坏掉的样子 成功的 oneshot 通常在命令完成后变为非活动状态,这是任务已经结束的正常表现,不是守护进程崩溃的证据。设置 RemainAfterExit=yes 后,管理器会在命令退出后仍把单元记为活动。因此,active (exited) 可以完全没有主进程;如果监控假定任何活动服务都必须有一个存活 PID,就会错误判断这种单元类型。 oneshot 允许多条 ExecStart,并按顺序执行。采用普通成功规则时,某条命令失败会阻止后续启动命令运行,并让本次激活失败。但前面的命令可能已经修改文件或数据库,systemd 不会回滚这些副作用。重新尝试整个激活,就可能重复之前已经成功的部分,不能把管理器的失败状态误当作业务事务已经全部撤销。 RemainAfterExit 保存的是管理器状态,不是持久的恰好一次执行保证。再次启动已经活动的单元不会重跑,重启却会。定时器指向一个仍然活动的 oneshot 时,也不会把每个到达的时间点都变成另一次执行。只有它确实代表一个持续成立的状态时,才适合保留活动标记;需要反复触发的定时工作,应在各次执行之间回到非活动状态。 ...

十一月 28, 2027 · 1 分钟 · XB Field Notes
两个命名空间边界内的 left0 与 right0 构成一对链路;右侧还拥有回环目的地址。

veth 对在两个端点之间搬运帧,路由决定这些帧承载的路径

veth 对由两个互联的虚拟以太网设备组成。从一端发送的帧到达另一端,再由接收网络栈或网桥处理。两端可以放在不同 Linux 网络命名空间,因此常用于容器网络。创建这一对设备只提供链路,地址、路由、桥接关系、转发和访问策略都需要另外配置。不能把接口已经出现,等同于完整网络已经准备好。 坏掉的样子 接口显示管理状态 UP,仍可能因为 veth 对端关闭而没有载波。只读取 UP 标志,会让尚未完成的配置看起来已经就绪。应检查两个端点,并区分管理状态与 LOWER_UP。路由也可能已经指向这个接口,但链路仍无法承载有效流量,所以路由列表本身不是连通性检查,更不能代替一次真实应用交换。 veth 对不会把 IP 载荷限制为对端接口自身地址。路由选择报文经过哪个链路,随后以太网转发或接收网络栈决定下一步处理。发往容器直接邻居之外的流量,也可以通过这一对设备到达路由器或网桥。声称“不发给对端的包都会留在本地”,混淆了链路交付与 IP 目的地选择,容易误导容器出口排障。 两端生命周期也相互关联。删除一个 veth 端点,会移除另一个端点,即使对端由另一个命名空间管理。因此宿主侧清理动作可能删除容器接口。影响活跃工作负载时,应有意识地安排停止流量、调整路由和删除设备的顺序。命名空间分离了管理视图,却没有让成对设备变成两项互不影响的资源。 veth:跨命名空间设备对:概念机制与预期检查,不是新增运行证据。 快速验证 将脚本保存为 check-veth.py,仅在已获授权的一次性 Linux 实验环境中,使用核实为本地的 Docker daemon,并保留 Python 断言。配方注明 Python 3.11.16、iproute2 6.1.0,但镜像架构、包与宿主内核仍需核对。NET_ADMIN、SYS_ADMIN 无法覆盖强制策略的挂载拒绝,Docker 默认 AppArmor 配置即存在此限制。应使用兼容且获批的实验环境,不应为运行示例削弱宿主保护。嵌套命名空间与 proc 挂载需要实际内核权限,运行时隔离不代表拉取镜像和构建离线;清理命令仅尝试执行,未全部核对退出码。 ...

十一月 21, 2027 · 2 分钟 · XB Field Notes
内存访问可进入内存中的次缺页处理、涉及换入 I/O 的主缺页处理,或非法访问处理;缺页数不等于全部存储请求数。

缺页并不总是意味着存储读取

缺页表示处理器无法依据当前映射或权限完成一次访问。它不等于存储读取,有些缺页还代表非法访问,而不是可以恢复的按需分页。Linux 的次缺页与主缺页计数,区分处理过程中是否需要页面换入输入输出。解释时应结合映射类型与工作负载,不能把任意一个计数直接当作应用全部存储流量,更不能仅凭名字中的“主”“次”判断问题严重程度。 次缺页不代表没有成本 次缺页可能为已有文件缓存页建立映射,也可能为匿名内存分配并清零物理页,或在写时复制时复制一页内容。内核可能需要分配内存、维护页表、复制数据和同步。把它说成只增加一个指针,会遗漏这些真实成本。即使完全没有存储读取,密集次缺页仍然可以消耗可观处理器时间,影响请求延迟,不能因为分类是 minor 就无条件忽略。 应用第一次访问,并不保证每个基本页都产生一次新的缺页。预先建立映射、已存在的映射、预读相关处理和大页都可能改变计数。系统页大小也不是处处都等于四千零九十六字节。示例应读取本机值,不能从循环次数直接承诺精确缺页数量;否则换一台机器或启用不同内存策略,实验就可能因为错误前提而失败。 主缺页描述另一条处理路径 主缺页意味着处理涉及页面换入输入输出,常见于文件映射或已换出的内存。背后可能是存储设备,也可能是远程文件系统,因此这个分类不是物理磁盘延迟基准。一次缺页还可能触发预读,让后续访问直接获益,计数与页数或磁盘请求数并不一一对应。反过来,普通文件读取也可能产生输入输出,却不在应用里形成对应的主缺页。 文件页已经在主机缓存中时,新进程即使从未访问过它,也可能只产生次缺页。重启进程不等于清空整台机器的页缓存。匿名内存首次写入通常通过分配零填充页面完成,不必从存储读回旧载荷,但压力和同时发生的活动仍会影响测量。“匿名”不能保证整个进程的主缺页计数在任何环境都保持不变;应以本次实际计数为准。 坏掉的样子 冷启动延迟可能与主缺页同时出现,但在查清缺少什么之前,不应一律建议预热。提前触碰不需要的页面会消耗内存,甚至挤掉有用缓存。错开启动也许能缓解已确认的共享瓶颈,另一个负载却可能需要更小映射、不同访问模式或更多容量余量。单靠缺页数无法在这些方案之间作出可靠选择,需要把变化与业务延迟联系起来。 持续交换会让被逐出的页面反复进入工作集,并拉长等待。增加工作者可能扩大内存需求,使情况更差。应关联缺页、换入换出、回收、存储等待和请求延迟,而不是一看到主缺页上升就宣布存储设备损坏,或认定所有缓存缺失都本来可以避免。对顺序扫描和随机访问,也应分别理解预读与局部性的作用。 进程级计数还包含测量代码之外的工作。解释器、启动过程和其他线程都可能贡献变化。常驻情况查询只是一个时刻的快照,紧接着访问时状态可能已经改变。不要为了得到更明显的图表就清空全局缓存,或不断扩大分配直到影响其他任务。诊断实验应该有明确资源预算,并保留失败时的原始观察,而不是只保留符合预期的数字。 次缺页、主缺页与非法访问走不同路径;匿名内存示例并未覆盖所有路径。 快速验证 保存为 fault_probe.py,在有足够空闲内存的 Linux 上使用 Python 3.9 或更新版本执行 python3 fault_probe.py。程序创建十六 MiB 的私有匿名映射,按本机页大小每页写一个字节,再重复一轮。结束时自动关闭映射,不操作文件、不修改全局缓存设置,也不改变任何内存限制。 import mmap import resource import sys import time if not sys.platform.startswith("linux"): raise SystemExit("This exercise requires Linux getrusage semantics") PAGE = mmap.PAGESIZE SIZE = 16 * 1024 * 1024 def counters(): value = resource.getrusage(resource.RUSAGE_SELF) return value.ru_minflt, value.ru_majflt def touch(region): for offset in range(0, len(region), PAGE): region[offset] = 1 with mmap.mmap(-1, SIZE, flags=mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS, prot=mmap.PROT_READ | mmap.PROT_WRITE) as region: before = counters() start = time.perf_counter() touch(region) first_seconds = time.perf_counter() - start middle = counters() start = time.perf_counter() touch(region) second_seconds = time.perf_counter() - start after = counters() first = tuple(b - a for a, b in zip(before, middle)) second = tuple(b - a for a, b in zip(middle, after)) print(f"page_size={PAGE} mapping_bytes={SIZE}") print(f"first: minor={first[0]} major={first[1]} seconds={first_seconds:.6f}") print(f"second: minor={second[0]} major={second[1]} seconds={second_seconds:.6f}") assert first[0] > 0, "expected demand-fault activity" assert second[0] < first[0], "repeat pass did not show reduced minor faults" assert first[1] == second[1] == 0, "observed major faults: this run is not a no-major example" print("PASS: this run had fewer second-pass minor faults and no measured major faults") 本实验预期第一轮出现按需缺页,第二轮次缺页减少,并且这次运行没有测得主缺页。本轮两种语言的完整示例均通过这些断言。计数与耗时只是观察,不是固定目标。脚本没有断言第二轮一定更快,因为计时、调度和机器负载会变化。使用本机页大小,也避免把某种平台常见值误当成通用常量。 ...

十一月 13, 2027 · 1 分钟 · XB Field Notes
只要 socket 单元仍活动,管理器便在本例工作进程回复并主动退出后保留监听器。Accept=no 传递监听器,并不强制每连接创建一个进程。

Socket 激活把监听器交给兼容的服务

Socket 激活让 systemd 在应用进程启动之前先打开监听端点。端点上的活动随后可以触发兼容服务,并把管理器已经持有的套接字描述符传给它。这里真正分开的,是监听器所有权与应用生命周期。端点已经开放,还不证明应用完成初始化、接受了请求或生成了正确响应;传输层能连接与业务层能工作,需要分别验证。 socket 单元先活动,对应服务的 MainPID 为 0。首个连接触发工作进程,它校验 LISTEN_PID、LISTEN_FDS 1 及监听中的 FD 3,处理有完整边界的请求并回复。工作进程主动退出,管理器保留监听器;后续连接启动不同的进程,仍使用 FD 3。重复绑定会得到 EADDRINUSE。停止 socket 后移除路径,是因为本例明确配置了 RemoveOnStop=yes。 仅示意选定的原文机制;本轮没有执行示例。 坏掉的样子 应用必须理解激活协议。如果守护进程无条件再次绑定同一个端点,就可能因为管理器已经占用而失败。激活机制把绑定责任移到 socket 单元,并不会消除地址冲突、接口缺失、文件系统权限错误,或其他导致首次绑定失败的条件。因此不能把“端口先开好”写成所有启动竞态已经自动消失,真正的监听建立仍然可能失败。 使用 Accept=no 时,服务接收监听套接字并自行接受连接;Accept=yes 则为合适的流套接字支持按连接启动服务实例的模式。它们对应不同的应用契约,不能互换。任何一种模式都不意味着任意守护进程会自动判断空闲、完成工作后退出,或者只在安全时重新启动。是否退出,以及退出时哪些请求已经完成,仍然由应用行为决定。 ...

十一月 8, 2027 · 2 分钟 · XB Field Notes
三个启动位于同一个括起的十秒窗口内。等长重启间隔指向下一次启动,粗停止栏在上限三次处挡住第四次请求。这些数值是原文样例配置。

systemd 重启策略要明确失败与限流规则

重启策略决定当前进程结束后,服务管理器是否应该再启动一个进程。它可以帮助恢复短暂故障,但修不好缺失配置或必然重复的程序错误。一个有用的契约需要分别回答三个问题:哪些结束方式应该重试、重试前等多久、一个时间窗口内允许启动多少次。这些设置各管一件事,不能把它们笼统理解为“失败后会自己恢复”。 先分类结束原因,再应用固定延迟和窗口启动上限。100 毫秒、十秒、三次启动来自原文 systemd 252.39 样例,图形不按时间比例,也不是本轮实测或生产推荐值。清除限流前先读取并修复失败原因。 坏掉的样子 Restart=on-failure 不代表任何信号结束都会重启。对于 oneshot 以外的服务类型,通常的 SIGHUP、SIGINT、SIGTERM 和 SIGPIPE 终止,默认被视为干净结束;非零退出码,以及 SIGKILL 这样的异常信号,才符合默认失败规则。SuccessExitStatus 和专门的重启例外指令还能改变分类,因此要检查最终生效的单元,不能只看其中一行策略名字。 on-abnormal 也不是 on-failure 的“更严格同义词”:它排除了普通的非零进程退出码,保留异常信号以及部分超时或看门狗故障。选择依据应是程序怎样报告可恢复失败与主动结束。如果进程始终存活,却已经不再提供有用工作,还需要单独配置发现这种状态的机制;基于退出的重启逻辑不会自动推断业务是否健康。 RestartSec 提供下一次重启前的等待时间。在下面的 systemd 252 实验里,它是固定延迟,不是越来越长的指数退避。StartLimitIntervalSec 和 StartLimitBurst 限制窗口内的启动次数,手动启动也算在内;它们既不是整个生命周期的重试预算,也不会在超限后自动预约下一窗口边界再尝试一次。限流与恢复触发应分别理解。 ...

十一月 7, 2027 · 2 分钟 · XB Field Notes
时刻一读取 status,时刻二读取 stat,来自不断变化的进程。受控子进程退出并被回收后,其 proc 目录消失;多次读取不会组成原子快照。

按文档读取不断变化的进程 procfs 视图

Linux procfs 通过文件系统接口暴露进程和内核信息。许多入口在读取时生成内容,而不是保存普通磁盘文件字节;每个入口都有具体含义、权限模型和格式。连续读取多个入口,不会冻结进程,也不会得到事务一致快照。因此,有用的观察必须对应正确进程实例和正确解释,不能因为内容都来自 proc,就假定它们是同一时刻的完整状态。 成功读取链接文本,不保证能够通过链接打开对象。Linux 还会单独检查被引用 inode 的权限。反过来,只读取描述符链接文字也不等于消费了持有者的数据。应分清查看元数据、打开引用对象以及读取对象内容,尤其是管道等带状态的数据流,不能把本例普通文件的结果泛化到所有描述符。 每个 procfs 入口都是有文档定义且受权限检查的视图。读取时刻、进程身份、专用解析方式和对象寿命,共同决定观察能够证明什么。 坏掉的样子 普通文件假设可能立刻失效。proc 条目报告大小为零,却仍然返回有用内容;逐进程目录可能随着进程退出消失,数字 PID 之后也可能指向另一个实例。读取失败不一定表示系统安装损坏,某个编号也不会永久代表同一工作负载。生命周期变化本来就是观测需要处理的条件,应避免把正常退出当成异常环境。 不同入口需要按各自格式解析。status 包含有标签字段,stat 则使用固定字段顺序,其中括号包住的命令名可以包含空格和右括号。直接把整行按空白拆分,会让预期字段错位。cmdline 与 environ 使用空字符分隔,普通按行或空格切分,会破坏参数和值的边界。解析方式必须来自接口文档,而不是文件看起来像文本这一点。 视图本身也有语义边界。environ 描述程序执行时建立的环境区域,不保证是所有后续环境修改的实时字典;命令行内容也可以被进程修改。描述符链接以 deleted 结尾,不表示打开对象已经消失。这些入口提供具体状态证据,却不是可以互相替代的身份凭据或访问保证,需要先明确每项观察到底回答什么问题。 ...

十月 29, 2027 · 2 分钟 · XB Field Notes