Linux 系统服务

前言

  • 这篇覆盖「在一台 Linux 上把活干完」的成套工具:压缩打包、管道与重定向、看网络看进程、管服务看日志、防火墙、SSH 远程连接、装软件。
  • 环境同前两篇:Ubuntu 26.04.1 LTS(WSL2),systemd 259,日期 2026-11-03。为了把每条命令真跑一遍,我在这台 WSL 里额外装了 zip unzip bzip2 net-tools ufw openssh-server gcc make build-essential(apt 装完都留着,磁盘从 1.3G 涨到 2.0G),测完把 ufw、ssh 都恢复成了未启用状态。

压缩与打包:先分清两件事

这是新手最容易混的两个概念:

概念 干什么 工具
打包(archive) 把很多文件合成一个文件,顺便保留权限/属主/目录结构,不一定变小 tar
压缩(compress) 把一个文件变短,不处理目录结构 gzip、bzip2、xz、zstd

所以 .tar.gz 其实是两步:先 tar 打包成 .tar,再 gzip 压缩。.zip 则是一步到位——zip 同时干了打包和压缩两件事,这也是它在跨平台时更省事的原因。

原笔记那句「gzip/bzip2 只能压缩单文件,不能压缩文件夹」是对的,但要补:它们遇到目录直接报错,而 tar 天生就是为目录设计的。

一个有意思的实测对比(三个 18 字节的小文件):

1
2
3
$ ls -l 123.zip pkg.tar.gz | awk '{print $5, $9}'
338 123.zip
154 pkg.tar.gz

小文件多的时候 zip 反而更大,因为每个文件都有自己的目录项和压缩头,压不动;tar.gz 把所有内容连成一个流再压,效率高得多。文件多而小的时候别用 zip。

gzip / bzip2 / xz / zip 实测

gzip:默认吃掉源文件

1
2
3
4
5
6
7
8
9
$ gzip 1.txt
$ ls -A
1.txt.gz 2.txt 3.txt sub # 1.txt 没了
$ gzip -d 1.txt.gz # 解压,等价于 gunzip
$ ls -A
1.txt 2.txt 3.txt sub
$ gzip -k 2.txt # -k 保留源文件
$ ls -A
1.txt 2.txt 2.txt.gz 3.txt sub
用法 作用
gzip 文件 压缩并删除源文件(实测)
gzip -k 保留源文件
gzip -d / gunzip 解压,两者等价
gzip -1 ~ -9 压缩级别,实测 18 字节的文件压成 44 字节(比原文件还大,因为有小头部开销)
zcat / gzcat 不解压直接看内容
gzip -t 只测试完整性

对已存在的目标,gzip 会问一句并跳过,实测:gzip: 2.txt already exists; not overwritten。

bzip2 / xz

1
2
3
4
5
6
7
8
9
10
11
$ bzip2 1.txt
$ ls -A
1.txt.bz2 ... # 同样删源文件
$ bzip2 -k 2.txt # 保留
$ bunzip2 1.txt.bz2 # 解压
$ bzip2 -d 2.txt.bz2 # 等价写法
bzip2: Output file 2.txt already exists. # 目标已存在时的报错
$ xz -k 1.txt && ls -l 1.txt 1.txt.xz
18 1.txt
84 1.txt.xz
$ xz -d 1.txt.xz

xz 压得最小但最慢,gzip 最快、兼容性最好,bzip2 是两者之间的老折中。日常结论:分发源码包用 .tar.xz 或 .tar.gz,日志轮转用 gzip(logrotate 默认就是它)。

zip / unzip

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
$ zip 123.zip 1.txt 2.txt 3.txt
adding: 1.txt (stored 0%)
adding: 2.txt (stored 0%)
adding: 3.txt (stored 0%)

$ unzip -o 123.zip -d unz
extracting: unz/1.txt
extracting: unz/2.txt
extracting: unz/3.txt

$ zip -r rec.zip sub # 打包目录必须 -r
adding: sub/deep/ (stored 0%)
adding: sub/deep/inner.txt (stored 0%)

$ zip -d 123.zip 2.txt
deleting: 2.txt

$ unzip -l 123.zip # 列内容用 unzip -l,不是 zip -l
Archive: 123.zip
Length Date Time Name
--------- ---------- ----- ----
20 2026-11-03 18:22 1.txt

$ unzip -t 123.zip # 测试完整性
testing: 3.txt OK
No errors detected in compressed data of 123.zip.
命令 作用
zip -q 静默,实测真的没有输出
zip -r 递归,打包目录必加
zip -d 包 文件 从包里删文件
zip -F / -FF 修复损坏的包(-FF 更彻底)
unzip -l 只列不解压
unzip -o 覆盖已存在文件不提示
unzip -d 目录 解到指定目录
unzip -q 静默
zipinfo 包 第三种列法,格式和 ls -l 一样

版本实测:This is Zip 3.0 (July 5th 2008), by Info-ZIP——zip 这个格式二十多年没大改过,胜在到处都能开。

tar:最常用的打包工具

三个主选项互斥(一次只能选一个),加上修饰选项:

1
2
3
4
5
$ tar --help | grep -E -- "-c, --create|-x, --extract|-t, --list|-C, --directory"
-c, --create create a new archive
-t, --list list the contents of an archive
-x, --extract, --get extract files from an archive
-C, --directory=DIR change to directory DIR
选项 作用 实测
-c 创建包 tar -cvf all.tar 1.txt 2.txt 3.txt 输出三个文件名
-x 解包 —
-t 只列内容 tar -tf all.tar 列出 1.txt 2.txt 3.txt
-v 显示过程 -tvf 还能看到权限、属主、大小、时间
-f 指定包文件名,必须紧挨着文件名 —
-z 顺便用 gzip 生成 all.tar.gz
-j 顺便用 bzip2 生成 .tar.bz2
-J 顺便用 xz 生成 all.tar.xz,实测 208 字节
-C 目录 切换工作目录(解到别处 / 从别处打包) tar -xvf all.tar -C outdir 实测解出三个文件
--exclude 排除 tar --exclude='3.txt' -cvf ex.tar 1.txt 2.txt 3.txt 实测包里只剩 1、2

