u、g、m 声明指向创建缺少的服务用户,并建立其主组与附加组关系。

systemd-sysusers 创建缺少的服务账号

服务包经常需要先准备专用账号,才能拥有文件或避免以 root 身份运行。systemd-sysusers 用一份小配置声明账号及其组,核心操作是创建缺少的身份,并建立要求的成员关系。它不是通用账号迁移引擎,不负责创建所有目录,也不保证自动分配的数字标识在每台机器上都相同。把这些职责分清楚,才能判断声明已经执行究竟证明了什么。 sysusers 创建缺少的身份与要求的成员关系;不会迁移既有账号字段、保证跨主机自动 UID 一致,或创建家目录。图解基于原文;本轮没有更改账号数据库。 坏掉的样子 u 条目创建缺少的系统用户,通常也创建同名主组;g 创建组,m 添加附加组关系。ID 写成 - 表示自动分配,r 条目向分配池添加区间,多条区间合并使用;已有账号占用了某个数字,就会改变剩余可用范围。因此相同的分配池并不能建立跨机器身份一致性,尤其不能因为两边使用同一份配置,就假定共享存储里的数字所有者一定对应同一人。 更改既有账号的声明,不会自动为它重新编号,也不会自动更新家目录和描述字段。这种行为便于重复执行包初始化,但同时意味着配置差异不等于活动账号记录已经发生变化。真正迁移 UID,需要另外处理既有文件归属、正在运行的进程、共享存储和回退策略;不能只改一行声明,再把命令返回成功当作迁移完成。 家目录列设置的是账号数据库字段,不会创建实际目录。因此,一个服务可能已经拥有有效账号,却仍因为状态路径不存在或不可写而启动失败。需要目录时,应使用合适的受管理服务目录或 tmpfiles 规则,明确创建、所有权和权限。账号存在与目录可用,是两项独立条件,应沿真实服务启动路径分别验证。 快速验证 在装有 systemd-sysusers 的一次性 Linux 环境中,以管理员身份运行下面的 Python 脚本。它不修改机器正在使用的账号数据库,恢复文本记载的环境为 Debian 12、systemd 252.39,但原始运行日志不可用。本轮没有执行此样例;以下结果说明其预期断言,不能视为本轮实测。每组测试都通过 --root 指向单独的临时根目录;shadow 数据库只在内部比较内容,不会把值打印出来: from pathlib import Path from tempfile import TemporaryDirectory import subprocess definition='''r - 61000-61010 u field-worker - "Field worker" /var/lib/field-worker /usr/sbin/nologin g field-readers - m field-worker field-readers ''' def prepare(root,occupied=None): (root/'etc').mkdir(parents=True) passwd='root:x:0:0:root:/root:/bin/sh\n';group='root:x:0:\n' if occupied is not None: passwd+=f'occupied:x:{occupied}:{occupied}:Fixture:/nonexistent:/usr/sbin/nologin\n' group+=f'occupied:x:{occupied}:\n' (root/'etc/passwd').write_text(passwd);(root/'etc/group').write_text(group) directory=root/'usr/lib/sysusers.d';directory.mkdir(parents=True) (directory/'field.conf').write_text(definition) def apply(root): result=subprocess.run(['systemd-sysusers','--root='+str(root)],capture_output=True,text=True,timeout=10) assert result.returncode==0,result.stderr def entries(root,name): return {line.split(':')[0]:line.split(':') for line in (root/'etc'/name).read_text().splitlines()} with TemporaryDirectory(prefix='xbcatm-sysusers-') as directory: first=Path(directory)/'first';second=Path(directory)/'second';prepare(first);apply(first) worker=entries(first,'passwd')['field-worker'];groups=entries(first,'group') assert worker[3]==groups['field-worker'][2] assert 'field-worker' in groups['field-readers'][3].split(',') assert worker[5:]==['/var/lib/field-worker','/usr/sbin/nologin'] assert not (first/'var/lib/field-worker').exists() before={name:(first/'etc'/name).read_bytes() for name in ('passwd','group','shadow','gshadow')} apply(first) assert before=={name:(first/'etc'/name).read_bytes() for name in before} print('user, primary group, and supplementary membership created; repeat leaves databases unchanged') print('home and nologin shell recorded; home directory was not created') prepare(second,int(worker[2]));apply(second) other=entries(second,'passwd')['field-worker'] assert other[2]!=worker[2] print('same allocation range and definition choose different UIDs when existing accounts differ') changed=definition.replace('u field-worker - "Field worker" /var/lib/field-worker', 'u field-worker 62000 "Changed description" /changed-home') (first/'usr/lib/sysusers.d/field.conf').write_text(changed);apply(first) assert entries(first,'passwd')['field-worker']==worker print('changing a declaration does not renumber or rewrite an existing account') assert not (first/'changed-home').exists() print('only temporary alternate-root account databases were changed') 第一份根目录起初只有样本 root 账号。应用声明后,会生成工作用户、它的主组、读者组,以及工作用户的附加组成员关系。脚本检查数字主组关系和实际成员列表,而不是只检查配置能够解析。这样可以明确证明创建结果满足所声明的访问结构,避免把一个存在的组误认为用户已经加入其中。 ...

