返回介绍

systemd.exec 中文手册

发布于 2020-11-08 09:27:19 字数 91576 浏览 1145 评论 0 收藏 0

名称

systemd.exec — 执行环境配置

大纲

service.service,socket.socket,mount.mount,swap.swap

描述

本手册列出了 service, socket, mount, swap 单元所共有的、用于定义进程执行环境的配置选项(亦称"配置指令"或"单元属性")。

通用于所有单元类型的配置选项位于systemd.unit(5) 手册。上述四种单元各自专用的配置选项分别位于systemd.service(5),systemd.socket(5),systemd.swap(5),systemd.mount(5) 手册。根据单元类型的不同,这些共有的选项(亦称"指令"或"属性")分别位于单元文件的 [Service], [Socket], [Mount], [Swap] 小节。

此外,通过 cgroup 控制资源占用的选项位于systemd.resource-control(5).手册中。它们是对本文所列选项的补充。

隐含依赖

某些选项会导致自动添加额外的依赖关系:

  • 设置了 WorkingDirectory=, RootDirectory=,RootImage=, RuntimeDirectory=, StateDirectory=,CacheDirectory=, LogsDirectory=,ConfigurationDirectory= 之一的单元,将自动获得对访问指定路径所需的所有 mount 单元的Requires=After= 依赖。这等同于将这些 mount 单元明确的在RequiresMountsFor= 中列出来。

  • 类似的,设置了 PrivateTmp=yes 的单元,将自动获得对访问 /tmp/var/tmp 所需的所有 mount 单元的 Requires=After= 依赖。此外,还将自动获得对systemd-tmpfiles-setup.serviceAfter= 依赖。

  • 如果单元的标准输出(StandardOutput=)或标准错误(StandardError=)中含有 journal,syslog, kmsg 之一,那么该单元将会自动获得 After=systemd-journald.socket 依赖。

路径

这些选项可以用来改变服务单元所看到的文件系统视图。注意,必须使用绝对路径,且不能在路径中包含 ".."

WorkingDirectory=

设置进程的工作目录。既可以设为特殊值 "~" 表示 User= 用户的家目录,也可以设为一个以 RootDirectory= 为基准的绝对路径。例如当 RootDirectory=/sysroot 并且 WorkingDirectory=/work/dir 时,实际的工作目录将是 /sysroot/work/dir 。当 systemd 作为系统实例运行时,此选项的默认值是 / ; 当 systemd 作为用户实例运行时,此选项的默认值是对应用户的家目录。如果给目录加上 "-" 前缀,那么表示即使此目录不存在,也不算致命错误。如果未设置 RootDirectory=/RootImage= 选项,那么为 WorkingDirectory= 设置的绝对路径 将以主机(或容器)的根目录(也就是运行 systemd 的系统根目录)为基准。注意,设置此选项将会导致自动添加 额外的依赖关系(见上文)。

RootDirectory=

此选项仅可用于系统单元(不适用于用户单元)。设置以 chroot(2) 方式执行进程时的根目录。必须设为一个以主机(或容器)的根目录(也就是运行 systemd 的系统根目录)为基准的绝对路径。如果设置了此选项,必须确保进程及其辅助文件在 chroot() 监狱中确实可用。注意,设置此选项将会导致自动添加额外的依赖关系(见上文)。

MountAPIVFS= 以及 PrivateUsers=RootDirectory= 一起使用时特别有意义。详见下文。

RootImage=

可设为一个块设备节点或者一个普通文件。此设置的含义与 RootDirectory= 相同,不同之处在于是从块设备或回环文件挂载一个文件系统(而不是直接使用一个现成的目录)。需要注意的是,块设备或回环文件中必须包含合法的文件系统,同时还需要满足以下条件之一: (1)不包含任何分区表;(2)仅包含单独一个 Linux 能够识别的 MBR/MS-DOS 或 GPT 分区; (3)包含一组完全遵守 Discoverable Partitions Specification 规范的 GPT 分区。

如果 DevicePolicy= 的值为 "closed", "strict" 之一、 或者 DevicePolicy=autoDeviceAllow= 非空,那么使用此选项将导致 /dev/loop-control 被赋予 rw 模式、 DeviceAllow= 中的 "block-loop" 与 "block-blkext" 被赋予 rwm 模式。详见 systemd.resource-control(5) 以了解 DevicePolicy=DeviceAllow= 选项。此外,下面的 PrivateDevices= 选项也可能会改变 DevicePolicy= 的设置。此选项仅可用于系统单元(不适用于用户单元)。

MountAPIVFS=

接受一个布尔值。设为 yes 表示为该单元内的进程创建私有的挂载名字空间,并在其中挂载 /proc, /sys, /dev 虚拟文件系统(除非它们已经被挂载了)。此设置仅在与 RootDirectory=/RootImage= 一起使用时才有意义,因为宿主机上一般已经挂载了这三个虚拟文件系统; 同时,除非切换到了不同的根目录,否则私有的挂载名字空间将会完全按原样从宿主机复制一份,这其中当然也就包含了这三个挂载点。注意,当 PrivateDevices=no (默认值)时, /dev 将会以绑定(--bind)的方式从宿主机上挂载。要想让服务运行在一个私有的最小化 /dev/ 环境中,请务必联合 PrivateDevices=yes 一起使用。此选项仅可用于系统单元(不适用于用户单元)。

BindPaths=, BindReadOnlyPaths=

设置专属于该单元的绑定挂载点(bind)。通过"绑定挂载",可以将该单元原本不可见的挂载点,放入该单元的可见范围内。注意,通过此选项创建的绑定挂载点专属于该单元,对运行该单元的主机并不可见。这些选项接受一个空格分隔的绑定挂载点的定义列表。列表中的每一项定义都遵守 "[-]源路径[:目标路径[:挂载选项]]" 格式(中括号表示可选)。目标路径的默认值是源路径,挂载选项只能设为 "rbind"(递归) 或 "norbind"(不递归) 之一。如果省略了目标路径,那么必须同时一起省略挂载选项。前缀 "-" 表示若源路径不存在 则忽略此项(而不是导致错误)。

BindPaths= 用于创建可读写绑定挂载点(除非源挂载点本来就是只读的)。 BindReadOnlyPaths= 用于创建只读绑定挂载点。可以多次使用这些选项,以设置更多的绑定挂载点。注意,若将某个选项设为空,则表示撤消所有先前设置的绑定挂载点列表,包括可读写绑定挂载点以及只读绑定挂载点。也就是任意一个选项都会一次性撤销两个选项先前的全部设置。

这些选项一般和 RootDirectory=/RootImage= 一起使用,此时,源路径一般是主系统上的某个挂载点,而目标路径则是该单元根目录下的某个路径。此选项仅可用于系统单元(不适用于用户单元)。

凭证

User=, Group=

设置进程在执行时使用的用户与组。既可以设为一个数字形式的 UID/GID 也可以设为一个字符串形式的名称。对于系统服务(由 PID=1 的 systemd 系统实例管理)以及由 root 运行的用户服务(由 root 用户启动的 systemd --user 用户实例管理),User= 的默认值是 "root" ,同时亦可明确将 User= 设为其他用户。对于普通用户运行的用户服务,User= 的默认值就是该用户自身,并且禁止将 User= 切换为其他用户。如果没有明确设置 Group= 选项,则使用 User= 所属的默认组。此选项不影响 带有 "+" 前缀的命令。

注意,为了避免歧义以及确保在不同 Linux 系统之间的兼容性,用户与组的名称必须满足以下规则: (1)仅可包含 a-z, A-Z, 0-9, "_", "-" 字符; (2)首字母只能是 a-z, A-Z, "_" 之一(也就是禁止使用数字与 "-" 字符); (3)字符串长度必须介于 1~31 之间。

当与 DynamicUser=yes 一起使用时,指定的用户与组将在服务启动时动态分配,并在服务停止时自动释放,除非它们已经被静态的创建了(见下文)。当与 DynamicUser=no 一起使用时,指定的用户与组必须在服务启动之前就已经被静态的创建了,例如在系统启动或安装软件包时,使用 sysusers.d(5) 机制所创建的用户与组。

DynamicUser=

设置是否动态分配用户。默认值为 "no" 。设为 yes 表示在该单元启动时,为其动态分配一个 user/group 对,并在该单元停止时释放。动态分配的 user/group 不会被添加到 /etc/passwd/etc/group 文件中,而是由 glibc 的 NSS 插件 nss-systemd(8) 进行维护,以确保能够从系统的 user/group 数据库中查询到动态分配的 user/group 对。可以通过上文的 User=Group= 明确指定动态分配的 user/group 的名称。若未明确指定名称,则自动根据单元的名称生成: 如果单元名(不含类型后缀)恰好符合用户名规则,那么就直接使用单元名,否则将使用单元名的哈希值。如果 User=Group= 恰好指定了一个已经静态存在的名称,那么将直接使用已经静态存在的 user/group (而不是动态分配)。注意,如果指定的 User= 名称恰好与某个静态组同名,那么在 User= 中设置的用户名也必须是静态用户。同样,如果指定的 Group= 名称恰好与某个静态用户同名,那么在 Group= 中设置的组名也必须是静态组。因为动态分配的 UID/GID 范围在 61184-65519 之间,所以静态存在的 UID/GID 应该避免使用这个范围。在任意时间点上,每一个动态分配的 UID/GID 只能对应最多一个动态分配的 user/group 。因为动态分配的 UID/GID 在单元停止后会被回收,并且会被反复循环使用,所以使用动态分配用户的单元不应该在其停止后遗留下任何属于动态分配用户的文件或目录,否则其他单元有可能在未来取得相同的 UID/GID ,从而成为这些遗留文件或目录的拥有者。当 DynamicUser=yes 时,也同时隐含的设置了 RemoveIPC=yesPrivateTmp=yes ,从而确保将单元的 IPC 对象与临时文件的生存期 与单元自身的生存期、为该单元动态分配的 user/group 的生存期绑定在一起。因为除 /tmp/var/tmp 之外,通常不存在其他全局可写的目录,所以,这通常也确保了使用动态分配 user/group 的单元,不可能在单元停止之后还遗留任何文件或目录。进一步,DynamicUser=yes 还同时隐含了 ProtectSystem=strictProtectHome=read-only ,以禁止单元写入文件系统上的敏感路径。如果想要允许单元写入某些特定的路径,那么必须将这些路径使用 ReadWritePaths= 白名单明确列出。注意,必须小心使用这个白名单,以避免由于循环使用 UID/GID 带来的安全问题。可以使用下文的 RuntimeDirectory= 设置一个运行时的写入目录,该目录的拥有者将会被自动设为动态分配的 user/group ,并会在单元停止后被自动删除。可以使用下文的 StateDirectory=, CacheDirectory=, LogsDirectory= 来设置一组专门用途的可写目录,以避免由于循环使用 UID/GID 带来的安全问题。

SupplementaryGroups=

设置进程在执行时使用的附加组。值是一个空格分隔的组名或组ID列表。可以多次使用此选项,以添加更多的附加组。若设为空,则表示清空先前已设置的列表。注意,此选项并不覆盖系统现有的附加组,而只是在现有的附加组基础上进行扩展。此选项不影响 带有 "+" 前缀的命令。

PAMName=

设置建立PAM会话所使用的PAM服务名称。此选项仅在与 User= 连用时才有意义(否则将被忽略)。若设置,那么将以设置的名称为进程注册一个PAM会话。若未设置,那么将不会为进程打开任何PAM会话。详见 pam(8) 手册。

注意,对于每一个使用了此选项的单元,都会在其生存期内维护一个PAM会话处理进程(作为该单元的一部分),以确保在整个单元的生存期内,PAM机制始终可以正常工作。此PAM会话处理进程名为 "(sd-pam)" 并且始终作为该单元的主进程的直接子进程存在。

使用此选项非常可能(取决于PAM的配置)导致在该单元启动时,将该单元的主进程迁移到自己的会话 scope 单元中,从而使得同一个主进程被关联到两个单元: (1)启动该主进程的单元(也就是配置了 PAMName= 选项的单元); (2)与该单元对应的会话 scope 单元。例如,将此选项与 NotifyAccess=all 一起使用时,就会导致上述结果。在这种情况下,该主进程的所有子进程将会被仅关联到对应的会话 scope 单元,从而导致这些子进程无法通过通知消息反映原始服务单元的状态变化(因为这些通知消息仅属于会话 scope 单元,而非原始服务单元)。因此,不应该将 PAMName=NotifyAccess=all 一起使用。

能力(capability)

CapabilityBoundingSet=

