前阵子帮朋友调试一台新上线的服务器装好Nginx之后我习惯性敲了service nginx start结果屏幕上直接回了一句nginx: unrecognized service。愣了两秒才反应过来这台机器走的是 systemd 管理早就不靠/etc/init.d下的脚本来干活了得用systemctl。这个错误看着小但暴露了一个很普遍的问题很多 Linux 用户对 systemd 的认知还停留在“听说过、会用两三个命令”的阶段一涉及 service 和 target 的区分、单元文件的编写、依赖关系的声明就彻底发怵。其实 systemd 没那么玄。它的核心思路是把系统里每个可管理的东西都抽象成“单元unit”而日常打交道最多、也最需要弄明白的就是 service服务单元和 target启动目标。service 管的是“某个具体程序怎么跑”target 管的是“系统处于什么运行状态”两者一横一纵基本覆盖了日常 80% 的服务管理需求。这篇文章就顺着这两条线把 systemd 拆开讲顺手把 systemctl 里常用命令按场景过一遍。适合刚开始从 SysVinit 转过来的运维新手也适合用了很久 systemd 但一直是“能跑就行、报错就重启”的中级用户。1. 先搞清楚systemd到底在管什么1.1 从SysVinit到systemd启动不再是按编号跑一串脚本老一代Linux用户应该都有印象SysVinit 时代的启动流程是一串脚本/etc/rc.d/rc3.d/下面有一堆S01xxx、S99xxx之类的符号链接系统开机时会按照数字编号从小到大逐个执行。这种做法逻辑简单问题也很明显脚本之间依赖关系全靠编号硬撑想插一个新服务进去你得小心翼翼地排编号想并行启动几个没有依赖关系的服务几乎做不到系统启动时间被拖得很长。systemd 把这些脚本式的启动流程换成了单元文件unit file。每个单元文件描述“一个东西该怎么启动、怎么停止、依赖谁、在什么阶段运行”系统根据这些声明的依赖关系能并行的就并行必须串行的就串行。这套设计让系统启动速度明显提升也让服务管理的思路从“跑脚本”变成了“管理状态”。说到这里必须澄清一个误区systemd 不是一个“更复杂的 init”它本身就是系统的 1 号进程。也就是说内核启动后第一个用户态进程就是 systemd所有用户态服务的拉起、监控、重启都由它负责。你日常敲的systemctl命令本质就是跟这个 1 号进程对话告诉它“帮我启动某个东西”或“查一下某个东西的状态”。1.2 service与target一个是“程序”一个是“状态”systemd 里的单元类型很多常见的有 service、target、socket、timer、mount、path 等。其中日常用得最多的是 service 和 target我习惯用生活里的例子来理解这两个东西service 像是商场里的一家店铺。店铺有营业时间和打烊流程对应服务的启动和停止店铺有自己的老板和员工对应服务的进程管理策略。target 像是商场的“运营状态”。比如“正常营业”“夜间巡逻”“紧急闭店”每种状态规定了哪些店铺需要开门、哪些店铺必须关门。target 本身不干活它只是把一堆 service 和其他 target 组织成一个集合。举个例子multi-user.target就是 Linux 服务器最常用的“运营状态”开机后进入多用户命令行环境。系统里那些WantedBymulti-user.target的服务会在进入这个状态时被自动拉起。理解了这层关系你会发现 systemd 的设计其实很优雅职责分离service 管具体程序target 管全局状态。单元类型作用类比service管理一个守护进程或一组进程店铺的完整运营流程target组织一组单元的启动状态商场的运营状态socket管理监听套接字可按需触发服务店铺的「预约通道」timer定时触发任务商场的定时巡检闹钟mount / automount管理文件系统挂载仓库的开关门2. service单元日常管理命令与单元文件编写2.1 systemctl命令实战不背选项按场景记命令很多教程喜欢把 systemctl 的所有子命令拉成一张大列表说实话看完就忘。我的建议是别背按使用场景记用多了自然就熟了。下面这张表是我日常使用频率最高的命令组合按“管理单个服务”“管理开机自启”“查看状态”三组划分基本能覆盖你 90% 的操作需求。场景命令说明立即启动systemctl start nginx启动服务。这里的.service后缀可以省略systemd 会按默认类型去找立即停止systemctl stop nginx停止服务对应执行 ExecStop 里定义的逻辑重启systemctl restart nginx先 stop 再 start常用于配置修改后平滑重载配置systemctl reload nginx通知进程重新读取配置不中断服务需要服务本身支持开机自启systemctl enable nginx在对应 target 的.wants目录里创建符号链接取消开机自启systemctl disable nginx移除刚才的符号链接不停止当前运行的服务查看运行状态systemctl status nginx显示服务状态、主进程 PID、最近日志推荐排障首选只看是否在运行systemctl is-active nginx输出 active 或 inactive适合写脚本判断只看是否开机自启systemctl is-enabled nginx输出 enabled 或 disabled重新加载单元文件systemctl daemon-reload每次增删改单元文件后必须执行让 systemd 重新读取磁盘配置屏蔽服务systemctl mask nginx彻底禁用连手动 start 都会被拒绝取消屏蔽systemctl unmask nginx解除 mask清除失败状态systemctl reset-failed nginx服务反复失败后把失败计数清零有几个坑值得单独说。第一restart和reload不是一回事。restart会杀掉旧进程再启动新进程期间服务会短暂中断reload只是向进程发送信号让它重新读取配置文件进程 PID 不变。能用 reload 就不要轻易 restart尤其是对数据库这类启动耗时的服务。但 reload 的前提是服务支持这个操作不是所有 service 文件都定义了 ExecReload。第二enable不等于start。很多新人写完单元文件执行了systemctl enable xxx就以为服务在跑了结果一查进程根本没有。enable管的是“开机时是否自动启动”start管的是“当前是否立即运行”两者互不替代。正确姿势是先enable再start或者用systemctl enable --now xxx一步到位。第三千万别再敲service nginx start这种老命令了。systemd 环境下有些版本会做一个 SysV 兼容层通过/etc/init.d/里的脚本去调用 systemctl但前提是系统里还装了sysvinit相关的兼容脚本。你写一个自定义 service 单元后service xxx start很可能直接报unrecognized service因为/etc/init.d/下根本没有对应脚本。习惯必须改。2.2 手写service文件从配置到start只差一个daemon-reload单元文件最常见的位置有两个系统包自带的放在/usr/lib/systemd/system/管理员自定义的放在/etc/systemd/system/。后者优先级更高系统升级时也不会被覆盖所以自定义服务一律写到/etc/systemd/system/下。我以一个 Redis 服务单元为例逐段解释关键配置[Unit] DescriptionRedis 7.x persistent server Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Userredis Groupredis ExecStart/usr/local/bin/redis-server /etc/redis/redis.conf ExecReload/bin/kill -USR2 $MAINPID Restarton-failure RestartSec3 LimitNOFILE65535 EnvironmentFile-/etc/sysconfig/redis [Install] WantedBymulti-user.target[Unit]段里的Description是给人看的说明Afternetwork-online.target表示本服务要在网络就绪之后再启动它解决的是“顺序”问题Wantsnetwork-online.target表示我希望网络就绪这个目标存在但它失败了不会拖累本服务。[Service]段是灵魂。Typesimple表示 ExecStart 启动的进程就是服务主进程systemd 只需要保证这个进程活着就算服务在运行。还有一种常见类型是Typeforking它表示主进程启动后会 fork 出子进程跑到后台父进程退出。对于 Redis 这类可以在配置文件里设置daemonize yes的软件有人习惯用 forking 模式。但在 systemd 环境下我的建议是把daemonize改成 no用Typesimple让程序直接在前台运行这样 systemd 能更精确地跟踪主进程状态避免出现“父进程退了但子进程没退干净”的尴尬局面。ExecStart必须写绝对路径这是新手最容易踩的坑。systemd 执行命令时不会帮你加载登录 Shell 的环境变量/usr/local/bin这种目录不一定在它默认的 PATH 里。你手动在终端敲能跑由 systemd 拉起就报Exec format error或者No such file or directory原因就在这里。Restarton-failure表示进程异常退出时自动拉起正常的systemctl stop退出不会触发重启。RestartSec3是重启前等待 3 秒防止快速崩溃死循环时把系统资源耗尽。如果你希望服务只要退出就无条件重启可以用Restartalways但用之前要评估好业务场景避免出现“明明想手动停一下结果它又自己起来了”的绝望体验。LimitNOFILE65535是给服务进程提高文件描述符上限。很多高并发服务在 systemd 环境下莫名其妙报too many open files就是因为默认限制是 1024完全不够用。这里补充一句ulimit -n改的是当前登录会话的限制对 systemd 拉起来的服务没用必须在单元文件里配 LimitNOFILE。EnvironmentFile-/etc/sysconfig/redis前面的-号表示这个文件存在就读不存在也不报错。这是我在部署脚本里非常爱用的一种容错写法。[Install]段是给systemctl enable用的。WantedBymulti-user.target的意思是当执行 enable 时systemd 会在/etc/systemd/system/multi-user.target.wants/目录下生成一个指向本单元文件的符号链接。这样系统进入 multi-user 状态时就会自动把这个服务拉起来。写完单元文件后必须执行一次systemctl daemon-reload让 systemd 重新扫描目录并加载新配置。这一步很容易被忽略结果执行 enable 或 start 的时候报Unit xxx.service not found实际上文件就在那里只是 systemd 还不知道而已。2.3 一个容易被忽略的细节enable到底做了什么我用一个实际例子说明 enable 的产物。假设你写好了/etc/systemd/system/myapp.service执行systemctl enable myapp.servicesystemd 会创建一个符号链接/etc/systemd/system/multi-user.target.wants/myapp.service - /etc/systemd/system/myapp.service也就是说enable 不是修改服务本身的什么属性也不是在服务进程里打标记而是在对应 target 的wants目录里“登记”了一下。这个设计非常干净服务自己只声明“我希望在 multi-user 状态下运行”真正把服务跟状态关联起来的是 enable 时创建的那个符号链接。顺着这个思路手动模拟 enable 的效果也完全可行你自己在/etc/systemd/system/multi-user.target.wants/下创建一个符号链接指向某个 service 文件效果跟enable一模一样。反过来系统里存在这个符号链接但你执行systemctl disablesystemd 也只是把它删掉并不会 stop 正在运行的服务。wants目录里的符号链接只表达“倾向性”如果被链接的服务启动失败不会影响 target 本身。对应地还有一种requires目录表达“必需性”链接进去的服务如果起不来整个 target 也会失败。日常自定义服务基本用 WantedBy 就够了因为没人愿意因为一个业务服务起不来导致系统连登录界面都进不去。3. target机制不只是一张“启动菜单”3.1 target到底是什么和runlevel怎么对应target 的官方定义是“一组单元的集合”它本身不执行任何程序只是把多个单元按依赖关系组织起来。我用更直白的话描述target 就是一张“状态清单”系统处于某个 target 时清单里列出的东西就该处于某种状态。老 SysVinit 里的 runlevel 数字在 systemd 里被映射成了带语义名的 target。这个对应关系在不少文章里都有但我建议别死记数字理解语义更重要传统runlevelsystemd target含义0poweroff.target关机1rescue.target单用户救援模式2、3、4multi-user.target多用户命令行模式5graphical.target图形界面模式6reboot.target重启这里有两点需要特别提醒。第一systemd 的 target 不依赖数字编号新 target 可以取任意名字。比如你完全可以创建一个myapp.target把一批业务服务组织在一起让它们作为一个整体被启动或停止。这点是 runlevel 时代做不到的。第二emergency.target是 rescue 模式更进一步的最小环境只有根文件系统以只读方式挂载适合做系统级排障。真到了那一步说明系统常规启动路径已经走不通了。查看系统当前的默认 target以及切换 target用下面几条命令systemctl get-default # 查看默认启动目标 systemctl set-default multi-user.target # 设置默认启动目标 systemctl isolate multi-user.target # 立即切换到目标状态 systemctl list-units --typetarget # 查看当前已激活的target systemctl list-dependencies multi-user.target # 查看某个target的依赖树isolate是很有意思的命令。它会把当前系统“切换到”指定的 target 状态过程中会停止所有不在该 target 依赖树里的单元。你可以把systemctl isolate reboot.target理解成reboot把systemctl isolate poweroff.target理解成poweroff本质上就是在切换状态时顺带停掉不相干的进程。3.2 用target做模式切换把服务器切成“临时维护状态”target 的实用价值除了开机默认状态更在于它可以帮你快速在多个“运行模式”之间切换。我就做过一个这样的场景一台服务器平时跑正常的业务服务偶尔需要进入“维护模式”在这种模式下只保留 SSH 和基础系统服务业务服务全部停掉方便做数据库迁移或者磁盘扩容。传统做法是手动 stop 一堆服务维护结束再一个个 start 回来操作繁琐还容易漏。用 target 的思路可以先定义一个维护模式的 target[Unit] DescriptionMaintenance Mode Requiresmulti-user.target Aftermulti-user.target AllowIsolateyes关键在AllowIsolateyes这个选项表示该 target 允许被isolate切换。如果不加systemctl isolate maintenance.target会直接报错拒绝执行这是 systemd 防止你误操作的安全机制。然后把这台机器上的业务服务全部改成WantedBymaintenance.target。这里要做的是相反的逻辑希望从 maintenance 模式切回正常业务模式时这些服务能自动恢复。所以我实际的做法是再定义一个production.target业务服务WantedByproduction.target默认启动也设为 production.target平时用isolate在两个模式之间切换。这套方案的副作用很明显isolate会停掉不在目标依赖树里的所有东西所以在生产环境操作前一定要先用systemctl list-dependencies production.target看清依赖树确认里面有没有你不希望停掉的进程。我的建议是非必要不 isolate如果只是想临时停某几个服务老老实实systemctl stop更安全。target 模式切换更适合用在有明确停机窗口的运维操作里。4. 完整实战让三个服务按顺序开机自启4.1 场景nginx、redis、myapp怎么管住它们理论讲再多不如完整跑一个例子。假设我要部署一套环境包含三个组件redis为应用提供缓存mysql为应用提供数据库myapp一个 Java Spring Boot 应用依赖 redis 和 mysql我对开机顺序的需求是系统网络就绪后先启动 mysql再启动 redis等这两个依赖都就绪后再启动 myapp。如果 mysql 起不来myapp 就不要尝试启动避免应用启动时连接数据库失败、反复报错。redis 原则上也是强依赖但考虑到缓存服务偶尔短暂不可用对应用影响没那么致命可以设计成“最好依赖”。这套需求如果用 SysVinit 的编号脚本去排得仔细算 S 编号还容易出现脚本等待超时。用 systemd 就在三个 service 文件里声明依赖关系系统会自动帮你梳理执行顺序。4.2 编写三个service单元声明依赖顺序假设 redis、mysql 已经通过系统包或编译方式安装好二进制和配置文件路径如下redis/usr/local/bin/redis-server配置/etc/redis/redis.confmysql这里直接用系统服务mysql.service已有单元文件myapp/opt/myapp/myapp.jar运行用户appredis 的服务单元可以直接用上一节写过的配置。mysql 如果是系统自带的大概率已经自带单元文件不需要我重写。重点看 myapp 这个业务服务的依赖声明[Unit] DescriptionMyApp Spring Boot Service Requiresmysql.service redis.service Aftermysql.service redis.service network-online.target Wantsnetwork-online.target [Service] Typesimple Userapp Groupapp EnvironmentFile/etc/myapp/myapp.conf ExecStart/usr/bin/java -jar /opt/myapp/myapp.jar ExecStop/bin/kill -TERM $MAINPID Restarton-failure RestartSec5 TimeoutStopSec20 [Install] WantedBymulti-user.target这里最核心的是Requires和After的配合使用。Requiresmysql.service redis.service表示 myapp 跟这两个服务有强依赖关系启动 myapp 时systemd 会尝试同时启动 mysql 和 redis如果这两个服务启动失败myapp 也无法启动。Aftermysql.service redis.service表示执行顺序上myapp 必须等这两个服务进入 active 状态后才启动。为什么要同时写这两个字段因为Requires只表达“依赖”不表达“顺序”。如果只有 Requires 没有 Aftersystemd 可能并行启动三个服务结果 myapp 启动时数据库还没就绪应用照样报错。如果只有 After 没有 Requires那只是“排队”myapp 不会主动触发依赖服务的启动万一 mysql 没启动myapp 等不到会一直卡着。两个字段各管一件事缺一不可。Wantsnetwork-online.target不是必须项但配合Afternetwork-online.target可以确保应用启动时网络已经真正可用。很多人只写Afternetwork.target结果发现服务启动时网卡还没拿到地址启动失败就是因为 network.target 只表示网络服务开始初始化不表示网络已就绪。ExecStop/bin/kill -TERM $MAINPID是我刻意加的一个配置。对于 Java 应用默认 stop 时会发送 SIGTERM 给主进程Java 应用收到后能自动触发 Spring 的优雅停机流程。有些服务需要更复杂的停止逻辑比如先调 API 下线再从注册中心摘除这时候可以写一个停止脚本或者用ExecStopPost做善后处理。4.3 验收启动、查依赖、看日志写完全部单元文件后按下面顺序执行systemctl daemon-reload systemctl enable --now mysql redis myapp第一行命令让 systemd 重新加载单元文件第二行同时完成“加入开机自启”和“立即启动”。执行完以后查看 myapp 的依赖树确认 systemd 理解的依赖关系和我们的预期一致systemctl list-dependencies myapp.service输出会是一棵由 myapp 展开的依赖树里面能看到 mysql.service、redis.service、network-online.target 等条目。查看反向依赖也就是“谁依赖了 myapp”systemctl list-dependencies --reverse myapp.service这个命令对排查“为什么服务被莫名其妙拉起/停止”特别有用。如果启动过程有问题优先用journalctl看日志。-u指定单元名-n控制输出行数-f是跟踪模式journalctl -u myapp.service -f再有一个好用的校验命令是systemd-analyze verify它会静态检查单元文件里的语法错误、未知配置项、路径是否存在但不检查目标程序本身能不能跑systemd-analyze verify /etc/systemd/system/myapp.service如果单元文件本身有问题运行完这行命令systemd 会直接把问题列表打印到终端。我习惯每次写完单元文件都跑一遍 verify然后再 daemon-reload能省掉不少来回排查的时间。5. 常见问题与排查技巧现场报错别慌5.1 高发报错速查表我在实际处理过的 systemd 相关问题里挑了几个最高频的报错整理成一份速查表。如果你遇到类似报错先按表格里的方法来大概率能快速定位。报错信息典型原因排查与解决Job for xxx.service failed because the control process exited with error codeExecStart 里的命令启动失败退出码非 0先systemctl status xxx.service -l看最近日志再手动在终端执行 ExecStart 的命令看真实报错Unit xxx.service could not be found服务名写错、单元文件没放对位置、没执行 daemon-reload检查/etc/systemd/system/下文件是否存在执行daemon-reload后再试The unit files have no installation config单元文件缺少[Install]段补充[Install] WantedBymulti-user.target然后再 enableUnit xxx.service is masked服务被人为 mask 了执行systemctl unmask xxx.serviceFailed to start xxx.service: Unit is not active手动 stop 一个本就没在运行的服务用systemctl status确认实际状态不要盲目 restartservice redis does not support chkconfig还在用老service命令或 chkconfig 管理服务改用systemctl或检查/etc/init.d/redis脚本是否带了 chkconfig 头Job for docker.service failed because the control process exited with error codedockerd 启动失败原因可能是网络配置、存储驱动、权限等用journalctl -xeu docker.service看详细日志重点看 dockerd 的启动参数和配置5.2 最经典的“control process exited”排查思路control process exited这个报错几乎每个 systemd 用户都见过。它的字面意思是“控制进程退出了而且返回了非零退出码”翻译成人话就是你配置的 ExecStart 命令执行失败了。以我遇到过一次的 docker.service 启动失败为例执行systemctl status docker时看到类似Job for docker.service failed because the control process exited with error code很多人到这里就不知道下一步干嘛了。我的固定排查流程是三步第一步看更详细的错误信息systemctl status docker -l-l表示不折叠输出避免长行被截断。这一步通常会直接显示 dockerd 启动日志的最后几行比如failed to start daemon: error initializing graphdriver。第二步看 journald 全量日志journalctl -xeu docker.service-x会附带一些解释信息-e直接跳到日志末尾-u指定单元。这个命令组合是排障时的第一利器很多教程不会强调它但实际工作中几乎天天用。第三步手动执行 ExecStart 里的命令。dockerd 这种命令可以直接在终端跑/usr/bin/dockerd前台运行时所有报错都会直接打在终端上配合日志基本能定位问题。如果 ExecStart 里涉及环境变量先手动 source 对应的 EnvironmentFile 再执行。5.3 enable时报“The unit files have no installation config”这个错我见过太多人问了它本质上是单元文件里少了[Install]段。enable操作的原理我们在 2.3 节讲过它需要在某个 target 的wants目录下创建符号链接而创建的依据就是[Install]段里的WantedBy。没有这个段systemd 就不知道应该把你这个服务挂到哪个 target 下面自然拒绝执行。解决办法很简单在单元文件末尾加上[Install] WantedBymulti-user.target然后执行systemctl daemon-reload再重新 enable。这个错误顺便提醒了一件事如果你写的服务是一个“一次性任务”或者“被其他服务拉起”的辅助单元不打算开机自启确实可以不加[Install]段。但只要你需要enable就必须加。5.4 mask与reset-failed两个容易被误解的操作mask是 systemd 里一个特别激进的禁用方式。mask 之后即使你手动执行systemctl start xxx也会被拒绝提示Unit xxx.service is masked。它的原理是把单元文件符号链接到/dev/null等于告诉 systemd“这个单元不存在”。这个操作适合用来彻底屏蔽某个系统服务比如某些机器上没用的自动更新服务。跟 mask 常一起出现的是reset-failed。systemd 会记录每个服务的失败次数如果服务崩溃后自动重启、又失败、又重启积累到一定次数systemd 会进入一种“暂时放弃”的状态此时手动 start 可能一直报失败。执行systemctl reset-failed xxx.service把失败计数清零服务就能再次尝试启动了。有时候服务反复重启不是因为程序修好了而是失败的“余额”用完了你 reset 一下反而能继续观察问题。5.5 SysV脚本兼容问题service命令为什么不好使了热词里有一个service redis does not support chkconfig这个报错在纯 systemd 环境里出现的频率其实很高。原因是有些软件安装包默认只提供 SysVinit 脚本放在/etc/init.d/下而没有提供 systemd 单元文件。service命令检测到/etc/init.d/redis存在就尝试用兼容层去启动结果脚本里的 chkconfig 头信息缺失或者格式不对就报了这个错误。遇到这种情况我的建议是认清现实既然系统已经是 systemd 了就老老实实给它补一个 service 单元文件而不是去研究怎么修 chkconfig 头。按照 2.2 节的格式写一个简单的单元文件放到/etc/systemd/system/下然后daemon-reload和enable。十分钟不到就能解决后续管理也更顺。另外说一句在 systemd 环境里执行service命令时有些发行版会做一个“翻译层”把service redis start转成systemctl start redis。这个翻译层对 redis 这种没有 systemd 单元的服务是无效的所以别指望老命令能通吃一切。5.6 跨环境报错的联想重复定义与同名冲突前面热词里有几条跟服务注册相关的报错比如“指定的服务已存在”之类的。虽然那条更常见于 Windows 服务场景但这让我想到 systemd 里一个类似的坑同名单元文件重复定义。比如你在/usr/lib/systemd/system/和/etc/systemd/system/下各放了一个相同文件名的 service实际生效的是/etc/systemd/system/下的那个因为它优先级更高。如果两个文件差别很大你会发现明明改了系统包自带的配置却不生效就是因为被/etc/systemd/system/下的同名文件覆盖了。排查方法很简单systemctl cat xxx.service这个命令会显示 systemd 实际加载的单元文件内容以及来源路径。如果输出里出现了# /etc/systemd/system/xxx.service开头说明这个单元是你自己覆盖的版本别再去改/usr/lib/systemd/system/下的原文件了。6. 写在最后一点个人经验和一个技巧用 systemd 这么多年我最大的感受是它真正把“启动流程”变成了一种可配置、可审计的资源而不是放任一堆脚本在后台互相踩。你可以在单元文件里清楚地看到每个服务的启动顺序、依赖关系、重启策略、资源限制这比 SysVinit 时代“靠脚本里的注释和人肉记忆”要强太多。刚接触时我也觉得它反 Unix 哲学但用顺之后说实话回不去了——因为systemctl status一下就能看到全部状态journalctl -u一下就能看到对应日志这种体验在老 init 环境里很难实现。最后分享一个我踩过坑之后养成的固定习惯凡是系统自带的服务想改它的启动参数绝对不要直接编辑/usr/lib/systemd/system/下的原始文件。原因很简单软件包升级时原始文件会被覆盖你的修改会无声无息地消失。正确做法是用systemctl edit创建一个覆盖片段systemctl edit nginxsystemd 会在/etc/systemd/system/nginx.service.d/下生成一个 override.conf 文件你在里面覆盖需要修改的配置项即可。这个文件优先级高于原始文件而且不会影响软件包升级。如果想直接复制一份完整的单元文件来改用systemctl edit --full --force nginx这会把原始文件内容复制到/etc/systemd/system/nginx.service你可以整体修改。不过这种方式有个缺点就是未来软件包更新了原始单元文件你的复制版不会自动同步需要自己关注变化。鉴于这个风险大多数场景我更推荐 override.conf 的方式小修改用它干净又安全。