13 - 子 Shell 与进程管理
13 - 子 Shell 与进程管理
13.1 子 Shell(Subshell)
子 Shell 是当前 Shell 进程的一个副本,拥有独立的变量空间和执行环境。
# 小括号创建子 Shell
(result="hello"; echo "$result")
echo "$result" # 空——子 Shell 中的修改不影响父 Shell
# 管道的每一段都在子 Shell 中执行
echo "hello" | read greeting
echo "$greeting" # 空!
# 对比:花括号在当前 Shell 中执行
{ result="hello"; }; echo "$result" # 输出: hello
# 后台命令也在子 Shell 中
background_var="before"
sleep 1 &
background_var="after"
echo "$background_var" # after——子 Shell 的修改不影响父 Shell
wait
子 Shell 与当前 Shell 的区别
| 特性 | 子 Shell ( ) | 当前 Shell { } |
|---|---|---|
| 变量修改 | 不影响父 Shell | 影响当前 Shell |
| 退出码传递 | 是 | 是 |
| 文件描述符 | 继承 | 共享 |
| cd 命令 | 不影响父 Shell | 影响当前 Shell |
| 性能 | 需 fork 进程 | 无需 fork |
13.2 进程替换(Process Substitution)
进程替换将命令的输出/输入伪装成文件。
# <(command) —— 将命令输出变为"文件"
# >(command) —— 将命令输入变为"文件"
# 比较两个命令的输出
diff <(ls /tmp) <(ls /var/tmp)
# 合并多个文件排序
sort -u <(cat file1.txt) <(cat file2.txt) <(cat file3.txt)
# 读取命令输出到循环变量(解决管道子 Shell 问题)
while IFS= read -r line; do
echo "处理: $line"
done < <(find /tmp -name "*.log")
# 同时输出到终端和文件
echo "hello" | tee >(gzip > output.gz)
# 同时处理多个文件
paste <(cut -d',' -f1 data.csv) <(cut -d',' -f3 data.csv)
# 将数组传递给命令
arr=("line 1" "line 2" "line 3")
mapfile -t result < <(printf '%s\n' "${arr[@]}" | sort)
13.3 后台任务
# 在后台运行命令
long_command &
# 获取后台任务 PID
long_command &
pid=$!
echo "后台进程 PID: $pid"
# 等待特定后台任务完成
wait $pid
echo "任务完成,退出码: $?"
# 等待所有后台任务完成
wait
# 后台任务 + 错误处理
set -o pipefail
command1 &
pid1=$!
command2 &
pid2=$!
wait $pid1
code1=$?
wait $pid2
code2=$?
if [[ $code1 -ne 0 || $code2 -ne 0 ]]; then
echo "至少一个任务失败"
exit 1
fi
13.4 jobs 命令
# 查看后台任务
jobs
# 带详细信息
jobs -l
# 只显示 PID
jobs -p
# 将后台任务调到前台
fg %1
# 将挂起的任务放到后台
bg %1
# 继续运行被挂起的任务(Ctrl+Z 后)
long_command # Ctrl+Z 暂停
bg # 后台继续
jobs # 查看状态
13.5 并行执行
基本并行
#!/bin/bash
# 并行下载多个文件
URLS=(
"https://example.com/file1.tar.gz"
"https://example.com/file2.tar.gz"
"https://example.com/file3.tar.gz"
)
download() {
local url="$1"
local filename
filename=$(basename "$url")
echo "下载: $filename"
curl -sLO "$url"
echo "完成: $filename"
}
# 并行下载
for url in "${URLS[@]}"; do
download "$url" &
done
# 等待所有下载完成
wait
echo "所有下载完成"
限制并发数(信号量模式)
#!/bin/bash
# 并行执行,限制最大并发数
MAX_CONCURRENT=3
RUNNING=0
# 进程数组
declare -a PIDS=()
wait_for_slot() {
while [[ $RUNNING -ge $MAX_CONCURRENT ]]; do
# 等待任意子进程结束
wait -n 2>/dev/null || true
# 重新计算运行中的进程数
RUNNING=0
for pid in "${PIDS[@]}"; do
kill -0 "$pid" 2>/dev/null && ((RUNNING++))
done
# 清理已结束的 PID
PIDS=("${PIDS[@]/#*/}")
PIDS=($(for pid in "${PIDS[@]}"; do kill -0 "$pid" 2>/dev/null && echo "$pid"; done))
done
}
process_item() {
local item="$1"
echo "[START] $item (PID: $$)"
sleep $((RANDOM % 3 + 1))
echo "[DONE] $item"
}
# 主循环
for i in {1..10}; do
process_item "item-$i" &
PIDS+=($!)
((RUNNING++))
done
# 等待所有进程
wait
echo "全部完成"
# 更简洁的方法:使用 xargs
# seq 10 | xargs -P 3 -I {} bash -c 'echo "处理 {}"; sleep $((RANDOM % 3))'
GNU Parallel
# 安装:sudo apt-get install parallel
# 基本并行
seq 10 | parallel "echo 处理 {} && sleep 1"
# 限制并发数
seq 100 | parallel -j 10 "echo {}"
# 并行处理文件
find . -name "*.txt" | parallel "wc -l {}"
# 带进度条
seq 20 | parallel --progress "sleep 1; echo {}"
# 并行执行命令(替代 xargs -P)
cat urls.txt | parallel -j 4 "curl -sO {}"
13.6 业务场景:批量服务器健康检查
#!/bin/bash
# parallel_check.sh —— 并行检查多台服务器
set -euo pipefail
readonly MAX_CONCURRENT=10
readonly TIMEOUT=5
readonly RESULT_DIR=$(mktemp -d)
SERVERS=(
"web-01:192.168.1.101"
"web-02:192.168.1.102"
"web-03:192.168.1.103"
"db-01:192.168.1.201"
"db-02:192.168.1.202"
"cache-01:192.168.1.211"
"mq-01:192.168.1.221"
"lb-01:192.168.1.1"
)
check_server() {
local name="$1"
local ip="$2"
local result_file="$RESULT_DIR/$name"
# Ping 检查
if ping -c 1 -W "$TIMEOUT" "$ip" &>/dev/null; then
local latency
latency=$(ping -c 1 -W "$TIMEOUT" "$ip" 2>/dev/null | \
grep 'time=' | sed 's/.*time=\([0-9.]*\).*/\1/')
echo "UP|$latency" > "$result_file"
else
echo "DOWN|0" > "$result_file"
fi
}
echo "========================================"
echo " 并行服务器健康检查"
echo " 服务器数量: ${#SERVERS[@]}"
echo " 最大并发: $MAX_CONCURRENT"
echo "========================================"
echo ""
# 并行检查
running=0
pids=()
for server_info in "${SERVERS[@]}"; do
IFS=: read -r name ip <<< "$server_info"
check_server "$name" "$ip" &
pids+=($!)
((running++))
# 限制并发
if [[ $running -ge $MAX_CONCURRENT ]]; then
wait -n 2>/dev/null || true
((running--))
fi
done
# 等待所有检查完成
wait
# 汇总结果
echo ""
echo "检查结果:"
echo "----------------------------------------"
printf "%-15s %-18s %-8s %-10s\n" "服务器" "IP" "状态" "延迟(ms)"
echo "----------------------------------------"
up_count=0
down_count=0
for server_info in "${SERVERS[@]}"; do
IFS=: read -r name ip <<< "$server_info"
result_file="$RESULT_DIR/$name"
if [[ -f "$result_file" ]]; then
IFS='|' read -r status latency < "$result_file"
if [[ "$status" == "UP" ]]; then
printf "%-15s %-18s %-8s %-10s\n" "$name" "$ip" "🟢 UP" "${latency}ms"
((up_count++))
else
printf "%-15s %-18s %-8s %-10s\n" "$name" "$ip" "🔴 DOWN" "-"
((down_count++))
fi
else
printf "%-15s %-18s %-8s %-10s\n" "$name" "$ip" "❓ ERR" "-"
fi
done
echo "----------------------------------------"
echo "总计: ${#SERVERS[@]} 台 | 上线: $up_count | 下线: $down_count"
# 清理
rm -rf "$RESULT_DIR"
[[ $down_count -gt 0 ]] && exit 1 || exit 0
13.7 注意事项
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 管道子 Shell 变量丢失 | 管道中的变量修改不传播 | < <() 进程替换 |
| 并发写文件 | 多进程写同一文件可能冲突 | 使用独立输出文件或加锁 |
| fork 炸弹 | `:(){ : | :& };:` 会耗尽系统资源 |
| wait 不返回状态 | 不带参数时返回最后等待的进程码 | wait $pid 获取特定状态 |
| 竞态条件 | 并发修改共享资源 | 使用 flock 文件锁 |