Unix/Linux 工具系统
Unix/Linux 工具系统是一套遵循 "do one thing well"(做好一件事)哲学的软件工具集,通过管道(pipe)将简单工具组合成强大的数据处理流水线。从 GNU Coreutils 的 100+ 个基础命令,到 grep/sed/awk 文本处理三剑客,再到 find/xargs/locate 文件定位系统,构成了 Unix/Linux 平台的基石。
1. 概述
Unix 工具系统的设计可追溯到 1970 年代 Bell Labs 的 Unix V1,Ken Thompson 的 pipe 概念彻底改变了软件组合方式。今天的 Linux 工具系统由多个 GNU 项目共同构成,涵盖文件操作、文本处理、进程管理、网络通信等所有基础领域。
| 工具包 | 项目 | 核心工具 |
|---|---|---|
| 基础文件操作 | GNU Coreutils | ls, cp, mv, rm, cat, mkdir, chmod, chown |
| 文本处理 | GNU Grep / Sed / Gawk | grep, sed, awk, tr, cut, sort, uniq |
| 文件查找 | GNU Findutils | find, locate, xargs, updatedb |
| Shell | Bash / Zsh / Fish | 管道符, 重定向, 变量, 控制流 |
| 进程管理 | procps-ng | ps, top, kill, nice, renice |
| 网络诊断 | net-tools / iproute2 | ping, netstat, ss, curl, wget |
| 压缩归档 | GNU Tar / Gzip / Info-ZIP | tar, gzip, bzip2, unzip |
| 编译工具链 | GCC / Binutils | gcc, ld, as, objdump, nm, strip |
2. Unix 哲学
Unix 工具系统的设计建立在几条核心原则上,这些原则由 Doug McIlroy、Ken Thompson、Dennis Ritchie 等人在 Bell Labs 提出,至今仍深刻影响着软件开发:
- 每个程序只做一件事,并把它做好 — 不要在一个程序中塞入所有特性
- 期待每个程序的输出成为另一个程序的输入 — 避免人机交互,支持流水线组合
- 尽早设计测试,甚至先于代码 — 快速原型,尽早失败
- 使用工具而非人力来解决重复劳动 — 自动化的自动化
- 一切皆文件 — 数据、设备、进程都以统一的文件描述符接口访问
ls -l | grep "\.c$" | wc -l — 三个独立的工具,通过管道串联,实现了"统计当前目录下 C 源文件数量"的功能。每个工具都不知道自己处于管道中。
第一部分:行为图(系统"做什么")
2.1 用例图 — 用户与工具系统的交互场景
2.2 活动图 — 管道数据处理流程
以下活动图展示了一个典型的"日志分析"管道:从读取文件 → 过滤 → 提取 → 排序 → 统计 → 输出的完整数据流过程。
2.3 序列图 — Shell 协调工具执行
Shell 如何解析命令行、创建管道文件描述符、fork 子进程、执行工具并回收结果。
2.4 状态机图 — 进程生命周期
一个工具程序从被 shell 启动到结束的完整状态变化。
第二部分:结构图(系统"由什么组成")
3.1 组件图 — 工具生态系统架构
3.2 包图 — 工具功能分类
3.3 类图 — 文件描述符与 I/O 抽象
Unix "一切皆文件" 理念的核心:文件描述符、管道、文件、套接字共享统一的 I/O 接口。
3.4 部署图 — 工具在系统中的位置
第三部分:工具目录
4. 管道与过滤器模式
管道是 Unix 工具系统最核心的"胶水"。在 Linux 内核中,管道通过 pipe() 系统调用创建一对文件描述符(读端和写端),Shell 用 dup2() 将进程的标准输出/输入重定向到管道的对应端。
5. GNU Coreutils — 基础命令集
GNU Coreutils 是 Linux 系统上最基础、使用最频繁的命令集,包含约 100 个工具,按功能可分为以下类别:
| 类别 | 工具 | 功能 |
|---|---|---|
| 文件输出 | cat, tac, nl, od, base64 | 输出文件内容 |
| 格式化 | fmt, pr, fold | 格式化文本 |
| 文件部分 | head, tail, split, csplit | 输出文件的部分内容 |
| 摘要 | wc, sum, cksum, md5sum, sha256sum | 统计和校验 |
| 排序操作 | sort, shuf, uniq, comm, ptx, tsort | 排序文件操作 |
| 字段操作 | cut, paste, join | 列/字段操作 |
| 字符操作 | tr, expand, unexpand | 字符转换 |
| 目录列表 | ls, dir, vdir, dircolors | 目录和文件信息 |
| 基本操作 | cp, dd, install, mv, rm, shred | 复制/移动/删除 |
| 特殊文件 | ln, mkdir, mkfifo, mknod, rmdir | 创建链接/目录/FIFO |
| 文件属性 | chown, chgrp, chmod, touch | 修改文件属性 |
| 磁盘空间 | df, du, stat, sync, truncate | 文件系统空间 |
| 文本输出 | echo, printf, yes | 打印文本 |
| 条件 | test ( [ ), expr, true, false | 条件判断 |
| 重定向 | tee | 多路输出 |
| 文件名 | basename, dirname, realpath, mktemp | 路径操作 |
| 工作上下文 | pwd, stty, printenv, tty | 当前环境 |
| 用户信息 | id, logname, whoami, groups, who | 用户身份 |
| 系统上下文 | date, uname, hostname, uptime, nproc | 系统信息 |
| 命令调用 | chroot, env, nice, nohup, timeout | 修改执行上下文 |
| 进程控制 | kill, sleep | 信号/延时 |
| 数值运算 | factor, numfmt, seq | 数值处理 |
6. 文本处理三剑客 + 配套工具
grep、sed、awk 是 Unix 文本处理的核心三件套,配合 sort、uniq、cut、tr 等工具构成强大的文本数据处理流水线。
| 工具 | 项目 | 定位 | 核心功能 |
|---|---|---|---|
grep | GNU Grep | 行过滤器 | 正则匹配,打印匹配行;支持 BRE/ERE/PCRE |
sed | GNU Sed | 流编辑器 | 逐行编辑:替换、删除、插入、打印;支持脚本 |
awk | GNU Awk (gawk) | 列处理器 | 按字段处理:模式匹配 + 操作,内置算术和字符串函数 |
sort | GNU Coreutils | 排序 | 多字段排序、数值排序、去重、稳定排序 |
uniq | GNU Coreutils | 去重/计数 | 去除或报告连续重复行;计数/仅重复/仅唯一 |
cut | GNU Coreutils | 列提取 | 按字节/字符/分隔符提取列 |
tr | GNU Coreutils | 字符翻译 | 替换/删除/压缩字符 |
wc | GNU Coreutils | 计数 | 统计行数/词数/字节数 |
tee | GNU Coreutils | 分流 | 同时输出到文件和管道下游 |
comm | GNU Coreutils | 集合运算 | 比较两个有序文件:交集/差集/并集 |
diff | GNU Diffutils | 差异 | 逐行比较两个文件 |
patch | GNU Patch | 补丁 | 应用 diff 输出到文件 |
7. 文件查找工具 (GNU Findutils)
| 工具 | 功能 |
|---|---|
find | 实时遍历文件系统,按名称/类型/大小/时间/权限等条件查找文件,支持对结果执行操作(-exec, -delete) |
locate | 基于预建的文件名数据库快速搜索,速度远快于 find,但信息可能过时 |
xargs | 将标准输入转换为命令参数,解决命令行参数过长问题;支持 -P 并行执行 |
updatedb | 更新 locate 使用的文件名数据库 |
8. Shell — 工具编排器
Shell 本身就是一个强大的工具编排系统,提供:
- 管道 (
|):连接进程的标准输出和标准输入 - 重定向 (
> >> < <<):将 I/O 流指向文件 - 变量/环境变量:存储和传递配置信息
- 控制流 (
if/for/while/case):条件判断和循环 - 命令替换 (
$()):将命令输出作为另一个命令的参数 - 进程替换 (
<() >()):用文件描述符替代临时文件 - Here Document/Here String:内联多行文本重定向
- 子 Shell (
()) 和命令组 ({ }):聚合命令的输出
9. 进程与网络工具
| 类别 | 工具 | 功能 |
|---|---|---|
| 进程查看 | ps, pgrep, pstree, top, htop, pidstat, lsof | 查看进程状态、系统负载、进程打开的文件 |
| 进程控制 | kill, killall, pkill, nice, renice | 发送信号,调整优先级 |
| 任务调度 | crontab, at, batch, systemd-timer | 定时/延时任务 |
| 网络连接 | ping, traceroute, ss, netstat, nmap | 网络诊断和连接查看 |
| 数据传输 | curl, wget, rsync, scp, nc (netcat) | 远程文件传输和网络 I/O |
| DNS 查询 | dig, nslookup, host | 域名解析 |
9.1 查看用户进程与进程文件
| 需求 | 命令 | 说明 |
|---|---|---|
| 查看某用户的全部进程 | ps -u <用户名> | 列出该用户名下所有进程 |
| 查看某用户进程(详细) | ps aux | grep <用户名> | 全格式输出后过滤 |
| 只列某用户进程 PID | pgrep -u <用户名> | 只输出 PID,便于脚本处理 |
| 查看某进程打开的所有文件 | lsof -p <PID> | 列出该进程打开的文件/描述符/套接字 |
| 按进程名查看其打开的文件 | lsof -c <进程名> | 匹配命令名过滤 |
| 直接读取内核 fd 目录 | ls -l /proc/<PID>/fd/ | 无需 lsof,查看文件描述符符号链接 |
kthreadd、kworker、ksoftirqd)运行在内核态,本身没有 uid/gid 概念。它们在 ps aux 中显示为 root(uid 0)只是统计上的归属,不代表真实用户。识别方法:命令名以方括号包裹(如 [kworker/0:1]),且无法被普通用户 kill。
9.2 查看进程的线程
| 需求 | 命令 | 说明 |
|---|---|---|
| 统计某进程的线程数量 | ls /proc/<PID>/task/ | wc -l | 直接数内核线程目录,最准确 |
| ps 线程视图计数 | ps -T -p <PID> | wc -l | 减去表头即为线程数 |
| 查看每个线程详情 | ps -Lf -p <PID> | 含 LWP/TID 列,逐线程列出 |
| 交互式查看线程 | top -H -p <PID> | 实时观察各线程 CPU 占用 |
task_struct,区别仅在共享资源(内存、文件描述符等)。每个线程有独立线程号 TID(即 ps -Lf 的 LWP 列),线程组组长(主线程)的 LWP 才等于 PID。
ps 只能看到 M)。用户态线程只能由运行时自身统计:Go 用 runtime.NumGoroutine()/pprof,Java 用 jstack/jcmd Thread.print,Python 用 threading.active_count()。
9.3 线程与协程的分类
注意:Linux 中线程不是子进程。子进程(fork)有独立 PID 和地址空间,与父进程是父子关系;线程(clone/pthread)与主线程共享 PID、内存、fd 表,是同组兄弟关系,仅每个线程有独立 LWP。
| 需求 | 子进程 (fork) | 线程 (clone/pthread) |
|---|---|---|
| PID | 独立新 PID | 与主线程共享 PID (TGID) |
| 地址空间 | 独立副本 (COW) | 共享同一份内存 |
| 文件描述符 | 独立 (fork 时复制) | 共享同一张 fd 表 |
| 关系 | 父子关系 | 同组兄弟关系 |
| 调度实体 | 独立 LWP | 每个线程独立 LWP |
用户态线程 ≠ 协程:协程是用户态线程的子集。所有协程都是用户态线程,但用户态线程不全是协程。
| 层级 | 单元 | 调度者 | 抢占性 | 典型例子 |
|---|---|---|---|---|
| 1 | 进程 (Process) | 内核 | 抢占式 | fork 出的独立程序 |
| 2 | 内核级线程 (LWP) | 内核 | 抢占式 | pthread(1:1) |
| 3 | 用户态线程 (User/Green Thread) | 运行时库 | 可抢占(定时切换) | 早期 Java green threads(N:1) |
| 4 | 协程 (Coroutine) | 程序自身 | 协作式(显式 yield) | Go goroutine、asyncio、Lua coroutine、Python 生成器 |
| 5 | 纤程 (Fiber) | 程序自身 | 协作式 | Boost.Fiber、Windows Fiber |
| 6 | 异步回调/事件循环 | 事件循环 | 非栈式状态机 | Node.js、libuv |
yield 让出、需要可保存/恢复的执行栈。用户态线程可以是抢占式(green thread 靠定时器切换)也可以是协作式(协程)。M:N 混合模型下 N 个用户态线程映射到 M 个内核线程(Go 的 GMP:goroutine → P → M)。
第四部分:用户使用手册
10. 常用组合模式
以下是 Unix 工具系统中最经典的数据处理模式:
| 模式 | 命令示例 | 说明 |
|---|---|---|
| 过滤-排序-去重-计数 | grep ERROR app.log | cut -d' ' -f3 | sort | uniq -c | sort -rn | 从日志中提取错误来源 IP,统计 Top N |
| 查找-遍历-执行 | find . -name "*.tmp" -mtime +7 -delete | 删除 7 天前的临时文件 |
| 查找-xargs 并行 | find . -name "*.jpg" -print0 | xargs -0 -P4 -I{} convert {} -resize 50% small/{} | 并行批量处理图片缩放 |
| 文件内容聚合 | cat *.csv | awk -F, '{sum[$1]+=$2} END{for(k in sum) print k,sum[k]}' | 合并多个 CSV 文件并按键聚合求和 |
| 实时监控 | tail -f /var/log/nginx/access.log | grep " 500 " | 实时监控 HTTP 500 错误 |
| 批量重命名 | for f in *.JPG; do mv "$f" "${f%.JPG}.jpg"; done | 批量修改文件扩展名 |
| tee 分流 | ./long-script.sh | tee output.log | grep ERROR | 同时保存完整日志并实时过滤错误 |
11. I/O 重定向速查
11.1 I/O 重定向是什么
程序默认从 标准输入读数据、往 标准输出写结果。I/O 重定向就是把程序的数据流引向文件或另一个程序,从而组合出任意复杂度的数据处理流程。这是 Unix "管道哲学"的基石:每个工具只做一件事,靠标准输入/输出串起来。
每个进程启动时内核自动分配三个默认文件描述符(编号就是打开文件的句柄):
| 编号 | 名称 | 含义 | 默认指向 |
|---|---|---|---|
0 | stdin(标准输入) | 程序读入数据的通道 | 键盘 |
1 | stdout(标准输出) | 程序正常结果输出的通道 | 终端(屏幕) |
2 | stderr(标准错误) | 程序错误/诊断信息输出的通道 | 终端(屏幕) |
所以重定向语法中的 1、2 就是文件描述符编号:1> 重定向正常输出、2> 重定向错误输出。2>&1 表示把 stderr 合并进 stdout。缺省编号时默认为 stdout,例如 cmd > file 等价于 cmd 1> file。底层实现由内核 pipe() 创建管道、Shell 用 dup2() 将文件描述符重新绑定(见 3.3 节时序图)。
| 语法 | 含义 |
|---|---|
cmd > file | stdout 覆盖写入 file |
cmd >> file | stdout 追加写入 file |
cmd < file | stdin 从 file 读取 |
cmd 2> file | stderr 覆盖写入 file |
cmd 2>&1 | stderr 合并到 stdout |
cmd &> file | stdout + stderr 都写入 file |
cmd1 | cmd2 | cmd1 的 stdout → cmd2 的 stdin |
cmd1 |& cmd2 | cmd1 的 stdout+stderr → cmd2 的 stdin |
cmd <(cmd2) | 进程替换:cmd2 的输出作为 cmd 的"文件"参数 |
cmd << "EOF" | Here Document:内联多行文本作为 stdin |
cmd <<< "string" | Here String:单行字符串作为 stdin |
12. 实战技巧
性能优化
- 用
grep -F(固定字符串)替代正则可提速 10x+ find ... -exec cmd {} +比\;快得多(批量传参)- 用
xargs -P$(nproc)并行化 CPU 密集型任务 - 使用
LC_ALL=C sort回避 locale 开销,排序提速 2-3x - 管道中尽早过滤(grep close to source)减少数据量
安全实践
- 文件名含空格/换行时用
-print0+xargs -0 - 危险操作前用
echo预演命令(如echo rm *.tmp) rm -rf慎用:可用--preserve-root保护根目录- 脚本中引用变量始终加双引号:
"$var" - 密码类信息走 stdin 而非命令行参数(避免 ps 暴露)
调试与排障
set -x打印 Shell 脚本每条执行命令set -e遇错即停;set -u引用未定义变量时报错set -o pipefail管道中任一命令失败则整体失败strace -e trace=file跟踪程序的文件操作time cmd测量命令执行时间cmd | tee /dev/stderr | wc -l在管道中间插入调试输出
可移植性技巧
- 用 POSIX SHELL 语法,避免 bashism(如
[[ ]]→[ ]) - 用
printf代替echo -n(echo 实现差异大) - 用
mktemp创建临时文件而非手动命名 - 用
--分隔选项和参数:rm -- -f - 用
$HOME而非~在脚本中