只要 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
fork 从父进程 P 分出新子进程 C,P 继续运行。成功 exec 在 C 内替换程序映像,并保留同一子进程 PID。

fork 创建进程,exec 替换其中的程序

fork 后接 exec,是 Unix 常见的进程启动模型之一。fork 创建子进程,成功的 execve 则替换调用进程中的程序映像。它们解决不同问题,也具有不同继承规则。在一个程序为另一个命令安排输入、输出、环境和生命周期管理时,必须分清这两个动作,否则很容易把创建新身份与替换运行内容混为一谈。 fork 创建子进程,成功 exec 替换其中的程序。私有内存、共享资源、继承描述符和显式环境分别遵循不同边界;需要验证启动行为,也要观察子进程实际完成。 坏掉的样子 fork 不会让之后所有状态都相互独立。普通私有内存通常通过写时复制逐渐分离,但共享映射仍然共享。继承描述符也继续引用父进程对应的同一份打开文件描述,因此任一侧读取普通文件,都可能改变另一侧观察到的偏移。复制描述符表,不等于为每个文件重新独立执行 open;进程不同,也不能证明资源状态完全不同。 exec 同样不会简单保留全部启动设置。带 close-on-exec 的描述符在成功执行时关闭,其他符合条件的描述符可以继续存在。execve 接收显式环境数组,不是自动复制调用者当前环境。被捕获信号的处理方式会重置,旧内存映射也会消失,即使进程身份和若干其他属性仍然保留。需要逐项理解哪些状态跨越了替换边界。 ...

十月 12, 2027 · 2 分钟 · XB Field Notes
请求模式按普通 umask 过滤或默认 ACL 继承分支;既有 inode 使用独立路径。

umask 过滤新建权限,默认 ACL 会改变规则

进程的 umask 在普通模式位创建规则下,为新文件和目录移除权限位。应用先提供请求模式,掩码再限制这份请求。它既不是完整访问策略,也不会重写所有既有文件。如果父目录具有默认访问控制列表,创建计算方式还会改变,因此只检查进程掩码,可能无法准确预测最后得到的权限。排查时需要同时看到应用请求和目录环境。 坏掉的样子 没有默认 ACL 时,普通权限结果是请求模式与 umask 按位取反后的值进行按位与。例如请求 0666、掩码 0022,得到 0644。这个计算不是算术减法:相同掩码下请求 0600,仍然得到 0600。应用没有请求执行权限时,宽松掩码也不会主动添加执行位;掩码负责移除,而不是凭空授予应用未请求的普通权限。 限制严格的 umask 不会修复已经存在的全员可写文件。打开并截断该文件,会改变内容,却保留既有模式。后续 chmod 也可以不受当前掩码限制地扩大权限。因此,把 umask 当作权限永远不能超过的上界,会遗漏既有文件与之后权限修改两类情况。必须区分文件首次创建,以及此后围绕同一 inode 发生的操作。 父目录具有默认 ACL 时,Linux 从它派生新对象的访问 ACL,再按应用请求模式限制权限,不再按普通方式应用进程 umask。因此,即使创建者掩码是 0077,共享目录里仍可能生成组可访问文件。遇到与预期不同的模式时,应检查继承的 ACL,而不能只看到严格掩码就断言内核没有遵守权限设置。 普通掩码与默认 ACL 继承是不同创建分支。已有文件截断与后续 chmod 另行处理;案例属于实验预期。 快速验证 把脚本保存为 mask.py,在安装 Python 3 和 setfacl 的 Linux 环境中运行 python3 mask.py。应使用获准且没有既有默认 ACL 的临时目录基址,所在文件系统须支持默认 ACL,并启用断言。历史文章报告 Python 3.11.2、acl 2.3.1,以及先在 tmpfs 被拒绝、再在 ext4 执行的过程;但未找到原始日志,本轮没有更改存储或重跑。该次拒绝不代表所有 tmpfs 配置都不支持 ACL: ...

十月 9, 2027 · 2 分钟 · XB Field Notes
生产者把标准输出通过有限容量的字节管道送到并发消费者的标准输入;标准错误另行流向终端。程序自身的缓冲仍可能延迟字节进入管道。

管道让进程彼此流式连接

