摘要:本文系统讲解 Linux 系统管理与 Shell 编程的核心知识,涵盖软件安装(APT、dpkg、snap)、进程管理(PID、fork、僵尸与孤儿进程、管道、进程组与会话)、变量作用域与传递,以及 Bash 脚本编程(条件判断、循环、函数、文件锁与调试),并通过实战案例帮助读者掌握自动化运维技能。
目录
第十七章 软件安装与管理:软件中心、APT、dpkg、snap 等安装方式
第十八章 Linux 进程:进程标识、fork/exec、僵尸与孤儿、管道、进程组与会话
第十九章 Linux 变量:环境变量、特殊变量、自定义变量、作用域与 profile
第二十章 Bash Shell 编程:脚本格式、条件判断、循环、函数、文件锁与调试
第二十一章 Shell 脚本实战案例:系统信息报告、批量重命名、菜单脚本、TCP 服务器
第十七章 软件安装与管理
- Ubuntu 软件中心(GUI)
- APT 包管理器(CLI - Command Line Interface 在线安装)
- DPKG - DEB 包安装与卸载(本地软件包安装)
- 源码编译安装(了解)
- snap 包管理器(容器化安装,将来的主推方向)
17.1 软件中心(GUI)
Ubuntu 软件中心提供图形化界面,可搜索、安装、卸载软件。

17.2 APT 包管理器
Advanced Package Tool,Ubuntu 的包管理工具(最常用)。
bash
sudo apt update # 更新软件源
sudo apt install nginx -y # 安装软件
sudo apt remove nginx # 删除软件
sudo apt purge nginx # 彻底删除(含配置)
sudo apt autoremove # 清理无用依赖
apt list --installed | grep python # 查看已安装
sudo apt upgrade # 升级所有包
17.3 更新 Ubuntu 系统中的可用软件列表
/etc/apt/sources.list 记录软件源的地址。
获取列表的过程:
1. 读取配置:读取 /etc/apt/sources.list 文件和 /etc/apt/sources.list.d/ 目录下的所有配置文件。
2. 连接服务器:根据文件中记录的地址,APT 会连接到对应的软件源服务器。
3. 下载清单文件:每个软件源都包含一个名为 Packages.xz(或类似 Packages.gz)的文件。里面列出了该源提供的所有软件包的名字、版本、依赖关系等信息。
4. 更新本地缓存:APT 会将下载到的最新清单数据保存在本地的缓存数据库中:/var/lib/apt/lists。
$ sudo apt update
17.4 软件安装的过程
1. 解析依赖:读取本地的软件包列表缓存,分析待安装软件包需要哪些依赖包(直接依赖和间接依赖),得到完整的安装列表。
2. 检查冲突与建议:检查是否有软件包与要安装的包冲突。
3. 下载软件包:从软件源服务器下载所有需要的 .deb 包,暂存在 /var/cache/apt/archives/ 目录。
4. 调用 dpkg 执行安装:会按照依赖顺序调用 dpkg -i 逐一安装所有 .deb 包。
5. 保留缓存:下载的 .deb 包会保留在 /var/cache/apt/archives/ 中,便于下次重新安装。可通过 apt clean 进行清理。
17.5 apt install安装在线软件包
apt install - 安装在线软件包
sudo apt install [参数] ...
| `-y`/ '--yes' | 自动回答 Yes,跳过确认提示 |
| `-d` | 只下载软件包,不安装 |
| `--reinstall` | 重新安装软件包 |
| `-f` | 修复软件依赖关系(常与 install 配合使用) |
| `-s` | 模拟安装 |
17.6 apt list 查看已安装软件包
$ apt list [软件包]
删除已安装软件包:
bash
sudo apt remove <软件包> # 删除已安装软件包
sudo apt purge <软件包> # 删除已安装软件包,并清除配置文件
17.7 包安装方式(dpkg)
bash
# 安装 .deb 包
sudo dpkg -i package.deb
# 删除
sudo dpkg -r package_name
# 查看已安装
dpkg -l | grep package
17.8 修复依赖
安装 `.deb` 缺依赖时:
bash
sudo apt install -f
实际示例:
bash
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome-stable_current_amd64.deb
17.9 源码编译安装(了解)
从源代码编译安装软件:
bash
# 通用步骤:
wget https://example.com/software-1.0.tar.gz
tar -xzf software-1.0.tar.gz
cd software-1.0
./configure # 检查环境,生成 Makefile
make # 编译
sudo make install # 安装到系统
# 卸载:
cd software-1.0
sudo make uninstall # 仅当 Makefile 支持时
17.10 snap 包管理器
优点:
- 独立运行,不干扰系统:自带依赖,不用担心破坏系统。
- 自动更新:默认会自动更新到新版,保持软件安全。
- 安装简单:无需像 apt 那样手动添加软件源。
缺点:
- 占用空间大:每个 Snap 都包含了其依赖的库文件。
- 启动较慢:首次启动可能慢一些。
snap 安装过程
1. 解析与校验:snap 客户端会先解析命令,通过 snapd 后台服务向 Snap Store(snapcraft.io)查询软件包,校验包的完整性,并确认是否有合适的版本。
2. 下载软件包:将整个 `.snap` 文件下载到本地 `/var/lib/snapd/snaps/` 目录,相当于软件的安装包缓存。
3. 挂载软件包:将 `.snap` 文件作为只读虚拟磁盘,挂载到 `/snap/<软件名>/<版本号>/` 下。
4. 创建隔离环境:为新软件创建严格隔离的沙盒环境,来限制软件能访问的系统资源。
5. 配置并启动:软件在隔离环境中完成初始化配置,然后通过应用菜单或终端命令来启动。
软件相关文件

