服务端5月30日 00:10
Shell 中 if 和 case 语句有什么区别?如何使用?`if` 适合判断条件真假,比如文件是否存在、变量是否为空、命令是否执行成功;`case` 适合一个变量匹配多个模式,比如菜单选项、文件后缀、运行环境。写 Shell 条件判断时,变量要加引号,复杂字符串判断优先用 `[[ ]]`,数字计算用 `(( ))`。
## 追问
### `[ ]` 和 `[[ ]]` 有什么区别?
`[ ]` 是传统 test 写法,兼容性好但限制多;`[[ ]]` 是 Bash 扩展,支持 `&&`、`||`、模式匹配和正则,变量未加引号时也更不容易出错。写 Bash 脚本通常优先用 `[[ ]]`。
### if 能直接判断命令结果吗?
可以。`if grep -q "ERROR" app.log; then ... fi` 判断的是命令退出码,0 表示成功,非 0 表示失败。这比手动判断 `$?` 更清晰。
### case 适合替代多个 elif 吗?
当你围绕同一个变量做多分支匹配时,case 更清楚。它支持通配符,比如 `*.log)`、`start|restart)`,最后用 `*)` 兜底。
### 常见坑是什么?
`[ $name = foo ]` 在变量为空时会语法错误,应该写 `[ "$name" = "foo" ]`。另外 `-a`、`-o` 可读性差,建议拆成 `[ cond1 ] && [ cond2 ]`。
## 写段代码
```bash
file="$1"
if [[ -f "$file" && -r "$file" ]]; then
echo "readable file"
fi
case "$file" in
*.sh) echo "shell script" ;;
*.log) echo "log file" ;;
*) echo "unknown" ;;
esac
```标签
Shell
Shell 是一个命令行解释器,它提供了一个用户界面,用于访问操作系统的服务。在 shell 中,用户可以输入命令、执行程序和管理文件系统。Shell 也可以运行存储在文本文件中的命令序列,这些文本文件通常被称为 shell 脚本或批处理文件。

