日志文件经过轮转从活动变为归档,之后 vacuum 可以删除归档文件。

systemd journal 的保留范围需要明确约定

systemd journal 把消息与结构化元数据一起保存在日志文件中,因此可以查询服务输出,不必先为每种应用文本设计一套解析器。但让这些消息保留足够长时间,是另一项需要明确的要求。存储位置、空间策略、轮转方式与转发路径,决定了事故发生一段时间后还有哪些证据可查。查询方便,不等于历史永远完整,也不等于数据已经离开这台机器安全保存。 存储路径与保留范围是不同问题。轮转将旧文件关闭成归档并打开替代文件;vacuum 可以删除归档,不能删除活动文件。journald 服务重启不能证明整机重启或断电耐久性,字节目标也不保证固定证据窗口。本图是原文模型,不是新的执行结果。 坏掉的样子 默认命名空间通常使用 Storage=auto:存在 /var/log/journal 时选择持久存储,否则使用运行时存储。Storage=volatile 把文件放在 /run/log/journal 下,并不是简单借用内核的文本环形缓冲区。这些运行时文件通常会在整台机器重新启动后消失,但重启 journald 服务是不同的事件,并不必然清空文件。排查“重启后日志去哪了”之前,必须明确重启的是哪个对象。 Storage=persistent 优先使用 /var/log/journal,早期启动阶段或持久位置不可写时仍可能使用运行时存储。满足文档规定条件时,flush 操作可以转移运行时数据。因此应检查真实文件和最终生效配置,不能只因为某份配置片段里出现了 persistent,就推定所有消息已经写在预期磁盘上。路径、挂载状态与执行时点都需要对应到实际记录。 journald 本身有默认空间限制和轮转行为,不会仅仅因为管理员没有写一个轮转任务就无限增长。不过,SystemMaxUse 也不是保证文件系统永不写满的精确硬配额。运行时存储有另一组设置,可用空间限制同时起作用,其他程序也会占用磁盘,而清理时只能删除已经归档的 journal 文件。把这些条件压缩成“设置一个上限就绝对安全”,会掩盖真正的磁盘压力来源。 快速验证 在由 systemd 管理的一次性 Linux 系统中,以管理员身份运行下面的 Python 脚本。恢复文本记载的环境为 Debian 12、systemd 252.39,但原始运行日志不可用。本轮没有执行此样例;以下结果说明其预期断言,不能视为本轮实测。脚本创建唯一日志命名空间,显式配置持久存储,通过临时服务写入两条无害消息,最后只删除该命名空间的数据。实验中的 vacuum 会有意删除自己刚写入的旧消息: from pathlib import Path import json,shutil,subprocess,uuid name='xbcatm-journal-'+uuid.uuid4().hex[:8] config=Path('/etc/systemd')/('journald@'+name+'.conf') directory=Path('/var/log/journal')/(Path('/etc/machine-id').read_text().strip()+'.'+name) assert not config.exists() and not directory.exists() def run(*args): result=subprocess.run(args,text=True,capture_output=True,timeout=15) assert result.returncode==0,(args,result.stderr) return result.stdout def journal(*args):return run('journalctl','--namespace='+name,*args) def emit(message): run('systemd-run','--quiet','--wait','--collect','--unit='+name+'-'+message, '--property=LogNamespace='+name,'--property=StandardOutput=journal', '--property=SyslogIdentifier='+name,'/usr/bin/printf',message+'\n') journal('--sync') def messages(): rows=journal('--no-pager','-o','json','SYSLOG_IDENTIFIER='+name) return [json.loads(line)['MESSAGE'] for line in rows.splitlines()] config.write_text('[Journal]\nStorage=persistent\nSystemMaxUse=16M\nRuntimeMaxUse=8M\n') try: emit('old');journal('--flush');journal('--sync') assert messages()==['old'] and list(directory.glob('*.journal')) run('systemctl','restart','systemd-journald@'+name+'.service') assert messages()==['old'] print('persistent namespace: message survives journald service restart') journal('--vacuum-size=1') assert messages()==['old'] print('vacuum cannot remove the active journal file to satisfy a one-byte target') journal('--rotate');emit('new') assert messages()==['old','new'] assert list(directory.glob('*@*.journal')) journal('--vacuum-size=1') assert messages()==['new'] and not list(directory.glob('*@*.journal')) print('after rotation: archived old message removed; active new message remains') print('persistent files observed under /var/log/journal; no reboot durability claim') finally: run('systemctl','stop','systemd-journald@'+name+'.service', 'systemd-journald@'+name+'.socket','systemd-journald-varlink@'+name+'.socket') config.unlink(missing_ok=True) for path in (directory,Path('/run/log/journal')/directory.name,Path('/run/systemd')/('journal.'+name)): if path.exists():shutil.rmtree(path) 第一条消息位于该命名空间的 /var/log/journal 目录中,重启对应 journald 服务后仍然能够查到。这验证了选中的存储位置,以及消息跨越日志守护进程重启仍然存在;它没有模拟突然断电,也没有证明存储设备的持久性,更没有执行整台机器重启。说明验证范围,可以避免把一个真实但有限的结果扩大成并未做过的耐久性测试。 ...

