全部文章 笔记与思考

Linux 实用教程:从零基础到日常使用与服务管理

从 Ubuntu 环境搭建开始,系统介绍终端操作、文件权限、日志分析、开发环境、进程、网络、SSH、systemd 与 Shell 自动化,并通过完整示例讲解服务管理、备份恢复和常见故障排查。

Linux 广泛用于服务器、开发环境和各种计算设备。日常使用 Linux,最常接触的是终端、文件、权限、进程和网络:通过命令管理文件与软件,运行程序,查看日志,并在出现问题时逐步找到原因。

这篇教程从零基础开始,将常用概念和实际操作结合起来。主线使用 Ubuntu 24.04 LTS、Bash 和一个小型 HTTP 服务,逐步介绍文件操作、权限管理、日志分析、开发环境、进程与网络,再进入服务管理和 Shell 自动化。示例代码均包含在正文中。

不需要先学会 Python,也不需要购买云服务器。 Windows 用户可以用 WSL 2;已有较新的 Ubuntu 环境也可以使用,软件版本和个别输出可能不同。

1. 阅读路线与示例约定

初次接触 Linux 时,建议按顺序阅读并运行示例;已经掌握基础操作后,可以按需要回查对应章节。各部分的关系如下:

内容 主要操作 对应章节
环境与文件 安装环境,创建、编辑、查找和管理文件 第 2~3 节
权限与日志 理解访问权限,组合命令分析文本与日志 第 4~5 节
开发与进程 配置依赖,运行程序,查看进程和系统资源 第 6~7 节
网络与 SSH 检查连接,远程登录,传输文件 第 8 节
服务与自动化 使用 systemd 管理服务,编写脚本,备份和恢复文件 第 9~10 节
故障排查 根据日志、权限、配置和端口信息定位问题 第 11 节

Linux 学习路线:从环境搭建到服务管理与故障排查

全篇使用以下约定,先看清再复制:

标记 在哪里执行
powershell 代码块 Windows PowerShell;需要管理员时会单独说明
bash 代码块 Ubuntu 的终端,使用普通用户;需要提权的命令会写 sudo
text、ini、python 输出示例或文件内容,不能整段当成 Shell 命令运行
“本机”“服务器” SSH 客户端所在机器与 SSH 连接后的目标机器;即使同一台电脑也要分清

代码块没有 $ 提示符,便于复制。# 开头的是注释。遇到 YOUR_SERVER_IP 等占位符,必须替换;不要把尖括号形式的占位符直接放进 Shell,因为 <、> 有特殊含义。

全部文件练习集中在 ~/linux-lab,备份放在 ~/linux-lab-backups。服务管理部分会明确创建 /opt/linux-lab 和对应的 systemd 配置,适合在自己的练习机执行。

2. 准备一个真正可用的 Linux 环境

先区分三个概念。 Linux 内核负责进程、内存、设备等底层工作;Ubuntu 这样的发行版把内核、软件包和系统工具组织起来;Bash 是解释你输入的命令的 Shell。终端只是承载输入输出的窗口。打开一个黑色窗口不代表已经进入 Linux。

Windows 主线:在管理员 PowerShell 中先查看可安装的发行版,再安装指定版本:

1
2
wsl --list --online
wsl --install -d Ubuntu-24.04

以列表中的实际名称为准。按提示重启,再打开 Ubuntu,创建 Linux 用户和密码。密码输入时通常没有星号或其他回显,这是正常现象。该密码与 Windows 登录密码不是同一个概念。

如果已经安装 Ubuntu,直接使用现有环境。检查并进入:

1
2
wsl --list --verbose
wsl -d Ubuntu-24.04

已有环境名称可能只是 Ubuntu,此时最后一条改成 wsl -d Ubuntu。列表中 VERSION 应为 2;确实需要转换旧环境时,才使用 wsl --set-version Ubuntu 2,名称仍按实际替换。安装要求和异常处理见 Microsoft WSL 安装文档。

macOS 用户可准备一台 Ubuntu 虚拟机,Apple Silicon 选择 ARM64 镜像。已有 Linux 电脑或 Ubuntu 虚拟机的读者可以直接继续。macOS 自带终端使用的系统工具与 GNU/Linux 有差异,不能把它当成本教程的 Linux 环境。

进入 Ubuntu 后执行:

1
2
3
4
5
whoami
pwd
cat /etc/os-release
uname -r
ps -p 1 -o comm=

分别确认当前用户、目录、发行版、内核版本和 1 号进程。服务章节要求最后一条输出 systemd。

若 WSL 中没有启用 systemd,先在 PowerShell 用 wsl --version 检查 WSL 版本;必要时执行 wsl --update。然后在 Ubuntu 执行 sudo nano /etc/wsl.conf,保留已有配置,在已有 [boot] 节中设置,或新增该节:

1
2
[boot]
systemd=true

保存工作后,在 PowerShell 执行 wsl --shutdown,它会停止所有运行中的 WSL 发行版;重新进入 Ubuntu 再检查 PID 1。systemd 支持要求 WSL 0.67.6 或更高版本,具体见 Microsoft systemd 文档。WSL 退出、Windows 关机或休眠之后,不能把其中的练习服务当成持续在线的服务器。

准备常用工具:

1
2
3
4
5
6
sudo apt update
sudo apt install -y man-db nano vim-tiny curl wget git unzip zip \
  tree file less procps iproute2 iputils-ping dnsutils lsof \
  openssh-client rsync tmux python3 python3-venv shellcheck
mkdir -p ~/linux-lab/{notes,data,logs,app,scripts}
cd ~/linux-lab

apt update 更新软件包索引,不是升级所有软件。apt install 安装软件;-y 表示接受安装确认。换行前的 \ 表示命令未结束,后面不要再加空格。这里只用系统仓库,无需把网上的安装脚本直接交给 sudo bash。

WSL 中,/mnt/c、/mnt/d 对应 Windows 磁盘;本教程使用 ~ 下的 Linux 文件系统,避免 Windows 挂载路径带来的权限语义和性能差异。在 Ubuntu 中运行 explorer.exe . 可以用 Windows 文件管理器查看当前目录。使用 VS Code 时可以安装 WSL 扩展后从 Ubuntu 运行 code .,仍要确认编辑器连接的是 WSL 环境。Microsoft 文件系统说明

动手练习: 关闭终端后重新打开,能进入 Ubuntu,说明当前用户和发行版,并独立回到 ~/linux-lab。

3. 文件、路径、编辑器:日常工作的基本盘

Linux 文件名区分大小写,App.py 和 app.py 是两个名字。路径用 /。/ 是文件系统根目录,~ 是当前用户的家目录,. 是当前目录,.. 是上一级;/root 则是 root 用户的家目录。

目录 通常用来做什么 常见内容
/home/用户名 普通用户文件 代码、实验数据、个人配置
/etc 系统配置 SSH、服务和其他软件的配置
/var/log 日志 查系统或应用运行记录
/tmp 临时文件 临时测试;不能依赖其长期保存
/usr/bin 系统安装的可执行程序 查 python3、git 实际来自哪里
/opt 额外部署的软件 本教程的服务文件
/proc 内核提供的进程和系统信息 理解它不是普通磁盘目录即可