服务端5月30日 00:10
grep、sed、awk 和 cut 分别适合处理什么文本问题?Shell 文本处理常用四件套:`grep` 负责找行,`sed` 负责按规则改行,`awk` 负责按列计算和格式化,`cut` 负责简单切字段。判断工具时别背命令,先看问题:是搜索、替换、列处理,还是固定分隔符提取。
## 追问
### grep 适合什么?
适合按关键字或正则筛选行。常用参数是 `-i` 忽略大小写、`-n` 显示行号、`-r` 递归目录、`-v` 反向过滤。
### sed 和 awk 怎么区分?
`sed` 更像“流式编辑器”,擅长替换、删除、打印某段行;`awk` 更像“小型报表工具”,擅长按字段取列、条件过滤、求和统计。
### cut 还有必要学吗?
有。字段分隔很固定时,`cut -d ':' -f1` 比 awk 更直接。但它不擅长复杂条件,也不适合多空格混合的文本。
### 实际排日志会怎么组合?
先用 `grep` 缩小范围,再用 `awk` 提取字段,最后用 `sort | uniq -c` 统计。管道里每一步只做一件事,脚本会更好查错。
## 写段代码
```bash
# 查 ERROR,提取第 5 列 IP,统计出现次数
grep "ERROR" app.log | awk '{print $5}' | sort | uniq -c | sort -nr
# 批量替换配置
sed -i.bak 's/port=8080/port=9090/' app.conf
```服务端5月30日 00:10
Shell 脚本中如何定义和使用数组?Shell 数组主要分两类:普通数组和关联数组。普通数组用数字下标,适合保存文件名、参数列表;关联数组用字符串 key,适合保存配置。写脚本时最重要的一点是:访问数组尽量用双引号包住 `"${arr[@]}"`,否则遇到空格文件名会翻车。
## 追问
### 普通数组怎么定义和读取?
普通数组用空格分隔定义,索引从 0 开始。`${arr[0]}` 取第一个元素,`${arr[@]}` 取全部元素,`${#arr[@]}` 取长度。
### `${arr[@]}` 和 `${arr[*]}` 有什么区别?
加双引号时区别最大:`"${arr[@]}"` 会保留每个元素的边界,`"${arr[*]}"` 会把所有元素拼成一个字符串。遍历数组时优先用 `"${arr[@]}"`。
### 关联数组怎么用?
Bash 4+ 支持关联数组,必须先 `declare -A map`。例如 `map[host]=localhost`,读取用 `${map[host]}`,遍历 key 用 `${!map[@]}`。
### 删除数组元素会重新编号吗?
不会。`unset arr[1]` 只删除该位置,其他索引不变,所以遍历稀疏数组时用 `${!arr[@]}` 更稳。
## 写段代码
```bash
arr=("a.txt" "b file.txt")
arr+=("c.txt")
for i in "${!arr[@]}"; do
echo "$i => ${arr[$i]}"
done
declare -A conf=([host]="127.0.0.1" [port]="3306")
echo "${conf[host]}:${conf[port]}"
```服务端5月29日 01:20
Shell 脚本中单引号和双引号有什么区别?单引号是强引用,内部所有字符按字面量处理,变量 $var、命令替换 $(cmd)、转义符 \n 全部原样输出。双引号是弱引用,允许变量展开、命令替换和少数转义(\$ \" \\ \n),其余字符原样保留。实际开发中 90% 的场景用双引号——既能展开变量,又能防止空格拆分和 glob 展开。只在需要原样输出 $ 符号或特殊字符时才用单引号。另外 $'...' 语法支持 \n \t 等转义序列,是 echo -e 的替代方案。
## 追问
**1. 不加引号、双引号、单引号对变量赋值有什么不同?**
不加引号时变量值会被 word splitting 和 glob 展开:var=hello world 会报错,var=*.txt 会展开为匹配的文件列表。双引号阻止 word splitting 和 glob 但保留变量展开。单引号阻止一切展开。
**2. 单引号里怎么输出单引号本身?**
单引号内无法转义单引号。解决办法是用拼接:'it'\''s' ——结束单引号、转义单引号、重新开始单引号。或改用双引号 "it's"。
**3. $'...' 和 "..." 都支持转义,有什么区别?**
$'...' 只做转义展开,不做变量和命令替换,是真正的转义字符串。"..." 既做转义又做变量/命令替换。需要转义但不需变量展开时用 $'...' 更精确。
**4. 双引号能防止哪些问题?举个实际例子。**
防止空格拆分:file="my doc.txt"; rm "$file" 正确删除,不加引号会删 my 和 doc.txt 两个文件。防止 glob 展开:echo "*.txt" 输出字面量,不加引号会列出所有 .txt 文件。
**5. 单引号和双引号有性能差异吗?**
理论上单引号更快(跳过展开解析),但差异极小可忽略。选择依据应是语义正确性而非性能。
## 写段代码
```bash
name="world"
echo 'Hello $name' # Hello $name
echo "Hello $name" # Hello world
echo $'line1\nline2' # 换行输出
echo 'it'\''s me' # it's me
```服务端5月29日 01:20
Shell 脚本中 for、while、until 循环怎么用?Shell 有三种循环:for 遍历列表或范围,while 条件为真时重复,until 条件为真时停止。for 有两种写法——for item in list 遍历集合,for ((i=0;i<N;i++)) 是 C 风格计数循环。while 最经典的用法是逐行读文件:while IFS= read -r line。until 和 while 逻辑相反,常用于等待服务就绪。break N 可跳出 N 层循环,continue 跳过当前迭代。无限循环用 while true 或 for ((;;))。
## 追问
**1. for item in $(ls) 有什么问题?应该怎么遍历文件?**
$(ls) 会按空格拆分,含空格的文件名会被切成多段。正确做法是 for item in * 或 for item in dir/*,由 Shell 的路径展开直接生成文件列表,不会拆分含空格的文件名。
**2. while read 读文件为什么要加 IFS= 和 -r?**
不加 IFS=,前导尾随空白会被截掉;不加 -r,反斜杠会被当作转义符吞掉。正确写法 while IFS= read -r line 是防御性编程的标准范式。
**3. while 和 until 分别适合什么场景?**
while 用于"满足条件就继续"(如逐行处理),until 用于"不满足就等"(如轮询服务状态)。until curl -s localhost:8080 >/dev/null; do sleep 1; done 比 while ! curl ... 更直观。
**4. 怎么控制循环的并发数?**
用计数器+wait 控制后台任务数:每启动 N 个后台任务执行一次 wait,等上一批完成再继续。或用 GNU parallel / xargs -P 等工具。
**5. for 循环中 {1..$n} 为什么不生效?**
{} 范围展开在变量替换之前执行,所以 {1..$n} 不会展开。应改用 C 风格 for ((i=1;i<=n;i++)) 或 seq 1 $n。
## 写段代码
```bash
# 安全遍历文件 + while 逐行处理
for f in /var/log/*.log; do
[ -f "$f" ] || continue
while IFS= read -r line; do
echo "${f##*/}: $line"
done < "$f"
done
```服务端5月29日 01:19
Shell 脚本中常用的字符串操作有哪些?Shell 字符串操作全部通过 ${} 参数展开完成,无需外部命令。拼接直接并排书写即可:"$a$b" 或 "${a}_${b}"。长度用 ${#var}。截取用 ${var:offset:length},支持负偏移从末尾取。最常用的模式删除:${var#pattern} 删最短前缀、${var##pattern} 删最长前缀、${var%pattern} 删最短后缀、${var%%pattern} 删最长后缀——这是提取文件名、路径、扩展名的标准做法。替换用 ${var/pattern/replacement} 替首次、${var//pattern/replacement} 替全部。
## 追问
**1. # 和 ## 删除前缀有什么区别?% 和 %% 呢?**
# 从左删最短匹配,## 删最长匹配。以 path=/a/b/c.txt 为例,${path#*/} 得 a/b/c.txt(删到第一个 /),${path##*/} 得 c.txt(删到最后一个 /)。% 和 %% 从右删,逻辑对称。记忆:# 在键盘左边删左边,% 在右边删右边。
**2. ${var:offset:length} 的 offset 是从 0 还是 1 开始?负数偏移要注意什么?**
offset 从 0 开始。负偏移表示从末尾计数,但冒号后必须加空格:${var: -3} 取末尾3个字符,不加空格会被误认为默认值语法 ${var:-3}。
**3. 怎么提取文件路径中的目录、文件名和扩展名?**
dir=${path%/*}、name=${path##*/}、ext=${path##*.}、base=${name%.*}。这比 dirname/basename 命令更快,因为不需要 fork 子进程。
**4. 字符串替换能用正则吗?**
${var/pattern/replacement} 的 pattern 是 glob 通配符(* ? [abc]),不是正则。需要正则匹配要用 [[ $var =~ regex ]] 配合 BASH_REMATCH 数组,或调用 sed/awk。
## 写段代码
```bash
path="/data/logs/app.access.log"
filename=${path##*/} # app.access.log
dir=${path%/*} # /data/logs
base=${filename%.*} # app.access
ext=${filename##*.} # log
echo "$dir / $base / $ext"
```服务端5月29日 01:19
Shell 脚本中如何定义函数?参数传递和返回值怎么处理?Shell 函数用 name() { } 或 function name { } 定义,调用时直接写函数名。参数通过 $1、$2、$@ 等位置变量访问,不写在括号里。返回值有两个机制:return 只能返回 0-255 的退出码,用于表示成功或失败;要返回字符串或计算结果,需用 echo 输出后由调用方通过 $(func) 命令替换捕获。函数内变量默认是全局的,必须用 local 声明才能限定作用域,这是 Shell 和大多数语言的重要区别。
## 追问
**1. return 和 echo 返回值有什么本质区别?**
return 设置的是函数的退出状态码(0-255),只能用于条件判断(if func; then)。echo 输出到 stdout,由 $(func) 捕获后可当字符串使用。两者常配合:echo 返回结果,return 返回状态。
**2. $@ 和 $* 有什么区别?什么时候加引号?**
不加引号时两者相同,展开为独立单词。加引号后 "$@" 保留每个参数的边界("$1" "$2" ...),"$*" 把所有参数合并为一个字符串("$1 $2 ...")。循环遍历参数时始终用 "$@"。
**3. 函数中不用 local 会怎样?**
变量变成全局的,会污染外部作用域甚至覆盖同名变量。这在调试时极难排查,是 Shell 脚本 bug 的常见来源。建议所有函数内变量都加 local。
**4. 如何在另一个脚本中复用函数?**
将函数写在独立文件中,用 source 或 . 命令导入。也可以用 export -f func_name 将函数导出给子进程,但只对 bash 子进程有效。
**5. 函数能递归调用吗?有什么限制?**
可以递归,但 Shell 没有尾递归优化,深度递归会很快耗尽栈空间。实际中递归深度超过几百层就会报错,复杂数据结构建议用其他语言。
## 写段代码
```bash
# return 退出码 + echo 返回值
grep_key() {
local file=$1 key=$2
grep -q "$key" "$file" || return 1
grep "$key" "$file" | head -1
}
result=$(grep_key /etc/hosts localhost)
if [ $? -eq 0 ]; then
echo "Found: $result"
fi
```服务端5月29日 01:18
Shell 重定向和管道的工作原理是什么?Shell 通过文件描述符(FD)管理数据流:stdin(0) 读入、stdout(1) 正常输出、stderr(2) 错误输出。重定向改变数据流向,> 覆盖写、>> 追加写,2> 重定向错误,2>&1 将 stderr 合并到 stdout,&> 是 Bash 4+ 的简写。管道 | 将左侧 stdout 传给右侧 stdin,但 stderr 不经过管道——需先用 2>&1 转换。/dev/null 是黑洞设备,丢弃输出用 > /dev/null 2>&1。Here Document(<<) 和 Here String(<<<) 用于内联输入,进程替换 <() 让两个命令的输出直接比较而无需临时文件。
## 追问
**管道为什么只传 stdout?如何让 stderr 也通过管道?**
管道连接的是 FD1→FD0,stderr 走 FD2 所以被丢弃。用 `cmd 2>&1 | grep err` 先将 FD2 合并到 FD1 即可。
**set -o pipefail 有什么用?**
默认管道返回最后一个命令的退出码,中间命令失败会被忽略。pipefail 使管道任一命令失败就返回非零,适合脚本严格错误检查。
**进程替换 <() 和管道有什么区别?**
管道是进程间 stdin/stdout 直连,只能单流向;<() 将命令输出映射为临时文件路径,支持多个输入源同时使用,如 `diff <(cmd1) <(cmd2)`。
**exec 3> file.txt 是什么用法?**
exec 打开自定义 FD,后续通过 >&3 写入、<&3 读取,用完 exec 3>&- 关闭。适合脚本中多次读写同一文件避免反复打开。
## 写段代码
```bash
# 分别记录输出和错误,同时用管道过滤错误
./deploy.sh 1>deploy.log 2>&1 | grep -i "error"
# 进程替换对比两个目录
if ! diff <(ls dir1) <(ls dir2); then
echo "目录内容有差异"
fi
```服务端5月29日 01:18
什么是 Shell?常见的 Shell 类型有哪些?Shell 是用户与操作系统内核之间的命令解释器,负责将用户输入的命令翻译给内核执行并返回结果。它同时也是一种脚本语言,可以将命令序列写入文件批量执行。Linux 默认 Shell 通常是 bash,macOS 从 Catalina 起默认切换为 zsh。生产环境中需关注兼容性:写可移植脚本时应以 POSIX sh 为基准,避免使用 bashism。Debian/Ubuntu 的 /bin/sh 实际指向 dash,执行速度比 bash 快但不支持其扩展语法。fish 交互体验好但语法不兼容 POSIX,不适合写通用脚本。
## 追问
**1. /bin/sh 和 /bin/bash 有什么区别?为什么要区分?**
/bin/sh 是 POSIX 标准定义的 Shell,/bin/bash 在其基础上扩展了数组、[[ ]]、(( )) 等特性。脚本首行写 #!/bin/sh 意味着只使用 POSIX 语法,保证跨平台可移植;写 #!/bin/bash 则可以使用 bash 扩展但牺牲了可移植性。
**2. 怎么查看当前系统使用的 Shell?怎么临时切换?**
echo $SHELL 查看默认 Shell,echo $0 查看当前 Shell。临时切换直接输入 bash 或 zsh 即可,永久切换用 chsh -s /bin/zsh。
**3. zsh 相比 bash 有哪些优势?为什么 macOS 要切换?**
zsh 支持更强大的 Tab 补全(命令参数、文件路径)、右侧提示符、插件生态(oh-my-zsh)和递归通配符 **/*.sh。macOS 切换主因是 bash 3.2 受 GPL v3 许可限制无法升级,zsh 采用 MIT 许可更灵活。
**4. dash 为什么比 bash 快?适合什么场景?**
dash 代码量小、不加载交互功能,启动速度快约 4 倍。适合作为 /bin/sh 执行系统启动脚本(如 /etc/init.d/),不适合交互使用。
## 写段代码
```bash
#!/bin/sh
# 检测当前 Shell 并给出建议
case "$SHELL" in
*/bash) echo "当前: bash" ;;
*/zsh) echo "当前: zsh" ;;
*/dash) echo "当前: dash" ;;
*) echo "当前: $SHELL" ;;
esac
```服务端5月28日 02:12
Shell 脚本中如何进行错误处理和调试?有哪些常用的技巧?Shell 脚本没有编译器的类型检查和异常机制,错误一旦发生往往悄无声息地传播,导致脚本在错误的状态下继续运行,产出难以排查的脏数据。所以错误处理和调试能力是区分"能跑的脚本"和"可靠脚本"的分水岭,也是面试中的高频考点。
## 错误处理的核心手段
### set 选项:让脚本在错误面前不再沉默
Shell 默认行为是"命令失败了就失败了,继续执行下一条",这在自动化场景中极其危险。`set` 选项可以从源头改变这个行为:
```bash
set -e # 任何命令返回非零退出码时,脚本立即终止
set -u # 引用未定义变量时报错退出,而非当作空字符串
set -o pipefail # 管道中任意命令失败,整个管道返回失败状态
```
面试中常问的是**为什么要组合使用** `set -euo pipefail`:
- 只用 `set -e` 不够:`false | true` 这条管道,`false` 失败了但管道整体返回 `true` 的退出码 0,`set -e` 不会触发退出。加上 `pipefail` 才能捕获管道内的失败。
- 只用 `set -e` + `pipefail` 仍不够:`echo $UNDEFINED_VAR` 在 `set -u` 缺失时只输出空行,不会报错;如果这个变量是文件路径,后续操作会作用在错误的目标上。
需要注意 `set -e` 的一个坑:它在 `if`、`while`、`&&`、`||` 等条件上下文中不会触发退出,因为 Shell 认为你已经在主动处理该命令的返回值了。
### trap:在脚本退出时做最后一件事
`trap` 用于捕获信号或事件,最常见的用途是资源清理:
```bash
cleanup() {
rm -f "$TEMP_FILE"
echo "Cleaned up temporary resources" >&2
}
trap cleanup EXIT
```
面试追问:`trap` 能捕获哪些信号?`EXIT` 是信号吗?
- `EXIT` 不是真正的信号,而是 Shell 内置的伪信号,在脚本以任何方式退出时都会触发(包括正常退出、`set -e` 触发的退出、未被捕获的信号导致的退出)。
- `INT`(Ctrl+C)、`TERM`(kill 默认信号)是真正的信号,可以在脚本运行中被外部触发。
- `ERR` 是另一个伪信号,仅在命令失败(返回非零)时触发,可以用来做错误日志记录:
```bash
error_handler() {
local line_no=$1
echo "Error at line $line_no" >&2
}
trap 'error_handler $LINENO' ERR
```
### 自定义错误处理函数
对于需要携带上下文信息的错误,封装一个错误处理函数比直接 `exit 1` 更实用:
```bash
die() {
echo "[FATAL] $1 (line: ${BASH_LINENO[0]})" >&2
exit "${2:-1}"
}
# 使用示例
[ -f "$CONFIG" ] || die "Config file not found: $CONFIG"
```
`BASH_LINENO` 是 Bash 的内置数组,`BASH_LINENO[0]` 给出调用者所在行号,比手动标注更可靠。
## 调试的核心手段
### set -x:最常用的调试开关
```bash
# 对整个脚本开启
bash -x script.sh
# 对脚本中某一段开启
set -x
# 需要调试的代码
set +x
```
`set -x` 的输出以 `+` 开头,会显示变量展开后的实际值。`set -v` 则只显示原始输入行,不做变量展开。两者可以组合使用(`set -xv`),但实践中 `set -x` 单独使用频率远高于 `-v`。
### ShellCheck:静态分析比手动调试更高效
面试中提到调试,很多人只想到 `set -x`,但**静态分析工具 ShellCheck** 才是效率最高的手段。它能捕获类型错误、未引用变量、不安全的 `rm` 操作、废弃语法等问题:
```bash
shellcheck myscript.sh
```
安装一次、受益全程。在 CI 流水线中加入 `shellcheck` 检查,可以在代码合并前拦截大量低级错误,比运行时调试成本低得多。
### 条件式调试输出
生产环境不能到处加 `echo`,一个受环境变量控制的调试函数更合适:
```bash
debug() {
[ "${DEBUG:-0}" = "1" ] && echo "[DEBUG] $*" >&2
return 0
}
```
关键细节:用 `${DEBUG:-0}` 提供默认值,避免 `set -u` 下未定义变量报错;输出重定向到 `>&2`(标准错误),不污染标准输出的正常数据流。
## 面试中容易忽略的点
**管道中的错误处理**:这是面试高频追问。`set -e` + `set -o pipefail` 是基本答案,但更完整的做法是用 `PIPESTATUS` 数组获取管道中每个命令的退出码:
```bash
grep "error" app.log | sort | uniq -c
echo "Exit codes: ${PIPESTATUS[@]}"
```
**子 Shell 中的 set 选项不向上传播**:管道中的每个命令运行在子 Shell 中,`set -e` 在子 Shell 中触发退出只退出子 Shell,不会导致父脚本退出。这是 `set -o pipefail` 存在的原因之一。
**临时文件的安全创建**:`mktemp` 比 `/tmp/myfile.$$` 更安全,后者存在符号链接攻击风险:
```bash
TEMP_FILE=$(mktemp) || die "Failed to create temp file"
trap 'rm -f "$TEMP_FILE"' EXIT
```
## 一个可靠脚本的骨架
把以上手段组合起来,得到一个可以直接复用的模板:
```bash
#!/usr/bin/env bash
set -euo pipefail
# === 错误处理 ===
die() {
echo "[FATAL] $*" >&2
exit 1
}
# === 资源清理 ===
TEMP_FILE=""
cleanup() {
[ -n "$TEMP_FILE" ] && rm -f "$TEMP_FILE"
}
trap cleanup EXIT INT TERM
# === 调试支持 ===
debug() {
[ "${DEBUG:-0}" = "1" ] && echo "[DEBUG] $*" >&2
return 0
}
# === 主逻辑 ===
main() {
local input="${1:-}"
[ -n "$input" ] || die "Usage: $0 <input>"
[ -f "$input" ] || die "File not found: $input"
TEMP_FILE=$(mktemp) || die "mktemp failed"
debug "Created temp file: $TEMP_FILE"
# 业务逻辑 ...
debug "Done"
}
main "$@"
```
面试中给出这个骨架,再配合对每个 `set` 选项和 `trap` 信号的讲解,基本能覆盖错误处理和调试这两个考点的全部内容。
服务端5月28日 02:12
Shell 脚本中如何进行进程管理?如何启动、监控和终止进程?Shell 进程管理是运维和后端面试的高频考点,核心考查你对进程生命周期、信号机制和作业控制的理解。
## 进程启动
Shell 中启动进程有前台、后台和脱离终端三种方式,区别在于进程与当前 shell 的绑定关系。
### 前台与后台执行
前台执行会阻塞当前 shell,后台执行在命令末尾加 `&`,shell 立即返回控制权:
```bash
# 前台执行(阻塞 shell)
./long_task.sh
# 后台执行(shell 不阻塞)
./long_task.sh &
echo "后台进程 PID: $!"
```
`$!` 保存最近一个后台进程的 PID,是进程管理的关键变量。
### nohup 与 disown:脱离终端
用户退出终端时,shell 会向所有子进程发送 SIGHUP 信号,导致后台进程一并终止。`nohup` 和 `disown` 用于解决这个问题:
```bash
# nohup:启动时即忽略 SIGHUP,输出重定向到 nohup.out
nohup ./long_task.sh > task.log 2>&1 &
# disown:对已启动的后台进程脱离 shell 控制
./long_task.sh &
disown %1
# disown -h:标记为不接收 SIGHUP,但仍在 jobs 列表中
./another_task.sh &
disown -h %2
```
**面试追问**:nohup 和 disown 有什么区别?
> nohup 在启动时就屏蔽 SIGHUP,是预防性的;disown 是事后将已有作业从 shell 的作业表移除,是补救性的。nohup 还会自动重定向输出,disown 不会。
### 子 shell 与进程替换
括号 `()` 创建子 shell,子 shell 中的变量和目录切换不影响父 shell:
```bash
# 子 shell 中修改变量不影响父进程
( export MY_VAR="child"; echo "子: $MY_VAR" )
echo "父: $MY_VAR" # 空
# 用子 shell 实现并行执行
( task_a.sh ) &
( task_b.sh ) &
wait
echo "两个任务都完成了"
```
## 进程监控
### 查看进程信息
`ps` 是最基础的进程查看命令,不同参数组合提供不同维度的信息:
```bash
# 查看所有进程的完整信息
ps aux
# 查看进程树(显示父子关系)
ps auxf
# 自定义输出列
ps -eo pid,ppid,user,%cpu,%mem,cmd --sort=-%cpu | head
# 查看指定进程的详细信息
ps -p $(pgrep -d, nginx) -o pid,ppid,state,%cpu,%mem,cmd
```
**面试追问**:ps aux 和 ps -ef 有什么区别?
> 两者都显示所有进程,但格式不同。`ps aux` 是 BSD 风格,`aux` 不加横杠,输出包含 %CPU、%MEM 等资源占用列;`ps -ef` 是 System V 风格,`-ef` 加横杠,输出包含 PPID 列但无资源占用。实际工作中 `ps aux` 更常用。
### 进程查找
```bash
# pgrep:按名称模式查找,直接输出 PID
pgrep -f "python.*app.py"
# pidof:精确匹配进程名
pidof nginx
# pgrep -P:查找子进程
pgrep -P $PARENT_PID
```
### 实时监控
`top` 和 `htop` 提供实时视图。脚本中更常用的是定时轮询:
```bash
# 检查进程是否存活
is_alive() {
kill -0 "$1" 2>/dev/null
}
if is_alive 12345; then
echo "进程 12345 正在运行"
fi
```
`kill -0` 不发送任何信号,仅检测进程是否存在,是脚本中判断进程存活的标准做法。
## 进程终止与信号
### 信号机制
信号是进程间通信的基础机制,理解信号是进程管理的核心:
```bash
# 列出所有信号
kill -l
# 常用信号
# SIGHUP (1) - 挂起,常用于通知守护进程重载配置
# SIGINT (2) - 中断,等同于 Ctrl+C
# SIGTERM (15) - 优雅终止,进程可捕获并做清理
# SIGKILL (9) - 强制终止,进程无法捕获或忽略
# SIGSTOP (19) - 暂停,进程无法捕获
# SIGCONT (18) - 继续执行被暂停的进程
```
**面试追问**:SIGTERM 和 SIGKILL 有什么区别?为什么不推荐直接用 kill -9?
> SIGTERM (15) 是优雅终止信号,进程可以捕获该信号并执行清理操作(关闭连接、保存状态、释放锁),然后主动退出。SIGKILL (9) 是强制终止,进程无法捕获或忽略,操作系统立即回收资源。直接使用 kill -9 会导致:1)进程来不及释放资源(锁文件、临时文件、socket 连接);2)子进程可能变成孤儿进程;3)数据库等应用可能损坏数据文件。正确做法是先 kill(默认 SIGTERM),等待超时后再 kill -9。
### 终止进程的多种方式
```bash
# 按 PID 终止
kill 12345 # SIGTERM
kill -9 12345 # SIGKILL
# 按名称终止
pkill -f "python.*app.py" # 匹配命令行模式
killall nginx # 精确匹配进程名
# 终止进程组
kill -- -$PGID # 向进程组发送信号
# 优雅重启
kill -HUP $(cat /var/run/nginx.pid)
```
### wait 与退出码
`wait` 命令等待后台进程结束并获取其退出码:
```bash
./task_a.sh & PID_A=$!
./task_b.sh & PID_B=$!
wait $PID_A; STATUS_A=$?
wait $PID_B; STATUS_B=$?
if [ $STATUS_A -eq 0 ] && [ $STATUS_B -eq 0 ]; then
echo "全部成功"
else
echo "task_a 退出码: $STATUS_A, task_b 退出码: $STATUS_B"
fi
```
## 僵尸进程与孤儿进程
这是面试中区分候选人水平的关键考点。
### 僵尸进程(Zombie)
子进程退出后,父进程必须调用 `wait()` 读取其退出状态,否则子进程的进程描述符会残留在系统中,成为僵尸进程(状态 Z):
```bash
# 查找僵尸进程
ps aux | awk "$8 ~ /Z/ {print}"
# 或用更精确的方式
ps -eo pid,ppid,stat,cmd | awk "$3 ~ /Z/ {print}"
```
僵尸进程本身已死,不占 CPU 和内存,但占用 PID 资源和内核进程表项。大量僵尸进程会耗尽 PID,导致无法启动新进程。
**解决方法**:
1. 修复父进程,让其正确调用 `wait()`/`waitpid()`
2. 杀死父进程,僵尸进程会被 init 进程(PID 1)接管并回收
3. 在代码中设置 `SIGCHLD` 信号处理函数自动回收
```bash
# Shell 中预防僵尸进程
trap "wait" CHLD
```
### 孤儿进程(Orphan)
父进程先于子进程退出,子进程被 init 进程(PID 1)收养,成为孤儿进程。孤儿进程不是问题进程,init 会负责回收。但在容器环境中,PID 1 可能不是 init,需要特别注意:
```bash
# 容器中用 init 系统确保孤儿进程被回收
# Dockerfile 中使用
cmd ["/sbin/tini", "--", "./app.sh"]
```
**面试追问**:僵尸进程和孤儿进程有什么区别?
> 僵尸进程是子进程已死但父进程未回收其状态,进程还在进程表中但已不运行;孤儿进程是父进程已死,子进程还在运行,被 init 接管。僵尸进程是问题需要处理,孤儿进程是正常现象。
## 进程优先级
Linux 用 nice 值控制进程优先级,范围 -20(最高优先级)到 19(最低优先级),默认 0:
```bash
# 以低优先级启动进程
nice -n 10 ./heavy_task.sh &
# 修改运行中进程的优先级
renice -n 5 -p 12345
# 查看进程优先级
ps -eo pid,nice,cmd
```
普通用户只能降低优先级(增大 nice 值),只有 root 可以提高优先级(减小 nice 值)。
## 信号捕获与 trap
`trap` 命令捕获信号,实现优雅退出是面试常考点:
```bash
#!/bin/bash
cleanup() {
echo "清理临时文件..."
rm -f /tmp/my_script_*.lock
rm -f /tmp/my_script_*.tmp
echo "清理完成"
exit 0
}
# 捕获 SIGINT、SIGTERM 和脚本退出
trap cleanup INT TERM EXIT
# 创建锁文件防止重复运行
LOCK_FILE="/tmp/my_script.lock"
if [ -f "$LOCK_FILE" ]; then
OLD_PID=$(cat "$LOCK_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
echo "脚本已在运行 (PID: $OLD_PID)"
exit 1
fi
fi
echo $$ > "$LOCK_FILE"
# 主逻辑
while true; do
echo "工作中... $(date)"
sleep 5
done
```
**面试追问**:trap 中捕获 EXIT 信号和捕获 TERM 有什么区别?
> EXIT 是 shell 特殊信号,在脚本以任何方式退出时(正常结束、exit 命令、收到信号等)都会触发。TERM 只在收到 SIGTERM 时触发。捕获 EXIT 可以确保无论脚本如何退出,清理逻辑都能执行。
## 进程间通信
### 管道
匿名管道是最常用的 IPC 方式,命名管道(FIFO)支持无亲缘关系进程间通信:
```bash
# 匿名管道
cat access.log | awk "{print \$7}" | sort | uniq -c | sort -rn | head
# 命名管道
mkfifo /tmp/task_pipe
# 生产者
echo "task_data" > /tmp/task_pipe &
# 消费者
while read line; do
process "$line"
done < /tmp/task_pipe
```
### 进程替换
进程替换是 Bash 的高级特性,将进程的输出映射为临时文件描述符:
```bash
# 比较两个命令的输出
diff <(sort file1.txt) <(sort file2.txt)
# 合并多个进程的输出
paste <(command_a) <(command_b)
```
## /proc 文件系统
`/proc` 提供内核和进程的实时信息,面试中常问如何不依赖工具获取进程信息:
```bash
# 查看进程的命令行参数
cat /proc/12345/cmdline | tr "\0" " "
# 查看进程的环境变量
cat /proc/12345/environ | tr "\0" "\n"
# 查看进程的文件描述符
ls -la /proc/12345/fd/
# 查看进程的内存映射
cat /proc/12345/maps
# 查看进程状态
cat /proc/12345/status | head -20
```
## 实战场景
### 守护进程管理脚本
```bash
#!/bin/bash
APP_NAME="my_app"
APP_CMD="/usr/local/bin/my_app"
PID_FILE="/var/run/${APP_NAME}.pid"
LOG_FILE="/var/log/${APP_NAME}.log"
start() {
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
echo "$APP_NAME 已在运行 (PID: $(cat $PID_FILE))"
return 1
fi
nohup $APP_CMD >> "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
echo "$APP_NAME 已启动 (PID: $!)"
}
stop() {
if [ ! -f "$PID_FILE" ]; then
echo "$APP_NAME 未运行"
return 1
fi
local pid=$(cat "$PID_FILE")
kill "$pid"
local timeout=10
while [ $timeout -gt 0 ] && kill -0 "$pid" 2>/dev/null; do
sleep 1
((timeout--))
done
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid"
echo "$APP_NAME 强制终止"
else
echo "$APP_NAME 已停止"
fi
rm -f "$PID_FILE"
}
restart() {
stop
sleep 1
start
}
case "$1" in
start) start ;;
stop) stop ;;
restart) restart ;;
status)
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
echo "$APP_NAME 运行中 (PID: $(cat $PID_FILE))"
else
echo "$APP_NAME 未运行"
fi
;;
*) echo "用法: $0 {start|stop|restart|status}" ;;
esac
```
### 并发控制
```bash
#!/bin/bash
MAX_PARALLEL=4
running=0
for task in tasks/*.sh; do
bash "$task" &
((running++))
if [ $running -ge $MAX_PARALLEL ]; then
wait -n # 等待任意一个后台进程完成
((running--))
fi
done
wait # 等待所有完成
echo "全部任务完成"
```
`wait -n` 是 Bash 4.3+ 的特性,等待任意一个后台进程完成,是并发控制的关键。
### 进程超时控制
```bash
# 使用 timeout 命令
timeout 30s ./may_hang.sh
# 手动实现超时
run_with_timeout() {
local cmd="$1"
local timeout=$2
$cmd &
local pid=$!
(
sleep $timeout
kill -9 $pid 2>/dev/null
) &
local watchdog=$!
wait $pid 2>/dev/null
local exit_code=$?
kill $watchdog 2>/dev/null
return $exit_code
}
run_with_timeout "./long_task.sh" 60服务端5月28日 00:52
Shell 脚本中 $0、$1、$@、$*、$#、$? 等特殊变量分别是什么含义?Shell 脚本中 $0、$1、$2、$@、$*、$#、$? 等特殊变量是处理命令行参数和控制脚本流程的核心工具。掌握它们的含义和用法,是写好 Shell 脚本的基本功,也是运维和后端面试的高频考点。
## 一图总览
| 变量 | 含义 | 常见用途 |
|------|------|----------|
| $0 | 脚本文件名或调用路径 | 日志输出、Usage 提示 |
| $1~$n | 第 1 到第 n 个位置参数 | 接收脚本入参 |
| $# | 参数个数 | 参数校验 |
| $@ | 所有参数(各自独立) | 遍历参数 |
| $* | 所有参数(合并为一个字符串) | 拼接参数 |
| $? | 上一条命令的退出状态码 | 判断命令是否成功 |
| $$ | 当前 Shell 进程 PID | 生成临时文件 |
| $! | 最近一个后台进程的 PID | 跟踪后台任务 |
| $- | 当前 Shell 选项标志 | 调试模式检测 |
| $_ | 上一条命令的最后一个参数 | 快速复用参数 |
## 位置参数:$0、$1、$2、...
### $0 — 脚本自身的名称
$0 保存的是调用脚本时使用的路径,不一定是脚本的真实文件名。
```bash
#!/bin/bash
echo "I am: $0"
```
```bash
# 不同调用方式,$0 的值不同
$ ./script.sh # $0 = ./script.sh
$ /tmp/script.sh # $0 = /tmp/script.sh
$ bash script.sh # $0 = script.sh
$ source script.sh # $0 = /bin/bash(当前 shell 名)
```
用 `basename` 可以提取纯文件名,常用于 Usage 提示:
```bash
echo "Usage: $(basename "$0") <file> <pattern>"
```
### $1, $2, ..., ${10} — 位置参数
$1 是第一个参数,$2 是第二个,依此类推。第 10 个及以后的参数必须用花括号:${10}。
```bash
#!/bin/bash
echo "First: $1"
echo "Second: $2"
echo "Tenth: ${10}"
```
```bash
$ ./greet.sh Alice Engineer Beijing
# First: Alice
# Second: Engineer
```
注意:如果脚本未传参,$1 为空字符串,不会报错。这在脚本中容易埋坑,建议始终做参数校验。
### shift — 轮转位置参数
`shift` 命令将所有位置参数左移一位:$2 变成 $1,$3 变成 $2,$0 不受影响。
```bash
#!/bin/bash
while [ $# -gt 0 ]; do
echo "Processing: $1"
shift
done
```
```bash
$ ./loop.sh a b c
# Processing: a
# Processing: b
# Processing: c
```
这在解析带参数的选项时特别有用,比如 `--port 8080 --host localhost`。
## 特殊参数:$#、$@、$*、$?
### $# — 参数个数
```bash
#!/bin/bash
if [ $# -lt 2 ]; then
echo "Usage: $0 <source> <dest>"
exit 1
fi
echo "Copying $1 to $2..."
```
$# 配合参数校验是脚本健壮性的第一道防线。
### $@ 和 $* — 所有参数
这是最容易混淆的一对。不加引号时两者表现相同,加了双引号后行为完全不同:
```bash
#!/bin/bash
echo '--- Without quotes ---'
for arg in $@; do
echo " [$arg]"
done
echo '--- With quotes: "$@" ---'
for arg in "$@"; do
echo " [$arg]"
done
echo '--- With quotes: "$*" ---'
for arg in "$*"; do
echo " [$arg]"
done
```
```bash
$ ./test.sh "hello world" foo bar
# --- Without quotes ---
# [hello]
# [world] ← 空格被拆分了
# [foo]
# [bar]
# --- With quotes: "$@" ---
# [hello world] ← 参数保持独立
# [foo]
# [bar]
# --- With quotes: "$*" ---
# [hello world foo bar] ← 所有参数合并成一个字符串
```
核心区别一句话:**"$@" 保留每个参数的独立性,"$*" 把所有参数合并为一个字符串**。
"$*" 合并时用什么分隔?由 IFS(内部字段分隔符)的第一个字符决定,默认是空格。
```bash
IFS='|'
echo "$*"
# 输出: hello world|foo|bar
```
实际开发中,99% 的场景应该用 "$@"。只有在需要把所有参数拼接成一个字符串时才用 "$*"。
### $? — 上一条命令的退出状态码
0 表示成功,非 0 表示失败。Linux 约定 1~255 为不同的错误类型。
```bash
mkdir /root/test 2>/dev/null
if [ $? -ne 0 ]; then
echo "Failed to create directory (exit code: $?)"
fi
```
更简洁的写法是直接判断:
```bash
if mkdir /root/test 2>/dev/null; then
echo "OK"
else
echo "Failed"
fi
```
常见退出码含义:1 一般错误,2 命令用法错误,126 权限不足,127 命令未找到,128+N 信号 N 导致的退出(如 128+9=137 表示被 SIGKILL 杀掉)。
### $$ 和 $! — 进程 ID
```bash
#!/bin/bash
# $$:当前脚本的 PID,常用于生成唯一的临时文件
TMPFILE="/tmp/myapp_$$.tmp"
echo "Processing..." > "$TMPFILE"
# $!:最近一个后台进程的 PID
sleep 60 &
BG_PID=$!
echo "Background task PID: $BG_PID"
# 可以在需要时杀掉后台任务
# kill $BG_PID
```
注意:在子 shell ( ) 中,$$ 仍然是父 shell 的 PID,不是子 shell 的。如果需要子 shell 的真实 PID,用 `$BASHPID`。
### $- 和 $_
**$-** 显示当前 Shell 启用的选项标志:
```bash
echo $-
# 常见输出: himBHs
# h: 记录命令历史 i: 交互式 m: 作业控制
# B: 花括号展开 H: 历史替换 s: 从 stdin 读命令
```
可以用来检测脚本是否在调试模式:
```bash
if [[ $- == *x* ]]; then
echo "Debug mode is ON"
fi
```
**$_** 保存上一条命令的最后一个参数:
```bash
ls -la /etc/passwd
echo $_ # 输出: /etc/passwd
mkdir -p /very/long/path/to/dir
cd $_ # 快速进入刚创建的目录
```
## 常见陷阱与避坑
### 陷阱 1:$@ 不加引号
```bash
# 错误:带空格的参数会被拆分
for arg in $@; do
echo "$arg"
done
# 正确:始终加双引号
for arg in "$@"; do
echo "$arg"
done
```
### 陷阱 2:$0 不可靠
$0 的值取决于调用方式,不要用它判断脚本的真实路径。获取真实路径的正确方式:
```bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
```
### 陷阱 3:$? 被覆盖
$? 只保存最近一条命令的状态,必须紧跟要判断的命令使用:
```bash
# 错误:echo 会覆盖 $?
some_command
echo "Result: $?" # 这里 $? 已经是 echo 的状态
if [ $? -ne 0 ]; then ... # 永远为 0
# 正确:立即保存
some_command
result=$?
if [ $result -ne 0 ]; then
echo "Failed with code $result"
fi
```
### 陷阱 4:位置参数越界
```bash
# 错误:$10 会被解析为 $1 后面跟字符 "0"
echo $10
# 正确:用花括号
echo ${10}
```
## 综合实战脚本
```bash
#!/bin/bash
set -euo pipefail
# 参数校验
if [ $# -lt 2 ]; then
echo "Usage: $(basename "$0") <dir> <pattern> [files...]"
exit 1
fi
SEARCH_DIR="$1"
PATTERN="$2"
shift 2 # 移除已处理的参数,剩余的就是文件列表
echo "=== Search Info ==="
echo "Script: $(basename "$0")"
echo "PID: $$"
echo "Search dir: $SEARCH_DIR"
echo "Pattern: $PATTERN"
echo "Extra files: $*"
echo "File count: $#"
# 在指定目录搜索
grep -rn "$PATTERN" "$SEARCH_DIR"
if [ $? -eq 0 ]; then
echo "Pattern found."
else
echo "Pattern not found (exit code: $?)"
fi
# 处理额外的文件
for file in "$@"; do
if [ -f "$file" ]; then
echo "Processing: $file"
else
echo "Skip (not found): $file"
fi
done
echo "Done."
```
## "$@" 与 "$*" 速查对比
| 对比项 | "$@" | "$*" |
|--------|------|------|
| 引号中的行为 | 每个参数独立保留 | 合并为一个字符串 |
| 空格处理 | 保持参数中的空格 | 参数间用 IFS 首字符连接 |
| 遍历结果 | 逐个参数 | 整体一次 |
| 推荐程度 | 推荐 | 仅在拼接场景使用 |
记住一条原则:**遍历参数用 "$@",拼接参数用 "$*"**。