Linux 学习笔记

一份完整的 Linux 学习笔记。学多少记多少,重点讲"为什么这么设计"(费曼式 📌 讲解),配 draw.io 图。

怎么用这份笔记

  1. 学习/复习 → 顺序看正文,重点看 📌 类比和"为什么"
  2. 查命令 → 翻 附录 A 速查手册
  3. 检验 → 做 附录 B 自测题,盖住答案自答
  4. 落地 → 跟 附录 C 实战项目做一遍,把知识串起来

图表在 /Linux学习笔记/diagrams/ 目录。图注名即原图文件名(如图注"图 1 · Linux 架构分层"对应 图1_Linux架构分层.d2 / 图1_Linux架构分层.d2.svg),改图请编辑同名 .d2 源文件后运行 scripts/build-d2.ps1 重新渲染。

1 · Linux 概述与架构

1.1 Linux 是什么

  • Linux 严格来说只是内核,由 Linus Torvalds 于 1991 年创建,负责管理 CPU、内存、磁盘、网卡等硬件资源
  • GNU/Linux 才是完整的操作系统 = Linux 内核 + GNU 用户空间工具(bash、ls、grep…)
  • 发行版 (Distro) = 内核 + GNU 工具 + 包管理器 + 桌面环境,开箱即用

主流发行版两大流派

流派 包管理器 代表发行版
Debian 系 apt Ubuntu · Kali · Linux Mint
Red Hat 系 dnf / yum RHEL · Fedora · Rocky Linux

1.2 整体架构(分层模型)

图 1 \xb7 Linux 架构分层

每层说明

层 作用 你能看到的例子
用户应用程序 最顶层,用户直接交互 浏览器、终端、自己写的脚本
GNU 工具链/Shell 封装 syscall 的命令行工具 cat、ls、bash、gcc
系统调用 (syscall) 内核对外暴露的 API,是用户空间进入内核的唯一合法入口 open()、read()、write()、fork()
Linux 内核 唯一能直接操控硬件的软件 进程调度、内存分配、文件系统、驱动
硬件 物理资源 CPU、RAM、磁盘、网卡

关键规则

用户程序永远不能直接操控硬件,必须通过 syscall 请求内核代劳。这是 Linux 安全隔离的基础。

深入理解:syscall 为什么存在(从设计者视角)

TIP

📌 打个比方:内核像银行金库,硬件是金库里的钱。普通职员(你的程序)不能自己进金库拿钱,必须填一张单子(syscall)递给柜员(内核),柜员核验后帮你取。柜台窗口(syscall 接口)就是金库唯一的合法入口。

设计者面对的问题:如果让每个程序都能直接读写磁盘、内存、网卡,会怎样?

  • 一个写错的程序就能覆盖另一个程序的内存,整机崩溃
  • 恶意程序能直接读走别人的数据,毫无安全可言
  • 两个程序同时写同一块磁盘,数据全乱

解法:CPU 硬件层面分两种模式

用户态 (User Mode) 程序平时待的地方,权限受限,碰不到硬件 内核态 (Kernel Mode) 只有内核能进,拥有操控硬件的全部权限

程序想读文件时,执行一条特殊 CPU 指令(如 syscall),CPU 从用户态切换到内核态,跳进内核代码执行;内核做完事,再切回用户态把结果交给程序。这一进一出叫"上下文切换"。

TIP

📌 所以 syscall 不是普通函数调用:普通函数调用是在你自己的代码里跳转;syscall 是"敲内核的门",伴随一次 CPU 模式切换,开销更大。这也是为什么频繁的小 I/O 慢——每次都在敲门。理解这点,后面学"缓冲区""批量读写"为什么能提速就顺理成章了。

1.3 一个命令的完整旅程

以 cat /etc/os-release 为例,图中红色注解展示了从输入到输出穿越每一层的过程:

  1. bash 解析:识别 cat 是外部命令,去 $PATH 找到 /bin/cat
  2. cat 运行:进入 GNU 工具层,程序开始执行
  3. 发起 syscall:cat 调用 open("/etc/os-release") 和 read() 进入内核
  4. 内核处理:通过文件系统驱动,找到磁盘上对应的数据块
  5. 磁盘 I/O:硬件层完成读取,数据沿原路返回,最终 write() 到终端

1.4 常用探索命令

$ cat /etc/os-release   # 查看发行版身份
$ uname -r              # 内核版本
$ uname -m              # CPU 架构
$ uptime                # 系统运行时长

2 · 文件系统与目录结构

2.1 核心规则:一棵树,没有盘符

Linux 只有一个根 /,所有文件都在这棵树下。这一点确实像 Windows 只有一个 C 盘——你在 C 盘里照样可以建无数子目录。

真正的区别在于额外的磁盘怎么接入:

  • Windows:插一块新磁盘或分区,系统分配一个新盘符(D:\ E:\),出现一个新的根——目录树变多了
  • Linux:插一块新磁盘,你把它"挂载"到现有树里的某个空目录(如 /data)——目录树还是一棵,只是那个子目录的内容现在来自新磁盘

注意:Linux 有分区,只是分区不以盘符暴露。你看到的 /、/home、/data 背后可能各自是独立的分区(各有自己的文件系统和 inode 表),只是被挂载进同一棵目录树,对用户透明。df -h 可以看到当前所有挂载的分区。

/ ← 唯一的根 ├── home/ ← 可能是另一块磁盘挂载进来的 ├── mnt/usb/ ← U盘挂载后就在这里 └── ...
TIP

📌 为什么这么设计?Windows 盘符把"物理设备"这个细节暴露给了用户和程序——你得知道文件在哪块盘。Linux 把物理设备藏在统一目录树后面:程序只管访问 /data,至于背后是本地盘、U盘还是网络存储,全由挂载这个动作透明化了。

好处:路径稳定。把 /data 从一块盘迁移到更大的盘?重新挂载,路径不变,上层程序完全无感。Windows 里 D 盘改成 E 盘,所有硬编码路径的程序就可能找不到文件了。

2.2 各目录作用

图 2 \xb7 FHS 目录结构

2.3 几个重要目录详解

目录 关键点
/etc 纯文本配置,改配置就改这里的文件,如 /etc/passwd(用户列表)、/etc/ssh/sshd_config
/proc 不是真实文件,是内核暴露给用户空间的实时数据接口,读 /proc/cpuinfo 就是在问内核
/dev 设备也是文件,/dev/sda 是第一块磁盘,/dev/null 是数据黑洞
/var/log 所有日志在这里,出问题先来这里查
/tmp 任何人可写,重启后清空,不要在这里放重要文件
TIP

📌 “一切皆文件”到底是什么意思?这是 Linux 最核心的设计思想。设计者让磁盘、键盘、内核状态……全都伪装成“文件”,这样你只需学会 open/read/write 几个操作,就能操作万物。

  • 看 CPU 信息?cat /proc/cpuinfo(读文件)
  • 丢掉输出?> /dev/null(写文件)
  • 读磁盘?/dev/sda(还是文件)

不用为每种设备学一套新 API——这就是为什么 cat、grep 这些“只会处理文件”的小工具能通吃整个系统。

📌 /proc 怎么理解?它里的文件不占磁盘,是内核实时生成的“假文件”。你 cat /proc/cpuinfo 的一瞬间,内核才临时把 CPU 信息组装成文本给你。所以 /proc 里看到的永远是“此刻”的状态,是你与内核对话的窗口。

2.4 常用探索命令

$ ls /                         # 看根目录
$ ls -la / | grep "^l"         # 查看根目录下的符号链接
$ df -h                        # 看磁盘挂载情况
$ cat /proc/cpuinfo | head -20  # 从内核虚拟文件读 CPU 信息
$ ls /dev/sd*                  # 看磁盘设备文件

2.5 inode 与链接(理解"文件"的本质)

要真正理解 Linux 文件,得先认识 inode。

TIP

📌 inode 是什么?磁盘上一个文件其实分两部分存:文件内容(真正的数据块)和 inode(记录文件的元信息:大小、权限、所有者、时间戳,以及数据块在磁盘哪个位置)。inode 里唯独没有文件名。

那文件名在哪?在目录里。目录本质是一张表,记录"文件名 → inode 号"的对应关系。所以"打开文件"的真实过程是:文件名 →(查目录)→ inode 号 →(查 inode)→ 数据块。

ls -i file.txt          # 看文件的 inode 号
df -i                   # 看各分区 inode 使用情况
stat file.txt           # 看 inode 详细信息(大小、权限、三个时间)

硬链接 vs 软链接(高频考点)

理解了 inode,两种链接就一目了然:

硬链接:另起一个文件名,指向【同一个 inode】 软链接:一个独立的小文件,内容是【另一个文件的路径】
ln  原文件 硬链接名      # 硬链接(不加 -s)
ln -s 原文件 软链接名     # 软链接(symbolic,加 -s)
硬链接 软链接(符号链接)
本质 同一 inode 的另一个名字 存"目标路径"的独立文件
删原文件后 仍可用(inode 还在,引用计数减1) 失效(指向的路径没了,成死链接)
跨文件系统 不行(inode 号只在本分区唯一) 可以
链接目录 不允许 可以
ls -l 显示 和普通文件一样 链接名 -> 目标
TIP

📌 打个比方:硬链接像"一个人有两个身份证号都指向同一个真人"——销毁一个号,人还在。软链接像"一张写着某人家地址的便利贴"——人搬走了(删原文件),便利贴就成了无效地址。

📌 ls -l 第2列那个数字(硬链接数)现在懂了吧?它就是"有多少个文件名指向这个 inode"。每 ln 一次加1,删一个名字减1,减到 0 内核才真正回收磁盘数据。这也是为什么删文件叫 unlink——删的是"名字到 inode 的链接",不是数据本身。

为什么要设计两种链接?

硬链接不是"设计出来的功能",而是 inode 机制的自然产物。目录本质是"文件名 → inode 号"的映射表,往表里多加一条记录就是硬链接——内核几乎不用额外代价。

但硬链接有两个根本限制:

  1. 不能跨文件系统:inode 号只在同一个分区内唯一,换个分区 inode 号可能重复,跨分区硬链接没有意义
  2. 不能链目录:如果允许,目录树会出现环(A 目录的硬链接在 B 目录下,B 目录又在 A 里),find、du 这类递归工具会无限循环

软链接是为了补这两个洞而专门设计的:它是一个独立的小文件,内容就是一串路径字符串,与 inode 无关,所以能跨分区、能链目录。代价是多一次路径解析,以及原文件删了就变死链接。

硬链接的实际用途

日常 90% 用软链接,但硬链接有一个重要的杀手级用途:节省空间的增量备份。

rsync --link-dest 备份原理:

# 第一次完整备份
rsync -a /data/ /backup/2026-01-01/

# 第二次备份:没变的文件创建硬链接(不复制),只有变化的文件才真正拷贝
rsync -a --link-dest=/backup/2026-01-01/ /data/ /backup/2026-01-02/

结果是:/backup/2026-01-01/ 和 /backup/2026-01-02/ 看起来都是"完整备份",但没变的文件只占一份磁盘空间(两个目录名指向同一个 inode)。macOS 的 Time Machine 就是这个原理。

软链接不行,是因为删掉某天的备份目录后,其他天的软链接就全断了。硬链接则互相独立,删哪天都不影响其他天。

3 · 核心命令

3.0 命令的通用结构

命令名 [选项] [参数] ls -alh /etc find /var -name "*.log" ↑ ↑ 短选项可合并 操作对象 -a -l -h = -alh
  • 短选项 -x:单字母,可合并写 -alh
  • 长选项 --word:单词形式,不可合并,更易读,如 --all
  • 参数:操作对象,通常是路径或字符串
TIP

📌 打个比方:一条命令就是一句话。命令 是动词(做什么),选项 是副词(怎么做),参数 是宾语(对谁做)。 比如 ls -l /etc = "列出(动词)/etc(宾语),用详细方式(副词)"。理解这个结构,看任何陌生命令都不慌——先找动词,再看它对谁、怎么做。

图 3 \xb7 核心命令分类

3.1 ls — 列出目录内容

原理

ls 调用 getdents() 系统调用读取目录项,本质是读一个特殊文件(目录也是文件)。

输出解读

$ ls -alh /etc
总用量 1.1M drwxr-xr-x 130 root root 12K 6月 17 10:23 . drwxr-xr-x 20 root root 4.0K 6月 10 08:15 .. -rw-r--r-- 1 root root 3.0K 1月 15 2023 adduser.conf lrwxrwxrwx 1 root root 14 3月 1 2023 mtab -> ../proc/self/mounts ↑ ↑ ↑ ↑ ↑ ↑ ↑ 权限字符串 硬链 所有者 所属组 大小 最后修改时间 文件名

第1列权限字符串 分4段(下节权限章节详讲):

- rw- r-- r-- ↑ ↑↑↑ ↑↑↑ ↑↑↑ │ │ │ └── 其他人权限 │ │ └────── 所属组权限 │ └────────── 所有者权限 └───────────── 文件类型:- 普通文件 d 目录 l 符号链接 b 块设备

第2列硬链接数:目录至少为2(本身 + .),普通文件通常为1。

TIP

📌 为什么 ls 默认不显示隐藏文件?因为以 . 开头的文件多是配置文件(如 .bashrc),平时不该被误改、误删,藏起来更清爽。这不是"加密",只是"眼不见为净"——加 -a 就全看见了。

📌 -h 为什么必须配合 -l?因为只有 -l 长格式才会显示文件大小这一列,-h 是把那一列的 4096 美化成 4.0K。没有 -l,根本没有大小列可美化,-h 就无处施展。

常用选项

选项 含义 记忆规律
-a 显示隐藏文件(以 . 开头的文件) all — 全部
-l 长格式,显示权限、大小、时间等详细信息 long — 长格式
-h 文件大小用人类可读格式(K/M/G),必须配合 -l human-readable
-S 按文件大小降序排列 Size — 大写,影响排序
-t 按修改时间降序排列(最新的在前) time
-r 逆序排列(常与 -t 或 -S 组合) reverse
-R 递归列出子目录 Recursive — 大写 = 小写的升级版(影响范围更大)

应用场景

ls -alh          # 日常查看当前目录,包含隐藏文件和大小
ls -lSh /var     # 找出 /var 下最大的文件
ls -lt /var/log  # 看最近修改的日志文件(最新在前)
ls -la | grep "^d"  # 只列出目录(权限首字符为d)

3.2 cd / pwd — 路径导航

pwd — 打印当前工作目录

$ pwd
/home/hisos/projects

原理:读取进程的当前工作目录(内核为每个进程维护一个 cwd 属性)。

cd — 切换目录

cd /etc          # 绝对路径(从根开始)
cd ../logs       # 相对路径(.. 是上级目录)
cd ~             # 回家目录(~ 展开为 $HOME)
cd -             # 切回上一个目录(shell 记录了 $OLDPWD)
cd               # 不带参数 = cd ~

绝对路径 vs 相对路径:

当前在 /home/hisos/projects/ 绝对路径:/etc/nginx/nginx.conf ← 从 / 开始,任何位置都有效 相对路径:../../etc/nginx.conf ← 从当前位置算,更短但依赖位置
TIP

📌 打个比方:绝对路径像写完整地址"中国北京市朝阳区XX路1号"——在任何地方寄信都能送到;相对路径像说"往前走两个路口右拐"——只有知道你现在站哪才有意义。

什么时候用哪个:脚本里永远用绝对路径(脚本可能在任何目录被执行,相对路径会算错);手动敲命令图省事用相对路径。这是新手脚本最常见的 bug 来源。

3.3 mkdir / touch / rm / cp / mv

mkdir — 创建目录

mkdir dir                # 创建单层目录,父目录必须已存在
mkdir -p a/b/c/d         # -p:递归创建,父目录不存在则一起创建
mkdir -m 755 mydir       # -m:指定创建时的权限

场景:mkdir -p 是脚本中最常用的,避免因父目录不存在而报错。

touch — 创建空文件 / 更新时间戳

touch file.txt           # 文件不存在:创建空文件;已存在:更新访问和修改时间
touch -t 202301011200 f  # 手动设置时间戳为 2023-01-01 12:00

本质:调用 open() + utimens() 系统调用。常用于让 make 认为文件被修改过,触发重新编译。

cp — 复制

cp file.txt backup.txt       # 复制文件
cp -r src/ dst/              # -r/--recursive:复制目录(必须加,否则报错)
cp -p file.txt backup.txt    # -p:保留权限、时间戳、所有者
cp -a src/ dst/              # -a = -r -p --no-dereference,最完整的复制
cp -i file.txt dst/          # -i:目标已存在时询问(interactive)

-a vs -r 的区别:-r 只递归复制,-a 还保留所有元信息(权限、符号链接等),备份时用 -a。

mv — 移动 / 重命名

mv old.txt new.txt           # 重命名(同目录)
mv file.txt /tmp/            # 移动到其他目录
mv -i src dst                # -i:目标已存在时询问

原理:同一文件系统内 mv 只修改目录项(极快,不复制数据);跨文件系统则等于 cp + rm。

rm — 删除

rm file.txt                  # 删除文件
rm -r dir/                   # -r:递归删除目录
rm -f file.txt               # -f:强制删除,不存在也不报错
rm -rf dir/                  # 最危险的命令,无提示递归强删
rm -i *.txt                  # -i:逐个询问确认
WARNING

🚧 rm -rf / 会删光整个系统。现代系统对此有保护,但 rm -rf /important/ 一样没救。删前先 ls 确认。

3.4 cat / less / head / tail — 文本查看

cat — 输出文件内容

cat file.txt                 # 输出全部
cat -n file.txt              # -n:显示行号
cat file1 file2 > merged.txt # 拼接两个文件(concatenate 的本意)
cat /dev/null > file.txt     # 清空文件内容(重定向空设备)

场景:短文件用 cat,长文件用 less(cat 会一下刷屏)。

less — 分页查看

less file.txt

交互操作(less 进入后的按键):

按键 动作
空格 / f 向下翻一页
b 向上翻一页
g 跳到文件开头
G 跳到文件末尾
/pattern 向下搜索(n 下一个,N 上一个)
q 退出

为什么叫 less:less 是对早期 more 命令的改进,more 只能向下翻,less 可以双向,所以"less is more"。

head / tail

head -n 20 file.txt          # 前20行,-n 可缩写为 -20
tail -n 20 file.txt          # 后20行
tail -f /var/log/syslog      # -f(follow):文件新增内容实时显示,看日志神器
tail -F /var/log/syslog      # -F:文件被轮转重建后也继续追踪

tail -f 的原理:内部用 inotify(Linux 内核文件监控接口)监听文件变化,有新数据写入就打印。

3.5 grep — 搜索文本内容

原理

grep 逐行读取文件,用正则表达式匹配,打印匹配的行。名字来自 ed 编辑器命令 g/re/p(globally search regular expression and print)。

输出解读

$ grep -n "root" /etc/passwd
1:root:x:0:0:root:/root:/bin/bash ↑ ↑ 行号 匹配的完整行(匹配部分默认高亮)

常用选项

选项 含义 记忆规律
-n 显示行号 number
-i 忽略大小写 case-insensitive
-v 反向匹配(输出不匹配的行) invert
-r 递归搜索目录下所有文件 recursive
-l 只输出文件名,不输出匹配行 list(只列清单)
-c 只输出匹配行数 count
-E 使用扩展正则(支持 + ? ` `)
--color 高亮匹配部分(终端默认已开启) 直接看词义

应用场景