设置进程的 capability 集合(bounding, effective, permitted, inheritable)中应该包含哪些 capabilities(7) 。选项值是一个空格分隔的 capability 名称(例如 CAP_SYS_ADMIN, CAP_DAC_OVERRIDE, CAP_SYS_PTRACE)列表 。列表中的 capabilities 将会被包含在 capability 集合中,而所有其他不在列表中的 capabilities 则会被剔除。如果列表以 "~" 符号开头,那么表示取反,也就是所有列表之外的 capabilities 将会被包含在 capability 集合中。若未设置此选项,则表示不修改进程的 capability 集合。若多次设置此选项,则表示将多个设置的 capability 集合合并: 一般情况下使用 OR 逻辑合并,但以"~"开头的行则用 AND 逻辑合并。若设为空,则表示清空所有已设置的 capability 集合。若设为一个单独的 "~" 字符,则表示清空先前的所有设置,并将 capability 集合重置为包含所有的 capabilities 。此选项不影响带有 "+" 前缀的命令。

例子:如果一个单元拥有如下设置:

CapabilityBoundingSet=CAP_A CAP_B
CapabilityBoundingSet=CAP_B CAP_C

那么表示 CAP_A, CAP_B, CAP_C 全部被设置。如果在第二行前面加上 "~" 前缀:

CapabilityBoundingSet=CAP_A CAP_B
CapabilityBoundingSet=~CAP_B CAP_C

那么表示仅有 CAP_A 被设置。

AmbientCapabilities=

设置进程的 ambient capability 集合中应该包含哪些 capabilities(7) 。选项值是一个空格分隔的 capability 名称列表,例如 CAP_SYS_ADMIN, CAP_DAC_OVERRIDE, CAP_SYS_PTRACE 。若多次设置此选项,则表示合并多个已设置的 ambient capability 集合(参见上文 CapabilityBoundingSet= 的例子)。如果列表以 "~" 符号开头,那么表示取反,也就是所有列表之外的 capabilities 将会被包含在 ambient capability 集合中。若设为空,则表示清空所有已设置的 ambient capability 集合。若设为一个单独的 "~" 字符,则表示清空先前的所有设置,并将 ambient capability 集合重置为包含所有的 capabilities 。注意,添加到 ambient capability 集合中的 capabilities 也会被添加到进程的 inherited capability 集合中。你可以使用 ambient capability 集合给以普通用户身份运行的进程赋予某些 capabilities 。注意,此时 keep-caps 将被自动添加到 SecureBits= 中,以确保此处设置的 capabilities 不受用户设置的影响。此选项不影响带有 "+" 前缀的命令。

安全

NoNewPrivileges=

接收一个布尔值。设为 yes 表示该服务的所有进程与子进程都不能通过 execve() 调用获得任何新权限(例如通过 setuid/setgid 位或者文件系统 capability)。该选项是最简单也是最有效的防止进程提升权限的方法。默认值为 no ,但是,当 SystemCallFilter=, SystemCallArchitectures=, RestrictAddressFamilies=, RestrictNamespaces=, PrivateDevices=, ProtectKernelTunables=, ProtectKernelModules=, MemoryDenyWriteExecute=, RestrictRealtime=, LockPersonality= 之一被开启的时候,此选项将会被自动强制设为 yes (不过 systemctl show 依然显示此处设置的值)。参见 No New Privileges Flag

SecureBits=

设置进程的安全位。值是一个空格分隔的列表。可用列表项如下: keep-caps, keep-caps-locked, no-setuid-fixup, no-setuid-fixup-locked, noroot, noroot-locked 。可以多次使用此选项,以合并(OR)多个安全位。若设为空,则表示将安全位重置为"0"。此选项不影响带有 "+" 前缀的命令。参见 capabilities(7) 手册。

强制访问控制

SELinuxContext=

设置进程的 SELinux 安全上下文。设置此选项会覆盖自动域名转换,不过,安全策略依然需要对转换进行授权。此选项仅在 SELinux 确实被开启的情况下才有意义。若加上 "-" 前缀则表示忽略一切错误。此选项不影响带有 "+" 前缀的命令。详见 setexeccon(3) 文档。

AppArmorProfile=

设置进程的 profile 名称,进程将在启动时切换到此 profile 。设置的 profile 必须已经加载到内核中,否则该单元将无法启动。此选项仅在 AppArmor 确实被开启的情况下才有意义。若加上 "-" 前缀则表示忽略一切错误。此选项不影响带有 "+" 前缀的命令。

SmackProcessLabel=

设置进程的 SMACK64 安全标签。进程将以设定的标签启动,SMACK 将会根据此标签决定是否允许该进程启动。若允许,进程将继续以此标签运行,除非该可执行文件有其自身的 SMACK64EXEC 标签(此时将会切换到其自身标签运行)。默认值是 systemd 的运行时标签。该选项仅在 SMACK 已启用的情况下才有意义。

若加上 "-" 前缀则表示忽略一切错误。设为空表示撤消先前的设置。此选项不影响带有 "+" 前缀的命令。

进程属性

LimitCPU=, LimitFSIZE=, LimitDATA=, LimitSTACK=, LimitCORE=, LimitRSS=, LimitNOFILE=, LimitAS=, LimitNPROC=, LimitMEMLOCK=, LimitLOCKS=, LimitSIGPENDING=, LimitMSGQUEUE=, LimitNICE=, LimitRTPRIO=, LimitRTTIME=

设置进程的各种软/硬资源限制。详见 setrlimit(2) 手册。这些指令的值有两种表示法,一个单独的 value 值表示将软硬两种限制设为同一个值。而冒号分隔的 soft:hard 值表示分别设置软限制与硬限制(例如 LimitAS=4G:16G)。特殊值 infinity 表示没有限制。对于以字节为单位的选项,可以使用以1024为基数的 K, M, G, T, P, E 后缀(例如 LimitAS=16G)。对于时间限制,可以加上 "ms"(毫秒), "s"(秒), "min"(分钟), "h"(小时), "d"(天), "w"(周) 等明确的时间单位后缀(systemd.time(7))。如果仅设置了一个整数而没有单位,那么对于 LimitCPU= 来说默认单位是秒。而对于 LimitRTTIME= 来说默认单位是微秒(百万分之一秒)。注意,这些资源限制值的实际效果可能会受到各自有效粒度的影响。例如 LimitCPU= 所设置的时间会被向上取整到一秒钟的整数倍。LimitNICE= 的值有两种表示法: 可以设为带有 "+" 或 "-" 前缀的谦让值(介于 -20 到 19 之间)。也可以设为无前缀的原始资源限制参数(介于 1 到 40 之间)。

注意,对进程的资源限制是针对单个进程的。当父进程派生出一个子进程的时候,子进程便获得了一个全新的资源集(重新计算资源限制),而不是共享父进程的资源限制,这可能会导致资源超限。而且 LimitRSS= 在Linux平台是没有意义的(因为没有被Linux实现)。建议使用 systemd.resource-control(5) 中的资源限制方法,而不是此处这些针对单个进程的限制指令。因为前者的限制是针对整个单元的,并且可以在运行时动态调整,所以是更好的选择。例如 MemoryLimit= 就是对 LimitRSS= 更好的替代。

对于系统单元来说,可以自由地设置资源限制。而对于用户单元(也就是由 systemd(1) 用户实例运行的单元)来说,只能在操作系统分配给该用户的全部资源范围内,进一步限制单元的资源消耗。

对于一个单元来说,未明确限制的资源的默认值由 systemd-system.conf(5) 中的 DefaultLimitCPU=, DefaultLimitFSIZE=, … 系列选项的值确定。而这一系列选项的默认值则 取决于底层操作系统的配置(全系统或针对单个用户)。

表 1. 资源限制指令、对应的 ulimit 命令、单位

指令等价的 ulimit 命令单位
LimitCPU=ulimit -t
LimitFSIZE=ulimit -f字节
LimitDATA=ulimit -d字节
LimitSTACK=ulimit -s字节
LimitCORE=ulimit -c字节
LimitRSS=ulimit -m字节
LimitNOFILE=ulimit -n文件描述符的数量
LimitAS=ulimit -v字节
LimitNPROC=ulimit -u进程的数量
LimitMEMLOCK=ulimit -l字节
LimitLOCKS=ulimit -x锁的数量
LimitSIGPENDING=ulimit -i信号队列的长度(排队的信号数量)
LimitMSGQUEUE=ulimit -q字节
LimitNICE=ulimit -e谦让度
LimitRTPRIO=ulimit -r实时优先级
LimitRTTIME=不存在微秒

UMask=

设置文件创建掩码。详见 umask(2) 手册。默认值为 0022

KeyringMode=

控制如何设置服务单元的内核会话密钥环(参见 session-keyring(7) 以详细了解会话密钥环的更多详情)。可设为 inherit, private, shared 之一。设为 inherit 表示不对密钥环做特别的设置,直接使用内核的默认行为。设为 private 表示每调用一个服务进程,都会为其分配一个不与任何用户密钥环连接的全新会话密钥环。建议系统服务使用此设置,以确保使用同一个用户身份(通常是 root)运行的多个不同服务之间不会共享各自的密钥。设为 shared 表示与 private 类似,也会为每一个服务进程分配一个会话密钥环,但不同之处在于,User= 用户的密钥环将会被连接到这个新分配的密钥环之中,从而允许单元中的进程请求分配给 User= 用户的密钥。除非设为 inherit 模式,并且将单元的 $INVOCATION_ID 值按照 "invocation_id" 名称,作为一个受保护的密钥,添加到新创建的会话密钥环中; 否则,在 shared 模式下,使用同一个用户身份运行的多个不同服务之间会共享各自的密钥。对于系统服务单元来说,默认值是 private ; 对于非服务单元以及用户单元来说,默认值是 inherit

OOMScoreAdjust=

设置进程因内存不足而被杀死的优先级。可设为 -1000(禁止被杀死) 到 1000(最先被杀死)之间的整数值。详见 proc.txt 文档。

TimerSlackNSec=

设置进程的定时器粒度。详见 prctl(2) 手册。定时器的粒度大小控制着进程被操作系统定时器唤醒的时间精度(也就是最小时间片)。如果仅设为一个整数而没有单位,那么单位是纳秒。也可以在整数后面加上时间单位后缀:"ms"(毫秒), "s"(秒), "min"(分钟), "h"(小时), "d"(天)

Personality=

当进程调用 uname(2) 时,应该返回哪种体系结构标识符,可设为 x86, x86-64, ppc, ppc-le, ppc64, ppc64-le, s390, s390x 之一。必须按照硬件的实际情况设置此选项。通常,64位架构都隐含支持与其对应的32位架构。例如 x86-64 同时支持 x86-64x86 。这主要用于在 x86-64 平台上运行 32-bit 服务的场合。若未设置,则返回未经修改的原始值(取决于主机的内核)。

IgnoreSIGPIPE=

接受一个布尔值。默认值 yes 表示忽略发送给进程的 SIGPIPE 信号,因为该信号通常仅对shell管道有意义。

调度

Nice=

设置进程的默认谦让值。可以设为 -20(最高优先级) 到 19(最低优先级) 之间的整数值。详见 setpriority(2) 手册。

CPUSchedulingPolicy=

设置进程的CPU调度策略。可设为 other, batch, idle, fifo, rr 之一。详见 sched_setscheduler(2) 手册。

CPUSchedulingPriority=

设置进程的CPU调度优先级。有效值范围取决于 CPUSchedulingPolicy= 的设置。例如对于实时调度策略(fifo, rr)来说,可以设为 1(最低优先级) 到 99(最高优先级) 之间的整数。详见 sched_setscheduler(2) 手册。

CPUSchedulingResetOnFork=

是否为派生(fork)的子进程重置CPU调度策略与优先级。若设为 yes 则表示:当父进程的CPU调度策略与优先级高于默认值时,将重置派生的子进程的CPU调度策略与优先级为默认值。这样就可以阻止派生的子进程继承不应有的高优先级。详见 sched_setscheduler(2) 手册。 默认值为 no 。

CPUAffinity=

设置进程的CPU关联性。值是一个逗号分隔的CPU编号与CPU范围列表。CPU范围可以用"编号下限-编号上限"格式表示。若多次设置此选项,则表示将多个选项值以掩码的方式相融合。若设为空,则表示重置掩码并清空先前设置的所有CPU编号列表。详见 sched_setaffinity(2) 手册。

IOSchedulingClass=

设置进程的IO调度类型。可设为 0 到 3 之间的数字或对应的 none, realtime, best-effort, idle 字符串。将此选项设为空字符串表示同时撤销先前给 IOSchedulingClass=IOSchedulingPriority= 设置的值。详见 ioprio_set(2) 手册。

