现在的 Linux 服务器,几乎所有的服务都由 systemd 管理:Nginx、MySQL、Redis、Docker、你自己写的程序,开机自启、崩溃自动拉起、日志集中查看,全都靠它。但很多人只会 systemctl start/stop/restart 三板斧,遇到服务起不来就一头雾水——status 显示 failed 却看不出原因,日志不知道去哪找,自己写的脚本想做成服务又不知从哪下手。
这篇文章把 systemd 讲透:它和传统 init 有什么区别、单元(unit)有哪些类型、systemctl 和 journalctl 的常用命令、怎么给自己的程序写一个 unit 文件、三节结构(Unit/Service/Install)各字段的含义、怎么用 systemctl edit 做安全覆盖,以及服务失败时怎么一步步定位原因。看完你应该能独立地把任意一个程序做成 systemd 服务,并在它出问题时快速找到症结。
systemd 和传统 init 的区别
老式的 SysV init 靠一串 shell 脚本(/etc/init.d/ 下的那些)按顺序启动服务,用的是"运行级别"(runlevel)的概念。它的毛病是:串行执行、启动慢、依赖关系靠脚本自己处理、管不住进程(父进程退出后子进程可能变成孤儿)。
systemd 是这几年的标准,核心变化:
| 维度 | SysV init | systemd |
|---|---|---|
| 启动方式 | 串行执行脚本 | 按依赖并行启动 |
| 配置 | shell 脚本 | 声明式 unit 文件 |
| 状态 | runlevel 0-6 | target |
| 进程管理 | 不跟踪子进程 | cgroup 跟踪,可管理整个进程树 |
| 日志 | 各服务自行写文件 | 统一 journald 收集 |
| 依赖 | 手写 | 显式声明 After/Requires/Wants |
systemd 用 cgroup 把服务及其所有子进程圈进一个组里,所以 systemctl stop 能干净地停掉整个进程树,不会留下孤儿进程。它还统一了日志(journald)、定时任务(timer)、挂载(mount)、socket 激活等,是一整套系统和服务管理器。
验证当前是不是 systemd:
ps -p 1 -o comm=
# 输出 systemd 即为 systemd 系统
systemctl --version
单元类型:service、target、socket、timer
systemd 把一切都抽象成"单元(unit)",常见的类型:
| 单元后缀 | 类型 | 作用 |
|---|---|---|
.service |
服务 | 一个后台进程,如 nginx.service |
.target |
目标 | 一组单元的集合,相当于运行级别 |
.socket |
套接字 | 端口/socket 激活,收到请求才拉起服务 |
.timer |
定时器 | 替代 cron 的定时任务 |
.mount |
挂载 | 文件系统挂载点 |
.device |
设备 | 内核识别的设备 |
.path |
路径 | 监控文件/目录变化触发 |
.slice |
切片 | cgroup 资源分组 |
查看某类单元:
systemctl list-units --type=service
systemctl list-units --type=service --all # 含未激活的
systemctl list-units --type=timer
systemctl list-unit-files --type=service # 所有已安装的 unit 文件
target 是 systemd 的运行级别概念,和传统 runlevel 的对应关系:multi-user.target 对应 runlevel 3(多用户命令行),graphical.target 对应 runlevel 5,rescue.target 是单用户救援,poweroff.target/reboot.target 是关机和重启。
systemctl get-default
systemctl list-units --type=target
systemctl list-dependencies multi-user.target | head -n 30
systemctl 常用命令
这是日常使用最频繁的一组命令,务必记熟:
# 启停与重启
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx # 停再起,会断开现有连接
sudo systemctl reload nginx # 重载配置不停服务(需服务支持)
sudo systemctl reload-or-restart nginx
# 开机自启
sudo systemctl enable nginx # 开机自启
sudo systemctl disable nginx # 取消开机自启
sudo systemctl enable --now nginx # 设置开机自启并立即启动
# 查看状态
systemctl status nginx # 最常用,看运行状态和最近日志
systemctl is-active nginx # 是否在运行 active/inactive
systemctl is-enabled nginx # 是否开机自启
systemctl is-failed nginx
systemctl show nginx # 查看所有属性(脚本友好)
# 配置变更后
sudo systemctl daemon-reload # 修改 unit 文件后必须执行
# 查看依赖
systemctl list-dependencies nginx
systemctl list-dependencies --reverse nginx
systemctl status 是最重要的诊断命令,输出包含:服务状态(active/inactive/failed)、主进程 PID、所属 cgroup、以及最近的几行日志。看到 failed 时,这几行日志往往就指向原因。
daemon-reload 的坑:修改了 unit 文件(或 systemctl edit 生成的覆盖文件)后,必须执行 sudo systemctl daemon-reload 让 systemd 重新读取配置,之后再 restart 服务。很多人改了 unit 却没 reload,发现改动没生效,就是这个原因。
journalctl:查看服务日志
systemd 用 journald 收集日志,用 journalctl 查看。查服务日志最常用的几条:
# 查看某服务的日志
journalctl -u nginx
journalctl -u nginx -n 50 # 最近 50 行
journalctl -u nginx -f # 实时跟踪(类似 tail -f)
journalctl -u nginx --since today
journalctl -u nginx --since "2026-10-12 10:00:00"
journalctl -u nginx --since "1 hour ago"
journalctl -u nginx -p err # 只看错误级别
# 结合启动次数
journalctl -u nginx -b # 只看本次启动
journalctl -u nginx -b -1 # 上次启动(排查"上次为什么挂了")
# 服务失败的详细原因(含提示)
journalctl -u nginx -x # 附加解释性文字
journalctl -xeu nginx # 三者合一的常用组合
# 内核日志
journalctl -k
-x 会给日志加上解释性说明,帮助理解错误;-e 直接跳到日志末尾(配合 -u 使用即"看该服务的最后日志")。所以日常排障最爱用的组合是:
sudo systemctl status nginx
sudo journalctl -xeu nginx
journal 的持久化:默认日志可能存在内存(重启丢失)或 /var/log/journal/(持久化)。想让日志重启后还在:
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
# 限制日志占用空间
sudo journalctl --disk-usage
sudo journalctl --vacuum-size=500M
sudo journalctl --vacuum-time=30d
写一个自定义 unit 文件
把自研程序做成 systemd 服务,需要写一个 .service 文件。位置分两类:
/usr/lib/systemd/system/:软件包安装的 unit,不要手改,升级会覆盖;/etc/systemd/system/:管理员自定义或覆盖的 unit,优先级更高,自己写的服务放这里。
一个 unit 文件分三节:[Unit](元信息和依赖)、[Service](怎么跑)、[Install](怎么被启用)。可以先用 systemd-analyze verify 校验。
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application Service
Documentation=https://example.internal/myapp
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment="JAVA_HOME=/usr/lib/jvm/java-17"
EnvironmentFile=-/etc/myapp/myapp.env
ExecStartPre=/opt/myapp/bin/precheck.sh
ExecStart=/opt/myapp/bin/myapp --config /etc/myapp/config.yml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
[Unit] 节常用字段
| 字段 | 含义 |
|---|---|
Description |
服务描述,systemctl status 里显示 |
After |
在这些单元之后启动(只定顺序,不建依赖) |
Before |
在这些单元之前启动 |
Requires |
强依赖,被依赖者失败则本服务也停 |
Wants |
弱依赖,被依赖者失败不影响本服务 |
Conflicts |
互斥,启动本服务会停掉对方 |
After 和 Requires 要分清:After 只管启动顺序,不保证对方一定启动;Requires 才建立"必须"的依赖。想"等网络就绪再启动",通常写 After=network.target,需要更严格的可用网络则用 Wants=network-online.target 配合 After=network-online.target。
[Service] 节常用字段
| 字段 | 含义 |
|---|---|
Type |
服务类型:simple(默认)、forking、oneshot、notify、exec |
ExecStart |
启动命令(必填,通常只能一条) |
ExecStop |
停止命令(可选) |
ExecReload |
重载命令,systemctl reload 触发 |
User/Group |
以哪个用户/组运行(强烈建议别用 root) |
WorkingDirectory |
工作目录 |
Environment |
环境变量 |
EnvironmentFile |
从文件读环境变量,- 前缀表示文件不存在也不报错 |
Restart |
何时自动重启:no、on-failure、always、on-abnormal |
RestartSec |
重启前等待秒数 |
LimitNOFILE |
文件描述符上限(systemd 服务的关键配置) |
Type 的选择很关键:
| Type | 适用场景 |
|---|---|
simple |
前台运行的进程(大多数现代应用,如 java -jar) |
forking |
会自己 fork 到后台的传统守护进程(如老式服务) |
oneshot |
执行一次就退出的任务(如初始化脚本) |
notify |
支持 sd_notify 协议的进程,就绪后才算启动完成 |
exec |
类似 simple,但等 exec 完成才算启动 |
如果进程是前台运行的(不自己 daemonize),用 simple;如果它自己 fork 后台、父进程退出,要用 forking,否则 systemd 会误以为服务退出了。
[Install] 节
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target 表示执行 systemctl enable 时,把本服务挂到 multi-user.target 下,实现开机自启。Alias 可以给服务起别名。
启用自定义服务:
# 1. 校验 unit 文件语法
sudo systemd-analyze verify /etc/systemd/system/myapp.service
# 2. 让 systemd 读取
sudo systemctl daemon-reload
# 3. 设为开机自启并启动
sudo systemctl enable --now myapp
# 4. 查看状态
systemctl status myapp
journalctl -u myapp -f
用 systemctl edit 做覆盖
想修改已安装服务的行为,不要直接改 /usr/lib/systemd/system/ 里的原始文件(升级会被覆盖)。正确做法是用 systemctl edit:
sudo systemctl edit nginx
这会创建一个 drop-in 覆盖文件 /etc/systemd/system/nginx.service.d/override.conf,你只需写要改的部分:
[Service]
LimitNOFILE=65535
Environment="TZ=Asia/Shanghai"
保存后:
sudo systemctl daemon-reload
sudo systemctl restart nginx
systemctl show nginx -p LimitNOFILE # 验证生效
drop-in 的合并规则:同一节里,后面的设置覆盖前面的(除了像 ExecStart= 这种会累加的,需要先清空——写一个空的 ExecStart= 再写新值)。要丢弃自己的覆盖,删掉那个 drop-in 文件再 daemon-reload 即可回滚。
故障诊断:服务起不来怎么办
服务失败时,按下面的顺序排查,绝大多数问题都能定位。
第一步:看状态和最近日志
systemctl status myapp
journalctl -xeu myapp
status 输出里的 Active: 一行说明状态(failed 就是失败),下面的日志往往直接给出原因,比如 Exec format error(脚本没有 shebang 或不可执行)、No such file or directory(路径写错)、Permission denied(权限问题)、Address already in use(端口被占)。
第二步:理解常见失败原因
| 现象 | 常见原因 | 排查命令 |
|---|---|---|
Exec format error |
脚本缺 shebang、没执行权限、架构不符 | head -1 /path/script、chmod +x |
No such file or directory |
ExecStart 路径错、二进制依赖库缺失 | ls -l、ldd /path/bin |
Permission denied |
运行用户无权访问文件/端口 | namei -l /path、检查 User= |
Address already in use |
端口被其他进程占用 | ss -tlnp | grep 端口 |
启动后立刻 inactive |
Type 选错(程序会 fork 但用了 simple) | 改 Type=forking |
一直 activating |
程序没真正就绪但用了非 notify | 看程序日志,或改 Type |
第三步:区分是 systemd 的问题还是程序自身的问题
手动以服务配置的用户和路径跑一遍命令,看是不是程序本身启动不了:
# 用服务里的用户手动执行(模拟 systemd 环境)
sudo -u myapp /opt/myapp/bin/myapp --config /etc/myapp/config.yml
如果手动跑也失败,就是程序或配置的问题,跟 systemd 无关;手动跑正常但 systemd 起不来,那多半是环境变量、工作目录、权限或 Type 配置的差异。systemd 服务默认的环境变量比登录 shell 少(特别是 PATH),这是常见坑——服务里 PATH 不含某个二进制路径,导致 ExecStart 找不到命令。解决办法是用绝对路径,或在 unit 里显式设置 Environment=PATH=...。
第四步:查看 cgroup 里实际启动了哪些进程
systemctl status myapp
# 里的 CGroup 段会列出该服务下的所有进程
systemctl show myapp -p MainPID -p ActiveState -p SubState
第五步:让失败原因更明确
加 -x 拿到解释性提示,journalctl -u myapp -b 看本次启动的完整日志。如果日志被截断,可以调 journald 的配置或让程序日志落到 journal 之外。
常见问题
修改了 unit 文件,为什么重启服务没变化?
改了 unit 文件(或 drop-in)后必须执行 sudo systemctl daemon-reload,让 systemd 重新加载配置,然后再 restart。只 restart 不 daemon-reload 是无用的。
reload 和 restart 有什么区别?
reload 是让服务重新读取配置,不中断现有连接/进程(前提是服务支持并有 ExecReload);restart 是彻底停掉再启动,会断开连接、有短暂不可用。改配置优先用 reload,改二进制或依赖用 restart。
自己写的服务老是启动后立刻退出(inactive)?
先看 journalctl -u 服务 的实际报错。最可能是 Type 选错:如果程序自己 fork 到后台、前台进程退出,应该用 Type=forking,否则 systemd 会认为它退出了。也可能程序确实启动失败了,用前面说的"手动以服务用户跑一遍"来区分。
服务的环境变量为什么和登录 shell 里不一样?
systemd 服务不读取 /etc/profile、~/.bashrc,它只有系统默认的少量变量。所以要用绝对路径,或用 Environment=、EnvironmentFile= 显式设置。这个差异经常导致"手动能跑、服务不能跑"。
想让服务崩溃后自动拉起,怎么配?
在 [Service] 里设 Restart=on-failure 和 RestartSec=5。on-failure 只在非正常退出时重启,always 则在任何退出(包括正常退出)后都重启。注意配合 StartLimitIntervalSec/StartLimitBurst 防止疯狂重启。
总结
- systemd 用 cgroup 跟踪进程树、用声明式 unit 文件、用 target 代替 runlevel,比传统 init 更快、更可管;
multi-user.target是服务器最常用的目标。 - 日常记牢
systemctl start/stop/restart/reload/enable/disable/status/is-active/daemon-reload,改 unit 后一定要daemon-reload再重启。 - 日志用
journalctl -u 服务 -f、-xeu、-b -1查看,-xeu是排障首选;启用 journal 持久化并做空间限制,避免日志丢失或撑爆磁盘。 - 自定义服务写
.service文件放/etc/systemd/system/,三节结构 Unit/Service/Install,Type选对是关键(前台程序用 simple,会 fork 的用 forking),优先禁用 root、用User=降权。 - 修改已装服务用
systemctl edit生成 drop-in 覆盖,别改原始文件;服务起不来时先status+journalctl -xeu,再手动以服务用户执行来区分是 systemd 还是程序自身的问题。
企业QQ咨询