grep -n "error" /var/log/syslog          # 找日志中的错误行
grep -ri "password" /etc/               # 不区分大小写递归搜配置
grep -v "^#" /etc/ssh/sshd_config       # 过滤注释行,只看有效配置
grep -E "^(root|hisos)" /etc/passwd     # 正则:以root或hisos开头的行
ps aux | grep nginx                      # 配合管道查找进程

3.6 find — 按条件查找文件

原理

find 从指定目录开始递归遍历整个目录树,对每个文件测试所有条件,通过则执行动作(默认打印路径)。

语法

find [起始路径] [条件...] [动作]

条件详解

# 按名称(支持通配符,必须加引号)
find /etc -name "*.conf"
find /etc -name "SSH*"        # 大小写敏感
find /etc -iname "ssh*"       # -iname:忽略大小写

# 按类型
find . -type f                # f=普通文件
find . -type d                # d=目录
find . -type l                # l=符号链接

# 按时间(数字前的 + 表示"超过",- 表示"不超过",无符号表示"恰好")
find . -mtime -7              # 最近7天内修改过
find . -mtime +30             # 30天前修改的(可能是过期文件)
find . -newer /tmp/ref.txt    # 比某文件更新

# 按大小
find . -size +100M            # 大于100MB(M=MB,k=KB,G=GB)
find . -size -1k              # 小于1KB

# 按权限
find . -perm 644              # 权限恰好是644
find . -perm -u+x             # 所有者有执行权限的文件

# 组合条件(默认AND,-o 是OR,! 是NOT)
find /var -name "*.log" -size +10M    # 大于10M的log文件
find . -name "*.tmp" -o -name "*.bak" # tmp 或 bak 文件

动作

find . -name "*.log" -print           # 打印路径(默认)
find . -name "*.log" -delete          # 直接删除(危险,先 -print 确认)
find . -name "*.sh" -exec chmod +x {} \;  # 对每个结果执行命令
# {} 代表当前找到的文件,\; 是 -exec 的结束符

应用场景

# 找大文件,排查磁盘占用
find / -size +500M -type f 2>/dev/null

# 清理30天前的临时文件
find /tmp -mtime +30 -type f -delete

# 找所有 SUID 文件(安全审计)
find / -perm -4000 -type f 2>/dev/null

3.7 管道与重定向 — 数据流控制

三个标准流

每个进程启动时,内核自动为它打开三个文件描述符:

键盘 ──→ [0] stdin ──→ 进程 ──→ [1] stdout ──→ 终端 └──→ [2] stderr ──→ 终端
TIP

📌 打个比方:想象每个程序身上插着三根管子:

  • **stdin(0)**是"进料口",默认接键盘
  • **stdout(1)**是"成品出口",默认流到屏幕
  • **stderr(2)**是"次品/报错出口",默认也流到屏幕

为什么要把正常输出和错误输出分成两根管?这样你可以"把成品存起来、报错单独看"——比如 find / -name foo > 结果.txt 2>/dev/null,把找到的存文件,一堆"Permission denied"报错直接丢掉。

管道 |

cmd1 | cmd2

原理:内核创建一个匿名管道(内存缓冲区),把 cmd1 的 stdout 接到 cmd2 的 stdin。两个进程并发运行,不是串行。

TIP

📌 打个比方:管道就是工厂流水线。每个命令是一个工位,只干一件事,做完把半成品传给下一个工位。cat 文件 | grep 过滤 | sort 排序 就像"原料 → 筛选 → 打包"。

这正是 Linux 的核心哲学:与其造一个万能的大工具,不如造很多只干一件事的小工具,用管道自由组合。所以 ls、grep、sort 都很简单,但拼起来威力无穷。

# 找日志错误,数行数
grep "ERROR" /var/log/app.log | wc -l

# 查进程,过滤,取第一列PID
ps aux | grep nginx | awk '{print $2}'

# 链式管道
cat /etc/passwd | grep -v "^#" | cut -d: -f1 | sort
#   读文件    →   过滤注释   →  取第1列  →  排序

重定向

# stdout 重定向
ls /etc > list.txt        # 覆盖写(文件不存在则创建)
ls /etc >> list.txt       # 追加写

# stderr 重定向
find / -name foo 2> err.txt          # 错误信息写文件,正常输出到终端
find / -name foo 2>/dev/null         # 丢弃错误(/dev/null 是黑洞)

# stdout + stderr 合并
find / -name foo > out.txt 2>&1      # 都写到文件
find / -name foo &> out.txt          # 同上,bash 简写

# stdin 重定向
sort < unsorted.txt                  # 从文件读入而非键盘
TIP

📌 打个比方:重定向就是"改水管接口"。程序的输出默认像水一样流到屏幕,> 就是把水管拔下来接到文件上。> 是接上新桶(覆盖),>> 是往老桶里继续注(追加)。

深入理解 2>&1(从设计者视角)

这个写法第一眼很懵:为什么是 &1 而不是 1?要看懂它,得先理解 shell 设计者面对的问题。

第一步:文件描述符就是几个整数

内核为每个进程维护一张"打开文件表",表的下标就是文件描述符(fd)。约定俗成:

fd 0 → stdin (输入) fd 1 → stdout (正常输出) fd 2 → stderr (错误输出)

这些数字不是魔法,就是这张表的下标编号。> 重定向的完整写法其实是带编号的:

ls > out.txt      # 完整形式是 1> out.txt,1 可省略
                  # 因为重定向输出,最常见的就是 stdout,所以默认省略 1
2> err.txt        # 重定向 fd 2,编号不能省

所以 N> 文件 的通用格式是:把 fd N 接到某个文件。

第二步:设计者遇到的歧义难题

现在我想把 stderr 也指到 stdout 已经指的地方(比如都写进同一个文件)。最自然的写法似乎是:

find / -name foo > out.txt 2> 1    # 想表达:fd 2 也指向 fd 1

但 2> 1 会被解析成"把 fd 2 重定向到一个名叫 1 的文件"!因为 > 右边按规矩就是文件名,1 在这里是个普通文件名,不是文件描述符。

这就是设计者必须解决的歧义:右边的 1,到底是"文件名 1"还是"文件描述符 1"?光看数字分不清。

第三步:用 & 消除歧义

设计者的解法:在数字前加 &,明确告诉 shell "这是个文件描述符,不是文件名"。

2>&1
│└┴─ &1 = "fd 1 当前指向的那个地方"
└──── 2  = 要操作的是 fd 2
  • 2> 文件 → fd 2 接到一个文件
  • 2>&1 → fd 2 接到 fd 1 现在指的地方

这里的 & 可以理解为 C 语言里的"取地址 / 引用"——&1 不是数字 1,而是"1 号管子指向的目标的引用"。一加 &,歧义消失,shell 立刻知道你说的是描述符而非文件。

TIP

📌 一句话记牢:> 右边默认是文件名;想说"我指的是某个 fd",就给它戴上 & 帽子。所以 2>&1 = "让 2 号管,去 1 号管现在指的地方"。

第四步:为什么顺序不能反

2>&1 复制的是 fd 1 此刻的指向,是一次"快照",不是"永久绑定"。所以顺序至关重要:

# 正确:先把 1 接到文件,再让 2 跟上
find / -name foo > out.txt 2>&1
#  ① 1 号管 → out.txt
#  ② 2 号管 → 复制 1 号此刻的指向 → 也指向 out.txt

# 错误:顺序反了
find / -name foo 2>&1 > out.txt
#  ① 2 号管 → 复制 1 号此刻的指向 → 还在屏幕!
#  ② 1 号管 → out.txt(但 2 号已经定格在屏幕了)
#  结果:正常输出进文件,错误还是打在屏幕

第五步:bash 提供了简写

正因为 > out.txt 2>&1 又长又容易写错顺序,bash 后来加了个简写:

find / -name foo &> out.txt      # 等价于 > out.txt 2>&1
find / -name foo &>> out.txt     # 追加版

&> 直接表示"stdout 和 stderr 都重定向",省心。但 &> 是 bash 专有,写可移植脚本(如 /bin/sh)时还得用经典的 > file 2>&1。

3.8 man — 手册系统

man ls              # 查看 ls 的完整手册
man 5 passwd        # 第5章:文件格式(/etc/passwd 的格式说明)
man -k keyword      # 搜索包含关键词的手册页

手册分章:

章节 内容
1 用户命令(最常用)
2 系统调用(syscall)
3 C 库函数
5 文件格式与配置
8 系统管理命令(root 用)

man 2 open 可以查看 open() 系统调用的内核文档——这就是上面架构图里 syscall 层的直接体现。

4 · 用户与权限

4.1 核心模型

Linux 是多用户系统,每个文件都有三个问题的答案:

这个文件是谁的? → Owner(所有者) 属于哪个团队? → Group(所属组) 其他人能做什么? → Other

内核在每次文件访问时检查:你是 owner?是 group 成员?还是 other?然后按对应的权限位决定放不放行。

4.2 权限模型图

图 4 \xb7 用户与权限模型

4.3 权限字符串深度解析

$ ls -l /bin/passwd
-rwsr-xr-x 1 root root 59976 3月 22 2023 /bin/passwd
- rwx r-x r-x ↑ ↑↑↑ ↑↑↑ ↑↑↑ │ │ │ └── other: r-x = 5(可读可执行,不可写) │ │ └────── group: r-x = 5 │ └────────── owner: rwx = 7(可读可写可执行) └───────────── 文件类型:- 普通文件

rwx 的含义因文件类型而异:

权限位 对普通文件 对目录
r 可以读取文件内容 可以 ls 列出目录内容
w 可以修改文件内容 可以在目录中创建/删除文件
x 可以执行该文件 可以 cd 进入目录

目录没有 x 权限就进不去,即使有 r 也只能看到文件名,无法访问文件内容。

4.4 数字权限计算

每组权限是三个二进制位,相加得到数字:

r w x 4 2 1 rwx = 4+2+1 = 7 rw- = 4+2+0 = 6 r-x = 4+0+1 = 5 r-- = 4+0+0 = 4 --- = 0+0+0 = 0
TIP

📌 为什么是 4/2/1 而不是 1/2/3?这是设计者的巧思:用二进制的位来表示权限。rwx 刚好是三个开关,对应二进制三位:

r w x 1 1 1 → 二进制 111 = 十进制 7 1 0 1 → 二进制 101 = 十进制 5

因为 4、2、1 是二进制的位权(2²、2¹、2⁰),任意组合相加的结果都不会冲突——4+1=5 只能是 r-x,不可能是别的组合。如果用 1/2/3,那 1+2=3 和单独的 3 就分不清了。这就是为什么用 2 的幂。

快速心算:7=rwx、6=rw-、5=r-x、4=r--。看到 755 就是 rwx/r-x/r-x,熟了一眼就认出来。

常见权限组合:

数字 符号 用途
755 rwxr-xr-x 目录、可执行脚本(owner全权,其他只读执行)
644 rw-r--r-- 普通文本文件(owner可写,其他只读)
600 rw------- 私密文件,如 SSH 私钥(只有 owner 能读写)
700 rwx------ 私有脚本(只有 owner 能用)
777 rwxrwxrwx 所有人全权,生产环境严禁

4.5 chmod — 修改权限

# 数字模式(直接指定三组权限)
chmod 755 script.sh
chmod 644 config.txt
chmod -R 755 dir/        # -R:递归修改目录下所有文件

# 符号模式(相对修改,更灵活)
chmod u+x script.sh      # 给 owner 加执行权限
chmod g-w file.txt       # 去掉 group 的写权限
chmod o=r file.txt       # 把 other 权限设为只读(= 是赋值)
chmod a+r file.txt       # a = all(u+g+o),所有人加读权限

符号模式语法:[ugoa][+-=][rwx]

符号 记忆规律
u user(owner)
g group
o other
a all(= u+g+o)
+ 加权限
- 去权限
= 直接赋值(覆盖原有)

4.6 chown / chgrp — 修改归属

chown alice file.txt            # 修改所有者为 alice
chown alice:devs file.txt       # 同时修改所有者和所属组
chown :devs file.txt            # 只修改组(:前省略user)
chown -R alice:devs dir/        # -R:递归修改

chgrp devs file.txt             # 只修改所属组(等价于 chown :devs)

只有 root 或文件的 owner 才能修改归属,普通用户不能把文件"送给"别人(防止绕过权限)。

4.7 用户管理

Linux 的用户信息存在三个文件里:

文件 内容
/etc/passwd 用户账户信息(用户名、UID、家目录、shell)
/etc/shadow 密码哈希(root 才能读)
/etc/group 用户组信息

/etc/passwd 格式解读

root : x : 0 : 0 : root : /root : /bin/bash ↑ ↑ ↑ ↑ ↑ ↑ ↑ 用户名 密码 UID GID 描述信息 家目录 登录shell (x=在shadow里)

UID 规律:

  • 0 = root(超级用户)
  • 1~999 = 系统账户(服务用,无法登录)
  • 1000+ = 普通用户

常用用户管理命令

# 查看
id alice                  # 查看 alice 的 UID、GID、所属组
whoami                    # 当前是谁
groups alice              # alice 属于哪些组
cat /etc/passwd | grep alice

# 创建/修改/删除
useradd -m -s /bin/bash alice   # -m:创建家目录,-s:指定shell
passwd alice                     # 设置密码
usermod -aG sudo alice           # -aG:追加到 sudo 组(-a 不覆盖原有组)
userdel -r alice                 # -r:同时删除家目录和邮件

# 切换用户
su - alice                       # 切换到 alice(- 表示加载其完整环境)
sudo command                     # 以 root 权限执行单条命令
sudo -i                          # 切换到 root 交互式 shell

4.8 sudo 原理

普通用户 hisos │ │ sudo ls /root ▼ 内核检查 /etc/sudoers 配置文件 │ │ hisos 在 sudo 组?→ 是 ▼ 以 root 身份执行 ls /root

visudo 编辑 sudoers 文件(有语法检查,比直接 vi 安全)。

4.9 特殊权限位(进阶)

权限 数字 名称 作用
SUID 4xxx Set UID 执行时临时获得 owner 的权限(如 passwd 命令需要以 root 身份改 /etc/shadow)
SGID 2xxx Set GID 目录下新建文件自动继承该目录的所属组
Sticky 1xxx 粘滞位 目录中只有文件 owner 才能删自己的文件(如 /tmp)
TIP

📌 SUID 要解决什么难题(设计者视角)?考虑一个矛盾:你要改自己的密码,密码存在 /etc/shadow,而这个文件是 600 root —— 只有 root 能写。那普通用户怎么改密码?

  • 方案 A:把 /etc/shadow 开放给所有人写?那你能改别人密码,灾难。
  • 方案 B:让 passwd 这个程序运行时临时变身 root,只在这个受控程序里才有权限改 shadow。这就是 SUID。

所以 ls -l /usr/bin/passwd 看到 -rwsr-xr-x 的 s:谁执行它,运行期间就临时拥有文件 owner(root)的身份。本质是“用受信任的程序做代理,而不是直接摆开权限”。也因此 SUID 程序是黑客重点目标——一旦 SUID root 程序有漏洞,就能被利用提权到 root。

# 查看 SUID 示例
ls -l /usr/bin/passwd
# -rwsr-xr-x  ← owner 的 x 位显示为 s,表示 SUID 生效

# /tmp 的 sticky bit
ls -ld /tmp
# drwxrwxrwt  ← other 的 x 位显示为 t

4.10 用户组深入:主组 vs 附加组

第 7 节建过用户、加过组,但"主组""附加组"到底什么区别,很多人没搞清。

TIP

📌 一个用户可以属于多个组,分两类:

  • 主组(primary group):只有一个。你新建文件时,文件的所属组自动就是你的主组。建用户时若不指定,系统通常建一个和用户名同名的组当主组。
  • 附加组(supplementary groups):可以有多个。用来获得额外权限,比如把你加进 sudo 组就能用 sudo,加进 docker 组就能免 sudo 跑 docker。 :::
id                       # 看自己的 uid、主组(gid)、所有附加组
groups alice             # 看 alice 属于哪些组
id alice
$ id uid=1000(hisos) gid=1000(hisos) groups=1000(hisos),27(sudo),999(docker) └─ 主组 ─┘ └──────── 所有组(含附加)────────┘

改组的命令(注意 -a 的坑)

usermod -aG docker alice     # 追加到 docker 附加组(-a = append)
usermod -G docker alice      # 危险!没有 -a 会用 docker 覆盖掉所有附加组
usermod -g staff alice       # 改主组(小写 -g)
newgrp docker                # 临时切换当前会话的主组

:::tip 📌 -aG 为什么必须带 a?这是经典翻车点:-G 是"设置附加组列表",不带 -a 会用新组覆盖用户原有的所有附加组——比如本来在 sudo 组,一条 usermod -G docker 就把 sudo 弄丢了,可能直接没了管理员权限。记牢:加组永远用 -aG。

📌 改组后为什么要重新登录才生效?因为组身份是在你登录、创建 shell 那一刻确定并写进进程的,呼应 06 进程章——已运行的 shell 不会自动更新。退出重登(或 newgrp)让新 shell 带上新组身份。

5 · Shell 脚本

5.1 什么是 Shell 脚本

Shell 脚本就是把一系列命令写进文件,让 shell(通常是 bash)按顺序执行。它能加变量、判断、循环,把手动操作自动化。

Shell 是什么:架构图里 GNU 工具层的那个 bash,既是你敲命令的交互界面,也是脚本解释器。

5.2 脚本结构与执行流程图

图 5 \xb7 Shell 脚本执行流程

5.3 第一个脚本

#!/bin/bash
# 这是注释,# 开头的行不执行

echo "Hello, Linux"

逐行解释:

  • #!/bin/bash — Shebang,必须在第一行第一列。它告诉内核用哪个程序解释这个文件。#! 后跟解释器的绝对路径。
  • # 这是注释 — 除 Shebang 外,# 开头都是注释。
  • echo — 打印文本到标准输出。

5.4 Shebang 的原理

$ ./script.sh

内核执行时,读取文件首行,看到 #!/bin/bash,于是实际执行的是:

/bin/bash ./script.sh
TIP

📌 “Shebang”这个名字从哪来?首行开头是 #! 两个符号。# 在英语口语里叫 sharp 或 hash,! 叫 bang,连起来就是 “sharp-bang”→“shebang”。它不是给人看的,是给内核看的标记。

📌 内核怎么知道用哪个解释器?当你跑 ./script.sh,内核读文件头几个字节,看到 #! 这个“魔数”,就明白“这是脚本,不是二进制程序”,于是拿 #! 后面的路径当解释器,把脚本文件当参数传进去。这就是为什么 Python、Perl 脚本也用 Shebang(#!/usr/bin/python3)——机制是通用的。

为什么推荐 #!/usr/bin/env bash:直接写死 /bin/bash 在某些系统路径不同时会失效;env 会去 $PATH 里找 bash,更可移植。

5.5 三种运行方式(重要区别)

方式 是否需要执行权限 是否开子进程 说明
bash script.sh 不需要 是 显式指定解释器,最常用
./script.sh 需要 chmod +x 是 靠 Shebang 找解释器
source script.sh 或 . script.sh 不需要 否 在当前 shell 执行

关键差异:./script.sh 在子进程运行,脚本里 cd 或改环境变量不影响当前终端;而 source 在当前 shell 运行,能改变当前环境(所以 source ~/.bashrc 能让配置立即生效)。

TIP

📌 为什么 ./script.sh 里的 cd 管不了外面?联系 06 进程章:./ 运行会 fork 出一个子 shell 去执行脚本。子进程改自己的当前目录、环境变量,都是改“复制品”,子进程一退出,这些改动随之消失,父 shell(你的终端)根本没受影响。