十月 26, 2027 · 1 分钟 · XB Field Notes
连续三次相同且未读的写入可以只产生一条 modify 通知。溢出信号表示事件历史已丢失;重扫恢复当前状态,不能补回缺失顺序。

inotify 报告变化,但不是完整历史记录

inotify 通过一个带注册监视项和可读事件队列的实例,投递 Linux 文件系统通知。每条记录包含监视编号、事件掩码,也可能包含名字和移动 cookie。这能减少对未变化路径的反复检查,但事件流不是无限审计日志。正确消费者应把通知理解为相关状态可能需要重新检查的证据,而不能默认它保存了文件发生过的每一次操作。 通知是有边界的变化证据。覆盖范围、对象替换与队列丢失,都要求重新核对文件系统当前状态;重扫不能补回缺失的事件历史。 坏掉的样子 监视目录不会递归监视后代。它报告直接目录项上的相关活动,因此需要观察更深变化时,新子目录也需要独立监视。目录树监视器必须处理初次枚举和之后加入的目录。假定只注册根目录就覆盖整棵树,会悄悄漏掉更深层变化;能收到子目录被创建,并不代表能收到它内部之后的全部操作。 文件监视跟随具体对象,不会永久绑定路径。编辑器可以先写完并关闭临时文件,再用 rename 替换目标。父目录会报告替换,旧文件监视却不会转移到新 inode。如果消费者等待目标名字之后再出现 close-write,可能永远等不到,因为替代文件在发布前已经关闭。不能把一种编辑器保存顺序当成所有更新方式的固定流程。 连续产生且尚未读取的相同事件可以合并,队列满后会丢弃事件并报告 IN_Q_OVERFLOW。因此,事件数量不是可靠写入计数。溢出后,消费者无法仅靠剩余记录重建全部缺失操作,必须重新核对当前监视状态,不能继续假定根据事件维护的模型完整。历史丢失与当前状态仍可读取,是需要分别理解的两件事。 快速验证 把脚本保存为 inotify-events.py,在一次性 Linux 环境中运行 python3 inotify-events.py。此前记录使用 Linux 6.12.76 和 ext4;2026-10-15 的复核记录了两种语言样本在 Linux 6.18.44、Python 3.12.14 上通过。该记录属于历史执行,此次未重跑。样本创建 ASCII 名称的临时条目,并有意填满自己的队列。配置的队列上限必须不超过三万二千七百六十八;脚本只读取上限,不修改任何 sysctl: ...

十月 13, 2027 · 2 分钟 · XB Field Notes
source 与 alias 两个目录项共同指向一个 inode。通过任一名字原地写入,都会改变共享字节;两个名字都没有天然的原件地位。

硬链接共享 inode,替换名字后关系会改变

硬链接为已有文件增加另一个目录项。两个名字都指向同一文件系统里的同一个 inode,其中没有哪个天然具有“原件”地位。内容、所有者、权限和链接数属于这个共享对象,文件名只是到达它的入口。因此,需要把目录项的变化与文件对象本身的变化分开理解,才能判断修改、删除和替换到底影响了哪些访问路径。 名字与打开引用的寿命不同。应跟踪 inode,并检查实际更新方式。 坏掉的样子 把硬链接当成独立备份,会产生错误的恢复边界。通过任意名字写入或截断共享 inode,另一名字读到的内容也会改变;修改模式位,同样改变的是一个对象的权限。第二个路径可以在第一个路径被删除后继续存在,却不能抵御针对共享文件的普通原地修改,也不会自动保留先前历史版本。 许多编辑器和部署工具采用另一种更新方式:先创建替代文件,再通过 rename 覆盖某个路径。该目录项此时指向新 inode,其他硬链接仍然到达旧对象。两个名字是否一起显示新内容,取决于实际更新方法。因此不能仅凭它们昨天建立过硬链接,就断言今天仍共享身份;应检查当前对象以及写入工具的真实行为。 硬链接还受文件系统挂载边界约束。即使两个目录在统一路径树里看起来相邻,link 也不能随意把不同挂载中的对象连接起来。Linux 还禁止普通目录硬链接。这些限制意味着,硬链接不能作为跨目录层次、跨挂载符号链接的通用替代品;使用前必须先确认目标对象和边界,而不是只看路径字符串。 ...