体积实测(同样三个小文件):

1
2
3
4
$ ls -l all.tar all.tar.gz all.tar.xz | awk '{print $5, $9}'
10240 all.tar
157 all.tar.gz
208 all.tar.xz

all.tar 是 10240 字节是因为 tar 默认按 20 字节的块对齐、一次写满一个 10 KB 的块,不是真实数据量——这也是为什么小文件打包后 tar 显得虚胖。这里 xz 反而比 gzip 大,同样是「内容太小、头部开销占主导」,文件大时 xz 通常明显更小。

三个高频用法,直接抄:

1
2
3
tar -czvf 项目.tar.gz 项目目录/        # 打包 + gzip 压缩
tar -tzvf 项目.tar.gz # 先看里面有什么,别急着解
tar -xzvf 项目.tar.gz -C /opt/target # 解到指定目录(目录要先存在)

两个实测到的细节:

  • 新版 tar 会自己识别压缩格式:tar -xvf all.tar.gz 不加 -z 也能正常解压(实测解出三个文件)。所以解压时统一写 tar -xvf 就行,不用猜是 gz 还是 xz。
  • --exclude 必须放在文件名之前,原笔记那条 tar -zcvf 12.tar.gz *.txt --exclude 实测报错:
1
2
$ tar -cvf ex2.tar *.txt --exclude
tar: option '--exclude' requires an argument

.tgz 是 .tar.gz 的简写,实测 tar -tzvf all.tgz 正常列出内容;.tar.bz2 的简写是 .tbz2/.tbz。

管道与 xargs

| 把前一个命令的标准输出接到后一个命令的标准输入。前提是后一个命令愿意读标准输入——这一点是所有管道困惑的根源。

1
2
$ ls | grep "^[12]"
1.txt 1.txt.gz 1.txt.xz 2.txt 2.txt.gz

反例,实测:

1
2
3
$ echo 1.txt 2.txt | rm
rm: missing operand
Try 'rm --help' for more information.

rm 不读标准输入,它只要参数,所以管道到这里就断了,两个文件一个都没删(实测当时目录里 7 个 txt 全在)。这就是 xargs 存在的意义:把标准输入变成命令行参数。

1
2
3
$ echo "1.txt 2.txt" | xargs rm -f
$ ls -A
1.txt.gz 1.txt.xz 2.txt.gz 3.txt ... # 1.txt 和 2.txt 没了

原笔记那条 find -name 2.txt | xargs rm -rf 思路对,写法有两个问题:find 不写路径默认从当前目录开始(见《Linux 文件操作》),以及删单个文件用 rm -f 就够,别习惯性 -rf。实测:

1
$ find . -maxdepth 1 -name "2.txt" | xargs rm -f

带空格的文件名会翻车,因为 xargs 默认按空白切分。实测两种写法对比:

1
2
3
4
$ find . -maxdepth 1 -name "*.txt" | xargs echo 参数是:
参数是: ./has ./space.txt ./1.txt ./3.txt # has 和 space.txt 被拆成两个参数
$ find . -maxdepth 1 -name "*.txt" -print0 | xargs -0 echo 用 -0:
用 -0: ./has space.txt ./1.txt ./3.txt # 正确

所以标准写法永远是 find ... -print0 | xargs -0 ...(或者干脆用 find ... -exec ... +,见上一篇)。

xargs 的其他常用项,实测:

写法 作用 实测输出
xargs -n1 每次只给一个参数 printf 'a b c\n' | xargs -n1 echo → 三行,每行一个
xargs -I{} 用占位符决定参数位置 printf 'one two\n' | xargs -I{} echo 替换:{} → 替换:one two
xargs -0 按 NUL 分隔(配 -print0) 见上
xargs -d 自定义分隔符,如 -d, 处理逗号列表 —
xargs -P2 并行跑,2 个进程 —

参数太多时 xargs 会自动分批执行(实测把 2000 个参数喂给它照样成功),这正是它比 shell 通配符强的地方:rm *.txt 在文件极多时会报 Argument list too long,find | xargs rm 不会。

重定向与文件描述符

每个进程一出生就有三个打开的文件描述符,实测能直接看到:

1
2
3
4
$ ls -l /proc/self/fd
lr-x------ 1 loong loong 64 ... 0 -> pipe:[33761]
l-wx------ 1 loong loong 64 ... 1 -> pipe:[38004]
l-wx------ 1 loong loong 64 ... 2 -> pipe:[38004]
编号 名字 默认去哪
0 stdin 标准输入 键盘(终端)
1 stdout 标准输出 终端
2 stderr 标准错误 终端

1 和 2 是两个独立的流,这是重定向所有坑的来源。

1
2
3
4
5
6
7
$ ls > o1.txt            # 覆盖写:文件不存在就建,存在就清空
$ ls >> o1.txt # 追加
$ printf 'OLD\n' > o2.txt && echo NEW > o2.txt && cat o2.txt
NEW # 实测:> 真的把 OLD 清掉了
$ ls /不存在xyz 2> e1.txt
$ cat e1.txt
ls: cannot access '/不存在xyz': No such file or directory

把错误也写进同一个文件有两种写法,实测都有效:

1
2
$ ls /不存在xyz > both.txt 2>&1      # 先把 2 指向 1 的当前位置,再重定向 1
$ ls /不存在xyz &> both2.txt # bash 简写,等价

顺序错了结果完全不同,实测:

1
2
3
4
$ ls /不存在xyz 2>&1 > order.txt
ls: cannot access '/不存在xyz': No such file or directory # 错误仍然打到屏幕上
$ test -s order.txt && echo 有 || echo 空
空

因为 2>&1 先执行时,1 还指向终端,于是错误被绑到终端;之后 > order.txt 只改了 1。记法:> 文件 2>&1(先重定向 1,再让 2 跟上)才是「两个都进文件」。

其余常用招数,全部实测:

1
2
3
4
5
6
7
8
9
10
11
$ command > /dev/null 2>&1          # 输出全丢,只保留退出码(实测退出码 2)
$ cat < in.txt # 输入重定向
from file
$ echo hi | tee tee.txt && cat tee.txt
hi
hi # tee 既显示又存盘
$ diff <(echo a) <(echo b)
1c1
< a
---
> b # 进程替换,bash 特性