而 source 是“把脚本里的命令拿到当前 shell 里逐条执行”,不开子进程,所以改动直接作用于当前环境。记住:改了 .bashrc、.profile 这种环境配置,必须 source 才生效,用 ./ 执行是白忙。

5.6 变量

name="Tom"          # 赋值,等号两边绝对不能有空格
echo $name          # 取值
echo ${name}        # 推荐写法,边界清晰:${name}_suffix
echo "$name"        # 双引号:变量会展开
echo '$name'        # 单引号:原样输出 $name

# 命令替换:把命令的输出赋给变量
today=$(date +%Y-%m-%d)
files=$(ls | wc -l)
WARNING

🚧 赋值 name="Tom" 等号两边不能有空格,name = "Tom" 会被当成执行 name 命令。

5.7 特殊变量(脚本参数)

#!/bin/bash
echo "脚本名:$0"
echo "第1个参数:$1"
echo "第2个参数:$2"
echo "参数个数:$#"
echo "所有参数:$@"
echo "上条命令退出码:$?"

运行 bash test.sh hello world:

脚本名:test.sh 第1个参数:hello 第2个参数:world 参数个数:2 所有参数:hello world

$? 退出码:每个命令结束都返回一个数字,0 = 成功,非 0 = 失败。脚本里靠它判断上一步是否成功。

TIP

📌 为什么是 “0 成功、非 0 失败”,和日常直觉相反?这是设计者的巧思:成功只有一种,失败有千百种。用唯一的 0 表示成功,剩下的 1~255 可以用来区分不同的错误原因(如 1=一般错误、2=误用、3=文件不存在……)。

这让脚本能精确判断:if 命令; then 背后就是检查退出码是不是 0。也是为什么 cmd1 && cmd2(前者成功才跑后者)、cmd1 || cmd2(前者失败才跑后者)能工作——它们都在看 $?。

5.8 条件判断

#!/bin/bash
if [ -f /etc/passwd ]; then
    echo "文件存在"
elif [ -d /tmp ]; then
    echo "是目录"
else
    echo "都不是"
fi
WARNING

🚧 [ ] 是 test 命令的简写,方括号内侧必须有空格:[ -f file ] 对,[-f file] 错。

常用测试条件:

条件 含义 记忆规律
-f file 文件存在且是普通文件 file
-d dir 目录存在 directory
-e path 路径存在(文件或目录) exists
-z str 字符串长度为0(空) zero
-n str 字符串非空 not empty
str1 = str2 字符串相等
n1 -eq n2 数字相等 equal
n1 -ne n2 数字不等 not equal
n1 -lt n2 小于 less than
n1 -gt n2 大于 greater than

字符串比较用 =,数字比较用 -eq,别混。

5.9 循环

# for 循环:遍历列表
for i in 1 2 3 4 5; do
    echo "数字 $i"
done

