Skip to content

Latest commit

 

History

History
363 lines (281 loc) · 13.8 KB

File metadata and controls

363 lines (281 loc) · 13.8 KB

02. 系统初始化与进程管理

PID 1 是整个用户空间的起点。理解 systemd,等于理解了现代 Linux 的一半。

这一层解决什么问题

内核启动完成后,需要有人来回答三个问题:

  1. 启动哪些服务、按什么顺序?(初始化)
  2. 谁在跑、占用多少资源、挂了怎么办?(进程管理 + 监护)
  3. 我怎么控制它们?(命令行接口)

传统上这由 init + 一堆脚本完成,现代 Linux 上由 systemd 统一接管。

一、PID 1 的特殊性

无论体系怎么变,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 —— 没有任何进程能收拾残局。

二、init 的演化

世代 名字 特点 典型发行版
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 核心概念

3.1 Unit:管理的颗粒度

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

3.2 依赖与顺序:最容易搞混的一点

systemd 里依赖和启动顺序是两件独立的事,必须分别声明:

指令 含义 比喻
Requires= 强依赖,对方失败我也失败 少了零件装不起来
Wants= 弱依赖,对方失败我照跑(最常用) 有更好,没有也行
After= 顺序:我在它之后启动 排队
Before= 顺序:我在它之前启动 插队
Conflicts= 互斥 一山不容二虎

新手最常犯的错:以为写了 Requires=A 就会在 A 之后启动。不一定!只写 Requires 是并行启动,必须同时写 After=。

还有一个反直觉点:systemd 默认是并行启动的。所谓"依赖顺序"只在你明确声明时才生效 —— 这正是它比 SysV 快的原因。

3.3 target:启动到哪一步

$ 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 多用户图形界面

3.4 socket 激活:按需启动

systemd 可以先监听端口,来连接了再启动服务。好处是服务崩溃后下一个连接自动拉起,且开机不用等它。

$ systemctl list-sockets            # 当前哪些 socket 在监听
$ systemctl cat ssh.socket

3.5 单元状态与文件位置

$ 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 等所有活跃任务完成再跑 输出装饰性内容

五、日志:journalctl

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。

六、进程管理基础

6.1 进程与线程

$ 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 空闲内核线程

6.2 信号:进程间通信的原始方式

$ 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 可能留下锁文件、损坏数据。

6.3 前后台与作业控制

$ 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

6.4 优先级与资源

$ 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/1

OOM 判定(内存不足时内核选谁杀)由 oom_score 决定:

$ cat /proc/1234/oom_score
$ cat /proc/1234/oom_score_adj     # -1000 到 1000,越小越不容易被杀

6.5 时间相关的调度

工具 适用 配置
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 nginx

常见误区

  • Requires 不等于顺序,顺序要 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 资源限制的完整参数表(MemoryHigh vs MemoryMax、CPUWeight……)
  • Alpine/OpenRC 与容器内 init(tini、dumb-init)的对比
  • 内核线程(kthread)与 /proc/PID/ 各项内容详解
  • ptrace / strace 与进程观测工具(见 15)

← 上一章:引导与内核 · 返回目录 · 下一章:文件系统与存储 →