原笔记里 ls /xxx /home &> 1.tt 那个 1.tt 是笔误。补三条实战常用:

  • exec > /tmp/run.log 2>&1 之后脚本里所有输出都进日志。
  • cmd 2>/dev/null 只屏蔽错误(find / 时必备)。
  • cmd > file 2>&1 与 cmd &> file 在 bash 里等价,但前者是 POSIX 标准写法,脚本里用前者。

网络服务与网卡

原笔记这一节的东西今天变化最大,逐条实测:

老命令 现状(实测) 现代替代
ping 还在,/usr/bin/ping 一样用:ping -c 2 域名
ifconfig 默认没装,属于 net-tools 包(我 apt 装上才有的) ip addr / ip -br addr
netstat 默认没装,同属 net-tools ss(/usr/bin/ss,默认可用)
ifup / ifdown 没装,属于 ifupdown 包,Ubuntu 已不用它 netplan(netplan.io 1.2-1ubuntu5 装着)
server networking start 这个命令不存在(command -v server 无输出) systemctl restart systemd-networkd

ping 实测(WSL 里访问公网走的是 Windows 的网络栈):

1
2
3
4
5
$ ping -c 2 archive.ubuntu.com
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 232.123/235.170/238.218/3.047 ms
$ echo $?
0

-c 指定次数很关键:不加的话 ping 会一直发,新手常以为卡死了。

ip 的三条最常用的,实测输出:

1
2
3
4
5
6
7
8
9
10
11
12
$ ip -br addr
lo UNKNOWN 127.0.0.1/8 10.255.255.254/32 ::1/128
eth0 UP 192.168.104.119/20 fe80::215:5dff:fec0:be03/64

$ ip route
default via 192.168.96.1 dev eth0 proto kernel onlink
192.168.96.0/20 dev eth0 proto kernel scope link src 192.168.104.119

$ ifconfig eth0 | head -4
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.104.119 netmask 255.255.240.0 broadcast 192.168.111.255
ether 00:15:5d:c0:be:03 txqueuelen 1000 (Ethernet)

注意 192.168.104.119 这个地址:WSL2 默认是 NAT 网络,Linux 侧拿到的是 Hyper-V 虚拟网段的地址,Windows 从局域网访问它需要端口转发;这也是为什么「在 WSL 里起了服务,手机连不上」。要练真实服务器网络,还是得上云主机或虚拟机桥接。

DNS 配置在 WSL 里是自动生成的:

1
2
3
4
5
$ head -3 /etc/resolv.conf
# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to /etc/wsl.conf:
# [network]
# generateResolvConf = false
nameserver 10.255.255.254