IOSchedulingPriority=

设置进程的IO调度优先级。可设为 0(最高优先级) 到 7(最低优先级) 之间的数字。实际可用的优先级取决于 IOSchedulingClass= 的设置。将此选项设为空字符串表示同时撤销先前给 IOSchedulingClass=IOSchedulingPriority= 设置的值。参见 ioprio_set(2) 手册。

沙盒

下面这些沙盒选项可以有效的限制系统对单元进程的暴露程度。对于每一个单元,在确保不妨碍进程正常工作的前提下,都应该尽可能使用最多的沙盒选项进行限制。注意,在底层安全机制缺失的系统上,不可用的沙盒特性都会优雅地失效。例如, ProtectSystem= 会在不支持文件系统名字空间的内核上失效,或者,如果服务管理器在容器中运行(文件系统名字空间不可用), ProtectSystem= 也同样会失效。类似的,RestrictRealtime= 会在不支持 SECCOMP 系统调用过滤的内核上失效、也会在禁用 SECCOMP 系统调用过滤的容器中失效。

注意,因为底层内核功能只能由特权进程访问,所以某些沙盒功能在用户服务(由用户服务管理器实例运行的服务)中不可用。典型的,例如需要文件系统名字空间支持的沙盒选项(例如 ProtectSystem=)不可用。

ProtectSystem=

可设为布尔值或 "full" 或 "strict" 之一。若设为 yes 则表示为该单元内的进程以只读模式挂载 /usr/boot 目录。若设为 "full" 则表示为该单元内的进程以只读模式挂载 /etc/usr/boot 目录。若设为 "strict" 则表示为该单元内的进程以只读模式挂载 除虚拟文件系统 /dev, /proc, /sys 之外的所有其他目录(对这些目录可以使用 PrivateDevices=, ProtectKernelTunables=, ProtectControlGroups= 进行保护)。这样可以有效禁止该单元对操作系统、配置文件、本地挂载点进行任何修改。推荐为所有需要长时间运行的服务开启此选项,除非该单元确实需要对系统进行修改。开启此选项之后,还可以使用 ReadWritePaths= 来将某些特定的目录改为读写模式。当 DynamicUser=yes 时,此选项的默认值为 "yes" ,否则默认值为 "no" 。注意,此选项并不确保在任何情况下都能提供有效的保护(尤其是主机与单元间的挂载传递),它与 ReadOnlyPaths= 选项 具有相同的局限性(详见后文)。

ProtectHome=

接受一个布尔值或特殊值 "read-only" 或 "tmpfs" 。设为 yes 表示对该单元内的进程屏蔽 /home, /root, /run/user 目录(内容为空且不可访问)。设为 "read-only" 表示这三个目录仅为只读(不可写入)。设为 "tmpfs" 表示在这三个目录上挂载只读模式的临时文件系统,这样既可以隐藏与单元内的进程无关的用户目录,又可以让 BindPaths=BindReadOnlyPaths= 中列出的目录仍然对单元内的进程可见。

设为 "yes" 相当于将这三个目录放入 InaccessiblePaths= 中;设为 "read-only" 相当于将这三个目录放入 ReadOnlyPaths= 中;设为 "tmpfs" 相当于将这三个目录放入 TemporaryFileSystem= 中。

推荐为所有需要长时间运行的服务开启此选项(特别是面向网络的服务),以确保其无法访问隐私数据。当 DynamicUser=yes 时,此选项的默认值为 "yes" ,否则默认值为 "no" 。注意,此选项并不确保在任何情况下都能提供有效的保护(尤其是主机与单元间的挂载传递),它与 ReadOnlyPaths= 选项具有相同的局限性(详见后文)。此选项仅可用于系统单元(不适用于用户单元)。

RuntimeDirectory=, StateDirectory=, CacheDirectory=, LogsDirectory=, ConfigurationDirectory=

这些选项接受一个空格分隔的目录名称列表。目录名称必须是相对路径,并且不能包含 ".." 成分。如果设置了这里的选项,那么在单元启动时,将会在下表指定的父目录之下,按照指定的名称创建一个或多个目录(含上级目录)。同时,还会以目录的完整路径为值,定义与选项对应的环境变量,如果设置了多个目录,那么环境变量中的多个路径之间使用冒号分隔。

表 2. 自动目录创建与环境变量

目录用于系统单元的父目录用于用户单元的父目录自动设置的环境变量
RuntimeDirectory=/run$XDG_RUNTIME_DIR$RUNTIME_DIRECTORY
StateDirectory=/var/lib$XDG_CONFIG_HOME$STATE_DIRECTORY
CacheDirectory=/var/cache$XDG_CACHE_HOME$CACHE_DIRECTORY
LogsDirectory=/var/log$XDG_CONFIG_HOME/log$LOGS_DIRECTORY
ConfigurationDirectory=/etc$XDG_CONFIG_HOME$CONFIGURATION_DIRECTORY

对于 RuntimeDirectory= 来说,除非 RuntimeDirectoryPreserve= 被设为 restartyes ,否则,当单元停止时,将会自动删除已经创建的目录(实际仅删除最末级的目录)。但是对于 StateDirectory=, CacheDirectory=, LogsDirectory=, ConfigurationDirectory= 来说,即使单元已经停止,也不会删除已经创建的目录。

对于除 ConfigurationDirectory= 之外的其他选项来说,在创建指定的目录时,将会以 User=Group= 作为拥有者(实际仅为最末级的目录)。如果指定的目录已经存在,并且这些目录自身并不符合 User=Group= 的设置,那么将会强制按照 User=Group= 的设置,递归的修改这些目录。作为一种优化措施,如果指定的目录已经存在,并且这些目录自身已经符合 User=Group= 的设置,那么即使这些目录下的内容并不符合 User=Group= 的设置,也会按原样保持这些目录下的内容不变。对指定的目录(实际仅为最末级的目录),将会分别按照 RuntimeDirectoryMode=, StateDirectoryMode=, CacheDirectoryMode=, LogsDirectoryMode=, ConfigurationDirectoryMode= 强制设置其访问权限。

这些选项创建的目录都被隐含的添加到了 BindPaths= 之中。当这些选项与 RootDirectory=RootImage= 一起使用时,这些路径将始终驻留在宿主系统中,并且会被挂载到单元内的文件系统名字空间中。

如果 DynamicUser=yes ,那么 StateDirectory=, CacheDirectory=, LogsDirectory= 将会分别在主机的 /var/lib/private, /var/cache/private, /var/log/private 目录中创建指定的目录。因为非特权用户无法访问主机上的这些目录,所以这些目录不会因为动态用户UID循环而被意外访问,同时,自动创建的软连接,也消除了这种做法造成的路径差异。最终,无论是从宿主系统的视角,还是从单元内部的视角,这些目录都将始终直接位于 /var/lib, /var/cache, /var/log 目录之中。

使用 RuntimeDirectory= 来管理单元的运行时目录,可以将运行时目录与单元的生命周期绑定在一起,以确保这些运行时目录在单元启动时自动创建、并在单元停止时自动删除。这非常适合于那些没有权限在 /run 中创建运行时目录的非特权守护进程。对于需要更复杂配置或者生存期保证的运行时目录,可以考虑使用 tmpfiles.d(5) 功能。

例如,如果在一个系统服务单元中存在如下设置:

RuntimeDirectory=foo/bar baz

那么,当该服务启动时,将会自动创建 /run/foo(若不存在), /run/foo/bar, /run/baz 目录并将 /run/foo/bar/run/baz (/run/foo 除外) 的拥有者设置为 User=Group=; 当该服务停止时,这两个目录(/run/foo 除外)也会被自动删除。

例如,如果在一个系统服务单元中存在如下设置:

RuntimeDirectory=foo/bar
StateDirectory=aaa/bbb ccc

那么环境变量 "RUNTIME_DIRECTORY" 与 "STATE_DIRECTORY" 将被创建,并且分别被赋值为 "/run/foo/bar" 与 "/var/lib/aaa/bbb:/var/lib/ccc" 。

RuntimeDirectoryMode=, StateDirectoryMode=, CacheDirectoryMode=, LogsDirectoryMode=, ConfigurationDirectoryMode=

分别设置 RuntimeDirectory=, StateDirectory=, CacheDirectory=, LogsDirectory=, ConfigurationDirectory= 目录的访问权限(必须是八进制形式)。默认值是 0755 。对于权限位的含义,可参见 path_resolution(7) 手册的 "Permissions" 小节。

RuntimeDirectoryPreserve=

是否保留运行时目录(RuntimeDirectory=),可设为布尔值或特殊值 restart 。默认值 no 表示在单元停止或重启时无条件的删除运行时目录。设为 restart 表示仅在单元重启时保留运行时目录。注意,所谓"重启"包括由 Restart= 定义的自动重启,以及由 systemctl restart foo.service 命令触发的手动重启。设为 yes 表示始终无条件的保留运行时目录(即使单元已经被停止)。注意,因为 /run 是一个 "tmpfs" 文件系统,所以 RuntimeDirectory= 总是会在系统重启之后消失。

ReadWritePaths=, ReadOnlyPaths=, InaccessiblePaths=

为进程设置一个新的文件系统名字空间,也就是限制进程可访问的文件系统范围。每个选项的值都是一个空格分隔的绝对路径列表。注意,这里所说的"绝对路径"实际上是以主机或容器根目录(也就是运行 systemd 的系统根目录)为基准的绝对路径。注意,如果路径是一个软连接,那么在追踪软连接时将以 RootDirectory=/RootImage= 设置的根目录为基准。

对于 ReadWritePaths= 中列出的路径,进程从名字空间内访问与从外部访问的权限是一样的。对于 ReadOnlyPaths= 中列出的路径,即使进程从外部访问时拥有写入权限,从名字空间内访问时,进程也依然只能拥有只读权限。可将 ReadWritePaths= 嵌套于 ReadOnlyPaths= 内,以实现在只读目录内嵌套可写子目录的功能。当 ProtectSystem=strict 时,可以使用 ReadWritePaths= 设置可写入路径的白名单。

对于 InaccessiblePaths= 中的路径,进程从名字空间内访问时没有任何权限(既不能读取也不能写入)。因为不可能在其中嵌入 ReadWritePaths=, ReadOnlyPaths=, BindPaths=, BindReadOnlyPaths= 选项,所以有可能限制过头了。可以看看更灵活的 TemporaryFileSystem= 选项。

这里所说的"路径"可以是目录、文件、软连接。可以多次使用这些选项,以在文件系统名字空间内增加更多的受限路径。若将某选项设为空,则表示撤消该选项先前设置的所有路径列表。

可以在路径前加上 "-" 前缀,表示忽略不存在的路径。也可以在路径前加上 "+" 前缀,表示路径以单元的 RootDirectory=/RootImage= 为基准(而不是以主机或容器的根目录为基准[见上文])。如果想要在同一个路径上同时使用 "-" 与 "+" 前缀,那么必须确保 "-" 在 "+" 之前。

注意,这些选项使得文件系统的挂载无法从单元向主机传递。因此,不可将这些选项用于需要在主机名字空间中挂载文件系统的服务。对于 ReadWritePaths=ReadOnlyPaths= 来说,文件系统的挂载从主机向单元的传递依然有效,也就是主机上的挂载/卸载在单元内也是同步可见的。特别需要注意的是,从主机到单元的挂载是原封不动传递的,也就是,主机上可写的挂载在单元内也是可写的,甚至即使传递到单元内的挂载点位于 ReadOnlyPaths= 路径下,也仍然是可写的! 因此,这些选项所设置的访问限制并不会延伸到后来在这些路径下新挂载的文件系统上。这意味着这些设置提供的锁定是不完整的,并不能提供完全的保护。

注意,特权进程可以撤销这些设置的效果。为了给单元设置一个有效的沙盒环境,建议将这些设置与 CapabilityBoundingSet=~CAP_SYS_ADMINSystemCallFilter=~@mount 一起使用。

TemporaryFileSystem=

接受一个空格分隔的挂载点列表,用于挂载临时文件系统(tmpfs)。如果设置了此选项,那么将会为进程新建一个文件系统名字空间,并在列表中的每一个挂载点上都挂载一个临时文件系统(tmpfs)。可以多次使用此选项,以设置更多的挂载点。若将此选项设为空,则表示撤消所有先前设置的挂载点列表。可以在挂载点末尾使用冒号后缀来设置挂载选项(也就是 "挂载点:挂载选项" 格式)。每个挂载点都默认带有 "nodev,strictatime,mode=0755" 挂载选项,但是可以通过明确设置例如 "dev" 与 "nostrictatime" 等挂载选项进行更改。