# for 遍历文件
for f in /etc/*.conf; do
    echo "配置文件:$f"
done

# C 风格 for
for ((i=0; i<5; i++)); do
    echo $i
done

# while 循环
count=1
while [ $count -le 3 ]; do
    echo "第 $count 次"
    count=$((count + 1))    # $(( )) 做算术运算
done

5.10 函数

#!/bin/bash

# 定义函数
greet() {
    echo "Hello, $1"     # $1 是函数的第1个参数
    return 0             # 返回退出码(不是返回值)
}

# 调用
greet "World"
greet "Linux"

注意:bash 函数的 return 只能返回 0-255 的退出码,要"返回"数据用 echo 输出再用 $() 捕获。

5.11 实战示例:批量备份脚本

#!/bin/bash
# 备份指定目录到带时间戳的压缩包

src="$1"
if [ -z "$src" ]; then
    echo "用法:$0 <要备份的目录>"
    exit 1                          # 非0退出码表示出错
fi

if [ ! -d "$src" ]; then
    echo "错误:$src 不是目录"
    exit 1
fi

backup="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
tar -czf "$backup" "$src"
echo "备份完成:$backup"

逐段解释:

  • src="$1" — 把第1个参数存入变量
  • [ -z "$src" ] — 检查是否没传参数
  • exit 1 — 出错时以非0码退出,调用者可用 $? 检测
  • [ ! -d "$src" ] — ! 取反,判断"不是目录"
  • $(date +...) — 命令替换生成时间戳文件名
  • 变量都用双引号包裹,防止路径含空格时出错

6 · 进程管理

6.1 什么是进程

进程 = 正在运行的程序实例。程序是磁盘上的静态文件,运行起来后内核给它分配内存、CPU 时间、文件描述符,就成了进程。

每个进程有:

  • PID:进程号,唯一标识
  • PPID:父进程号
  • 状态:运行/睡眠/停止等
  • owner:以哪个用户身份运行(决定它的权限)

6.2 进程模型图

图 6 \xb7 进程管理

6.3 进程的诞生:fork + exec

Linux 创建新进程只有一种机制,分两步:

1. fork() — 父进程复制出一个一模一样的子进程(连内存都复制) 2. exec() — 子进程用新程序覆盖自己的内存空间,开始运行新程序

举例:你在 bash 里敲 ls:

  1. bash 调用 fork() 生出一个子 bash
  2. 子 bash 调用 exec("/bin/ls"),自己变成了 ls 进程
  3. ls 运行完退出,bash 用 wait() 回收它

PID 1:系统启动的第一个进程(现代是 systemd),是所有进程的祖先。父进程死了的"孤儿进程"会被 PID 1 收养。

TIP

📌 为什么要 fork + exec 两步,而不是一步“创建并运行新程序”(设计者视角)?这是 Unix 最妙的设计之一。拆成两步是为了在“复制”和“运行”中间留出一个口子,让你能动手脚。

比如 shell 要实现 ls > out.txt:

  1. fork() 复制出子进程(此时还是 bash)
  2. 在子进程里把 stdout 重定向到 out.txt(趁 exec 之前的空档调整环境)
  3. 再 exec("/bin/ls"),ls 继承了调整好的 stdout,输出自然进了文件

如果是一步完成,你根本没机会插手设置重定向、环境变量。所以重定向、管道这些机制能存在,根源就是 fork/exec 分家。

📌 什么是僵尸进程(Zombie)?子进程死了,但它的退出码还留在进程表里等父进程来“收尸”(wait())。这段期间它就是僵尸——人死了但档案还在。如果父进程写得不好、不收尸,僵尸就会堆积。为什么要保留退出码?因为父进程可能想知道子进程是成功还是失败(还是那个 $?)。

6.4 ps — 查看进程快照

输出解读

$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.1 16780 11000 ? Ss 10:00 0:01 /sbin/init hisos 2000 0.1 0.5 22000 9000 pts/0 Ss 10:05 0:00 bash hisos 2100 0.0 0.2 12000 4000 pts/0 R+ 10:10 0:00 ps aux ↑ ↑ ↑ ↑ ↑ ↑ ↑ 用户 PID CPU% 内存% 终端 状态 命令
列 含义
USER 进程所有者
PID 进程号
%CPU / %MEM CPU 和内存占用百分比
VSZ 虚拟内存大小(KB)
RSS 实际占用物理内存(KB)
TTY 关联的终端,? 表示无终端(守护进程)
STAT 进程状态(见图中②)
COMMAND 启动命令

常用组合

ps aux                    # 列出所有进程(最常用)
ps aux | grep nginx       # 找特定进程
ps -ef                    # 另一种格式,显示 PPID
ps aux --sort=-%mem | head  # 按内存占用排序找出大户

aux 的含义:a=所有用户的进程,u=显示详细信息,x=包括无终端的进程。这是 BSD 风格选项,不加 -。

6.5 top / htop — 实时监控

top      # 实时刷新的进程列表
htop     # 更友好的彩色版(可能需 apt install htop)

top 交互按键:

按键 动作
P 按 CPU 排序
M 按内存排序
k 输入 PID 杀进程
q 退出

top 头部信息:

load average: 0.50, 0.40, 0.35 ↑ ↑ ↑ 1分钟 5分钟 15分钟 的平均负载

负载值约等于"等待 CPU 的进程数",超过 CPU 核心数就说明繁忙。

6.6 信号与 kill

进程间通过信号通信。kill 不只是"杀",是"发信号"。

kill 2050              # 默认发 SIGTERM(15),礼貌请求终止
kill -9 2050           # 发 SIGKILL(9),强制杀死
kill -15 2050          # 显式发 SIGTERM
kill -l                # 列出所有信号

pkill nginx            # 按名字杀进程
pkill -9 -f "python app.py"  # -f 匹配完整命令行
killall firefox        # 杀掉所有同名进程

常用信号:

信号 编号 作用 记忆
SIGTERM 15 礼貌终止(默认),进程可清理后退出 TERMinate
SIGKILL 9 强制杀死,进程无法拦截 数字9好记
SIGINT 2 中断 = Ctrl+C INTerrupt
SIGSTOP 19 暂停 = Ctrl+Z STOP
SIGHUP 1 挂起,常用于让服务重载配置 HangUP

顺序很重要:先用 kill 15(礼貌),让进程有机会保存数据;无效再 kill -9(强杀,可能丢数据)。

TIP

📌 为什么 kill 叫“kill”却不一定是杀?这是个历史遗留的起名失误。kill 实际是“向进程发一个信号”,信号可以是“你该重载配置了”(SIGHUP)、“你暂停一下”(SIGSTOP),只不过默认发的是“请终止”(SIGTERM)而已。把它理解成 send-signal 就顺了。

📌 为什么 kill -9 万能但不该乱用?SIGTERM(15)是“敲门通知”,进程可以接住信号、先保存数据再优雅退出;SIGKILL(9)是内核直接拔插,进程压根没机会反应(这也是为什么 9 拦不住)。所以顺序是先 15 后 9:给个体面退场的机会,实在不行再强杀。直接 9 可能让进程来不及写盘、锁不释放。

6.7 前台与后台作业

# 后台运行:命令末尾加 &
sleep 100 &              # 立即返回,sleep 在后台跑
                         # 输出:[1] 2050  ← 作业号和PID

# 查看当前 shell 的作业
jobs                     # [1]+ Running  sleep 100 &

# 前台 ←→ 后台切换
Ctrl+Z                   # 暂停当前前台作业
bg                       # 让暂停的作业在后台继续
bg %1                    # 指定作业号
fg                       # 把后台作业调回前台
fg %1

# 退出终端后仍运行
nohup ./long_task.sh &   # 忽略挂断信号,日志写入 nohup.out

作业 vs 进程:作业是 shell 层面的概念(用 %1 引用),进程是内核层面的(用 PID 引用)。

6.8 进程优先级(nice 值)

nice -n 10 ./task.sh     # 以低优先级启动(nice 值 -20~19,越大越谦让)
renice -n 5 -p 2050      # 调整运行中进程的优先级

nice 值越高,进程越"谦让",CPU 紧张时优先让给别人。只有 root 能设负值(提高优先级)。

6.9 实战:找出并杀掉占用资源的进程

# 1. 找出 CPU 占用最高的进程
ps aux --sort=-%cpu | head -5

# 2. 找出某个程序的所有 PID
pgrep -a nginx

# 3. 优雅地终止,无效再强杀
kill 2050
sleep 2
kill -9 2050 2>/dev/null    # 如果还在就强杀

# 4. 实时观察某进程
top -p 2050

7 · 网络基础

7.1 为什么要懂网络

服务器的核心价值就是提供网络服务。理解 IP、端口、TCP,才能排查"连不上""端口被占""DNS 解析失败"这类日常问题。

7.2 网络模型图

图 7 \xb7 网络基础

7.3 TCP/IP 四层模型

数据从应用发出时逐层封装,接收时逐层解封:

层 作用 关键概念
应用层 具体应用协议 HTTP、SSH、DNS
传输层 端到端传输 TCP/UDP、端口号
网络层 跨网络寻址路由 IP 地址、路由
链路层 物理网络传输 MAC 地址、网卡

TCP vs UDP:

TCP UDP
可靠性 可靠,有确认重传 不保证送达
速度 较慢(有握手开销) 快
场景 网页、SSH、文件传输 视频、游戏、DNS

7.4 IP 地址 + 端口

192.168.1.10 : 443 ↑ ↑ IP地址 端口号 定位哪台机器 定位机器上哪个程序
TIP

📌 打个比方:IP 地址像一栋楼的地址(定位是哪栋楼),端口像楼里的门牌号(定位是哪个房间)。只有 IP 找到机器还不够——一台服务器上跑着网站、SSH、数据库几十个程序,端口才能告诉系统“数据该送给哪个程序”。所以 IP:端口 合起来才能唯一定位一个服务。

IP 分类:

  • 公网 IP:全球唯一,可直接访问
  • 私网 IP:局域网内部使用,三个保留段:
    • 10.0.0.0/8
    • 172.16.0.0/12
    • 192.168.0.0/16
  • 127.0.0.1:localhost,指本机

常见端口:

端口 服务
22 SSH
80 HTTP
443 HTTPS
53 DNS
3306 MySQL
6379 Redis

0-1023 是知名端口,需 root 权限才能监听。

📌 为什么低端口要 root 才能用?这是安全设计:80、443 这些是“大家约定俗成”的服务端口,浏览器默认去 80 找网站。如果任意普通用户都能占 80 端口,就能冒充官方网站骗人。要 root 才能监听,相当于“只有管理员能挂招牌”。

7.5 TCP 三次握手

建立连接前,客户端和服务器要"对暗号"确认双方收发正常:

客户端 ──── ① SYN(我要连,序号x) ────→ 服务器 客户端 ←── ② SYN+ACK(同意,序号y,确认x+1) ─ 服务器 客户端 ──── ③ ACK(确认y+1) ───────→ 服务器 连接建立,开始传数据
TIP

📌 为什么偏偏是三次,不是两次或四次(设计者视角)?核心是要让双方都确认“我能发、我能收,你也能发能收”。拿打电话打个比方:

① A:你能听到吗? → B 收到,知道“A的口、B的耳”正常 ② B:能听到,你呢? → A 收到,知道“B的口、A的耳”也正常 ③ A:我也能听到! → B 收到,双方都确认双向畅通
  • 只握一次:B 不知道自己说的 A 能不能听到
  • 只握两次:A 确认了双向,但 B 还没确认“自己发的 A 能收到”
  • 握三次:恰好让双方都确认了收发能力,不多不少

这也是为什么断开连接要四次挥手:建连时服务器的“同意+请求”能合成一包,断开时可能一方还有数据没发完,得拆开成四步。

一句话总结:三次握手是为了双方都确认彼此的发送和接收能力都正常,少一次就有一方无法确认。

7.6 ip — 查看和配置网络

ip a                    # 查看所有网卡和 IP 地址(addr 缩写)
ip a show eth0          # 只看 eth0 网卡
ip route                # 查看路由表(数据往哪走)
ip link                 # 查看网卡状态(up/down)

ip a 输出解读

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ... link/ether 00:1a:2b:3c:4d:5e ← MAC 地址(链路层) inet 192.168.1.10/24 ... ← IPv4 地址,/24 是子网掩码 inet6 fe80::21a:2bff:... ← IPv6 地址

/24 表示前 24 位是网络号,即子网掩码 255.255.255.0。

7.7 ping — 测试连通性

ping baidu.com          # 持续发包测试,Ctrl+C 停止
ping -c 4 baidu.com     # -c 4:只发4个包
64 bytes from 14.215.177.38: icmp_seq=1 ttl=53 time=12.3 ms ↑ ↑ ↑ 第几个包 剩余跳数 往返延迟

ping 用的是网络层的 ICMP 协议,不涉及端口。ping 通说明网络层连通,但不代表对应的服务端口开着。

7.8 ss — 查看端口和连接

ss(socket statistics)取代了老的 netstat:

ss -tlnp                # 查看所有 TCP 监听端口及进程
ss -tunlp               # TCP + UDP 都看
ss -t                   # 查看已建立的 TCP 连接

选项记忆:

选项 含义 记忆
-t TCP tcp
-u UDP udp
-l 监听状态 listen
-n 显示数字端口(不解析服务名) numeric
-p 显示进程 process

输出解读

State Recv-Q Local Address:Port Peer Address:Port Process LISTEN 0 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=800)) ↑ ↑ 监听所有IP的22端口 是 sshd 进程在监听

0.0.0.0:22 表示监听所有网卡的 22 端口;127.0.0.1:3306 则只监听本机。

排查"端口被占用":

ss -tlnp | grep :80     # 看 80 端口被哪个进程占用

7.9 curl — HTTP 请求工具

curl https://example.com              # GET 请求,输出响应体
curl -I https://example.com           # -I:只看响应头
curl -o file.zip https://.../file.zip # -o:下载保存到文件
curl -L https://example.com           # -L:跟随重定向
curl -X POST -d '{"k":"v"}' \
     -H "Content-Type: application/json" \
     https://api.example.com           # POST JSON 数据

选项记忆:-I=headIng(头),-o=output,-L=Location(跟随跳转),-X=请求方法。

7.10 DNS 解析

域名要先解析成 IP 才能连接:

www.baidu.com → 查 /etc/hosts → 查本地缓存 → 问 DNS 服务器 → 得到 IP
dig baidu.com           # 详细 DNS 查询
nslookup baidu.com      # 简单查询
cat /etc/resolv.conf    # 查看用哪个 DNS 服务器
cat /etc/hosts          # 本地静态域名映射(优先级最高)

/etc/hosts 的用途:手动指定域名对应 IP,常用于测试环境或屏蔽网站。优先级高于 DNS。

7.11 ssh — 远程登录(你正在用)

ssh user@192.168.1.10           # 登录远程主机
ssh -p 2222 user@host           # -p 指定端口(默认22)
ssh-keygen -t ed25519           # 生成密钥对(免密登录)
ssh-copy-id user@host           # 把公钥拷到远程,实现免密
scp file.txt user@host:/path/   # 通过 SSH 复制文件

SSH 免密原理:公钥放服务器 ~/.ssh/authorized_keys,私钥留本地。登录时服务器用公钥加密挑战,只有持有私钥的你能解开,无需传密码。

7.12 rsync — 高效文件同步

07 章前面用 scp 传文件,但 rsync 才是运维备份/同步的主力,因为它只传有变化的部分(增量传输),还能断点续传。

# 本地同步(注意源目录结尾的 / )
rsync -av 源目录/ 目标目录/      # -a 保留属性递归,-v 显示过程

# 推到远程(走 SSH)
rsync -avz 本地目录/ user@host:/远程目录/   # -z 传输时压缩,省带宽
rsync -avz -e "ssh -p 2222" 本地/ user@host:/远程/  # 指定 SSH 端口

# 从远程拉回本地
rsync -avz user@host:/远程目录/ 本地目录/

# 常用增强
rsync -av --delete 源/ 目标/     # 目标里多余的文件也删(保持完全一致,慎用)
rsync -av --progress 源/ 目标/   # 显示每个文件的进度
rsync -avn 源/ 目标/             # -n 演练(dry-run),只看会做什么、不真改
TIP

📌 rsync 比 scp 强在哪?scp 是"全量复制"——每次都把所有文件重传一遍。rsync 用差量算法只传变化的块:第一次同步 10G,之后改了几个文件再同步,可能只传几 MB。所以定时备份、部署代码都用 rsync,配合 09 章 cron/timer 就是自动备份方案。

结尾 / 的坑:rsync 源/ 目标/(源带斜杠)= 把源目录"里面的内容"放进目标;rsync 源 目标/(源不带斜杠)= 把源目录"本身"放进目标。先用 -n 演练确认。

7.13 tcpdump — 抓包看真相

当"连不上、丢包、协议对不对"靠日志看不出来时,tcpdump 直接抓网卡上的数据包,是网络排查的终极手段。

sudo tcpdump -i eth0                    # 抓 eth0 网卡所有包
sudo tcpdump -i any port 80             # 只抓 80 端口(任意网卡)
sudo tcpdump -i any host 192.168.1.10   # 只抓和某 IP 的往来
sudo tcpdump -i any port 443 -nn        # -nn 不解析域名/端口名,更快
sudo tcpdump -i any port 80 -w cap.pcap # 存成文件,用 Wireshark 打开分析
sudo tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0'  # 只看 SYN 包(看握手)
TIP

📌 什么时候才用 tcpdump?排查顺序是先 ping(通不通)、ss(端口开没)、curl(应用层响应),这些都看不出问题时再 tcpdump 抓包看最底层到底收发了什么。比如"客户端说发了请求、服务端说没收到",抓包就能判断包到底有没有到网卡——呼应 07 章 TCP/IP 分层,tcpdump 看的是链路层实际流量。

抓 HTTPS 内容是加密的看不到明文,但能看到握手、连接是否建立、有没有重传,足够定位大多数网络问题。

7.14 实战:排查"服务连不上"

# 1. 先 ping 测网络层是否通
ping -c 3 目标IP

# 2. 通了再测端口是否开放
ss -tlnp | grep :端口         # 本机看监听
curl -v http://目标IP:端口     # 远程测试连接

# 3. DNS 问题?换 IP 直连试试
dig 域名
ping 域名                     # 解析失败会报 unknown host

8 · 软件包管理

8.1 包管理器解决什么问题

一个软件常常依赖几十个库。手动安装要逐个找依赖、按顺序编译,极其痛苦。包管理器自动完成依赖解析、下载、安装、升级、卸载。

TIP

📌 什么是“依赖地狱”?想装 A,A 需要 B 和 C;B 又需要 D;C 需要 D 的另一个版本……手动装你会陷入“装一个发现还缺三个”的无底洞,这就是依赖地狱(dependency hell)。

📌 包管理器怎么解决?它维护一份“全量软件目录”,里面记着每个包依赖谁、版本要求是什么。你说装 A,它自动算出整棵依赖树,一次性把 B/C/D 按正确版本都装好。这就是为什么你只需 apt install nginx 一句话的原因。

8.2 包管理图

图 8 \xb7 软件包管理

8.3 两大流派

Debian 系(你的 Ubuntu) Red Hat 系
包格式 .deb .rpm
高层工具 apt dnf / yum
底层工具 dpkg rpm
源配置 /etc/apt/sources.list /etc/yum.repos.d/

高层 vs 底层:apt 会自动处理依赖、从网络源下载;dpkg 只能安装本地单个 .deb 文件,不解决依赖。日常用 apt。

8.4 apt 核心命令(Ubuntu 重点)

# 刷新软件源索引(装软件前必做)
sudo apt update

# 升级所有已安装的软件
sudo apt upgrade

# 安装软件
sudo apt install nginx
sudo apt install nginx vim git   # 一次装多个

# 卸载
sudo apt remove nginx            # 卸载但保留配置文件
sudo apt purge nginx             # 彻底卸载,连配置一起删

# 搜索 / 查看信息
apt search keyword               # 搜索软件
apt show nginx                   # 查看软件详情
apt list --installed             # 列出已安装的包

# 清理
sudo apt autoremove              # 删除不再需要的依赖
sudo apt clean                   # 清理下载的安装包缓存

update vs upgrade 别混:update 只刷新"有哪些新版本"的索引清单,不安装;upgrade 才真正下载安装新版本。标准流程是 apt update && apt upgrade。

📌 用“网购”打个比方记住区别:apt update = 刷新商品页面,看看现在都有什么新货、什么价(只更新信息,不买);apt upgrade = 真正下单买下来装上。为什么装软件前要先 update?因为不刷新目录,你手里的是旧价目录,可能“照着旧地址去拿货”发现货架空了(报找不到包)。

8.5 apt update 的原理

sudo apt update ↓ 读取 /etc/apt/sources.list 里的软件源地址 ↓ 从每个源下载最新的包索引(哪些包、什么版本、依赖什么) ↓ 本地缓存这份"目录",install 时据此解析依赖

update 不动你的软件,只更新这份"目录"。所以装新软件前先 update,否则可能装到旧版本或报"找不到包"。

8.6 dpkg — 底层工具

sudo dpkg -i package.deb         # 安装本地 deb 文件(-i = install)
dpkg -l                          # 列出所有已安装的包(-l = list)
dpkg -L nginx                    # 列出 nginx 安装了哪些文件
dpkg -S /usr/sbin/nginx          # 反查某文件属于哪个包(-S = search)

场景:从官网下载的 .deb 用 dpkg -i 安装。如果报依赖错误,再用 sudo apt install -f 自动补依赖。

8.7 软件源(sources)

软件源就是存放 .deb 包的服务器地址。

cat /etc/apt/sources.list              # 主源配置
ls /etc/apt/sources.list.d/            # 第三方源(每个软件一个文件)

换国内镜像源(加速下载):把 sources.list 里的官方地址替换成阿里云/清华镜像,再 apt update。这是新装系统后常做的优化。

8.8 其他打包格式

现代 Linux 还有跨发行版的通用打包方式:

格式 特点
Snap Ubuntu 主推,自带依赖,沙箱隔离(snap install)
Flatpak 跨发行版桌面应用
AppImage 单文件免安装,下载即运行

你系统里的 /snap 目录就是 Snap 包的挂载点。

8.9 实战:安装并验证一个软件

# 1. 更新索引
sudo apt update

# 2. 安装 htop(更好用的进程监控)
sudo apt install htop

# 3. 验证安装位置
which htop                       # /usr/bin/htop
dpkg -L htop | grep bin          # 看它装了哪些可执行文件

# 4. 运行
htop

# 5. 不需要了就卸载
sudo apt remove htop

9 · systemd 服务

9.1 什么是 systemd

systemd 是现代 Linux 的初始化系统和服务管理器,就是进程章节里的 PID 1。它负责:

  • 开机时按依赖顺序启动所有服务
  • 管理服务的启动/停止/重启
  • 收集日志
  • 处理定时任务、挂载等

取代了老的 SysVinit(/etc/init.d/ 脚本)。

TIP

📌 为什么需要一个 PID 1 总管家?回想 06 章:所有进程都是 fork 出来的,能追溯到一个最初的祖先——这个祖先就是 PID 1。开机时内核只负责启动 PID 1,剩下“启动网络、启动 SSH、挂载磁盘……”全交给它。你可以把 systemd 想象成公司的总经理:老板(内核)只雇了他一个人,其他员工都是他招的,出事也找他。

📌 systemd 为什么能加快开机?老的 SysVinit 是一个接一个串行启动服务(像排队);systemd 看懂服务间的依赖关系,把互不依赖的服务并行启动,就像多个收银台同时开工,自然快。

9.2 systemd 模型图

图 9 \xb7 systemd 服务管理

9.3 核心概念:Unit

systemd 把所有管理对象抽象为 Unit(单元),按后缀分类:

类型 作用
.service 后台服务(最常用,如 nginx.service)
.socket 套接字监听
.timer 定时器(替代 cron)
.target 一组 unit 的集合(如开机目标)
.mount 挂载点

Unit 文件位置:

  • /lib/systemd/system/ — 软件包自带的(不要改)
  • /etc/systemd/system/ — 管理员自定义的(改这里)

9.4 systemctl — 服务管理核心命令

运行控制

sudo systemctl start nginx      # 启动
sudo systemctl stop nginx       # 停止
sudo systemctl restart nginx    # 重启(先停后启,会断连接)
sudo systemctl reload nginx     # 重载配置(不断连接,平滑)
systemctl status nginx          # 查看状态(最常用,不需 sudo)

开机自启控制

sudo systemctl enable nginx     # 设置开机自启
sudo systemctl disable nginx    # 取消开机自启
sudo systemctl enable --now nginx  # 启用 + 立即启动(一步到位)
systemctl is-enabled nginx      # 查询是否开机自启
systemctl is-active nginx       # 查询是否正在运行

关键区别:enabled(开机是否启动)和 active(现在是否运行)是两个独立概念。可以 enabled 但当前 stopped,也可以 active 但 disabled。

TIP

📌 用“闹钟”区分 enabled 和 active:

  • active(运行中)= 闹钟现在正响。start/stop 控制它。
  • enabled(已启用)= 你设了“每天早上自动响”这个闹铃。enable/disable 控制它。

两者独立:你可以手动按响闹钟(active)但没设闹铃(disabled)——重启后就不响了;也可以设了闹铃(enabled)但此刻它没响(stopped)。部署服务要两者都要:enable --now = 设好闹铃 + 现在立马响。新手常踩的坑:start 了但忘了 enable,服务器一重启服务就没了。

9.5 status 输出解读

$ systemctl status sshd
● ssh.service - OpenBSD Secure Shell server Loaded: loaded (/lib/systemd/system/ssh.service; enabled; ...) Active: active (running) since Tue 2026-06-17 10:00:00; 2h ago Main PID: 800 (sshd) Tasks: 1 (limit: 4915) CGroup: /system.slice/ssh.service └─800 /usr/sbin/sshd -D
字段 含义
● 圆点颜色 绿=运行,红=失败,灰=停止
Loaded unit 文件路径 + 是否 enabled
Active 当前状态 + 运行时长
Main PID 主进程的 PID
CGroup 该服务所有进程(systemd 用 cgroup 追踪)

9.6 journalctl — 查看日志

systemd 用 journal 统一收集所有服务日志:

journalctl -u nginx              # 查看 nginx 服务的全部日志
journalctl -u nginx -f           # 实时跟踪(类似 tail -f)
journalctl -u nginx -n 50        # 最近 50 行
journalctl -u nginx --since "1 hour ago"   # 按时间过滤
journalctl -u nginx -p err       # 只看错误级别(-p priority)
journalctl -b                    # 本次开机以来的所有日志

排查服务起不来:systemctl status 看概况,journalctl -u 服务名 -n 50 看详细报错。

9.7 编写一个自定义 service

把你自己的脚本/程序变成 systemd 管理的服务:

# /etc/systemd/system/myapp.service
[Unit]
Description=My Custom App
After=network.target

[Service]
Type=simple
User=hisos
WorkingDirectory=/home/hisos/myapp
ExecStart=/home/hisos/myapp/run.sh
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

逐段解释:

字段 含义
[Unit] Description 服务描述
After=network.target 在网络就绪后才启动
[Service] Type=simple 前台运行的普通进程
User 以哪个用户身份运行(安全:别用 root)
ExecStart 启动命令(必须绝对路径)
Restart=on-failure 崩溃时自动重启
RestartSec=5 重启前等 5 秒
[Install] WantedBy enable 时挂到哪个 target(multi-user 是常规多用户模式)

应用流程:

sudo systemctl daemon-reload          # 改了 unit 文件必须重载
sudo systemctl enable --now myapp     # 启用并启动
systemctl status myapp                # 检查
journalctl -u myapp -f                # 看日志
WARNING

🚧 每次修改 .service 文件后,必须 daemon-reload systemd 才知道变化。

9.8 实战:观察系统已有服务

# 列出所有服务及状态
systemctl list-units --type=service

# 只看正在运行的
systemctl list-units --type=service --state=running

# 看 SSH 服务(你正在用的)
systemctl status ssh
journalctl -u ssh -n 20

# 看开机启动耗时分析
systemd-analyze              # 总耗时
systemd-analyze blame        # 各服务耗时排名

10 · 存储与 LVM

10.1 存储栈:从磁盘到目录

一块裸磁盘要经过四步才能存文件,对应回 02 章"一切挂载到目录树":

物理磁盘 → 分区 → 格式化(文件系统)→ 挂载到目录 → 可读写 /dev/sda /dev/sda1 ext4 /mnt/data
TIP

📌 为什么要分这么多步,不能插上磁盘就用?用“盖楼”打个比方:

  • 裸磁盘 = 一块空地,什么都没有
  • 分区 = 在空地上划出几块宅基(一块盘可分成几个独立区域)
  • 格式化 = 盖房子、分房间(建立文件系统,决定文件怎么组织存放)
  • 挂载 = 给房子安个门牌号接入小区(接入目录树,才能走进去)

每一层职责不同,才能灵活组合:同一块盘可以分多个区、每个区用不同文件系统、挂到不同目录。这就是为什么不能“一步到位”。

10.2 存储模型图

图 10 \xb7 存储与 LVM

10.3 设备命名规则

/dev/sda 第1块 SATA/SCSI 磁盘 /dev/sda1 该磁盘的第1个分区 /dev/sdb 第2块磁盘 /dev/nvme0n1 第1块 NVMe 固态盘 /dev/nvme0n1p1 NVMe 盘的第1个分区

字母 a/b/c 表示第几块盘,数字表示第几个分区。

10.4 查看存储

lsblk                   # 树状显示磁盘和分区(最直观)
df -h                   # 各挂载点的容量和使用率
du -sh /var/log         # 某目录占用大小(-s 汇总,-h 可读)
free -h                 # 内存和 swap 使用情况
sudo fdisk -l           # 磁盘分区表详情
sudo blkid              # 查看分区的 UUID 和文件系统类型

lsblk 输出解读

NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS sda 8:0 0 500G 0 disk ├─sda1 8:1 0 512M 0 part /boot/efi └─sda2 8:2 0 499G 0 part / ↑ ↑ ↑ 大小 类型 挂载点

df vs du 的区别

命令 视角 用途
df 文件系统级 看整个分区还剩多少空间
du 目录/文件级 看具体哪个目录占用大

排查磁盘满:先 df -h 找出满的分区,再 du -sh /path/* 逐层定位大目录。

10.5 分区 / 格式化 / 挂载(操作裸盘)

WARNING

🚧 以下操作会清空数据,务必先 lsblk 确认设备名,别操作错盘。

# 1. 分区(交互式)
sudo fdisk /dev/sdb       # n新建 → p主分区 → w保存

# 2. 格式化为 ext4
sudo mkfs.ext4 /dev/sdb1

# 3. 临时挂载
sudo mkdir /mnt/data
sudo mount /dev/sdb1 /mnt/data
df -h | grep sdb1         # 验证挂载成功

# 4. 卸载
sudo umount /mnt/data

临时挂载重启后失效,要永久挂载得写进 /etc/fstab。

10.6 /etc/fstab — 开机自动挂载

# 设备 挂载点 文件系统 选项 dump fsck UUID=abc-123 /mnt/data ext4 defaults 0 2
字段 含义
设备 设备名或 UUID(推荐 UUID,设备名可能变)
挂载点 挂到哪个目录
文件系统 ext4 / xfs / swap
选项 defaults 是常规默认;noatime 可提升性能
dump 备份标记,通常 0
fsck 开机检查顺序,根分区 1,其他 2,不检查 0
sudo blkid                # 查 UUID 填进 fstab
sudo mount -a             # 测试 fstab 是否正确(不重启就验证)
WARNING

🚧 fstab 写错会导致开机失败!改完一定用 mount -a 测试无报错再重启。

10.7 LVM — 逻辑卷管理

传统分区大小固定,扩容麻烦。LVM 在分区和文件系统之间加一层抽象,让存储空间可以动态调整。

TIP

📌 传统分区痛在哪?传统分区像把蛋糕切好分给几个人——切完就固定了。某个分区快满了,旁边分区还空着,也没法把空闲的匀过去,只能重新分区(风险极大)。

📌 LVM 的三层为什么这么设计?用“蓄水”打个比方:

  • PV(物理卷) = 一个个水源(每块磁盘分区)
  • VG(卷组) = 把多个水源汇成一个大水库(存储池)
  • LV(逻辑卷) = 从水库里接出一根根水管给各个用户(当分区用)

加了“水库”这个中间层,带来灵活性:某根水管(LV)不够用了,从水库再分点量给它就行;水库也不够了,加块新磁盘当新水源注进去。全程不用捣上层的文件。这就是为什么生产服务器几乎都用 LVM。

三层结构

PV (物理卷) → VG (卷组) → LV (逻辑卷) 物理分区 存储池 可当分区用的逻辑块 /dev/sda1 ┐ /dev/sdb1 ┼→ vg_data → lv_app (100G) /dev/sdc1 ┘ lv_db (200G)

把多块磁盘的分区(PV)汇成一个池(VG),再从池里切出逻辑卷(LV)。LV 用起来和普通分区一样,但能在线扩容。

常用命令

# 查看
pvs / pvdisplay           # 物理卷
vgs / vgdisplay           # 卷组
lvs / lvdisplay           # 逻辑卷

# 创建流程
sudo pvcreate /dev/sdb1 /dev/sdc1     # 1. 标记为PV
sudo vgcreate vg_data /dev/sdb1 /dev/sdc1  # 2. 组成VG
sudo lvcreate -L 100G -n lv_app vg_data    # 3. 切出100G的LV
sudo mkfs.ext4 /dev/vg_data/lv_app    # 4. 格式化
sudo mount /dev/vg_data/lv_app /mnt/app    # 5. 挂载

# 在线扩容(LVM 的核心优势)
sudo lvextend -L +50G /dev/vg_data/lv_app  # 扩50G
sudo resize2fs /dev/vg_data/lv_app    # 让文件系统识别新空间

LVM 最大价值:磁盘空间不够时,加块新硬盘 → 加进 VG → 扩 LV → resize 文件系统,全程不停机、不重装。

10.8 实战:排查磁盘空间不足

# 1. 哪个分区满了
df -h

# 2. 定位大目录(逐层往下钻)
sudo du -sh /* 2>/dev/null | sort -rh | head
sudo du -sh /var/* | sort -rh | head

# 3. 常见元凶:日志
sudo du -sh /var/log/*
journalctl --vacuum-size=500M     # 清理 journal 日志到500M

# 4. 找大文件
sudo find / -size +1G -type f 2>/dev/null

10.9 磁盘没满却报"空间不足"——inode 用尽

一个经典坑:df -h 显示分区还有大把空间,但创建文件却报 No space left on device。

TIP

📌 根因是 inode 耗尽(呼应 02 章)。文件系统在格式化时就固定了 inode 的总数。每个文件(哪怕 0 字节)都要占一个 inode。如果你有海量小文件(比如几百万个缓存碎片、session 文件),inode 先被用光了,而数据块还空着——于是有空间也建不了新文件。

df -i                    # 关键命令!看 inode 使用率(IUse% 到 100% 就是它)
df -h                    # 对比:这个看的是数据块空间
$ df -i Filesystem Inodes IUsed IFree IUse% Mounted on /dev/sda2 6553600 6553600 0 100% / ← inode 满了!

定位哪个目录小文件最多:

# 统计各目录下的文件数量,找出元凶
sudo find / -xdev -type f 2>/dev/null | cut -d/ -f2 | sort | uniq -c | sort -rn | head
# 进一步钻某个目录
for d in /var/*; do echo "$(find "$d" 2>/dev/null | wc -l) $d"; done | sort -rn | head
TIP

📌 两种"满"要分清:df -h 满 = 数据太大(删大文件/扩容解决);df -i 满 = 文件太多(删碎文件解决)。报"空间不足"时两个都要查,否则对着 df -h 怎么也想不通。

10.10 logrotate — 日志轮转(根治日志撑爆磁盘)

上一节手动清日志只是救急。日志会一直增长,迟早撑爆磁盘。logrotate 是系统自带的方案,自动切割、压缩、删除旧日志。

TIP

📌 什么叫"轮转"?把当前日志 app.log 改名归档(app.log.1),新日志写进空的 app.log;下次再把 .1 变 .2……保留 N 份后最老的删掉。像超市只留最近几天的监控录像,旧的自动覆盖,既留了近期记录又不会无限占空间。

cat /etc/logrotate.conf          # 全局配置
ls /etc/logrotate.d/             # 每个软件一个独立规则文件
sudo logrotate -d /etc/logrotate.d/nginx   # -d 演练,看会怎么转(不真做)
sudo logrotate -f /etc/logrotate.d/nginx   # -f 强制立即轮转一次(测试用)

给自己的应用写一条规则 /etc/logrotate.d/myapp:

/var/log/myapp/*.log { daily # 每天轮转 rotate 7 # 保留 7 份(一周) compress # 旧日志 gzip 压缩 delaycompress # 推迟一轮再压(避免压到正在写的) missingok # 日志不存在不报错 notifempty # 空日志不轮转 copytruncate # 复制后清空原文件(应用不用重开日志句柄) }
TIP

📌 copytruncate 解决什么问题?呼应 06 进程章:程序打开日志文件后一直握着那个文件句柄写。如果直接把日志改名,程序还在往"旧句柄"写,新文件一直是空的。copytruncate 先复制内容再把原文件清空(truncate),程序的句柄没变继续写,无需重启应用。另一种方案是轮转后给程序发 HUP 信号让它重开日志(呼应 06 章信号)。

📌 logrotate 自己怎么定时跑?它本身不是常驻服务,而是被 cron 或 systemd timer 每天调用一次(呼应 15 章 cron / 09 章 timer)。这就是为什么配置里写 daily 它就能每天执行——上层有定时器在驱动它。

11 · 安全与防火墙

11.1 安全的核心思路

Linux 服务器安全围绕三个"最小化":

  • 最小权限:服务用专用低权用户跑,不用 root
  • 最小开放:只开必需端口,关闭不用的服务
  • 及时更新:打补丁、防暴力破解
TIP

📌 为什么是“最小化”而不是“防得越多越好”?安全里有个概念叫“攻击面”——你暴露给外界的每个端口、每个服务、每个高权限进程,都是一扇可能被撬开的门。门越多,黑客可试的入口越多。

所以安全的本质不是“加多少锁”,而是“少开几扇门”。用低权用户跑服务——即使被攻破,黑客也只拿到低权限;只开必需端口——没开的门根本被不了。这比事后装一堆防护软件有效得多。

11.2 安全模型图

图 11 \xb7 安全与防火墙

11.3 防火墙原理

防火墙按规则过滤进出的网络包。底层都是内核的 netfilter 框架,上层工具只是它的配置前端:

iptables 传统命令行(强大但复杂) nftables iptables 的现代替代 ufw Ubuntu 推荐,简单易用 ← 重点 firewalld Red Hat 系常用

数据包到达时,防火墙检查:源 IP?目标端口?协议?匹配规则后决定放行还是丢弃。

11.4 ufw — Ubuntu 防火墙

sudo ufw enable                 # 启用防火墙
sudo ufw disable                # 关闭
sudo ufw status verbose         # 查看规则和默认策略

# 放行 / 拒绝端口
sudo ufw allow 22               # 放行 SSH
sudo ufw allow 80/tcp           # 放行 HTTP(指定协议)
sudo ufw allow 443/tcp          # 放行 HTTPS
sudo ufw deny 3306              # 拒绝 MySQL 外部访问

# 限定来源 IP
sudo ufw allow from 192.168.1.0/24 to any port 22   # 只允许内网连SSH

# 删除规则
sudo ufw delete allow 80
sudo ufw status numbered        # 带编号查看
sudo ufw delete 2               # 按编号删除

# 默认策略
sudo ufw default deny incoming  # 入站默认拒绝(推荐)
sudo ufw default allow outgoing # 出站默认允许
WARNING

🚧 启用防火墙前务必先 ufw allow 22!否则规则生效瞬间你的 SSH 会被切断,远程服务器再也连不上。

📌 为什么这个坑特别致命?因为 ufw default deny incoming 一旦生效,默认拒绝所有入站连接——包括你正在用的这条 SSH!你人在外地,机器在机房,连不上就只能去机房接显示器。这是运维新手最经典的“把自己锁门外”事故。顺序必须是:先 allow 22,再启用防火墙。

11.5 SSH 加固(远程服务器重点)

SSH 是最常被攻击的入口。编辑 /etc/ssh/sshd_config:

sudo vim /etc/ssh/sshd_config
PermitRootLogin no              # 禁止 root 直接登录
PasswordAuthentication no       # 禁用密码,只允许密钥登录
Port 2222                       # 改非标准端口(减少自动扫描)
MaxAuthTries 3                  # 限制尝试次数
AllowUsers hisos                # 只允许特定用户登录
sudo systemctl restart ssh      # 改完重启服务生效
WARNING

🚧 改 SSH 配置时保留当前连接别断开,另开一个新连接测试能登录后,再关旧连接。否则配置写错会把自己锁在外面。

密钥登录原理(比密码安全):

ssh-keygen -t ed25519           # 本地生成密钥对
ssh-copy-id user@host           # 公钥上传到服务器
# 之后免密登录,私钥不出本机,无法被暴力破解

11.6 fail2ban — 防暴力破解

自动封禁多次登录失败的 IP:

sudo apt install fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd   # 查看被封的 IP

原理:监控日志(如 /var/log/auth.log),某 IP 短时间内失败次数超阈值,就用防火墙临时封禁它。

11.7 查看登录与异常

last                    # 最近成功登录记录
lastb                   # 失败登录尝试(暴力破解迹象)
who                     # 当前在线用户
w                       # 在线用户 + 正在做什么

# 审计可疑项
ss -tlnp                # 检查意外开放的端口
ps aux                  # 检查可疑进程
sudo find / -perm -4000 -type f 2>/dev/null   # 列出所有 SUID 文件
sudo grep "Failed password" /var/log/auth.log | tail   # 看失败登录

11.8 系统更新(打补丁)

sudo apt update && sudo apt upgrade    # 更新所有软件,修复已知漏洞
sudo apt list --upgradable             # 看哪些有更新

# 自动安全更新(Ubuntu)
sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades

漏洞修复主要靠及时更新,长期不更新的系统是最大的安全隐患。

11.9 实战:新服务器安全初始化清单

# 1. 更新系统
sudo apt update && sudo apt upgrade -y

# 2. 创建低权用户(不用 root 日常操作)
sudo useradd -m -s /bin/bash deploy
sudo usermod -aG sudo deploy

# 3. 配置 SSH 密钥登录后,禁用密码登录
#    (编辑 sshd_config,见第5节)

# 4. 配置防火墙:先放行 SSH,再启用
sudo ufw allow 22
sudo ufw default deny incoming
sudo ufw enable

# 5. 安装 fail2ban
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban

# 6. 验证
sudo ufw status verbose
systemctl status ssh fail2ban

12 · 性能监控

12.1 性能监控看什么

系统性能瓶颈无非四类资源之一。监控就是找出哪类资源紧张,再定位到哪个进程:

CPU · 内存 · 磁盘 I/O · 网络

12.2 性能监控图

图 12 \xb7 性能监控

12.3 CPU 监控

top                     # 实时进程,按 P 排CPU
uptime                  # 快速看负载
nproc                   # CPU 核心数
mpstat 1                # 每核使用率(需 sysstat 包)

load average 解读

load average: 2.50, 1.80, 1.20 ↑ ↑ ↑ 1分钟 5分钟 15分钟

负载 ≈ 等待 + 运行的进程数。判断标准:和 CPU 核心数比。

  • 4 核机器,负载 4 = 满载
  • 负载持续 > 核心数 = CPU 不够用
TIP

📌 用“收银台排队”理解 load average:把 CPU 核心想象成超市收银台,进程是顾客。

  • load = 1.0(单核):收银台刚好有一人在结账,不排队,刚刚好
  • load = 2.0(单核):一人在结账,还有一人干等——繁忙,有人被阻塞
  • load = 0.5(单核):一半时间收银台空着,很闲

所以要和核心数比:4 核机器有 4 个收银台,load 4 才是满载。为什么给 1/5/15 分钟三个值?看趋势——如果 1 分钟高、15 分钟低,说明是刚起的瞬间峰;三个都高,说明持续繁忙该查原因了。

top 的 CPU 行

%Cpu(s): 25.0 us, 5.0 sy, 0.0 ni, 68.0 id, 2.0 wa ↑ ↑ ↑ ↑ 用户态 内核态 空闲 等待IO
字段 含义
us 用户态进程占用
sy 内核态占用
id 空闲
wa 等待 I/O(高说明磁盘是瓶颈,不是CPU)
TIP

📌 wa(iowait)是最容易误判的指标:很多人看到系统卡,第一反应是“CPU 不够”,加 CPU 却没用。其实要先看 wa:它高说明 CPU 在干等磁盘(比如查询在等慢硬盘返回数据),CPU 本身是空的(id 也可能不低)。这种情况加 CPU 毫无用处,该换快盘或优化 I/O。记住:id 高但系统卡 → 第一个怀疑 wa。

12.4 内存监控

free -h                 # 内存和 swap 总览
top                     # 按 M 排内存
ps aux --sort=-%mem | head   # 内存占用排名
vmstat 1                # 内存 + swap 动态变化

free 输出解读

total used free shared buff/cache available Mem: 15Gi 4Gi 2Gi 0.5Gi 9Gi 10Gi Swap: 2Gi 0Gi 2Gi ↑ 真正可用的内存看这列

关键:别看 free 列(Linux 会拿空闲内存做缓存),看 available 才是真正可用的。buff/cache 是可回收的缓存,需要时会自动让出。

TIP

📌 为什么 free(空闲)列总是很小,是内存不够了吗?不是!这是 Linux 的聪明之处:空着的内存是浪费的,所以它把暂时不用的内存拿去做磁盘缓存(buff/cache),加速文件读写。

用“停车场”打个比方:free 是“空着的车位”,buff/cache 是“临时停了车但随时可走的”。你真要停车时,这些临时车会马上让位。所以 available(free + 可回收的 cache)才是你真正能用的。看到 free 很小别慌,看 available。

📌 swap 为什么一用就卡?swap 是把内存里的数据换到磁盘上,腾出内存。但磁盘比内存慢几千倍!一旦频繁 swap,等于把“闪电的内存”降级成“龟速的磁盘”,性能断崖式下跌。所以 swap 频繁使用 = 内存不够的报警信号。

swap 预警:swap 被频繁使用说明物理内存不足,系统会把内存换到磁盘,性能急剧下降。

12.5 磁盘 I/O 监控

iostat -x 1             # 磁盘读写详情(需 sysstat 包)
sudo iotop              # 按进程看谁在读写磁盘
df -h                   # 磁盘空间
du -sh /path            # 目录占用

iostat 关键指标

指标 含义
%util 磁盘繁忙度,接近 100% = 磁盘瓶颈
await I/O 平均等待时间,高=响应慢
r/s w/s 每秒读/写次数

12.6 网络监控

ss -s                   # 连接状态统计
sudo iftop              # 实时带宽(需安装)
sudo nethogs            # 按进程看网络流量(需安装)
sar -n DEV 1            # 网卡流量(需 sysstat)

关注:带宽是否打满、TIME_WAIT 连接是否异常堆积。

12.7 一站式综合工具

htop                    # 交互式,CPU/内存/进程一屏看(强烈推荐)
glances                 # 全维度:CPU/内存/磁盘/网络一起看
dstat                   # 实时多指标综合统计

htop 是日常首选,彩色直观,可直接在界面里搜索、杀进程、调优先级。

12.8 性能排查方法论(核心)

遇到"系统卡",按这个顺序自顶向下定位:

① 看全局:top / htop → 判断是哪类资源紧张?CPU? 内存? 磁盘? 网络? ② 用专项工具深入: CPU高 → top 按 P,找占 CPU 的进程 内存高 → free 确认,ps --sort=-%mem 找进程 IO高 → iostat 看 %util,iotop 找进程 wa 高 → 是磁盘 I/O 拖累,不是 CPU ③ 定位到进程后: → 看进程日志、调整配置、加资源、或优化代码

典型误判:CPU id 很高但系统很卡 → 看 wa,多半是磁盘 I/O 瓶颈,不是 CPU 问题。

12.9 实战:系统卡顿排查

# 1. 先看全局
top                              # 看 load、%Cpu 各项、内存

# 2. 内存够不够
free -h                          # 看 available 和 swap

# 3. 磁盘是否瓶颈
iostat -x 1 3                    # 看 %util 和 await

# 4. 找出资源大户
ps aux --sort=-%cpu | head -5    # CPU 排名
ps aux --sort=-%mem | head -5    # 内存排名

# 5. 看系统日志有无异常
journalctl -p err -n 50          # 最近的错误日志
dmesg | tail                     # 内核消息(如 OOM 杀进程)

13 · 专题:Nginx 配置

接上 project.md 里装的 nginx,深入它的配置。Nginx 是最主流的 Web 服务器/反向代理。

13.1 Nginx 是什么、能干什么

Nginx 读作 "engine-x",是一个高性能的 Web 服务器 + 反向代理。它的核心价值:用户的所有请求先到 Nginx,由它决定怎么处理。

两个角色:

  • Web 服务器:直接返回静态文件(html、图片、css)
  • 反向代理:把请求转发给后端应用(如 Node.js、Java 程序)

13.2 Nginx 工作模型图

图 13 \xb7 Nginx 配置

13.3 正向代理 vs 反向代理(最容易混)

TIP

📌 用"代购"和"前台"区分:

  • 正向代理(如翻墙 VPN):替客户端出门办事。服务器不知道真实的你,只看到代理。像代购替你出国买东西。
  • 反向代理(Nginx):替服务器接待客人。客户端不知道背后有几台服务器,只看到 Nginx。像公司前台,访客只跟前台说,前台转达给具体部门。

一句话:正向代理藏客户端,反向代理藏服务器。Nginx 是后者。

13.4 为什么要用反向代理(设计动机)

明明应用自己也能监听端口,为什么前面要套一层 Nginx?

痛点 Nginx 解决方式
后端应用直接暴露公网不安全 Nginx 当唯一入口,隐藏后端
应用只能跑在某端口(如 3000) Nginx 监听 80/443,转发给 3000
想跑多个网站 一个 Nginx 用 server_name 区分多站点
HTTPS 证书每个应用都配麻烦 Nginx 统一做 SSL 终结
一台后端扛不住 Nginx 负载均衡分发到多台
静态文件让应用处理太浪费 Nginx 直接返回静态文件,快得多
TIP

📌 核心思想:把"通用的网络层杂活"(HTTPS、缓存、限流、负载均衡)从应用里剥离,交给专门的 Nginx 做。应用只管业务逻辑。这是关注点分离。

13.5 配置文件结构

Nginx 配置是层层嵌套的块(block):

http {                          # 所有 HTTP 配置的总容器
    server {                    # 一个虚拟主机(一个网站)
        listen 80;              # 监听端口
        server_name example.com;  # 匹配的域名

        location / {            # 匹配的 URL 路径
            root /var/www/html; # 文件根目录
            index index.html;   # 默认首页
        }
    }
}

三层关系:

  • http — 全局设置(所有站点共享)
  • server — 一个站点(一个域名/端口组合),可以有多个
  • location — 站点内不同 URL 路径的处理规则,可以有多个
TIP

📌 像三层抽屉:http 是整个柜子的通用规则,server 是一个抽屉(一个网站),location 是抽屉里的分隔格(不同路径分开处理)。请求来了,先找哪个 server(按域名+端口),再找哪个 location(按路径)。

13.6 配置文件位置(Ubuntu)

/etc/nginx/nginx.conf            # 主配置,引入下面的站点
/etc/nginx/sites-available/      # 写站点配置(仓库)
/etc/nginx/sites-enabled/        # 软链接指向 available(已启用)
/var/www/html/                   # 默认网站根目录
TIP

📌 available 和 enabled 为什么分两个目录?这是个聪明设计,呼应 04 章的软链接:sites-available 是"所有写好的站点配置仓库",sites-enabled 放软链接,只链接当前要启用的。想临时停用一个站点,删软链接即可,配置文件还留着,不用删。

# 启用一个站点 = 建软链接
sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
# 停用 = 删软链接(配置还在 available)
sudo rm /etc/nginx/sites-enabled/mysite

13.7 四大常见配置

① 静态网站

server {
    listen 80;
    server_name mysite.com;
    location / {
        root /var/www/mysite;    # 文件在这个目录找
        index index.html;
    }
}

② 反向代理(转发给后端应用)

server {
    listen 80;
    server_name api.mysite.com;
    location / {
        proxy_pass http://127.0.0.1:3000;       # 转发给本机 3000 端口
        proxy_set_header Host $host;            # 把原始 Host 传给后端
        proxy_set_header X-Real-IP $remote_addr; # 把真实客户端 IP 传给后端
    }
}
TIP

📌 为什么要 proxy_set_header?因为请求经 Nginx 转发后,后端看到的"客户端"是 Nginx(127.0.0.1),看不到真实用户。这几行把原始信息(域名、真实 IP)塞进请求头传给后端,否则后端日志里全是 Nginx 的 IP。

③ 负载均衡(分发到多台后端)

upstream backend {           # 定义一组后端服务器
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
    server 10.0.0.3:3000;
}
server {
    listen 80;
    location / {
        proxy_pass http://backend;   # 转发给这组,Nginx 自动轮流分发
    }
}

默认轮询(round-robin)依次分发;可改 least_conn(发给连接最少的)等策略。

④ HTTPS

server {
    listen 443 ssl;
    server_name mysite.com;
    ssl_certificate     /etc/nginx/ssl/cert.pem;    # 证书
    ssl_certificate_key /etc/nginx/ssl/key.pem;     # 私钥
    location / { root /var/www/mysite; }
}
server {                          # HTTP 自动跳转 HTTPS
    listen 80;
    server_name mysite.com;
    return 301 https://$host$request_uri;
}
TIP

📌 什么是"SSL 终结"?HTTPS 加解密很耗 CPU。让 Nginx 统一处理加解密(在 Nginx 这里"终结"加密),它和后端之间走普通 HTTP(内网安全)。后端应用完全不用管证书和加密,省心又省 CPU。

13.8 核心操作命令

sudo nginx -t                    # 检查配置语法(改完必做!)
sudo systemctl reload nginx      # 平滑重载配置(不断连接)
sudo systemctl restart nginx     # 重启(会断连接)
sudo nginx -s reload             # 等价于 reload
tail -f /var/log/nginx/access.log  # 看访问日志
tail -f /var/log/nginx/error.log   # 看错误日志(排查必看)
TIP

📌 为什么改配置后先 nginx -t 再 reload?这是血泪经验:配置写错时直接 reload,Nginx 可能加载失败导致整个网站挂掉。nginx -t 先做语法检查,通过了再 reload 才安全。呼应 10 章 fstab 的 mount -a 思想——危险操作前先验证。

📌 reload 和 restart 区别?呼应 09 章:reload 是平滑重载,老连接继续服务、新连接用新配置,用户无感;restart 是先停后起,会瞬间断开所有连接。生产环境优先 reload。

13.9 实战:配一个反向代理站点

# 1. 写站点配置(假设后端应用跑在 3000)
sudo tee /etc/nginx/sites-available/myapp << 'EOF'
server {
    listen 80;
    server_name myapp.local;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
EOF

# 2. 启用站点(建软链接)
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/

# 3. 检查语法
sudo nginx -t

# 4. 平滑重载
sudo systemctl reload nginx

# 5. 排查时看日志
sudo tail -f /var/log/nginx/error.log

13.10 排查 Nginx 问题的顺序

① nginx -t 语法对不对 ② systemctl status nginx 服务起没起 ③ ss -tlnp | grep nginx 端口监听没 ④ tail error.log 具体报什么错 ⑤ curl -I localhost 本机能不能访问 ⑥ ufw status 防火墙放行没(11章)

全部串联了前面学的:服务(09)、端口(07)、日志、防火墙(11)。

13.11 免费 HTTPS 证书(certbot / Let's Encrypt)

第 7 节配 HTTPS 需要证书。生产上证书从哪来?Let's Encrypt 提供免费证书,certbot 工具自动申请、配置、续期,一条命令搞定。

# 装 certbot 的 nginx 插件
sudo apt install -y certbot python3-certbot-nginx

# 自动申请证书并改好 nginx 配置(要求域名已解析到本机、80端口可访问)
sudo certbot --nginx -d mysite.com -d www.mysite.com

# 测试自动续期(证书有效期 90 天)
sudo certbot renew --dry-run
TIP

📌 certbot 怎么证明"这域名是你的"?它用 ACME 协议做验证:certbot 在你服务器上放一个特定文件,Let's Encrypt 通过 http://你的域名/.well-known/... 来访问这个文件——能访问到,就证明你确实控制这个域名和服务器,于是签发证书。所以申请前域名必须先解析到本机、80 端口要能被外网访问(呼应 07 DNS + 11 防火墙放行 80)。

📌 为什么有效期只有 90 天?短有效期是安全设计:万一私钥泄露,危害窗口短。但人工每 90 天换一次太麻烦,所以 certbot 装好后会自动加一个 systemd timer(呼应 09 章)定期续期,全程无人值守。certbot --nginx 还会自动帮你改好 nginx 配置(加 ssl_certificate、配 80→443 跳转),省去手写。

14 · 专题:Docker 容器

现代部署的核心。理解 Docker 前先回顾 06 进程章——容器本质就是"被隔离的进程"。

14.1 Docker 解决什么问题

经典痛点:"在我电脑上能跑,到服务器就挂了"。原因是两边的依赖版本、环境变量、系统库不一致。

Docker 把应用和它的整个运行环境(依赖、库、配置)打包成一个标准盒子,这个盒子在任何装了 Docker 的机器上跑起来都一模一样。

TIP

📌 用"集装箱"理解(Docker 的 logo 就是装满集装箱的鲸鱼):以前运货,散装货物在轮船/火车/卡车间换装时各种麻烦。有了标准集装箱,里面装什么不管,外面尺寸统一,任何运输工具都能直接搬。Docker 就是软件界的集装箱——把应用标准化打包,任何机器都能直接运行。

14.2 Docker 工作模型图

图 14 \xb7 Docker 容器

14.3 三个核心概念

概念 是什么 类比
镜像 Image 只读模板,含应用+依赖+环境 菜谱 / 安装光盘 / class
容器 Container 镜像运行起来的实例 做好的菜 / 装好的系统 / 对象
仓库 Registry 存放镜像的地方(Docker Hub) 应用商店 / GitHub
TIP

📌 镜像和容器的关系 = 类和对象:镜像是静态模板(只读),容器是它跑起来的实例。一个镜像能起任意多个容器,就像一个 class 能 new 出多个对象。改容器里的东西不影响镜像,删了容器再从镜像起一个又是干净的。

14.4 容器 vs 虚拟机(关键区别)

TIP

📌 为什么容器比虚拟机轻这么多?回顾 01 章的内核概念:

  • 虚拟机:每个 VM 里装一个完整的操作系统(含自己的内核),GB 级、启动要几分钟。像在楼里每间房都盖一套独立水电系统。
  • 容器:所有容器共享宿主机的同一个内核,只打包应用和必要的库,MB 级、秒级启动。像每间房共用大楼的水电主管道,只是各自隔开。

容器靠 Linux 内核的两个机制实现隔离(呼应 06 进程章):

  • 命名空间(namespace):让容器以为自己独占进程树、网络、文件系统(其实是被"障眼法"隔开的)
  • cgroups:限制每个容器能用多少 CPU、内存(资源配额)

所以本质上,容器就是宿主机上一个被隔离、被限额的普通进程,不是什么虚拟机。

14.5 镜像操作

docker pull nginx              # 从仓库下载镜像
docker images                  # 列出本地镜像
docker rmi nginx               # 删除镜像
docker search nginx            # 搜索镜像
docker pull nginx:1.25         # 指定版本(标签 tag),默认 latest
TIP

📌 镜像的分层(layer):镜像不是一整块,而是一层层叠起来的(基础系统层 + 依赖层 + 应用层)。多个镜像能共享相同的底层,省空间。改动只在最上层,这也是为什么改一行代码重新构建很快——底层不变直接复用缓存。

14.6 容器操作(最常用)

# 运行容器
docker run -d -p 8080:80 --name web nginx
#          ↑   ↑          ↑          ↑
#       后台  端口映射   起名字     用哪个镜像

docker ps                      # 列出运行中的容器
docker ps -a                   # 包括已停止的
docker stop web                # 停止
docker start web               # 启动
docker restart web             # 重启
docker rm web                  # 删除(需先停止)
docker logs -f web             # 看容器日志(呼应 tail -f)
docker exec -it web bash       # 进入容器内部的 shell
TIP

📌 -p 8080:80 端口映射什么意思?呼应 07 网络章:容器内部 nginx 监听 80,但容器是隔离的,外面访问不到。-p 8080:80 在宿主机 8080 端口和容器 80 端口之间打通道——访问 宿主机IP:8080 就转到容器的 80。冒号左边是宿主机,右边是容器。

📌 docker exec -it web bash 为什么能"进入"容器?因为容器就是个进程,这条命令是在那个隔离环境里再起一个 bash 进程,让你像 SSH 进服务器一样在容器内部操作。-it = 交互式终端。

14.7 数据持久化:数据卷(Volume)

TIP

📌 为什么需要数据卷?容器是"用完即弃"的——删掉容器,里面写的数据全没了(像便利贴,撕了就没)。但数据库、上传的文件不能丢。数据卷把宿主机的目录挂进容器,数据实际存在宿主机上,容器删了数据还在。这本质就是 02 章的 mount 挂载思想。

# 把宿主机目录挂进容器(-v 宿主机路径:容器路径)
docker run -d -v /home/hisos/data:/var/lib/mysql --name db mysql

# 用 docker 管理的命名卷
docker volume create mydata
docker run -d -v mydata:/data --name app myapp
docker volume ls               # 列出所有卷

14.8 Dockerfile:构建自己的镜像

Dockerfile 是构建镜像的"说明书",一行行写清楚怎么搭环境:

FROM node:18                   # 基于哪个基础镜像
WORKDIR /app                   # 设工作目录
COPY package.json .            # 复制依赖清单进镜像
RUN npm install                # 在构建时执行(装依赖)
COPY . .                       # 复制源码进镜像
EXPOSE 3000                    # 声明容器监听的端口
CMD ["node", "server.js"]      # 容器启动时运行的命令
docker build -t myapp:1.0 .    # 按 Dockerfile 构建镜像(. 是上下文目录)
docker run -d -p 3000:3000 myapp:1.0
TIP

📌 RUN 和 CMD 的区别(容易混):RUN 在构建镜像时执行(如装依赖,结果固化进镜像);CMD 在容器启动时执行(如启动应用)。一个是"做菜时的准备",一个是"上桌时的动作"。

📌 为什么 COPY package.json 和 COPY . . 分两步?利用第 5 节说的分层缓存:依赖不常变,单独一层;源码常改,放最上层。改代码重新 build 时,依赖那层命中缓存不用重装,构建飞快。这是 Dockerfile 优化的核心技巧。

14.9 Docker Compose:编排多容器

真实应用常是"网站 + 数据库 + 缓存"多个容器。用 docker-compose.yml 一个文件全描述、一条命令全启动:

services:
  web:
    image: nginx
    ports:
      - "8080:80"
  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: secret
    volumes:
      - dbdata:/var/lib/mysql
volumes:
  dbdata:
docker compose up -d           # 按 yml 启动所有容器
docker compose down            # 停止并删除所有容器
docker compose logs -f         # 看所有容器日志
docker compose ps              # 看状态
TIP

📌 为什么要 Compose?手动一个个 docker run 还要配网络让它们互通,繁琐易错。Compose 把整套服务的定义写进一个文件,up 一下全起来、自动配好互联网络。呼应 09 章 systemd 的思想——把启动编排交给配置文件,而非手敲命令。

14.10 实战:容器化一个 Web 服务

# 1. 拉取并运行 nginx,映射端口
docker run -d -p 8080:80 --name myweb nginx

# 2. 验证(呼应 07 curl)
curl -I http://localhost:8080      # 应返回 200

# 3. 把自定义页面挂进去(数据卷)
echo "<h1>Hello Docker</h1>" > ~/index.html
docker rm -f myweb
docker run -d -p 8080:80 \
  -v ~/index.html:/usr/share/nginx/html/index.html \
  --name myweb nginx
curl http://localhost:8080         # 看到自定义内容

# 4. 进容器内部看看
docker exec -it myweb bash
#  (容器内) ls /usr/share/nginx/html ; exit

# 5. 查看资源占用(呼应 12 性能)
docker stats --no-stream

# 6. 清理
docker rm -f myweb

14.11 排查 Docker 问题的顺序

① docker ps -a 容器在不在、什么状态(Exited 说明挂了) ② docker logs 容器 看容器内部报什么错 ③ docker inspect 容器 看详细配置(端口、挂载、网络) ④ docker exec -it 容器 sh 进去看内部环境 ⑤ ss -tlnp | grep 端口 宿主机端口映射对不对(07) ⑥ docker stats 资源是否爆了(12)

全程串联前面:进程隔离(06)、端口(07)、服务编排思想(09)、挂载(02)、性能(12)。

15 · 常用系统技能

几个日常高频但前面没专门讲的系统技能:定时任务、vim、压缩归档、环境变量。

15.1 cron — 定时任务

09 章讲过 systemd timer,但传统的 cron 更普及、更简单,至今广泛使用。

crontab -e          # 编辑当前用户的定时任务
crontab -l          # 查看
crontab -r          # 删除所有(慎用)
sudo crontab -e -u root   # 编辑指定用户的

五个时间字段(核心):

* * * * * 要执行的命令 │ │ │ │ │ │ │ │ │ └─ 星期几 (0-7,0和7都是周日) │ │ │ └─── 月 (1-12) │ │ └───── 日 (1-31) │ └─────── 时 (0-23) └───────── 分 (0-59)

常见例子:

0 3 * * *      /home/hisos/backup.sh      # 每天凌晨3点
*/5 * * * *    /usr/bin/check.sh          # 每5分钟
0 0 * * 0      /usr/bin/weekly.sh         # 每周日0点
30 2 1 * *     /usr/bin/monthly.sh        # 每月1号2:30
0 9-18 * * 1-5 /usr/bin/worktime.sh       # 工作日9-18点每小时
TIP