普通 Ubuntu 服务器上,网络配置看 /etc/netplan/*.yaml(netplan.io 已装),但这台 WSL 里 /etc/netplan 是空的,网络由 WSL + systemd-networkd 负责。所以「改 netplan 配静态 IP」这类操作在 WSL 里不适用。

看端口、看资源、看进程

netstat 与 ss

netstat -tunpl 是原笔记的写法,实测(-t TCP、-u UDP、-n 不解析名字、-p 显示进程、-l 只看监听):

1
2
3
4
5
6
7
$ netstat -tunpl | head -6
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.54:53 0.0.0.0:* LISTEN 79/systemd-resolved
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1/init
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 79/systemd-resolved
udp 0 0 127.0.0.54:53 0.0.0.0:* 79/systemd-resolved

同一件事用 ss 更快、信息更准:

1
2
3
4
$ ss -tunlp | head -4
Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
udp UNCONN 0 0 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=79,fd=18))
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=79,fd=16))

上面那行 0.0.0.0:22 LISTEN 1/init 很有教学意义:那时我还没启动 sshd,但 22 端口已经被 PID 1(systemd)占着——因为新版 Ubuntu 的 ssh 用 socket 激活,ssh.socket 先占住端口,有连接进来才拉起 ssh.service。ss 的输出里能同时看到两个持有者:

1
2
$ ss -lntp | grep :22
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=3668,fd=3),("systemd",pid=1,fd=139))

lsof 也能干这件事,实测用一个临时监听验证:

1
2
3
4
5
6
$ nc -l -p 8899 &            # 起一个监听
$ lsof -i :8899 -P -n
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nc 10890 root 3u IPv4 58092 0t0 TCP *:8899 (LISTEN)
$ ss -lntp | grep 8899
LISTEN 0 1 0.0.0.0:8899 0.0.0.0:* users:(("nc",pid=10890,fd=3))

看总量用 ss -s:

1
2
3
$ ss -s
Total: 266
TCP: 5 (estab 0, closed 2, orphaned 0, timewait 2)

排查「端口被占用」的三件套:ss -lntp | grep 端口 → lsof -i :端口 → ps -p PID -o pid,user,cmd。

free / w / uptime / top

1
2
3
4
5
6
7
8
9
$ free -h
total used free shared buff/cache available
Mem: 7.8Gi 526Mi 6.8Gi 4.1Mi 581Mi 7.2Gi
Swap: 2.0Gi 0B 2.0Gi

$ w
18:20:04 up 8 min, 2 users, load average: 2.20, 0.67, 0.22
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
loong pts/2 - 18:19 11.00s 0.01s 0.01s -bash

free 一定要看 available 而不是 free:buff/cache 里那 581 Mi 是页缓存,内存紧张时内核会自动让出来,所以真正能用的是 7.2 Gi。-b/-k/-m/-g 实测分别是 8329555968 / 8134332 / 7943 / 7,单位是字节/KiB/MiB/GiB。

w 一行就把「几个人在线、各自在干什么、负载多少」说完了,比 who + ps 方便。load average 三个数是 1/5/15 分钟平均,和 CPU 核数比才有意义(本机 8 核,2.20 属于轻载)。

top 交互界面在无终端环境下没法测,但批处理模式可以,实测 top -b -n 1 的前四行正是原笔记说的结构:

1
2
3
4
top - 18:21:17 up 9 min,  2 users,  load average: 0.88, 0.60, 0.23
Tasks: 37 total, 1 running, 36 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7943.7 total, 6397.7 free, 545.0 used, 1164.8 buff/cache

top 里几个必学的交互键:P 按 CPU 排序、M 按内存、c 显示完整命令行、1 展开每个核、k 杀进程、q 退出。htop 更好用但默认没装(实测)。

ps 的两种写法与实用查询

1
2
3
4
5
6
7
$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 2 18:19 ? 00:00:00 /sbin/init

$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 1.1 0.1 24692 15876 ? Ss 18:19 0:01 /sbin/init

-ef 和 aux 结果几乎一样,差别只在列:-ef 有 PPID(父进程),aux 有 %CPU/%MEM/VSZ/RSS/STAT。找父子关系用 -ef,看资源占用用 aux。

比 ps -ef | grep xxx 更好的写法,实测:

1
2
3
4
5
6
7
8
9
10
11
12
$ ps -C bash -o pid,user,etime,cmd
PID USER ELAPSED CMD
380 root 01:37 -bash
475 loong 01:21 -bash

$ ps -u root -o pid,ppid,cmd --no-headers | head -3
1 0 /sbin/init
2 1 /init

$ ps -eo pid,user,%cpu,%mem,stat,etime,comm --sort=-%mem --no-headers | head -3
240 root 0.0 0.4 Ssl 06:56 unattended-upgr
148 root 0.0 0.3 Ss 06:56 networkd-dispat

pgrep/pidof 直接给 PID,ps -ejH 给进程树,实测:

1
2
3
4
5
6
7
8
$ pgrep -c bash
3
$ pidof bash
1898 475 380
$ ps -ejH | head -3
PID PGID SID TTY TIME CMD
1 1 1 ? 00:00:01 systemd
2 0 0 hvc0 00:00:00 init-systemd(Ub

进程管理:kill 与信号

kill 不是「杀」,是「发信号」。实测起一个后台 sleep 再操作它:

1
2
3
4
5
6
7
8
9
$ sleep 300 &
[1] 2062
$ ps -p 2062 -o pid,ppid,user,stat,cmd --no-headers
2062 1898 loong S+ sleep 300

$ kill -l 15; kill -l 9; kill -l 1
TERM
KILL
HUP
信号 编号 行为 实测
TERM 15 默认信号,请求进程自己收尾(可被捕获/忽略) kill 2062 后进程消失
KILL 9 内核直接回收,进程无法捕获,没有清理机会 kill -9 2068 后 ps -p 查不到
HUP 1 挂断;对守护进程的习惯用法是「重载配置」 —
INT 2 相当于 Ctrl-C —
STOP 19 暂停(不可捕获) 实测 STAT 变成 T+
CONT 18 继续 实测 STAT 回到 S+
QUIT 3 退出并产生 core dump —

STOP/CONT 这组实测最有意思:

1
2
3
4
$ kill -STOP 2075; ps -p 2075 -o stat=
T+
$ kill -CONT 2075; ps -p 2075 -o stat=
S+

先 STOP 再 CONT 是「冻结一个进程」的标准手法,比直接 -9 温和得多(比如临时按住一个失控的程序)。

pkill 按名字/命令行杀,实测:

1
2
3
$ sleep 200 &
$ pkill -f "sleep 200" && echo 命中
命中

-f 表示匹配完整命令行(不加 -f 只匹配进程名,15 字符以上还会截断)。用完记得 pkill -f 有可能误伤自己,写模式时先用 pgrep -f 看一眼。

STAT 列的字母含义(实测本机统计):

1
2
3
$ ps -eo stat= | cut -c1 | sort | uniq -c
2 R
38 S

R 运行/就绪、S 可中断睡眠、D 不可中断睡眠(等 IO,常见于磁盘卡住)、T 停止、Z 僵尸(子进程退出但父进程没回收)。Z 进程 kill -9 也杀不掉,只能处理它的父进程。

nice 调优先级,实测可用(值越大越谦让,范围 -20 到 19):

1
2
3
4
$ nice -n 10 sleep 1 &
$ command -v nice renice
/usr/bin/nice
/usr/bin/renice

vmstat 装了,iostat/sar/dstat/htop 都没装(属于 sysstat、htop 包,要用得 apt install)。

systemd:管服务的正确姿势

原笔记用 service ssh start|stop|restart 控制服务。实测在 Ubuntu 26.04 上,service 只是个包装脚本(/usr/sbin/service 开头注释写着「A convenient wrapper for the /etc/init.d init scripts」),它把命令转给 systemctl,所以两种写法都能用,但该学的是 systemctl。

以 cron 为例,五个必学命令,全部实测:

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
$ systemctl status cron --no-pager | head -6
● cron.service - Regular background program processing daemon
Loaded: loaded (/usr/lib/systemd/system/cron.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-11-03 18:19:36 CST; 6min ago
Docs: man:cron(8)
Main PID: 142 (cron)
Tasks: 1 (limit: 9520)

$ systemctl cat cron.service | head -12
# /usr/lib/systemd/system/cron.service
[Unit]
Description=Regular background program processing daemon
Documentation=man:cron(8)
After=remote-fs.target nss-user-lookup.target

[Service]
EnvironmentFile=-/etc/default/cron
ExecStart=/usr/sbin/cron -f -P $EXTRA_OPTS
Restart=on-failure

$ systemctl show cron.service -p ExecStart -p Restart -p WantedBy
WantedBy=multi-user.target
Restart=on-failure
ExecStart={ path=/usr/sbin/cron ; argv[]=/usr/sbin/cron -f -P $EXTRA_OPTS ; ... }

$ systemctl list-dependencies cron.service | head -5
cron.service
● ├─system.slice
● └─sysinit.target
○ ├─apparmor.service

status 那行里的 enabled / active (running) 是两个不同维度:enabled = 开机自启,active = 现在在跑。四种组合都合法,最常见的困惑是「服务在跑但重启后没了」= active + disabled。

启停与开关机自启,实测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ systemctl stop cron  && systemctl is-active cron
inactive
$ systemctl start cron && systemctl is-active cron
active
$ systemctl restart cron && systemctl is-active cron
active
$ systemctl reload cron
Failed to reload cron.service: Job type reload is not applicable for unit cron.service.
$ systemctl reload-or-restart cron && echo 成功
成功

$ systemctl is-enabled cron
enabled
$ systemctl disable cron
Synchronizing state of cron.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
Executing: /usr/lib/systemd/systemd-sysv-install disable cron
$ systemctl is-enabled cron
disabled
$ systemctl enable cron && systemctl is-enabled cron
enabled

三个要点:

  • 不是所有服务都支持 reload(单元文件里得有 ExecReload)。cron 不支持,所以要用 reload-or-restart。
  • enable 只是建软链接(把单元挂进 /etc/systemd/system/multi-user.target.wants/),不会立刻启动;想「启用并马上启动」用 systemctl enable --now 服务。
  • disable 那两行输出说明白了一件事:systemd 会同步 SysV 脚本(/etc/init.d/cron 确实还在,3103 字节,#!/bin/sh 开头)。这就是《Linux 入门》里说「/etc/rc5.d 还只是兼容层」的同一个机制。

自己改了单元文件(/etc/systemd/system/*.service)之后必须:

1
$ systemctl daemon-reload

没做这一步的话,systemctl 会提醒你,实测看到过这条警告:Warning: The unit file, source configuration file or drop-ins of cron.service changed on disk. Run 'systemctl daemon-reload' to reload units.

日志:journalctl 与 /var/log

systemd 的日志进 journald,传统服务的日志还是写文件,两套并存,实测本机都有:

1
2
3
4
5
6
7
8
9
10
11
12
$ journalctl --no-pager -u cron -n 3
Nov 03 18:25:51 Y9000P systemd[1]: Stopped cron.service - Regular background program processing daemon.
Nov 03 18:25:51 Y9000P systemd[1]: Started cron.service - Regular background program processing daemon.
Nov 03 18:25:51 Y9000P cron[10609]: (CRON) INFO (pidfile fd = 3)

$ ls -l /var/log/syslog /var/log/auth.log
-rw-r----- 1 syslog adm 27130 Nov 3 18:23 /var/log/auth.log
-rw-r----- 1 syslog adm 567422 Nov 3 18:25 /var/log/syslog

$ tail -2 /var/log/syslog
2026-11-03T18:25:51.997854+08:00 Y9000P systemd[1]: Reloading...
2026-11-03T18:25:52.100887+08:00 Y9000P systemd[1]: Reloading finished in 102 ms.

journalctl 的常用过滤,实测都可用:

命令 作用
journalctl -u 服务名 只看这个服务(排障第一招)
journalctl -u 服务 -f 实时跟踪
journalctl -b / -b -1 本次 / 上次开机的日志
journalctl --since "10 min ago" / --since -10min 按时间
journalctl -p err 只看 error 及以上级别
journalctl --disk-usage 占了多少磁盘(实测 48M)
journalctl --vacuum-size=100M 清理到指定大小
journalctl -xe 最后 20 行 + 崩溃现场,带 --no-pager 更好用

-p err -b 实测直接暴露了这台机器上两个真实故障(ubuntu-insights-collect.service: Failed to set up mount namespacing、WSL 显卡驱动的 Ioctl failed: -2)——这就是它的价值。

内核消息还能从 dmesg 看(dmesg | tail),它和 journal 是同一份数据的两个入口。

rsyslog.service 在跑(实测 list-units 里有),所以 /var/log/syslog、/var/log/auth.log 这些传统文件仍然有;它们的权限是 -rw-r----- syslog adm,普通用户读不了,要 sudo 或把自己加进 adm 组。轮转由 /etc/cron.daily/logrotate 和 logrotate.timer 负责(实测两个都在)。

定时任务:cron 与 systemd timer

crontab 的字段顺序是分 时 日 月 周 命令,实测写入再读回:

1
2
3
4
5
6
7
8
$ crontab -l
no crontab for root
$ echo "5 0 * * * /usr/bin/true" | crontab -
$ crontab -l
5 0 * * * /usr/bin/true
$ crontab -r # 清空当前用户的 crontab
$ crontab -l
no crontab for root
位置 字段 范围 例子
1 分 0-59 5 0 * * * = 每天 00:05
2 时 0-23 0 3 * * * = 每天 03:00
3 日 1-31 */10 * * * * = 每 10 天
4 月 1-12 —
5 周 0-7(0 和 7 都是周日) 0 9 * * 1-5 = 工作日 9 点
6 命令 — 绝对路径,或自己 cd

