一、为什么要系统学习 systemctl?
- systemctl 经常遇到的真实场景
- 服务起不来,只会
systemctl status xxx - 需要写一个开机启动脚本,不想每次都要手动操作
- 想控制某程序在后台自动重启
- 服务起不来,只会
- 过去的方案(init.d、rc.local)为什么不够用了?(引入并行启动 的概念)
- 在
systemd(systemctl是其管理工具)普及之前,Linux 主要使用 SysVinit (即/etc/init.d下的脚本)来管理服务。传统的 SysV init 是串行的(一个接一个启动)效率极低,导致系统启动缓慢。 - 而 systemd 是并行的,
systemd会分析服务之间的依赖关系,没有依赖冲突的服务会同时启动。这大大缩短了 Linux 的启动时间。 - 可以提到 systemd 不仅管理服务,还管理挂载点、Socket 等资源,是系统的"大管家"(没有理解啥意思)。有管理接口统一,日志集中,启动快可控等优势。
- 在
二、systemctl 与 systemd 的关系(核心概念扫盲)
很多新手(甚至老手)经常把 systemctl 与 systemd 混为一谈,这很正常,如果你想真正搞懂,先要理解三个核心概念:systemd 、systemctl 和 Unit。简单来说:
- systemd是后台核心。
- systemctl 是systemd的控制接口。
- Unit是被管理的对象。
2.1 什么是 systemd?
-
systemd 是:
Linux系统启动后运行的第一个进程,内核加载完后,第一个运行的就是它。PID永远是1(可通过
ps -p 1 -o pid,comm,cmd查看),systemd 是第一个用户态进程,作为老祖宗进程,他肩负着巨大责任:-
负责启动后续所有的系统服务
-
服务管理:时刻监控着系统的各种服务(如 Nginx、MySQL),确保它们按需运行。
-
资源编排:它不仅管服务(service),还管挂载点(mount)、网络设备(socket)、定时任务(timer)等。
-
服务管理器
-
资源与依赖编排工具,负责回收孤儿进程。
-
-
systemd 的设计哲学:
并行启动,按需激活。不像老前辈SysVinit那样死板的按顺序执行脚本,而是通过依赖关系智能调度。
2.2 systemctl 是什么?
搞清楚了systemd后,systemctl 就很好理解了:
- systemctl ≠ systemd
- systemctl = systemd 的控制接口**(CLI 工具)**
systemd是一个运行在后台的守护进程,你无法直接跟守护进程对话,你需要通过systemctl这个命令行工具向守护进程发送指令。
2.3 Unit 是什么?
Unit 是systemd世界里最重要的概念,在systemd眼中,一切被管理的资源都被抽象成了一个Unit。Systemd 使用单位文件(Unit Files)来描述和管理系统服务、挂载点、套接字、计时器等系统资源。每个单位文件(Unit Files)包括了一个服务的启动,停止和重启的定义,以及一类关系和执行条件等信息。这些单位文件通常存储在 /etc/systemd/system/ 或 /lib/systemd/system/ 目录下。下面第四节会详细介绍 Unit files 文件的书写格式。
-
/usr/lib/systemd/system/(或/lib/systemd/system/)上面这俩目录的内容是一样的。
- 身份:系统默认目录
- 谁在管理:软件包管理器 (如
yum,apt,rpm),当你安装 Nginx、MySQL 等软件时,官方自带的.service文件会被包管理器自动安装到这里。 - 注意:这些软件是软件维护者提供的出厂设置,禁止修改 这里的文件(一旦你升级了软件,比如
apt update,系统会重新覆盖这个目录下的文件,你辛苦修改的配置就全部丢失了。)
/etc/systemd/system/(高优先级)
-
身份:管理员自定义目录(用户特权区)。
-
谁在管理:你(系统管理员)
-
特点:你可以在此处放自己写的 Unit 文件,该目录下的单位文件不会因为
apt update软件升级而被覆盖。如果该目录下出现了与/usr/lib/systemd/system/目录同名的 Unit 文件,那么systemd会无条件使用/etc目录下的 Unit 单位文件。
三、最常用的 systemctl 基本命令(入门必会)
掌握了概念,接下来就是"真刀真枪"的实战了。这一节我们只讲最高频、最实用的命令。
3.1 查看服务状态
当你想知道一个服务怎么样时,第一反应肯定是:
shell
systemctl status xxx #(注:xxx的.service 后缀通常可以省略,systemctl 会自动补全)
如果你觉得上面的命令输出太多不好处理,可以用:
shell
systemctl is-active xxx # 自动化运维常用,适合配合 if 语句做判断。
我们拿systemctl status sshd举个例子介绍一下怎么看输出:
Active:active (running):服务活得好好的。inactive (dead):服务挂了或者没启动。failed:出错了!这是排障的第一步。
Loaded:- 这里会告诉你配置文件是从哪里加载的(通常是
/usr/lib...或/etc...),以及是否enabled(开机自启动)。
- 这里会告诉你配置文件是从哪里加载的(通常是
Main PID:- 服务的主进程ID,如果你想用kill命令强杀该进程,这就是目标。
- 底部的日志 :
- 这里显示了最近的几行日志。如果服务启动失败,红色的报错信息通常就在这里。
3.2 启停与重启(start / stop)
shell
systemctl start xxx # 启动
systemctl stop xxx # 停止
systemctl restart xxx # 重启(先停后启)
systemctl reload xxx # 卸载(不中断服务)
systemctl enable --now xxx # 同时实现"设置开机自启"和"立即启动",一条顶两条
!NOTE
这里有一个非常经典 的知识点:
restart和reload有什么区别?
systemctl restart xxx:先杀后启,服务会暂停中断。需要 彻底更新必须用此命令。systemctl reload xxx:平滑重载,主进程不退出,有时为了业务连续性需要用该命令,比如只是重新读取配置文件。
3.3 开机启动管理(enable/disable)
怎么保证重启后服务还能自动跑起来呢?
shell
systemctl enable xxx # 设置开机自启动(创建软连接,在启动名单文件夹里建立一个指向服务的快捷方式)
systemctl disable xxx # 取消开机自启
systemctl is-enabled xxx # 坚持是否开启开机自启动(返回 enabled 或 disabled)
!NOTE
systemctl enable xxx只是标记下一次开机自启动,并不会立即激活服务,这也是上面为啥推荐大家使用systemctl enable --now <service>的原因。
3.4 列出系统中所有服务
当你忘记某个服务的名字或者想看看系统跑着哪些服务,可以使用如下命令:
-
查看当前已经激活(Active)的服务:
shellsystemctl list-units --type=service # 只想看服务(.service 文件) -
查看所有已安装的服务(包括没激活的)
shellsystemctl list-unit-files --type=service # 只想看服务(.service 文件)该命令可以用来找服务名:
systemctl list-unit-files | grep ssh
掌握了上面的命令,你已经能应付 90% 的日常服务管理工作了。接下来,我们要进入深水区------自己写一个服务文件。
四、Service Unit 文件详解(核心重点,博客亮点)
前面我们学会了怎么用,这一节要学习写 Unit file 文件的规则,你将学会自定义任何程序的启动规则。
4.1 一个最简单的 service 示例
为了让你感受更直观,直接上手写一个简单的服务。场景:假设你有个 Python 脚本,你想让它变成一个系统服务。
-
准备脚本(随便写个死循环脚本):
创建一个文件
/opt/myapp.py:pythonimport time while True: print("我是后台服务,我还在运行...") time.sleep(5) -
编写Uint文件:
在
/etc/systemd/system/目录下创建一个名为myapp.service的文件,内容如下:ini[Unit] Description=My first Demo Service After=network.target [Service] Type=simple ExecStart=/usr/bin/python3 -u /opt/myapp.py Restart=always [Install] WantedBy=multi-user.target提示 :
-u的作用是关闭stdout/stderr缓冲,使print()立刻进入 journal。 -
立即生效:
shell# 通知 systemd 管理员更新了单元文件(Unit file),刷新一下配置 sudo systemctl daemon-reload # 启动并设置开机自启 sudo systemctl enable --now myapp # 查看状态 sudo systemctl status myapp
如果一切正常,你会看到服务变成了 active (running),他的日志被 systemd 统一收进了 journald (可通过sudo journalctl -u myapp.service命令查看打印),后面会讲到。
4.2 Unit 文件怎么写
上门文件虽然简单,但是已经包含了systemd配置文件的三大核心板块,我们可以把它想象成一份员工入职说明书。
[Unit]:介绍我是谁,我依赖谁Description:对服务的简短描述,当你输入systemctl status myapp时打印的就是它。After / Requires:依赖关系,需要在被依赖的服务后启动,比如有些服务需要依赖web,就得写After=network.target。
[Service]:介绍怎么干活Type=:服务类型,坑最多,后面4.3小节讲解。ExecStart=:启动命令,因为systemd启动时环境变量很少,所以**必须要写绝对路径**。ExecStop=:定义服务停止命令,systemd停服务时,用你指定的方式"收尾",比如ExecStop=/usr/bin/kill -9 myapp,当你的程序本来就可以优雅推出时(不需要特殊清理逻辑),可以不写。Restart=:重启策略always:只要不是管理员systemctl stop手动叫停的,无论程序是正常退出的(exit code 0)、还是异常崩溃的(exit code 1)、还是被信号杀掉的,它都会重启。on-failure:只有当程序崩溃时才自动重启,如果程序自己跑完任务正常退出后不自动重启。no:不自动重启,即使进程异常退出也不管。
User / Group=:指定运行的用户,建议不要用root用户而是指定一个普通用户。Environment=:可以向ExecStart / ExecStop注入环境变量,后面会提到用法。
[Install]:介绍怎么安装,决定了服务在什么场景下被启用WantedBy:依附目标multi-user.target:此处与systemctl enable myapp有配合,当你执行该命令时,systemctl会去读取myapp.service文件,然后寻找[Install]段。意思是"当系统进入多用户模式(也就是正常的命令行模式)时,把我加载进去"。命令执行后systemctl会在/etc/systemd/system/multi-user.target.wants/这个目录下,创建一个指向myapp.service的软连接。如果[Install]被删了,则执行enable时就会报错。
4.3 Unit 文件中 [Service] 字段 Type 的区别(很多坑)
Type告诉systemd如何判断"服务已经启动成功了"。
-
Type=simple默认最推荐:- 行为:
systemd认为ExecStart启动的那个进程就是主进程,它会一直霸占前台运行。 - 适用:现代程序、Docker 容器、或者不会自动后台化的脚本。
- 坑点:如果你的程序启动后自动后台化了(比如
nohup或&),systemd会认为主进程退出了,从而认为服务启动失败,甚至把他杀掉。
- 行为:
-
Type=forking传统守护进程:- 行为:
systemd认为程序启动时会分身(fork),父进程退出了,子进程还在后台运行,systemd回去追踪那个子进程。 - 适用:老式的软件,他们习惯启动后把自己扔到后台。
- 使用这种类型时,最好配合
PIDFile=参数,告诉systemd子进程的 PID 写在哪里,否则它可能找不到进程。
- 行为:
-
Type=oneshot一次性任务:-
行为:程序执行完就退出了。
systemd不会认为它"挂了",而是认为它"完成任务了"。 -
适用:初始化脚本,任务备份等。
-
技巧:配合
RemainAfterExit=yes使用,这样即使脚本跑完了,systemctl status依然会显示它是active (exited)的。当你想跑初始化脚本时这样写:iniType=oneshot ExecStart=/opt/setup.sh RemainAfterExit=yes
-
👉 避坑总结:
- 如果你写的脚本是死循环,或默认在前台跑,则用
simple。 - 如果你的脚本里用了
nohup ... &让程序后台跑,要么去掉nohup改用simple,要么把类型改为forking。 - 如果你只是想开机跑个初始化脚本 ,跑完就结束,需要用
oneshot。
五、写一个真实可用的开机服务(实战)
待补充,我还没合适的应用场景。
六、systemctl + journald:日志debug
写好了服务文件,也启动了,但是服务有bug刚启动就退出了,咋办?新手会狂敲systemctl status xxx然后盯着三行命令发呆,老手都知道,线索藏在journalctl里。systemd自带强大日志系统journalctl,他把所有服务的标准输出(stdout/stderr)都收集起来,学会这个你就能调试服务了。(老版本sysVinit中使用syslogd)
6.1 journalctl 基本用法
假设你的服务叫 myapp.service,下面介绍其基本用法:
shell
journalctl -u # 看所有服务揉在一起的日志
journalctl -u myapp # 只看 myapp 服务的日志,显式所有log,很多很多。
journalctl -u myapp -f # -f:follow 模式,日志会像直播一样实时显示在终端里。显式最近10条。
journalctl -u myapp -b # 代表 boot,只看当前这次启动周期内的日志。但我看还是很多log啊
journalctl -u myapp -b --no-pager # 一次性把当前启动所有日志打印处理(说实话我没理解这个当前启动)
!NOTE
如果你是通过
sudo权限才让编辑/etc/systemd/system/文件的单位文件,那么上面的命令需要加上sudo权限。
6.2 常见debug套路
-
服务启动失败或秒退
- 现象:
systemctl status显示failed。 - 排查步骤:
systemctl status myapp看退出码,然后再journalctl看日志找原因。
- 现象:
-
environment 变量缺失
-
现象:你在终端里手动运行脚本好好的,但放到
systemd里就报错说"command not found"或"配置加载失败"。 -
原因:
systemd启动服务时,环境变量非常干净(甚至可以说是简陋),没有你熟悉的$PATH。 -
解决:在 Unit 文件的
[Service]段里显式指定环境变量(用全局路径可能可以减少此类问题):ini[Service] Environment="PATH=/usr/local/bin:/usr/bin:/bin" Environment="MY_APP_CONFIG=/etc/myapp/config.yaml"
-
七、进阶:targets / timers / 依赖关系(选读)
到这,你已经掌握了90%的systemctl的用法,下面可能用不到,但一但遇到复杂场景(比如服务器开机黑屏、定时任务不执行、服务启动顺序错乱),它们就是你的救命稻草。
7.1 target 是什么?
在老版本的 Linux(SysVinit)中,有一个概念叫"运行级别"(Runlevel),比如 Runlevel 3 是多用户文本模式,Runlevel 5 是图形界面模式。systemd 用 Target 取代了 Runlevel。你可以把 Target 理解为 "系统状态里程碑"。
- Target 不是一个具体的服务,而是一个服务的集合
- 当系统启动到某个 Target 时,意味着这个 Target 包含的所有服务(以及依赖的服务)都应该已经启动了。
最常用的两个Target:
multi-user.target(对应 Runlevel 3)
- 含义:多用户命令行模式。
- 场景 :这是大多数服务器的标准状态。系统启动,网络通了,SSH 能连了,但没有图形桌面界面。
graphical.target(对应 Runlevel 5)
- 含义:图形界面模式。
- 场景 :它不仅包含了
multi-user.target的所有功能,还额外启动了显卡驱动、显示管理器(GDM/KDM)等。这是你个人电脑(Ubuntu Desktop/CentOS GUI)的默认状态。
常用命令:
-
查看当前默认启动目标:
shellsystemctl get-default -
修改默认启动目标(比如服务器想省资源,不想进图形界面):
shellsudo systemctl set-default multi-user.target
7.2 timer:替代 crontab 的现代方案
cron 大家都很熟悉(熟悉个der,运维才经常用这个命令,crontab 是 Linux 里最传统的"定时执行任务"的机制),但它在 systemd 时代显得有些"原始"。systemd 提供了 Timer 单元,用来替代定时任务。
systemd timer 可以精确到秒甚至毫秒。timer 可以直接用 journalctl 看日志。systemd timer 支持 Persistent=true,开机后会自动补跑错过的任务。总结起来就是比cron更精确,更便捷。
怎么使用?
Timer 需要成对出现:一个 .service(干什么)和一个 .timer(什么时候干)。
示例:每 5 分钟执行一次备份脚本
-
定义任务 (
backup.service):ini[Uint] Description=Run Backup Script [Service] Type=oneshot ExecStart=/opt/scripts/backup.sh -
定义时间 (
backup.timer)('.'前名字要一样哦):ini[Unit] Description=Run Backup Every 5 Mins [Timer] OnBootSec=5min OnUnitActiveSec=5min Persistent=true [Install] WantedBy=timers.target -
启动:
shellsudo systemctl enable --now backup.timer
7.3 服务依赖与启动顺序控制
排序:在 init.d 时代,服务启动顺序是靠脚本文件名前面的数字(S01, S02...)决定的,非常僵化,而在systemd 中,我们通过 Unit 文件的 [Unit] 字段中的After和Before来排序。
依赖:在systemd 中,我们通过 Unit 文件的 [Unit] 字中的Requires和Wants来精确控制依赖。
排序(Ordering):谁先谁后?
After=network.target:意思是"等 网络好了,再启动我"。Before=xxx.service:意思是"我必须在 xxx 之前启动"。- 注意 :
After仅仅管顺序,不管死活。如果network.target启动失败了,配置了After的服务依然会尝试启动。
依赖(Dependency):没你不行?
Requires=postgresql.service:强依赖 。如果数据库启动失败,我的服务也别想启动(直接报错退出)。Wants=redis.service:弱依赖 。我会尝试启动 Redis,但如果 Redis 挂了,我的服务依然会继续启动。
简单例子:对于 Web 应用来说,最稳妥的配置通常是:
ini
[Unit]
Description=My Web App
# 强依赖数据库,如果数据库挂了我也挂死
Requires=postgresql.service
# 弱依赖缓存,缓存挂了我也能跑
Wants=redis.service
# 必须等网络和数据库都起来了,我才能动
After=network.target postgresql.service
有了该配置,你就不需要再写复杂的Shell脚本在ExecStart 里用 while 循环去检查数据侧端口是否正常了,------systemd 帮你搞定了一切。
八、我在使用 systemctl 中踩过的坑(个人经验总结)
讲完理论,最后讲一些实战容易翻车的细节,希望这些血泪史能帮你避坑。
8.1 忘了 daemon-reload:最经典的"假死"现场
场景 :新手最常犯的错误,你兴冲冲地修改了 /etc/systemd/system/myapp.service,把端口从 8080 改到了 9090。然后执行 systemctl restart myapp。发现改没改都一样,为啥?
原因 :systemd 为了性能,会把 Unit 文件配置缓存到内存,你修改了硬盘上的文件,但是内存的配置还是旧的。
解决办法 :养成肌肉记忆,修改文件后,必须先执行:sudo systemctl daemon-reload。
8.2 ExecStart 写 Shell 脚本但没加 Type=oneshot
场景 :你想启动一个复杂的 Java 应用,于是写了个 start.sh 脚本来组装命令。
[Service]
ExecStart=/opt/app/start.sh
结果服务启动后瞬间变成 inactive (dead)。
原因 :Shell 脚本的执行机制是:启动 -> 执行命令 -> 退出。对于 Type=simple(默认值为simple),systemd 认为 ExecStart 指定的进程必须一直活着。脚本执行完就退出了,systemd 以为服务挂了,于是把它标记为停止。
解决 :如果必须用脚本,且脚本只是初始化一下就跑完了,记得加上 Type=oneshot 和 RemainAfterExit=yes。
8.3 环境变量和 PATH 问题("明明手动能跑,服务就跑不了")
场景 :你在终端里 ./myapp 跑得飞起,但放到 systemd 里就报错 command not found 或者 file not found。
原因:
- PATH 不同 :
systemd启动的环境非常纯净,它的$PATH变量通常只有/usr/local/bin:/usr/bin:/bin。如果你脚本里用了相对路径,或者命令在/opt/local/bin下,它就找不到。 - 环境变量缺失 :你在
.bashrc里配置了JAVA_HOME或LD_LIBRARY_PATH,但systemd并不会加载.bashrc。
解决:
- 绝对路径 :在
ExecStart里永远写绝对路径(如/usr/bin/python3而不是python3)。 - 显式声明 :在 Unit 文件里通过
Environment手动指定环境变量如:Environment="PATH=/usr/local/bin:/usr/bin:/bin:/opt/myapp/bin",Environment="JAVA_HOME=/usr/lib/jvm/java-11"
8.4 U-Boot / Embedded Linux 等场景下没用 systemd
场景 :如果你是在做嵌入式开发(比如基于 ARM 的板子),可能会遇到 systemctl 命令报错:
System has not been booted with systemd as init system (PID 1).
原因:
- 很多精简版的 Linux(如 Alpine, OpenWrt)或者嵌入式系统,为了省资源,默认使用
SysVinit、OpenRC或BusyBox init,而不是systemd。 - 即使安装了
systemd包,如果内核启动参数(cmdline)里指定了init=/bin/sh,systemd也不会作为 PID 1 启动。
解决:
- 确认 PID 1 是谁:
ps -p 1 -o comm=。 - 如果不是
systemd,那就别折腾systemctl了,老老实实写/etc/init.d/脚本吧。
九、什么时候得用 systemd ?啥时候不适合用?
虽然 systemd 现在是 Linux 发行版的主流,但它并不是万能的。作为一个专业的系统工程师,要知道它的边界在哪里。什么时候必须要用,什么时候不适合使用。
适合:
- 长期运行的守护进程 :比如 Web 服务器(Nginx)、数据库(MySQL)、应用服务。
systemd的自动重启、资源限制和日志收集功能,是管理这类服务的最佳拍档。 - 有复杂依赖关系的场景 :如果你的服务启动依赖于网络、磁盘挂载或者其他服务,
systemd的After、Requires等依赖管理功能,能帮你省去写复杂 Shell 脚本的麻烦。
不适合:
- 纯一次性任务 :虽然可以用
Type=oneshot,但如果你的脚本只是开机跑一次就完事,完全不需要守护和重启,那么传统的/etc/rc.local或者简单的 init 脚本可能更轻量、更直接。 - 极简或容器化系统 :
- 嵌入式/精简版 Linux :像 Alpine Linux、OpenWrt 或者基于 BusyBox 的系统,它们的设计哲学是"小而美",通常使用
OpenRC或BusyBox init。在这些环境下强行安装systemd既困难又没必要。 - Docker 容器 :容器的最佳实践是"一个容器一个进程"。在 Docker 容器里跑
systemd会增加不必要的复杂度和资源开销(虽然技术上可行),通常我们更推荐直接在前台启动主进程。
- 嵌入式/精简版 Linux :像 Alpine Linux、OpenWrt 或者基于 BusyBox 的系统,它们的设计哲学是"小而美",通常使用
总结 :工具没有好坏,只有适不适合。systemctl 是现代服务器的利器,但在极简环境下,别强求它。
十、总结及 systemctl 推荐阅读
虽然这篇博客覆盖了核心用法,但 systemd 的博大精深远超于此。如果你想继续深入,建议按照以下路径探索:
- 入门 :熟练使用
systemctl管理现有服务,习惯看journalctl日志。 - 进阶 :尝试编写自定义
.service文件,理解Type=simple和oneshot的区别。 - 高阶 :研究
.timer替代 Crontab,使用.socket实现按需启动,或者配置cgroup限制服务的 CPU/内存使用。
推荐进一步阅读:
最好的文档永远是官方文档(Man Pages)。当你遇到不懂的参数时,直接查阅手册:
man systemd.service:这是最核心的手册,详细解释了[Service]字段里每一个参数的含义(如RestartSec,Environment等)。man systemd.unit:了解 Unit 文件的通用格式和依赖逻辑,(如 讲解了Before,After等的用法)。man journalctl:探索更多日志过滤和导出的技巧。
希望这篇博客能成为你手边的一本小册子,在下次面对各种service时,让你更有底气。祝你的服务永远 active (running)!