六月 8, 2027 · 2 分钟 · XB Field Notes
通过引用的 $@ 转发时,四个精确值保留四个独立边界。

$@ 等特殊变量给出函数的完整入参清单

包装脚本应当把收到的参数按原有边界继续传递。在 Bash 的命令参数位置,"$@" 会让每个位置参数分别成为一个带引号的词,因此能保留空值、空格、换行和字面通配符。只用两个简单文件名测试成功,并不能证明包装脚本正确;一旦它把参数列表拼成字符串再拆开,真实调用者传入的边界就可能丢失。转发是否可靠,应当用能暴露这种差异的输入来验证。 应转发列表本身。引号保留参数边界,但不决定下游选项解析,也不会让其他 shell 状态自动局部化。 参数列表与拼接文本不同 位置参数属于当前脚本或正在执行的函数。$# 返回参数数量,内容为空字符串的参数也计算在内。"$1" 读取第一个参数,"${10}" 读取第十个;后者需要花括号,以区别于第一个参数后面接字符零的写法。$0 表示启动的 shell 或脚本名称,不会作为额外元素出现在 "$@" 中,不能把它误算进业务参数列表。 在普通命令边界上,"$@" 为每个参数保留一个独立词;完全没有位置参数时,它不产生任何参数。相比之下,"$*" 始终形成一个字符串,用 IFS 的第一个字符连接各个值。IFS 未设置时使用空格,设为空字符串时则不插入分隔符。它并不会删除参数内部原有的空格,但各参数原来的边界通常已经无法恢复,因此不适合转发任意列表。 省略引号的 $@ 不是等价简写,展开后的内容仍可能被分词和文件名展开处理。调用者传入的字面星号,到了包装脚本所在目录可能突然匹配出一组文件。往拼接文本里补引号字符也不能恢复结构。需要把列表保存到之后使用时,在 Bash 中写成 args=("$@"),并通过 "${args[@]}" 转发,即使随后调整位置参数,保存的列表仍保留各个元素。 坏掉的样子 函数执行期间拥有自己的位置参数列表。调用时传入的参数暂时替换当前可见列表,返回时恢复调用者的列表。函数内部使用 set 或 shift 修改的是这次函数调用的参数,但这不代表其他所有 shell 状态都自动局部化。例如,没有局部声明就修改 IFS,可能影响调用者之后的命令。参数作用域和普通变量作用域应分别理解,不能互相推导。 ...

四月 16, 2027 · 2 分钟 · XB Field Notes
当前状态轨道先出现失败工作的 7,随后成功日志产生 0。立即保存的分支进入独立的 saved=7 轨道,并一直保留到显式 return 或 exit。

退出码是用一个数字汇报成败