📌 怎么记这五个字段顺序?从左到右"分时日月周",单位从小到大(分钟最小)。* 表示"每",*/5 表示"每隔5"。先把分钟定死(如 0),否则 * * * * * 会变成每分钟都跑。

📌 cron vs systemd timer 怎么选?cron 简单、一行搞定、适合个人脚本;systemd timer 功能强(能依赖服务、记日志、开机错过能补跑),适合正式服务。新手日常用 cron 足够。

# 排查 cron 没执行:看日志
grep CRON /var/log/syslog        # 看 cron 有没有触发
# 注意:cron 环境变量极少,脚本里命令最好写绝对路径

15.2 vim — 服务器必备编辑器

服务器上改配置躲不开 vim(前面多次出现 vim /etc/...)。它有两个核心模式,理解了就不慌。

TIP

📌 为什么 vim 这么"反人类"?因为它诞生于没有鼠标、连方向键都未必有的年代,设计目标是"手不离主键盘区就能高效编辑"。它分模式:普通模式下按键是命令(移动、删除),插入模式下按键才是打字。新手卡住通常是忘了切模式。

打开 vim → 默认在【普通模式】(不能直接打字!) 按 i → 进入【插入模式】(这时才能输入文字) 按 Esc → 回到【普通模式】 输入 :wq → 保存退出

