☰
Ubuntu service 与 systemctl:服务启停、自启与排错
2026/10/1 13:19:45 网站建设 项目流程

1. 把 Ubuntu 服务管理这件事先说清楚

刚接触 Ubuntu 的人几乎都会在同一个地方卡一次:装完 Nginx 或者 MySQL,想启动它,敲了sudo service nginx start,屏幕一闪什么都没输出,然后心里就没底了——到底起来了没有?再敲一次stop,还是什么都不说。这种"沉默的命令行"是很多新手最不适应的地方。这篇文章就把service这条命令从头到尾拆开讲,包括它在 Ubuntu 上到底是什么、底层转发给了谁、启动/关闭/重启/重载分别在做什么、什么场景该用哪个、以及真正踩过的那些坑。

service命令在 Ubuntu 上的角色,其实比很多人想象的复杂。它不是"Ubuntu 原生的服务管理工具",而是一个兼容层。Ubuntu 15.04 之前的系统用 Upstart 做 1 号进程,之后全面切换到了 systemd。但大量老教程、老脚本、老运维习惯里写的都是service xxx start这种写法,为了不把这些东西全部打断,Ubuntu 保留了一个/usr/sbin/service的 shell 脚本,它负责把请求转发到当前系统真正在用的初始化系统上。所以你敲的service,在今天的 Ubuntu 上,十有八九最后执行的是systemctl。

理解了这一点,后面很多事情就通了。为什么service nginx enable会报错?因为service这个脚本压根没实现 enable 这个动作,它只转发 start/stop/restart/reload/status 这几个。为什么service和systemctl有时行为不一致?因为转发逻辑里会先看/etc/init.d/下有没有同名脚本,有的话优先按 SysV 方式跑,没有才去找 systemd 单元。这条分支决定了你写的服务到底以哪种方式被拉起,值得记一下。

这篇文章的目标读者很明确:刚上手 Ubuntu 服务管理的开发者、需要在自己机器或服务器上部署应用的人、以及被service: unrecognized service报错卡住的运维新人。内容从命令语法讲到自建服务,再到故障排查速查表,全部是我自己在实际机器上跑过的流程,尽量给到可以直接复制粘贴的程度。

2. service 命令的语法结构与底层转发逻辑

2.1 一条命令的完整构成拆解

service的语法非常朴素,就两种形式:

service <服务名> <动作> service --status-all

第一部分是服务名,注意这里不带 .service 后缀,写的是nginx不是nginx.service。第二部分是动作,常见的有:

动作含义是否所有服务都支持
start启动服务是
stop停止服务是
restart停止后再启动是
reload重载配置,不中断进程否,取决于服务自身实现
status查看运行状态是
force-reload强制重载,必要时才重启否

--status-all是一个特殊参数,它会遍历系统里所有能识别的服务,逐个调用它们的 status 动作,然后汇总输出。输出格式长这样:

[ + ] cron [ - ] apache2 [ ? ] alsa-utils

三个符号的含义必须记牢,这是新手最容易误读的地方:

  • [ + ]表示服务正在运行
  • [ - ]表示服务已停止
  • [ ? ]表示状态未知,通常是这个服务的 status 动作没有返回值,或者脚本本身写得不够规范,不代表它一定有问题,也不代表它一定没问题

很多人看到一堆[ ? ]就慌了,以为系统坏了。实际上 Ubuntu 里有相当多的 SysV 风格脚本本来就没实现规范的 status 返回码,[ ? ]是常态。想看准确状态,还是得回到systemctl去查。

另外提一句,service --status-all执行起来非常慢,因为它是一个串行循环,逐个调用脚本。在装了几十个服务的机器上等十几秒很正常。如果你只是想快速看几个关键服务的状态,别用这个命令。

2.2 转发逻辑:service 到底调用了谁