退出状态是 shell 用来决定下一步执行什么的小整数。对 shell 控制流来说,零表示成功,非零进入失败分支,但这并不意味着每个数字都有统一的业务含义。搜索没有匹配、输入格式错误和服务器不可用,都可能返回非零,却需要不同处理。决定重试、忽略或终止之前,应先阅读目标命令的契约。不能只因为状态不为零,就把所有结果归为同一种事故。 趁状态仍对应目标命令时立即保存。日志、控制流变换和后台完成检查之后,仍需保留并传播正确结果。 让状态始终对应原来的命令 Bash 通过 $? 提供最近前台命令的状态。需要保留时,必须在记录日志、执行另一个条件判断或清理操作之前保存;这些操作也有自己的状态,会覆盖原本想观察的证据。函数通常使用最后执行的命令状态作为返回值,因此,末尾一句成功的日志输出就可能把前面的失败包装成整个函数成功,调用者看到的结果与实际工作脱节。 ...

三月 27, 2027 · 1 分钟 · XB Field Notes
在同一个共享 Shell 中,函数取得自己的位置参数表并移位;返回后调用者原有列表恢复。共享变量与这些临时参数帧并存。

Shell 函数像脚本一样读取自己的参数

Shell 函数把一组命令组织到同一个名称之下,并接收本次调用自己的位置参数。普通调用时,函数体在当前 Shell 中执行,不会自动启动独立进程。这方便复用操作,也意味着赋值、切换目录及其他 Shell 状态变化可能影响调用方。函数接口除了规定输入,还应明确输出、退出状态和有意产生的副作用,不能只因为把几行命令包进了函数,就认为它们已经与外层隔离。 普通函数调用共享 Shell。返回时恢复位置参数帧,但嵌套辅助函数仍能修改动态局部绑定;数据和失败状态需要分别约定。此图表示原文实验预期,本轮未重新执行。 每次调用都有自己的参数列表 调用 demo_func first second 后,这次函数内部的前两个位置分别是传入的两个值,参数数量为二。再次调用其他函数,会暂时安装下一层自己的参数列表;返回后,调用者的列表重新可见。因此,在被调用函数中移位,不会消耗调用者的位置参数。Bash 的 $0 也不会因此变成当前函数名称,不能用它判断自己正在哪个函数里面。 使用 "$@" 转发整份列表,才能保留空参数、空格以及通配符字符各自的边界。应先检查数量,再读取必需的位置,同时区分省略参数与明确传入空字符串。Shell 函数没有自动执行这些规则的参数声明,调用时语法上能写出一串值,并不意味着这些值已经满足函数的业务约定。 局部变量使用动态作用域 Bash 的局部声明会创建一个变量,遮住外层的同名绑定;函数返回之后,原来的绑定再次可见。若修改调用者变量并不是接口的一部分,应为临时值、循环计数器等声明局部变量。面向多个解释器的可移植脚本,不能假设每个 Shell 都实现了 Bash 的这套局部变量设施,解释器要求必须与使用的语法保持一致。 ...

三月 16, 2027 · 1 分钟 · XB Field Notes
一侧对比折返与顺序磁头路径,另一侧展示客户端 A 与 B 各自的请求行;图形说明局部性与竞争需求,不保证性能或公平性。

磁盘调度器给 I/O 排好队,让磁头移动更省

