系统启动流程与 systemd 服务管理
本文合并自:1-8-Linux系统服务与日志管理.md、02-服务管理与Shell环境.md
本文覆盖从按下电源到出现登录提示符的完整启动链路,以及两套服务管理体系:CentOS 6 的 SysVinit(service / chkconfig / 运行级别)与 CentOS 7 及以上的 systemd(systemctl / Unit / target)。两套体系并列讲解、逐项标注适用版本,便于在混合环境中对照使用。
相关内容的交叉引用:
- 日志系统(
rsyslog、journalctl详解、logrotate、常见日志文件)见08-02-日志管理.md。 - GRUB2 配置、内核启动参数、Swap 与引导修复见
03-02-Swap与系统引导.md、05-03-内核编译与系统启动过程.md。 at/cron定时任务见进程与任务管理相关文档;本文只讲 systemd 的.timer,并给出与 cron 的对照。- Shell 环境变量与配置文件的完整讲解见
06-01-Shell脚本编程.md;本文只在"登录"阶段列出会被读取的文件清单。
一、系统启动流程
1.1 启动阶段总览
通电
→ 1. BIOS / UEFI 自检(POST),初始化硬件,按启动顺序查找启动设备
→ 2. 读取 MBR(BIOS+MBR 分区表)或 ESP 分区(UEFI+GPT),加载引导加载器
→ 3. GRUB2 显示菜单,读取 /boot/grub2/grub.cfg,加载内核与 initramfs
→ 4. 内核解压自身,挂载 initramfs 作为临时根文件系统
→ 5. 内核初始化:探测硬件、加载驱动、挂载真正的根分区(/etc/fstab)
→ 6. 内核启动第一个用户态进程(PID 1):CentOS 6 为 init,CentOS 7+ 为 systemd
→ 7. 初始化系统读取配置:/etc/inittab + /etc/rc.d/rcN.d(SysVinit)
或 default.target + Unit 依赖树(systemd)
→ 8. 按运行级别 / target 启动目标服务,最后执行 /etc/rc.local
→ 9. 启动 getty / 显示管理器,出现登录提示符,用户登录
源文档中给出的简化速记(含 Upstart 时代的 /etc/init/*.conf,Ubuntu 系较常见):
通电
→ BIOS/UEFI(主板内置程序)
→ GRUB(/boot/grub/grub.cfg)
→ Linux 内核(/boot/vmlinuz*)+ 临时文件系统(initrd*)
→ 挂载根分区(/etc/fstab)
→ systemd(第一个用户态进程)
→ /etc/init/*.conf
→ rc-sysinit.conf # 定义运行级别
→ rcS.conf
→ rc.conf # 开机服务:/etc/rc*.d/*
→ lightdm.conf # 图形界面
→ rc.local # 用户自定义脚本(最后执行)
→ 用户登录
/etc/init/*.conf属于 Upstart 的配置格式(Ubuntu 14.04 及以前、RHEL 6 的部分服务)。CentOS 6 使用纯 SysVinit +/etc/inittab,CentOS 7 起使用 systemd,均不再使用/etc/init/。
1.2 各阶段的作用与产物
| 阶段 | 主要动作 | 产物 / 结果 |
|---|---|---|
| 1. BIOS / UEFI 自检 | 上电自检(POST),检测 CPU、内存、显卡、磁盘等关键硬件;初始化设备;按 Boot Order 选择启动设备 | 硬件可用;确定启动设备;把控制权交给引导程序 |
| 2. MBR / GPT + 引导加载器 | BIOS 模式:读取磁盘第 0 扇区 512 字节的 MBR,其中前 446 字节为引导代码(GRUB stage1),64 字节为分区表,2 字节结束标志。UEFI 模式:读取 GPT 上的 ESP(EFI System Partition,FAT32,通常挂载在 /boot/efi),执行 /EFI/centos/grubx64.efi |
引导加载器被载入内存并运行 |
| 3. GRUB2 加载内核 | 读取 /boot/grub2/grub.cfg(UEFI 为 /boot/efi/EFI/centos/grub.cfg),显示启动菜单,支持在菜单按 e 临时编辑内核参数、按 c 进入命令行;根据菜单项加载内核与 initramfs 到内存,并传递内核参数(如 root=、rhgb quiet、single、systemd.unit=) |
内核文件 /boot/vmlinuz-* 与 /boot/initramfs-*.img 载入内存 |
| 4. initramfs | 内核挂载 initramfs(CentOS 6 早期叫 initrd)作为临时根文件系统,内含真正挂载根分区所需的驱动与工具(LVM、RAID、加密卷、特殊 SCSI/NVMe 驱动等),由 dracut 生成 |
具备访问真实根分区的能力,避免"驱动在根分区里、根分区又需要驱动才能挂载"的死循环 |
| 5. 内核初始化 | 解压内核、初始化内存管理与进程调度、探测硬件并加载驱动、按 /etc/fstab 以只读方式挂载根分区并随后重挂为读写、切换根(switch_root)丢弃 initramfs |
内核完全就绪;真实根文件系统在 / 上可用;内核检测信息记入内核环形缓冲区(dmesg / /var/log/dmesg) |
| 6. 第一个进程 init / systemd | 内核执行 /sbin/init,即 PID 1。CentOS 6 指向 SysVinit 的 init;CentOS 7+ 是指向 /lib/systemd/systemd 的符号链接。PID 1 是所有进程的祖先,负责收养孤儿进程 |
用户态启动开始;ps -p 1 -o comm= 可确认当前是 init 还是 systemd |
| 7. 初始化系统读取配置 | CentOS 6:读 /etc/inittab 得到默认运行级别 → 执行 /etc/rc.d/rc.sysinit 做系统初始化 → 执行 /etc/rc.d/rc N。CentOS 7+:读 /etc/systemd/system/default.target(符号链接)→ 解析 Unit 依赖树 |
确定要进入的运行级别 / target;构建启动作业队列 |
| 8. 启动目标服务 | CentOS 6:按 /etc/rc.d/rcN.d/ 中 KXXname 顺序停服务、SXXname 顺序起服务(串行)。CentOS 7+:按依赖关系并行拉起 Unit。最后执行用户自定义脚本 /etc/rc.local |
网络、sshd、crond 等服务进入运行状态 |
| 9. 登录 | 级别 3 / multi-user.target:由 getty(systemd 为 getty@tty1.service)提供字符登录界面。级别 5 / graphical.target:启动显示管理器(gdm / lightdm)提供图形登录。用户通过 PAM 认证后读取 Shell 配置文件 |
出现登录提示符;登录成功后得到交互式 Shell 或图形桌面 |
判断当前系统属于哪一套体系的最快方法:
ps -p 1 -o comm= # 输出 init → CentOS 6;输出 systemd → CentOS 7+ cat /etc/redhat-release # 查看发行版版本
1.3 登录阶段读取的环境配置文件
用户登录成功后,Shell 会按顺序读取以下文件(完整讲解见 06-01-Shell脚本编程.md):
| 文件 | 作用 |
|---|---|
/etc/profile |
任何用户登录时执行(全局环境变量) |
/etc/profile.d/*.sh |
/etc/profile 调用的额外脚本,各软件在此注入自己的环境变量 |
/etc/bashrc(Debian 系为 /etc/bash.bashrc) |
终端启动全局配置(全局别名、Shell 选项) |
~/.bash_profile(Debian 系常用 ~/.profile) |
当前用户登录时执行一次 |
~/.bashrc |
当前用户每次打开交互式终端时执行 |
~/.bash_logout(源文档写作 ~/.bash.logout) |
当前用户登出时执行 |
登录 Shell 的读取顺序:
/etc/profile
└─> /etc/profile.d/*.sh
└─> ~/.bash_profile
└─> ~/.bashrc
└─> /etc/bashrc
1.4 图形界面自动启动配置(级别 5 / graphical.target)
图形登录后自动运行程序的两个入口:
/etc/xdg/autostart/*.desktop/etc/X11/xinit/xinitrc.d/*.sh
.desktop 示例:
[Desktop Entry]
Type=Application
Name=KyoTerminal
Comment=kyo autostart terminal
Icon=preferences-desktop-screensaver
Exec=gnome-terminal
注意:
.desktop文件必须具有可执行权限才会生效。
1.5 启动相关目录速查
| 路径 | 说明 | 适用版本 |
|---|---|---|
/boot/vmlinuz-* |
内核文件 | 通用 |
/boot/initramfs-*.img |
临时根文件系统镜像(旧称 initrd) |
通用 |
/boot/grub/grub.conf、/boot/grub/menu.lst |
GRUB Legacy 配置 | CentOS 6 |
/boot/grub2/grub.cfg |
GRUB2 配置(BIOS 引导) | CentOS 7+ |
/boot/efi/EFI/centos/grub.cfg |
GRUB2 配置(UEFI 引导) | CentOS 7+ |
/etc/inittab |
定义默认运行级别 | CentOS 6 |
/etc/rc.d/rc.sysinit |
系统初始化脚本 | CentOS 6 |
/etc/init.d/(等价于 /etc/rc.d/init.d/) |
SysV 服务启动脚本目录 | CentOS 6,CentOS 7 兼容保留 |
/etc/rc.d/rc0.d ~ rc6.d(软链为 /etc/rc0.d ~ /etc/rc6.d) |
各运行级别的服务链接目录 | CentOS 6 |
/etc/rc.d/rc.local(软链为 /etc/rc.local) |
开机最后执行的自定义脚本 | 通用(CentOS 7 需手动加执行权限) |
/etc/init/*.conf |
Upstart 启动配置目录(旧版 / Ubuntu 系) | 非 CentOS 6/7 主线 |
/usr/lib/systemd/system/ |
软件包自带的 Unit 文件 | CentOS 7+ |
/etc/systemd/system/ |
管理员自定义的 Unit 文件 | CentOS 7+ |
1.6 与启动、登录相关的查询命令
在线用户与开机时间:
who # 查看在线用户
who -b # 最后一次系统启动时间
who -aH # 列出当前用户详细信息(带表头)
who -r # 查看当前运行级别
向在线用户发送消息(重启前通知很常用):
write soul /dev/pts/5 # 给指定用户发消息,Ctrl+D 结束
wall # 给所有用户发消息,Ctrl+D 结束
mesg n # 关闭接收消息
mesg y # 开启接收消息
echo "Message content" > /dev/pts/5 # 即使对方关闭 mesg 也能发送
登录与开关机历史:
last # 所有用户登录/登出时间
last -F # 打印完整时间
last -x | grep -E '^(shutdown|reboot)' # 查看开机关机时间
lastb # 登录失败记录(默认仅 root 可见)
lastlog # 用户最近一次登录记录(不显示图形界面登录)
这些命令读取的是二进制日志文件,权限受限:
ls -l /var/log/btmp
# -rw-------. 1 root utmp 6528 May 9 19:26 /var/log/btmp
/var/log/wtmp、/var/log/btmp、/var/log/lastlog等日志文件的完整说明见08-02-日志管理.md。
二、SysVinit 体系(CentOS 6 / RHEL 6)
RHEL 6 系列使用 SysV init 管理服务,服务启动脚本统一放在 /etc/init.d/(与 /etc/rc.d/init.d/ 等价)。其特点是串行启动:按编号顺序一个一个执行脚本,前一个不结束后一个不开始,因此开机较慢。
2.1 /etc/inittab 与默认运行级别
CentOS 6 的 /etc/inittab 只保留一行有效配置,用于指定默认运行级别:
cat /etc/inittab
# id:3:initdefault: # 早期写法
# 或 CentOS 6 中的简化写法:
# initdefault: 3
修改默认运行级别(把 3 改成 5 即默认进图形界面):
vi /etc/inittab
# 将 id:3:initdefault: 改为 id:5:initdefault:
CentOS 7 中
/etc/inittab只剩说明文字,修改无效,必须用systemctl set-default(见 3.5 节)。
2.2 七个运行级别(runlevel 0-6)
| 级别 | 说明 |
|---|---|
| 0 | 关机(不要设为默认级别,否则开机即关机) |
| 1 | 单用户模式(维护模式,不启动网络与多用户服务,常用于重置 root 密码) |
| 2 | 多用户模式,通常无网络(无 NFS) |
| 3 | 多用户模式,有网络,无图形界面(服务器最常用) |
| 4 | 预留,一般未使用(用户可自定义) |
| 5 | 多用户模式,有网络,有图形界面 |
| 6 | 重启(不要设为默认级别,否则无限重启) |
2.3 运行级别切换与查询
init 3 # 切换到运行级别 3(字符界面)
init 5 # 切换到运行级别 5(图形界面)
init 0 # 关机
init 6 # 重启
runlevel # 查询当前运行级别,输出 "N 3" 表示上一级别为 N(无)、当前为 3
who -r # 也可以查询运行级别
2.4 /etc/rc.d/rcN.d 目录与 SXXname / KXXname 命名规则
init 进入级别 N 时会执行 /etc/rc.d/rc N,该脚本遍历 /etc/rc.d/rcN.d/ 下的符号链接:
ls -l /etc/rc.d/rc3.d/
# lrwxrwxrwx. 1 root root 15 K15httpd -> ../init.d/httpd
# lrwxrwxrwx. 1 root root 16 S10network -> ../init.d/network
# lrwxrwxrwx. 1 root root 17 S55sshd -> ../init.d/sshd
命名规则:首字母 + 两位数字 + 服务名。
| 组成 | 含义 |
|---|---|
K |
进入该级别时执行 脚本 stop,即开机不启动(Kill) |
S |
进入该级别时执行 脚本 start,即开机启动(Start) |
| 两位数字 | 执行顺序,数字小的先执行(K 与 S 各自独立排序,先跑完所有 K 再跑 S) |
| 服务名 | 与 /etc/init.d/ 下的脚本同名,链接指向该脚本 |
例如 S10network 先于 S55sshd 启动,符合"网络先起、sshd 后起"的依赖关系。这些链接由 chkconfig 自动维护,不建议手工创建。
不同发行版的开机自启管理工具:
发行版 工具 示例 CentOS 6 chkconfigchkconfig --level 5 服务名 off/onCentOS 7 systemctlsystemctl disable 服务名/systemctl enable 服务名Ubuntu sysv-rc-conf—
2.5 /etc/rc.local
/etc/rc.local(实体文件为 /etc/rc.d/rc.local)是启动流程中最后执行的脚本,用于放置不便写成服务的自定义开机命令:
vi /etc/rc.local
# 在末尾追加
/usr/local/myapp/bin/start.sh
/usr/sbin/ntpdate ntp.aliyun.com
版本差异:
- CentOS 6:
/etc/rc.d/rc.local默认有执行权限,写入即生效。 - CentOS 7+:
rc-local.service仍然存在,但/etc/rc.d/rc.local默认没有执行权限,必须手动赋权后才会被执行:
chmod +x /etc/rc.d/rc.local
systemctl status rc-local.service # 确认服务被拉起
rc.local属于遗留兼容手段:没有状态管理、没有失败重启、没有独立日志。CentOS 7 上应优先编写 service Unit(见第四节)。
2.6 service 命令(CentOS 6)
以 network 服务为例,有两种等价写法:
# 方式一:使用 service 命令
service 服务名 start # 启动
service 服务名 stop # 停止
service 服务名 restart # 重启(先 stop 再 start)
service 服务名 reload # 重新加载配置文件(不中断服务,需脚本支持)
service 服务名 status # 查看状态
service --status-all # 查看所有服务状态
# 方式二:直接调用脚本
/etc/init.d/服务名 start
/etc/init.d/服务名 stop
/etc/init.d/服务名 restart
/etc/init.d/服务名 status
实际示例:
sudo service network start
sudo service network stop
sudo service network restart
sudo service network status
/etc/init.d/network start
/etc/init.d/network stop
/etc/init.d/network restart
/etc/init.d/network status
说明:
service本质就是去执行/etc/init.d/下的同名脚本,并清理环境变量后传入参数。在 CentOS 7 上
service被保留为兼容命令:执行service httpd start会被重定向到systemctl start httpd.service,终端会打印Redirecting to /bin/systemctl start httpd.service。若服务仍是老式 SysV 脚本,则 systemd 通过systemd-sysv-generator自动把它转换成一个临时 Unit 来管理。因此源文档中"service命令通常调用systemctl,systemctl最终调用/etc/init.d/下的脚本"的说法,指的正是这套双向兼容机制。
2.7 chkconfig:管理开机自启(CentOS 6)
chkconfig 用于控制服务在各运行级别是否随系统启动,它的实质就是在 /etc/rc.d/rcN.d/ 下创建或删除 SXX/KXX 符号链接。
chkconfig --list # 列出所有系统服务及其各级别开关状态
chkconfig --list | grep httpd # 只查看 httpd
chkconfig --list 服务名 # 查看单个服务
chkconfig 服务名 on # 设置为开机启动(默认 2/3/4/5 级别)
chkconfig 服务名 off # 设置为开机不启动
chkconfig 服务名 on --level 23 # 仅在 2、3 级别启动
chkconfig 服务名 off --level 23 # 仅在 2、3 级别关闭
chkconfig --level 5 服务名 off # 等价写法(选项位置可变)
chkconfig --add 服务名 # 将服务加入 chkconfig 管理
chkconfig --del 服务名 # 从 chkconfig 管理中移除(脚本文件仍保留)
--list 输出示例:
httpd 0:off 1:off 2:off 3:off 4:off 5:off 6:off
network 0:off 1:off 2:on 3:on 4:on 5:on 6:off
sshd 0:off 1:off 2:on 3:on 4:on 5:on 6:off
--add/--del仅控制 chkconfig 是否"看到"该服务,不会删除/etc/init.d/下的脚本文件。--add时会按脚本头部的# chkconfig:信息自动创建各级别符号链接。
2.8 启动脚本中的 chkconfig 注释头
要让自己的脚本被 chkconfig 管理,脚本头部必须包含专用注释行:
# chkconfig: - 85 15
三段含义:
| 字段 | 含义 |
|---|---|
- |
默认在哪些运行级别启动。- 表示 2、3、4、5 级别启动;也可写具体数字,如 2345 或 35 |
85 |
开机启动顺序号(S 链接序号),数值越小越早启动 |
15 |
关机停止顺序号(K 链接序号) |
配合的第二行注释 # description: 也是必需的,否则 chkconfig --add 会报错 service xxx does not support chkconfig。
2.9 自己编写 SysV 启动脚本(模板)
把源码安装的程序纳入 CentOS 6 的服务管理,需要在 /etc/init.d/ 下写一个符合规范的脚本。
vi /etc/init.d/myapp
#!/bin/bash
#
# chkconfig: - 85 15
# description: My Application daemon
# processname: myapp
# pidfile: /var/run/myapp/myapp.pid
#
### BEGIN INIT INFO
# Provides: myapp
# Required-Start: $network $local_fs
# Required-Stop: $network $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: start and stop myapp
### END INIT INFO
# 加载函数库,提供 daemon / killproc / status / action 等函数
. /etc/rc.d/init.d/functions
PROG=myapp
EXEC=/usr/local/myapp/bin/myapp
PIDFILE=/var/run/myapp/myapp.pid
LOCKFILE=/var/lock/subsys/$PROG
OPTS="--config /usr/local/myapp/conf/myapp.conf"
start() {
[ -x $EXEC ] || exit 5
echo -n "Starting $PROG: "
daemon --pidfile=$PIDFILE $EXEC $OPTS
retval=$?
echo
[ $retval -eq 0 ] && touch $LOCKFILE # 必须创建锁文件,否则关机时不会调用 stop
return $retval
}
stop() {
echo -n "Stopping $PROG: "
killproc -p $PIDFILE $PROG
retval=$?
echo
[ $retval -eq 0 ] && rm -f $LOCKFILE
return $retval
}
restart() {
stop
start
}
reload() {
echo -n "Reloading $PROG: "
killproc -p $PIDFILE $PROG -HUP
echo
}
case "$1" in
start) start ;;
stop) stop ;;
restart) restart ;;
reload) reload ;;
status) status -p $PIDFILE $PROG ;;
condrestart)
[ -f $LOCKFILE ] && restart ;; # 仅在服务正在运行时才重启
*)
echo "Usage: $0 {start|stop|restart|reload|status|condrestart}"
exit 2
esac
exit $?
部署与启用:
chmod 755 /etc/init.d/myapp # 必须可执行
chkconfig --add myapp # 按 chkconfig 头创建各级别链接
chkconfig --list myapp # 确认级别开关
chkconfig myapp on # 设为开机自启
service myapp start # 启动
service myapp status # 查看状态
ls -l /etc/rc.d/rc3.d/ | grep myapp # 验证生成了 S85myapp
关键点:
# chkconfig:与# description:两行注释缺一不可。- 脚本必须支持
start|stop|restart|status四个参数,退出码要正确(0 成功、非 0 失败)。 /var/lock/subsys/<服务名>锁文件必须在 start 时创建、stop 时删除,关机脚本靠它判断是否需要停止服务。- 引入
/etc/rc.d/init.d/functions才能使用daemon、killproc、status、action等标准函数,输出格式也才会带[ OK ]样式。
三、systemd 体系(CentOS 7 及以上)
3.1 设计目标与并行启动
CentOS 7 起,PID 1 由 systemd 接管(/sbin/init 是指向 /lib/systemd/systemd 的符号链接)。相比 SysVinit,systemd 的设计目标是:
| 目标 | 做法 |
|---|---|
| 并行启动、加快开机 | 用依赖关系图代替串行编号,无依赖关系的服务同时启动 |
| 按需启动 | .socket 单元先监听端口/套接字,有请求时才拉起真正的服务(取代 xinetd) |
| 减少启动阻塞 | 提前建立好所有服务的通信 socket,使互相依赖的服务可以同时启动 |
| 统一的配置语法 | 声明式 Unit 文件(ini 风格)取代几百行 Shell 脚本 |
| 精确的进程跟踪 | 用 cgroup 而不是 PID 文件跟踪服务的全部子进程,停止服务时不会漏杀 |
| 统一的日志 | 服务的 stdout/stderr 自动被 journald 收集(见 08-02-日志管理.md) |
| 状态可查、失败可重启 | systemctl status 看真实状态,Restart= 自动拉起崩溃的进程 |
| 挂载、定时、设备一体化管理 | 用 .mount、.timer、.device 等类型统一纳管,取代 fstab 之外的零散机制 |
副作用:由于并行启动,开机日志的输出顺序不再固定,排错要靠
journalctl而不是"看屏幕滚动顺序"。
3.2 Unit 的类型
systemd 把一切被管理的对象抽象为 Unit,文件后缀即类型:
| 类型 | 作用 |
|---|---|
.service |
服务(最常用),定义如何启动/停止一个程序 |
.target |
目标,一组 unit 的集合,类似运行级别(如 multi-user.target 对应级别 3) |
.socket |
套接字,实现按需启动(有连接才拉起服务) |
.timer |
定时器,替代 cron 触发任务(详见第五节) |
.mount |
挂载点,等价于 /etc/fstab 条目(systemd 会把 fstab 自动转换成 .mount 单元) |
.automount |
自动挂载点,访问时才挂载,配合同名 .mount 使用 |
.path |
路径监控,文件/目录变化时触发服务 |
.swap |
交换分区/交换文件(详见 03-02-Swap与系统引导.md) |
.slice |
资源切片,用 cgroup 树对一组 unit 统一限制 CPU/内存/IO(如 user.slice、system.slice) |
.scope |
由 systemd 外部创建的进程组(如 ssh 会话),不由 unit 文件定义 |
.device |
设备(由 udev 自动生成,可作为其他 unit 的依赖条件) |
查看当前系统上各类型 Unit:
systemctl -t help # 列出所有支持的 unit 类型
systemctl list-units --type=service # 只看 service
systemctl list-units --type=target # 只看 target
systemctl list-units --type=mount # 只看挂载点
systemctl list-units --type=timer # 只看定时器
3.3 Unit 文件的存放位置与优先级
| 目录 | 说明 | 优先级 |
|---|---|---|
/etc/systemd/system/ |
管理员自定义,自己写的 unit 放这里 | 最高 |
/run/systemd/system/ |
运行时动态生成,重启丢失 | 中 |
/usr/lib/systemd/system/ |
软件包自带(yum/rpm 安装时释放),不要直接改 | 最低 |
关键规则:同名 unit 文件,
/etc/systemd/system/会完全覆盖/usr/lib/systemd/system/中的同名文件。因此定制官方服务时,不要去改/usr/lib/systemd/system/下的文件(升级软件包会被覆盖),应使用 4.5 节的 drop-in 覆盖方式。另外
systemctl enable的本质,就是在/etc/systemd/system/<target>.wants/下创建一个指向 unit 文件的符号链接——作用等同于 2.4 节/etc/rc*.d/中的S/K链接。
drop-in 目录(局部覆盖,优先级高于 unit 文件本身):
/etc/systemd/system/<unit名>.d/*.conf # 管理员的局部覆盖,最常用
/usr/lib/systemd/system/<unit名>.d/*.conf # 软件包提供的局部覆盖
.d/ 目录下的所有 .conf 文件按文件名顺序加载并叠加,可拆成 10-limits.conf、20-env.conf 分类管理。查看最终生效结果用 systemctl cat <unit>。
3.4 systemctl 完整命令
运行控制
systemctl start httpd.service # 启动(.service 后缀可省略)
systemctl stop httpd # 停止
systemctl restart httpd # 重启(先停再起,会中断连接)
systemctl reload httpd # 重新加载服务自己的配置文件,不中断服务
systemctl reload-or-restart httpd # 支持 reload 就 reload,否则 restart
systemctl try-restart httpd # 仅在服务正在运行时才重启
systemctl kill httpd # 向服务的所有进程发信号(默认 SIGTERM)
systemctl kill -s SIGKILL httpd # 指定信号强制杀死
对照 CentOS 6 的写法:
# 启动
sudo service network start
sudo systemctl start network.service
# 停止
sudo service network stop
sudo systemctl stop network.service
# 重启
sudo service network restart
sudo systemctl restart network.service
# 查看状态
sudo service network status
sudo systemctl status network.service
状态查询
systemctl status httpd # 查看状态(含最近 10 行日志)
systemctl status httpd -l # 不截断长行
systemctl status httpd --no-pager # 不分页,便于脚本与复制
systemctl is-active httpd # 是否运行中(active/inactive/failed),适合脚本判断
systemctl is-enabled httpd # 是否已设开机自启(enabled/disabled/static/masked)
systemctl is-failed httpd # 是否处于失败状态
开机自启
systemctl enable httpd # 设为开机自启(创建 .wants 符号链接)
systemctl disable httpd # 取消开机自启
systemctl enable --now httpd # 设为自启【并立即启动】(部署时最常用)
systemctl disable --now httpd # 取消自启并立即停止
systemctl reenable httpd # 先 disable 再 enable,用于修正错乱的链接
屏蔽与解除
systemctl mask firewalld # 彻底屏蔽,任何方式都无法启动
systemctl unmask firewalld # 解除屏蔽
列表与依赖
systemctl # 不带参数:列出所有已加载且 active 的 unit
systemctl list-units # 同上,列出【已加载】的 unit
systemctl list-units --all # 包含 inactive 的
systemctl list-units --type=service # 只看 service 类型
systemctl list-units --failed # 只看启动失败的(巡检常用)
systemctl list-units --state=running
systemctl list-unit-files # 列出【磁盘上所有】unit 文件及其 enable 状态
systemctl list-unit-files --type=service --state=enabled # 只看已启用的服务
systemctl list-unit-files --state=disabled
systemctl list-dependencies httpd.service # 查看依赖树(本服务依赖谁)
systemctl list-dependencies --reverse httpd.service # 反向依赖(谁依赖本服务)
systemctl list-dependencies --all multi-user.target # 展开某 target 下的全部服务
systemctl list-sockets # 列出套接字与其触发的服务
systemctl list-timers # 列出定时器(见第五节)
配置管理
systemctl daemon-reload # 重新扫描并加载所有 unit 文件(改动 unit 后必须执行)
systemctl daemon-reexec # 重新执行 systemd 自身(升级 systemd 后使用)
systemctl reset-failed # 清除所有 failed 状态与重启计数
systemctl reset-failed httpd # 只清除指定服务(触发 StartLimit 后必须先执行才能再启动)
systemctl cat httpd # 查看最终生效的完整配置(含 drop-in 来源)
systemctl show httpd # 显示全部属性及生效值
systemctl edit httpd # 创建/编辑 drop-in 覆盖
systemctl revert httpd # 删除所有自定义覆盖,回到软件包原始状态
3.5 target 与运行级别的对应
systemd 用 target 取代运行级别,并保留了 runlevelN.target 兼容软链:
| 运行级别(CentOS 6) | systemd target(CentOS 7+) | 兼容软链 | 说明 |
|---|---|---|---|
| 0 | poweroff.target |
runlevel0.target |
关机 |
| 1 | rescue.target |
runlevel1.target |
单用户/救援模式 |
| 2 | multi-user.target |
runlevel2.target |
多用户,无图形 |
| 3 | multi-user.target |
runlevel3.target |
多用户、有网络、无图形(服务器默认) |
| 4 | multi-user.target |
runlevel4.target |
预留 |
| 5 | graphical.target |
runlevel5.target |
多用户、有网络、有图形 |
| 6 | reboot.target |
runlevel6.target |
重启 |
| — | emergency.target |
— | 紧急模式,只挂载只读根,比 rescue 更底层 |
| — | basic.target |
— | 基础系统就绪(挂载、sysctl、socket 等) |
| — | default.target |
— | 默认启动目标,是指向上述某个 target 的符号链接 |
systemd 中 2、3、4 都指向
multi-user.target,因此级别 2/3/4 的区分在 CentOS 7 上不再有意义。
查询与设置默认 target:
systemctl get-default # 查看默认 target
# graphical.target
systemctl set-default multi-user.target # 设为字符界面(等价于 CentOS 6 改 inittab 为 3)
systemctl set-default graphical.target # 设为图形界面(等价于级别 5)
ls -l /etc/systemd/system/default.target # set-default 本质就是重建这个符号链接
立即切换(不重启):
systemctl isolate multi-user.target # 切到字符界面,等价于 CentOS 6 的 init 3
systemctl isolate graphical.target # 切到图形界面,等价于 init 5
systemctl isolate rescue.target # 切到救援模式,等价于 init 1
systemctl rescue # 同上,并向已登录用户发通知
systemctl emergency # 进入紧急模式
isolate的含义是"只保留目标 target 依赖的 unit,其余全部停止",因此对正在运行的业务有影响,生产环境慎用。只有AllowIsolate=yes的 target 才能被 isolate。CentOS 7 上
init 3/init 5仍可用,systemd 会把它翻译成对应的isolate操作;runlevel、who -r也仍能输出兼容的级别号。
3.6 systemctl 电源管理
systemctl poweroff # 关机(等价 shutdown -h now / init 0)
systemctl halt # 停机但不断电
systemctl reboot # 重启(等价 shutdown -r now / init 6)
systemctl suspend # 挂起到内存(S3,唤醒快,仍耗电)
systemctl hibernate # 休眠到磁盘(S4,写入 swap,不耗电,唤醒慢)
systemctl hybrid-sleep # 混合睡眠:同时写内存与磁盘
systemctl suspend-then-hibernate # 先挂起,一段时间后转休眠(新版本支持)
传统命令在 CentOS 7 上依然可用,并且更适合需要"定时关机、通知用户"的场景:
shutdown -h now # 立即关机
shutdown -h +10 "系统将在10分钟后维护" # 10 分钟后关机并广播消息
shutdown -r 23:30 # 23:30 重启
shutdown -c # 取消已计划的关机
poweroff / halt / reboot # 直接调用
hibernate需要 swap 空间不小于物理内存,且未被resume=参数排除,详见03-02-Swap与系统引导.md。
3.7 CentOS 6 与 CentOS 7 命令对照总表
| 功能 | CentOS 6(SysVinit) | CentOS 7+(systemd) |
|---|---|---|
| 启动服务 | service httpd start / /etc/init.d/httpd start |
systemctl start httpd |
| 停止服务 | service httpd stop |
systemctl stop httpd |
| 重启服务 | service httpd restart |
systemctl restart httpd |
| 重载配置 | service httpd reload |
systemctl reload httpd |
| 查看状态 | service httpd status |
systemctl status httpd |
| 所有服务状态 | service --status-all |
systemctl list-units --type=service |
| 设开机自启 | chkconfig httpd on |
systemctl enable httpd |
| 禁开机自启 | chkconfig httpd off |
systemctl disable httpd |
| 查自启状态 | chkconfig --list httpd |
systemctl is-enabled httpd |
| 列出所有自启 | chkconfig --list |
systemctl list-unit-files --type=service |
| 指定级别自启 | chkconfig httpd on --level 35 |
无级别概念,用 WantedBy= 指定 target |
| 彻底禁止启动 | 删除脚本 / 改名 | systemctl mask httpd |
| 默认启动级别 | 改 /etc/inittab |
systemctl set-default multi-user.target |
| 查看当前级别 | runlevel / who -r |
systemctl get-default(runlevel 兼容可用) |
| 切换级别 | init 3 / init 5 |
systemctl isolate multi-user.target(init 3 兼容可用) |
| 服务脚本位置 | /etc/init.d/ |
/usr/lib/systemd/system/、/etc/systemd/system/ |
| 自启实现机制 | /etc/rc.d/rcN.d/SXX 软链 |
<target>.wants/ 软链 |
| 自定义开机命令 | /etc/rc.local(默认可执行) |
写 Unit 文件;用 rc.local 需 chmod +x |
| 服务日志 | 各服务自己的日志 + /var/log/messages |
journalctl -u httpd + rsyslog |
四、编写 service Unit 文件(CentOS 7+)
第三节的 systemctl start/stop/enable 操作的都是已存在的 unit。当需要把自己的程序(Tomcat、Redis、Java 应用、Python 脚本)纳入 systemd 管理时,就要自己编写 unit 文件——这是取代"改 startup.sh""往 /etc/rc.local 追加命令"等临时方案的正规做法。
4.1 .service 文件结构详解
一个 .service 文件由 [Unit]、[Service]、[Install] 三个段组成,以下为带完整注释的模板:
[Unit]
# ---------- 描述与依赖关系 ----------
Description=My Application Service # 一句话描述,systemctl status 中显示
Documentation=man:myapp(8) https://example.com/doc # 文档地址(可选)
After=network.target mariadb.service # 启动顺序:在这些 unit 之后启动(仅管顺序,不管成败)
Before=nginx.service # 启动顺序:在这些 unit 之前启动
Requires=mariadb.service # 强依赖:依赖失败则本服务也不启动;对方停止本服务也停止
Wants=redis.service # 弱依赖:尽量拉起对方,对方失败不影响本服务(推荐)
Conflicts=myapp-debug.service # 冲突:启动本服务会先停掉对方
[Service]
# ---------- 进程行为 ----------
Type=simple # 启动类型,详见 4.2 节(最易出错的一项)
ExecStart=/usr/local/myapp/bin/start.sh # 启动命令,必须用【绝对路径】
ExecStop=/usr/local/myapp/bin/stop.sh # 停止命令,不写则 systemd 直接发 SIGTERM
ExecReload=/bin/kill -HUP $MAINPID # reload 命令,$MAINPID 为主进程 PID
ExecStartPre=/usr/bin/mkdir -p /var/run/myapp # 启动前执行(可写多条)
ExecStartPost=/usr/local/myapp/bin/notify.sh # 启动后执行
PIDFile=/var/run/myapp/myapp.pid # Type=forking 时【必须】指定,供 systemd 跟踪主进程
# ---------- 运行身份与环境 ----------
User=myapp # 以指定用户运行(不要用 root 跑业务进程)
Group=myapp # 以指定用户组运行
WorkingDirectory=/usr/local/myapp # 工作目录,相当于执行前先 cd 过去
UMask=0027 # 文件创建掩码
Environment=JAVA_HOME=/usr/local/jdk1.8.0_212 # 单个环境变量,可写多行
Environment="OPTS=-Xms512m -Xmx1024m" # 含空格的值要加引号
EnvironmentFile=/etc/sysconfig/myapp # 从文件读取环境变量(KEY=VALUE 格式)
# 路径前加 - 表示文件不存在也不报错
# ---------- 资源限制 ----------
LimitNOFILE=65535 # 最大打开文件数(对应 ulimit -n,Nginx/MySQL 必调)
LimitNPROC=65535 # 最大进程数(对应 ulimit -u)
LimitCORE=infinity # core dump 大小上限
LimitMEMLOCK=infinity # 可锁定内存大小(Redis、Elasticsearch 需要)
MemoryLimit=2G # cgroup 内存上限(新版写作 MemoryMax=)
CPUQuota=200% # 最多占用 2 个 CPU 核
Nice=0 # 进程优先级
# ---------- 失败重启策略 ----------
Restart=on-failure # 非正常退出时自动重启(生产环境强烈建议开启)
RestartSec=10s # 重启前等待 10 秒,避免疯狂重启刷日志
StartLimitBurst=5 # 配合下面:限制重启频率
StartLimitInterval=60s # 60 秒内最多重启 5 次,超过则放弃
TimeoutStartSec=300 # 启动超时(秒),慢启动的应用(如 Tomcat)需放宽
TimeoutStopSec=60 # 停止超时,超时后强制 SIGKILL
KillMode=control-group # 停止时杀掉 cgroup 内所有进程
[Install]
# ---------- systemctl enable 时的挂载点 ----------
WantedBy=multi-user.target # 开机自启到"多用户无图形"级别(等价于运行级别 3)
# 图形环境用 graphical.target
[Unit] 段依赖关系指令小结:
| 指令 | 类型 | 含义 |
|---|---|---|
After= |
顺序 | 在列出的 unit 之后启动,只管先后、不管对方成败,也不会拉起对方 |
Before= |
顺序 | 在列出的 unit 之前启动 |
Requires= |
强依赖 | 会一起启动列出的 unit;对方启动失败则本服务也失败;对方被停止本服务也被停止。不隐含顺序,通常要同时写 After= |
Wants= |
弱依赖 | 会尝试一起启动对方,对方失败不影响本服务(推荐,耦合最松) |
Requisite= |
强依赖 | 要求对方已经启动,不会主动拉起 |
BindsTo= |
强绑定 | 比 Requires= 更严格,对方以任何方式停止(包括设备消失)本服务立即停止 |
PartOf= |
从属 | 对方 restart/stop 时本服务跟着 restart/stop(单向) |
Conflicts= |
冲突 | 启动本服务会先停掉对方,二者不能同时运行 |
常用的依赖目标:
| target | 含义 |
|---|---|
network.target |
网络管理服务已启动(不保证网络已可用) |
network-online.target |
网络真正可用(有 IP、可路由),需要访问远程资源的服务应依赖它 |
remote-fs.target |
远程文件系统(NFS 等)已挂载 |
sysinit.target / basic.target |
系统基础初始化完成 |
multi-user.target |
多用户系统就绪,最常见的 WantedBy= 目标 |
syslog.target |
日志服务就绪(CentOS 7 中已被 journald 取代,写着仅为兼容) |
Restart= 取值:
| 取值 | 何时重启 |
|---|---|
no |
从不重启(默认值) |
on-success |
仅正常退出(退出码 0 或在 SuccessExitStatus= 中)时重启 |
on-failure |
非零退出码、被信号杀死、超时、看门狗超时时重启(生产最常用) |
on-abnormal |
被信号杀死、超时时重启,正常非零退出不重启 |
on-abort |
仅被未捕获的信号终止时重启 |
on-watchdog |
仅看门狗超时时重启 |
always |
无论如何都重启(含手动 systemctl stop 之外的所有退出) |
Restart=不会对手动执行的systemctl stop生效——管理员主动停止的服务不会被自动拉起。触发
StartLimitBurst/StartLimitInterval限制后,服务会进入 failed 且拒绝再启动,报start request repeated too quickly,此时必须先systemctl reset-failed <unit>再启动。
[Install] 段:
| 指令 | 含义 |
|---|---|
WantedBy=multi-user.target |
enable 时在 /etc/systemd/system/multi-user.target.wants/ 创建软链(弱依赖,最常用) |
RequiredBy=xxx.target |
enable 时创建到 xxx.target.requires/(强依赖,本服务失败会影响 target) |
Alias= |
为 unit 起别名,enable 时创建别名软链 |
Also= |
enable 本 unit 时顺带 enable 其他 unit(如 .service 带上 .socket) |
只被其他 unit 依赖或只被 timer 触发的 service 不需要
[Install]段。没有[Install]段的 unit 执行systemctl enable会报错,is-enabled显示为static。
4.2 Type= 取值说明(unit"启动后马上就死"的头号原因)
Type= 描述的是程序自身如何 fork,必须与程序实际行为匹配,否则 systemd 会误判进程状态。
| Type | 适用程序行为 | systemd 如何判定"启动完成" |
|---|---|---|
simple |
前台运行、不 fork(默认值) | ExecStart 一执行就算成功,不等待 |
forking |
自身 fork 到后台(传统守护进程写法) | 父进程退出即算启动完成,需配合 PIDFile= |
oneshot |
跑完就退出的一次性任务(脚本、初始化) | 进程退出后才算完成,可配 RemainAfterExit=yes |
notify |
启动完成后主动调用 sd_notify() 通知 systemd |
收到通知才算完成,最精确(如 nginx、mariadb 新版) |
dbus |
在 D-Bus 上注册指定名称后算启动完成 | 需配合 BusName= |
idle |
与 simple 相同,但延后到其他启动任务完成后再执行 |
避免启动日志交错输出 |
排错口诀:
- 程序自己会转后台(脚本里有
&、nohup、daemon()),却写了Type=simple→ systemd 认为主进程已退出,随即把整个 cgroup 杀掉,表现为"start返回成功,几秒后服务变inactive (dead)"。 - 程序在前台不退出,却写了
Type=forking→ systemd 一直等父进程退出,最终报Start operation timed out,状态变failed。 - 拿不准时,先用
Type=simple配合程序的"前台运行模式"(如catalina.sh run、nginx -g 'daemon off;'、redis-server --daemonize no),这是最稳、最推荐的写法。 Type=oneshot的脚本执行完就退出,服务状态会变成inactive (dead)但不是失败;若希望状态保持active (exited)(例如后续有 unit 依赖它"已完成"),加RemainAfterExit=yes。
4.3 完整实例:把源码安装的 Tomcat 纳入 systemd 管理
以下是可直接使用的 Tomcat unit 文件,用它替代"修改 startup.sh"或"往 /etc/rc.local 追加启动命令"的做法。
写法一:Type=forking(沿用 startup.sh,必须提供 CATALINA_PID)
# /etc/systemd/system/tomcat.service
[Unit]
Description=Apache Tomcat 8 Web Application Container
After=network.target syslog.target
Wants=network-online.target
[Service]
Type=forking
# startup.sh 会 fork 后台进程并立即退出,因此用 forking;
# 必须设置 CATALINA_PID,脚本才会写 pid 文件,systemd 才能跟踪主进程
PIDFile=/usr/local/tomcat/temp/tomcat.pid
Environment=JAVA_HOME=/usr/local/jdk1.8.0_212
Environment=CATALINA_HOME=/usr/local/tomcat
Environment=CATALINA_BASE=/usr/local/tomcat
Environment=CATALINA_PID=/usr/local/tomcat/temp/tomcat.pid
Environment="CATALINA_OPTS=-Xms512M -Xmx1024M -server"
Environment="JAVA_OPTS=-Djava.awt.headless=true -Dfile.encoding=UTF-8"
ExecStart=/usr/local/tomcat/bin/startup.sh
ExecStop=/usr/local/tomcat/bin/shutdown.sh
ExecReload=/bin/kill -s HUP $MAINPID
User=tomcat
Group=tomcat
WorkingDirectory=/usr/local/tomcat
Restart=on-failure
RestartSec=10
TimeoutStartSec=300
TimeoutStopSec=60
[Install]
WantedBy=multi-user.target
写法二:Type=simple(推荐,用 catalina.sh run 保持前台)
# /etc/systemd/system/tomcat.service
[Unit]
Description=Apache Tomcat 8 Web Application Container
After=network.target
[Service]
Type=simple
# catalina.sh run 在前台运行、不 fork,交由 systemd 直接管理进程,
# 无需 PIDFile,日志也能被 journald 完整捕获(journalctl -u tomcat)
Environment=JAVA_HOME=/usr/local/jdk1.8.0_212
Environment=CATALINA_HOME=/usr/local/tomcat
Environment=CATALINA_BASE=/usr/local/tomcat
Environment="CATALINA_OPTS=-Xms512M -Xmx1024M -server"
ExecStart=/usr/local/tomcat/bin/catalina.sh run
ExecStop=/bin/kill -15 $MAINPID
User=tomcat
Group=tomcat
WorkingDirectory=/usr/local/tomcat
Restart=on-failure
RestartSec=10
SuccessExitStatus=143 # Tomcat 收到 SIGTERM 后退出码为 143,声明为正常退出
[Install]
WantedBy=multi-user.target
启用与验证:
sudo chown -R tomcat:tomcat /usr/local/tomcat # 确保 User= 指定的用户有权限
sudo systemctl daemon-reload # 让 systemd 读到新文件
sudo systemctl start tomcat # 启动
sudo systemctl status tomcat # 确认 active (running)
sudo systemctl enable tomcat # 开机自启
sudo systemctl enable --now tomcat # 也可一步完成:设自启并立即启动
sudo journalctl -u tomcat -f # 跟踪日志(详见 08-02-日志管理.md)
采用 unit 文件后的收益:
- 不再需要往
/etc/rc.local追加启动命令,systemctl enable tomcat即是标准自启方式;rc.local无状态管理、无失败重启、无日志,属于遗留兼容手段(且 CentOS 7 上/etc/rc.d/rc.local还需手动加执行权限才生效)。- 不必修改
startup.sh去硬编码JAVA_HOME,环境变量由Environment=/EnvironmentFile=统一注入,软件包升级不丢配置。- 崩溃后由
Restart=on-failure自动拉起;systemctl status可查真实状态;日志统一进 journal。
另一个精简示例:把一个 Python 脚本做成常驻服务
# /etc/systemd/system/monitor.service
[Unit]
Description=Custom Monitor Agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=monitor
Group=monitor
WorkingDirectory=/opt/monitor
EnvironmentFile=-/etc/sysconfig/monitor
ExecStartPre=/usr/bin/mkdir -p /var/log/monitor
ExecStart=/usr/bin/python3 /opt/monitor/agent.py --config /opt/monitor/agent.yml
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now monitor
systemctl status monitor
4.4 daemon-reload:什么时候必须执行
systemd 会把所有 unit 配置缓存在内存中,磁盘上的文件改动不会自动生效。
sudo systemctl daemon-reload # 重新扫描并加载所有 unit 文件
必须执行的场景:
- 新增了 unit 文件(如刚创建
tomcat.service)。 - 手工用编辑器修改了已有 unit 文件的内容。
- 删除了 unit 文件。
不需要执行的场景:
- 只是
start/stop/restart服务。 - 通过
systemctl edit修改(该命令退出时会自动 reload)。
易混淆点:
daemon-reload重载的是 systemd 自身的配置;systemctl reload <unit>让某个服务重读它自己的配置文件(如 nginx 重读nginx.conf)。两者完全不同。忘记
daemon-reload的典型症状:明明改对了配置,systemctl start依然按旧配置报同样的错。此时 systemd 通常会提示Warning: unit changed on disk, ...。
4.5 定制官方 unit 的正确方式:systemctl edit 与 drop-in
需求场景:yum 安装的 nginx.service 位于 /usr/lib/systemd/system/,想改它的内存限制。直接编辑该文件会在软件包升级时被覆盖,正确做法有两种。
方式一:drop-in 局部覆盖(推荐,只改需要改的指令)
sudo systemctl edit nginx.service
# 自动创建并打开 /etc/systemd/system/nginx.service.d/override.conf
在打开的空文件中只写要覆盖的部分:
[Service]
Restart=always
RestartSec=5
MemoryLimit=1G
保存退出后 systemd 自动 reload。效果是原 unit 的其余配置全部保留,只有这几项被覆盖。
也可手动创建,效果完全相同:
sudo mkdir -p /etc/systemd/system/nginx.service.d
sudo vi /etc/systemd/system/nginx.service.d/override.conf
sudo systemctl daemon-reload # 手工创建时需要自己 reload
sudo systemctl restart nginx
.service.d/目录下所有.conf文件都会被按文件名顺序加载,可以拆成10-limits.conf、20-env.conf等多个文件分类管理。
方式二:完整覆盖(改动很大时)
sudo systemctl edit --full nginx.service
# 把原 unit 内容【完整复制】到 /etc/systemd/system/nginx.service 供编辑
清除自定义、恢复官方配置:
sudo systemctl revert nginx.service # 删除 drop-in 与完整覆盖,回到软件包原始状态
特别注意:
ExecStart=这类可累加的指令,在 drop-in 中直接写会追加而非替换,导致启动两个进程。必须先用空值清空再赋值:[Service] ExecStart= ExecStart=/usr/sbin/nginx -c /etc/nginx/custom.conf
4.6 unit 信息查询命令
systemctl cat tomcat.service # 查看最终生效的完整配置(含所有 drop-in,排错必用)
systemctl show tomcat.service # 显示全部属性及其生效值(含未写明的默认值)
systemctl show -p Restart,MemoryLimit tomcat.service # 只看指定属性
systemctl list-units # 列出【已加载】的 unit(默认只显示 active)
systemctl list-units --all # 包含 inactive 的
systemctl list-units --type=service # 只看 service 类型
systemctl list-units --failed # 只看启动失败的(巡检常用)
systemctl list-unit-files # 列出【磁盘上所有】unit 文件及其 enable 状态
systemctl list-unit-files --type=service --state=enabled # 只看已启用的服务
systemctl list-dependencies tomcat.service # 查看依赖树(本服务依赖谁)
systemctl list-dependencies --reverse tomcat.service # 反向依赖(谁依赖本服务)
systemctl list-dependencies --all multi-user.target # 展开某 target 下的全部服务
systemctl is-active tomcat # 是否运行中(适合脚本判断)
systemctl is-enabled tomcat # 是否已设开机自启
systemctl is-failed tomcat # 是否处于失败状态
| 命令 | 区别要点 |
|---|---|
systemctl cat |
看人写的配置文本,包含注释与 drop-in 来源路径 |
systemctl show |
看解析后的全部属性,含默认值,无注释 |
list-units |
内存中已加载的 unit |
list-unit-files |
磁盘上所有存在的 unit 文件(含从未加载的) |
4.7 mask 与 disable 的区别
| 命令 | 效果 | 能否被手动/依赖启动 |
|---|---|---|
systemctl disable X |
删除 *.wants/ 中的自启链接 |
可以,systemctl start X 仍能启动,其他服务也能依赖拉起 |
systemctl mask X |
把 unit 名软链到 /dev/null |
完全不能,手动启动报 Unit is masked,依赖触发也无效 |
sudo systemctl disable firewalld # 仅取消开机自启
sudo systemctl mask firewalld # 彻底屏蔽,任何方式都无法启动
sudo systemctl unmask firewalld # 解除屏蔽(恢复后仍需 enable 才会自启)
systemctl is-enabled firewalld # 输出 disabled / enabled / masked
典型用途:安装 MySQL 时需要确保 firewalld 或 postfix 绝不被任何依赖链意外拉起,此时 disable 不够,要用 mask。
注意:
mask之后unmask只是恢复"可被启动",并不会自动恢复开机自启,需要再执行一次systemctl enable。
4.8 部署自定义 unit 的标准流程
# 1. 创建运行用户(不要用 root 跑业务)
sudo useradd -r -s /sbin/nologin myapp
# 2. 写 unit 文件
sudo vi /etc/systemd/system/myapp.service
# 3. 静态语法检查
sudo systemd-analyze verify /etc/systemd/system/myapp.service
# 4. 重载 systemd 配置
sudo systemctl daemon-reload
# 5. 启动并观察
sudo systemctl start myapp
sudo systemctl status myapp --no-pager
# 6. 确认无误后设为开机自启(也可用 enable --now 合并 5、6 步)
sudo systemctl enable myapp
systemctl is-enabled myapp
# 7. 验证最终生效配置与自启链接
systemctl cat myapp
ls -l /etc/systemd/system/multi-user.target.wants/ | grep myapp
五、systemd 定时器 timer(cron 的现代替代)
CentOS 7 起,systemd 提供 .timer 类型的 unit 来定时触发 .service,功能上覆盖并超越 cron + anacron 的组合。at 与 crontab 的完整用法见任务管理相关文档,本节只讲 timer 及两者的对照迁移。
5.1 为什么用 timer 而不是 cron
| 能力 | cron | systemd timer |
|---|---|---|
| 依赖管理 | 无,只能在脚本里 sleep 等待 |
After=/Requires= 声明依赖,如等数据库就绪 |
| 日志 | 需自己重定向到文件,或翻 /var/log/cron |
自动进 journal,journalctl -u backup.service 一条命令搞定 |
| 漏执行补跑 | 需依赖 anacron,且只支持天/周/月 |
Persistent=true 即可,任意周期都支持 |
| 资源限制 | 无 | 直接用 [Service] 的 CPUQuota=、MemoryLimit=、Nice= |
| 与 service 复用 | 无关联 | 同一个 .service 既可手动 systemctl start,也可被 timer 触发 |
| 执行环境 | PATH 极简,常因环境变量不同而"手动能跑、cron 不能跑" | Environment=/EnvironmentFile= 显式声明,行为可预期 |
| 错峰执行 | 需手动错开分钟数 | RandomizedDelaySec= 自动打散 |
| 状态可观测 | 只能看日志猜 | systemctl list-timers 直接看上次/下次触发时间 |
5.2 成对示例:backup.service + backup.timer
timer 本身不含执行逻辑,它只负责"何时触发",真正干活的是同名 .service。
第一步,写被触发的服务 /etc/systemd/system/backup.service:
[Unit]
Description=Nightly Database Backup
# 备份依赖数据库可用,声明启动顺序
After=network-online.target mariadb.service
[Service]
# oneshot:跑完就退出的一次性任务,timer 场景的标准选择
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/data/backup
EnvironmentFile=/etc/backup/backup.env
ExecStart=/usr/local/bin/backup.sh --full
# 资源限制,避免备份把机器压垮(cron 做不到)
Nice=19
IOSchedulingClass=idle
# 注意:仅由 timer 触发的 service 不需要 [Install] 段,
# 也不要 systemctl enable backup.service(否则开机就会跑一次)
第二步,写定时器 /etc/systemd/system/backup.timer(文件名必须与 service 同名):
[Unit]
Description=Run Nightly Database Backup
# 说明本 timer 触发哪个 unit;同名时可省略,此处显式写出更清晰
# Requires=backup.service
[Timer]
# 每周一凌晨 2:30 执行
OnCalendar=Mon *-*-* 02:30:00
# 关机/宕机错过了执行时间点,开机后立即补跑一次(anacron 式行为)
Persistent=true
# 在触发时刻上随机延迟 0~1800 秒,避免多台机器同时压向备份服务器
RandomizedDelaySec=1800
# 允许的时间误差,默认 1min。放宽可让 systemd 合并唤醒、更省电;
# 要求精确到秒时设为 1us
AccuracySec=1min
# 单位名默认取同名 .service,需要触发别的 unit 时才显式指定
# Unit=backup.service
[Install]
# timer 必须挂到 timers.target 才会随开机启用
WantedBy=timers.target
第三步,启用与验证:
sudo systemctl daemon-reload # 新增 unit 后必须重载(见 4.4 节)
sudo systemctl enable --now backup.timer # 设为开机自启并立即启动定时器
# 注意:enable 的是 .timer,不是 .service
sudo systemctl list-timers --all # 查看所有定时器状态
sudo systemctl status backup.timer # 查看定时器本身
sudo systemctl start backup.service # 手动立即执行一次(不等定时器)
sudo journalctl -u backup.service -n 50 # 查看任务执行日志
list-timers 输出解读:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-08-24 02:30:00 CST 1 day 8h Mon 2026-08-17 02:41:13 CST 5 days ago backup.timer backup.service
| 列 | 含义 |
|---|---|
NEXT / LEFT |
下次触发的绝对时间 / 剩余时间 |
LAST / PASSED |
上次触发的时间 / 距今多久 |
UNIT / ACTIVATES |
定时器名 / 它触发的服务名 |
5.3 验证 OnCalendar 表达式
OnCalendar 写错是最常见的问题,写完务必用 systemd-analyze 验证:
systemd-analyze calendar "Mon *-*-* 02:30:00"
# Normalized form: Mon *-*-* 02:30:00
# Next elapse: Mon 2026-08-24 02:30:00 CST
# (in UTC): Sun 2026-08-23 18:30:00 UTC
# From now: 1 day 8h left
systemd-analyze calendar --iterations=5 "*:0/15" # 预览未来 5 次触发时间
若表达式非法会直接报 Failed to parse calendar expression,能在部署前发现问题。
OnCalendar 语法格式为 DayOfWeek Year-Month-Day Hour:Minute:Second,其中星期与日期部分可省略。也支持简写关键字:
| 简写 | 等价表达式 |
|---|---|
minutely |
*-*-* *:*:00 |
hourly |
*-*-* *:00:00 |
daily |
*-*-* 00:00:00 |
weekly |
Mon *-*-* 00:00:00 |
monthly |
*-*-01 00:00:00 |
yearly |
*-01-01 00:00:00 |
5.4 crontab 表达式 → OnCalendar 对照表
下表覆盖常见的 cron 写法,便于直接对比迁移:
| 需求 | crontab 表达式 | 等价 OnCalendar |
|---|---|---|
| 每分钟 | * * * * * |
*-*-* *:*:00 或 minutely |
| 每 2 分钟 | */2 * * * * |
*-*-* *:0/2:00 |
| 每 5 分钟 | */5 * * * * |
*-*-* *:0/5:00 或 *:0/5 |
| 每小时(整点) | 0 * * * * |
*-*-* *:00:00 或 hourly |
| 每天 2:00 | 0 2 * * * |
*-*-* 02:00:00 |
| 每天 0:00 | 0 0 * * * |
*-*-* 00:00:00 或 daily |
| 每周日 0:00 | 0 0 * * 0 |
Sun *-*-* 00:00:00 |
| 每月 1 号 0:00 | 0 0 1 * * |
*-*-01 00:00:00 或 monthly |
| 每周一 2:30 | 30 2 * * 1 |
Mon *-*-* 02:30:00 |
| 每小时前 30 分钟 | 0-29 * * * * |
*-*-* *:00/1:00 需拆分,建议写 *:0..29 |
| 每天 1/3/7/8/13/21 点 05 分 | 5 1,3,7-8,13,21 * * * |
*-*-* 01,03,07,08,13,21:05:00 |
| 每月 5 号和 20 号 4-13 点 15 分 | 15 4-13 5,20 * * |
*-*-05,20 04..13:15:00 |
| 每周三每 2 小时 | 0 */2 * * 3 |
Wed *-*-* 0/2:00:00 |
| 每年 7 月 6 日 5:03 | 3 5 6 7 * |
*-07-06 05:03:00 |
| 工作日 9:00 | 0 9 * * 1-5 |
Mon..Fri *-*-* 09:00:00 |
语法差异要点:
| 写法 | cron | OnCalendar |
|---|---|---|
| 步进 | */5(从 0 开始) |
0/5(显式写起点,*:0/5) |
| 范围 | 4-13 |
4..13 |
| 列举 | 1,3,5 |
1,3,5(相同) |
| 星期 | 数字 0-7 |
英文缩写 Mon/Sun,范围用 Mon..Fri |
| 秒级精度 | 不支持 | 支持,第三段即秒 |
注意:cron 中"日"与"周"同时为数字时是或关系(一个为数字以此为准;两个都为数字是"或";两个都为
*代表每天);而OnCalendar中星期与日期是与关系,即Mon *-*-01表示"1 号且恰好是周一"。迁移时若原表达式依赖"或"语义,需写成两条OnCalendar=(同一[Timer]段中可重复该指令,取并集)。
5.5 单调定时器:OnBootSec / OnUnitActiveSec
除了 OnCalendar 这种"绝对日历时间",timer 还支持相对某个事件计时的单调定时器,用于"每隔多久跑一次"的场景(cron 无法优雅表达):
| 指令 | 起算点 |
|---|---|
OnBootSec= |
系统启动之后 |
OnStartupSec= |
systemd 自身启动之后 |
OnActiveSec= |
timer 被激活之后 |
OnUnitActiveSec= |
目标 unit 上次启动之后 |
OnUnitInactiveSec= |
目标 unit 上次结束之后 |
典型的"开机 5 分钟后首次执行,之后每 30 分钟执行一次":
[Timer]
OnBootSec=5min
OnUnitActiveSec=30min
Unit=healthcheck.service
[Install]
WantedBy=timers.target
时间单位支持 us/ms/s/min/h/d/w,可组合书写如 OnUnitActiveSec=1h30min。
说明:
OnCalendar与单调指令可以在同一个 timer 中共存,任一条件满足即触发。但单调定时器不支持Persistent=true,因为它没有"错过的绝对时间点"这一概念。
5.6 cron 与 timer 如何取舍
仍然优先用 cron 的场景:
- 任务简单,就是一行命令加重定向,
crontab -e30 秒搞定。 - 需要跨平台/跨发行版可移植,包括老旧的 CentOS 6、非 systemd 系统、容器镜像。
- 用户级任务,普通用户无 root 权限,无法写
/etc/systemd/system/(systemd 虽有systemctl --user,但 CentOS 7 上用户级实例默认不随登录常驻,配置繁琐)。 - 团队更熟悉 crontab,运维习惯与现有脚本体系已成型。
改用 timer 更合适的场景:
- 任务有启动依赖,如必须等网络、数据库、挂载点就绪。
- 需要补跑错过的任务,且周期不是天/周/月(
anacron覆盖不到)。 - 需要可观测性:结构化日志、上次/下次执行时间、失败退出码,全部由 systemd 统一记录。
- 需要资源隔离,限制任务的 CPU、内存、IO 优先级。
- 任务既要定时跑、也常要手动跑一次——同一个
.service两种触发方式共用一份配置。 - 需要秒级或随机错峰调度。
折中建议:存量简单任务留在 cron,不必为迁移而迁移;新增的、有依赖或需要排查的重要任务(备份、清理、健康检查)用 timer 编写。两套机制可以长期并存。
六、开机自启管理与启动性能分析
6.1 查看当前开机会启动哪些服务
CentOS 7+:
systemctl list-unit-files --state=enabled # 所有已启用的 unit
systemctl list-unit-files --type=service --state=enabled # 只看服务
systemctl list-unit-files --type=service --state=disabled
systemctl list-unit-files --type=service --state=masked
systemctl list-dependencies --all default.target # 展开默认 target 的完整启动树
systemctl list-units --type=service --state=running # 当前实际在跑的服务
CentOS 6:
chkconfig --list | grep '3:on' # 级别 3 会启动的服务
chkconfig --list | grep '5:on' # 级别 5 会启动的服务
ls /etc/rc.d/rc3.d/S* # 直接看级别 3 的启动链接
6.2 启动耗时分析:systemd-analyze
systemd-analyze
# Startup finished in 1.234s (kernel) + 3.456s (initrd) + 25.678s (userspace) = 30.368s
# 分别是:内核耗时 + initramfs 耗时 + 用户态(systemd 启动服务)耗时
systemd-analyze time # 同上
systemd-analyze blame # 按耗时降序列出每个 unit 的启动时间(找慢服务)
systemd-analyze critical-chain # 打印启动关键路径(真正拖慢开机的依赖链)
systemd-analyze critical-chain httpd.service # 只看某服务的关键链
systemd-analyze plot > boot.svg # 生成可视化启动时序图(浏览器打开)
systemd-analyze dump # 导出 systemd 内部完整状态(信息量极大)
systemd-analyze verify /etc/systemd/system/myapp.service # 静态检查 unit 语法
systemd-analyze calendar "*-*-* 03:00:00" # 校验日历表达式
blame 输出示例:
12.345s NetworkManager-wait-online.service
5.678s kdump.service
3.210s postfix.service
1.987s tuned.service
critical-chain 输出示例(@ 表示该 unit 启动完成的时刻,+ 表示自身耗时):
multi-user.target @25.678s
└─httpd.service @24.100s +1.500s
└─network-online.target @24.050s
└─NetworkManager-wait-online.service @11.700s +12.345s
└─NetworkManager.service @11.600s +95ms
阅读要点:
blame只说明"谁慢",但慢服务如果是并行启动的,未必影响总时长;critical-chain才能告诉你"谁真正卡住了后续启动"。上例中真正的瓶颈是NetworkManager-wait-online.service(等待网络就绪 12 秒)。
6.3 精简开机自启服务
原则:先确认用途,再禁用;对不确定的服务用 disable 而不是 mask,便于回退。
systemctl disable --now postfix # 不发本机邮件时可关(注意:部分脚本依赖 sendmail)
systemctl disable --now kdump # 内核崩溃转储,非排障期可关,能省数百 MB 内存
systemctl disable --now bluetooth # 服务器无蓝牙
systemctl disable --now cups # 无打印机
systemctl disable --now avahi-daemon # 局域网服务发现,服务器一般不需要
systemctl disable --now NetworkManager-wait-online # 静态 IP 环境可关,明显缩短开机时间
systemctl disable --now firewalld # 仅在确有替代方案(iptables/云安全组)时才关
CentOS 6 的等价操作:
chkconfig postfix off
chkconfig bluetooth off
chkconfig cups off
chkconfig --level 2345 avahi-daemon off
注意:
NetworkManager-wait-online.service是network-online.target的实现者,禁用后依赖"网络真正可用"的服务可能提前启动而失败。使用静态 IP 且服务能容忍重试时才禁用。
七、服务排错
7.1 通用排查流程
1. systemctl status <unit> -l --no-pager 看状态、退出码、最后几行日志
2. journalctl -xe 看带解释的最新错误现场
3. journalctl -u <unit> -n 100 --no-pager 看该服务完整错误上下文
4. systemctl cat <unit> 确认最终生效的配置(含 drop-in)是否如预期
5. 手工前台执行 ExecStart 的命令 绕开 systemd,判断是程序问题还是 unit 问题
6. 检查资源:端口占用、磁盘空间、权限、SELinux
7. 检查配置文件语法(nginx -t / httpd -t / 等)
8. systemd-analyze verify <unit 文件> 检查 unit 自身语法与依赖
7.2 status 输出解读
● tomcat.service - Apache Tomcat 8 Web Application Container
Loaded: loaded (/etc/systemd/system/tomcat.service; enabled; vendor preset: disabled)
Active: failed (Result: exit-code) since Sat 2026-08-22 10:12:33 CST; 5s ago
Process: 6721 ExecStart=/usr/local/tomcat/bin/catalina.sh run (code=exited, status=1/FAILURE)
Main PID: 6721 (code=exited, status=1/FAILURE)
CGroup: /system.slice/tomcat.service
| 字段 | 解读 |
|---|---|
| 行首圆点颜色 | 绿色 ● 运行正常;白色 ○ 未运行;红色 ● failed |
Loaded: |
unit 文件路径 + 是否 enabled。若为 not-found 说明文件名/路径错或没 daemon-reload;masked 说明被屏蔽 |
Active: |
active (running) 正常常驻;active (exited) oneshot 已完成;active (waiting) 等待触发;inactive (dead) 未运行;failed 启动失败;activating/deactivating 中间态 |
Result: |
失败原因分类:exit-code(非零退出)、timeout(超时)、signal(被信号杀死)、core-dump、protocol(Type 与实际行为不匹配)、resources |
Process: |
各 Exec* 命令的实际执行结果与退出码 |
Main PID: |
主进程 PID;已退出时显示退出码 |
CGroup: |
该服务 cgroup 下的所有进程,能看到全部子进程 |
常见退出码含义:
| 退出码 | 含义 |
|---|---|
status=0/SUCCESS |
正常退出 |
status=1/FAILURE |
程序自身报错,需看程序日志 |
status=200/CHDIR |
WorkingDirectory= 指定的目录不存在 |
status=203/EXEC |
命令无法执行:路径错、无执行权限、shebang 错误 |
status=217/USER |
User= 指定的用户不存在 |
status=226/NAMESPACE |
命名空间相关限制(PrivateTmp= 等)导致失败 |
status=143 |
收到 SIGTERM 后退出(Tomcat 等 Java 程序的正常停止码,可用 SuccessExitStatus=143 声明) |
7.3 用 journalctl 定位(详见 08-02-日志管理.md)
sudo systemctl status tomcat -l --no-pager # 先看状态摘要与最后几行日志
sudo journalctl -xe # -x 附加解释说明,-e 跳到最新
sudo journalctl -u tomcat -n 100 --no-pager # 该服务最近 100 行
sudo journalctl -u tomcat -b --no-pager # 本次启动以来该服务的全部记录
sudo journalctl -u tomcat -f # 实时跟踪,另开窗口再 start,观察全过程
sudo journalctl -p err -b # 本次启动的所有错误级别日志
常见能从 journalctl -xe 中直接定位的问题:
| 日志关键字 | 原因 |
|---|---|
Failed to determine user credentials |
User= 指定的用户不存在 |
No such file or directory |
ExecStart 路径写错或缺少解释器 |
Executable path is not absolute |
ExecStart 必须用绝对路径 |
PID file ... not readable |
Type=forking 但 PIDFile= 路径不对 |
Start operation timed out |
Type= 选错(详见 4.2 节 Type 说明) |
status=203/EXEC |
脚本无执行权限或 shebang 有问题 |
Unit is masked |
服务被 systemctl mask 屏蔽,需 unmask |
start request repeated too quickly |
触发 StartLimitBurst 限制,需 systemctl reset-failed |
Address already in use |
端口被占用(见 7.4 节) |
Permission denied |
文件权限、SELinux 或端口 <1024 而未以 root 运行 |
CentOS 7 默认不持久化 journal 日志,重启后无法查看上次启动的记录。排查异常重启前请先按
08-02-日志管理.md开启持久化。
7.4 端口占用
ss -tunlp | grep 8080 # 查看 8080 端口的监听进程(推荐,CentOS 7 默认有)
netstat -tunlp | grep 8080 # 老写法,需安装 net-tools
lsof -i :8080 # 查看占用该端口的进程
fuser -n tcp 8080 # 同上
fuser -k -n tcp 8080 # 杀掉占用该端口的进程(慎用)
ps -ef | grep java # 确认是否有残留的老进程未退出
典型场景:systemctl restart tomcat 报 Address already in use,原因是上一次进程没被正确停止(Type= 或 ExecStop= 写错,systemd 以为停了、实际还在跑)。修正 Type=/KillMode= 后,先手工 kill 残留进程再启动。
7.5 SELinux 导致的启动失败
现象:程序手工执行正常,通过 systemd 启动就失败,日志里出现 Permission denied,且文件权限看起来完全正确。
getenforce # Enforcing / Permissive / Disabled
sestatus # 详细状态
# 1. 快速验证是否为 SELinux 引起
setenforce 0 # 临时切到 Permissive(重启失效)
systemctl start myapp # 若此时能启动,即可确认是 SELinux
setenforce 1 # 验证完立刻改回
# 2. 查看被拒绝的具体操作
grep -i denied /var/log/audit/audit.log | tail -20
ausearch -m avc -ts recent # 更易读
sealert -a /var/log/audit/audit.log # 需要 setroubleshoot-server,给出修复建议
常见修复手段:
# 情况一:文件安全上下文不对(如把网站目录挪到了 /data/www)
ls -Zd /data/www # 查看上下文
semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
restorecon -Rv /data/www # 应用上下文
chcon -R -t httpd_sys_content_t /data/www # 临时改法,relabel 后会丢失
# 情况二:需要监听非标准端口
semanage port -l | grep http_port_t
semanage port -a -t http_port_t -p tcp 8000 # 允许 httpd 监听 8000
# 情况三:需要打开某个布尔开关
getsebool -a | grep httpd
setsebool -P httpd_can_network_connect on # -P 永久生效
完整的 SELinux 讲解见防火墙与 SELinux 相关文档。生产环境不建议直接
SELINUX=disabled,应按上下文/端口/布尔值精准放行。
7.6 配置文件语法检查
启动失败最常见的原因其实是服务自己的配置文件写错,先自检再启动:
nginx -t # Nginx 配置语法检查
httpd -t / apachectl configtest # Apache
sshd -t # SSH
named-checkconf / named-checkzone # DNS
mysqld --validate-config # MySQL 8/部分版本
visudo -c # sudoers
crontab -l # 检查 cron 条目
systemd-analyze verify /etc/systemd/system/myapp.service # unit 文件本身
7.7 依赖循环
现象:开机时某些服务被无故跳过,日志中出现:
systemd[1]: Found ordering cycle on b.service/start
systemd[1]: Found dependency on a.service/start
systemd[1]: Breaking ordering cycle by deleting job a.service/start
systemd[1]: Job a.service/start deleted to break ordering cycle starting with b.service/start
systemd 检测到 After= 形成的环时,会任意删掉环中的一个作业来打破循环,被删掉的服务就不会启动——这就是"某个服务时而启动、时而不启动"的根源。
排查与修复:
journalctl -b | grep -i 'ordering cycle' # 定位环上的 unit
systemctl list-dependencies a.service # 正向依赖
systemctl list-dependencies --reverse a.service # 反向依赖
systemctl list-dependencies --after a.service # 谁在本服务之后启动
systemctl list-dependencies --before a.service # 谁在本服务之前启动
修复要点:
- 找出环中最不必要的那条
After=/Before=并删除,通常是自己写的 drop-in 引入的。 - 区分"顺序"与"依赖":只想控制先后就用
After=,不要滥用Requires=。 - 把强依赖
Requires=换成弱依赖Wants=,可打断大部分循环且不影响功能。 DefaultDependencies=no只在明确知道后果时使用(会去掉basic.target等隐式依赖,容易引入新问题)。
7.8 其他高频原因清单
| 现象 | 常见原因 |
|---|---|
start 返回成功,几秒后变 inactive (dead) |
程序自己转后台但写了 Type=simple |
Start operation timed out 后 failed |
程序前台运行但写了 Type=forking;或启动确实慢,需放宽 TimeoutStartSec= |
| 改了 unit 却毫无变化 | 忘记 systemctl daemon-reload |
| 改了 drop-in 后启动了两个进程 | ExecStart= 未先清空再赋值 |
服务无法 enable,提示 no installation config |
unit 缺少 [Install] 段(is-enabled 显示 static) |
| 手工能跑、服务起不来 | 环境变量差异(PATH、JAVA_HOME 未通过 Environment= 注入);WorkingDirectory= 未设置 |
日志报 Too many open files |
未设置 LimitNOFILE=(/etc/security/limits.conf 对 systemd 启动的服务无效) |
| 磁盘满导致大量服务失败 | df -h / df -i 检查空间与 inode |
八、命令速查
| 类别 | 命令 | 作用 |
|---|---|---|
| 体系判断 | ps -p 1 -o comm= |
判断 PID 1 是 init 还是 systemd |
| 运行级别(C6) | init 3 / init 5 / init 0 / init 6 |
切换运行级别 / 关机 / 重启 |
| 运行级别(C6) | runlevel / who -r |
查询运行级别 |
| 运行级别(C6) | vi /etc/inittab |
修改默认运行级别 |
| 服务管理(C6) | service 服务名 start/stop/restart/reload/status |
管理服务 |
| 服务管理(C6) | /etc/init.d/服务名 start |
直接调用启动脚本 |
| 服务管理(C6) | service --status-all |
查看所有服务状态 |
| 开机自启(C6) | chkconfig --list / chkconfig 服务名 on/off |
查看/设置自启 |
| 开机自启(C6) | chkconfig 服务名 on --level 35 |
指定运行级别自启 |
| 开机自启(C6) | chkconfig --add/--del 服务名 |
纳入/移出 chkconfig 管理 |
| 服务管理(C7) | systemctl start/stop/restart/reload/status 服务名 |
管理服务 |
| 服务管理(C7) | systemctl kill -s SIGKILL 服务名 |
强制杀死服务全部进程 |
| 开机自启(C7) | systemctl enable/disable 服务名 |
设置开机自启/禁用 |
| 开机自启(C7) | systemctl enable --now 服务名 |
设自启并立即启动 |
| 开机自启(C7) | systemctl is-enabled/is-active/is-failed 服务名 |
脚本中判断状态 |
| target | systemctl get-default / set-default |
查看/设置默认 target |
| target | systemctl isolate multi-user.target |
立即切换 target |
| 电源 | systemctl poweroff/reboot/suspend/hibernate |
关机/重启/挂起/休眠 |
| Unit 编写 | systemctl daemon-reload |
重载 unit 配置(改完必须执行) |
| Unit 编写 | systemctl cat <unit> |
查看 unit 完整内容(含 drop-in) |
| Unit 编写 | systemctl edit <unit> |
创建 drop-in 覆盖 |
| Unit 编写 | systemctl edit --full <unit> |
完整覆盖式编辑 |
| Unit 编写 | systemctl revert <unit> |
撤销所有自定义,恢复官方配置 |
| Unit 编写 | systemctl show <unit> |
查看 unit 全部生效参数 |
| Unit 编写 | systemctl list-units [--all/--failed] |
列出已加载的 unit |
| Unit 编写 | systemctl list-unit-files [--state=enabled] |
列出所有 unit 文件及启用状态 |
| Unit 编写 | systemctl list-dependencies [--reverse] <unit> |
查看依赖树 |
| Unit 编写 | systemctl mask/unmask <unit> |
彻底屏蔽/解除屏蔽服务 |
| Unit 编写 | systemctl reset-failed [<unit>] |
清除失败状态与重启计数 |
| timer | systemctl enable --now <t>.timer |
启用并立即启动定时器 |
| timer | systemctl list-timers --all |
查看所有定时器及下次触发时间 |
| timer | systemd-analyze calendar "表达式" |
验证 OnCalendar 表达式 |
| 性能分析 | systemd-analyze / blame / critical-chain |
启动耗时与关键路径 |
| 性能分析 | systemd-analyze plot > boot.svg |
生成启动时序图 |
| 排错 | systemd-analyze verify <unit 文件> |
静态检查 unit 语法 |
| 排错 | journalctl -xe / journalctl -u <unit> -f |
排查服务启动失败 |
| 排错 | ss -tunlp \| grep 端口 / lsof -i :端口 |
查端口占用 |
| 排错 | getenforce / setenforce 0 / ausearch -m avc -ts recent |
排查 SELinux 拦截 |
| 用户查看 | who / who -aH / who -b |
查看在线用户/最后启动时间 |
| 消息 | write / wall / mesg |
发送/控制消息 |
| 登录历史 | last / lastb / lastlog |
查看登录记录 |
| 关机 | shutdown -h now / shutdown -r +10 "msg" / shutdown -c |
关机/定时重启/取消 |
九、注意事项
- 修改
/etc/rc*.d/中的服务链接时,注意文件名前缀S/K及启动顺序数字;正常情况应交给chkconfig维护,不要手工建链接。 service最终调用/etc/init.d/脚本;在 CentOS 7 上它会被重定向到systemctl,不同发行版建议优先使用systemctl。- 自写 SysV 脚本必须包含
# chkconfig:与# description:两行注释,否则chkconfig --add会失败;并且要在 start 时创建/var/lock/subsys/<服务名>,否则关机时不会调用 stop。 - 默认运行级别不要设为 0(开机即关机)或 6(无限重启)。
- CentOS 7 上修改
/etc/inittab无效,必须用systemctl set-default。 systemctl isolate会停掉目标 target 不需要的所有服务,生产环境慎用。- 新增或修改
/etc/systemd/system/下的 unit 文件后,必须执行systemctl daemon-reload,否则 systemd 仍使用旧配置。 systemctl edit <unit>创建的 drop-in 覆盖文件无需手动daemon-reload,systemd 会自动检测;手工创建.d/*.conf时则需要自己 reload。- 不要直接修改
/usr/lib/systemd/system/下的文件,软件包升级会覆盖;用 drop-in 或复制到/etc/systemd/system/。 - drop-in 中覆盖
ExecStart=等可累加指令时,必须先写一行空值清空再赋新值,否则会变成追加、启动两个进程。 Type=forking的服务必须在[Service]段指定PIDFile=,否则systemctl status可能无法正确跟踪进程状态。ExecStart=必须使用绝对路径,且 systemd 不解析 Shell 语法(管道、重定向、通配符);需要时用/bin/bash -c "..."包裹。- 被
systemctl mask的服务无法以任何方式启动(包括依赖触发),如需恢复使用systemctl unmask,之后还要重新enable才会自启。 - 只被 timer 触发的
.service不要enable(否则开机就会执行一次),要enable的是.timer。 - 使用 systemd timer 时,
OnCalendar表达式应通过systemd-analyze calendar验证,避免语法错误;单调定时器不支持Persistent=true。 /etc/rc.local在 CentOS 7 上默认无执行权限,需chmod +x /etc/rc.d/rc.local才生效;新需求应优先写 Unit 文件。/etc/security/limits.conf对 systemd 启动的服务无效,文件描述符上限要写在 unit 的LimitNOFILE=。- 触发
StartLimitBurst/StartLimitInterval限制后服务会拒绝启动,必须先systemctl reset-failed <unit>。 network.target不代表网络可用,需要网络就绪的服务应依赖network-online.target并同时写Wants=network-online.target。- 排查异常重启前需确认 journal 已持久化(CentOS 7 默认易失),详见
08-02-日志管理.md。
十、练习
一、启动流程
1. 按顺序写出从通电到出现登录提示符的九个阶段,并说明 initramfs 的作用。
答:
- BIOS/UEFI 自检(POST)→ 2. 读取 MBR 或 ESP,加载引导加载器 → 3. GRUB2 读
grub.cfg,加载内核与 initramfs → 4. 内核挂载 initramfs 作为临时根 → 5. 内核初始化、加载驱动、按/etc/fstab挂载真实根分区并switch_root→ 6. 启动 PID 1(CentOS 6 为init,CentOS 7+ 为systemd)→ 7. 读取/etc/inittab或default.target,确定运行级别/target → 8. 启动目标服务,最后执行/etc/rc.local→ 9. 启动getty或显示管理器,用户登录。
initramfs 的作用:它是一个内存中的临时根文件系统,携带挂载真实根分区所需的驱动与工具(LVM、RAID、加密卷、特殊存储驱动),解决"驱动放在根分区里、而挂载根分区又需要该驱动"的死循环。
2. 如何一条命令判断当前系统是 SysVinit 体系还是 systemd 体系?
答:
ps -p 1 -o comm= # 输出 init → CentOS 6(SysVinit);输出 systemd → CentOS 7+
二、运行级别与 target
3. 填写运行级别对应表。
答:0 关机 / 1 单用户模式 / 2 多用户无网络 / 3 多用户有网络无图形(服务器常用)/ 4 预留 / 5 多用户有网络有图形 / 6 重启。
4. 分别在 CentOS 6 和 CentOS 7 上:把系统默认启动到字符界面,并立即切换过去。
答:
# CentOS 6
vi /etc/inittab # 将 id:5:initdefault: 改为 id:3:initdefault:
init 3 # 立即切换
# CentOS 7
systemctl set-default multi-user.target
systemctl get-default # 验证
systemctl isolate multi-user.target # 立即切换
5. multi-user.target 和 graphical.target 分别对应哪些运行级别?
答:multi-user.target 对应级别 2、3、4;graphical.target 对应级别 5。此外 poweroff.target=0、rescue.target=1、reboot.target=6。
三、SysVinit 服务管理
6. /etc/rc.d/rc3.d/S55sshd 这个文件名的每一部分是什么含义?
答:S 表示进入级别 3 时执行 sshd stop 之外的 start 动作(即开机启动;K 则表示执行 stop、不启动);55 是启动顺序号,数字越小越早启动;sshd 是服务名,该文件是指向 /etc/init.d/sshd 的符号链接。
7. 脚本头部 # chkconfig: 2345 85 15 三段分别是什么?
答:2345 表示默认在 2、3、4、5 级别启动(写 - 表示同样是 2345 但默认不开启);85 是开机启动顺序号(S 链接编号);15 是关机停止顺序号(K 链接编号)。
8. 只让 httpd 在运行级别 2 和 3 自启,其他级别关闭,并验证结果。
答:
chkconfig httpd off # 先全部关闭
chkconfig httpd on --level 23 # 仅 2、3 级别开启
chkconfig --list httpd # 验证:2:on 3:on,其余 off
ls -l /etc/rc.d/rc3.d/ | grep httpd # 验证 S 链接存在
9. chkconfig --del httpd 会删除 /etc/init.d/httpd 吗?
答:不会。--del 只是把服务从 chkconfig 的管理中移除(删除各级别的符号链接),/etc/init.d/ 下的脚本文件仍然保留,仍可用 service httpd start 手动启动。
四、systemd 与 Unit 文件
10. 说明 /usr/lib/systemd/system/、/etc/systemd/system/、/etc/systemd/system/xxx.service.d/ 三者的优先级与用途。
答:优先级从低到高为 /usr/lib/systemd/system/(软件包自带,不要改)→ /run/systemd/system/(运行时生成,重启丢失)→ /etc/systemd/system/(管理员自定义,同名文件会完全覆盖软件包版本)。xxx.service.d/*.conf 是 drop-in 局部覆盖,只覆盖其中写明的指令,其余配置沿用原 unit,是定制官方服务的推荐方式。
11. systemctl disable 与 systemctl mask 有什么区别?什么场景必须用 mask?
答:disable 只删除 *.wants/ 中的自启链接,服务仍可被手动 start 或被其他服务的依赖关系拉起;mask 把 unit 软链到 /dev/null,任何方式都无法启动(报 Unit is masked)。当需要确保某服务绝不被依赖链意外拉起时(如安装 MySQL 前屏蔽 postfix、firewalld)必须用 mask。unmask 后还需再 enable 才能恢复自启。
12. 某服务 systemctl start 返回成功,几秒后 status 显示 inactive (dead),最可能的原因和修法?
答:Type= 写错。程序自身 fork 到后台(脚本里有 &/nohup/daemon())但 unit 写了 Type=simple,systemd 认为主进程已退出,于是杀掉整个 cgroup。修法二选一:改成 Type=forking 并正确设置 PIDFile=;或让程序以前台模式运行(如 catalina.sh run、nginx -g 'daemon off;'、redis-server --daemonize no)并保持 Type=simple(更推荐)。
13. 写一个 unit 文件,把 /usr/local/myapp/bin/myapp(前台运行,需要 JAVA_HOME,以 myapp 用户运行,崩溃后 10 秒自动重启,文件描述符上限 65535)纳入 systemd 管理并设为开机自启。
答:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/usr/local/myapp
Environment=JAVA_HOME=/usr/local/jdk1.8.0_212
EnvironmentFile=-/etc/sysconfig/myapp
ExecStart=/usr/local/myapp/bin/myapp
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=10
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
systemd-analyze verify /etc/systemd/system/myapp.service
systemctl daemon-reload
systemctl enable --now myapp
systemctl status myapp --no-pager
14. 要给 yum 安装的 nginx 增加 Restart=always 和 LimitNOFILE=65535,写出正确做法,并说明为什么不能直接编辑 /usr/lib/systemd/system/nginx.service。
答:直接编辑软件包自带文件会在 yum update nginx 时被覆盖。正确做法用 drop-in:
systemctl edit nginx.service # 打开 /etc/systemd/system/nginx.service.d/override.conf
[Service]
Restart=always
RestartSec=5
LimitNOFILE=65535
systemctl restart nginx
systemctl cat nginx # 确认 drop-in 已生效
systemctl show -p Restart,LimitNOFILE nginx
# 需要撤销时:systemctl revert nginx.service
15. systemctl daemon-reload 与 systemctl reload nginx 有什么区别?
答:daemon-reload 让 systemd 自身重新扫描并加载磁盘上的 unit 文件(新增/修改/删除 unit 后必须执行);systemctl reload nginx 是让 nginx 这个服务重读它自己的配置文件 nginx.conf(通过 unit 中的 ExecReload= 实现),不中断服务。两者作用对象完全不同。
16. systemctl list-units 与 systemctl list-unit-files 有何区别?
答:list-units 列出已加载到内存的 unit(默认只显示 active,加 --all 含 inactive);list-unit-files 列出磁盘上所有存在的 unit 文件及其 enabled/disabled/static/masked 状态,包括从未被加载过的。
五、定时器
17. 写一对 unit,实现"每天凌晨 3:10 执行 /usr/local/bin/clean.sh,错过则开机补跑,随机延迟 5 分钟以内",并给出启用与验证命令。
答:
# /etc/systemd/system/clean.service
[Unit]
Description=Daily Cleanup Job
[Service]
Type=oneshot
ExecStart=/usr/local/bin/clean.sh
Nice=19
# /etc/systemd/system/clean.timer
[Unit]
Description=Run Daily Cleanup Job
[Timer]
OnCalendar=*-*-* 03:10:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
systemd-analyze calendar "*-*-* 03:10:00" # 先验证表达式
systemctl daemon-reload
systemctl enable --now clean.timer # 注意 enable 的是 .timer
systemctl list-timers --all | grep clean
systemctl start clean.service # 手动立即跑一次
journalctl -u clean.service -n 50
18. 把下列 crontab 表达式改写成 OnCalendar:*/5 * * * *、0 2 * * *、30 2 * * 1、0 9 * * 1-5。
答:分别为 *-*-* *:0/5:00(或 *:0/5)、*-*-* 02:00:00、Mon *-*-* 02:30:00、Mon..Fri *-*-* 09:00:00。
19. cron 与 OnCalendar 在"日"和"周"同时指定时语义有何不同?
答:cron 中两者同时为数字时是或关系(满足其一即执行);OnCalendar 中星期与日期是与关系(必须同时满足)。若要在 systemd 中表达"或",需在同一 [Timer] 段写多条 OnCalendar=,取并集。
20. 只由 timer 触发的 service 需要 [Install] 段吗?可以 systemctl enable backup.service 吗?
答:不需要 [Install] 段。也不应该 enable 该 service,否则开机时它会被立即执行一次;要 enable 的是 backup.timer。
六、性能分析与排错
21. 系统开机很慢,写出定位思路与命令。
答:
systemd-analyze # 先看 kernel / initrd / userspace 三段耗时占比
systemd-analyze blame # 找耗时最长的 unit
systemd-analyze critical-chain # 找真正阻塞启动的关键依赖链(比 blame 更重要)
systemd-analyze plot > boot.svg # 可视化时序,看并行度
systemctl list-unit-files --type=service --state=enabled # 精简无用自启服务
常见结论:NetworkManager-wait-online.service(等网络)、kdump.service、postfix.service 是典型耗时项;静态 IP 环境可禁用 NetworkManager-wait-online。
22. 服务启动失败,写出标准排查动作(至少 5 条命令)。
答:
systemctl status myapp -l --no-pager # 状态、Result、退出码
journalctl -xe # 带解释的最新错误
journalctl -u myapp -n 100 --no-pager # 该服务完整上下文
systemctl cat myapp # 确认最终生效配置(含 drop-in)
systemd-analyze verify /etc/systemd/system/myapp.service # unit 语法检查
ss -tunlp | grep <端口> # 端口占用
nginx -t / httpd -t # 服务自身配置语法
getenforce; ausearch -m avc -ts recent # SELinux 拦截
df -h; df -i # 磁盘与 inode
23. 下列退出码分别代表什么:203/EXEC、217/USER、200/CHDIR、143?
答:203/EXEC 命令无法执行(路径错、无执行权限、shebang 错);217/USER User= 指定的用户不存在;200/CHDIR WorkingDirectory= 目录不存在;143 收到 SIGTERM 后退出(Java 程序常见的正常停止码,可用 SuccessExitStatus=143 声明为成功)。
24. 程序手工执行正常,systemd 启动却报 Permission denied,权限也没问题,怎么排查?
答:极可能是 SELinux。先 getenforce 确认状态,用 setenforce 0 临时切 Permissive 验证(能启动即可确认),随后立刻 setenforce 1 改回;再用 ausearch -m avc -ts recent 或 grep denied /var/log/audit/audit.log 查看被拒绝的具体操作,按需 semanage fcontext + restorecon 修正上下文、semanage port -a 放行非标准端口、或 setsebool -P 打开布尔开关。不建议直接关闭 SELinux。
25. 日志中出现 Found ordering cycle on b.service/start,会有什么后果?如何修复?
答:systemd 会任意删除环中的一个启动作业来打破循环,被删掉的服务不会启动,表现为"某服务时而启动、时而不启动"。修复:用 journalctl -b | grep -i 'ordering cycle' 找出环上的 unit,用 systemctl list-dependencies [--reverse|--after|--before] 分析依赖关系,删除环中多余的 After=/Before=(通常是自己写的 drop-in 引入的),并把不必要的 Requires= 降级为 Wants=。
26. systemctl restart 后报 Address already in use,原因和处理?
答:上一个进程没被真正停止(常见于 Type= 或 ExecStop= 写错,systemd 以为已停止而实际残留)。处理:ss -tunlp | grep <端口> 或 lsof -i :<端口> 找到残留进程并 kill,然后修正 unit 的 Type=、ExecStop=、KillMode=(可用 KillMode=control-group 确保杀掉 cgroup 内全部进程)后再启动。
27. 一台 CentOS 7 服务器上需要"开机后执行一条自定义命令",用 /etc/rc.local 需要注意什么?为什么更推荐写 Unit?
答:CentOS 7 的 /etc/rc.d/rc.local 默认没有执行权限,必须 chmod +x /etc/rc.d/rc.local 才会被 rc-local.service 执行。更推荐写 Unit 的原因:rc.local 没有状态管理(status 查不到)、没有失败自动重启、没有独立日志、无法声明依赖顺序;而 Unit 可以用 systemctl status 查状态、Restart= 自愈、日志自动进 journal、用 After=/Wants= 精确控制启动时机。
此处评论已关闭