此选项既可以隐藏与该单元无关的文件与目录,又可以让 BindPaths=BindReadOnlyPaths= 中列出的文件与目录仍然对该单元内的进程可见。

例如,如果在单元中存在如下设置:

TemporaryFileSystem=/var:ro
BindReadOnlyPaths=/var/lib/systemd

那么,该单元中的进程将看不见 /var 目录下的任何内容(但 /var/lib/systemd 目录下的内容除外)。此选项仅可用于系统单元(不适用于用户单元)。

PrivateTmp=

设为 yes 表示在进程的文件系统名字空间中挂载私有的 /tmp/var/tmp 目录,也就是不与名字空间外的其他进程共享临时目录。这样做会增加进程的临时文件安全性,但同时也让进程之间无法通过 /tmp/var/tmp 目录进行通信。同时,当服务停止之后,所有先前在临时目录中创建的文件都将被删除。可以通过 JoinsNamespaceOf= 选项(参见 systemd.unit(5) 手册)将多个单元 运行在同一个名字空间的私有 /tmp/var/tmp 中。当 DynamicUser=yes 时,此设置的默认值为 yes ,否则默认值为 no 。注意,此选项并不确保在任何情况下都能提供有效的保护(尤其是主机与单元间的挂载传递),它与 ReadOnlyPaths= 选项具有相同的局限性(详见前文)。开启此选项的同时也隐含的在该单元中添加了对 /tmp/var/tmp 挂载点 的 Requires=After= 依赖,以及对 systemd-tmpfiles-setup.service 单元的 After= 依赖。

注意,因为此选项的设置有可能在实际上无法落实(例如挂载名字空间不可用),所以,在编写单元文件的时候,不应该将单元的安全依赖于此选项必然生效的假定。此选项仅可用于系统单元(不适用于用户单元)。

PrivateDevices=

设为 yes 表示为该单元内的进程设置一个全新的 /dev 挂载点,并仅在其中添加诸如 /dev/null, /dev/zero, /dev/random, /dev/ptmx, /dev/pts/ … 之类的虚拟设备。注意,并不包括 /dev/sda, /dev/md0, /dev/mem, /dev/port … 之类的物理设备。这样可以有效的关闭进程对物理设备的访问。设为 yes 将会安装一个禁止使用底层I/O系统调用(@raw-io)的过滤器、 强行从 CapabilityBoundingSet= 中移除 CAP_MKNODCAP_SYS_RAWIO 项、 强行设置 DevicePolicy=closed (详见 systemd.resource-control(5) 手册)。因此,不可将该选项用于需要在主机名字空间中挂载文件系统的服务。注意:(1)这个全新的 /dev 挂载点将以 "ro,noexec" 选项挂载(只读,不可执行)。(2)"noexec"可能会导致某些老旧的程序故障,因为这些程序企图通过对 /dev/zero 使用 mmap(2) 来设置可执行内存,而不是使用新式的 MAP_ANON 方法。注意,此选项并不确保在任何情况下都能提供有效的保护(尤其是主机与单元间的挂载传递),它与 ReadOnlyPaths= 选项具有相同的局限性(详见前文)。如果开启此选项,并且该单元运行于用户模式或者缺少 CAP_SYS_ADMIN capability 的系统模式(例如明确将 User= 设为普通用户),那么将自动隐含 NoNewPrivileges=yes 的设置。

注意,因为此选项的设置有可能在实际上无法落实(例如挂载名字空间不可用),所以,在编写单元文件的时候,不应该将单元的安全依赖于此选项必然生效的假定。此选项仅可用于系统单元(不适用于用户单元)。

PrivateNetwork=

接受一个布尔值。设为 yes 表示为该单元内的进程设置一个新的网络名字空间,并在其中仅配置一个 "lo" 本地回环设备(没有任何物理网络设备)。这样就可以有效关闭进程对实际物理网络的访问。默认值是 no 。可以通过 JoinsNamespaceOf= 选项(参见 systemd.unit(5) 手册)将多个单元运行在同一个私有网络名字空间中。注意,此选项将会从主机断开所有套接字(包括 AF_NETLINKAF_UNIX)。必须特别注意的是,(1)对于 AF_NETLINK 来说,这意味着单元内的进程将无法从 systemd-udevd.service(8) 接收设备事件;(2)对于 AF_UNIX 来说,这意味着单元内的进程将无法访问那些位于主机抽象套接字名字空间中的 AF_UNIX 套接字(不过仍然可以访问位于文件系统上的 AF_UNIX 套接字)。

注意,因为此选项的设置有可能在实际上无法落实(例如网络名字空间不可用),所以,在编写单元文件的时候,不应该将单元的安全依赖于此选项必然生效的假定。此选项仅可用于系统单元(不适用于用户单元)。

PrivateUsers=

接受一个布尔值。设为 yes 表示为该单元内的进程设置一个新的用户名字空间,并仅在其中保留最小化的 user/group 映射。具体说来就是仅在该名字空间内保留 "root" 用户与组、 单元自身的用户与组,同时将所有其他用户与组统一映射到 "nobody" 用户与组。这样就可以安全的将该单元所使用的 user/group 数据库从主机系统中剥离出来,从而为该单元创建一个有效的沙盒环境。所有不属于 "root" 或该单元自身用户的 文件、目录、进程、IPC 对象……等资源,在该单元内部依然可见,但是它们将会变为全部属于 "nobody" 用户与组。开启此选项之后,无论单元自身的用户与组是否为 "root" ,在主机的名字空间内,该单元内的所有进程都将以非特权用户身份运行。特别地,这意味着该单元内的进程在主机的名字空间内没有任何 capability ,但是在该单元的用户名字空间内部,仍然拥有所有全部的 capability 。诸如 CapabilityBoundingSet= 之类的设置,将仅在该单元的用户名字空间内部有意义(不能突破到主机的名字空间)。默认值是 no 。

此设置在与 RootDirectory=/RootImage= 一起使用时比较有意义,因为在此场景中仅需要映射 "root", "nobody" 以及单元自身的用户与组。

注意,因为此选项的设置有可能在实际上无法落实(例如用户名字空间不可用),所以,在编写单元文件的时候,不应该将单元的安全依赖于此选项必然生效的假定。此选项仅可用于系统单元(不适用于用户单元)。

ProtectKernelTunables=

设置是否保护内核变量。设为 yes 表示所有通过 /proc/sys, /sys, /proc/sysrq-trigger, /proc/latency_stats, /proc/acpi, /proc/timer_stats, /proc/fs, /proc/irq 访问的内核变量,对于该单元内的进程,将全部变为只读。通常,仅在系统启动的时候设置内核变量,比如通过 sysctl.d(5) 配置。因为只有极少数服务需要在运行时修改内核变量,所以建议为绝大多数服务单元开启此选项。注意,此选项并不确保在任何情况下都能提供有效的保护(尤其是主机与单元间的挂载传递),它与 ReadOnlyPaths= 选项具有相同的局限性(详见前文)。默认值为 "no" 。如果将此选项设为 yes 并且该单元运行在用户模式,或者运行在没有 CAP_SYS_ADMIN capability 的系统模式(也就是明确设置了 User= 为非特权用户),那么将自动隐含 NoNewPrivileges=yes 的设置。注意,此选项不能阻止通过 IPC 调用其他进程的方式间接修改内核变量。不过可以使用 InaccessiblePaths= 来屏蔽 IPC 对象。注意, ProtectKernelTunables=yes 隐含的设置了 MountAPIVFS=yes 。此选项仅可用于系统单元(不适用于用户单元)。

ProtectKernelModules=

设置是否保护内核模块。此选项仅可用于系统单元(不适用于用户单元)。默认值为 no 。设为 yes 表示禁止显式加载/卸载内核模块。对于绝大多数不需要使用特别的文件系统或内核模块才能工作的服务,建议明确将此选项设为 yes 。设为 yes 之后,将会删除该单元的 CAP_SYS_MODULE capability 并且添加一个系统调用过滤器,以禁止加载或卸载内核模块,同时,禁止访问 /usr/lib/modules 目录。注意,此选项并不确保在任何情况下都能提供有效的保护(尤其是主机与单元间的挂载传递),它与 ReadOnlyPaths= 选项具有相同的局限性(详见前文)。注意,将此选项设为 yes ,既不会阻止自动加载用户明确配置为自动加载的内核模块,也不会阻止内核自身为了完成特定功能而自动加载的内核模块。要想彻底禁止内核模块的自动加载功能,可以参考 sysctl.d(5) 中的 kernel.modules_disabled 参数,以及讲解 /proc/sys/kernel/modules_disabled 的文档。如果开启了此选项,并且该服务运行在用户模式或者没有 CAP_SYS_ADMIN capability 的系统模式(例如明确将 User= 设为普通用户),那么将自动隐含 NoNewPrivileges=yes

ProtectControlGroups=

设置是否保护 cgroups 。默认值为 no 。设为 yes 表示仅允许该单元内的进程以只读模式访问 Linux Control Groups (cgroups(7)),也就是仅允许以只读模式访问 /sys/fs/cgroup 。除了容器管理程序,其他服务不应该对 cgroups 拥有控制权。因此,建议为绝大多数服务单元开启此选项。注意,此选项并不确保在任何情况下都能提供有效的保护(尤其是主机与单元间的挂载传递),它与 ReadOnlyPaths= 选项具有相同的局限性(详见前文)。注意, ProtectControlGroups=yes 隐含了 MountAPIVFS=yes 。此选项仅可用于系统单元(不适用于用户单元)。

RestrictAddressFamilies=

限制该单元内的进程可以访问的套接字类型。值是一个空格分隔的地址族列表(默认为白名单)。例如 AF_UNIX, AF_INET, AF_INET6 … 列表默认解释为白名单。但也可以在列表开头添加 "~" 字符表示反转(变成黑名单),也就是仅禁止使用列出的套接字。注意,此限制仅作用于 socket(2) 系统调用。通过其他方式传递给进程的套接字(例如,基于套接字启动的单元,参见 systemd.socket(5))、 以及由 socketpair() 创建的 AF_UNIX 套接字,不受此选项影响。此外,此选项在 32-bit x86, s390, s390x, mips, mips-le, ppc, ppc-le, pcc64, ppc64-le 架构上是无效的(注意,在 x86-64 上是有效的)。在支持混合ABI的系统上(例如 x86/x86-64),建议关闭次要的ABI(例如关闭 x86 以使用纯 x86-64 环境),以确保进程无法通过次要ABI接口绕过此处的限制。在实践中,明确设置 SystemCallArchitectures=native 是一种非常好的做法。如果以用户模式运行或者以不含 CAP_SYS_ADMIN capability 的系统模式运行(例如设置了 User=nobody),那么将自动隐含 NoNewPrivileges=yes 的设置。该选项默认不作任何限制。明确设为空字符串表示撤销先前所有的限制(也就是重置为默认值)。此选项不影响带有 "+" 前缀的命令。

此选项旨在限制对进程的远程访问,特别是通过例如 AF_PACKET 这样敏感的协议。注意,在大多数情况下,应该将 AF_UNIX 包含在白名单中,因为包括通过 syslog(2) 发送日志之类的本地进程间通信需要使用它。

RestrictNamespaces=

限制该单元内的进程对名字空间的访问。可设为一个布尔值,或者一个空格分隔的名字空间类型标识符列表。有关 Linux 名字空间的详细解说,参见 namespaces(7) 手册。默认值 no 表示不对名字空间的创建和切换做任何限制。设为 yes 表示禁止访问任何类型的名字空间。可使用的名字空间类型标识符如下: cgroup, ipc, net, mnt, pid, user, uts 。列表默认为白名单,表示仅允许访问明确列出的名字空间类型。可以在列表开头添加 "~" 字符表示反转(变成黑名单),也就是仅禁止访问明确列出的名字空间类型。设为空字符串等价于重置为默认值 no (不作任何限制)。可以多次使用此选项,表示将多个名字空间类型使用 OR 逻辑合并(但以"~"开头的行则用 AND 逻辑合并)(参见下面的例子)。在系统内部,此处的设置实际上是限制可以对 unshare(2), clone(2), setns(2) 系统调用使用那些标记参数。注意,如果使用此选项限制了对特定类型的名字空间的创建与切换,那么同时也将禁止对 setns() 系统调用使用空标记参数。此选项仅在 x86, x86-64, mips, mips-le, mips64, mips64-le, mips64-n32, mips64-le-n32, ppc64, ppc64-le, s390, s390x, 架构上有意义。如果以用户模式运行或者以不含 CAP_SYS_ADMIN capability 的系统模式运行(例如设置了 User=nobody),那么将自动隐含 NoNewPrivileges=yes 的设置。