十月 10, 2027 · 2 分钟 · XB Field Notes
两次日历触发启动相互重叠的七十秒工作实例。

Cron 按日历重复触发任务

Cron 按日历字段匹配时间,并在匹配时启动命令。它不会天然等待同一个命令的上一次执行结束。如果每分钟触发的任务要运行七十秒,下一分钟就可能再启动一份。因此,触发规则、并发规则和成功完成状态必须分别设计,不能因为写好了一个周期表达式,就认为重复工作的所有行为都已经确定。 示意轨道展示七十秒工作结束前,下一次日历触发已到来。共享非阻塞锁使后来实例记录跳过,但不保证业务效果恰好一次。原文此前的计时结果未在本轮重跑。 把字段理解为日历选择 传统用户 crontab 一行依次包含分钟、小时、月内日期、月份、星期,然后是命令。星号表示该字段的全部取值。分钟位置的 */15 是在每个小时内选择相应分钟值,不是从上次任务结束之后再计时十五分钟。步长如果不能均匀划分字段范围,跨过边界时就可能出现不同长度的间隔,不能把它当作连续经过时长。 Debian/Vixie 格式对于两个日期字段有特殊规则:当月内日期和星期都受到限制时,满足其中一个日期条件就可能运行,而分钟、小时和月份限制仍然必须满足。这不是月份与星期之间的“或”关系。安装简短表达式之前,应列出具有代表性的实际日期核对意图,避免以为“每月某日恰好是周五”却写成两个日期条件各自都能触发。 时区、夏令时变化和停机都会影响日历调度,不同实现对跳过或重复时段的处理并不完全相同。Debian 文档为部分小幅时钟调整和固定时间任务定义了特殊行为,从其他调度器复制结论可能不适用。本实验没有修改时钟,也没有覆盖这些切换情况。日历触发同样不等于持久队列,不能自动保证停机期间错过的每次任务都会补跑。 坏掉的样子 重叠任务可能重复产生副作用、争夺资源或相互覆盖文件。如果要求同一时刻只有一个实例,就要明确实现这一政策。非阻塞建议锁可以让后来的实例记录跳过;阻塞锁则可能留下等待进程。两种选择都不自动提供业务效果恰好一次,也不能修复已经执行了一半的操作。是否允许跳过,应来自任务自己的业务要求。 本地文件锁只协调使用同一相关锁机制的参与者,不会自动串行化无关主机上的工作。网络文件系统的锁语义也需要单独确认。锁必须覆盖实际工作生命周期,跳过结果则应能被观察。如果一个实例永久卡住,后来每次触发都静默丢弃,表面上调度器还在工作,但业务可能早已停止推进,因此还要监控最近成功完成距今多久。 Cron 也并非一律没有日志。Debian 默认通过 syslog 记录任务启动,并提供更细的日志选项。命令输出可能在可用邮件链路存在时被投递,但极简容器经常没有这样的链路。应把启动、完成、失败和跳过结果写到明确目的地。启动记录只能证明任务被派发,不能证明真正的应用工作已经成功完成。 快速验证 2026-10-19 修复草案:此版本只进行了静态检查与离线命令/文件状态替身测试,没有执行 cron、crontab、账号或信号操作。技术与实际运行验证仍然受阻;下文测量值属于原始示例,不能视为修订版的执行结果。 未来执行必须另行获得授权,并使用新建的一次性 Debian 12 实验环境,预装 Python 3.11+、cron 3.0pl1-162 和 procps。环境不得挂载宿主目录,不得已有 root 任务表、cron 进程、/etc/crontab、/etc/cron.d 或其他用户任务表中启用的无关计划(包括周期目录的派发),也不得有并发管理员操作。root 只是权限前提,不能证明隔离。保存为 cron-overlap.py,未来命令为 python3 cron-overlap.py --isolated-disposable-lab。此参数仅确认由外部核实的前提,不能建立隔离。应预留约两分钟等待真实日历触发,并在结束后丢弃整个环境。 ...