snap 软件相关命令
bash
snap find <软件> # 在 snap store 中查询软件
sudo snap install <软件> [--classic] # 在线安装 snap 软件包
sudo snap install <本地软件包.snap> --dangerous # 安装本地下载的 snap 软件包
sudo snap remove [<软件>] # 删除 snap 安装的软件包
sudo snap refresh [<软件>] # 更新 snap 安装的软件包
snap list [<软件>] # 查询 snap 安装的软件包列表
snap info [<软件>] # 查看 snap 安装的软件包的详情
第十八章 Linux 进程
- 什么是进程:程序与进程的区别
- 进程的标识:PID / PPID,查看进程的常用命令
- 父进程与子进程:fork、exec、exit、僵尸与孤儿进程
- 变量在进程间的共享与隔离(export)
- 管道 `|`:利用子进程实现命令间的数据流转
- 进程与线程的对比
18.1 什么是进程
进程(Process)= 正在运行的程序(program)的实例。
- **动态性**:程序是磁盘上的文件,进程是加载到内存中正在执行的程序。
- **唯一性**:每个进程都有唯一的 PID,内核靠它来管理进程。
- **资源**:进程拥有独立的地址空间,是资源分配的基本单位。
18.2 程序与进程
程序是静态的,进程是动态的。

18.3 进程的标识:PID 与 PPID
每个进程都有唯一的 PID,并记录父进程 PPID。
bash
ps -ef | head -5 # 进程快照(含 PID / PPID)
echo $$ # 当前 Shell 进程的 PID
echo $PPID # 父进程(Shell)的 PID
echo $! # 最近后台进程的 PID
ps -o pid,ppid,comm # 只看 PID / PPID / 命令名
kill <PID> # 结束指定进程
示例:

18.4 查看进程的常用命令
从快照、实时、进程树等角度查看进程:
| `ps -ef` | 所有进程快照 |
| `ps aux` | 附加 CPU / 内存占用 |
| `top` | 实时刷新(按 q 退出) |
| `pstree -p` | 进程树,展示父子关系 |
| `pgrep -l bash` | 按名称查找进程 PID |
| `kill -9 <PID>` | 强制结束进程 |
18.5 父进程与子进程
进程由另一个进程创建,形成父子关系。

示例:`ps -eo pid,ppid,comm` 的输出:

技巧:$PPID 可随时查看父 Shell 的 PID
18.6 子进程如何被创建:fork 的过程
Shell 执行一条外部命令时,会经历 `fork → exec`:
1. **fork**:复制当前 Shell,产生子进程(新 PID)。
2. **exec**:子进程加载并运行新程序,替换体内内容。
3. **wait**:父进程等待子进程结束并回收。
4. **exit**:子进程退出,返回退出码(`$?` 可见)。
运行外部命令时:
bash
bash script.sh # 在子进程中执行
echo "$$" # 子进程 PID
echo $? # 子进程的退出码
18.7 僵尸进程与孤儿进程
子进程退出后仍然需要父进程回收
僵尸进程(zombie)
- 子先 exit,父未 wait 回收。
- 进程表遗留 `<defunct>` 记录。
- 占用 PID,积累多了会耗尽。
- 处理:父进程及时 wait。
- 当僵尸子进程的父进程退出(或被杀死)后,僵尸子进程将被其他进程收养,进行资源的回收。
孤儿进程(orphan)
- 父先 exit,子进程成为孤儿。
- 由其它进程收养,进行资源的回收。
- 常见场景:后台任务(`&`)。
- 不永久占用资源,会被回收。
18.8 变量在进程间的传递:export
子进程继承父进程的环境变量,但不继承普通 Shell 变量。
bash
x=5 # 普通 Shell 变量
bash -c 'echo $x' # 子进程看不到,输出为空
export x # 导出为环境变量
bash -c 'echo $x' # 子进程继承,输出 5
env | grep '^x=' # 在环境变量中可见
环境变量的情况 示例:

普通变量的情况 示例:

18.9 变量在进程间的隔离
子进程修改自己的变量,不会影响父进程。
bash
x='父'; bash -c 'x=子; echo $x' # 子进程中修改
echo $x # 父进程仍为“父”,不受影响
source cfg.sh # 当前进程执行(可影响父)
./cfg.sh # 子进程执行(不影响父)

结论:进程内存独立,需靠环境变量 / 文件 / 管道互通。
18.10 管道 `|` 的作用
管道将左边命令的标准输出接到右边命令的标准输入。
- 把两条命令连接起来:前者的标准输出(stdout)传给后者的标准输入(stdin)。
- 管道中的数据由内核缓冲区传递,单向流动。
- 使用管道时,Bash 会为管道两端各创建一个子进程。
- 管道是最简单的进程间通信(IPC)方式。
bash
ls | wc -l # 统计行数
history | tail -5 # 最近 5 条历史
ps -ef | grep nginx # 过滤特定进程

18.11 管道的工作原理
管道把两个子进程连接起来:一端写,一端读。

$ who | sort # 用户列表按编码排序
$ ls | wc -l # 统计文件个数
$ ps -ef | grep nginx | wc -l # 统计 nginx 进程数
注:管道两端的命令各自是一个子进程,这就是“管道与子进程创建”的联系。
18.12 bash 内嵌命令与外部命令
注意:bash 只为外部命令创建子进程,对于 bash 内嵌的命令,会在当前进程中执行。
bash
echo "hello" | wc
`echo` 是内嵌命令,因此不会为其创建子进程;
`wc` 是外部命令,因此 bash 会为其创建子进程。
查看 bash 中的内嵌命令:
bash
enable
compgen -b
help
使用 `type` 检查命令的类型:
bash
type -t <cmd>
返回值
含义
builtin
内建命令
keyword
关键字(如 if、while)
function
函数
file
外部文件
alias
别名
18.13 管道 `|` 的坑
bash
sum=0
echo -e "1\n2\n3\n4\n5" | while read n; do
((sum += n))
done
echo "sum = $sum"
输出:
sum = 0
原因:管道后的 命令在子进程中执行,变量修改不会影响父进程。

18.14 解决管道变量丢失



18.15 管道' | '的作用
进程替换 < <(...)
bash
sum=0
while read n; do
((sum += n))
done < <(echo -e "1\n2\n3")
echo "$sum"
输出:
sum = 6
< <(...) 的组合把命令的输出重定向到循环的标准输入,循环本身在当前 Shell 中运行,因此变量修改会保留。这是解决“管道导致变量丢失”问题最常用的变通方法
<(...) 将括号内运行的命令结果写入特殊文件 /dev/fd/n
< 将该特殊文件的内容作为输入

18.16 后台进程 `&` 与 `$!`
在命令后加 `&` 让进程在后台运行,`$!` 保存其 PID。
bash
sleep 10 & # 放到后台执行,立即返回
echo $! # 后台进程的 PID
jobs # 查看后台任务列表
wait $! # 等待该后台进程结束
kill $! # 必要时终止后台进程
# fg / bg 可在前台后台之间切换作业
18.17 进程与线程
进程是资源分配的单位,线程是 CPU 调度的单位。

