高级特性
本章介绍 Shell 脚本的高级特性,包括子 Shell、后台任务管理、信号处理、并行执行、进程管理等。这些特性可以让你编写更强大、更高效的脚本。
子 Shell
子 Shell 是当前 Shell 进程的子进程,它继承了父 Shell 的环境变量、工作目录等,但对变量、函数、工作目录的修改不会影响父 Shell。
创建子 Shell
使用括号 () 创建子 Shell:
# 在子 Shell 中执行命令
(cd /tmp; ls -la)
# 当前目录不会改变
# 子 Shell 中的变量修改不影响父 Shell
x=1
(
x=2
echo "子 Shell 中: x=$x"
)
echo "父 Shell 中: x=$x"
# 输出:
# 子 Shell 中: x=2
# 父 Shell 中: x=1
子 Shell 的执行流程
当执行 (commands) 时,Shell 会:
- 创建一个子进程
- 复制父 Shell 的环境(变量、函数、工作目录等)
- 在子进程中执行命令
- 子进程退出,控制权返回父 Shell
子 Shell 的典型应用
在独立环境中执行命令
#!/bin/bash
# 在子 Shell 中执行一系列操作,不影响当前环境
(
cd /tmp/project
rm -rf build
mkdir build
cd build
cmake ..
make
)
# 当前目录和变量都未改变
echo "当前目录: $(pwd)"
并行执行任务
#!/bin/bash
# 使用子 Shell 并行执行多个任务
(
echo "任务 1 开始"
sleep 2
echo "任务 1 完成"
) &
(
echo "任务 2 开始"
sleep 1
echo "任务 2 完成"
) &
wait
echo "所有任务完成"
保存和恢复环境
#!/bin/bash
# 保存当前工作目录
pushd() {
OLD_DIR=$(pwd)
cd "$1"
}
popd() {
cd "$OLD_DIR"
}
# 或者使用子 Shell 更简洁
(cd /tmp; some_command)
# 自动恢复原目录
子 Shell 与花括号的区别
# 子 Shell:在独立进程中执行
x=1
(x=2)
echo $x # 输出:1(父 Shell 的值未变)
# 花括号:在当前 Shell 中执行
x=1
{ x=2; }
echo $x # 输出:2(当前 Shell 的值已变)
后台任务
后台任务允许命令在后台运行,不阻塞当前 Shell。
基本用法
使用 & 将命令放到后台执行:
# 在后台运行命令
sleep 100 &
# 输出:[1] 12345(作业号和进程 ID)
# 后台任务不影响当前 Shell
echo "这会立即输出"
jobs 命令
jobs 命令查看当前 Shell 的后台任务:
sleep 100 &
sleep 200 &
jobs
# 输出:
# [1]- Running sleep 100 &
# [2]+ Running sleep 200 &
# 只显示进程 ID
jobs -p
# 输出:12345 12346
# 显示进程 ID 和作业号
jobs -l
# 输出:
# [1]- 12345 Running sleep 100 &
# [2]+ 12346 Running sleep 200 &
作业号旁边的 + 表示最近的任务,- 表示倒数第二个任务。
fg 和 bg 命令
# 将后台任务调到前台
fg %1 # 使用作业号
fg # 默认调出最近的任务(带 + 号的)
# 将暂停的任务放到后台继续运行
# 先按 Ctrl+Z 暂停前台任务
sleep 100
# 按 Ctrl+Z
# [1]+ Stopped sleep 100
bg %1 # 放到后台继续运行
nohup:脱离终端运行
nohup 让命令在退出终端后继续运行:
# 使用 nohup 运行命令
nohup ./long_script.sh &
# 输出默认保存到 nohup.out
nohup ./script.sh > output.log 2>&1 &
disown:从任务列表移除
disown 将任务从当前 Shell 的任务列表移除,使其在 Shell 退出后继续运行:
# 启动后台任务
sleep 100 &
# [1] 12345
# 从任务列表移除
disown %1
# 或使用进程 ID
disown 12345
# 验证:jobs 不再显示
jobs
wait 命令
wait 命令等待后台任务完成:
#!/bin/bash
# 启动多个后台任务
sleep 3 &
pid1=$!
sleep 5 &
pid2=$!
echo "等待任务完成..."
# 等待特定进程
wait $pid1
echo "任务 1 完成"
# 等待所有后台任务
wait
echo "所有任务完成"
wait -n(Bash 4.3+)等待任意一个后台任务完成:
#!/bin/bash
sleep 5 &
sleep 3 &
sleep 1 &
# 等待任意一个完成
wait -n
echo "有一个任务完成了"
# 继续等待剩余任务
wait
echo "所有任务完成"
信号处理
信号是进程间通信的一种机制。Shell 脚本可以使用 trap 命令捕获和处理信号。
常见信号
| 信号 | 编号 | 含义 | 触发方式 |
|---|---|---|---|
| SIGHUP | 1 | 挂起,终端关闭 | kill -HUP PID |
| SIGINT | 2 | 中断,Ctrl+C | Ctrl+C |
| SIGQUIT | 3 | 退出,Ctrl+\ | Ctrl+\ |
| SIGKILL | 9 | 强制终止 | kill -9 PID |
| SIGTERM | 15 | 终止请求 | kill PID |
| SIGUSR1 | 10 | 用户自定义信号 1 | kill -USR1 PID |
| SIGUSR2 | 12 | 用户自定义信号 2 | kill -USR2 PID |
trap 命令
trap 命令用于捕获信号并执行指定命令:
# 基本语法
trap 'commands' signals
捕获 SIGINT(Ctrl+C)
#!/bin/bash
trap 'echo "收到中断信号,正在退出..."; exit 1' INT
echo "按 Ctrl+C 退出"
while true; do
sleep 1
done
捕获多个信号
#!/bin/bash
cleanup() {
echo "正在清理..."
rm -f "$temp_file"
exit
}
# 捕获 INT、TERM 和 EXIT 信号
trap cleanup INT TERM EXIT
temp_file=$(mktemp)
echo "工作目录: $temp_file"
# 主逻辑
echo "正在处理..."
sleep 10
忽略信号
# 忽略 SIGINT
trap '' INT
# 脚本不能被 Ctrl+C 中断
恢复默认行为
# 先忽略
trap '' INT
# 恢复默认行为
trap - INT
EXIT 信号
EXIT 信号在脚本退出时触发,无论正常退出还是异常退出:
#!/bin/bash
temp_file="/tmp/script_$$"
cleanup() {
echo "清理临时文件..."
rm -f "$temp_file"
}
trap cleanup EXIT
# 创建临时文件
touch "$temp_file"
# 主逻辑
echo "处理中..."
# 即使脚本出错或被中断,cleanup 也会执行
DEBUG 信号
DEBUG 信号在每条命令执行前触发:
#!/bin/bash
trap 'echo "执行: $BASH_COMMAND (行号: $LINENO)"' DEBUG
x=1
y=2
echo $((x + y))
ERR 信号
ERR 信号在命令返回非零状态时触发:
#!/bin/bash
trap 'echo "错误: 命令失败于行 $LINENO: $BASH_COMMAND"' ERR
# 这个命令会失败
ls /nonexistent_directory
实际应用:优雅退出
#!/bin/bash
PID_FILE="/tmp/my_script.pid"
LOG_FILE="/var/log/my_script.log"
# 确保只有一个实例运行
if [[ -f "$PID_FILE" ]]; then
old_pid=$(cat "$PID_FILE")
if kill -0 "$old_pid" 2>/dev/null; then
echo "脚本已在运行 (PID: $old_pid)"
exit 1
fi
fi
echo $$ > "$PID_FILE"
cleanup() {
echo "$(date): 收到退出信号,正在清理..." >> "$LOG_FILE"
rm -f "$PID_FILE"
exit 0
}
trap cleanup INT TERM EXIT
echo "$(date): 脚本启动" >> "$LOG_FILE"
# 主循环
while true; do
echo "$(date): 心跳检测" >> "$LOG_FILE"
sleep 60
done
并行执行
Shell 提供了多种方式实现并行执行。
使用 & 和 wait
#!/bin/bash
process_file() {
local file=$1
echo "处理: $file"
sleep 1
echo "完成: $file"
}
# 并行处理所有 txt 文件
for file in *.txt; do
process_file "$file" &
done
# 等待所有后台任务完成
wait
echo "所有文件处理完成"
限制并发数
#!/bin/bash
MAX_JOBS=4
running=0
process_file() {
local file=$1
echo "处理: $file"
sleep 2
}
for file in *.txt; do
process_file "$file" &
((running++))
# 当达到最大并发数时,等待任意一个任务完成
if ((running >= MAX_JOBS)); then
wait -n
((running--))
fi
done
# 等待剩余任务
wait
echo "所有任务完成"
使用 xargs 并行
xargs 的 -P 选项可以并行执行:
# 并行处理文件,最多 4 个并发
find . -name "*.txt" -print0 | xargs -0 -P 4 -I {} sh -c 'echo "处理: {}"; sleep 1'
# 并行压缩
find . -name "*.log" -print0 | xargs -0 -P 4 gzip
使用 GNU parallel
GNU parallel 是专门的并行执行工具:
# 安装
# Ubuntu/Debian: apt install parallel
# macOS: brew install parallel
# 基本用法
parallel echo ::: a b c d
# 输出:a b c d(可能乱序)
# 处理文件
parallel gzip ::: *.txt
# 从标准输入读取
find . -name "*.txt" | parallel gzip
# 指定并发数
parallel -j 4 process_file {} ::: *.txt
# 保持顺序输出
parallel -k echo ::: a b c
# 显示进度
parallel --progress gzip ::: *.txt
并行执行示例:批量下载
#!/bin/bash
download_file() {
local url=$1
local output=$(basename "$url")
echo "下载: $output"
curl -s -o "$output" "$url" && echo "完成: $output" || echo "失败: $output"
}
urls=(
"https://example.com/file1.txt"
"https://example.com/file2.txt"
"https://example.com/file3.txt"
"https://example.com/file4.txt"
"https://example.com/file5.txt"
)
max_parallel=2
running=0
for url in "${urls[@]}"; do
download_file "$url" &
((running++))
if ((running >= max_parallel)); then
wait -n
((running--))
fi
done
wait
echo "所有下载完成"
进程管理
查看进程
# 当前 Shell 的 PID
echo $$
# 查看进程
ps aux
# 查看特定进程
ps aux | grep nginx
# 查看进程树
pstree
# 动态监控
top
htop
# 查看进程详细信息
cat /proc/PID/status
终止进程
# 发送 SIGTERM(默认)
kill PID
# 发送 SIGKILL(强制终止)
kill -9 PID
# 发送其他信号
kill -HUP PID # 重载配置
kill -INT PID # 中断
# 按名称终止
killall nginx
# 使用 pkill(支持模式匹配)
pkill nginx
pkill -f "python.*script.py"
# 终止所有匹配的进程
pkill -9 -f "process_pattern"
进程优先级
进程优先级范围从 -20(最高)到 19(最低),默认为 0。
# 以较低优先级运行
nice -n 10 ./script.sh
# 以更高优先级运行(需要 root)
sudo nice -n -5 ./script.sh
# 修改运行中进程的优先级
renice -n 5 -p PID
renice -n -5 -p PID # 提高,需要 root
# 查看进程优先级
ps -o pid,ni,comm -p PID
进程资源限制
# 查看当前限制
ulimit -a
# 设置最大文件打开数
ulimit -n 65536
# 设置最大进程数
ulimit -u 4096
# 设置最大内存大小(KB)
ulimit -v 1048576
# 设置最大 CPU 时间(秒)
ulimit -t 3600
exec 命令
exec 命令用于替换当前 Shell 或操作文件描述符。
替换当前 Shell
当 exec 后跟命令时,会用新命令替换当前 Shell:
# 启动新的 Bash
exec bash
# 启动其他 Shell
exec zsh
# 执行脚本后退出
exec ./script.sh
# 脚本执行完毕后,Shell 退出(不会返回)
操作文件描述符
# 将所有输出重定向到文件
exec > output.log 2>&1
echo "这会写入文件"
ls /nonexistent # 错误也写入文件
# 保存并恢复标准输出
exec 3>&1 # 将 fd 3 指向 stdout
exec > output.log # 重定向 stdout
echo "写入文件"
exec >&3 # 恢复 stdout
exec 3>&- # 关闭 fd 3
echo "输出到屏幕"
# 同时管理多个文件描述符
exec 3< input.txt # 打开输入文件
exec 4> output.txt # 打开输出文件
read -u 3 line
echo "处理: $line" >&4
exec 3<&- 4>&- # 关闭文件描述符
日志记录脚本
#!/bin/bash
LOG_FILE="script.log"
# 重定向所有输出到日志文件(同时显示在屏幕上)
exec > >(tee -a "$LOG_FILE")
exec 2>&1
echo "脚本开始执行: $(date)"
echo "处理数据..."
echo "脚本执行完毕: $(date)"
进程替换进阶
进程替换将命令的输出或输入表现为文件路径。
输入替换 <(...)
# 比较两个目录
diff <(ls dir1) <(ls dir2)
# 比较排序后的文件
diff <(sort file1.txt) <(sort file2.txt)
# 合并并排序
sort -m <(sort file1.txt) <(sort file2.txt)
# 使用 join 合并文件
join <(sort file1.txt) <(sort file2.txt)
输出替换 >(...)
# 将输出同时发送到多个命令
echo "test" | tee >(grep -o "t" > t.txt) >(grep -o "e" > e.txt)
# 创建压缩归档
tar -cf >(gzip > archive.tar.gz) /path/to/files
# 同时计算多种校验和
cat file.txt | tee >(md5sum > file.md5) >(sha256sum > file.sha256) > /dev/null
实用示例
带进度指示的并行处理
#!/bin/bash
total=100
completed=0
lock_file=$(mktemp)
update_progress() {
flock -x "$lock_file" -c "((completed++))"
percent=$((completed * 100 / total))
bar_width=50
filled=$((percent * bar_width / 100))
empty=$((bar_width - filled))
printf "\r["
printf "%${filled}s" | tr ' ' '='
printf "%${empty}s" | tr ' ' ' '
printf "] %3d%% (%d/%d)" "$percent" "$completed" "$total"
}
process_item() {
local item=$1
# 模拟处理
sleep 0.$((RANDOM % 5 + 1))
update_progress
}
# 并行处理
for i in $(seq 1 $total); do
process_item $i &
done
wait
echo
echo "处理完成"
rm -f "$lock_file"
进程池
#!/bin/bash
# 进程池实现
process_pool() {
local pool_size=$1
shift
local items=("$@")
local running=0
local index=0
while ((index < ${#items[@]} || running > 0)); do
# 启动新任务
while ((running < pool_size && index < ${#items[@]})); do
process_item "${items[$index]}" &
((running++))
((index++))
done
# 等待任务完成
if ((running > 0)); then
wait -n
((running--))
fi
done
}
process_item() {
echo "处理: $1"
sleep 1
}
items=("a" "b" "c" "d" "e" "f" "g" "h")
process_pool 4 "${items[@]}"
守护进程脚本
#!/bin/bash
DAEMON_NAME="my_daemon"
PID_FILE="/var/run/${DAEMON_NAME}.pid"
LOG_FILE="/var/log/${DAEMON_NAME}.log"
start() {
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "$DAEMON_NAME 已在运行 (PID: $pid)"
return 1
fi
fi
# 启动守护进程
nohup "$0" _run >> "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
echo "$DAEMON_NAME 已启动"
}
stop() {
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
kill "$pid" 2>/dev/null && echo "$DAEMON_NAME 已停止"
rm -f "$PID_FILE"
else
echo "$DAEMON_NAME 未运行"
fi
}
status() {
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "$DAEMON_NAME 正在运行 (PID: $pid)"
else
echo "$DAEMON_NAME 已停止(PID 文件存在)"
fi
else
echo "$DAEMON_NAME 未运行"
fi
}
# 守护进程主循环
run() {
trap 'rm -f "$PID_FILE"; exit' TERM INT
while true; do
echo "$(date): 守护进程工作中..."
# 这里放置实际的工作逻辑
sleep 60
done
}
# 主入口
case "${1:-}" in
start) start ;;
stop) stop ;;
status) status ;;
_run) run ;;
*)
echo "用法: $0 {start|stop|status}"
exit 1
;;
esac
小结
本章介绍了 Shell 脚本的高级特性:
- 子 Shell:使用
()创建独立环境执行命令 - 后台任务:
&、jobs、fg、bg、nohup、disown、wait - 信号处理:
trap命令捕获和处理信号,实现优雅退出 - 并行执行:
&+wait、xargs -P、GNU parallel - 进程管理:查看、终止、优先级、资源限制
- exec 命令:替换 Shell 和操作文件描述符
- 进程替换:
<(...)和>(...)将命令作为文件使用
掌握这些高级特性可以让你编写更强大、更高效的 Shell 脚本。下一章将介绍调试与测试。