系统级的 /etc/crontab 多一个用户名字段(实测文件头注释就写着「These files also have username fields, that none of the other crontabs do」),SHELL=/bin/sh 也在那里定义。

/etc 下和定时有关的目录,实测内容:

1
2
3
4
$ ls /etc/cron.d
e2scrub_all
$ ls /etc/cron.daily
apport apt-compat dpkg logrotate man-db

/etc/cron.hourly、cron.weekly、cron.monthly 也是同一族,往里放一个可执行脚本就会被定时执行。

新系统更推荐 systemd timer(本机实测在跑的一堆):

1
2
3
4
5
$ systemctl list-timers | head -6
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-11-03 18:34:36 CST 8min - - systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Wed 2026-11-04 00:00:00 CST 5h 33min Tue 2026-11-03 16:31:04 CST - dpkg-db-backup.timer dpkg-db-backup.service
Wed 2026-11-04 00:19:39 CST 5h 53min Tue 2026-11-03 16:31:04 CST - logrotate.timer logrotate.service

timer 比 cron 好在:能看下一次/上一次执行时间、日志进 journal、支持依赖和随机延迟(RandomizedDelaySec)。日常结论:给用户用的定时任务写 crontab,给系统组件写的用 timer,别混。

WSL 的一个坑:wsl --shutdown 之后 cron 里错过的任务不会补跑(VM 停了就是停了),所以别把重要定时任务放在 WSL 里。

防火墙:ufw 全流程实测