一个进程至少包含一个(主)线程。
18.18 进程组
进程组 = 一组相关进程的集合,统一管理、统一接收信号。
- 每个进程都从属于一个进程组。
- 组内进程共享同一个 PGID。
- PGID 的值等于组长进程的 PID 值。
- 一条命令 / 脚本派生的子进程常在同一个组。
- 向进程组发信号 = 一次影响整组(含所有子进程)。
示例:


类比:进程组像一个“小组”,PGID 是小组编号,信号送给整个小组
18.19 会话、进程组与终端


- **前台进程组**:跟终端交互,能收到 Ctrl+C。
- **后台进程组**:`&` 启动,不接收终端输入。
- **控制终端**:建立会话的那个终端设备。
- 一个会话同一时刻只有 1 个前台进程组。
- 一个会话可以有多个后台进程组。
ps -eo pgid,pid,ppid,sid,comm | head -5 # 看 SID
一个会话 = 1 个前台进程组 + 若干个后台进程组

•每个会话都有且只有一个会话首进程,它的 PID 就是会话 ID(SID)•该进程也是创建会话的那个进程•一个会话最多关联一个控制终端,该终端成为这个会话的控制终端•终端关闭(终端窗口关闭、SSH 断线)时,内核把 SIGHUP 发给和终端关联的会话,会话首进程再把这个信号转发给会话里的其他进程。
会话首进程(Session leader) = 一个会话中运行的第一个
18.20 查看进程组

bash
ps -eo pid,ppid,pgid,sid,comm | head -10
ps -o pgid= -p $$ # 当前 shell 所在组
ps -o pid,pgid,sid -g 12345 # 组 12345 的全部成员
pstree -p # 进程树(含 PID)
pstree -g # 进程树(含 PGID)
•PGID 列 = 进程组 ID;SID 列 = 会话 ID•同一行的进程属于同一个组(相同 PGID)•pstree 用树状展示父子关系,便于理解分组
18.21 进程组与信号

- 前台进程组:Ctrl+C → 组内所有进程同时收到 SIGINT。
- 后台进程组:`&` 启动,不接收终端产生的信号。
- `kill` 可以向进程组发任意信号。
- 负的进程号 = 进程组号:`kill -信号 -PGID`。

信号按组传播:命令 + 它派生的所有子进程一起处理
18.22 向整个进程组发信号

bash
ps -eo pid,pgid,comm | grep mytask
kill -TERM -300 # 优雅停止整组
kill -9 -300 # 立即杀死整组
kill -g 300 # 按组号清理

kill -- -PGID 写法更保险,避免与普通 PID 混淆
18.23 脱离会话与终端:setsid
正常情况下,一个会话是从终端启动的,所以一个会话会对应一个终端。但是可以使用 `setsid` 命令,将会话与终端脱离。这样当终端关闭时,命令不会被杀掉。
让会话离开终端、独立于终端运行:
- `setsid cmd`:在新会话中运行 `cmd`,使 `cmd` 脱离控制终端和进程组。
- `setsid -f cmd`:即使 `cmd` 是一条命令,也直接开新会话。

18.24 示例:会话、进程组、进程的关系
以下面程序执行的顺序为例:
1. 从终端 `/dev/pts/0` 登录,进入 bash。
2. 在 bash 中运行 `cmd1 &`。
3. 在 bash 中运行 `cmd2 | cmd3 | cmd4 &`。
4. 在 bash 中运行 `setsid cmd5 &`。
5. 在 bash 中运行 `(cmd6; cmd7) &`。
6. 在 bash 中运行 `cmd8`。
18.24.1 从`/dev/pts/0` 登录
登录成功 → 内核创建会话,bash 成为会话首进程

•登录成功 → 新生会话•bash 是会话首进程•PID = PGID = SID = 1000•前台进程组 = { bash }
会话首进程 = 组长进程;登录 shell 天生在前台进程组
18.24.2 运行cmd1 &
一条命令 = 一个后台进程组

•cmd1 后台启动•新进程组,组长 = cmd1•仍在 SID=1000 会话内•后台组不读终端输入
& 只是“去后台”:组是新的,会话还是原来那个
18.24.3 运行 cmd2 | cmd3 | cmd4 &
一条管道 = 一个进程组,三个进程

•整条管道 = 1 个进程组•PGID = 组长 cmd2•三个PID进程同时产生•组内管道两端互通•仍属 SID=1000 会话
整组同生共死:信号按组传播,跨过管道全部生效
18.24.4 运行 setsid cmd5 &
新建会话,完全脱离终端