最小够用命令集:

操作 按键 说明
进入插入模式 i 在光标前插入
回普通模式 Esc 退出编辑
保存退出 :wq 或 ZZ write + quit
不保存退出 :q! 强制退出丢弃改动
删除一行 dd (普通模式)
撤销 / 重做 u / Ctrl+r
搜索 /关键词 回车 n 下一个
跳到行首/尾 0 / $
跳到文件首/尾 gg / G
显示行号 :set nu
TIP

📌 新手最常卡的两件事:① 打开就猛敲字却没反应——你在普通模式,先按 i。② 想退出退不出——按 Esc 再输 :q!(丢弃)或 :wq(保存)。记住"卡住就按 Esc"。

15.3 tar / 压缩归档

备份脚本里用过 tar -czf,这里系统讲。注意**"归档"和"压缩"是两件事**:

TIP

📌 归档 vs 压缩:tar 本来只"归档"——把一堆文件打包成一个 .tar 文件(像把散件装进一个箱子,不减体积)。gzip 才负责"压缩"(把箱子抽真空变小)。常见的 .tar.gz 就是先 tar 打包再 gzip 压缩,所以命令里 -c(打包) 和 -z(gzip) 一起用。

# 打包压缩(最常用)
tar -czf backup.tar.gz /path/to/dir
#     ↑↑↑
#     c=创建 z=gzip压缩 f=指定文件名

# 解压
tar -xzf backup.tar.gz           # x=解压
tar -xzf backup.tar.gz -C /tmp   # 解到指定目录

# 只看内容不解压
tar -tzf backup.tar.gz           # t=列出

# 加 v 看过程
tar -czvf backup.tar.gz /path    # v=显示每个文件
TIP

📌 怎么记 czf / xzf:c=create(打包)、x=extract(解压)、t=list(看),三选一;z=用 gzip;f=后面跟文件名(必须放最后)。打包用 c、解压用 x,其余不变。

# 其他常见格式
tar -cjf x.tar.bz2 dir/   # j=bzip2(压缩率更高更慢)
zip -r x.zip dir/         # zip(跨平台,Windows 友好)
unzip x.zip               # 解 zip
gzip file / gunzip file.gz  # 单文件压缩/解压

15.4 环境变量与 PATH

前面多次提到 $PATH、.bashrc,这里集中讲清。环境变量是 shell 和程序共享的"全局设置"。

echo $PATH               # 看可执行文件搜索路径
echo $HOME               # 家目录
env                      # 列出所有环境变量
export NAME="value"      # 设置环境变量(当前会话)
unset NAME               # 删除
TIP

📌 PATH 到底是什么?回顾 01 章:你敲 ls,bash 怎么知道去哪找 ls 程序?它按 $PATH 里列的目录(用 : 分隔)逐个找,找到第一个 ls 就执行。这就是为什么自己写的程序要么放进 PATH 目录,要么写全路径 ./myprog——不在 PATH 里 bash 找不到。

# 临时把目录加进 PATH(仅当前会话)
export PATH="$PATH:/home/hisos/bin"

# 永久生效:写进 ~/.bashrc
echo 'export PATH="$PATH:/home/hisos/bin"' >> ~/.bashrc
source ~/.bashrc         # 立即生效(回顾 05 章 source)
TIP

📌 .bashrc、.profile、.bash_profile 区别?.bashrc 每开一个交互式终端就加载(最常改这个);.profile/.bash_profile 登录时加载一次。改了环境变量配置后必须 source 或重开终端才生效(呼应 05 章:用 ./ 跑改不了当前 shell)。

# 设置别名(高频,省敲命令)
alias ll='ls -alh'       # 当前会话
echo "alias ll='ls -alh'" >> ~/.bashrc   # 永久

15.5 时间与时区

服务器时间不对会引发一连串怪问题:日志时间戳错乱、HTTPS 证书校验失败、定时任务(cron)在错误时间触发、分布式系统数据不一致。

date                     # 看当前时间
date "+%Y-%m-%d %H:%M:%S"  # 格式化输出(脚本里常用)
timedatectl              # 看时间、时区、NTP 同步状态(推荐)
$ timedatectl Local time: 三 2026-06-18 18:20:00 CST Time zone: Asia/Shanghai (CST, +0800) System clock synchronized: yes ← NTP 是否已自动同步 NTP service: active
# 改时区
sudo timedatectl set-timezone Asia/Shanghai
timedatectl list-timezones | grep Asia   # 查可用时区

# 开启自动网络对时(NTP)
sudo timedatectl set-ntp true
TIP

📌 为什么服务器都用 UTC 或统一时区?跨地域的服务器如果各用本地时区,日志一对比就乱套(到底谁先谁后?)。运维惯例是服务器统一用 UTC,展示时再按用户所在时区转换。这样无论机器在哪,日志时间都能直接比较。

📌 什么是 NTP?Network Time Protocol,机器定期向时间服务器对表,自动校正几毫秒的偏差。不开 NTP,机器时钟会慢慢漂移,几个月后差好几分钟,证书、定时任务就出问题。System clock synchronized: yes 就是它在工作。

# 硬件时钟 vs 系统时钟
sudo hwclock --systohc   # 把系统时间写回主板硬件时钟(断电也不丢)
TIP

📌 两个时钟:系统时钟在内存里,开机时从硬件时钟(主板上有纽扣电池供电)读取初值,之后由内核维护。NTP 同步的是系统时钟,hwclock --systohc 再把它写回硬件,保证下次开机也准。

15.6 tmux — 终端复用(SSH 断了任务不挂)

TIP

📌 解决什么痛点?呼应 06 章:你 SSH 上服务器跑一个耗时几小时的任务,网络一抖断线,shell 进程收到 HUP 信号,连带任务一起被杀。nohup & 能让单个命令活下来,但你想"重新连上去看它跑得怎样、继续操作"就做不到了。tmux 把会话留在服务器端——断线后会话还在后台跑,重连后一条命令就回到原样,光标、输出、多个窗口全在。

tmux                     # 新建会话
tmux new -s work         # 新建并命名为 work
tmux ls                  # 列出所有会话
tmux attach -t work      # 重新连回 work 会话(断线重连就靠它)

进入 tmux 后的快捷键(都先按前缀 Ctrl+b,松开再按下一个键):

按键 作用
Ctrl+b 然后 d detach 脱离会话(任务继续在后台跑)
Ctrl+b 然后 c 新建一个窗口
Ctrl+b 然后 数字 切到第 N 个窗口
Ctrl+b 然后 % / " 左右 / 上下 分屏
Ctrl+b 然后 方向键 在分屏间跳
TIP

📌 典型用法:上服务器先敲 tmux,在里面跑任务,Ctrl+b d 脱离,关掉笔记本回家。第二天 tmux attach 连回来,任务还在跑、输出都在。这就是为什么运维跑长任务(数据迁移、编译、压测)几乎都在 tmux 里——会话的生命周期和你的 SSH 连接解绑了。screen 是更老的同类工具,思路一样。

16 · 文本三剑客:grep / sed / awk

前面 03 章学过 grep,附录速查表提了 sed/awk。这里系统讲三者分工——它们是 Linux 文本处理的核心,运维日志分析、批量改配置全靠它。