先亲手完成一轮文件操作:

1
2
3
4
5
6
7
8
9
cd ~/linux-lab
pwd
ls -lah
printf 'first Linux note\n' > notes/day1.txt
cp -i notes/day1.txt notes/day1.copy.txt
mv -i notes/day1.copy.txt notes/archive.txt
mkdir -p 'data/raw files'
cp -i notes/day1.txt 'data/raw files/sample.txt'
tree -a -L 3 .

-l 显示详细信息,-a 包含点开头的隐藏文件,-h 用易读的大小单位。cp 复制,mv 移动或改名;这里的 -i 会在覆盖已有文件前询问。操作含空格的路径时加引号。

再练习这些导航命令:

1
2
3
4
5
6
cd notes
pwd
cd ..
cd /etc
cd -
cd ~

cd - 回到上一个工作目录,cd ~ 回家目录。相对路径从当前目录解释,绝对路径从 / 开始。 如果命令提示文件不存在,先执行 pwd,往往比反复改命令更有效。

阅读和编辑。 小文件用 cat;大文件用 less,按 /关键词 搜索、n 下一个、q 退出。末尾几十行用 tail -n 50 文件名;持续观察新日志用 tail -f 文件名,按 Ctrl+C 退出。日志会轮转改名时,可以用 tail -F 继续追踪文件名。

1
nano ~/linux-lab/notes/day1.txt

Nano 中直接输入,Ctrl+O、回车保存,Ctrl+X 退出。至少再掌握 Vim 的“应急生存操作”:vim 文件名 打开,i 进入插入模式,Esc 回普通模式,:wq 保存退出,:q! 放弃修改退出,/关键词 搜索。看到空白编辑器先确认自己是否在正确模式。

查找文件和查找内容是两件事。

1
2
3
find ~/linux-lab -type f -name '*.txt'
grep -RIn --include='*.txt' 'Linux' ~/linux-lab/notes
file ~/linux-lab/notes/day1.txt

find 按文件属性查找;grep 查文本内容,-R 递归、-I 跳过二进制文件、-n 显示行号。'*.txt' 加引号,是为了让 find 解释通配模式,而不是被当前 Shell 提前展开。

再认识一下符号链接,它保存的是指向另一个位置的路径:

1
2
3
4
ln -s notes/day1.txt ~/linux-lab/latest-note
ls -l ~/linux-lab/latest-note
readlink ~/linux-lab/latest-note
cat ~/linux-lab/latest-note

这里的相对目标 notes/day1.txt 从链接所在目录 ~/linux-lab 解释。目标移动或删除后,链接可能失效;删除链接本身不会自动删除目标。查看程序路径时遇到链接,用 readlink -f 路径 可继续解析实际目标。

删除要先看清对象。 下面只删除刚创建的副本:

1
2
3
4
ls -l ~/linux-lab/notes/archive.txt
rm -i ~/linux-lab/notes/archive.txt
mkdir ~/linux-lab/empty-demo
rmdir ~/linux-lab/empty-demo

rm 通常不经过回收站,rmdir 只删除空目录。递归操作前检查 pwd 和目标路径;学到 rm -rf 并不意味着日常删除都应该带它。-- 可以结束选项解析,例如 rm -i -- ./-demo.txt 能正确处理以 - 开头的文件名。

不会记参数时如何自救。

1
2
3
4
ls --help
man chmod
type cd
command -v python3

man 里用 /关键词 搜索、q 退出。cd 是 Shell 内建命令,而 python3 通常是外部程序;不必把每个命令都背下来,但要能找到它的参数含义。终端中多用 Tab 补全、上下键找历史、Ctrl+R 搜索历史;Ctrl+L 清屏。意外按下 Ctrl+S 导致显示停住时,可用 Ctrl+Q 恢复。

动手练习: 创建一个含空格的目录,在其中编辑文件,用内容搜索找到它,复制到另一目录,再只删除那个副本。

4. 用户与权限:解决 Permission denied

执行 id 看 UID、主组和附加组,再观察:

1
ls -l ~/linux-lab/notes/day1.txt

输出可能是:

1
-rw-r--r-- 1 alice alice 17 Sep 14 10:00 day1.txt

第一位表示类型:- 是普通文件,d 是目录,l 是符号链接。后面三组分别给所有者、所属组、其他用户。alice alice 分别是所有者和组名。

权限 对普通文件 对目录
r,值 4 读内容 列出目录中的名字
w,值 2 修改内容 在目录内增删、改名条目,通常还需要 x
x,值 1 允许直接执行 穿过这个目录,访问其中的路径

Linux 文件路径与权限模型:路径每一级、rwx 分组和常见修复命令

所以 644 是所有者读写、组和其他人只读;755 是所有者读写执行、其他人读执行;600 是只有所有者读写。数字是每一组内部相加,不是一个需要死记的编号。

1
2
3
chmod 600 ~/linux-lab/notes/day1.txt
ls -l ~/linux-lab/notes/day1.txt
chmod u=rw,go=r ~/linux-lab/notes/day1.txt

目录上的 x 非常关键:即使文件是 644,如果别人不能经过它的父目录,仍然读不到。删除文件主要取决于父目录权限,还会受 sticky bit、ACL 等限制,不能只看文件有没有 w。

sudo 是按配置以其他身份执行一个命令,常见目标是 root。安装系统软件需要它,写自己的代码通常不需要。用 sudo 启动编辑器或构建工具,可能在项目内留下 root 所有的文件。

练习修复一个明确的错误所有者:

1
2
3
4
sudo touch ~/linux-lab/notes/root-owned.txt
ls -l ~/linux-lab/notes/root-owned.txt
sudo chown "$(id -un):$(id -gn)" ~/linux-lab/notes/root-owned.txt
printf 'ownership fixed\n' >> ~/linux-lab/notes/root-owned.txt

$(...) 把命令输出嵌入参数;这里得到当前用户和主组。只改指定文件,不把整个 /usr、/etc 或共享项目递归改成自己所有。

另一类常见坑是 sudo echo 内容 > /etc/某文件:> 由当前 Shell 处理,echo 提权不能让外层 Shell 获得写权限。需要写系统配置时可用 sudoedit 文件,或者明确使用 printf '内容\n' | sudo tee 文件。后者默认覆盖,所以仍要先核对目标。

排错顺序: id 看你是谁 → ls -ld 目标及父目录 看权限 → namei -l 完整路径 看路径的每一级 → 确认文件是否应由你修改。不要直接 chmod -R 777;它会让更多人获得写入和执行权限,也掩盖真正的所有者问题。

动手练习: 不看答案解释 -rwxr-x---;说明“文件可读但无法访问”的一种原因,并修复上面那个 root 所有文件。

5. Shell、管道和日志:把命令组合成工作流

Shell 在程序执行前处理变量、引号、通配符、管道和重定向。很多“命令奇怪地失效”,实际是参数在进入程序前就变了。