九月 30, 2027 · 4 分钟 · XB Field Notes
先选名称后打开之间存在竞争空隙,排他原子创建则返回已拥有文件的描述符。

在可信位置原子创建临时文件

共享临时目录是其他用户也可能写入的命名空间,并不意味着里面每个文件都公开可读;内容访问仍受文件权限约束。真正危险的假设,是以为选定一个路径,就已经拥有这个名字对应的对象。应原子地创建新对象,并继续使用返回的句柄,而不是信任一个可预测名字,或先检查它不存在、稍后再打开。 名字归属与内容访问是两回事 Linux 的 /tmp 常用模式 1777,即宽松的目录写入、搜索权限,加上 sticky 位。sticky 限制其他用户删除或重命名目录项,却不让现有文件自动保密,也不提前保留尚未创建的名字。文件属主、目录属主、能力及其他访问控制仍然重要。应检查实际环境,不能假定所有操作系统都采用同一路径、挂载方式和权限。 普通写入打开可能跟随预先存在的符号链接。O_CREAT|O_EXCL 拒绝已经存在的最后一个路径名称,包括符号链接。O_NOFOLLOW 则拒绝最后一段是符号链接的情况,但单独使用时不会拒绝既有普通文件,也不保护所有祖先目录。这些是不同契约。应使用可信父目录和适当的临时文件接口,不能把一个标志当成通用路径安全方案。 创建模式也只在真正新建文件时生效。以 O_CREAT 和模式 0600 打开已有文件,不会收紧它原先的权限。随机名字降低碰撞概率,但安全性质来自排他创建,而不只是字符串看起来难以猜测。只生成名字的接口仍留下一段空隙,其他进程可以在真正创建之前占用该名字,因此先检查再创建也不能消除竞争窗口。 位置设置不等于隔离 Python 的 tempfile.mkstemp 安全地创建并打开文件,调用方负责关闭和删除。TemporaryDirectory 提供受限目录和正常退出时的清理。TMPDIR 影响位置选择,却不会自己创建私有目录,也不验证隔离策略。Python 会缓存默认目录选择;如果操作必须使用某个位置,应明确传入 dir=,不要依赖运行中修改环境变量来重新选择。 systemd 的 PrivateTmp= 可以给服务单独的临时文件命名空间,但它改变共享行为,并有平台与 unit 条件,不等同于设置一个环境变量。服务分别使用独立命名空间时,不能依赖跨服务的 /tmp 路径通信。下方本地实验不会启用或验证 systemd 隔离,也不能据此宣称部署已经具有相同隔离边界。 tmpfs 随使用量分配空间,在可用且被允许时可以使用交换空间。没有指定大小时,默认容量上限是物理内存的一半;这是上限,不是挂载时预留的内存。数据分配额度和 inode 数量都可能分别阻止后续工作。/tmp 也不一定挂在 tmpfs 上,因此容量监控应依据实际挂载与负载,不能把目录名字直接当成存储类型。 ...

九月 26, 2027 · 2 分钟 · XB Field Notes
TERM 选择组 G:A 采用默认处理并退出,B 忽略 TERM 而存活。C 属于其他组,未被选中。图中结果来自文章受控样本。

进程组选择信号接收者,不等于进程家族树

进程组是内核维护的一组进程,通过进程组编号 PGID 标识,可用于向一个作业投递信号等操作。父子关系与组成员关系是两个维度:有亲缘关系的进程可以属于不同组,组长也不必始终存活,其他成员仍可继续属于原组。理解这些关系,才能判断一个控制动作到底覆盖哪些进程,以及某个进程退出意味着什么。 零号信号可以在不投递信号的情况下检查存在性与权限,但不是存活或完成确认。僵尸进程仍可能存在,检查之后目标状态也可能变化。因此,应保留本例通过等待回收确认完成的步骤,不能用一次成功的权限探测替代退出结果,更不能据此判断业务工作已经安全结束。 成员关系选择接收者,各进程的处理方式决定响应。组可以在组长退出后继续存在,发送成功不是完成证据。 坏掉的样子 向单个进程发信号,不会自动向它的所有子进程发信号。向进程组发信号,选择的是当前组成员,也不是整棵后代树。创建新会话或新组的子进程,可能已经位于目标范围之外;反过来,多个兄弟进程可以刻意共享一个组,即使任何一个都不是另一个的父进程。进程树图不能直接替代组成员清单。 组投递也不保证所有接收者作出相同响应。一个成员可能采用默认动作,另一个安装处理函数,还有成员可能忽略或阻塞允许处理的信号。因此,组操作不会自动形成原子停止,也不能证明组内全部工作完成。每个进程的信号处理方式和应用关停协议仍然决定最终结果,需要分别观察投递、响应与退出。 组长退出不等于整个组退出。只要还有成员,PGID 就可能继续有效,即使组长已经被父进程回收。只观察组长状态的监督程序,可能在成员仍运行时报告结束。组身份应与实际生命周期管理结合,不能被当作一批启动工作永久不变的标识,也不能单凭领导进程消失就推断所有资源都已释放。 快速验证 把脚本保存为 groups.py,在具有 Python 3.11 或更新版本的一次性 Linux 环境中运行 python3 groups.py。2026-10-15 的复核记录了该样本在 Linux 6.18.44/Python 3.12.14 上通过,此次未重跑。启动时 TERM 须采用默认处理方式,TERM 与 USR1 均须未被阻塞。只向已核对的新建子进程组发信号,绝不能使用继承或共享的进程组: ...

