强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Bash 脚本编写教程 / 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 文件锁

13.8 扩展阅读