标签

Shell

Shell 是一个命令行解释器,它提供了一个用户界面,用于访问操作系统的服务。在 shell 中,用户可以输入命令、执行程序和管理文件系统。Shell 也可以运行存储在文本文件中的命令序列,这些文本文件通常被称为 shell 脚本或批处理文件。

Shell
服务端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 首字符连接 | | 遍历结果 | 逐个参数 | 整体一次 | | 推荐程度 | 推荐 | 仅在拼接场景使用 | 记住一条原则:**遍历参数用 "$@",拼接参数用 "$*"**。