1
2
3
4
lab_dir="$HOME/linux-lab"
printf '%s\n' "$lab_dir"
printf '%s\n' '$lab_dir'
ls "$lab_dir/notes"

第一条 printf 输出路径,第二条输出字面量 $lab_dir。赋值时 = 两边没有空格。双引号允许变量展开,同时避免路径中的空格被拆开;单引号保留字面文本。注意 "~/linux-lab" 中的 ~ 不会展开,改用 "$HOME/linux-lab"。规则可查 Bash 引号文档。

输入、输出与错误。 一个普通命令有标准输入 0、标准输出 1 和标准错误 2:

写法 效果
command > out.log 标准输出写入文件,覆盖旧内容
command >> out.log 标准输出追加到文件
command 2> err.log 标准错误单独写入文件
command > all.log 2>&1 先重定向标准输出,再让标准错误跟随它
a | b 把 a 的标准输出交给 b 的标准输入
a | tee out.log 输出既显示在屏幕上,也写入文件

> all.log 2>&1 与 2>&1 > all.log 不同,重定向从左到右处理。普通 | 不自动传递标准错误。不要对同一个输入文件直接写 grep ... file > file,Shell 会先清空文件。详见 Bash 重定向文档。

准备一份能得到确定答案的日志。 下方 cat <<'EOF' 是 here-document:将两行标记之间的内容写入文件。结束的 EOF 独占一行;带引号的开始标记会避免内容中的变量被展开。

1
2
3
4
5
6
7
8
cat > ~/linux-lab/logs/access.log <<'EOF'
2026-09-14 10:00:01 level=INFO method=GET path=/health status=200 duration_ms=2
2026-09-14 10:00:02 level=INFO method=GET path=/api/orders status=500 duration_ms=120
2026-09-14 10:00:03 level=INFO method=GET path=/api/users status=500 duration_ms=80
2026-09-14 10:00:04 level=INFO method=GET path=/missing status=404 duration_ms=3
2026-09-14 10:00:05 level=INFO method=GET path=/api/orders status=500 duration_ms=150
2026-09-14 10:00:06 level=INFO method=GET path=/ status=200 duration_ms=4
EOF

先逐步观察,不要直接背一条很长的管道:

1
2
3
4
5
6
cd ~/linux-lab
wc -l logs/access.log
grep -n 'status=500' logs/access.log
grep -c 'status=500' logs/access.log
awk '$6 == "status=500" {print $5}' logs/access.log
awk '$6 == "status=500" {print $5}' logs/access.log | sort | uniq -c | sort -nr

结果应是总共 6 行、3 次 500,最后一条得到:

1
2
      2 path=/api/orders
      1 path=/api/users

awk 默认按空白分字段,$5 是第五列,$6 是第六列。sort 先让相同接口相邻;uniq -c 只对相邻相同行计数;最后的 sort -nr 按数字倒序排列。这套写法依赖这里约定的日志格式,不适合直接套在任意 JSON 或带空格的字段上。

再做两个高频操作:

1
2
3
grep -n -C 1 'path=/api/users' logs/access.log
sed -n '2,4p' logs/access.log
sed 's/level=INFO/level=DEMO/g' logs/access.log > logs/access.demo.log

grep -C 1 显示匹配行的上下文;sed -n '2,4p' 查看第 2~4 行;替换操作写入新文件,便于比较。对于真实服务,关注时间、请求 ID、错误类型和上下文,不要只数 ERROR 一词。

退出码决定自动化是否可靠。

1
2
3
grep -q 'status=503' logs/access.log
echo $?
mkdir -p notes/check && printf 'directory ready\n'

$? 是上一条命令的退出码,必须紧接着读取。通常 0 表示成功,非零表示其他情况;grep 的 1 是没有匹配,2 是错误,不能把它们混为一谈。a && b 在 a 成功时执行 b,a || b 在 a 失败时执行 b。

默认情况下,管道的退出码取最后一个命令;脚本中的 set -o pipefail 能让前段失败更容易暴露,但不等于整段逻辑从此没有漏洞。详见 Bash 管道文档。

动手练习: 从示例日志统计 404 次数,把全部 500 行保存到新文件,同时显示在终端;再解释为什么 uniq -c 前需要排序。

6. 开发环境:让程序在正确的地方运行

在新机器上接到项目,第一步是读项目的 README 和版本约束,再确认系统与工具。不要因为某个程序缺失,就在不同安装教程之间随机复制命令。

1
2
3
4
5
6
command -v git python3
git --version
python3 --version
apt show python3-venv
dpkg -l python3-venv
printf '%s\n' "$PATH"

PATH 是 Shell 查找命令的目录列表,用 : 分隔。输入 python3 时会从中寻找程序;./script.sh 则明确指当前目录的脚本。当前目录一般不在 PATH 中,所以“文件在眼前”不等于直接输入名字就能运行。

Ubuntu/Debian 常用 apt,Fedora/RHEL 系常用 dnf。包管理器负责系统级依赖,pip、npm 等负责对应语言的依赖。先理解这一层区别,再查自己发行版和项目的具体命令。

用 Python 虚拟环境练习依赖隔离。 本文应用没有第三方依赖,但仍创建一个环境,让启动方式与实际项目相似:

1
2
3
4
5
6
7
cd ~/linux-lab/app
python3 -m venv .venv
source .venv/bin/activate
command -v python
python -m pip --version
python -c 'import sys; print(sys.executable)'
deactivate

激活之后,解释器路径应位于 ~/linux-lab/app/.venv/bin/。真实项目如果提供了 requirements.txt,在激活环境后运行 python -m pip install -r requirements.txt;本实验不需要执行这一步。不要用 sudo pip install 改动系统 Python,也不要为了绕过系统环境保护直接添加 --break-system-packages。

source 文件 在当前 Shell 内执行其内容,因此能改变当前环境;bash 文件 启动子 Shell,子 Shell 的变量不会自动回写父 Shell。只 source 自己写的或已检查可信的文件。

环境变量也有作用范围:

1
2
3
4
5
export APP_MESSAGE='Hello from my shell'
python3 -c 'import os; print(os.getenv("APP_MESSAGE"))'
APP_MESSAGE='Only this command' python3 -c 'import os; print(os.getenv("APP_MESSAGE"))'
printf '%s\n' "$APP_MESSAGE"
unset APP_MESSAGE

export 将变量传给以后启动的子进程;命令前的一次性赋值只影响那一次执行。新终端不自动继承另一个终端的临时设置,已经运行的服务也不会跟着变。.bashrc 通常用于交互式 Bash 的个人设置,systemd 服务不会靠读取你的 .bashrc 获得配置。

Git 在 Linux 中的最低工作能力。 先在独立练习目录中做一次本地提交,不涉及推送:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mkdir -p ~/linux-lab/git-demo
cd ~/linux-lab/git-demo
git init
git config user.name 'Linux Learner'
git config user.email 'learner@example.com'
printf '# Linux practice\n' > README.md
printf '.venv/\n.env\n__pycache__/\n*.log\n' > .gitignore
git status
git add README.md .gitignore
git diff --cached
git commit -m 'docs: add Linux practice notes'
git log --oneline -3