例如,如果在一个单元中存在如下设置:

RestrictNamespaces=cgroup ipc
RestrictNamespaces=cgroup net

那么 cgroup, ipc, net 将会被设置。如果第二行带有 "~" 前缀,例如

RestrictNamespaces=cgroup ipc
RestrictNamespaces=~cgroup net

那么将只有 ipc 被设置。

LockPersonality=

接受一个布尔值,表示是否锁定 personality(2) 系统调用。设为 yes 表示禁止更改内核的执行域,也就是只能使用默认值或 Personality= 设置的值,这有助于提升系统的安全性。如果以用户模式运行或者以不含 CAP_SYS_ADMIN capability 的系统模式运行(例如设置了 User=),那么将自动隐含 NoNewPrivileges=yes 的设置。

MemoryDenyWriteExecute=

接受一个布尔值。设为 yes 表示:禁止创建可写可执行的内存映射、 禁止将已存在的内存映射修改为可执行、禁止将共享内存段映射为可执行。具体说来就是将会拒绝: 同时设置了 PROT_EXECPROT_WRITEmmap(2) 系统调用、 设置了 PROT_EXECmprotect(2)pkey_mprotect(2) 系统调用、 设置了 SHM_EXECshmat(2) 系统调用。注意,此选项与那些会在运行时动态生成可执行代码的程序与库有冲突,包括JIT(运行时编译执行)执行引擎、可执行堆栈、以及利用了C编译器"trampoline"特性生成的可执行程序。此选项可用于提升服务的安全性,因为它使得利用软件漏洞来动态改变运行时代码变得困难。不过服务单元可以写入没有设置 noexec 选项的文件系统(例如 /dev/shm)、或者使用 memfd_create() 系统调用来破解这种保护措施。为了应对这种可能的破解手段,可以通过屏蔽相关文件系统(例如 InaccessiblePaths=/dev/shm)、 设置系统调用过滤器(例如 SystemCallFilter=~memfd_create),这样的手段来预防。注意, x86-64 完整支持此特性,但 x86 仅部分支持。特别地,在 x86 上不能使用 shmat() 保护。在支持混合ABI的系统上(例如 x86/x86-64),建议关闭次要的ABI(例如关闭 x86 以使用纯 x86-64 环境),以确保进程无法通过次要ABI接口绕过此处的限制。在实践中,明确设置 SystemCallArchitectures=native 是一种非常好的做法。如果以用户模式运行或者以不含 CAP_SYS_ADMIN capability 的系统模式运行(例如明确将 User= 设为普通用户),那么将自动隐含 NoNewPrivileges=yes 的设置。

RestrictRealtime=

接受一个布尔值。设为 yes 表示禁止实时调度单元中的进程。也就是禁止访问 SCHED_FIFO, SCHED_RR, SCHED_DEADLINE 这些实时任务调度策略(参见 sched(7)手册)。如果以用户模式运行或者以不含 CAP_SYS_ADMIN capability 的系统模式运行(例如明确将 User= 设为普通用户),那么将自动隐含 NoNewPrivileges=yes 的设置。实时调度策略可能会导致CPU被长时间独占,进而导致系统失去响应或导致拒绝服务攻击。应该仅允许个别确实需要实时调度的单元使用实时调度策略。默认值为 no

RemoveIPC=

此选项仅可用于系统单元(不适用于用户单元)。接受一个布尔值。表示是否在该单元停止时删除该单元所拥有的全部 System V/POSIX IPC 对象(实际由运行该单元进程的用户与组持有)。对于使用相同用户/组的多个单元来说,仅在最后一个使用此 IPC 对象的单元停止之后,该 IPC 对象才会被删除。特别地,将会删除 System V 信号量(semaphore)、 System V 与 POSIX 共享内存段(shared memory segment)与消息队列(message queue)。此设置仅在至少明确使用了 User=, Group=, DynamicUser= 之一的情况下才有效。并且对 root 用户的 IPC 对象无效。当 DynamicUser=yes 时,此设置的默认值为 "yes" ,否则默认值为 "no" 。

PrivateMounts=

接受一个布尔值。默认值为 no 。设为 yes 表示该单元内的进程将会运行在自身的私有文件系统名字空间中,所有单元内的挂载都不会向主机传递。也就是说,单元内发生的所有文件系统挂载与卸载动作,仅对单元内的进程可见,而单元外的主机是完全看不见的。但另一方面,从主机向单元的传递依然有效,也就是说,主机上发生的文件系统挂载与卸载动作,对单元内的进程是可见的。详见 mount_namespaces(7) 手册以了解文件系统名字空间。

设为 yes 将会为单元内每一个被调用的进程依次执行三个操作: (1)创建一个新的 CLONE_NEWNS 名字空间; (2)使用 MS_SLAVE 标记重新挂载所有已经存在的挂载点,以禁止从单元向主机传递挂载(但从主机向单元的传递依然有效); (3)按照 MountFlags= 设置的传递模式(详见下文),再次重新挂载所有已经存在的挂载点。

文件系统名字空间分别针对单元内的每一个进程建立。因此,在 ExecStartPre= 进程的文件系统名字空间中建立的挂载点,将会在该进程退出时,立即被自动清除,而不会被后面的 ExecStart= 进程看到(这个规则同样适合于其他各种单元选项中设置的命令)。类似的, JoinsNamespaceOf= 并不允许在单元之间共享内核的文件系统名字空间,它只能做到在单元之间共享 /tmp//var/tmp/ 目录。

其他有关文件系统名字空间的选项( PrivateMounts=, PrivateTmp=, PrivateDevices=, ProtectSystem=, ProtectHome=, ReadOnlyPaths=, InaccessiblePaths=, ReadWritePaths=, … )都使用与此选项相同的方式启用文件系统名字空间(分别针对每个进程执行三个操作)。因此,如果没有使用其他选项,显式的开启此选项将非常有用。此选项仅可用于系统单元(不适用于用户单元)。

MountFlags=

设置单元与主机之间的挂载传递方式。可设为 shared, slave, private 之一。此选项控制着文件系统挂载点的挂载和卸载动作如何在主机与单元之间传递。参见 mount(2) 手册以详细了解挂载传递标记。

此选项只控制 为单元内进程创建的文件系统名字空间内的所有挂载点的最后挂载传递方式(第三步)。其他有关文件系统名字空间的选项(参见上文 PrivateMounts= 选项中的讨论),通过将单元内的文件系统名字空间中的所有挂载点的挂载传递标记首先修改为 slave 步骤(第二步),已经隐式地默认禁止了从单元向主机传递挂载与卸载。因此,将此选项设置为 shared 并不能重新在单元与主机之间建立双向传递。

此选项的默认值是 shared 。不过,如果已经使用了其他有关文件系统名字空间的选项(参见上文 PrivateMounts= 选项中的讨论),那么从单元向主机传递依然是关闭的(因为已经首先使用了 slave )。

切勿将此选项设为 private 。因为当 MountFlags=private 时, 单元无法从主机接收卸载事件,从而导致主机上的临时挂载(例如移动硬盘)变为持久挂载(一直被单元中的进程占用而无法卸载)。

最好忘记此选项(不要去设置它),转而使用其他更高级别的文件系统名字空间选项,特别是上文的 PrivateMounts= 选项。此选项仅可用于系统单元(不适用于用户单元)。

系统调用过滤

SystemCallFilter=

设置该单元内进程的系统调用过滤器。值是一个空格分隔的系统调用名称列表(默认为白名单)。如果该单元内的进程使用了白名单列表之外的系统调用,将会立即被 SIGSYS 信号杀死。可以在列表开头添加 "~" 字符表示反转(变成黑名单), 也就是仅禁止使用列表中列出的系统调用。对于在黑名单列表中列出的每一个系统调用(或系统调用集合)项,可选地,都可以使用冒号(":")来添加 "errno" 错误代码(0~4095)或错误名称(例如 EPERM, EACCES, EUCLEAN)后缀,其含义是,如果进程使用了黑名单中的系统调用,那么直接返回该系统调用后缀中指定的错误代码或错误名称(而不是简单的直接杀死进程)。通过后缀指定的错误代码或错误名称的优先级高于在 SystemCallErrorNumber= 中设置的值。如果以用户模式运行、或者以不含 CAP_SYS_ADMIN capability 的系统模式运行(例如设置了 User=nobody),那么将自动隐含 NoNewPrivileges=yes 的设置。该选项依赖于内核的 Secure Computing Mode 2 接口("seccomp filtering"),常用于强制建立一个最小化的沙盒环境。注意,execve, exit, exit_group, getrlimit, rt_sigreturn, sigreturn 以及查询系统时间与暂停执行(sleep)的系统调用是默认隐含于白名单中的。可以多次使用此选项以融合多个过滤器。若设为空,则表示清空先前所有已设置的过滤器。此选项不影响带有 "+" 前缀的命令。

注意,在支持混合ABI的系统上(例如 x86/x86-64),建议关闭次要的ABI(例如关闭 x86 以使用纯 x86-64 环境),以确保进程无法通过次要ABI接口绕过此处设置的限制。我们强烈建议将此选项与例如 SystemCallArchitectures=native 这样的设置一起使用。

注意,过于严苛的系统调用过滤器有可能会对服务单元的正常进程执行与故障处理机制产生不良影响。特别地,如果 execve 系统调用被屏蔽,那么将无法启动服务进程。此外,如果服务进程启动失败(例如未找到可执行文件),那么故障处理机制有可能需要额外访问更多的系统调用,以确保可以正常的收拾残局与记录错误日志。为了调试是否是因为系统调用过滤器过于严苛而导致的单元故障,可以临时禁用系统调用过滤器。

如果同时设置了白名单和黑名单,那么以先出现的名单为基准,后出现的名单将基于第一个名单对列表项进行加减。例如,首个列表为白名单,包含 readwrite 两项,随后又是一个仅包含一个 write 的黑名单,那么最终结果将是一个仅包含 read 的白名单。

因为系统调用的数量非常巨大,所以 systemd 预定义了一些以 "@" 字符开头的系统调用的集合,以方便使用。

表 3. 预定义的系统调用集合