blk-mq 提供 CPU 或节点本地的软件暂存队列,以及硬件派发队列。请求可以在派发前经过调度和合并,但驱动与设备行为也会影响实际服务。软件提交顺序不保证完成顺序。调度器能够影响竞争请求进入设备的方式,却不控制整个存储栈后续的每一项决定。分析时应分别记录提交、派发与完成,避免从一条队列箭头推出设备执行的完整时间线。 在旋转介质上,改善请求局部性可以减少磁头移动。闪存没有机械寻道成本,但合并、并行度、排队和仲裁仍会影响性能。高效排序与公平服务回答不同问题,都不能给出普遍适用的调度器排名。应先确认当前内核与设备实际提供哪些选项,再测量相关负载,不要因为设备名称包含 NVMe 就把所有队列政策的成本与收益都当成零。 磁头才是预算 7200 RPM 下半圈约为 4.17 ms,可作为理想化的平均旋转等待。传输与寻道成本还依赖设备和负载。原文给出 2000 个请求、67907411 与 99977 条磁道,以及 91 与 231 IOPS,但没有提供生成夹具或保留测量。这些数字只能作为未经核验的原文声称,不能写成已经测得的 Linux 调度器结果,更不能用倍数直接推算生产设备的容量。 对一组虚构磁道位置排序,可以说明磁头行程有可能缩短,但没有经过验证的设备模型或测量,就不能建立 IOPS 收益。此前的寻道百分比和约 40 ms 的 NVMe 总耗时,也有同样的证据缺口。图中更短的路径因此只表示定性关系,不是延迟预测。实际比较应保留请求序列、模型假设、设备信息与原始输出,才能判断差异是否来自调度。 每种调度器是干什么的 none 不增加调度器重排,但不承诺全局先到先完成。mq-deadline 结合扇区顺序和批次之间的到期检查,fifo_batch 限制每批数量;这些政策影响派发,并不保证端到端完成期限。kyber 通过限流尝试达到读取与同步写入的目标延迟,目标值不是每个请求都必然达标的证明。设备处理时间与后续排队仍然存在,不能把参数名当成业务延迟服务承诺。 bfq 提供按权重的服务分配和低延迟启发式,带宽或时间分配取决于配置与条件。空转可以帮助服务保证,却也可能降低内部有队列设备的吞吐,必须核对内核支持和实际设置。/sys/block//queue/scheduler 中带方括号的名称表示当前选项;修改它是运维动作,需要已授权的设备和可撤销测试方案。不能只看到一个名称,就推定持久设置、隔离效果或吞吐收益已经建立。 公平与排序是两个问题 90/10 的请求发出比例,不能直接建立服务份额或平均等待位置。原文的 0.099、1901 与 0.5 没有保留夹具作为依据。在持续有积压的简化模型里,相等权重可以说明分享概念,但真实结果还取决于需求、请求大小、优先级和调度设置。应分别比较收到的服务、应用延迟与请求到达数量,不能把不同分母下的比例当成同一个指标,更不能保证轻客户端永远取得一半吞吐。 ...

三月 9, 2027 · 4 分钟 · XB Field Notes
标准输出带 first、内部换行、second 及两个末尾换行;捕获结果为 first、换行、second。内部换行保留,两个末尾换行都被删除。

命令替换把一条命令的输出嵌进另一行

命令替换会收集一个命令的标准输出,再把得到的文本放入外层表达式。常见的 $(command) 适合获取时间戳、计算出的标签或其他较小的文本值,但它并不是任意字节的透明传输通道,也不是可靠的文件名列表容器。使用前需要理解三个边界:末尾换行如何处理、内部命令在哪个执行环境运行,以及失败状态如何传递。只看到变量里有内容,不能证明整个操作成功。 捕获文本与退出状态走不同路径。引号保留结果参数,不能恢复已删除的换行。标准 $(...) 隔离 Shell 状态,却不会撤销外部副作用。此图表示原文实验预期,本轮未重新执行。 先收集文本,再决定怎样使用 本文讨论的标准命令替换形式会让 Bash 在子 shell 环境中运行内部命令,收集标准输出,并删除末尾所有换行符。位于文本中间的换行仍然保留。双引号能阻止后续分词和文件名展开,却不能恢复已经删除的尾部换行。如果需要精确保留最后几个字节,应该使用文件或数据流,而不是依靠 shell 变量原样往返。 赋值语句 result=$(producer) 会把处理后的文本存进变量,不在此处拆成多个参数。后续使用时仍应写成 "$result",保留它的参数边界。类似地,consumer "$(producer)" 会传递一个参数,即使结果为空。去掉引号可能得到多个参数,也可能完全没有参数;输出中的星号还可能匹配当前目录的文件,让同样的生产者输出在不同目录产生不同调用结果。 标准错误和标准输出彼此独立,除非主动重定向,诊断信息仍可能显示在终端,而变量只收集结果正文。将两路输出合并后,警告文本可能污染本应供程序读取的值。因此,应先确定生产者的输出契约,再决定是否合并;直接吞掉所有诊断信息也可能让空结果失去可解释的原因。输出内容、诊断信息和退出状态应分别检查。 不要把这里的作用域结论推广到较新 Bash 的每一种捕获语法。本文使用的美元符号加圆括号形式具有实验验证的子 Shell 行为;较新的当前环境捕获形式会保留不同的副作用。说明一个捕获值时,应明确语法与解释器版本,而不是把所有能够取得文本的写法都视为等价。 ...