打开/usr/sbin/service这个文件,你会看到它本质上是一堆 shell 函数。核心判断逻辑大致是这样的:

  1. 先检查/etc/init.d/<服务名>这个文件是否存在且可执行;
  2. 如果存在,就按传统 SysV 方式执行/<path>/<服务名> <动作>;
  3. 如果不存在,就去 /lib/systemd/system 或 /etc/systemd/system 里找同名的.service单元,找到就转发给systemctl;
  4. 两个都找不到,直接报unrecognized service。

这个顺序很关键。假设你在/etc/init.d/下手动放了一个叫nginx的脚本,那么即使系统里同时存在 systemd 的nginx.service,service nginx start也会优先跑你那个脚本。这种"两套东西同名打架"的情况在迁移老项目时非常常见,症状是启动看起来成功了,但systemctl status nginx显示 inactive,两边状态对不上。

想知道自己系统上跑的到底是哪一套,一条命令就能确认:

ps -p 1 -o comm=

输出systemd就是 systemd 系统,输出init就是老式 SysV。现在能见到的 Ubuntu(16.04 以后)基本都是前者。

确认某个服务走哪条路径,可以这样查:

ls -l /etc/init.d/ | grep 服务名 systemctl list-unit-files | grep 服务名

哪个存在,基本就是走哪条路。两条都存在的话,按前面的规则,/etc/init.d/优先。

2.3 service 与 systemctl 的对应关系对照表

既然底层转发给了 systemctl,那把两套命令对照起来看会省很多事。下面这张表是我自己整理的常用对照,写脚本时按场景挑就行:

需求service 写法systemctl 写法说明
启动service nginx startsystemctl start nginx等价
停止service nginx stopsystemctl stop nginx等价
重启service nginx restartsystemctl restart nginx等价,进程会重新起
重载service nginx reloadsystemctl reload nginx不断连接,前提是服务支持
状态service nginx statussystemctl status nginxsystemctl 信息更全
开机自启不支持systemctl enable nginxservice 没这个动作
取消自启不支持systemctl disable nginx同上
是否自启不支持systemctl is-enabled nginx同上
彻底屏蔽不支持systemctl mask nginx比 disable 更狠
列出所有service --status-allsystemctl list-units --type=service后者更快更准

看到这里其实就有个结论了:新写的东西一律用 systemctl,service 只在维护老脚本时用。这不是喜新厌旧,而是因为 service 缺了 enable/disable/mask 这一整块能力,而开机自启几乎是每个生产服务都绕不开的需求。我见过不少人用service把服务跑起来了,重启机器之后发现服务没了,回头到处找原因,其实就是不知道自启要另外配。

3. 启动、关闭、重启、重载:四个动作到底做了什么

3.1 start 与 stop 背后的进程生命周期

start做的事情,本质上是让 systemd 按照服务单元文件里的ExecStart字段,fork 出一个进程,并且把它纳入这个服务的 cgroup 里管理。所谓 cgroup,你可以理解成给进程画了个圈,圈里所有进程的生死都由 systemd 统一负责。这个机制带来一个很实用的副作用:服务启动后它派生出来的子进程,也会被算在这个服务名下。所以用systemctl status nginx看到那一串进程树,都是同一次 start 拉起来的。

stop则相反,systemd 会向 cgroup 里的进程发信号。默认先发SIGTERM,也就是礼貌地请它退出,给进程一个清理现场的机会。如果超时时间(默认TimeoutStopSec=90s)内还没退,就补一个SIGKILL强杀。这个设计很合理,但也会带来一个经典现象:stop 命令敲下去后卡住不返回,就是因为它在那儿等进程自己退,等满 90 秒才动手强杀。

实操建议,遇到 stop 卡住的情况,别急着 Ctrl+C 或者开新终端去 kill。先看一眼目标进程是不是在做什么收尾工作,比如把内存里的数据刷盘。数据库服务尤其如此,MySQL 停止时会做 buffer pool 的落盘,强行打断有可能造成数据文件不一致。真要加速,可以调单元文件里的TimeoutStopSec,但这属于"我明确知道自己在干什么"才该动的参数。