ufw = Uncomplicated Firewall,默认没装(实测 command -v ufw 为空),装它:sudo apt install ufw(版本 0.36.2-9build1)。下面是在我这台机器上完整跑过一遍的流程,最后已经关掉恢复原状。

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
$ sudo ufw status
Status: inactive

$ sudo ufw enable
Firewall is active and enabled on system startup

$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

$ sudo ufw allow 5900
Rule added
$ sudo ufw allow 8080/tcp
Rule added (v6)
$ sudo ufw allow from 192.168.1.50 to any port 3306
Rule added
$ sudo ufw limit 22/tcp
Rule added

$ sudo ufw status numbered
Status: active

To Action From
-- ------ ----
[ 1] 5900 ALLOW IN Anywhere
[ 2] 8080/tcp ALLOW IN Anywhere
[ 3] 3306 ALLOW IN 192.168.1.50
[ 4] 22/tcp LIMIT IN Anywhere
[ 5] 5900 (v6) ALLOW IN Anywhere (v6)

$ sudo ufw delete allow 5900
Rule deleted
Rule deleted (v6)

$ sudo ufw disable
Firewall stopped and disabled on system startup

必须记住的几条:

  • ufw enable 之前一定要先 ufw allow OpenSSH(或 22/tcp),否则远程连接立刻断掉,只能回到机房/控制台救。这是新手最常见的自断后路操作。
  • 默认策略 deny (incoming), allow (outgoing):进来的全拒、出去的全放,这正是服务器要的。
  • 每条规则会自动生成 v4 和 v6 两份,所以 status numbered 里编号会跳。
  • ufw delete allow 5900 按完整规则文本删;按编号删(sudo ufw delete 1)会交互确认,脚本里别用。
  • ufw limit 是防暴力破解用的(限制连接频率)。
  • 原笔记那句 sudo ufw alllow 5900 是拼写错误。

「底层依赖 iptables」这句实测成立——iptables -L -n 里能看到 ufw 建的一整串链:

1
2
3
4
5
6
$ iptables -L -n | head -6
Chain INPUT (policy ACCEPT)
target prot opt source destination
ufw-before-logging-input all -- 0.0.0.0/0 0.0.0.0/0
ufw-before-input all -- 0.0.0.0/0 0.0.0.0/0
ufw-after-input all -- 0.0.0.0/0 0.0.0.0/0

规则本身存在 /etc/ufw/(实测有 before.rules、after.rules、user.rules、sysctl.conf、ufw.conf、applications.d)。iptables 1.8.11-2ubuntu3 是随 ufw 一起装上的。

云上还有一层安全组在实例外面,ufw 放行但安全组没放行照样连不上——排查端口问题时两层都要看。

远程连接:SSH

「Ubuntu 默认没有 ssh 服务」这句今天仍然成立,实测:sshd 未安装时 systemctl status ssh 报 Unit ssh.service could not be found。装它:

1
2
3
4
5
6
7
8
$ sudo apt install openssh-server        # OpenSSH_10.2p1 Ubuntu-2ubuntu3.8, OpenSSL 3.5.5
$ ls /usr/lib/systemd/system/ssh*
/usr/lib/systemd/system/ssh-access.target
/usr/lib/systemd/system/ssh.service
/usr/lib/systemd/system/ssh.socket
/usr/lib/systemd/system/sshd-keygen.service
/usr/lib/systemd/system/sshd.service
/usr/lib/systemd/system/sshd@.service

注意 ssh.socket 和 sshd@.service:新版 Ubuntu 默认用 socket 激活,systemctl start ssh 之后 status 里会出现 TriggeredBy: ● ssh.socket。这有两个实际后果:

1
2
3
$ systemctl is-enabled ssh ssh.socket
disabled
enabled

想彻底关掉 ssh,必须连 socket 一起停,否则 22 端口还被 systemd 占着(我实测踩过,ss -lntp | grep :22 仍显示 systemd,pid=1):

1
2
3
4
$ sudo systemctl stop ssh ssh.socket
$ sudo systemctl disable ssh ssh.socket
$ ss -lntp | grep :22 || echo 22 端口空闲
22 端口空闲(ssh 已恢复成未启用)

服务端配置在 /etc/ssh/sshd_config,实测关键行都默认注释着:

1
2
3
4
5
$ grep -nE "^#?(PermitRootLogin|PasswordAuthentication|Port|PubkeyAuthentication)" /etc/ssh/sshd_config
35:#Port 22
54:#PermitRootLogin prohibit-password
59:#PubkeyAuthentication yes
78:#PasswordAuthentication yes