集合名称描述
@aio异步 I/O 操作 (io_setup(2), io_submit(2), …)
@basic-io基本的 I/O 操作:读取、写入、寻址、文件描述符的复制与关闭 (read(2), write(2), …)
@chown更改文件归属 (chown(2), fchownat(2), …)
@clock更改系统时钟 (adjtimex(2), settimeofday(2), …)
@cpu-emulationCPU 模拟 (vm86(2) …)
@debug调试、性能监控、跟踪 (ptrace(2), perf_event_open(2) …)
@file-system文件系统操作:打开文件/目录、创建文件/目录、读写文件/目录、重命名文件/目录、删除文件/目录、读取文件属性、创建硬连接/软连接
@io-event事件循环 (poll(2), select(2), epoll(7), eventfd(2) …)
@ipc管道, SysV IPC, POSIX 消息队列, 其他IPC (mq_overview(7), svipc(7))
@keyring内核密钥环 (keyctl(2) …)
@memlock将内存锁定在 RAM 中 (mlock(2), mlockall(2) …)
@module加载/卸载内核模块 (init_module(2), delete_module(2) …)
@mount挂载/卸载文件系统 (mount(2), chroot(2), …)
@network-ioSocket I/O (包括 AF_UNIX): socket(7), unix(7)
@obsolete不常用/反对使用/未实现的系统调用 (create_module(2), gtty(2), …)
@privileged所有需要特权的调用 (capabilities(7))
@process进程控制、执行、名字空间操作 (clone(2), kill(2), namespaces(7), …
@raw-io原始 I/O 端口访问 (ioperm(2), iopl(2), pciconfig_read(), …
@reboot重新启动与准备重启 (reboot(2), kexec(), …)
@resources更改资源限制、内存使用、进程调度 (setrlimit(2), setpriority(2), …)
@setuid更改 UID/GID 凭证 (setuid(2), setgid(2), setresuid(2), …)
@signal操纵与处理进程信号 (signal(2), sigprocmask(2), …)
@swap挂载与卸载 swap 设备 (swapon(2), swapoff(2))
@sync将内存中的缓存刷写到磁盘上 (fsync(2), msync(2), …)
@system-service一组仅供常规系统服务使用的、合理的系统调用,不包含任何特殊用途的系统调用。建议以此作为设置系统服务的系统调用白名单的起点,因为它仅包含系统服务通常必需的系统调用,但不包括某些特定用途的系统调用(例如排除了 "@clock", "@mount", "@swap", "@reboot")。
@timer按时间编排执行计划 (alarm(2), timer_create(2), …)


注意,当内核增加新的系统调用时,上述集合的内容可能会随之发生变化。此外,集合的内容也会跟随内核版本以及编译 systemd 的目标架构的不同而变化。可以使用 systemd-analyze syscall-filter 精确的列出 每个过滤器中的系统调用。

通常,使用白名单(而不是黑名单)是更安全的模式。建议对所有长期运行的系统服务使用系统调用白名单进行限制。对于大多数系统服务来说,可以使用下面的设置作为安全设置的起点:

[Service]
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

推荐将文件系统名字空间相关的选项与 SystemCallFilter=~@mount 一起使用,以阻止单元内的进程改变挂载属性。文件系统名字空间相关的选项有: PrivateTmp=, PrivateDevices=, ProtectSystem=, ProtectHome=, ProtectKernelTunables=, ProtectControlGroups=, ReadOnlyPaths=, InaccessiblePaths=, ReadWritePaths=

SystemCallErrorNumber=

接受一个 "errno" 错误代码(1~4095)或错误名称(例如 EPERM, EACCES, EUCLEAN),表示当进程触犯 SystemCallFilter= 规则时,将会返回这里设定的错误代码或错误名称(而不是简单的直接杀死进程)。如果没有设置此选项或将此选项设为空,那么表示当进程触犯 SystemCallFilter= 规则时,将会简单的直接杀死进程。

SystemCallArchitectures=

接受一个空格分隔的体系结构标识符列表,设置进程可以使用哪些体系结构的系统调用。可以使用的标识符与 ConditionArchitecture= 相同(参见 systemd.unit(5) 手册),此外还包括 x32, mips64-n32, mips64-le-n32, native(编译 systemd 时的目标架构)。如果以用户模式运行或者以不含 CAP_SYS_ADMIN capability 的系统模式运行(例如设置了 User=nobody),那么将自动隐含 NoNewPrivileges=yes 的设置。该选项的默认值为空,表示不作任何限制。

如果设置了此选项,那么该单元中的进程就只能使用此选项指定体系结构的系统调用。注意,当此选项非空时,native 将被自动隐含的包含在列表中。为了此选项的目的, x32 体系结构被视为包括 x86-64 系统调用。然而,此选项仍然可以在 x32 上实现它的目的,如下所述。

系统调用过滤器并非在所有体系结构上都完全一致。例如,由于 ABI 的限制,无法在 x86 上对网络套接字相关的系统调用进行过滤,但是 x86-64 却不存在这个问题(也就是可以过滤)。在支持混合ABI的系统上(例如 x86/x86-64),建议关闭次要的ABI(例如关闭 x86 以使用纯 x86-64 环境),以确保进程无法通过次要ABI接口绕过系统调用过滤器的限制。在实践中,明确设置 SystemCallArchitectures=native 是一种非常好的做法。

还可以通过 SystemCallArchitectures= 在全局范围内限制可使用的ABI架构。详见 systemd-system.conf(5) 手册。

环境变量

Environment=

设置进程的环境变量,接受一个空格分隔的 VAR=VALUE 列表。可以多次使用此选项以增加新的变量或者修改已有的变量(同一个变量以最后一次设置为准)。设为空表示清空先前所有已设置的变量。注意: (1)不会在字符串内部进行变量展开(也就是"$"没有特殊含义); (2)如果值中包含空格或者等号,那么必须在字符串两边使用双引号(")界定。

例如:

Environment="VAR1=word1 word2" VAR2=word3 "VAR3=$word 5 6"

设置了 "VAR1", "VAR2", "VAR3" 三个变量,其值分别为 "word1 word2", "word3", "$word 5 6"

详见 environ(7) 手册。

注意,不要使用环境变量向单元中的进程传递机密信息(例如密码与口令之类)。一方面,环境变量会通过 D-Bus IPC 暴露给其他非特权客户端; 另一方面,环境变量在概念上也不属于需要保护的机密数据。此外,因为环境变量能够沿进程树传播,并且能够跨越安全边界(例如 setuid/setgid 程序),所以可能会将机密数据泄漏给不应访问的进程。

EnvironmentFile=

Environment= 类似,不同之处在于此选项是从文本文件中读取环境变量的设置。文件中的空行以及以分号(;)或井号(#)开头的行会被忽略,其他行的格式必须符合 VAR=VALUE 的shell变量赋值语法。行尾的反斜杠(\)将被视为续行符,这与shell语法类似。若想在变量值中包含空格,则必须在值的两端加上双引号(")界定。

文件必须用绝对路径表示(可以包含通配符)。但可在路径前加上 "-" 前缀表示忽略不存在的文件。可以多次使用此选项,以从多个不同的文件中读取设置。若设为空,则表示清空 所有先前已经从文件中读取的环境变量。

这里列出的文件将在进程启动前的瞬间被读取,因此可以由前一个单元生成配置文件,再由后一个单元去读取它。

从文件中读取的环境变量会覆盖 Environment= 中设置的同名变量。文件的读取顺序就是它们出现在单元文件中的顺序,并且对于同一个变量,以最后读取的文件中的设置为准。

PassEnvironment=

将某些 systemd 系统服务管理器进程(PID=1)所持有的环境变量传递给该单元中的进程。接受一个空格分隔的变量名列表。可以多次使用此选项以传递更多变量。若设为空,则表示清空先前已设置的所有变量。如果此处设置的变量并不是系统服务管理器进程(PID=1)所持有的环境变量,那么将会被悄无声息的忽略掉。注意,此选项仅可用于传递PID=1的 systemd 系统服务管理器进程所持有的环境变量,因为系统服务单元默认并不自动继承PID=1进程所持有的环境变量。又因为用户服务单元默认就会自动继承PID≠1的 systemd 用户服务管理器进程的所有环境变量,所以此选项对于用户服务管理器没有意义。

注意,通过此选项传递过来的环境变量的值会被 Environment=EnvironmentFile= 选项中的同名变量所覆盖。

例如:

PassEnvironment=VAR1 VAR2 VAR3

传递了 "VAR1", "VAR2", "VAR3" 三个变量,其值等于PID=1进程所持有的值。

参见 environ(7) 以了解更多环境变量的细节。

UnsetEnvironment=

明确撤销该单元的特定环境变量。接受一个空格分隔的变量名("NAME")与特定值变量("NAME=VALUE")列表。可以多次使用此选项以撤销更多变量。若设为空,则表示清空先前已设置的所有列表。特定值变量("NAME=VALUE")表示仅当变量"NAME"的值恰好等于"VALUE"时才会撤销"NAME"变量。而普通的变量名("NAME")则表示无论变量"NAME"的值是什么,都会无条件的撤销"NAME"变量。注意,因为 UnsetEnvironment= 的撤销操作发生在向单元内进程传递环境变量前的最后一步,所以此选项可以撤销来自各种渠道的环境变量,包括: (1)由 Environment=EnvironmentFile= 设置的环境变量; (2)继承自服务管理器全局设置的环境变量(参见 systemd-system.conf(5) 手册中的 DefaultEnvironment= 选项); (3)根据 PassEnvironment= 继承的环境变量; (4)由服务管理器设置的环境变量(例如 $NOTIFY_SOCKET 之类); (5)由 PAM 模块设置的环境变量(如果已设置 PAMName= 的话)。

参见 environ(7) 以了解更多有关环境变量的详细介绍。

日志与标准输入输出

StandardInput=

设置进程的标准输入(STDIN)。可设为 null, tty, tty-force, tty-fail, data, file:path, socket, fd:name 之一。

null 表示 /dev/null ,也就是所有读取都只会得到一个文件结束标记(EOF)。

tty 表示 TTY(由 TTYPath= 设置),也就是该进程将会成为终端的控制进程。若终端已被其他进程控制,则一直等到其他进程释放为止。

tty-forcetty 类似,不同之处在于,该进程将会强制立即取得终端的控制权,并剥夺其他进程的控制权。

tty-failtty 类似,不同之处在于,若终端已被其他进程控制,则会导致该进程自身启动失败。

data 用于设置通过标准输入传递给进程的任意文本或二进制数据。要传递的文本/二进制数据可以通过下文的 StandardInputText=/StandardInputData= 选项进行设置。注意,实际传递的文件描述符类型(内存文件、常规文件、unix管道 …)可能取决于内核和有效权限。无论如何,文件描述符总是只读的,并且读取到的数据末尾 总是带有一个 EOF 结束标记。

file:path 用于将指定的文件系统对象连接到标准输入。path 必须是一个指向常规文件、FIFO、特殊文件的绝对路径。如果指定了一个文件系统上的 AF_UNIX 套接字,那么会将其连接到一个流套接字,进而可以连接到任意系统服务进程的标准输入。

socket 仅可用于基于套接字启动的服务单元,并且要求关联的套接字单元文件(详见 systemd.socket(5)) 必须已经设置了 Accept=yes 或仅指定了单个套接字。主要用于兼容那些依赖于传统 inetd(8) 守护进程的单元。

fd:name 将标准输入连接到某个套接字单元提供的名为 name 的文件描述符。省略 name 表示使用隐含的默认名称 "stdin" (也就是 "fd" 等价于 "fd:stdin")。必须通过套接字单元的 Sockets= 选项定义至少一个指定名称的文件描述符,并且文件描述符的名称可以与定义它的套接字单元的名称不同。如果找到多个匹配的文件描述符,那么仅使用第一个匹配项。详见 systemd.socket(5) 手册中的 FileDescriptorName= 选项,以了解有关命名文件描述符及其顺序的详细信息。

此选项的默认值是 null

注意,如果某个服务单元设置了 DefaultDependencies=no 并且将 StandardInput=StandardOutput= 设为 tty/tty-force/tty-fail 之一,那么必须设置 After=systemd-vconsole-setup.service 以确保在该单元启动前首先完成 TTY 的初始化。

StandardOutput=

设置进程的标准输出(STDOUT)。可设为 inherit, null, tty, journal, syslog, kmsg, journal+console, syslog+console, kmsg+console, file:path, append:path, socket, fd:name 之一。

inherit 表示继承 StandardInput= 的值。

null 表示 /dev/null ,也就是所有输出都会被丢弃。

tty 表示 TTY(由 TTYPath= 设置)。如果仅用于输出,那么进程将无需取得终端的控制权,亦无需等待其他进程释放终端控制权。

journal 表示 systemd 日志服务(通过 journalctl(1) 访问)。注意,所有发到 syslogkmsg 的日志都会隐含的复制一份到 journal 中。

syslog 表示 syslog(3) 日志服务。注意,此时所有日志都会隐含的复制一份到 journal 中。

kmsg 表示内核日志缓冲区(通过 dmesg(1) 访问)。注意,此时所有日志都会隐含的复制一份到 journal 中。

journal+console, syslog+console, kmsg+console 与上面三个值类似,不同之处在于所有日志都会再复制一份到系统的控制台上。

file:path 用于将指定的文件系统对象连接到标准输出。其含义与上文 StandardInput= 中的解释完全相同。如果 path 是文件系统上的一个普通文件,那么它将被打开(若不存在则创建),并从头开始写入(但并不清空已有内容)。如果将标准输入与标准输出都指定为同一个文件,那么此文件仅打开一次,并同时用于输入与输出。这种用法主要用于文件系统上的 AF_UNIX 套接字,因为在这种情况下,只能创建一个同时用于输入与输出的流连接。

append:pathfile:path 类似,不同之处仅在于以附加(append)模式打开文件。

socket 仅可用于基于套接字启动的服务单元。其含义与上文 StandardInput= 中的解释完全相同。

fd:name 将标准输出连接到某个套接字单元提供的名为 name 的文件描述符。省略 name 表示使用隐含的默认名称 "stdout" (也就是 "fd" 等价于 "fd:stdout")。必须通过套接字单元的 Sockets= 选项定义至少一个指定名称的文件描述符,并且文件描述符的名称可以与定义它的套接字单元的名称不同。如果找到多个匹配的文件描述符,那么仅使用第一个匹配项。详见 systemd.socket(5) 手册中的 FileDescriptorName= 选项,以了解有关命名文件描述符及其顺序的详细信息。

如果单元的标准输出(StandardOutput=)或标准错误(StandardError=)中含有 journal, syslog, kmsg 之一,那么该单元将会自动隐含的获得 After=systemd-journald.socket 依赖(见上文)。注意,在这种情况下,标准输出(或下文的标准错误)将会是一个 AF_UNIX 流套接字(而不是可以重新打开的 FIFO 或管道)。这意味着 shell 脚本中类似 echo "hello" > /dev/stderr 这样向标准输出写文本的命令不能正常工作。变通的方法是,使用类似 echo "hello" >&2 这样的命令。

此选项的默认值等于 systemd-system.conf(5)DefaultStandardOutput= 选项的值(默认为 journal)。注意,设置此选项将会导致自动添加额外的依赖关系(见上文)。

StandardError=

设置进程的标准错误(STDERR)。取值范围及含义与 StandardOutput= 相同。但有如下例外: (1) inherit 表示使用 StandardOutput= 的值。(2) fd:name 的默认文件描述符名称为 "stderr"

此设置的默认值是 systemd-system.conf(5)DefaultStandardError= 的值(默认为 inherit)。注意,设置此选项将会导致自动添加额外的依赖关系(见上文)。

StandardInputText=, StandardInputData=

设置通过标准输入(STDIN)传递给进程的任意文本或二进制数据。这些选项仅在 StandardInput=data 时有意义。使用此选项可将进程的输入数据直接嵌入到单元文件中。

StandardInputText= 接受任意文本数据(可以使用C风格的转义字符以及 "%" 说明符)。每次使用此选项,指定的文本都会附加到该单元的数据缓冲区,并且在末尾添加一个换行符作为结束标记(因此每次使用都会在缓冲区末尾添加一个新行)。注意,此选项值的行首与行尾的空白字符将会被自动删除。设为空行表示清空缓冲区,因此,为了插入空行,请务必在行首或行尾使用 "\n" 来实现。

StandardInputData= 不支持转义序列或说明符,只接受 Base64 编码的任意二进制数据,并且还会忽略编码中的任何空白。

注意, StandardInputText=StandardInputData= 在同一个数据缓冲区上操作,并且可以混合使用,以便为同一个输入流同时配置二进制和文本数据。文本或二进制数据严格按照选项在单位文件中的出现顺序进行连接。为其中任意一个选项分配空字符串都将重置数据缓冲区。

为了保持可读性,可以将超长的选项值拆分成多行,方法是在每行(最后一行除外)末尾加上 "\" 字符(详见 systemd.unit(5) 手册)。此方法对于为这两个选项设置较大数据时特别有用。例如:

…
StandardInput=data
StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy4KSWNrIGtpZWtl \            LCBzdGF1bmUsIHd1bmRyZSBtaXIsCnVmZiBlZW1hbCBqZWh0IHNlIHVmZiBkaWUgVMO8ci4KTmFu \            dSwgZGVuayBpY2ssIGljayBkZW5rIG5hbnUhCkpldHogaXNzZSB1ZmYsIGVyc2NodCB3YXIgc2Ug \            enUhCkljayBqZWhlIHJhdXMgdW5kIGJsaWNrZSDigJQKdW5kIHdlciBzdGVodCBkcmF1w59lbj8g \            SWNrZSEK
…
LogLevelMax=

按此日志级别过滤该单元生成的日志消息。可设为一个 syslog 日志级别。也就是 emerg(最低日志级别,仅显示最致命的消息), alert, crit, err, warning, notice, info, debug(最高日志级别,显示最啰唆细致的调试消息) 之一。详见 syslog(3) 手册。默认无筛选(相当于设为 debug)。使用此选项可以让日志系统丢弃该服务产生的高于指定级别的日志消息。例如 LogLevelMax=info 表示关闭该单元的调试日志消息(太过啰唆冗长)。注意,此选项设置的日志级别,作用于此单元内全部进程产生的日志消息、以及通过全部日志协议发送的日志消息。此处设置的日志过滤器作用于日志管道的早期(在进行任何日志处理之前),成功通过此过滤器的日志消息仍可能会被日志子系统中稍后阶段的过滤器丢弃。例如在 journald.conf(5) 中配置的 MaxLevelStore= 就有可能禁止将更高级别的日志消息存储在磁盘上(即使该单元的 LogLevelMax= 允许对其进行处理)。

LogExtraFields=

在该单元内的进程产生的日志记录中添加额外的日志元数据字段。此选项接受一个或多个空格分隔的日志字段(格式为 "FIELD=VALUE")。有关日志字段的详细信息,请参阅 systemd.journal-fields(7) 手册。即使底层日志实现允许使用二进制字段值,在这里也只能使用有效的 UTF-8 字符串。如果想要在字段值中包含空格字符,那么必须在字符串两边使用双引号(")界定。可以在字段值中使用常规的环境变量(见下文"派生进程中的环境变量"小节)。注意,此选项不仅可以向单元的日志记录中添加额外的元数据,而且因为所有字段都会被索引,所以还可以实现跨单元的日志记录匹配。设为空字符串表示清空先前已设置的列表。

LogRateLimitIntervalSec=, LogRateLimitBurst=

限制单元生成日志的速率。如果在 LogRateLimitIntervalSec= 定义的时间段内,日志服务记录的日志条数大于 LogRateLimitBurst= 中指定的数量,那么在该时间段内的所有其他日志消息都将被丢弃,直到该时间段结束。同时会生成一条关于丢弃日志数量的日志消息。可在 LogRateLimitIntervalSec= 中使用 "s", "min", "h", "ms", "us" 等时间单位后缀(详见 systemd.time(7) 手册)。默认值由 journald.conf(5) 中的 RateLimitIntervalSec=RateLimitBurst= 决定。

SyslogIdentifier=

设置日志标识符(发送日志消息时加在行首的字符串)("syslog tag")。默认值是进程的名称。此选项仅在 StandardOutput=StandardError= 的值包含 journal(+console), syslog(+console), kmsg(+console) 之一时才有意义,并且仅适用于输出到标准输出或标准错误的日志消息。

SyslogFacility=

设置 syslog 的 facility 值。可设为 kern, user, mail, daemon, auth, syslog, lpr, news, uucp, cron, authpriv, ftp, local0, local1, local2, local3, local4, local5, local6, local7 之一(详见 syslog(3) 手册)。此选项仅在 StandardOutput=StandardError= 的值包含 journal(+console), syslog(+console), kmsg(+console) 之一时才有意义,并且仅适用于输出到标准输出或标准错误的日志消息。默认值为 daemon

SyslogLevel=

设置默认的 syslog 日志级别。可设为 emerg, alert, crit, err, warning, notice, info, debug 之一(详见 syslog(3) 手册)。此选项仅在 StandardOutput=StandardError= 的值包含 journal(+console), syslog(+console), kmsg(+console) 之一时才有意义,并且仅适用于输出到标准输出或标准错误的日志消息。注意,进程可以在某些日志行的头部附加一个自定义的日志级别标记,以覆盖此处设置的默认级别。但是可以通过设置下面的 SyslogLevelPrefix= 来禁止识别自定义的日志级别。参见 sd-daemon(3) 手册。默认值是 info

SyslogLevelPrefix=

接受一个布尔值。如果设为 yes 并且 StandardOutput=StandardError= 的值包含 journal(+console), syslog(+console), kmsg(+console) 之一,那么进程输出的带有日志级别前缀的日志行,将会按照前缀标记的日志级别进行处理,但是同时也会去掉这个标记日志级别的前缀。设为 no 则表示将日志行按原样传递而不作任何解析。此选项仅适用于输出到标准输出或标准错误的日志消息。详见 sd-daemon(3) 手册。默认值为 yes 。

TTYPath=

设置用于 STDIN, STDOUT, STDERR 的终端设备节点文件(参见前面三个选项)。默认值是 /dev/console

TTYReset=

是否在进程执行前与进程退出后,重置 TTYPath= 终端设备。默认值是 "no"

TTYVHangup=

是否在进程执行前与进程退出后,断开所有已打开 TTYPath= 终端设备的客户端。默认值是 "no"

TTYVTDisallocate=

TTYPath= 是一个虚拟控制台终端时,是否尝试在进程执行前与进程退出后,释放该终端,以确保清空屏幕与回滚缓冲区。默认值是 "no"

System V 兼容

UtmpIdentifier=

设置一个4字符长度的标识符,用于在 utmp(5) 与 wtmp 中标识自己。若长度超过4个字符,则截取4个结尾字符。此选项应该仅用于 agetty(8) 之类的服务,以及好像是被 getty 进程运行的服务(见下文 UtmpMode= 选项),因为它必须在执行前和退出后在 utmp/wtmp 中创建和清除记录。该选项可以识别 %I 风格的字符串。默认值为空,也就是不在 utmp/wtmp 中 创建和清除日志。

UtmpMode=

可设为 "init", "login", "user" 之一, 表示为此服务生成哪种类型的 utmp(5) 与 wtmp 记录。 此选项仅在设置了 UtmpIdentifier= 时才有意义。 "init" 表示仅生成 INIT_PROCESS 项, 被调用的进程必须实现与 getty 兼容的 utmp/wtmp 逻辑。 "login" 表示依次生成 INIT_PROCESSLOGIN_PROCESS 项,被调用的进程必须实现与 login(1) 兼容的 utmp/wtmp 逻辑。 "user" 表示依次生成 INIT_PROCESS, LOGIN_PROCESS, USER_PROCESS 项, 被调用的进程必须作为一个会话首进程(session leader)运行。 默认值是 "init"

派生进程中的环境变量

进程在被 systemd 启动的时候,会拥有多个不同来源的环境变量。由系统实例(PID=1)启动的进程仅从 systemd(PID=1) 继承 PassEnvironment= 中列出的环境变量;由用户实例(PID≠1)启动的进程则会自动从 systemd(PID≠1)继承全部环境变量。

对于每一个被 systemd 启动的进程,都会拥有来自下列多个不同来源的环境变量:

  • (1)通过 DefaultEnvironment= 设置的全局环境变量(参见systemd-system.conf(5) 手册)、(2)通过内核引导选项 systemd.setenv= 设置的全局环境变量(参见systemd(1) 手册)、(3)通过 systemctl set-environment 设置的全局环境变量(参见 systemctl(1) 手册)。

  • 由 systemd 为进程设置的环境变量(参见下文的列表)

  • 从 systemd 继承的环境变量:(1)由用户实例启动的进程继承全部环境变量;(2)由系统实例启动的进程仅继承 PassEnvironment= 中列出的环境变量。

  • 通过单元文件中的 Environment= 选项设置的环境变量

  • 通过单元文件中的 EnvironmentFile= 选项设置的环境变量

  • 设置了 PAMName= 选项之后,由 PAM 模块设置的环境变量。详见 pam_env(8) 手册。

如果上述多个来源都设置了同一个环境变量,那么将按照上述列表的顺序,以最后一个来源为准。注意,上述所有环境变量都会在启动进程之前的最后一步,按照UnsetEnvironment= 的设置进行清除。

systemd 将会为每一个被启动的进程设置或传递下列环境变量:

$PATH

可执行文件的目录列表(冒号分隔的绝对路径),此值固定为 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

$LANG

本地化设置。可以通过 locale.conf(5) 文件设置,也可以通过内核引导选项(参见 systemd(1)kernel-command-line(7))设置。

$USER, $LOGNAME, $HOME, $SHELL

用户名, 用户名, 家目录, 登录shell 。这几个变量仅对设置了 User= 选项的单元有效(包括 systemd 用户实例)。参见 passwd(5) 手册。

$INVOCATION_ID

在单元每次启动时随机生成的一个 128bit 唯一标识符,表现为一个32字符的十六进制字符串。在单元每一次从 inactive 状态变 activating 或 active 状态时,这个标识符都会发生变化,因此可以用于标识单元的每一个生命周期,特别是在存储例如日志这样的离线数据的时候。同一个单元内的进程,在单元的同一个生命周期内,都将收到相同的标识符。

$XDG_RUNTIME_DIR

保存运行时对象(例如 IPC 对象)以及运行时数据的目录。此环境变量仅适用于:(1)由 systemd 用户实例运行的用户服务; (2)在 PAMName= 中包含了 pam_systemd 的系统服务(详见后文以及 pam_systemd(8) 手册)。

$MAINPID

单元主进程的PID (如果能确定的话)。仅对由 ExecReload= 之类启动的控制进程设置。

$MANAGERPID

systemd 用户实例的PID ,仅为从 systemd 用户实例派生的进程设置。

$LISTEN_FDS, $LISTEN_PID, $LISTEN_FDNAMES

传递给服务的基于套接字启动的文件描述符的相关信息。参见 sd_listen_fds(3) 手册。

$NOTIFY_SOCKET

sd_notify() 所使用的套接字。参见 sd_notify(3) 手册。

$WATCHDOG_PID, $WATCHDOG_USEC

关于看门狗 keep-alive 通知的相关信息。参见 sd_watchdog_enabled(3) 手册。

$TERM

终端类型。仅为连接到终端的单元设置(StandardInput=tty, StandardOutput=tty, StandardError=tty) 。参见 termcap(5) 手册。

$JOURNAL_STREAM

如果进程的标准输出(STDOUT)或标准错误(STDERR)被连接到了日志(例如 StandardError=journal),那么 $JOURNAL_STREAM 将是形如 "device:inode" 格式的字符串,其中 "device" 是连接的文件描述符所在的设备,而 "inode" 则是连接的文件描述符的 inode 号(以十进制数表示)。这将允许进程安全的检测其标准输出(STDOUT)或标准错误(STDERR)是否被连接到了日志。进程应该将自己实际使用的文件描述符的 "device:inode" 值与环境变量中的值进行对比,以检测进程的标准输出(STDOUT)或标准错误(STDERR)是否依然连接在日志上。注意,仅检查是否设置了 $JOURNAL_STREAM 环境变量是远远不够的,因为服务进程有可能调用外部进程,并在未撤销此环境变量的情况下,替换这些外部进程的标准输出(STDOUT)或标准错误(STDERR)。

如果进程的标准输出(STDOUT)与标准错误(STDERR)都通过流套接字连接到了日志,那么此环境变量将只包含标准错误(STDERR)所使用的流套接字信息,因为错误信息通常是日志记录的首要目的。不过因为通常都会使用同一个流套接字来连接标准输出(STDOUT)与标准错误(STDERR),所以其实这两者所使用的流套接字信息实际上是完全一样的。

此环境变量主要用于让服务单元 将日志协议升级到 systemd 日志协议(使用 sd_journal_print(3) 等函数)。如果服务进程的标准输出(STDOUT)或标准错误(STDERR)被连接到了日志文件,那么就可以伴随日志消息一起保存相关的结构化元数据。

$SERVICE_RESULT

仅在 service 单元中存在。此环境变量将会传递给所有 ExecStop=ExecStopPost= 进程,其中包含了该服务的"结果"。当前定义了如下这些值:

表 4. 已定义的 $SERVICE_RESULT

含义
"success"服务启动成功之后又干净的退出了
"protocol"违反协议:服务没有按照单元的配置执行必需的步骤(特别是违反了 Type= 的设置)
"timeout"某个步骤操作超时
"exit-code"服务进程的退出码非零。可以通过下面的 $EXIT_CODE 获取具体的退出码
"signal"服务进程被某个信号异常终止且未进行内存转储。可以通过下面的 $EXIT_STATUS 获取具体的终止信号。
"core-dump"服务进程被某个信号异常终止且已进行内存转储。可以通过下面的 $EXIT_STATUS 获取具体的终止信号。
"watchdog"为服务开启了看门狗并且发生了喂狗超时
"start-limit-hit"因为超出了该单元的启动频率限制而启动失败。参见 systemd.unit(5) 手册中的 StartLimitIntervalSec=StartLimitBurst= 选项以了解详情。
"resources"总括性的表示操作失败

这些环境变量可以用于监视服务是否成功的终止了。虽然这些变量对 ExecStop=ExecStopPost= 都可见,但是将监视工具添加到后者是更好的选择,因为前者仅在服务启动成功的情况下才会被调用,而后者则在服务启动失败和运行中失败的情况下都会被调用。

$EXIT_CODE, $EXIT_STATUS

仅在 service 单元中存在。此环境变量将会传递给所有 ExecStop=, ExecStopPost= 进程,其中包含了该服务主进程的退出码/退出状态。要想查看退出码/退出状态的详细定义,可以查看 wait(2) 手册。$EXIT_CODE 总是 "exited", "killed", "dumped" 之一。如果 $EXIT_CODE 是 "exited" ,那么 $EXIT_STATUS 将包含字符串形式表示的退出状态(实际上是对退出码含义的解释),否则将包含终止信号的名称。注意,这些环境变量 仅在 systemd 成功定位到服务单元主进程的情况下才会被设置。

表 5. 服务终止状态变量表

$SERVICE_RESULT$EXIT_CODE$EXIT_STATUS
"success""exited""0"
"protocol"未设置未设置
"exited""0"
"timeout""killed""TERM", "KILL"
"exited""0", "1", "2", "3", …, "255"
"exit-code""exited""1", "2", "3", …, "255"
"signal""killed""HUP", "INT", "KILL", …
"core-dump""dumped""ABRT", "SEGV", "QUIT", …
"watchdog""dumped""ABRT"
"killed""TERM", "KILL"
"exited""0", "1", "2", "3", …, "255"
"start-limit-hit"未设置未设置
"resources"上述任意一个上述任意一个
注意:服务进程有可能被 systemd 之外的其他进程发送信号而终止。特别地,进程可以给自身发送任意信号(包括不可屏蔽的信号)。尽管如此,在上面的 "timeout" 与 "watchdog" 行,仅包括 systemd 发送的信号。此外,可以使用 SuccessExitStatus= 指定其他表示干净退出的状态(不在此表中)。

对于系统服务,如果在 PAMName= 中包含了 pam_systemd ,那么还会额外设置由 systemd 定义的 $XDG_SEAT, $XDG_VTNR 环境变量(详见pam_systemd(8) 手册)。

进程退出码

如果 systemd 在启动单元进程时,未能成功应用上文各种选项所设置的执行参数,那么已经启动的服务进程将会直接以非零退出码终止,并且不会执行任何命令行参数。具体说来就是,服务进程将会在被 fork(2)系统调用创建之后、进一步使用 execve(2)系统调用之前,直接以本小节列出的各种非零退出码终止。这些退出码的定义来自:(1)系统的标准C库;(2)LSB规范;(3) systemd 独有的规范。

下面是系统的标准C库定义的退出码:

表 6. 标准C库定义的退出码

退出码符号名称解释
0EXIT_SUCCESS通用的成功代码
1EXIT_FAILURE通用的失败代码

下面是 LSB specification 定义的退出码:

表 7. LSB规范定义的服务退出码

退出码符号名称解释
2EXIT_INVALIDARGUMENT参数无效或超量
3EXIT_NOTIMPLEMENTED功能尚未实现
4EXIT_NOPERMISSION用户权限不足
5EXIT_NOTINSTALLED程序尚未安装
6EXIT_NOTCONFIGURED程序尚未配置
7EXIT_NOTRUNNING程序尚未运行

因为LSB规范建议 200 及以上的退出码可以自由定义。所以 systemd 就定义了下列退出码:

表 8. systemd 定义的服务退出码

退出码符号名称解释
200EXIT_CHDIR切换进程的工作目录失败。参见上文的 WorkingDirectory= 选项。
201EXIT_NICE设置进程的调度优先级(谦让值)失败。参见上文的 Nice= 选项。
202EXIT_FDS关闭进程不需要的文件描述符失败,或者调整传递过来的文件描述符失败。
203EXIT_EXEC实际进程执行失败(也就是 execve(2) 系统调用失败)。这通常是因为可执行文件不存在或者没有执行权限造成的。
204EXIT_MEMORY内存不足导致操作失败
205EXIT_LIMITS调整进程的资源限制失败。参见上文的 LimitCPU= 等资源限制选项。
206EXIT_OOM_ADJUST调整进程的 OOM 设置失败。参见上文的 OOMScoreAdjust= 选项。
207EXIT_SIGNAL_MASK设置进程的信号屏蔽失败
208EXIT_STDIN设置进程的标准输入失败。参见上文的 StandardInput= 选项。
209EXIT_STDOUT设置进程的标准输出失败。参见上文的 StandardOutput= 选项。
210EXIT_CHROOT切换进程的根目录(chroot(2))失败。参见上文的 RootDirectory=/RootImage= 选项。
211EXIT_IOPRIO设置进程的IO调度优先级失败。参见上文的 IOSchedulingClass=/IOSchedulingPriority= 选项。
212EXIT_TIMERSLACK设置进程的定时器粒度失败。参见上文的 TimerSlackNSec= 选项。
213EXIT_SECUREBITS设置进程的安全位失败。参见上文的 SecureBits= 选项。
214EXIT_SETSCHEDULER设置进程的CPU调度优先级失败。参见上文的 CPUSchedulingPolicy=/CPUSchedulingPriority= 选项。
215EXIT_CPUAFFINITY设置进程的CPU关联性失败。参见上文的 CPUAffinity= 选项。
216EXIT_GROUP检测或修改进程的用户组失败。参见上文的 Group=/SupplementaryGroups= 选项。
217EXIT_USER检测或修改进程的用户身份失败、或者设置用户名字空间失败。参见上文的 User=/PrivateUsers= 选项。
218EXIT_CAPABILITIES修改进程的 capability 集合失败。参见上文的 CapabilityBoundingSet=/AmbientCapabilities= 选项。
219EXIT_CGROUP设置服务单元的控制组失败
220EXIT_SETSID未能创建新的进程会话
221EXIT_CONFIRM执行过程被用户取消。详见 kernel-command-line(7) 手册中的 systemd.confirm_spawn= 内核引导选项。
222EXIT_STDERR设置进程的标准错误失败。参见上文的 StandardError= 选项。
224EXIT_PAM设置进程的PAM会话失败。参见上文的 PAMName= 选项。
225EXIT_NETWORK设置进程的网络名字空间失败。参见上文的 PrivateNetwork= 选项。
226EXIT_NAMESPACE设置进程的文件系统名字空间失败。参见上文的 ReadOnlyPaths= 等文件系统相关选项。
227EXIT_NO_NEW_PRIVILEGES未能禁用进程的新权限。参见上文的 NoNewPrivileges=yes 选项。
228EXIT_SECCOMP设置进程的系统调用过滤器失败。参见上文的 SystemCallFilter= 等相关选项。
229EXIT_SELINUX_CONTEXT检测或设置进程的 SELinux 安全上下文失败。参见上文的 SELinuxContext= 选项。
230EXIT_PERSONALITY设置进程的执行域(体系结构)失败。参见上文的 Personality= 选项。
231EXIT_APPARMOR_PROFILE无法更改进程的 AppArmor profile 。参见上文的 AppArmorProfile= 选项。
232EXIT_ADDRESS_FAMILIES限制进程可以访问的套接字类型失败。参见上文的 RestrictAddressFamilies= 选项。
233EXIT_RUNTIME_DIRECTORY设置进程的运行时目录失败。参见上文的 RuntimeDirectory= 等相关选项。
235EXIT_CHOWN修改套接字的拥有者失败。仅用于 socket 单元。
236EXIT_SMACK_PROCESS_LABEL设置进程的 SMACK64 安全标签失败。参见上文的 SmackProcessLabel= 选项。
237EXIT_KEYRING设置内核密钥环失败
238EXIT_STATE_DIRECTORY设置单元的状态目录失败。参见上文的 StateDirectory= 选项。
239EXIT_CACHE_DIRECTORY设置单元的缓存目录失败。参见上文的 CacheDirectory= 选项。
240EXIT_LOGS_DIRECTORY设置单元的日志目录失败。参见上文的 LogsDirectory= 选项。
241EXIT_CONFIGURATION_DIRECTORY设置单元的配置目录失败。参见上文的 ConfigurationDirectory= 选项。

最后, BSD 操作系统定义了一组退出码,通常也适用于 Linux 系统:

表 9. BSD 退出码

退出码符号名称解释
64EX_USAGE命令行语法错误
65EX_DATAERR数据格式错误
66EX_NOINPUT不能打开输入
67EX_NOUSER未知的用户
68EX_NOHOST未知的主机名
69EX_UNAVAILABLE服务不可用
70EX_SOFTWARE软件内部错误
71EX_OSERR系统错误(例如不能 fork)
72EX_OSFILE缺少关键的操作系统文件
73EX_CANTCREAT不能创建(用户)输出文件
74EX_IOERR输入/输出 错误
75EX_TEMPFAIL临时性错误(用户可以尝试重试)
76EX_PROTOCOL协议出现远端错误
77EX_NOPERM没有权限
78EX_CONFIG配置错误

参见

systemd(1), systemctl(1), systemd-analyze(1), journalctl(1), systemd-system.conf(5), systemd.unit(5), systemd.service(5), systemd.socket(5), systemd.swap(5), systemd.mount(5), systemd.kill(5), systemd.resource-control(5), systemd.time(7), systemd.directives(7), tmpfiles.d(5), exec(3)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文