start还有一个容易被忽略的点:它不负责确认服务真的能用了。systemd 只保证进程起来了、没立刻退出,业务层面的可用性它管不了。所以正确姿势是start之后紧跟一个status,或者直接 curl 一下本地端口,确认服务真的对外可用了再去干别的。

3.2 restart 与 reload 的本质区别

这两个动作经常被混用,但差别非常大,选错了可能造成线上短暂不可用。

restart是杀掉旧进程,重新拉起新进程。这个过程中的一小段时间里,服务是不可用的。对于 Nginx 这种秒起的服务,可能只丢几十毫秒,用户感知不到;但对于一个要加载几个 G 索引的服务,重启可能就是几分钟的事。

reload是向正在运行的进程发送一个信号(通常是SIGHUP),让它自己去重新读取配置文件,进程本身不退出、不重启。理论上连接不会断,内存里的缓存也能保留。Nginx 的 reload 就是这么工作的:主进程收到信号后,按新配置起一批新的 worker,然后优雅地把老 worker 关掉,整个过程对外几乎无感。

但这个"好"是有前提的:

注意:不是所有服务都实现了 reload。有些服务单元里压根没写ExecReload字段,这时候执行 reload 会直接报错,或者被 systemd 当成 restart 处理。执行前先确认一下服务支不支持,最稳妥的做法是查单元文件里有没有 ExecReload。

systemctl cat nginx | grep ExecReload

有输出说明支持 reload,没输出就老老实实用 restart。

我自己的习惯是:改配置优先 reload,改代码或者依赖版本才 restart。这个判断标准简单好用,能避开大部分无谓的服务中断。

3.3 一条完整的实操演示

下面把整套流程走一遍,用 Nginx 举例。假设你刚apt install nginx完,服务默认已经起来了,我们把它停掉再重新走一遍。

先确认状态:

sudo service nginx status

输出里会看到Active: active (running),以及主进程 PID 和一串 worker 进程。

停掉它:

sudo service nginx stop

这次再查状态,会变成Active: inactive (dead)。

启动回来:

sudo service nginx start

改配置后重载(先验证语法再 reload,这个顺序很重要):

sudo nginx -t sudo service nginx reload

nginx -t会检查配置文件语法,输出syntax is ok和test is successful才算过。这一步千万别省,语法错了直接 reload,Nginx 会保持旧配置不变,你以为生效了其实没有,排查半天才发现是配置文件写错了。更坑的是重启场景,语法错了 restart 会直接失败,服务停在那儿起不来,这时候如果没有旧进程兜底,就是实打实的服务中断。

重启:

sudo service nginx restart

设成开机自启(这一步 service 做不到):

sudo systemctl enable nginx

验证是否设成功:

sudo systemctl is-enabled nginx

输出enabled就对了。反过来disable就是取消自启。

4. 把自写程序注册成系统服务

4.1 为什么要把程序做成服务

很多人写了个 Python 脚本或者 Go 二进制,习惯用nohup ./app &挂在后台。这种方式能用,但问题一堆:机器重启后进程没了;进程崩了没人拉起来;日志要自己重定向;想停止只能靠ps找 PID 然后 kill。这些事在开发和测试阶段无所谓,一旦到了需要长期稳定运行的场景,就全是隐患。

做成 systemd 服务之后,上面这些问题一次性解决:开机自启、崩溃自动重启、日志统一进 journald、启停用统一命令。代价只是写一个十几行的配置文件,非常划算。

4.2 单元文件的完整写法与逐字段解释

服务单元文件放在/etc/systemd/system/下,文件名就是服务名,比如myapp.service。

[Unit] Description=My Demo Application Documentation=https://example.internal/myapp After=network-online.target Wants=network-online.target [Service] Type=simple User=www-data Group=www-data WorkingDirectory=/opt/myapp Environment=PYTHONUNBUFFERED=1 Environment=APP_ENV=production ExecStart=/usr/bin/python3 /opt/myapp/app.py ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=3 TimeoutStopSec=20 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target