shell 管道把一个命令的标准输出连接到下一个命令的标准输入。各段命令可以并发运行,生产者不必全部完成后消费者才开始处理。这种连接支持增量处理,却不保证任一程序真的采用增量算法。用户态缓冲、提前结束以及各阶段的退出状态,都会影响整项操作是否达到预期。理解管道时,需要同时观察数据流、文件描述符生命周期和进程结果,不能只看最后一行输出。 管道在进程间传输字节。缓冲、所有打开端点的生命周期、每个阶段的退出结果,需要分别理解。 有限容量的字节流 管道传输的是字节,不是自带边界的记录。读者不能假设每次读取恰好得到一行,也不能假设一次读取对应生产者的一次写入。需要消息边界的程序必须自己定义和解析。标准错误默认独立于管道连接,因此,上游诊断可能显示在终端,却不会进入下游过滤器。是否合并诊断和数据应当明确决定,避免错误文本污染本来供机器读取的内容。 内核缓冲区容量有限。对于阻塞操作,管道满时写者等待;管道空但仍有写端打开时,读者等待。这种反压限制生产者在这个管道上领先消费者的距离。容量取决于操作系统和配置,并不是普遍固定为六十四千字节。程序自身仍可能维护不断增长的数据结构,因此,使用管道不等于整个处理链的内存已经有界,必须另外检查各段算法。 用户态缓冲还可能引入额外等待。某个程序向终端逐行刷新,改成管道输出后却可能先积累较大的块。此时消费者看起来没有动静,并不代表内核丢了数据;数据可能尚未离开生产者的用户态缓冲。应先检查程序提供的流式处理和刷新控制,再考虑调整内核限制。完整排序等转换本身也需要先看到更多输入,添加随意的睡眠无法改变这种依赖。 原子写入与缓冲容量是不同属性。Linux 对不超过 PIPE_BUF 的适当写入提供不与其他写者交错的保证,并有阻塞模式条件;这仍然不会为读者建立消息边界。几次短写入可能被一次读取取得,一次写入也可能分几次才读完,不能把某次观察到的块大小当作解析协议。 坏掉的样子 只有所有指向写端的文件描述符都关闭,并且缓冲中的剩余字节已经读完,读者才会看到文件结束。关闭写端不会丢弃已经进入管道的数据。如果某个子进程继承了不再使用的写端,即使看起来生产者已经退出,消费者也可能一直等不到结束。反方向上,所有读端关闭后继续写入会产生 SIGPIPE;忽略这个信号时,写操作会以 EPIPE 错误失败。因此,消费者主动提前结束,也可能让原本正常的生产者以非零状态退出。 Bash 默认返回最后一段命令的状态。上游失败、下游 cat 成功读完部分数据时,整体可能仍然成功。开启 pipefail 后,整体返回从左到右最靠右的非零状态,并不是时间上最先失败的进程,也不是最左侧失败命令。PIPESTATUS 数组短暂保留每段结果,应立即保存,因为后面一次赋值或打印就可能替换这份观察记录。 Bash 管道中的阶段通常运行在子 shell 环境。在管道右侧循环里给变量赋值,通常不会修改调用者的变量。lastpipe 选项在关闭作业控制时可以改变最后一段的执行位置,因此依赖状态保留的代码必须明确 shell 配置。需要保留循环状态时,也可以让循环直接通过重定向读取输入,避免把它放进管道带来的作用域变化。 ...

九月 28, 2027 · 1 分钟 · XB Field Notes
父进程导出的 local 产生两份子进程环境副本。子进程 A 将自己的 local 改为 child;子进程 B 与父进程仍保留 local。箭头只指向子进程,没有修改回传箭头。

环境变量属于各自的进程

环境是程序启动时收到的一组名称与字符串值。子进程通常接收父进程选定的环境,修改自己的副本不会回写父进程或兄弟进程。这是一条有用的配置边界,却不是命令没有副作用的保证:程序仍可能根据配置修改共享文件、数据库或其他资源。理解变量如何传递,与判断程序实际会做什么,是两个需要分别核对的问题。 沿直接启动者传入的值检查。环境副本不会回写;但子 Shell、新执行的 Bash 程序与当前 Shell 中的点命令,保留和修改的状态并不相同。 导出决定另一个程序能收到什么 Shell 变量与环境条目有关联,但并不是同一概念。DEMO_SETTING=local 创建或更新 Shell 参数,export DEMO_SETTING 才把它标记为启动外部命令时应包含的环境项。父 Shell 能打印变量,不代表新启动的 Bash 就能看到未导出的值。把赋值写进某个配置文件,也不会让每个 Shell 自动读取它;还必须确认文件是否在对应启动路径上被加载。 Bash 从环境继承的变量已经带有导出属性,之后重新赋值会保留该属性。因此子进程修改值后,再启动孙进程时可以直接传递新值,不要求每一代重复写一次导出命令。unset 删除变量,Bash 的 export -n 则取消导出属性但保留当前 Shell 中的值。真正需要确认的是直接启动者传入了什么,而不是机械地搜索祖先脚本里出现了多少次导出语句。 ...

九月 25, 2027 · 1 分钟 · 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
文章历史示例中,父 shell 的软硬值保持 32/64。第一个子进程只把自身改成 48/64;第二个子进程仍继承 32/64,修改不反向传播。

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

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

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

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

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

二月 21, 2027 · 1 分钟 · XB Field Notes