系统启动流程与 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 chkconfig chkconfig --level 5 服务名 off/on
CentOS 7 systemctl systemctl 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

关键点:

  1. # chkconfig: 与 # description: 两行注释缺一不可。
  2. 脚本必须支持 start|stop|restart|status 四个参数,退出码要正确(0 成功、非 0 失败)。
  3. /var/lock/subsys/<服务名> 锁文件必须在 start 时创建、stop 时删除,关机脚本靠它判断是否需要停止服务。
  4. 引入 /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 文件后的收益:

  1. 不再需要往 /etc/rc.local 追加启动命令,systemctl enable tomcat 即是标准自启方式;rc.local 无状态管理、无失败重启、无日志,属于遗留兼容手段(且 CentOS 7 上 /etc/rc.d/rc.local 还需手动加执行权限才生效)。
  2. 不必修改 startup.sh 去硬编码 JAVA_HOME,环境变量由 Environment=/EnvironmentFile= 统一注入,软件包升级不丢配置。
  3. 崩溃后由 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 -e 30 秒搞定。
  • 需要跨平台/跨发行版可移植,包括老旧的 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   # 谁在本服务之前启动

修复要点:

  1. 找出环中最不必要的那条 After=/Before= 并删除,通常是自己写的 drop-in 引入的。
  2. 区分"顺序"与"依赖":只想控制先后就用 After=,不要滥用 Requires=。
  3. 把强依赖 Requires= 换成弱依赖 Wants=,可打断大部分循环且不影响功能。
  4. 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 关机/定时重启/取消

九、注意事项

  1. 修改 /etc/rc*.d/ 中的服务链接时,注意文件名前缀 S/K 及启动顺序数字;正常情况应交给 chkconfig 维护,不要手工建链接。
  2. service 最终调用 /etc/init.d/ 脚本;在 CentOS 7 上它会被重定向到 systemctl,不同发行版建议优先使用 systemctl。
  3. 自写 SysV 脚本必须包含 # chkconfig: 与 # description: 两行注释,否则 chkconfig --add 会失败;并且要在 start 时创建 /var/lock/subsys/<服务名>,否则关机时不会调用 stop。
  4. 默认运行级别不要设为 0(开机即关机)或 6(无限重启)。
  5. CentOS 7 上修改 /etc/inittab 无效,必须用 systemctl set-default。
  6. systemctl isolate 会停掉目标 target 不需要的所有服务,生产环境慎用。
  7. 新增或修改 /etc/systemd/system/ 下的 unit 文件后,必须执行 systemctl daemon-reload,否则 systemd 仍使用旧配置。
  8. systemctl edit <unit> 创建的 drop-in 覆盖文件无需手动 daemon-reload,systemd 会自动检测;手工创建 .d/*.conf 时则需要自己 reload。
  9. 不要直接修改 /usr/lib/systemd/system/ 下的文件,软件包升级会覆盖;用 drop-in 或复制到 /etc/systemd/system/。
  10. drop-in 中覆盖 ExecStart= 等可累加指令时,必须先写一行空值清空再赋新值,否则会变成追加、启动两个进程。
  11. Type=forking 的服务必须在 [Service] 段指定 PIDFile=,否则 systemctl status 可能无法正确跟踪进程状态。
  12. ExecStart= 必须使用绝对路径,且 systemd 不解析 Shell 语法(管道、重定向、通配符);需要时用 /bin/bash -c "..." 包裹。
  13. 被 systemctl mask 的服务无法以任何方式启动(包括依赖触发),如需恢复使用 systemctl unmask,之后还要重新 enable 才会自启。
  14. 只被 timer 触发的 .service 不要 enable(否则开机就会执行一次),要 enable 的是 .timer。
  15. 使用 systemd timer 时,OnCalendar 表达式应通过 systemd-analyze calendar 验证,避免语法错误;单调定时器不支持 Persistent=true。
  16. /etc/rc.local 在 CentOS 7 上默认无执行权限,需 chmod +x /etc/rc.d/rc.local 才生效;新需求应优先写 Unit 文件。
  17. /etc/security/limits.conf 对 systemd 启动的服务无效,文件描述符上限要写在 unit 的 LimitNOFILE=。
  18. 触发 StartLimitBurst/StartLimitInterval 限制后服务会拒绝启动,必须先 systemctl reset-failed <unit>。
  19. network.target 不代表网络可用,需要网络就绪的服务应依赖 network-online.target 并同时写 Wants=network-online.target。
  20. 排查异常重启前需确认 journal 已持久化(CentOS 7 默认易失),详见 08-02-日志管理.md。

十、练习

一、启动流程

1. 按顺序写出从通电到出现登录提示符的九个阶段,并说明 initramfs 的作用。

答:

  1. 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= 精确控制启动时机。

最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