几个字段值得单独说清楚,这些是最容易配错的地方。

Type决定了 systemd 怎么判断服务"启动完成"。选错了会直接导致启动超时或者假死:

  • simple:ExecStart 一条命令直接跑在前台,不 fork。绝大多数自己写的应用都是这种,包括 Node、Python、Go 编译出的二进制。
  • forking:程序自己 fork 到后台,父进程退出。Nginx、PHP-FPM 这类传统守护进程是这个类型。用 simple 去配它们会导致 systemd 以为进程退出了,反复重启。
  • oneshot:执行一次就结束,适合初始化脚本、挂载动作这类。
  • notify:程序通过 sd_notify 主动通知启动完成,最精确但需要程序配合改造。

选错 Type 的典型症状是服务反复重启,journalctl里满屏的 Started/Stopped 记录。

Restart=on-failure是我最常用的策略,意思是进程以非 0 退出码结束、被信号杀死、或者超时的时候自动拉起。RestartSec=3是重启前等 3 秒,避免疯狂重启把 CPU 打满。需要注意的是,如果服务是被systemctl stop主动停的,on-failure不会把它拉起来,这正是我们想要的。

Environment和User这两项在日常排错里出现频率极高。忘记设 User 的话,服务以 root 身份跑,安全上是减分项;设了 User 但工作目录权限不对,服务会因为权限不足起不来。我的经验是新建一个专用低权限用户,把工作目录和日志目录的属主给它,比直接拿 www-data 凑合更清晰。

4.3 从零到跑起来的完整步骤

写完文件之后,按顺序执行这几步,缺一步都容易出问题:

# 1. 修正文件权限,避免被 systemd 警告 sudo chmod 644 /etc/systemd/system/myapp.service # 2. 关键一步:让 systemd 重新扫描单元文件 sudo systemctl daemon-reload # 3. 启动 sudo systemctl start myapp # 4. 查看状态,重点看最后几行日志 sudo systemctl status myapp # 5. 设置开机自启 sudo systemctl enable myapp

第 2 步是新手最常漏的。每次新增或修改/etc/systemd/system/下的文件,都必须执行daemon-reload,否则 systemd 还在用内存里的旧版本。漏了这一步的症状是:文件明明改了,systemctl start却报Unit myapp.service not found,或者行为跟改之前一模一样,让人怀疑人生。

还有一个习惯值得养成:不要直接改/lib/systemd/system/里的原始文件。那个目录属于软件包管理的范围,apt upgrade时你的修改会被覆盖掉。想改系统自带服务的配置,用 override 机制:

sudo systemctl edit nginx

这会在/etc/systemd/system/nginx.service.d/下生成一个override.conf,你写进去的内容会叠加在原始配置之上,升级也不会丢。想完全接管,用systemctl edit --full nginx,它会复制一份完整配置到/etc/systemd/system/下让你随便改。

4.4 老式 SysV 脚本方式(了解即可)

在一些老项目或者特定发行版里,你还会看到/etc/init.d/下的 shell 脚本。结构上就是接收start/stop/restart这几个参数,然后自己实现对应的操作,通常配合一个 PID 文件来记录进程号。开机启动靠update-rc.d管理:

sudo update-rc.d myapp defaults sudo update-rc.d -f myapp remove

这种写法今天已经不建议新项目使用了,主要是因为它不依赖 cgroup,进程跑丢了 systemd 根本不知道,也没法自动拉起。只有在维护十年前的遗留系统时才值得花时间研究。

5. 服务操作中的典型故障与排查思路

5.1 unrecognized service 的三类成因

这是service命令最高频的报错,看到它先按下面三条排查:

第一,服务名拼错了。service mysql status和service mysqld status是两个不同的名字,Ubuntu 上装 MySQL 一般是mysql,某些发行版是mysqld。不确定的话先列出所有单元名找找:

systemctl list-unit-files --type=service | grep -i mysql

第二,服务压根没装或者没装全。比如你只装了客户端没装服务端,自然找不到对应单元。

第三,单元文件存在但没被识别。这种情况通常是刚创建完文件没执行daemon-reload,执行一次就好。

5.2 启动失败但看不到任何报错的排查路径

service命令有个很讨厌的特性:启动失败时经常什么都不输出。这时候不要瞎猜,直接上systemctl status:

sudo systemctl status myapp --no-pager -l

--no-pager避免输出被翻页程序截断,-l表示完整显示长行不省略。输出里最关键的是最后那几行日志,服务的真实报错都在那里。

如果 status 里的信息还不够,去翻 journal:

# 看最近 50 行 sudo journalctl -u myapp -n 50 --no-pager # 实时跟踪,类似 tail -f sudo journalctl -u myapp -f # 只看本次开机之后的 sudo journalctl -u myapp -b # 按时间过滤 sudo journalctl -u myapp --since "30 min ago"

顺便说一下 journal 日志的持久化问题。默认情况下,Ubuntu 的 journald 日志是存在内存里的,重启就没了。想看历史日志,得先开启持久化:

sudo mkdir -p /var/log/journal sudo systemd-tmpfiles --create --prefix /var/log/journal sudo systemctl restart systemd-journald

之后日志会落到/var/log/journal/下,重启也能查。查日志占了多少磁盘用journalctl --disk-usage,清理用journalctl --vacuum-size=500M。

5.3 端口占用、权限不足、配置语法错误

这三类占了服务启动失败原因的绝大多数,值得单独列出来。

端口被占是最常见的。现象是服务启动后立刻退出,日志里出现Address already in use或者bind: address already in use。查是谁占的:

sudo ss -tlnp | grep :8080 # 或者 sudo lsof -i :8080

ss是netstat的现代替代品,速度更快,Ubuntu 新版本默认都装了。找到 PID 之后,先确认那个进程是不是你需要的,别上来就 kill。有时候是自己上次启动的残留进程没退干净,那确实该清理;有时候是另一个业务在正常使用这个端口,那就得改配置换端口。

权限不足的症状是日志里出现Permission denied,通常发生在几个地方:工作目录不可读、配置文件属主不对、要绑定的端口小于 1024(Linux 上普通用户不能绑 1024 以下的端口)。解决办法要么是给对应权限,要么是配置里加AmbientCapabilities=CAP_NET_BIND_SERVICE让非 root 用户也能绑低端口,这比直接拿 root 跑安全得多。

配置语法错误相对好办,因为大多数成熟软件都提供了自检命令:

服务语法检查命令
Nginxsudo nginx -t
Apachesudo apachectl configtest
MySQLsudo mysqld --validate-config
SSHsudo sshd -t
PHP-FPMsudo php-fpm -t

养成"先检查再重载"的习惯,能省掉大量排查时间。

5.4 常见问题速查表

把前面这些整理成一张表,出问题的时候对着查会更高效:

现象可能原因排查命令处理方式
unrecognized service名字错/未安装/未 daemon-reloadsystemctl list-unit-files | grep 名字修正名字或执行 daemon-reload
启动后立刻退出Type 选错journalctl -u 服务 -n 50simple 与 forking 互换试
反复重启Restart 策略 + 程序崩溃systemctl status 服务先修程序本身,再考虑 Restart
stop 卡住不返回进程收尾慢systemctl status 服务等待或调 TimeoutStopSec
端口被占残留进程或其他服务ss -tlnp | grep :端口清理或换端口
Permission denied用户/目录/端口权限ls -l 相关目录调整属主或用 capability
改了配置没生效忘记 reload 或 daemon-reload—补执行对应命令
status 显示[ ? ]SysV 脚本无返回码systemctl is-active 服务用 systemctl 判断