九月 24, 2027 · 1 分钟 · XB Field Notes
保存相对文本 data 的链接位于 first 时解析到 first/data,移入 second 后解析到 second/data;解析基准是链接所在目录。

符号链接保存路径,不永久绑定目标身份

符号链接是保存目标路径的文件系统对象。跟随链接时,系统按操作发生时的状态解析这段路径;链接并没有永久绑定目标 inode。这个区别解释了为什么符号链接可以跨文件系统、为什么会变成悬空链接,以及为什么保存的目标文本完全没变,最终访问对象却可能改变。理解链接对象与当前解析目标,是正确使用和诊断链接的基础。 符号链接保存的是路径。应说明解析基准、观察对象与遍历策略。 坏掉的样子 相对目标以符号链接所在目录为基准,而不是以调用进程的当前工作目录为基准。因此移动链接可能改变其含义;移动或重命名目标,则可能让链接悬空。部署过程如果调整目录结构,却没有核对这些解析基准,熟悉的链接就可能悄悄访问另一个文件。仅看 readlink 打印的文本没有变化,无法证明对象仍相同。 不同存在性检查回答不同问题。跟随链接的操作可能报告目标缺失,而检查目录项本身的操作仍能找到链接。同样,宽松路径解析函数可以在最终目标不存在时返回绝对字符串。规范化后的路径不是可用文件当前存在的证明,更不是文件身份的永久凭据;诊断必须说明自己检查了链接对象,还是跟随之后的目标。 当其他参与者能够修改相关目录项时,先检查路径再打开,会留下检查与使用之间的时间窗口。给路径加引号可以保护 shell 参数解析,却不能冻结文件系统解析。普通 open 的 O_NOFOLLOW 拒绝最后一个组件是符号链接,不会自动禁止前面目录组件中的链接。把参数安全、路径安全和对象稳定性混为一谈,会造成错误边界。 快速验证 把脚本保存为 symlinks.py,在一次性 Linux 环境中运行 python3 symlinks.py。实测使用 Linux 6.12.76 和 Python 3.11.2。脚本创建无害临时文件,并主动改变自己的链接,因此结果是确定性的,不需要并发攻击者,也不访问无关路径: ...

八月 3, 2027 · 2 分钟 · XB Field Notes
文章历史示例中,父 shell 的软硬值保持 32/64。第一个子进程只把自身改成 48/64;第二个子进程仍继承 32/64,修改不反向传播。

在真正启动工作负载的进程中设置文件上限

shell 内建的 ulimit 改变的是该 shell 及其随后启动进程的资源限制。对于文件描述符,RLIMIT_NOFILE 包含当前生效的软值与硬性上界。当终端显示很宽松的数字,独立受管服务却仍然无法打开文件或接受连接时,关键往往在于这些值从哪里继承。只知道某个命令可以调大数字,并不足以证明改到了真正执行工作负载的进程。 限制从实际启动者传给新子进程,子进程局部修改不会反向传播。Bash 赋值若不选 -S 或 -H,可能同时降低软硬值。 坏掉的样子 软值通常约束新的描述符分配。普通进程可以在现有硬上限内提高软值,也可以降低硬值;提高硬值则需要相应权限,而且仍然受内核约束。因此,启动时继承的硬值太低,可能使应用无法设置自己打算使用的软值。不能只检查应用配置写了多少,还要核对它是否真正有能力应用那个数字,以及失败有没有被正确处理。 ...

七月 19, 2027 · 2 分钟 · XB Field Notes