16.1 三者分工(一句话记住)

TIP

📌 打个比方,三剑客各管一摊,配合管道流水作业:

  • grep = 找行:从一堆文本里筛出匹配的行(过滤器)
  • sed = 改行:对行做替换、删除、插入(流式编辑器)
  • awk = 切列:把每行按列拆开,取字段、算数、做报表(列处理器)

口诀:grep 挑行、sed 改字、awk 取列。一行日志进来,先 grep 挑出错误行,再 awk 取出其中的 IP 列,这就是它们的典型配合。

16.2 sed — 流式编辑

最高频的就是替换:

sed 's/old/new/'  file       # 每行第一个 old 换成 new(只打印不改文件)
sed 's/old/new/g' file       # g=global,一行内全部替换
sed -i 's/old/new/g' file    # -i=直接改文件(危险,先不带 -i 预览!)
sed -i.bak 's/x/y/g' file    # -i.bak=改之前自动备份成 file.bak(更安全)

其他常用动作:

sed -n '5,10p'  file         # 只打印 5~10 行(-n 关闭默认输出,p 打印)
sed '3d'        file         # 删除第 3 行
sed '/^#/d'     file         # 删除所有注释行(配合正则)
sed '/^$/d'     file         # 删除空行
sed '2i\新内容'  file         # 在第 2 行前插入(i=insert,a=append 追加)
TIP

📌 为什么叫"流式"编辑器?sed 不像 vim 把整个文件读进来再编辑,而是逐行读入、处理、吐出,像流水线一样。所以它能处理超大文件不爆内存,也天然适合放进管道。

-i 是不可逆的,直接改原文件。养成习惯:先不带 -i 看输出对不对,确认了再加 -i,或者用 -i.bak 留个备份。

16.3 awk — 按列处理

awk 把每行自动按空白分割成字段:$1 第一列、$2 第二列……$0 是整行,NF 是列数,NR 是行号。

awk '{print $1}'        file    # 打印第 1 列
awk '{print $1, $3}'    file    # 第 1 和第 3 列
awk -F: '{print $1}'  /etc/passwd  # -F 指定分隔符为冒号(取用户名)
awk '{print NR, $0}'    file    # 给每行加行号
awk 'NF > 0'            file    # 只打印非空行(NF=字段数)

带条件和计算(awk 其实是个小型编程语言):

# 过滤:打印第 3 列大于 100 的行
awk '$3 > 100 {print $0}' file

# 求和:把第 2 列累加
awk '{sum += $2} END {print "总计:", sum}' file

# 统计:每个值出现几次(类似 uniq -c 但更灵活)
awk '{count[$1]++} END {for (k in count) print k, count[k]}' file
TIP

📌 {} 前面的是条件,后面的是动作:$3>100 {print} 读作"当第3列>100时,打印"。不写条件就对每行都执行,不写动作默认 print。BEGIN{} 在处理前跑一次(打表头),END{} 在最后跑一次(出汇总)。

16.4 实战:分析 Nginx 访问日志

把三剑客和前面学的管道串起来,这是运维日常:

# 日志格式:IP - - [时间] "GET /path" 状态码 ...

# 统计访问量 Top 10 的 IP(awk 取IP列 → 排序 → 去重计数 → 再排序)
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head

# 找出所有 500 错误的请求(grep 挑行 → awk 取列)
grep ' 500 ' access.log | awk '{print $1, $7}'

# 统计每个状态码出现次数
awk '{print $9}' access.log | sort | uniq -c | sort -rn

# 把日志里的内网 IP 脱敏后另存(sed 替换)
sed 's/192\.168\.[0-9]*\.[0-9]*/***.***/g' access.log > masked.log
TIP

📌 这就是 Linux 哲学的高光时刻(呼应 03 管道章):没有一个"日志分析专用软件",但 grep + awk + sort + uniq 几个小工具一拼,就是一套强大的日志分析流水线。学会组合,胜过记一堆专用命令。

16.5 正则表达式基础(三剑客的共同语言)

grep/sed/awk 的"匹配"能力都来自正则表达式(regex)——一套描述"文本模式"的通用语法。学一次,三个工具通用。

TIP

📌 正则在描述什么?普通搜索是"找字面量 abc";正则是"找符合某种规律的文本",比如"以数字开头""像邮箱的字符串""3 个连续的 a"。它用一些特殊符号当"占位规则",让你不必列举所有可能。

核心元字符

符号 含义 例子
. 任意单个字符 a.c 匹配 abc、a9c
* 前一项重复 0 次或多次 ab* 匹配 a、ab、abbb
+ 前一项重复 1 次或多次 ab+ 匹配 ab、abbb(不匹配 a)
? 前一项 0 次或 1 次(可选) colou?r 匹配 color、colour
^ 行首 ^# 匹配以 # 开头的行
$ 行尾 ;$ 匹配以分号结尾的行
[] 字符集合(任选一个) [0-9] 一个数字,[a-z] 一个小写字母
[^] 取反集合 [^0-9] 非数字
| 或 cat|dog 匹配 cat 或 dog
{n,m} 重复 n 到 m 次 [0-9]{3} 恰好 3 个数字
() 分组 (ab)+ 匹配 ab、abab
\ 转义(让特殊符号变普通) \. 匹配真正的点号

基础正则 vs 扩展正则(一个常见坑)

grep  "ab\+" file       # 基础正则(BRE):+ ? { } | () 要加反斜杠才生效
grep -E "ab+" file      # 扩展正则(ERE):-E 后这些符号直接用,更直观(推荐)
egrep "ab+" file        # = grep -E
sed -E 's/(a+)/[\1]/' file   # sed 也用 -E 开扩展正则
TIP

📌 为什么 grep 的 + 有时不灵?这是历史遗留:grep 默认用基础正则(BRE),+、?、|、() 被当普通字符,要写成 \+ 才有特殊含义。太反直觉,所以实践中一律加 -E 用扩展正则(ERE),符号直接用,和大多数语言的正则一致。记住:写正则没生效,先试试加 -E。

实用例子

grep -E "^[0-9]+" file              # 以数字开头的行
grep -E "\b[0-9]{1,3}(\.[0-9]{1,3}){3}\b" log   # 匹配 IP 地址
grep -E "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}" file  # 匹配邮箱
grep -vE "^#|^$" config             # 排除注释行和空行(看有效配置)
TIP

📌 grep -vE "^#|^$" 是查配置文件的神器:^# 注释行、^$ 空行,| 连起来,-v 反向排除。一条命令把几百行带满注释的配置文件,浓缩成几行真正生效的设置。

17 · 系统启动流程

理解开机时发生了什么,能串联 01(内核)、06(进程)、09(systemd)三章,也是排查"开机失败""卡住"的基础。

17.1 五个阶段(从按电源到登录界面)

图 15 \xb7 系统启动流程

① BIOS/UEFI → ② 引导程序 GRUB → ③ 内核 kernel → ④ systemd(PID 1) → ⑤ 登录 固件自检 从磁盘加载内核 挂载根文件系统 启动所有服务 给你 shell
阶段 干什么 呼应章节
① BIOS/UEFI 主板固件,自检硬件(POST),找到启动磁盘 —
② GRUB 引导程序,显示启动菜单,把内核加载进内存 —
③ 内核 kernel 解压自身、初始化硬件驱动、挂载根文件系统 / 01 内核
④ systemd 内核启动的第一个进程(PID 1),按依赖关系拉起所有服务 06 PID1 · 09 服务
⑤ 登录 systemd 启动登录服务,给出登录提示符 —
TIP

📌 一句话串起来:固件找到内核 → 内核准备好系统底座 → systemd 把所有服务一一拉起 → 你能登录用了。回顾 06 章——systemd 是 PID 1、所有进程的祖先,它的诞生就在第④步。

17.2 为什么需要"引导程序"GRUB

TIP

📌 鸡生蛋问题:内核是个文件,躺在磁盘上。但"读取磁盘上的文件"这件事,本来是内核才会干的——内核还没启动,谁来读内核?这就需要一个极小的、不依赖内核的程序先上场,它就是 GRUB。

GRUB 足够简单,能直接和 BIOS/磁盘打交道,把内核文件搬进内存并跳过去执行。它还能管理多系统启动(菜单里选 Ubuntu 还是 Windows)、传内核参数(比如进救援模式)。

17.3 运行级别 / target(开机进入什么状态)

systemd 用 target 描述系统要进入的状态(取代老的 runlevel):

systemctl get-default              # 看默认启动目标
systemctl set-default multi-user.target   # 设为命令行模式(无图形,省资源)
systemctl set-default graphical.target    # 设为图形界面模式
systemctl isolate rescue.target    # 立即切到救援模式(单用户,修系统用)
target 含义 类比老 runlevel
multi-user.target 多用户命令行(服务器常用) 3
graphical.target 图形界面 5
rescue.target 救援模式(最小环境,修复用) 1
TIP

📌 服务器为什么常设 multi-user.target?图形界面吃内存、占资源,服务器没人坐在前面看屏幕,纯命令行就够。呼应 12 性能章——省下的资源全留给业务。

17.4 排查开机/启动问题

systemd-analyze              # 看开机总耗时
systemd-analyze blame        # 按耗时排序,揪出拖慢开机的服务
journalctl -b                # 看本次开机的全部日志(-b = boot)
journalctl -b -1             # 看上一次开机的日志(排查上次崩溃)
journalctl -p err -b         # 只看本次开机的错误
systemctl --failed           # 列出启动失败的服务
TIP

📌 开机变慢怎么查?systemd-analyze blame 直接列出每个服务的启动耗时,排在最前的就是元凶(常见是某个网络等待、磁盘挂载超时)。这把 09 章的服务管理和 12 章的性能排查连起来了。

结业总结

整体知识地图

Phase 1 基础 01 架构 → 02 目录 → 03 命令 → 04 权限 Phase 2 进阶 05 脚本 → 06 进程 → 07 网络 → 08 包管理 Phase 3 实战 09 服务 → 10 存储 → 11 安全 → 12 性能 专题深入 13 Nginx 配置 → 14 Docker 容器 → 15 常用系统技能 16 文本三剑客 → 17 系统启动流程

贯穿全程的核心主线

  1. 一切皆文件:配置(/etc)、设备(/dev)、内核信息(/proc) 都是文件,所以 cat/grep 通吃
  2. 分层与隔离:用户态 → syscall → 内核 → 硬件;权限模型隔离用户
  3. 进程是核心:fork/exec 创建,systemd(PID1) 管理,信号控制
  4. 组合优于复杂:管道把单一命令串成强大流水线

运维排查万能流程

有问题 → 看日志(journalctl / /var/log) → 看资源(top / free / df) → 看进程(ps / ss) → 看配置(/etc 下对应文件)

附录 A · 命令速查手册

高频命令一页查。原理见对应正文章节。

文件与目录

命令 作用
ls -alh 列目录(含隐藏、详细、可读大小)
ls -lt / ls -lS 按时间 / 大小排序
cd - / cd ~ 回上个目录 / 回家目录
pwd 当前路径
mkdir -p a/b/c 递归建目录
cp -a src dst 完整复制(保留属性)
mv old new 移动/重命名
rm -rf dir/ 强制递归删除(危险)
ln -s 目标 链接 创建软链接
find / -name "*.log" -size +10M 按名+大小找文件
find . -mtime +30 -delete 删 30 天前的文件

文本处理

命令 作用
cat -n file 输出+行号
less file 分页(/ 搜索,q 退出)
head -20 / tail -20 前/后 20 行
tail -f log 实时追踪日志
grep -rn "词" 目录 递归搜内容+行号
grep -v "^#" 排除注释行
grep -i 忽略大小写
awk '{print $1}' 取第 1 列
sed 's/旧/新/g' 替换文本
`sort uniq -c`
wc -l 数行数
cut -d: -f1 按分隔符取列

管道与重定向

写法 作用
cmd1 | cmd2 管道:前者输出给后者
> file / >> file 覆盖 / 追加写
2> file 重定向错误
> out 2>&1 正常+错误都进文件
&> out 同上(bash 简写)
2>/dev/null 丢弃错误

权限与用户

命令 作用
chmod 755 file 数字改权限
chmod u+x file 符号改权限
chown user:group file 改归属
chmod -R 644 dir/ 递归改
id / whoami / groups 查身份/组
useradd -m -s /bin/bash 用户 建用户
usermod -aG sudo 用户 加入 sudo 组
passwd 用户 改密码
su - 用户 / sudo -i 切换用户 / 切 root

权限数字:r=4 w=2 x=1 → 755=rwxr-xr-x,644=rw-r--r--,600=私密

进程管理

命令 作用
ps aux 所有进程快照
ps aux --sort=-%cpu | head CPU 占用排名
top / htop 实时监控
pgrep -a 名字 按名找 PID
kill PID / kill -9 PID 礼貌终止 / 强杀
pkill 名字 / killall 名字 按名杀
cmd & 后台运行
jobs / fg / bg 作业控制
nohup cmd & 退出终端仍运行

信号:15(TERM 礼貌) · 9(KILL 强杀) · 2(INT=Ctrl+C) · 1(HUP 重载)

网络

命令 作用
ip a 查 IP 和网卡
ip route 路由表
ping -c4 host 测连通
ss -tlnp 查 TCP 监听端口+进程
ss -tlnp | grep :80 查 80 端口被谁占
curl -I url 看响应头
curl -o file url 下载
dig domain DNS 查询
ssh user@host -p 端口 远程登录
scp file user@host:/path 远程复制(全量)
rsync -avz 源/ user@host:/目标/ 增量同步(备份首选)
rsync -avn 源/ 目标/ 演练,只看不改
tcpdump -i any port 80 -nn 抓包排查
certbot --nginx -d 域名 申请免费 HTTPS 证书

常见端口:22(SSH) 80(HTTP) 443(HTTPS) 53(DNS) 3306(MySQL) 6379(Redis)

软件包(apt)

命令 作用
apt update 刷新索引(装前必做)
apt upgrade 升级所有软件
apt install 包 安装
apt remove 包 / purge 包 卸载 / 连配置卸载
apt search 词 搜索
dpkg -i 包.deb 装本地 deb
dpkg -L 包 看包装了哪些文件

systemd 服务

命令 作用
systemctl status 服务 查状态(最常用)
systemctl start/stop/restart 服务 起/停/重启
systemctl reload 服务 平滑重载配置
systemctl enable --now 服务 开机自启+立即启动
systemctl is-active/is-enabled 服务 查运行/自启状态
systemctl list-units --type=service 列所有服务
journalctl -u 服务 -f 实时看服务日志
journalctl -u 服务 -n 50 看最近 50 行日志
systemctl daemon-reload 改 unit 后重载

存储