6. 几个我实际踩过的坑和习惯养成

6.1 重启服务前,先问自己三个问题

第一个问题:这个服务现在有没有活跃连接?如果是个对外提供接口的服务,restart 的那几秒里新请求会失败。能 reload 就别 restart。判断有没有连接可以用ss -tn state established | grep :端口 | wc -l数一下。

第二个问题:我改的东西必须重启才能生效吗?很多配置项是支持热加载的,先翻翻文档再动手,能少一次中断就少一次。

第三个问题:出问题了怎么回滚?改配置之前先备份一份,cp nginx.conf nginx.conf.bak.20240101,这行命令花不了两秒,但能救命。我见过太多次改完配置重启失败、服务直接躺平,然后在一堆报错里手忙脚乱地找原来那行写的是什么。

另外,如果是在远程 SSH 会话里操作关键服务,建议先在 tmux 或 screen 里跑。万一网络抖一下断连,普通会话里的命令会被中止,可能停在一个不上不下的状态。tmux 里跑就安全得多,重连回来接着看输出。

6.2 批量操作与脚本化的小技巧

需要一次处理多个服务的时候,写个小循环比手动敲快得多:

#!/bin/bash services=(nginx mysql docker redis-server) for svc in "${services[@]}"; do if systemctl is-active --quiet "$svc"; then echo "$svc 运行中" else echo "$svc 未运行,尝试启动" sudo systemctl start "$svc" && echo " 启动成功" || echo " 启动失败" fi done

systemctl is-active --quiet这个组合很实用,它只返回退出码不输出内容,特别适合放在条件判断里。返回值 0 表示运行中,非 0 表示没运行,比去 grep status 的文本输出可靠多了。

同样的模式可以用来批量重启:

for svc in nginx php8.1-fpm; do sudo systemctl reload "$svc" || sudo systemctl restart "$svc" done

先试 reload,不支持再退回 restart,这个写法在改完一批服务的配置后特别好用。

6.3 关于 mask 与防火墙的两个提醒

systemctl mask这个操作值得单独提一下,因为它比disable彻底得多。disable只是取消开机自启,服务仍然可以手动启动;mask会在/etc/systemd/system/下建一个指向/dev/null的软链接,让这个服务彻底无法被启动,无论是手动还是被其他服务依赖触发:

sudo systemctl mask 服务名 sudo systemctl unmask 服务名

这个功能在"某个服务总是被别的服务意外拉起,但我现在确实不想要它"的场景下很有用。但它也是有副作用的,mask 掉的服务如果被别的东西强依赖,可能导致依赖方也起不来。用之前先想清楚。

还有一个常见误区是为了让服务能被外部访问就去关掉防火墙。这种做法我强烈不建议。正确的做法是放行需要的端口,规则明确、可追溯:

sudo ufw status sudo ufw allow 80/tcp sudo ufw allow 443/tcp

只开放真正需要的端口,比整片拆掉防护墙要稳妥得多。服务连不上有很多原因,防火墙只是其中之一,别把它当成第一嫌疑人。先用ss -tlnp确认服务确实在监听,再用curl 127.0.0.1:端口从本机测一下,本机通、外部不通,再去查防火墙和云平台安全组。

6.4 后续可以这样往下深挖

如果这篇内容对你来说大部分都还新鲜,那下一步最值得花时间的是把 systemd 的依赖关系搞明白。After=、Requires=、Wants=这几个字段决定了服务之间的启动顺序和依赖强度,写多服务应用的时候绕不开。特别是Wants=和Requires=的区别——前者是"希望它在",它起不来我也照跑;后者是"必须有它",它起不来我就不跑——这个差别在排查"某个服务莫名起不来"时经常是根因。

再往后就是资源的隔离与限制,CPUQuota=、MemoryMax=、IOWeight=这几个字段可以给服务加上资源配额,防止一个服务吃光整台机器。这个在单机跑多个服务的时候尤其有用,配置起来也就几行的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询