这里的 Git 身份仅配置当前练习仓库,不更改其他项目。真正拿到团队代码后,常见起点是 git clone 团队给的地址、进入目录、读 README、切工作分支、装依赖和运行测试。先检查 git status、git diff,再做修改;不要用 git reset --hard 清掉看不懂的改动。私钥、Token 和真实 .env 不应提交到仓库。

动手练习: 能证明程序使用的是虚拟环境的 Python,解释 export 与一次性环境变量的区别,完成一次看过 diff 的本地提交。

7. 进程与资源:知道谁在运行、为什么变慢

进程是运行中的程序,有 PID、用户、状态、工作目录和环境。一个程序可以创建多个进程,一个进程也可以有多个线程。

先用无副作用的 sleep 练习后台任务:

1
2
3
4
5
sleep 300 &
sleep_pid=$!
jobs -l
ps -p "$sleep_pid" -o pid,ppid,user,stat,etime,args
kill -TERM "$sleep_pid"

& 把命令放到后台,$! 是最近一次后台任务的 PID,jobs 只列当前 Shell 管理的作业。ps 查看系统进程。正常停止优先使用程序提供的退出方式或 TERM;kill -9 对应 KILL,进程无法捕获它做清理,只在确认对象且正常方式无效时使用。

再启动一次 sleep 300,按 Ctrl+Z 暂停,执行 jobs -l 查看停止状态,用 bg 在后台恢复、fg 回前台,最后 Ctrl+C 结束。暂停不等于退出,进程仍存在。

需求 合适工具 要理解的边界
当前终端中暂时并行运行 &、jobs、fg、bg 后台任务不等于可靠的常驻服务
SSH 断开后继续交互工作 tmux 可以重新接回会话,但不能承受机器关机
忽略挂断信号的临时任务 nohup command > run.log 2>&1 & 不负责自动重启、健康检查或开机启动
长期运行应用 systemd 管理生命周期、日志和重启策略

tmux 的最小用法:执行 tmux new -s linux-lab,在里面工作;按 Ctrl+B,松开后按 D,脱离会话;用 tmux ls 查看,再 tmux attach -t linux-lab 接回。需要结束时,在里面停止任务并输入 exit。

观察系统,不要只盯一个数字。

1
2
3
uptime
nproc
top

top 中按 P 按 CPU 排序,M 按内存排序,q 退出。uptime 的 load average 是 1、5、15 分钟平均负载,涉及可运行任务和不可中断睡眠任务,不是 CPU 百分比;要结合 CPU 数量和 I/O 判断。

1
2
3
4
5
free -h
df -h
df -i
du -h --max-depth=1 ~/linux-lab
ps aux --sort=-%mem | head -n 8
现象 优先看什么 如何解释
内存“剩余很少” free -h 的 available Linux 会利用内存做缓存,不能只看 free
磁盘写不进去 df -h、df -i 可能是空间满,也可能是 inode 耗尽
项目目录突然很大 du,逐级缩小范围 找到具体日志、缓存或数据目录再处理
程序突然被杀 journalctl -k -b,必要时 sudo 查看是否存在 OOM 等证据,不能看到 Killed 就断言 OOM
删除日志后空间未释放 sudo lsof +L1 文件可能仍被进程打开;确认后通过服务正常轮转或重启释放

df 看文件系统层面的使用量,du 统计路径下的文件占用,两者不保证相等。lsof 能查看进程打开的文件;在 Linux 中,网络 socket 也属于它能观察的对象。

动手练习: 找到自己启动的进程并停止它,解释“后台、暂停、退出”的区别;能定位一个占空间的目录,而不是随意清理系统文件。

8. 网络与 SSH:从本机服务到远程机器

先记住地址的含义:127.0.0.1 是当前网络环境自身的 IPv4 回环地址;0.0.0.0 作为监听地址表示所有 IPv4 接口,不是浏览器访问时应填写的服务器地址。端口区分同一台机器上的不同服务。WSL、容器和宿主机有各自的网络边界,不要假设两边的 localhost 总是相同。

Linux 网络与 SSH 心智模型:客户端、服务器、监听端口、密钥和端口转发

先做一个可以立即排错的网络实验。 在终端 A 运行:

1
2
3
cd ~/linux-lab/data
printf 'hello over HTTP\n' > index.html
python3 -m http.server 8000 --bind 127.0.0.1

这会把当前 data 目录作为静态文件目录。终端 A 保持运行,在第二个 Ubuntu 终端 B 执行:

1
2
3
ss -lntp 'sport = :8000'
curl -i --connect-timeout 2 --max-time 5 http://127.0.0.1:8000/
curl -i --max-time 5 http://127.0.0.1:8000/not-exist

第一条应看到 LISTEN、127.0.0.1:8000,同用户运行通常能看到进程信息;必要时用 sudo ss。第二条返回 200 和 hello over HTTP,第三条返回 404。404 表示你已经连上某个 HTTP 服务,问题在路由或资源层面。

回到终端 A 按 Ctrl+C 停止,再在 B 请求同一个地址,通常会得到连接被拒绝。这与之前的 404 是两个层次的问题。这个实验结束后保持 8000 端口空闲,后面的小应用会用它。

现象 下一步 不能直接推出的结论
Could not resolve host getent hosts 域名、dig 域名 不能据此判断应用宕机
Connection refused 查目标地址、端口、ss -lntp 通常是无监听或被主动拒绝,不是 HTTP 500
连接超时 查路由、防火墙、安全组和目标是否可达 不能只凭超时认定某一层有问题
TLS 证书校验失败 核对域名、时间、证书链 不应把 curl -k 当作永久修复
HTTP 401 / 403 检查认证和访问权限 不代表 Linux 文件权限一定错误
HTTP 404 / 500 查路径、请求参数、应用日志 不能靠更换 DNS 解决所有应用错误

日常分层检查可使用:

1
2
3
4
5
6
ip -br addr
ip route
getent hosts example.com
dig example.com
ping -c 3 example.com
curl -v --connect-timeout 3 --max-time 10 https://example.com/

ping 使用 ICMP,对方可能禁用;ping 不通不等于 HTTP 不通,ping 通也不能证明应用健康。getent 走系统名称解析,可能包含 /etc/hosts;dig 主要用于查看 DNS,结果有时因此不同。

curl -i 显示响应头,-v 显示连接细节,-L 跟随重定向,-o 文件 保存响应。普通 curl 遇到 HTTP 404/500 不一定返回非零退出码;脚本检查可使用 curl --fail-with-body --silent --show-error --max-time 5 URL。--connect-timeout 限制建立连接的阶段,--max-time 限制整次传输。参数语义见 curl 官方手册。

若 Linux 内访问成功、Windows 浏览器失败,先确认访问方向和 WSL 网络模式,再查看 Windows 防火墙或代理设置;NAT 与镜像模式的行为不同,见 Microsoft WSL 网络文档。本教程的验证以 Ubuntu 内的 curl 为准。

SSH 不需要先租服务器。 你可以在自己的 Ubuntu 练习机安装服务端,再从同一环境连接它。下面的安装通常会启用入站 SSH 服务,只在自己的实验环境执行;公司服务器已有 SSH 时不需要重新安装:

1
2
3
4
5
sudo apt install -y openssh-server
sudo systemctl start ssh
sudo ss -lntp 'sport = :22'
sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
ssh -o HostKeyAlgorithms=ssh-ed25519 "$(id -un)@127.0.0.1"

首次连接出现指纹时,与上一条命令显示的 SHA256 指纹核对,再确认并输入 Linux 用户密码。连接真正的远端时,应从管理员或可信控制台独立取得主机指纹。主机指纹证明服务器身份,用户密钥证明登录者身份,是两回事。

登录后执行 whoami、hostname、pwd,再 exit 回到客户端。连接同一台机器时,用户名和主机名可能一样;仍要养成核对的习惯。

接下来在 SSH 客户端,也就是退出远程会话后的 Ubuntu 终端 创建独立练习密钥;如果同名文件已存在,复用它或换新名字,不要覆盖已有密钥:

1
2
3
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_linux_lab -C 'linux-lab'
ssh-copy-id -i ~/.ssh/id_ed25519_linux_lab.pub "$(id -un)@127.0.0.1"
ssh -i ~/.ssh/id_ed25519_linux_lab "$(id -un)@127.0.0.1"

密钥可以设置口令;之后提示密钥口令与服务器密码是不同机制。私钥 id_ed25519_linux_lab 留在客户端,.pub 公钥可以装入服务端的 ~/.ssh/authorized_keys。推荐 .ssh 目录权限为 700,私钥及 authorized_keys 为 600,同时检查所有者和父目录权限。客户端还会在 known_hosts 中记录服务器主机密钥。

有些云服务器默认禁用密码认证,需要通过已有密钥连接或在云控制台安装公钥,不能假设 ssh-copy-id 一定可以靠密码首次登录。SSH 的连接、密钥和转发参数见 OpenSSH 手册。

再次 exit 回客户端后,传一个文件并校验:

1
2
3
4
5
scp -i ~/.ssh/id_ed25519_linux_lab ~/linux-lab/notes/day1.txt \
  "$(id -un)@127.0.0.1:linux-lab/notes/received.txt"
sha256sum ~/linux-lab/notes/day1.txt
ssh -i ~/.ssh/id_ed25519_linux_lab "$(id -un)@127.0.0.1" \
  'sha256sum ~/linux-lab/notes/received.txt'

两处哈希应相同。冒号之后的相对路径从远端用户家目录开始;远端目录须先存在。真服务器的连接形式是 ssh -p 端口 用户名@服务器地址,而 scp 的端口选项是大写 -P。

重复连接时,可用 nano ~/.ssh/config 添加一个 Host 别名。下面把 YOUR_LINUX_USER 替换成 id -un 的输出,之后便可运行 ssh linux-lab:

1
2
3
4
5
6
Host linux-lab
    HostName 127.0.0.1
    User YOUR_LINUX_USER
    Port 22
    IdentityFile ~/.ssh/id_ed25519_linux_lab
    IdentitiesOnly yes

目录同步先预演,再去掉 n 执行;末尾的 / 表示复制目录内容:

1
2
rsync -avn ~/linux-lab/notes/ linux-lab:linux-lab/notes-sync/
rsync -av ~/linux-lab/notes/ linux-lab:linux-lab/notes-sync/

需要访问服务器只监听回环地址的 8000 端口时,可以在客户端打开隧道:

1
ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:18000:127.0.0.1:8000 linux-lab

左边的 127.0.0.1:18000 是客户端监听地址,右边的 127.0.0.1:8000 从 SSH 服务端视角访问。保持这个终端运行,在客户端另一个终端访问 http://127.0.0.1:18000/health,就能转到下一节部署的应用。现在应用尚未启动,可以到下一节再验证;用 Ctrl+C 关闭隧道。

SSH 连不上时,用 ssh -v linux-lab 查连接和认证过程;未经确认的日志不要原样公开。修改服务端配置前先 sudo sshd -t 检查语法,保留原连接、另开窗口验证新连接。看到主机密钥变化,应先核实重装或换机情况,不能通过关闭主机检查直接跳过。sshd 检查与权限说明

动手练习: 说明 HTTP 404 与连接被拒绝的区别,完成一次密钥 SSH 登录和文件哈希校验;有真正远端时,再重复一次跨机器操作。

9. 综合实战:部署并管理一个 HTTP 服务

这一节把前面的技能串起来。应用只提供三个行为:/health 返回健康状态,/ 返回环境变量中的消息,其他路径返回 404。你不需要研究 Python 语法,关注程序放在哪里、以谁的身份执行、如何传配置、在哪里看日志。

使用 systemd 管理 Linux 服务:单元文件、环境配置、应用进程、端口与日志

先让它在前台工作。 执行 nano ~/linux-lab/app/app.py,粘贴下面的完整内容,保存退出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env python3
"""A small HTTP service for practicing Linux operations; no dependencies."""