命令 作用
lsblk 树状看磁盘分区
df -h 各挂载点用量
du -sh dir/ 目录占用大小
du -sh /* | sort -rh | head 找最大目录
free -h 内存/swap
mount /dev/sdb1 /mnt 挂载
blkid 查 UUID
mount -a 测试 fstab
pvs/vgs/lvs 看 LVM 三层

安全与防火墙

命令 作用
ufw allow 22 放行端口(启用前必做)
ufw enable / ufw status verbose 启用 / 查规则
ufw default deny incoming 入站默认拒绝
last / lastb 登录成功 / 失败记录
who / w 当前在线用户
find / -perm -4000 找 SUID 文件

性能监控

命令 作用
top / htop 全局监控
uptime 看 load average
free -h 看 available 和 swap
iostat -x 1 磁盘 I/O(看 %util、await)
ss -s 连接统计
journalctl -p err -n 50 最近错误日志
dmesg | tail 内核消息(OOM 等)

Nginx(专题13)

命令 作用
nginx -t 检查配置语法(改完必做)
systemctl reload nginx 平滑重载(不断连接)
ln -s sites-available/x sites-enabled/ 启用站点
tail -f /var/log/nginx/error.log 看错误日志
curl -I localhost 本机测试访问

Docker(专题14)

命令 作用
docker pull/images/rmi 镜像 拉取/列出/删镜像
docker run -d -p 8080:80 --name x 镜像 后台运行+端口映射
docker ps -a 列容器(含已停止)
docker stop/start/rm 容器 停/起/删容器
docker logs -f 容器 看容器日志
docker exec -it 容器 bash 进容器内部
docker run -v 宿主:容器 ... 挂数据卷
docker build -t 名:版 . 按 Dockerfile 构建
docker compose up -d / down 编排多容器
docker stats 看资源占用

常用系统技能(专题15)

命令 作用
crontab -e / -l 编辑/查看定时任务
vim:i 编辑 · Esc · :wq / :q! 改配置必备
tar -czf x.tar.gz dir/ 打包压缩
tar -xzf x.tar.gz -C /tmp 解压到目录
export PATH="$PATH:/dir" 加 PATH
source ~/.bashrc 重载配置生效
alias ll='ls -alh' 设别名
tmux / tmux attach -t 名 终端复用:断线任务不挂
Ctrl+b d / Ctrl+b c tmux 脱离 / 新窗口
timedatectl set-timezone 区 改时区
logrotate -d /etc/logrotate.d/x 演练日志轮转

文本三剑客与正则(专题16)

命令 作用
grep -E "模式" file 扩展正则搜索(推荐加 -E)
grep -vE "^#|^$" 配置 排除注释和空行看有效配置
sed -i 's/旧/新/g' file 批量替换(改文件,慎用)
sed -n '5,10p' file 只看 5~10 行
awk '{print $1}' file 取第 1 列
awk -F: '{print $1}' f 指定冒号分隔取列
awk '{s+=$2} END{print s}' 某列求和
... | sort | uniq -c | sort -rn 统计 Top 排行

正则核心:. 任意字符 · * 0+次 · + 1+次 · ^$ 行首尾 · [0-9] 字符集 · {n} 重复 n 次

排查万能流程

有问题 → 看日志(journalctl / /var/log) → 看资源(top / free / df) → 看进程(ps / ss) → 看配置(/etc 下对应文件)

性能定位:top 看全局 → CPU高看 ps、wa高看 iostat、内存看 free → 定位进程

附录 B · 自测题与闪卡

先盖住答案自己答,再对照。重点考"为什么",不只是"是什么"。答案在每节末尾折叠区。

Part 1 · 基础(01-04)

Q1. Linux 和 GNU/Linux 有什么区别?

Q2. 为什么用户程序不能直接操作硬件?这样设计有什么好处?

Q3. cat /proc/cpuinfo 读的是磁盘上的真实文件吗?为什么?

Q4. Windows 有 C: D: 盘,Linux 为什么只有一个 /?这样设计好在哪?

Q5. 权限 rwxr-xr-- 用数字表示是多少?为什么用 4/2/1 而不是 1/2/3?

Q6. passwd 命令为什么需要 SUID?它解决了什么矛盾?

Q7. 目录只有 r 没有 x 权限,能进去吗?能看到什么?

  • A1. Linux 严格指内核;GNU/Linux 指内核+GNU 用户空间工具(bash/ls 等)的完整操作系统。
  • A2. 防止程序互相破坏内存、越权读数据、并发写坏磁盘。好处是安全隔离+稳定。靠 CPU 用户态/内核态两种模式实现,程序通过 syscall 请内核代劳。
  • A3. 不是。/proc 是虚拟文件系统,文件不占磁盘,是你读取的瞬间内核临时生成的实时数据。
  • A4. 单根树把物理设备藏在挂载点后面,路径稳定——换硬盘只要挂到同一目录,上层程序无感。
  • A5. 754。用 4/2/1(二进制位权)保证任意组合相加不冲突,4+1=5 只能是 r-x。
  • A6. /etc/shadow 是 600 root,只有 root 能写。SUID 让 passwd 运行时临时变身 root,只在这个受控程序里有改密码的权限,而不必把文件开放给所有人。
  • A7. 进不去。目录的 x 才是"进入权",没有 x 即使有 r 也只能看到文件名列表,无法访问内容或 cd 进去。

Part 2 · 进阶(05-08)

Q8. 脚本第一行 #!/bin/bash 叫什么?是给谁看的?

Q9. ./script.sh 里的 cd /tmp 为什么不影响当前终端?想让它影响该怎么运行?

Q10. 退出码为什么 0 表示成功、非 0 表示失败(和直觉相反)?

Q11. 为什么创建进程要 fork + exec 两步,而不是一步搞定?

Q12. kill 为什么叫 kill 却不一定是"杀"?kill -9 和 kill -15 有什么区别,该先用哪个?

Q13. 2>&1 里的 & 是干什么的?为什么 > out.txt 2>&1 顺序不能反?

Q14. apt update 和 apt upgrade 区别是什么?为什么装软件前要先 update?

  • A8. Shebang(sharp-bang)。给内核看的标记,告诉内核用哪个解释器运行此文件。
  • A9. ./ 会 fork 出子 shell 执行脚本,cd 改的是子进程的目录,子进程退出就没了。想影响当前终端用 source script.sh(不开子进程)。
  • A10. 成功只有一种,失败有千百种。用唯一的 0 表示成功,1~255 区分不同错误原因。这让 if、&&、|| 能精确判断。
  • A11. 两步中间留出空档,让你能调整子进程的环境(重定向、改环境变量)再 exec。重定向、管道机制都依赖这个设计。
  • A12. kill 实际是"发信号",信号可以是重载/暂停等,默认才是终止。-15(TERM) 是礼貌通知,进程可保存数据后退出;-9(KILL) 是内核直接拔插,拦不住但可能丢数据。先 15 后 9。
  • A13. & 表示"这是文件描述符不是文件名",&1 = fd 1 当前指向的地方。2>&1 复制 fd 1 此刻的指向(快照),所以必须先 > out.txt 把 1 接到文件,再 2>&1 让 2 跟上。
  • A14. update 只刷新软件源索引(有哪些版本),upgrade 才真正下载安装。不先 update 用的是旧目录,可能找不到包或装到旧版。

Part 3 · 实战(09-12)

Q15. systemd 的 enabled 和 active 有什么区别?部署服务时该用什么命令一步到位?

Q16. 改完 .service 文件后必须执行什么命令?为什么?

Q17. TCP 三次握手为什么是三次,不是两次或四次?

Q18. 启用 ufw 防火墙前,为什么务必先 ufw allow 22?

Q19. LVM 的 PV/VG/LV 三层分别是什么?它比传统分区强在哪?

Q20. 系统很卡但 top 显示 CPU 空闲(id 高),最该先看哪个指标?为什么?

Q21. free -h 显示 free 很小,是内存不够了吗?该看哪一列?

Q22. 磁盘满了,用什么命令逐层定位是哪个目录占的?

  • A15. enabled=开机是否自启,active=当前是否运行,两个独立。一步到位用 systemctl enable --now 服务。
  • A16. systemctl daemon-reload。systemd 缓存了 unit 配置,不重载就不知道你改了文件。
  • A17. 要让双方都确认"我能发能收、你也能发能收"。一次/两次都有一方无法确认对方的收发能力,三次恰好。
  • A18. default deny incoming 生效后默认拒绝所有入站,包括你正在用的 SSH,会瞬间把自己锁在门外。必须先放行 22 再启用。
  • A19. PV=物理卷(磁盘分区/水源),VG=卷组(汇成的存储池/水库),LV=逻辑卷(切出来当分区用/水管)。强在能在线扩容,加盘进 VG、扩 LV、resize 文件系统,不停机。
  • A20. 先看 wa(iowait)。它高说明 CPU 在干等磁盘 I/O,CPU 本身是空的,加 CPU 没用,该换快盘或优化 I/O。
  • A21. 不是。Linux 把空闲内存拿去做磁盘缓存(buff/cache),需要时会让出。看 available 列才是真正可用的。
  • A22. df -h 找满的分区 → du -sh /* | sort -rh | head 逐层往下钻定位大目录。

Part 4 · 专题(13-15)

Q23. 正向代理和反向代理有什么区别?Nginx 属于哪种?

Q24. Nginx 改完配置后为什么先 nginx -t 再 reload?reload 和 restart 有什么区别?

Q25. sites-available 和 sites-enabled 两个目录为什么要分开?启用站点本质做了什么?

Q26. 镜像和容器是什么关系?用编程概念怎么类比?

Q27. 容器为什么比虚拟机轻?它靠什么实现隔离?

Q28. docker run -p 8080:80 里冒号两边分别是什么?为什么需要它?

Q29. 容器删了数据就没了,怎么持久化?这本质是哪一章的什么机制?

Q30. Dockerfile 里 RUN 和 CMD 有什么区别?

Q31. cron 的五个时间字段从左到右是什么?*/5 * * * * 是什么意思?

Q32. vim 打开后猛敲字没反应,为什么?想退出退不出怎么办?

Q33. tar -czf 里的 c/z/f 各是什么?归档和压缩有何不同?

Q34. 硬链接和软链接有什么区别?删掉原文件后各自还能用吗?

Q35. ls -l 第 2 列那个数字是什么?它和 inode 有什么关系?

Q36. 磁盘 df -h 显示还有空间,创建文件却报 No space left,为什么?怎么查?

Q37. usermod -aG docker alice 里的 -a 不加会怎样?主组和附加组有什么区别?

Q38. grep、sed、awk 三者分工是什么?用一句口诀概括。

Q39. 系统从按下电源到能登录,经历哪几个阶段?systemd 在第几步出现?

Q40. 为什么需要 GRUB 这个引导程序,内核不能自己启动吗?

  • A23. 正向代理替"客户端"出门(藏客户端,如 VPN);反向代理替"服务器"接客(藏后端,统一入口)。Nginx 是反向代理。
  • A24. nginx -t 先做语法检查,避免配置写错直接 reload 导致整站挂掉(同 fstab 的 mount -a 思想)。reload 平滑重载,老连接不断;restart 先停后起会断所有连接。生产优先 reload。
  • A25. available 是"所有站点配置仓库",enabled 放软链接只链当前启用的。启用站点本质就是在 enabled 里建一个指向 available 的软链接(呼应 04 章 ln -s),停用删软链接即可,配置仍保留。
  • A26. 镜像是只读模板,容器是镜像跑起来的实例。类比 class(镜像)和 object(容器)——一个镜像能起任意多个容器。
  • A27. 容器共享宿主机内核,只打包应用和必要库(MB 级、秒级启动);VM 各装完整 OS(GB 级、分钟级)。容器靠内核的 namespace(隔离视图)+ cgroups(限额)实现,本质是被隔离的进程。
  • A28. 左边宿主机端口,右边容器内端口。容器是隔离的,外面访问不到内部端口,-p 在两者间打通道,访问宿主机:8080 转到容器:80(呼应 07 端口)。
  • A29. 用数据卷 -v 宿主机目录:容器目录,数据存宿主机,容器删了还在。本质就是 02 章的 mount 挂载思想。
  • A30. RUN 在构建镜像时执行(如装依赖,结果固化进镜像);CMD 在容器启动时执行(如启动应用)。
  • A31. 分、时、日、月、周(单位从小到大)。*/5 * * * * = 每 5 分钟执行一次。
  • A32. 打开默认在普通模式,按键被当命令,先按 i 进插入模式才能打字。退不出按 Esc 再输 :q!(丢弃)或 :wq(保存)。
  • A33. c=create 打包、z=用 gzip 压缩、f=指定文件名。归档是把多文件打成一个包(不减体积,tar 的本职),压缩才减体积(gzip 干的),.tar.gz 是先打包再压缩。
  • A34. 硬链接是同一 inode 的另一个名字,删原文件仍可用(引用计数减1不归零);软链接存的是目标路径,删原文件就成死链接、失效。软链接能跨分区、能链目录,硬链接不行。
  • A35. 是硬链接数——有多少个文件名指向同一个 inode。每 ln 一次加1,删一个名字减1,减到 0 内核才真正回收磁盘数据(所以删文件本质是 unlink)。
  • A36. inode 用尽了。每个文件占一个 inode,海量小文件会先把 inode 耗光,而数据块还空着。用 df -i 查 inode 使用率(df -h 查的是空间,两者要分开看)。
  • A37. 不加 -a 会用 docker 覆盖掉所有附加组(可能把 sudo 弄丢,失去管理员权限)。主组只有一个、决定新建文件的所属组;附加组可多个、用来获得额外权限。加组永远用 -aG。
  • A38. grep 挑行(过滤匹配的行)、sed 改字(替换/删除/插入)、awk 取列(按字段处理、计算、报表)。口诀:grep 挑行、sed 改字、awk 取列。
  • A39. 五步:① BIOS/UEFI 自检 → ② GRUB 加载内核 → ③ 内核挂载根文件系统 → ④ systemd(PID 1) 拉起所有服务 → ⑤ 登录。systemd 在第 ④ 步出现,是所有进程的祖先。
  • A40. 鸡生蛋问题:内核是磁盘上的文件,而"读磁盘文件"本是内核的活,内核没启动谁来读它?所以需要一个极小、不依赖内核、能直接和 BIOS/磁盘打交道的程序先把内核搬进内存,这就是 GRUB。

快速问答闪卡(高频考点)

碎片时间快速过,看问题立刻在心里答。

问 答
权限 755 是什么? rwxr-xr-x
r/w/x 数字? 4/2/1
强杀进程信号? 9 (SIGKILL)
礼貌终止信号? 15 (SIGTERM)
查端口占用? ss -tlnp
查本机 IP? ip a
实时看日志? tail -f 或 journalctl -u 服务 -f
开机自启+启动? systemctl enable --now
装软件前先做? apt update
递归建目录? mkdir -p
丢弃错误输出? 2>/dev/null
内存真正可用看哪列? available
系统卡先看哪个 CPU 指标? wa (iowait)
找最大目录? du -sh /* | sort -rh
SSH 默认端口? 22
HTTP/HTTPS 端口? 80 / 443
Nginx 改配置先做? nginx -t 检查语法
启用 Nginx 站点? 在 sites-enabled 建软链接
镜像 vs 容器? 类 vs 对象(模板 vs 实例)
Docker 端口映射? -p 宿主:容器
容器数据持久化? -v 挂数据卷
进容器内部? docker exec -it 容器 bash
cron 五字段顺序? 分 时 日 月 周
vim 退出不保存? Esc → :q!
打包压缩目录? tar -czf x.tar.gz dir/
硬链接 vs 软链接? 同 inode 别名 vs 存路径的独立文件
删原文件软链接会怎样? 失效(死链接)
空间没满却建不了文件? inode 耗尽,df -i 查
加附加组命令? usermod -aG 组 用户(必带 a)
三剑客口诀? grep 挑行 · sed 改字 · awk 取列
取文件第1列? awk '{print $1}'
文本批量替换? sed -i 's/旧/新/g'
改时区? timedatectl set-timezone 区
开机五阶段? BIOS→GRUB→内核→systemd→登录
查谁拖慢开机? systemd-analyze blame
增量同步文件? rsync -avz 源/ 目标/
rsync 比 scp 强在? 只传变化部分(差量)
网络抓包工具? tcpdump -i any port X
日志自动轮转? logrotate(防撑爆磁盘)
SSH 断了任务不挂? tmux(会话留服务端)
tmux 脱离会话? Ctrl+b 然后 d
免费 HTTPS 证书? certbot / Let's Encrypt
grep 正则 + 不灵? 加 -E 用扩展正则
正则行首/行尾? ^ / $

实操检验题(去终端真做)

完成下面任务,每个都用到多个知识点:

  1. 找出 /etc 下最大的 5 个文件(提示:ls -lS / find + sort)
  2. 统计当前有多少个 bash 进程(提示:ps + grep + wc)
  3. 查看 SSH 服务状态和最近 10 条日志(提示:systemctl + journalctl)
  4. 找出占用 22 端口的进程(提示:ss)
  5. 写个脚本:接收一个目录参数,打印它的大小,不存在则报错退出
  6. 看你系统的负载、内存可用量、根分区使用率(提示:uptime / free / df)
  7. 把 /etc 打包压缩成带时间戳的 tar.gz(提示:tar + date)
  8. 设一个定时任务:每天凌晨 2 点跑某脚本(提示:crontab -e)
# 1
ls -lSh /etc | head -6
find /etc -type f -printf "%s %p\n" 2>/dev/null | sort -rn | head -5

# 2
ps aux | grep -c "[b]ash"

# 3
systemctl status ssh
journalctl -u ssh -n 10

# 4
ss -tlnp | grep :22

# 5
cat > check.sh << 'EOF'
#!/bin/bash
d="$1"
[ -z "$d" ] && { echo "用法: $0 <目录>"; exit 1; }
[ ! -d "$d" ] && { echo "错误: $d 不是目录"; exit 1; }
du -sh "$d"
EOF
bash check.sh /etc

# 6
uptime && free -h && df -h /

# 7
sudo tar -czf "etc_$(date +%Y%m%d).tar.gz" /etc

# 8
crontab -e
# 在编辑器里加一行:
# 0 2 * * * /path/to/script.sh

附录 C · 实战项目:部署一个带监控的 Web 服务

一个项目把前面知识串起来。从装软件到上线、加监控、做安全加固,全流程实操。每步标注用到哪节知识,完成后你就具备了基本运维能力。

项目目标

部署 nginx 网站 → 写监控脚本 → 配 systemd 定时任务 → 防火墙加固

涉及知识点:包管理(08) · 服务(09) · 脚本(05) · 进程(06) · 权限(04) · 网络(07) · 安全(11) · 存储(10)

阶段 1 · 安装并启动 nginx(08 + 09)

# 更新索引并安装(08 包管理)
sudo apt update
sudo apt install -y nginx

# 启动并设为开机自启(09 systemd)
sudo systemctl enable --now nginx

# 验证状态(绿点 = running)
systemctl status nginx

检查点:

# 看 nginx 监听了哪个端口(07 网络)
ss -tlnp | grep nginx        # 应看到 0.0.0.0:80

# 本机测试访问(07 curl)
curl -I http://localhost     # 应返回 200 OK
TIP

📌 串联:enable --now 同时设开机自启和立即启动(09);ss -tlnp 查监听端口(07)。

阶段 2 · 部署自定义网页(02 + 04)

# nginx 默认网站根目录
ls -l /var/www/html          # 看默认文件和权限(04)

# 写一个自己的首页
echo "<h1>Hello from $(hostname)</h1>" | sudo tee /var/www/html/index.html

# 验证
curl http://localhost
TIP

📌 串联:/var/www 属于可变数据目录(02);用 ls -l 看文件归属和权限(04)。

阶段 3 · 写一个监控脚本(05 + 06 + 10 + 12)

创建 ~/monitor.sh,监控 nginx 是否存活、记录系统资源:

cat > ~/monitor.sh << 'EOF'
#!/bin/bash
# nginx + 系统资源监控脚本

LOG="/var/log/mymonitor.log"
TIME=$(date "+%Y-%m-%d %H:%M:%S")

# 检查 nginx 进程是否在(06 进程)
if pgrep nginx > /dev/null; then
    status="OK"
else
    status="DOWN"
    # 挂了就尝试拉起(09 服务)
    systemctl start nginx
fi

# 采集资源(12 性能 + 10 存储)
load=$(uptime | awk -F'load average:' '{print $2}')
mem=$(free -h | awk '/Mem:/ {print $7}')      # available 列
disk=$(df -h / | awk 'NR==2 {print $5}')      # 根分区使用率

# 写日志(03 重定向)
echo "$TIME | nginx=$status | load=$load | mem_avail=$mem | disk=$disk" >> "$LOG"
EOF

chmod +x ~/monitor.sh        # 加执行权限(04)

# 手动跑一次测试
sudo ~/monitor.sh
sudo cat /var/log/mymonitor.log

逐处知识点:

  • pgrep nginx(06 按名查进程)
  • systemctl start(09 拉起服务)
  • free -h 取 available、df -h 取使用率(12 + 10)
  • >> 追加写日志(03 重定向)
  • chmod +x(04 权限)

阶段 4 · 配置定时执行(09 systemd timer)

用 systemd timer 每分钟跑一次监控(替代传统 cron,对比见 15 章)。

创建 service 单元:

sudo tee /etc/systemd/system/mymonitor.service << 'EOF'
[Unit]
Description=My Nginx Monitor

[Service]
Type=oneshot
ExecStart=/home/hisos/monitor.sh
EOF

创建 timer 单元:

sudo tee /etc/systemd/system/mymonitor.timer << 'EOF'
[Unit]
Description=Run monitor every minute

[Timer]
OnUnitActiveSec=1min
OnBootSec=1min

[Install]
WantedBy=timers.target
EOF

启用:

sudo systemctl daemon-reload              # 改 unit 必须重载(09)
sudo systemctl enable --now mymonitor.timer

# 验证
systemctl list-timers | grep mymonitor    # 看下次触发时间
sudo tail -f /var/log/mymonitor.log       # 实时看监控日志累积
TIP

📌 串联:daemon-reload 让 systemd 识别新 unit(09);tail -f 实时追踪日志(03)。

阶段 5 · 安全加固(11 防火墙)

# 先放行 SSH,否则会断连(11 血泪教训)
sudo ufw allow 22

# 放行 HTTP/HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# 入站默认拒绝,启用防火墙
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable

# 验证规则
sudo ufw status verbose

检查点:确认 22/80/443 放行,其他端口拒绝。

TIP

📌 串联:先 allow 22 再 enable,避免把自己锁门外(11)。

阶段 6 · 模拟故障 + 排查(综合)

模拟 nginx 挂掉,看监控能否自愈:

# 手动杀掉 nginx(06 信号)
sudo pkill nginx
ss -tlnp | grep :80          # 确认 80 端口没了

# 等一分钟,timer 触发监控脚本自动拉起
sleep 65
ss -tlnp | grep :80          # 应该又起来了
sudo grep DOWN /var/log/mymonitor.log   # 看到 DOWN 记录

排查练习:

# 如果 nginx 起不来,按排查流程走
systemctl status nginx              # 看状态概况(09)
journalctl -u nginx -n 30           # 看详细报错(09)
sudo nginx -t                       # 检查配置语法(13)
ss -tlnp | grep :80                 # 端口是否被别的进程占(07)

验收清单

  • curl http://localhost 返回你的自定义页面
  • systemctl is-enabled nginx 显示 enabled
  • /var/log/mymonitor.log 每分钟新增一行
  • 杀掉 nginx 后一分钟内自动恢复
  • ufw status 只放行 22/80/443

清理(练习完想还原)

sudo systemctl disable --now mymonitor.timer nginx
sudo rm /etc/systemd/system/mymonitor.{service,timer}
sudo systemctl daemon-reload
sudo apt remove --purge -y nginx
sudo rm -f ~/monitor.sh /var/log/mymonitor.log
sudo ufw disable

这个项目你学到了

把孤立的命令变成了一条完整的运维链路:

  1. 装服务 → 起服务 → 设自启(08+09)
  2. 写脚本自动化监控(05+06+12)
  3. 定时任务保活(09 timer)
  4. 防火墙收口(11)
  5. 故障排查闭环(综合)

真实的服务器运维,核心就是这套流程的放大版。

本页目录