所以原笔记那条「设置 root 远程连接」的做法(把 #PermitRootLogin prohibit-password 改成 PermitRootLogin yes)语法上没问题,但 yes 是危险选项:它允许 root 用密码登录,等于把最高权限账户暴露在爆破之下。生产环境的正确配置是:

1
2
3
PermitRootLogin prohibit-password     # 只允许密钥登录 root
PasswordAuthentication no # 关掉密码登录,全用密钥
Port 22 # 想低调可以换端口,但换端口不是安全措施

改 sshd 配置前,务必先跑一次语法检查(实测这条能救命):

1
2
$ sudo sshd -t; echo $?
0

sshd -t 只检查配置不启动服务,systemd 单元里也把它作为 ExecStartPre——配置写错服务根本起不来。改完重载:

1
$ sudo systemctl restart ssh        # 老写法 service ssh restart 实测等价

不要断开当前会话去验证新配置,另开一个终端测,连不上还能退回旧会话。

服务端起来之后,客户端连接实测:

1
2
3
4
$ ssh 用户@主机
$ ssh -o BatchMode=yes -o PasswordAuthentication=no 127.0.0.1 true
Warning: Permanently added '127.0.0.1' (ED25519) to the list of known hosts.
root@127.0.0.1: Permission denied (publickey,password).

第二行虽然被拒绝,但 Permission denied (publickey,password) 恰恰证明 TCP 连接、版本协商、认证方式都通了——排查 ssh 问题时,「Permission denied」是服务端活着,「Connection refused / timed out」才是网络或服务的问题。

不登录也能验证服务在不在,直接读横幅:

1
2
$ timeout 4 bash -c 'exec 3<>/dev/tcp/127.0.0.1/22; head -1 <&3'
SSH-2.0-OpenSSH_10.2p1 Ubuntu-2ubuntu3.8

配套命令本机都有:scp、sftp、ssh-keygen。传文件优先 rsync(实测在,/usr/bin/rsync),它断了能续传、只传差异:

1
rsync -avz --progress 本地目录/ 用户@主机:远端目录/

密钥登录的正确流程(比密码安全一个量级):

1
2
3
ssh-keygen -t ed25519 -C "备注"          # 本机生成密钥对
ssh-copy-id 用户@主机 # 把公钥推过去
ssh 用户@主机 # 之后免密

原笔记提到 putty/shell 工具——Windows 10 1809 起系统就自带 OpenSSH 客户端(PowerShell 里直接敲 ssh),Windows 11 自然也有,不再需要 PuTTY;想要图形界面可以用 Windows Terminal 或 VS Code 的 Remote-SSH。

装软件:apt、dpkg、源码三件套

apt:优先用它

apt 和 apt-get 是两个不同的可执行文件(实测大小 23072 与 59944 字节),命令基本一致;apt 的输出对人友好,apt-get 更稳定,脚本里用 apt-get(apt 会警告「does not have a stable CLI interface」)。

命令 作用 实测
apt update 只刷新索引,不装东西 必须最先跑
apt install 包 安装(自动解决依赖) 装 hello 时自动带上 libc6
apt remove 包 卸载但留配置 之后 dpkg -l 显示 rc 状态
apt purge 包 卸载并删配置 之后 dpkg -l 查不到
apt upgrade 升级已装的包 实测模拟显示「131 not upgraded」
apt -s install 包 模拟,不真动手 排障/预演神器
apt list --installed 列出已装(实测 581 行) dpkg -l | grep -c '^ii' = 580
apt search 词 / apt show 包 找包 / 看详情 —
apt download 包 只下 .deb 不装 实测下到 26068 字节
apt clean 清下载缓存 实测 /var/cache/apt/archives 从 54M 变 8K
apt autoremove 删掉不再需要的依赖 我用 -s 模拟,没真删
apt -f install 修复断掉的依赖 dpkg 装不上时的解药
apt-cache policy 包 看候选版本与来源 见下

软件源在 Ubuntu 24.04 之后换成了 deb822 格式,实测本机 /etc/apt/sources.list.d/ubuntu.sources:

1
2
3
4
5
Types: deb
URIs: http://archive.ubuntu.com/ubuntu/
Suites: resolute resolute-updates resolute-backports
Components: main universe restricted multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

不再是老教程里 sources.list 那种一行一条的写法,改源时要按这个格式改(国内加速常换成 USTC/清华镜像)。

apt-cache policy 是判断「为什么装不上/装的哪个版本」的第一命令,实测:

1
2
3
4
5
6
7
$ apt-cache policy hello
hello:
Installed: (none)
Candidate: 2.10-5build1
Version table:
2.10-5build1 500
500 http://archive.ubuntu.com/ubuntu resolute/main amd64 Packages

dpkg:只管本地 .deb

命令 作用 实测
dpkg -i 包.deb 安装本地包,不管依赖 装 hello 成功,退出码 0
dpkg -r 包名 卸载(保留配置) 之后 dpkg -l 包名 报 no packages found
dpkg --purge 包名 卸载并删配置 —
dpkg -l 列出包(grep -c '^ii' = 580) —
dpkg -L 包名 这个包装了哪些文件 /usr/bin/hello、/usr/share/doc/hello/
dpkg -S 文件路径 反查文件属于哪个包 dpkg -S /usr/bin/hello → hello
dpkg-deb -c 包.deb 不安装也能看包内容 列出 ./usr/bin/hello 及权限
dpkg-deb -I 包.deb 看包的元数据(依赖、描述) Depends: libc6 (>= 2.38)

两个实测到的坑:

  • dpkg -S /bin/ls 报 no path found matching pattern /bin/ls——因为 dpkg 数据库里记的是 /usr/bin/ls(/bin 只是软链接,见《Linux 目录结构及 VIM》)。反查时要用真实路径。
  • dpkg -i 遇到缺依赖会失败并且不会自动补,此时跑 sudo apt -f install 让它把依赖装上再继续。

WPS 那个例子(dpkg -i wps-office_*.deb 再手动放字体)思路仍然成立,只是字体目录建议放 /usr/share/fonts/truetype/ 后执行 fc-cache -f。

源码安装:configure → make → make install

原笔记的三步流程正确,但少了一个致命前置。实测:

1
2
3
4
5
6
$ gcc /tmp/t.c -o /tmp/t
/tmp/t.c:1:10: fatal error: stdio.h: No such file or directory
1 | #include <stdio.h>
| ^~~~~~~~~
$ ls /usr/include/stdio.h
ls: cannot access '/usr/include/stdio.h': No such file or directory

只装 gcc 是编不了东西的——标准头文件属于 libc6-dev,最小化系统里没有。当时 ./configure 的报错也因此是:

1
configure: error: C compiler cannot create executables

正确做法是一条命令装齐:

1
2
3
$ sudo apt install build-essential        # 12.12ubuntu2.26.04.2,带上 gcc/g++/make/libc6-dev
$ gcc /tmp/t.c -o /tmp/t && /tmp/t
ok

装完之后完整跑通了原笔记的三步(用 GNU hello 2.12.1 源码包,1033297 字节):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ tar -zxf hello-2.12.1.tar.gz
$ cd hello-2.12.1
$ ./configure --prefix=/tmp/mk/pfx # 只指定安装位置
config.status: creating config.h
$ grep -E "^prefix" Makefile
prefix = /tmp/mk/pfx # configure 的作用:把选项写进 Makefile
$ make
gcc -g -O2 -o hello src/hello.o ./lib/libhello.a
$ make install
$ find /tmp/mk/pfx -maxdepth 2
/tmp/mk/pfx/bin
/tmp/mk/pfx/bin/hello
/tmp/mk/pfx/share/man
/tmp/mk/pfx/share/info
$ /tmp/mk/pfx/bin/hello
Hello, world!

make 的增量特性也实测到了:

1
2
3
4
5
$ make
make: Nothing to be done for 'all'. # 没改动就不重编
$ touch hello.c
$ make
gcc -O2 -o hello hello.c # 源文件时间戳新了才重编

以及 Makefile 的语法要求(实测 cat -A 确认):每条命令前必须是制表符 ^I,不是空格,否则 make 报 missing separator。

源码安装的最后一步很重要:它没有任何包管理器记录,dpkg -l 查不到、apt upgrade 不会更新它、卸载只能 rm -rf /tmp/mk/pfx(实测)。这就是为什么能用 apt 就别用源码装。

原笔记的 nginx 例子(--prefix=/usr/local/nginx、./nginx 启动、看 html 目录)流程正确,但我没在本机复现(下载 nginx 源码 + 装 pcre/zlib 开发库成本较高),且现代做法是用 systemctl 管 nginx 而不是直接跑 ./nginx。另外它写的依赖 libpcre3-dev 在新版 Ubuntu 上已改名,实测源里是 libpcre2-dev。

命令串联的三条,实测行为如原笔记所述:

1
2
3
cmd1; cmd2      # 无条件依次执行,cmd1 失败也继续
cmd1 && cmd2 # cmd1 成功(退出码 0)才执行 cmd2 —— make && make install
cmd1 || cmd2 # cmd1 失败才执行 cmd2

常见坑速查

  • 用 zip -l 列内容:那是换行符转换选项,列内容用 unzip -l 或 zipinfo(实测报 Nothing to do!)。
  • 以为 gzip 目录 能压文件夹:单文件压缩工具遇到目录就报错,先 tar 再压缩。
  • tar 的 -C 写成 -c:-c 是创建,-C 才是切换目录(实测 --help 里两行分开)。
  • --exclude 放在文件名后面:必须放前面,实测报 requires an argument。
  • -f 后面忘了紧跟文件名:tar -cvf *.tar.gz dir 会把第一个匹配到的文件当成包名。
  • 管道给 rm/cp/mv:这些命令不读标准输入,要用 xargs(实测 echo a | rm 报 missing operand)。
  • find | xargs rm 不带 -print0/-0:文件名里有空格会被拆成两个参数(实测)。
  • 2>&1 > file:顺序错了,错误还是去了屏幕(实测),要写 > file 2>&1。
  • > file 当追加用:> 会清空原文件(实测 OLD 直接没了),追加用 >>。
  • ping 不加 -c:会一直发,看起来像卡死。
  • 想用 ifconfig/netstat:新系统默认没有 net-tools,改用 ip 和 ss。
  • 照抄 server networking restart:这命令不存在,用 systemctl。
  • free 只看 free 列:要看 available,buff/cache 是可回收的。
  • kill -9 当默认手段:先 kill(TERM)给它收尾机会,-9 会让程序来不及清理。
  • 改了 systemd 单元不 daemon-reload:会收到「changed on disk, run systemctl daemon-reload」警告(实测)。
  • 以为 enabled 就等于在跑:enabled 是开机自启,active 才是当前运行。
  • ufw enable 前没放行 SSH:远程连接立刻断,只能回控制台(务必先 ufw allow OpenSSH)。
  • 只 systemctl stop ssh:ssh.socket 还占着 22 端口,要 stop ssh ssh.socket(实测)。
  • 把 PermitRootLogin yes 当常规配置:等于开放 root 密码爆破面,用 prohibit-password + 密钥。
  • 改完 sshd 配置直接重启并断开当前会话:正确顺序是先 sshd -t 检查、另开终端验证、再断开。
  • 只装 gcc 就编译:缺 libc6-dev 会报 stdio.h: No such file or directory(实测),装 build-essential。
  • 源码装完忘了装在哪:没有包管理器记录,务必用 --prefix 指定独立目录方便删除。
  • apt-get autoremove 手快:它会删依赖,先用 apt-get -s autoremove 模拟看清单。

练习

  1. 造 200 个小文件,分别用 zip -r、tar -cf、tar -czf、tar -cJf 打包,记录四者体积;再用 tar -tzf 和 unzip -l 各列一次内容。
  2. 写一条命令完成:把 /var/log 下所有 .log 文件的路径列表喂给 wc -l,分别用管道、xargs、find -exec ... + 三种写法实现,并解释哪种在文件极多时最稳。
  3. 用重定向做一个「运行日志」:执行一段脚本,让它的正常输出和错误都进 run.log,同时屏幕上还能看到(提示:tee 与 2>&1 的组合)。
  4. 找出这台机器上所有处于 LISTEN 的 TCP 端口和对应进程,用 ss 和 lsof 各做一次,比较输出差别;再解释为什么 netstat 显示 0.0.0.0:22 的进程是 PID 1。
  5. 给 cron 服务做一次完整的生命周期演练:status → stop → is-active → start → restart → reload(观察报错并解释原因)→ disable → enable → daemon-reload,把每一步的输出记下来。
  6. 用 journalctl 找出本机最近一次失败启动的服务及其错误日志(提示:-p err -b),并说明你会去哪三个地方继续查。
  7. 写一条 crontab:每天 02:30 执行 /usr/bin/true,用 crontab -l 验证,最后 crontab -r 清掉。再回答:/etc/crontab 和用户 crontab 的字段差别是什么?
  8. 在 WSL 上装 openssh-server,用 sshd -t 检查配置,systemctl start ssh 后用 ss -lntp 和读横幅两种方式确认服务可用,然后用 ssh-keygen + ssh-copy-id 配好免密登录自己,最后把 ssh 停掉并 disable(含 ssh.socket)。
  9. 开一个 ufw:先 ufw allow OpenSSH,再 ufw enable,加三条规则(端口、from ... to any port ...、limit),用 status numbered 检查,删掉一条,最后 ufw disable。全程记录输出。
  10. 只装 gcc 编一个 hello.c,观察报错;再用 dpkg -S 查出缺的是哪个包,apt install build-essential 补齐后跑通 configure --prefix → make → make install,最后用 rm -rf 卸载并说明源码安装为什么必须自己记住安装位置。

参考