三月 3, 2027 · 1 分钟 · XB Field Notes
first 与 dup 共享一份打开文件描述;独立 open 使用另一份打开描述。两份描述指向同一个 inode。

文件描述符标识进程持有的打开引用

文件描述符是一个进程内部用于引用已打开资源的小整数。进程的描述符表把这个数字映射到内核维护的打开文件描述;对于普通文件,打开描述包含当前偏移和文件状态标志。另一个进程可以把相同整数用于完全不同的资源,因此数字本身不是全局文件身份。理解描述符表、打开描述与底层文件之间的关系,才能正确判断复制、关闭和继承的效果。 描述符编号只标识进程内的表项。dup 共享打开描述状态,独立 open 拥有独立状态;close 与 unlink 结束的是不同引用。 坏掉的样子 dup 分配另一个描述符,让它引用同一份打开文件描述;它不是简单复制整数,也不是重新按路径打开文件。因此,两份描述符读取普通文件时共享偏移。相反,再次打开相同路径,通常会得到具有独立偏移的新打开描述。设备与 inode 身份相同,不代表打开状态也相同,排查读取位置异常时必须区分这两个层面。 ...

二月 21, 2027 · 1 分钟 · XB Field Notes
已修改的环境文件分出两条路线:停止栏挡住通往现有进程的路线,箭头则把取值送入新进程。

EnvironmentFile 在 systemd 执行进程时供应取值

环境文件把部署取值与描述进程监管的单元定义分开。systemd 在执行服务命令前读取指定文件,再把取值放入那个进程的环境。修改文件不会直接重写已经运行的进程环境。不过,重载辅助命令本身也是一个新进程,因此可能读到比现有主进程更新的文件值。理解这个执行时点,比笼统记住“改配置后做一次 reload”更可靠。 EnvironmentFile 在服务命令执行时读取。second、third、fourth 是原文样例值,不是本轮观测。文件由 systemd 按自身规则解析,并非执行 shell。本轮保留历史运行验证受阻状态。 坏掉的样子 daemon-reload 要求管理器重新读取单元定义、更新依赖信息;reload 则请求服务执行已配置的重载操作,例如 ExecReload。两者都不是把现有进程环境整体替换掉的通用指令。服务可以自行实现应用配置刷新,但这种能力必须来自应用或重载辅助程序,不能仅凭管理器接受了一个重载请求就认定业务设置已经切换。 EnvironmentFile 路径前加短横线,表示这个文件可以不存在。缺失时 systemd 会忽略它,并不会发出操作者可能期待的那条警告。因此,路径笔误可能让一个覆盖配置悄悄消失。只有缺少该文件确实具有合法含义时,才适合使用可选形式;必需的部署输入不应借这个前缀被静默跳过,否则很容易把供应错误变成难以解释的默认行为。 文件由 systemd 解析,不是交给 shell 加载。它有自己的引号和转义规则,但不会把命令替换和变量展开当作 shell 执行。包含 $HOME 的值仍然是这段文本。后列出的文件覆盖前面的同名值,文件赋值又覆盖单元中的 Environment 赋值。因此,需要检查真实优先级,不能认为自己首先看见的那一行就是最终值。 快速验证 在 PID 1 为 systemd 的隔离 Linux 环境中,以管理员身份运行下面的 Python 脚本。它只使用无害的样本设置与临时路径,恢复文本记载的环境为 Debian 12、systemd 252.39,但原始运行日志不可用。本轮没有执行此样例;以下结果说明其预期断言,不能视为本轮实测。确认运行时单元名称尚未使用。工作程序仅记录四个特定测试值,不会把整个环境全部打印出来: ...

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