import json
import os
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        path = urlsplit(self.path).path
        if path == "/health":
            status, payload = 200, {"status": "ok", "service": "linux-lab"}
        elif path == "/":
            status, payload = 200, {"message": os.getenv("APP_MESSAGE", "Hello Linux")}
        else:
            status, payload = 404, {"error": "not found"}

        body = (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_request(self, code="-", size="-"):
        # repr escapes control characters in the request target.
        now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
        print(
            f"{now} level=INFO method={self.command} "
            f"path={self.path!r} status={code}",
            flush=True,
        )


if __name__ == "__main__":
    host = os.getenv("APP_HOST", "127.0.0.1")
    port = int(os.getenv("APP_PORT", "8000"))
    with ThreadingHTTPServer((host, port), Handler) as server:
        print(f"Listening on http://{host}:{server.server_port}", flush=True)
        try:
            server.serve_forever()
        except KeyboardInterrupt:
            print("Stopping linux-lab", flush=True)

在终端 A 执行:

1
2
cd ~/linux-lab/app
APP_MESSAGE='My first Linux service' .venv/bin/python app.py

在终端 B 验证:

1
2
3
4
curl -i --max-time 5 http://127.0.0.1:8000/health
curl -i --max-time 5 http://127.0.0.1:8000/
curl -i --max-time 5 http://127.0.0.1:8000/missing
ss -lntp 'sport = :8000'

健康接口应返回 {"status": "ok", "service": "linux-lab"},根路径返回 My first Linux service,不存在的路径返回 404。观察终端 A:每次请求都有 path= 和 status= 日志,时间使用 UTC。实际服务日志和第 5 节的固定练习日志是两份数据,不要混用预期数量。

若提示 Address already in use,先确认第 8 节的静态服务器是否已经停止。前台验证完成后,在 A 按 Ctrl+C 停止应用,释放端口。

这是用于学习运维操作的本地应用,采用 Python 标准库 http.server。它不是生产 Web 框架,官方也不建议将该模块直接用于生产环境;实际项目要按所用框架的部署要求配置应用服务器、入口与访问控制。Python http.server 文档

接着把运行环境固定下来。 以下命令在自己的 Ubuntu 练习机执行;如果已有同名用户、/opt/linux-lab 或同名服务,先确认它们确实属于本实验,再继续使用。

1
2
3
4
5
id -u linux-lab >/dev/null 2>&1 || sudo useradd --system --user-group \
  --home-dir /opt/linux-lab --no-create-home --shell /usr/sbin/nologin linux-lab
sudo install -d -o root -g root -m 755 /opt/linux-lab
sudo install -o root -g root -m 644 ~/linux-lab/app/app.py /opt/linux-lab/app.py
sudo /usr/bin/python3 -m venv /opt/linux-lab/.venv

这里创建专用系统用户,不允许它交互登录;应用代码由 root 持有,服务用户只需要读取和执行。重新创建部署用虚拟环境,避免把家目录下的虚拟环境直接搬过去,因为虚拟环境内可能记录绝对路径。

写入配置:

1
2
3
4
5
6
sudo tee /etc/linux-lab.env > /dev/null <<'EOF'
APP_HOST=127.0.0.1
APP_PORT=8000
APP_MESSAGE="Managed by systemd"
EOF
sudo chmod 600 /etc/linux-lab.env

再创建服务单元:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
sudo tee /etc/systemd/system/linux-lab.service > /dev/null <<'EOF'
[Unit]
Description=Linux practice HTTP service
After=network.target

[Service]
Type=simple
User=linux-lab
Group=linux-lab
WorkingDirectory=/opt/linux-lab
EnvironmentFile=/etc/linux-lab.env
ExecStart=/opt/linux-lab/.venv/bin/python -u /opt/linux-lab/app.py
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF
字段 在这里解决的问题
User、Group 明确以普通服务用户运行
WorkingDirectory 固定当前工作目录,避免依赖你从哪里启动
EnvironmentFile 显式传入配置,由系统服务管理器读取
ExecStart 使用解释器与程序的绝对路径
-u Python 输出不缓冲,便于及时查看日志
Restart=on-failure 异常失败后尝试重启;手动 stop 不会因此被自动拉起
RestartSec=3 重启尝试间隔 3 秒,避免紧密循环
WantedBy enable 时建立随相应 target 启动的关系

EnvironmentFile 不是随意执行的 Shell 脚本,不能把 export、命令替换或一般脚本逻辑直接写进去。ExecStart 也不会自动按 Bash 解释 >、| 和 ~。After=network.target 描述启动顺序,不保证此时互联网或数据库已经可用。参考 systemd 服务文档源码 与 执行环境文档源码。

检查配置、启动服务并验证响应:

1
2
3
4
5
6
7
sudo systemd-analyze verify /etc/systemd/system/linux-lab.service
sudo systemctl daemon-reload
sudo systemctl enable --now linux-lab
systemctl status linux-lab --no-pager -l
curl --fail-with-body --silent --show-error --max-time 5 http://127.0.0.1:8000/health
curl --fail-with-body --silent --show-error --max-time 5 http://127.0.0.1:8000/
sudo journalctl -u linux-lab -n 30 --no-pager

预期是 active (running)、健康接口返回 ok、根路径消息为 Managed by systemd。启动到监听就绪可能有短暂间隔;刚启动立即请求失败时先看日志,再复测。systemd 显示 active,只证明进程处于对应状态,仍需 HTTP 请求验证业务就绪。

几种常用操作要区分清楚:

操作 作用
sudo systemctl start linux-lab 现在启动
sudo systemctl stop linux-lab 现在停止
sudo systemctl restart linux-lab 停止后重新启动
sudo systemctl enable linux-lab 配置以后随系统相应阶段启动,不立即启动
sudo systemctl disable linux-lab 取消以后自动启动,不立即停止
sudo systemctl daemon-reload 服务单元改动后让 systemd 重读配置,不重启应用
sudo journalctl -u linux-lab -f 持续看服务日志,Ctrl+C 退出

修改 .service 后通常先 daemon-reload,再重启;仅改这里引用的 env 文件,重启应用即可重新读取。不是所有服务都支持 systemctl reload,本例就没有配置它。

动手用 sudo nano /etc/linux-lab.env 修改 APP_MESSAGE,保存后执行 sudo systemctl restart linux-lab,再请求根路径确认变化。试着只在交互终端 export APP_MESSAGE=...,你会发现它并不会修改已经运行的服务。

观察服务身份和重启次数:

1
2
3
systemctl show linux-lab -p MainPID -p User -p NRestarts
sudo ss -lntp 'sport = :8000'
sudo journalctl -u linux-lab --since '10 minutes ago' --no-pager

如果想验证故障后的自动拉起,可以在确认对象就是本练习服务后,先记下 MainPID,再执行一次下方命令。这是刻意模拟进程被强制杀死,日常停止仍用 systemctl stop:

1
sudo systemctl kill --kill-whom=main --signal=SIGKILL linux-lab

等待几秒后,再运行前面的 systemctl show 和健康检查,应该看到新 PID、重启计数增加,接口恢复。反过来,执行 systemctl stop 应让服务保持停止,直到你主动 start。

最后验证启动恢复:普通虚拟机保存工作后重启;WSL 用户保存全部 WSL 工作,在 PowerShell 执行 wsl --terminate Ubuntu-24.04,名称按实际替换,再进入发行版。检查 systemctl is-enabled linux-lab、systemctl is-active linux-lab 与健康接口。这里验证的是 Linux 环境重新启动后的服务恢复。

不再练习时可以执行 sudo systemctl disable --now linux-lab,停止服务并取消自动启动。配置、代码和笔记可以保留,方便复习。

动手练习: 不依赖手动打开终端维持程序,服务以 linux-lab 用户运行;能修改配置、查日志、重启、通过 SSH 隧道访问,并验证环境重启后的恢复。

10. Shell 自动化:备份必须能恢复

一个实用脚本至少要知道:输入是什么,失败怎么报告,文件名有空格会不会出错,重复执行会不会覆盖数据。下面用备份把这些要求落到实处。

备份与恢复闭环:校验输入、创建归档、临时恢复、比较结果并重复验证

先练两个很常用的组合。循环可以对一组文件重复执行同一操作:

1
2
3
4
5
for note in "$HOME/linux-lab/notes/"*.txt; do
    [[ -f "$note" ]] || continue
    printf '%s: ' "$note"
    wc -l < "$note"
done

这会逐个显示笔记文件的行数;每轮的 note 是一个路径。变量部分加引号,外面的 *.txt 仍允许展开;文件检查可以跳过没有匹配时残留的模式。不要用 for file in $(ls) 处理文件名,空格和换行容易让它拆错。

函数和条件分支可以把重复检查组织成一个动作。确保上一节服务正在运行,再执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
check_health() {
    curl --fail-with-body --silent --show-error --max-time 5 \
        http://127.0.0.1:8000/health
}

if check_health; then
    printf 'HTTP health check passed\n'
else
    printf 'HTTP health check failed; inspect the service logs\n' >&2
fi

这里 if 根据函数最后一个命令的退出码判断。这个检查确认 HTTP 请求成功;更严格的业务检查还应解析响应中的 status 字段。需要保存为脚本时加上 Bash shebang;如果失败要让调用者知道,应在失败分支明确 exit 1,因为最后一次 printf 成功不能代表服务健康。

先理解压缩归档的基本操作。确保 data 中已有前面的练习文件:

1
2
3
4
5
6
mkdir -p ~/linux-lab-backups
tar -czf ~/linux-lab-backups/data-demo.tar.gz -C ~/linux-lab data
tar -tzf ~/linux-lab-backups/data-demo.tar.gz
restore_dir=$(mktemp -d "$HOME/linux-lab-restore.XXXXXX")
tar -xzf ~/linux-lab-backups/data-demo.tar.gz -C "$restore_dir"
diff -r ~/linux-lab/data "$restore_dir/data"

c 创建,x 解开,t 查看列表,z 使用 gzip,f 后面跟归档文件,-C 指定操作目录。这个入门示例重复执行会覆盖同名 data-demo.tar.gz,下一段脚本会改成每次唯一的名字。diff -r 无输出且退出码为 0 表示内容一致。每次恢复都放入一个新的临时目录,不覆盖原数据。

只对可信归档按上面的方式解压,拿到不熟悉的归档先查看列表。正在写入的数据库不能靠复制目录或 tar 保证一致性,要用相应数据库的备份工具;本例处理静态练习文件。

执行 nano ~/linux-lab/scripts/backup.sh,写入完整脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -ne 2 ]]; then
    printf 'Usage: bash %s SOURCE_DIR BACKUP_DIR\n' "$0" >&2
    exit 64