•setsid = 建新会话•cmd5 是会话首进程•PGID = SID = 1105•与 /dev/pts/0 无关
& 只是后台(仍同会话);setsid 是真正独立成新会话
18.24.5 运行 (cmd6 ; cmd7) &
前台进程组换人:终端把前台交给 cmd6

•( ) 是一个新进程组•新进程组在后台 &•组长 = cmd6
18.24.6 运行 cmd8
前台进程组换人:终端把前台交给 cmd6

•cmd8 抢占前台组•Ctrl+C 现在归 cmd8•bash 转入后台•cmd8退出后 bash 收回前台
关系总结:
5 条命令 × 会话 / 进程组 / 前台后台 / 进程

18.25 kill命令的作用
给进程发信号:通知、暂停、唤醒 —— 不止是“杀”

- 默认不带信号 = 发 SIGTERM (15),优雅终止进程。
- 可发任意信号:SIGKILL 强杀 / SIGSTOP 暂停 / SIGCONT 唤醒。
- 目标支持:PID、后台作业号 `%n`、进程组 `-PGID`、按名称(`pkill`)。

18.26 kill常用信号(一):终止类信号
让进程结束的信号

SIGTERM(15)是 kill 的默认信号;SIGKILL(9)是最后手段
18.27 kill常用信号(二):暂停与继续信号
暂停、挂起、唤醒与自定义信号

配对使用:SIGSTOP/SIGTSTP 暂停,SIGCONT 恢复
18.28 终止前台进程
Ctrl+C / Ctrl+\ 直击前台,也可另开终端用 kill

- `Ctrl+C` → SIGINT:最常用,程序可捕获后自行清理退出。
- `Ctrl+\` → SIGQUIT:终止并生成 core dump 文件。
- `kill [-TERM] PID`:优雅终止进程。
- `kill -9 PID`:强制终止进程。

注意:对于交互式 shell,bash 默认会忽略 SIGTERM 信号。
`Ctrl+Z` = SIGTSTP,只是挂起不终止,配合 `jobs` / `kill %n` 使用。
18.29 终止进程组中的进程(含后台)
负 PGID / pkill -g 一次覆盖整组

- 后台作业:`jobs` 看编号,`kill %1` 按作业号终止。
- 整组终止:`kill -信号 -PGID`(负号 = 进程组)。
- `pkill -g PGID`
- `killall 名称`

后台进程组同样适用:-PGID / pkill -g 一次覆盖组内全部成员
18.30 终止会话中的所有进程
关闭会话或按 SID 整会批量送信号

- `exit` 关闭登录 shell → SIGHUP 传给会话内全部进程。
- 先查出 SID,再对会话内所有 PID 批量 kill。

18.31 kill 目标写法速查表
PID / 作业号 / 进程组 / 名称 / 会话


18.32 进程组小结

本章小结
- APT:在线安装,自动解析依赖。
- dpkg:本地安装,需手动修依赖。
- snap:容器化安装,自带依赖隔离。
- PID/PPID:唯一标识进程及父进程。
- fork/exec:复制进程并加载新程序。
- 僵尸/孤儿:父未回收或父先退出。
- 管道:子进程间单向传递数据。
- 进程组/会话:统一管理信号与终端。
第十九章 Linux 变量
- PATH、HOME、USER、SHELL、PWD、RANDOM 等环境系统变量
- `$?`、`$$`、`$!`、`$0`、`$#`、`$@`、`$1-$9` 特殊变量
- 自定义变量:`declare`、`export`、`unset`
- 变量类型:整数、只读、索引数组、关联数组
- 变量作用域:临时/用户永久/全局
- 用户配置文件 `.bashrc`、`.bash_logout` 等
19.1 系统环境变量
常用的系统预定义变量:
bash
echo $PATH # 命令搜索路径
echo $HOME # 用户 HOME 目录 (/home/john)
echo $USER # 当前用户名 (user)
echo $SHELL # 当前 Shell (/bin/bash)
echo $PWD # 当前目录
echo $RANDOM # 随机数 (0-32767)
echo $HOSTNAME # 主机名
echo $UID # 当前用户 ID
19.2 查看系统预定义变量
bash
env
printenv
19.3 特殊变量
Shell 编程中使用的特殊变量:

19.4 自定义变量
创建、赋值、使用和删除变量

19.5 声明指定类型的变量
声明指定类型的变量






19.6 索引数组与关联数组
存储多个值的变量类型。

19.7 变量作用域
变量生效的范围和持久化方式。



> 注:`-g` 的差别只在函数内部体现——函数内 `declare var` 是局部变量,`declare -g var` 是全局变量;在顶层作用域两者等价。子 Shell 只能看到被 `-x` 导出的环境变量,且变量永远无法反向影响父 Shell。
19.8 profile 文件
profile 文件是系统与用户环境的配置中心,它们决定了命令行的行为、环境变量、别名和启动程序。主要分为全局配置(影响所有用户)和用户级配置(仅影响当前用户)。

第二十章 Bash Shell 编程
- 脚本格式与执行方式
- 条件判断(if/elif/else/test/[[]])
- 循环语句(for/while/until)
- case 多分支判断,`$(...)`
- 函数定义与调用
- 脚本调试(`bash -x` / bashdb)
- flock 文件锁
- 完整脚本案例
20.1 bash shell 历史
Bourne Shell Again Shell(bash)发展简图:

DevOps 与云原生、CI/CD 流水线、自动化运维、数据科学等领域广泛使用 Bash。
20.2 脚本基本格式
Shell 脚本的创建和执行:

示例:

20.3 if 条件判断

20.4 数字比较
数字比较操作符

20.5 字符串比较
字符串比较操作符

20.6 文件判断
判断文件类型的操作符


20.7 [ ] [[ ]] (( ))
三种条件测试语法对比




20.8 ${...}
参数扩展(Parameter Expansion)的语法标记

echo $name_home echo ${name}_home # 当变量名与后面的字符连接在一起时,用 ${} 明确边界





20.9 for循环
遍历列表和数字范围的循环

20.10 while / until 循环
条件循环

20.11 break / continue
循环控制语句

20.12 case 多分支
多模式匹配判断

20.13 `` $(...)

20.14 函数
# 函数(function)和脚本(script)的比较
|
特性 |
Shell 函数 |
外部脚本 |
|
运行环境 |
在当前 Shell 中执行(不创建子进程) |
在子 Shell 中执行 |
|
变量访问 |
可以直接修改当前 Shell 的变量 |
无法修改父 Shell 的变量 |
|
执行速度 |
更快(无需创建进程) |
较慢(需 fork 新进程) |
|
适用范围 |
仅当前脚本/会话中可用 |
可独立运行 |





20.15 文件锁
通过“文件锁”来管理多个进程或脚本对共享资源的访问

文件锁 = 串行化对共享资源的访问,避免竞态条件(race condition)

20.16 共享锁与排他锁

20.17 flock 命令简介



不加 -n / -w 时是阻塞模式:拿不到锁就一直等待



20.18 脚本调试
调试 Shell 脚本的常用方法


# bashdb 调试器常用调试命令
|
命令 |
说明 |
|
n 或 next |
单步跳过:执行下一行代码,但不进入函数内部。 |
|
s 或 step |
单步进入:执行下一行代码,如果遇到函数则进入。 |
|
c 或 continue |
继续执行:运行到下一个断点或脚本结束。 |
|
b <行号> 或 b <函数名> |
设置断点,例如 b 10 或 b my_function。 |
|
info break |
查看所有断点。 |
|
p <变量名> |
打印变量值,如 p name。 |
|
l 或 list |
显示当前执行的代码上下文。 |
|
q 或 quit |
退出调试器。 |
第二十一章 Shell 脚本实战案例
- 案例 1:系统信息报告脚本
- 案例 2:批量重命名脚本
- 案例 3:菜单选择脚本
- 案例 4:简单的 TCP 服务器
21.1 案例 1:系统信息报告
保存为 `sysinfo.sh` → `chmod +x sysinfo.sh` → `./sysinfo.sh`

21.2 案例 2:批量重命名
保存为 `rename.sh` → 在图片目录运行。

21.3 案例 3:菜单脚本
保存为 `menu.sh` → `./menu.sh`

21.4 案例 4:简单 TCP 服务器
保存为 `server_code`:

保存为 server

保存为 client

先执行: server 再执行: client

658



被折叠的 条评论
为什么被折叠?



