PID 1 是整个用户空间的起点。理解 systemd,等于理解了现代 Linux 的一半。
内核启动完成后,需要有人来回答三个问题:
- 启动哪些服务、按什么顺序?(初始化)
- 谁在跑、占用多少资源、挂了怎么办?(进程管理 + 监护)
- 我怎么控制它们?(命令行接口)
传统上这由 init + 一堆脚本完成,现代 Linux 上由 systemd 统一接管。
无论体系怎么变,PID 1 永远特殊:
| 特性 | 含义 |
|---|---|
| 第一个用户态进程 | 内核直接执行它,不 fork |
| 所有进程的祖先 | 其他进程都是它的(后代) |
| 孤儿进程的收养者 | 父进程先死,它接管并负责回收 |
| 信号语义不同 | 没注册处理函数的信号,内核不会送默认给 PID 1 |
| 不能被 kill -9 | 内核保护它(kill -9 1 无效) |
$ ps -p 1 -o pid,comm,cmd # 看 PID 1 到底是谁
$ cat /proc/1/comm如果 PID 1 崩了,系统就 panic —— 没有任何进程能收拾残局。
| 世代 | 名字 | 特点 | 典型发行版 |
|---|---|---|---|
| 1 | SysV init | /etc/init.d/ 脚本 + runlevel,串行启动,慢 |
CentOS 6、老 Debian |
| 2 | Upstart | 事件驱动,并行化 | Ubuntu 6.10~14.10 |
| 3 | systemd | 并行、依赖声明、socket 激活、统一日志 | 现代主流 |
| — | OpenRC / runit / s6 | 轻量,脚本化 | Alpine、Gentoo、容器镜像 |
runlevel(SysV) 用 0-6 表示系统状态:0 关机、1 单用户、3 多用户命令行、5 多用户图形、6 重启。systemd 用 target 取代,但保留了兼容符号链接(runlevel3.target 等)。
systemd 管的一切东西都叫 unit,由 /usr/lib/systemd/system/(发行版自带)或 /etc/systemd/system/(管理员)下的文件定义:
| 扩展名 | 类型 | 例子 |
|---|---|---|
.service |
服务 | nginx.service |
.socket |
套接字(按需唤醒服务) | sshd.socket |
.timer |
定时器(替代 cron) | apt-daily.timer |
.target |
一组 unit 的集合(类似 runlevel) | multi-user.target |
.mount / .automount |
挂载点 | -.mount |
.path |
路径监控 | cups.path |
.slice / .scope |
cgroup 分组 | user-1000.slice |
.device |
设备(从 udev 生成) | dev-sda1.device |
systemd 里依赖和启动顺序是两件独立的事,必须分别声明:
| 指令 | 含义 | 比喻 |
|---|---|---|
Requires= |
强依赖,对方失败我也失败 | 少了零件装不起来 |
Wants= |
弱依赖,对方失败我照跑(最常用) | 有更好,没有也行 |
After= |
顺序:我在它之后启动 | 排队 |
Before= |
顺序:我在它之前启动 | 插队 |
Conflicts= |
互斥 | 一山不容二虎 |
新手最常犯的错:以为写了
Requires=A就会在 A 之后启动。不一定!只写Requires是并行启动,必须同时写After=。还有一个反直觉点:systemd 默认是并行启动的。所谓"依赖顺序"只在你明确声明时才生效 —— 这正是它比 SysV 快的原因。
$ systemctl get-default # 默认 target
$ sudo systemctl set-default multi-user.target # 改成命令行启动
$ systemctl list-units --type=target # 所有 target常用 target 对照:
| target | 等价 runlevel | 用途 |
|---|---|---|
poweroff.target / reboot.target |
0 / 6 | 关机 / 重启 |
rescue.target |
1 | 单用户,根可读写,少量服务 |
emergency.target |
— | 最小救援,根只读,几乎无服务 |
multi-user.target |
3 | 多用户命令行(服务器常态) |
graphical.target |
5 | 多用户图形界面 |
systemd 可以先监听端口,来连接了再启动服务。好处是服务崩溃后下一个连接自动拉起,且开机不用等它。
$ systemctl list-sockets # 当前哪些 socket 在监听
$ systemctl cat ssh.socket$ systemctl status nginx # 状态(含 cgroup、最近日志)
$ systemctl is-enabled nginx # 开机自启?
$ systemctl is-active nginx # 现在在跑?
$ systemctl cat nginx # 打印这个 unit 的完整定义+路径
$ systemctl show nginx -p MemoryMax # 看某个具体属性unit 文件查找优先级(后者覆盖前者):
/etc/systemd/system/ ← 管理员,最高优先级
/run/systemd/system/ ← 运行时生成
/usr/lib/systemd/system/ ← 发行版自带(RHEL 老版在 /lib/systemd/system)
覆盖而不是直接改发行版文件(推荐做法):
$ sudo systemctl edit nginx # 生成 /etc/systemd/system/nginx.service.d/override.conf最小可用模板:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Demo App
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
Environment="PORT=8080"
ExecStart=/opt/myapp/bin/myapp --port 8080
Restart=on-failure
RestartSec=5s
MemoryMax=512M
CPUQuota=50%
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target生效流程(记住这三步,缺一步都不生效):
$ sudo systemctl daemon-reload # ① 读盘加载新 unit(改动 unit 文件后必须执行)
$ sudo systemctl start myapp # ② 立即启动
$ sudo systemctl enable myapp # ③ 开机自启(创建符号链接)
$ systemctl status myapp # ④ 验证
$ journalctl -u myapp -f # ⑤ 跟踪日志Type= 的常见取值:
| 值 | 含义 | 适用 |
|---|---|---|
simple |
ExecStart 启动的就是主进程(默认) | 大多数前台程序 |
forking |
进程 fork 后父进程退出(老式 daemon) | 传统守护进程 |
oneshot |
执行完就退出,不算常驻 | 初始化脚本 |
notify |
程序主动通知 systemd 就绪 | 支持 sd_notify 的程序 |
dbus |
拿到 D-Bus 名字算就绪 | D-Bus 服务 |
idle |
等所有活跃任务完成再跑 | 输出装饰性内容 |
systemd 自带日志系统(journal),把内核日志、服务 stdout/stderr、syslog 全部收进一个二进制索引里,可以按服务/时间/优先级过滤。这是排查服务问题的第一现场。
$ journalctl -u nginx # 某服务的日志
$ journalctl -u nginx -f # 实时跟踪
$ journalctl -b # 本次开机以来
$ journalctl -b -1 # 上一次开机(排查"上次为什么崩")
$ journalctl -p err -b # 只看本次开机的 error 及以上
$ journalctl --since "10 min ago" --until "now"
$ journalctl _PID=1234 # 按进程过滤
$ journalctl -k # 只看内核消息(等价 dmesg)
$ journalctl --disk-usage # 日志占了多少空间
$ sudo journalctl --vacuum-size=500M # 清理,保留 500M持久化配置(默认可能只存内存,重启即丢):
# /etc/systemd/journald.conf ⚠️ 改 /etc 前先备份
[Journal]
Storage=persistent
SystemMaxUse=1G$ sudo mkdir -p /var/log/journal && sudo systemd-journal-flush想按关键字全文搜索:
journalctl -g "regex"(需要较新版本)。老系统可以journalctl | grep。
$ ps aux # 全部进程快照(BSD 风格)
$ ps -ef # 全部进程快照(SysV 风格)
$ ps -eo pid,ppid,user,%cpu,%mem,stat,cmd --sort=-%cpu | head
$ pstree -p # 进程树(看父子关系)
$ pgrep -a nginx # 按名字找 PID
$ pidof nginx
$ top # 实时(按 P 排 CPU、M 排内存、q 退出)
$ htop # 更友好的交互版(可能需要安装)
$ cat /proc/1234/status # 单进程详情
$ cat /proc/1234/cmdline | tr '\0' ' ' # 完整命令行ps 的 STAT 列(进程状态)值得记住:
| 状态 | 含义 |
|---|---|
R |
运行中 / 可运行 |
S |
可中断睡眠(等待事件,最常见) |
D |
不可中断睡眠(通常在等 IO,杀不掉) |
Z |
僵尸(已死但父进程没回收) |
T |
停止(被 Ctrl+Z 或信号暂停) |
I |
空闲内核线程 |
$ kill -l # 列出所有信号
$ kill 1234 # 默认 SIGTERM(15),请求退出
$ kill -9 1234 # SIGKILL,强杀,不可被捕获
$ kill -HUP 1234 # SIGHUP,很多服务用它表示"重载配置"
$ pkill -u axu firefox # 按条件批量杀常用信号:
| 信号 | 编号 | 语义 |
|---|---|---|
SIGTERM |
15 | 请体面退出(可捕获、可清理) |
SIGKILL |
9 | 立即消失(不可捕获,无法清理,慎用) |
SIGHUP |
1 | 挂起,惯例表示"重载配置" |
SIGINT |
2 | Ctrl+C |
SIGSTOP/SIGCONT |
19/18 | 暂停 / 继续 |
正确顺序:先
SIGTERM,等它自己退出(systemctl stop就是干这个,超时才SIGKILL)。直接上-9可能留下锁文件、损坏数据。
$ long_task & # 后台运行
$ jobs # 看当前 shell 的后台作业
$ Ctrl+Z # 把前台作业挂起
$ bg %1 # 挂起的作业转到后台继续
$ fg %1 # 调回前台
$ disown %1 # 脱离当前 shell(shell 退出也不受影响)
$ nohup cmd > out.log 2>&1 & # 忽略 SIGHUP,日志重定向
$ setsid cmd & # 新会话运行,彻底脱离终端更规范的做法是用
systemd-run或写一个.service,让它由 systemd 托管(自动重启、日志进 journal)。$ systemd-run --user --unit=mytask /path/to/cmd
$ nice -n 10 cmd # 以低优先级启动(-20 最高,19 最低)
$ renice -n 5 -p 1234 # 调整已有进程
$ ionice -c 3 -p 1234 # IO 优先级(best-effort/idle)
$ taskset -c 0,1 cmd # 绑定到 CPU 0/1OOM 判定(内存不足时内核选谁杀)由 oom_score 决定:
$ cat /proc/1234/oom_score
$ cat /proc/1234/oom_score_adj # -1000 到 1000,越小越不容易被杀| 工具 | 适用 | 配置 |
|---|---|---|
| cron | 老牌定时任务,分钟级 | crontab -e、/etc/cron.d/ |
| anacron | 补跑错过的任务(笔记本/不定时开机) | /etc/anacrontab |
| systemd timer | 现代方案,支持单调时钟、随机延迟、依赖 | *.timer |
| at | 一次性定时 | at 22:00 |
$ crontab -l # 当前用户的任务
$ sudo crontab -l -u root # root 的任务
$ systemctl list-timers # 所有 systemd 定时器(下次触发时间)
$ systemd-analyze calendar "Mon *-*-* 03:00:00" # 验证时间表达式cron 表达式五段:分 时 日 月 周
17 3 * * 1-5 /opt/backup.sh # 工作日 3:17 执行
*/5 * * * * /opt/check.sh # 每 5 分钟
@reboot /opt/onboot.sh # 每次启动
⚠️ cron 环境陷阱:cron 的 PATH 极简、没有你 shell 里 source 的环境变量。脚本里一律用绝对路径,并在脚本开头显式export PATH。这是"手动跑没问题、cron 跑就报错"的头号原因。
| 症状 | 命令 |
|---|---|
| 服务起不来 | systemctl status xxx → journalctl -u xxx -n 50 |
| 改了 unit 不生效 | 忘了 systemctl daemon-reload |
| 开机卡在某服务 | systemd-analyze blame、systemd-analyze critical-chain |
| 服务被杀 | dmesg | grep -i oom、systemctl show xxx -p MemoryMax |
| 进程杀不掉 | 看 STAT 是否 D(等 IO),或本身是 PID 1 |
| 端口被占 | ss -lntp(见 07) |
| 上次为什么崩 | journalctl -b -1 -p err |
$ systemd-analyze # 开机总耗时
$ systemd-analyze blame # 各服务耗时排行(找启动慢的元凶)
$ systemd-analyze critical-chain # 关键启动链
$ systemctl list-units --failed # 所有失败单元 ← 排障第一命令
$ systemctl list-dependencies nginxRequires不等于顺序,顺序要After=。enable不等于start,反之亦然。- 忘了
daemon-reload是"改了没反应"的第一号原因。 kill -9不是万能药,优先给SIGTERM。Z(僵尸)进程不用杀,要处理它的父进程;僵尸本身只占一个表项。- cron 不是 systemd timer,前者无日志无依赖,复杂调度请用后者。
- systemd 完整依赖图与启动流程(default.target 到具体 service 的解析过程)
-
systemd-analyze plot生成可视化启动图 - systemd user session 与
logind(systemctl --user、会话生命周期) - 写一个
Type=notify的服务并接入sd_notify - cgroup 资源限制的完整参数表(
MemoryHighvsMemoryMax、CPUWeight……) - Alpine/OpenRC 与容器内 init(tini、dumb-init)的对比
- 内核线程(kthread)与
/proc/PID/各项内容详解 -
ptrace/strace与进程观测工具(见 15)