fi

if [[ ! -d "$1" ]]; then
    printf 'Source directory does not exist: %s\n' "$1" >&2
    exit 1
fi

# realpath resolves symlinks; -m also accepts a destination not yet created.
source_dir=$(realpath -e -- "$1")
backup_dir=$(realpath -m -- "$2")
if [[ "$source_dir" == / || "$backup_dir" == "$source_dir" ||
      "$backup_dir" == "$source_dir/"* ]]; then
    printf 'Backup directory must be outside the source directory.\n' >&2
    exit 1
fi

mkdir -p -- "$backup_dir"
# mktemp creates a unique file, even for concurrent runs in the same second.
archive=$(mktemp "$backup_dir/backup-$(date -u +%Y%m%dT%H%M%SZ)-XXXXXX.tar.gz")
# A failed tar command must not leave an incomplete archive behind.
trap 'rm -f -- "$archive"' EXIT
tar -czf "$archive" -C "$source_dir" .
trap - EXIT

printf 'Backup created: %s\n' "$archive"

不要急着运行,先读懂关键行:

写法 用处
#!/usr/bin/env bash 直接执行脚本时,指定用 Bash 解释
$#、$1、$2 参数数量、第一和第二个参数
[[ ... ]]、if Bash 条件判断,例如目录存在与参数数量
>&2、exit 1 报错到标准错误,并返回非零状态
set -u 引用未定义变量时暴露错误
set -e、pipefail 让很多执行失败尽早终止;仍要理解条件和管道例外
realpath 解析真实路径,包括符号链接,避免把备份写回源目录内
mktemp 创建唯一文件,连续运行不会撞名覆盖
trap ... EXIT 失败退出时移除这次创建的不完整归档

脚本限定备份的是普通目录,不允许 /,也不允许备份目录位于源目录内部。否则可能把正在生成的备份再次打包。它适用于自己控制的练习目录,不负责解决多人并发修改目录等复杂情况。

在 Ubuntu 执行:

1
2
3
4
bash -n ~/linux-lab/scripts/backup.sh
shellcheck ~/linux-lab/scripts/backup.sh
chmod u+x ~/linux-lab/scripts/backup.sh
~/linux-lab/scripts/backup.sh "$HOME/linux-lab/data" "$HOME/linux-lab-backups"

bash -n 检查语法,ShellCheck 检查常见 Shell 问题。bash 脚本 不要求脚本具备执行权限;./脚本 需要执行权限与有效解释器。Windows 编辑后出现 /usr/bin/env: 'bash\r': No such file or directory 等错误时,用 file 脚本 检查换行符,在编辑器切换成 LF、UTF-8 后保存。

脚本输出 Backup created: 完整路径。把下面第一行的值换成刚才输出的归档路径,再验证恢复:

1
2
3
4
5
6
archive_path='/把这里替换为刚才输出的完整归档路径'
tar -tzf "$archive_path"
restore_dir=$(mktemp -d "$HOME/linux-lab-restore.XXXXXX")
tar -xzf "$archive_path" -C "$restore_dir"
diff -r "$HOME/linux-lab/data" "$restore_dir"
echo $?

这里没有 data/ 外层,因为脚本用 -C 源目录 .,所以比较恢复目录本身。预期 diff 无输出,退出码为 0。只看到压缩包存在,还不能证明备份可用。

再主动触发失败:

1
2
3
4
bash ~/linux-lab/scripts/backup.sh
echo $?
bash ~/linux-lab/scripts/backup.sh "$HOME/linux-lab/data" "$HOME/linux-lab/data/backups"
echo $?

第一组应显示用法并返回 64,第二组应拒绝备份到源目录内并返回 1。再试一次含空格的源目录,验证双引号的价值。不要用 bash -x 调试含真实密码的脚本,它会输出展开后的命令。

定时执行是选做加练。 在普通练习机安装并启动 cron:

1
2
3
sudo apt install -y cron
sudo systemctl enable --now cron
crontab -e

假设你的 Linux 用户名是 alice,在打开的编辑器中添加下面文件内容;把每处 /home/alice 替换成 echo "$HOME" 得到的真实家目录,日志目录要事先存在:

1
2
3
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
0 19 * * * /bin/bash /home/alice/linux-lab/scripts/backup.sh /home/alice/linux-lab/data /home/alice/linux-lab-backups >> /home/alice/linux-lab/logs/backup-cron.log 2>&1

前五项依次是分钟、小时、每月第几天、月份、星期;这里按机器时区每天 19:00 执行。用 date、timedatectl 核对时间,用 crontab -l 查看已保存的任务。cron 的环境与交互式终端不同,所以写绝对路径和明确 PATH。先在终端手动执行相同命令,确认成功,再等待一次调度并检查日志和新归档。

普通 cron 不会补执行关机时错过的任务;WSL 未运行时更不能依赖它做定时备份。练习后用 crontab -e 删除自己添加的那一行,保留其他任务。更稳定的服务端定时任务可继续学习 systemd timer,但不必在本教程里展开。

动手练习: 脚本正常时生成可恢复的备份,参数错误时非零退出,能正确处理空格目录,连续两次执行保留两份归档。

11. 常见故障的排查与恢复

端口占用、权限不足、配置值错误和路径拼写错误,都是运行服务时常见的问题。下面通过前面的示例服务复现这四种情况,观察它们的表现和处理方式。一次只复现一种,恢复并验证健康后,再继续。 这些操作只用于自己的练习环境。

Linux 服务故障排查流程:从现象、范围和证据走到根因、修复与复测

开始前确保第 9 节服务正常,并保存基线配置;如果备份文件已经存在,先确认是否保留,不要覆盖唯一的可用版本:

1
2
3
curl --fail-with-body --silent --show-error --max-time 5 http://127.0.0.1:8000/health
sudo cp -i /etc/linux-lab.env /etc/linux-lab.env.before-drill
sudo cp -i /etc/systemd/system/linux-lab.service /etc/systemd/system/linux-lab.service.before-drill

排查问题时,可以按下面的顺序梳理信息:

1
2
3
4
5
6
现象:用户看到了什么?从什么时候开始?
范围:这台机器、这个服务、这个接口,还是所有请求?
证据:运行了什么命令?哪几行输出支持判断?
根因:哪个配置、文件、进程或环境条件出了问题?
修复:修改了什么?为什么这能解决问题?
复测:状态、端口、HTTP、日志分别是什么结果?

故障 A:端口被另一个程序占用。 先停掉练习服务,在终端 A 启动临时服务器:

1
2
sudo systemctl stop linux-lab
python3 -m http.server 8000 --bind 127.0.0.1 --directory "$HOME/linux-lab/data"

保持 A 运行,在 B 执行 sudo systemctl start linux-lab。尝试访问 /health,它可能返回 404;这是因为当前端口上运行的是静态服务器。现在只看“8000 端口开着”就会误判。

任务:找出真正占用端口的进程,解释为什么 systemd 服务启动失败;恢复正确服务,验证 /health 的 JSON 内容。

故障 B:程序存在,但服务用户无法读取。 在 A 已停止、练习服务恢复正常后,执行:

1
2
sudo chmod 000 /opt/linux-lab/app.py
sudo systemctl restart linux-lab

任务:从日志和权限信息定位原因。不能通过改为 root 运行服务、给全目录 777,或者重装 Python 来修复。

故障 C:环境变量值无效。 修复 B 后,执行 sudo nano /etc/linux-lab.env,把 APP_PORT=8000 改成 APP_PORT=oops,保存并 sudo systemctl restart linux-lab。

任务:确认 Python 报错发生在读取端口配置时;解释为什么在当前终端 export APP_PORT=8000 无法修复这个服务。修复服务实际读取的文件并重新验证。

故障 D:服务的解释器路径拼错。 修复 C 后,执行 sudo nano /etc/systemd/system/linux-lab.service,只把 ExecStart 中的 .venv/bin/python 改成 .venv/bin/python-missing,其他内容不变,再执行:

1
2
sudo systemctl daemon-reload
sudo systemctl restart linux-lab

任务:通过 systemd 状态和日志区分“Python 已启动但报错”与“解释器根本没执行成功”。修复后说明为什么这次需要 daemon-reload。

参考:诊断依据与恢复方法

优先运行以下只读检查,先看证据,再改配置:

1
2
3
4
5
6
systemctl status linux-lab --no-pager -l
sudo journalctl -u linux-lab -n 50 --no-pager
sudo ss -lntp 'sport = :8000'
ls -l /opt/linux-lab/app.py
namei -l /opt/linux-lab/.venv/bin/python
systemctl cat linux-lab
故障 典型证据 恢复动作
A 日志有 Address already in use;ss 中 PID 属于临时静态服务器 回到启动临时服务器的 A 按 Ctrl+C,再启动 linux-lab
B Python 打开 app.py 时 Permission denied;文件权限是 ---------- sudo chmod 644 /opt/linux-lab/app.py,保留普通服务用户
C Python 日志有 ValueError,端口内容是 oops 将 /etc/linux-lab.env 的端口改回 8000
D 常见状态为 203/EXEC,解释器路径不存在 恢复 .venv/bin/python,然后 daemon-reload

这里 B 的解释器可以启动,因此应用文件读失败通常表现为 Python 退出码;D 才是执行解释器的步骤失败。具体日志文本随版本变化,以实际信息为准。

如果服务连续失败触发启动频率限制,修复后可 sudo systemctl reset-failed linux-lab,再 start。不要在根因未修复时重复 reset-failed。

四轮结束后,如需恢复实验前配置,可使用已经核对过的基线:

1
2
3
4
5
6
sudo cp /etc/linux-lab.env.before-drill /etc/linux-lab.env
sudo cp /etc/systemd/system/linux-lab.service.before-drill /etc/systemd/system/linux-lab.service
sudo chmod 644 /opt/linux-lab/app.py
sudo systemctl daemon-reload
sudo systemctl reset-failed linux-lab
sudo systemctl restart linux-lab

最后必须确认临时静态服务器已停止,再检查服务状态、监听进程、/health 内容和最新日志。每个故障都要有独立复测,不要只在四轮全部结束后做一次。

遇到教程外的问题,也可以按同一顺序缩小范围:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
我在哪台机器、以谁的身份执行?
        ↓
目标进程在吗?状态和最新日志是什么?
        ↓
文件路径、权限、工作目录、环境变量正确吗?
        ↓
监听地址和端口正确吗?curl 返回哪一层错误?
        ↓
本机成功、远端失败时,再检查网络边界与访问控制
        ↓
修复一个原因 → 按同一路径复测 → 记录证据

动手练习: 分别观察四种故障的状态和日志,比较它们的区别,并在每次修复后重新检查接口响应。

12. 常用命令与延伸阅读

前面几个章节中,一些容易混淆的概念可以归纳为下面这张表:

常见问题 理解要点
服务访问不了,先做什么? 确认机器和地址,查进程与日志,再查监听和 curl 错误层次
端口明明开着,为什么接口还是错? 可能是错误进程占用,需关联 PID 与实际响应内容
终端能跑,systemd 不能跑? 用户、目录、解释器路径、权限、环境变量不同
chmod 777 为什么不是通用修复? 可能掩盖所有者或父目录问题,还扩大了权限
nohup 和 systemd 有什么区别? 前者主要处理挂断,后者管理生命周期、日志与重启
机器内存 free 很少,就是内存不够吗? 结合 available、缓存、进程和 OOM 证据判断
备份成功怎么证明? 恢复到新目录并比较内容,仅有文件不够
一个 Shell 脚本写得可靠吗? 看参数校验、引用、退出码、失败处理和重复执行行为

需要执行具体操作时,可以从以下命令开始查询:

目的 常用入口
我是谁、在哪 whoami、id、hostname、pwd
找文件、查内容 find、grep -n、less、tail -F
看权限 ls -ld、namei -l
看进程与资源 ps、top、free -h、df -h、df -i、du
看连接 ss -lntp、curl -v、getent hosts
看服务 systemctl status、systemctl cat、journalctl -u
登录与传文件 ssh、scp、rsync
查帮助 命令 --help、man 命令

掌握这些基础之后,可以根据实际需求继续学习 Docker、反向代理、数据库管理、CI 或更深入的系统监控。内核编译、驱动开发和高级内核追踪属于更专门的主题,可以在遇到相关问题时再深入。

继续查阅的官方资料: Bash 手册、GNU Coreutils 手册、Ubuntu Server 文档、OpenSSH 手册、systemd 文档。遇到选项与本文不同,优先阅读练习机本地的 man,它通常更贴近实际安装的版本。