Shell 与 Python 运维脚本

共 17 题
📑 题目列表 17 题
#
★★★

1. Shell 脚本的健壮性中 set -euo pipefail 与错误处理如何配合?

set -euo pipefail 各选项分别解决什么问题?Shell 脚本的健壮错误处理还应该包括哪些实践?

  • set -e(errexit)、-u(nounset)、-o pipefail 的语义与副作用
  • 常见坑:命令替换中的失败、if 条件中的失败命令、管道失败
  • 配套实践:set -x 调试、trap ERR/EXIT 清理、错误消息输出

set -euo pipefail 是 Shell 脚本健壮性的三件套:set -e 让脚本在任何命令返回非零时立即退出(避免"失败后继续跑"造成连锁错误);set -u 把使用未定义变量当作错误退出(捕获拼写错误与未初始化变量);set -o pipefail 让管道返回"最后一个非零命令"的退出码(默认管道只返回最后一条命令的退出码,中间命令失败会被掩盖)。三者配合后,脚本默认"失败即停、未定义即报、管道不漏错"。注意 set -e 的边界:if/while 条件、&&/|| 左侧、命令替换(取决于版本与上下文)中的失败不会被触发,需要显式处理;临时放行用 cmd || trueset +e 局部关闭。

健壮性配套实践:用 set -x 调试跟踪执行;trap '...' ERR 在错误时打印脚本名/行号(结合 $LINENO、$BASH_SOURCE)便于定位;trap cleanup EXIT 在退出时清理临时文件与恢复环境;关键命令加显式检查(if ! cmd; then echo "fail" >&2; exit 1; fi);输出统一走 stderr(>&2)避免污染 stdout 管道;使用 local 限定函数变量、引号包裹所有变量("$var")防分词与通配符扩展;对外部命令返回值(如 curl、rsync)显式校验;脚本开头声明 #!/usr/bin/env bash 与执行前 shellcheck 静态检查。错误处理的目标是"可读、可定位、可重试"。

本题考察 Shell 脚本的健壮性工程。回答要点:三个 set 选项的准确语义与边界(尤其 set -e 的豁免场景与 pipefail 对中间命令失败的捕获)、trap/set -x/引号/local 等配套实践,体现对"脚本事故预防"的系统性理解。

#!/usr/bin/env bash
set -euo pipefail
trap 'echo "ERR at line $LINENO" >&2' ERR
trap cleanup EXIT
cleanup() { rm -f "$TMPFILE"; }
readonly TMPFILE=$(mktemp)
if ! curl -fsS -o "$TMPFILE" "$URL"; then
  echo "download failed" >&2
  exit 1
fi
#
★★

2. Python 脚本中 asyncio 如何提升高 IO 场景的并发吞吐?

Python 的 asyncio 如何提升高 IO 场景的并发吞吐?它的适用边界与常见坑是什么?

  • 协程与事件循环:IO 等待期间让出控制权
  • 适用场景:网络请求、文件/数据库 IO 等阻塞型等待
  • 边界:CPU 密集不适用;asyncio.run/gather 的用法与阻塞调用阻塞循环的坑

asyncio 基于协程 + 单线程事件循环:任务执行到 await 处(IO 等待)时主动让出控制权,事件循环转去执行其他就绪协程,IO 完成后再切回,从而在单线程内并发处理大量 IO 等待,消除"阻塞式串行等待"带来的吞吐损失。对高并发网络请求(HTTP API 调用、连接池)、高并发文件/数据库 IO 等场景,asyncio(配合 aiohttp、asyncpg、aiomysql 等异步库)能显著提升 QPS 与资源利用率,且无需多线程的锁与上下文切换开销。典型结构:asyncio.run(main()) 启动,asyncio.gather(*tasks) 并发收集结果,asyncio.Semaphore 控制并发上限防压垮下游。

边界与坑:asyncio 只对"可等待的异步 IO"有效——在协程里调用同步阻塞库(time.sleep、requests、同步文件 IO)会阻塞整个事件循环,等于并发归零,必须用异步替代库或丢到线程池(loop.run_in_executor);CPU 密集计算不适用(GIL 与单线程),应改用 multiprocessing;调试用 asyncio.run 与创建/清理循环(asyncio.new_event_loop/close)避免资源泄漏;生产注意超时(asyncio.wait_for)与异常聚合(gather 的 return_exceptions),防止单个任务失败拖垮整体。

本题考察异步编程的适用性与原理。回答要点:事件循环"等待时让出"的并发模型、适用场景(IO 密集)与性能收益来源、以及"同步阻塞调用会卡死循环、CPU 密集不适用、超时与并发控制"等工程坑,体现对 asyncio 收益边界的准确判断。

import asyncio, aiohttp
async def fetch(sem, session, url):
    async with sem:
        async with session.get(url) as r:
            return await r.text()
async def main():
    sem = asyncio.Semaphore(20)
    async with aiohttp.ClientSession() as s:
        return await asyncio.gather(*(fetch(sem, s, u) for u in urls), return_exceptions=True)
asyncio.run(main())
#
★★

3. Python 脚本中 concurrent.futures 如何实现并发任务?

Python 的 concurrent.futures 如何实现并发任务?ThreadPoolExecutor 与 ProcessPoolExecutor 如何选型?

  • 两种 Executor 的机制:线程池(共享内存、受 GIL)与进程池(独立解释器、多核并行)
  • submit/map 与 Future、as_completed 的用法
  • 选型:IO 密集用线程池、CPU 密集用进程池

concurrent.futures 提供统一的高级并发接口:ThreadPoolExecutor 用线程池执行任务(任务共享进程内存,受 GIL 限制——纯 Python 计算无法并行,但 IO 阻塞时 GIL 会释放,因此适合 IO 密集);ProcessPoolExecutor 用进程池(每进程独立解释器,绕过 GIL,真正并行利用多核,适合 CPU 密集)。用法:submit(fn, *args) 返回 Future 对象(.result() 阻塞取结果、.done() 查询状态),map() 按序批量执行,as_completed(futures) 按完成顺序迭代(先完成的先处理),配合 timeout 与异常捕获(future.exception())实现可控并发。

选型依据:任务以等待为主(网络、磁盘、DB、外部 API)→ ThreadPoolExecutor(创建线程便宜,IO 等待不占 CPU);任务以计算为主(解析大文件、复杂处理、压缩加密)→ ProcessPoolExecutor(多核并行),注意进程池的序列化开销(参数与返回值需 pickle,大数据量时成本高)与平台差异(Windows 需 if name == 'main' 保护)。工程细节:池大小按场景定(IO 密集可数十到数百线程,CPU 密集取核数)、统一超时与异常聚合(wait/with 上下文管理自动 shutdown)、监控任务积压(队列深度)防止无界堆积。

本题考察 Python 并发编程的工程选型。回答要点:两种 Executor 的机制差异(GIL 对计算并行的限制、进程隔离)、submit/map/as_completed 的用法、按 IO/CPU 密集选型与序列化/超时等工程细节,体现并发任务的规范实现。

from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch(url): ...            # IO 密集
with ThreadPoolExecutor(max_workers=20) as ex:
    futs = {ex.submit(fetch, u): u for u in urls}
    for fut in as_completed(futs, timeout=60):
        print(futs[fut], fut.result())
# CPU 密集改用 ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor
#
★★

4. Python 脚本中 multiprocessing 如何利用多核处理 CPU 密集任务?

Python 的 multiprocessing 如何利用多核处理 CPU 密集任务?进程间通信与数据传递有哪些方式?

  • 进程池(Pool)与 GIL 绕过原理
  • 数据传递:参数序列化、Queue/Pipe 通信、共享内存(Value/Array、SharedMemory)
  • 注意点:启动方式(fork/spawn)、pickle 限制、资源开销

multiprocessing 通过多进程绕过 GIL:每个进程有独立解释器与内存空间,可真正并行利用多核。CPU 密集场景典型用法是进程池 Pool(processes=N) + map/imap(任务分发到子进程并行计算,结果回收),或 Process 直接启动任务;子进程数一般取物理核数或核数减一(留系统余量),注意计算任务本身的开销要足够大,否则进程创建与调度成本吃掉收益。数据传递:任务参数与返回值经 pickle 序列化传递(大数据量是瓶颈,宜拆分任务粒度);进程间通信用 Queue/Pipe(消息传递)或共享内存(multiprocessing.Value/Array、SharedMemory 更高效,配合 Lock 防竞争)。

工程要点:启动方式——Linux 默认 fork(继承内存快照,需注意文件描述符与锁的继承问题),macOS/Windows 用 spawn(需 if __name__ == '__main__' 保护入口);pickle 限制——lambda、局部函数、部分对象无法序列化,任务函数必须模块级定义;资源开销——每进程一份解释器与内存拷贝(fork 时写时复制,实际内存按需),大量进程注意内存与 fd 上限;异常处理——imap 的迭代中断与结果顺序、任务抛异常会传播到主进程,需捕获;结合 concurrent.futures.ProcessPoolExecutor 可获得更简洁的 Future 接口。

本题考察多进程并行的完整工程。回答要点:进程池并行与 GIL 绕过的原理、任务序列化与通信方式(pickle、Queue/SharedMemory)、启动方式差异与内存/异常等工程坑,体现对多进程范式的准确掌握。

import multiprocessing as mp
def cpu_task(x): return sum(i*i for i in range(x))
if __name__ == '__main__':
    with mp.Pool(mp.cpu_count()) as pool:
        results = pool.map(cpu_task, [10_000_000] * 8)
    # 共享内存示例
    arr = mp.Array('d', 100, lock=mp.Lock())
#
★★

5. Python 脚本中 pathlib 如何简化路径处理?

Python 的 pathlib 如何简化路径处理?相比 os.path 有哪些优势,常用 API 有哪些?

  • Path 对象的面向对象语义与跨平台
  • 常用 API:joinpath、read_text/write_text、glob/rglob、mkdir/exists/unlink
  • 与 os.path 的兼容转换(os.fspath、str 转换)

pathlib 把路径抽象为 Path 对象,用面向对象与运算符语义替代 os.path 的字符串拼接:Path("/data") / "logs" / "app.log"(/ 运算符拼接)、.parent/.name/.suffix/.stem(路径部件访问)、.joinpath()Path.home()/Path.cwd()。读写类 API 直接封装:read_text()/write_text()/read_bytes()/write_bytes()(自带编码与关闭处理,比 open 简洁)、glob("*.log")/rglob("**/*.log")(模式匹配,返回迭代器)、mkdir(parents=True, exist_ok=True)(递归创建)、exists()/is_file()/is_dir()/unlink()/rename()resolve()(绝对化解析符号链接)、stat()touch()。跨平台自动处理分隔符(Windows 反斜杠与 POSIX 斜杠),天然规避"字符串拼接路径"的经典 bug。

实践要点:Path 可直接传给 open、os 模块函数(自动经 fspath 转换);需要 str 时用 str(p);对系统调用(如 subprocess 参数)传 Path 同样可转换;Path.glob 与 shell 通配差异(不递归需 rglob);性能上频繁路径操作差异可忽略,优先可读性。现代运维脚本(Python 3.6+)建议统一用 pathlib 处理路径,配合 context manager(with p.open() as f)与异常捕获(FileNotFoundError)实现健壮文件操作。

本题考察现代 Python 路径处理的正确姿势。回答要点:Path 对象语义(/ 拼接、部件访问)、常用 API 的能力清单(读写、匹配、创建、判断)、跨平台与 os.path 兼容转换,体现"少写字符串拼接"的工程习惯。

from pathlib import Path
base = Path("/var/log") / "myapp"
for f in base.glob("*.log"):
    print(f.name, f.stat().st_size)
out = Path("result.txt")
out.write_text("hello\n", encoding="utf-8")
new_dir = Path("/tmp/x/y/z")
new_dir.mkdir(parents=True, exist_ok=True)
print(out.resolve(), out.suffix)
#
★★

6. Python 脚本中 retry/tenacity 如何实现重试与退避策略?

Python 的 retry/tenacity 库如何实现重试与退避策略?装饰器常用参数与适用场景是什么?

  • tenacity 装饰器核心参数:stop、wait、retry
  • 退避策略:固定/指数/抖动(wait_exponential、jitter)
  • 适用场景与注意点:幂等性、超时兜底、日志与观测

tenacity 是 Python 重试的标准库:用装饰器声明重试策略——@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=30), retry=retry_if_exception_type(ConnectionError))。三组核心参数:stop 定义停止条件(stop_after_attempt 次数 / stop_after_delay 时长 / stop_any 组合);wait 定义重试间隔(wait_fixed 固定、wait_exponential 指数退避、wait_random 抖动 jitter——指数 + 随机抖动可避免"重试风暴"同频打爆下游);retry 定义触发条件(retry_if_exception_type 按异常类型、retry_if_result 按返回值)。还支持 before_sleep 钩子打印重试日志、reraise 决定最后一次异常是否继续抛出、retry_error_callback 兜底返回。

工程要点:重试只对"可重试失败"有意义——连接超时、5xx、瞬时抖动可重试;业务性错误(4xx、参数错误、校验失败)重试无意义;操作必须幂等(重试可能重复执行写操作,需配合请求 ID 或先查后写);重试与超时配合(单次请求超时 < 总重试预算,wait_for 兜底);抖动 jitter 是防雪崩的关键(指数退避 + 随机偏移);重试次数与间隔按 SLO 设计(如 3 次、共约 10 秒内完成),并记录重试次数指标供监控(暴露给 Prometheus 等)。retry 库(旧版)与 tenacity API 类似,新项目用 tenacity。

本题考察重试机制的工程实现。回答要点:stop/wait/retry 三组参数语义、指数退避与抖动防雪崩的原理、幂等性与超时预算的边界设计,以及重试观测,体现对"重试是双刃剑"的理解。

from tenacity import retry, stop_after_attempt, wait_exponential, wait_random, retry_if_exception_type
@retry(stop=stop_after_attempt(3),
       wait=wait_exponential(multiplier=1, max=30) + wait_random(0, 2),
       retry=retry_if_exception_type((TimeoutError, ConnectionError)),
       reraise=True, before_sleep=log_retry)
def call_api(): ...
#
★★

7. Python 脚本中 subprocess 如何实现子进程?

Python 的 subprocess 模块如何执行外部命令?run/Popen 的用法、输出捕获与安全注意点是什么?

  • subprocess.run 的推荐用法与参数(capture_output、check、timeout)
  • Popen 的高级控制:stdin/stdout/stderr 管道、实时输出
  • 安全:参数列表传递避免 shell 注入、编码与超时处理

subprocess 是执行外部命令的标准模块,推荐入口 subprocess.run(args, capture_output=True, text=True, check=True, timeout=60):args 用参数列表(非字符串)避免 shell 解析;capture_output 捕获 stdout/stderr(text=True 解码为文本);check=True 非零退出码抛 CalledProcessError(含输出信息);timeout 超时抛 TimeoutExpired 并需处理残留进程。返回 CompletedProcess(.returncode/.stdout/.stderr)。需要实时流式输出或交互时用 Popen:Popen(args, stdout=PIPE, stderr=STDOUT) 后 readline 逐行读取,或 stdin 写入输入,配合 communicate() 避免管道死锁(写大数据必须 communicate 或小心管道缓冲)。

安全与工程注意:默认不经过 shell(shell=False),传参列表直接 exec,天然免疫空格/通配符/分号注入;只有必须解析 shell 语法时才 shell=True,且输入必须严格校验转义(或用 shlex.quote);捕获输出注意编码(text/encoding 参数)与大小(大输出内存占用,必要时写文件);超时后 Popen 需 kill 子进程组(process_group 或 start_new_session);subprocess 替代 os.system/os.popen 的旧用法。生产脚本统一封装:异常分类(FileNotFoundError 命令不存在、CalledProcessError 执行失败、TimeoutExpired 超时)分别处理并记录日志。

本题考察 Python 调用外部命令的正确姿势。回答要点:run 的参数语义(check/capture/timeout)、Popen 的流式与管道处理(communicate 防死锁)、参数列表 vs shell 的安全差异与超时清理,体现运维脚本的安全与健壮性。

import subprocess
r = subprocess.run(["df", "-h", "/"], capture_output=True, text=True, timeout=10)
if r.returncode != 0:
    raise RuntimeError(f"df failed: {r.stderr}")
print(r.stdout)
# 流式读取
p = subprocess.Popen(["tail", "-f", "/var/log/app.log"], stdout=subprocess.PIPE, text=True)
for line in p.stdout:
    print(line.strip())
#
★★

8. Python 运维脚本的工程化要点中 argparse 参数校验、logging 分级输出与异常兜底策略如何设计

Python 运维脚本的工程化要点有哪些?argparse 参数校验、logging 分级输出与异常兜底策略如何设计?

  • argparse:必选参数、类型校验、choices、默认值
  • logging:分级(DEBUG/INFO/WARNING/ERROR)、格式化、输出到 stdout/文件/远端
  • 异常兜底:顶层 try/except 分类处理、退出码约定、幂等与可重试

工程化运维脚本的三块基础:argparse 参数解析——必选参数(required=True)、类型转换(type=int/Path)、取值范围(choices)、默认值(default)、子命令(add_subparsers),解析后集中校验互斥与依赖关系(parser.error 输出友好提示),保证"参数错误在运行前被发现";logging 日志——用 logging 而非 print:按级别分级(DEBUG 详细调试、INFO 关键步骤、WARNING 可恢复异常、ERROR 失败),配置 Formatter(时间、级别、文件名行号)与 Handler(控制台 + 文件轮转 RotatingFileHandler,或对接 journald/syslog/JSON 采集),关键脚本还应暴露结构化指标(重试次数、耗时);异常兜底——顶层结构 main() + if __name__ == '__main__': sys.exit(main()),main 返回退出码(0 成功、1 业务失败、2 参数错误、3 依赖缺失),异常分类捕获(OSError/网络/业务)分别记录与返回码,避免裸 traceback 输出到生产日志。

配套实践:配置外置(环境变量/配置文件,12-factor 风格);幂等设计(脚本可重复执行,配合锁与状态文件);执行前 dry-run(--dry-run)与 --yes 确认;超时与重试封装;把脚本纳入 CI 测试(pytest 单测关键函数)+ shellcheck/ruff 静态检查;日志中带上执行上下文(脚本名、参数摘要、耗时),方便审计与排障。目标是"任何人可运行、运行有日志、失败有退出码、可观测可重试"。

本题考察运维脚本的工程化规范。回答要点:argparse 的校验手段、logging 的分级与多 Handler 输出、异常兜底与退出码约定三层设计,以及幂等/dry-run/测试等配套实践,体现"脚本即产品"的质量意识。

import argparse, logging, sys
p = argparse.ArgumentParser()
p.add_argument("--target", required=True, type=Path)
p.add_argument("--level", choices=["low", "high"], default="low")
args = p.parse_args()
logging.basicConfig(level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s")
def main() -> int:
    try:
        ...
    except TimeoutError as e:
        logging.error("timeout: %s", e); return 3
    return 0
if __name__ == "__main__":
    sys.exit(main())
#
★★

9. crontab 环境变量缺失导致脚本手动可跑但 cron 失败的排查路径中 PATH、SHELL、HOME、MAILTO 的常见陷阱

crontab 中脚本"手动可跑、cron 失败"的常见原因是什么?PATH、SHELL、HOME、MAILTO 等环境变量的陷阱与排查路径是什么?

  • cron 的最小环境:PATH、SHELL、HOME、LANG 与登录 shell 差异
  • 常见失败:命令找不到、相对路径、依赖用户环境变量
  • 排查路径:cron 日志/邮件、脚本重定向、显式设置环境与绝对路径

cron 任务在精简环境中执行,与登录 shell 差异巨大:默认 SHELL=/bin/sh、PATH=/usr/bin:/bin(不含 /usr/local/bin、sbin、自定义目录)、HOME 为账户主目录、无交互环境变量(无 alias、无 .bashrc 加载、LANG 等 locale 未设置)。典型失败:脚本中调用未在 PATH 的命令(如自定义安装的 mysqldump 在 /usr/local/mysql/bin)报 command not found;相对路径/相对 cd 依赖当前目录;依赖 .bash_profile 里导出的变量(如 JAVA_HOME)而 cron 不加载;脚本用 bash 特性(数组、[[ ]])但 SHELL 是 sh;输出未重定向导致 cron 邮件堆积(MAILTO 未设置时发给账户,mbox 可能撑爆磁盘)。

排查路径:先确认任务是否执行(/var/log/cron、journalctl -u crond、cron 邮件),再看失败输出(脚本内所有输出重定向到日志文件,* * * * * /path/script.sh >> /var/log/cron-err.log 2>&1);修复规范:脚本内显式声明 #!/usr/bin/env bash、开头 set -euo pipefail 并自行 export PATH 与环境变量(或在 crontab 顶部定义 PATH=...),使用绝对路径,不依赖工作目录(脚本内 cd 到固定目录或使用 $0 定位);测试用 env -i /bin/bash -c '...' 模拟 cron 精简环境复现问题。定期巡检 cron 邮件与失败任务,配合日志采集集中观测。

本题考察 cron 环境差异这一高频故障。回答要点:cron 最小环境(PATH/SHELL/HOME)与登录环境的差异、典型失败模式(命令找不到、环境变量缺失、sh/bash 差异)、以及"重定向日志 + 显式环境 + 绝对路径 + env -i 复现"的排查修复路径,体现对任务调度环境的深刻理解。

# crontab 顶部显式设置
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
MAILTO=ops@example.com
* * * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# 模拟 cron 环境复现
env -i /bin/bash -c 'cd /opt/scripts && ./backup.sh'
#
★★

10. shell 函数中 local、typeset 如何限定变量作用域?

shell 函数中 local 与 typeset 如何限定变量作用域?不使用 local 会有什么问题?

  • local/typeset 声明函数内局部变量,防污染全局
  • 作用域与嵌套函数、与同名全局变量的关系
  • 陷阱:local 在命令替换/管道子 shell 的边界、动态作用域

bash 函数内变量默认是全局的(函数内外可见,甚至能覆盖外层同名变量),local var=value(或等效的 typeset)把变量声明为函数局部:仅函数及其调用链内可见,函数返回后自动消失,防止函数修改调用者的变量或全局污染——这是函数化脚本的基本卫生要求。local 支持在同一函数内嵌套调用子函数时"动态作用域":子函数能看到父函数局部变量(而非词法作用域),利用时需注意;同名局部变量会遮蔽(shadow)全局变量,函数内修改不影响全局(除非显式用 global 关键字)。

陷阱与边界:local 只对当前函数体生效,命令替换($(...))、管道各段(管道后的命令)、后台子 shell 是独立进程上下文,local 变量不会传播(如需传递用导出或写回 stdout);local 不声明类型(bash 动态类型),只限制作用域;在函数外用 local 会报错(bash 会拒绝或视为全局,POSIX sh 无 local 需注意兼容,可用 typeset 或遵守 POSIX 用环境变量);递归/嵌套时局部变量每层独立。实践:所有函数内部变量一律 local,参数用 $1/$2 并在函数入口局部化,可显著降低大型脚本的隐蔽 bug。

本题考察 shell 变量作用域机制。回答要点:local 的"函数局部 + 动态作用域 + 遮蔽"语义、不用的全局污染风险、以及命令替换/管道子 shell 的边界限制,体现对 shell 变量模型而非表面语法的理解。

count=0
process() {
  local count=5
  echo "inner: $count"        # 5
}
process
echo "outer: $count"          # 0(未被污染)
# 动态作用域示例
outer() { local x=1; inner; }
inner() { echo "x=$x"; }      # 可见 outer 的 x
#
★★

11. shell 脚本中 flock 如何实现文件锁以防止脚本并发执行?

flock 如何实现文件锁防止脚本并发执行?flock 命令与 flock 内置(fd 方式)的用法和区别是什么?

  • flock 的两种用法:命令包装(flock file cmd)与 fd 绑定(flock 9)
  • 锁文件机制与阻塞/非阻塞(-n)模式
  • 与 mkdir 锁、pidfile 锁的对比与 cron 防重入场景

flock 基于文件锁(flock 系统调用)实现互斥:flock /var/lock/myjob.lock -c 'myjob' 在获取锁后执行命令,释放后自动解锁;更稳的写法是 fd 绑定——脚本内 exec 9>/var/lock/myjob.lock; flock -n 9 把锁文件打开到 fd 9 再加锁(锁随进程存活,进程退出/被杀自动释放,无残留)。默认阻塞等待锁;-n(--nonblock)非阻塞:拿不到锁立即失败退出(exit 1),适合"任务只能跑一份,重入直接跳过"的 cron 场景;-w N 限时等待;-s 共享锁(读锁)与 -x 排他锁(写锁)区分并发读与写。锁文件本身只作锁载体(内容无关,需持久目录 /var/lock 或 /run/lock)。

与其他方案对比:pidfile 锁(写 pid 判活)有"僵尸 pid 残留导致误判"的坑,flock 由内核管理、进程死锁自动释放,更可靠;mkdir 锁(原子创建目录)需要手动清理,flock 免清理;跨主机场景 flock 无效(需分布式锁/etcd/Redis)。陷阱:同进程内不同 fd 锁语义(同一进程可重复加锁)、NFS 上的 flock 支持性差(用 fcntl 或避免)、管道场景 flock 命令与重定向的 fd 顺序。cron 防重入典型:* * * * * flock -n /run/lock/job.lock /opt/scripts/job.sh || true

本题考察 shell 互斥执行的正确实现。回答要点:flock 命令包装与 fd 绑定两种方式、阻塞/非阻塞模式与共享/排他锁语义、与 pidfile/mkdir 锁的可靠性对比及跨主机边界,体现对"并发防重入"的工程选型。

# 命令包装(阻塞)
flock /var/lock/backup.lock /opt/scripts/backup.sh
# fd 绑定 + 非阻塞(cron 防重入)
exec 9>/var/lock/backup.lock
if ! flock -n 9; then echo "already running" >&2; exit 1; fi
# 或单行:拿不到锁直接跳过
flock -n /run/lock/job.lock /opt/scripts/job.sh || exit 0
#
★★

12. shell 脚本中 getconf 如何获取系统配置常量(如页大小、参数上限)?

shell 脚本中 getconf 如何获取系统配置常量?它与 /proc、ulimit 等信息源的关系是什么?

  • getconf 的语法与常见键(PAGESIZE、ARG_MAX、OPEN_MAX、INT_MAX 等)
  • 用途:脚本内计算与自适应(页大小、路径缓冲、限制查询)
  • 与 sysctl、ulimit、/proc 的互补关系

getconf 是 POSIX 工具,从系统配置/glibc 限制中读取配置常量:getconf PAGESIZE 返回页大小(通常 4096)、getconf ARG_MAX 返回单次 exec 参数与环境最大字节数(影响 xargs 分块大小)、getconf OPEN_MAX 返回单进程打开文件上限(与 ulimit -n 对应)、getconf INT_MAX/LONG_MAX 返回类型上限、getconf PATH_MAX 返回路径最大长度、getconf _NPROCESSORS_ONLN 返回在线 CPU 数(GNU 扩展)。脚本中用 pagesize=$(getconf PAGESIZE) 做自适应计算(如按页数估算内存分配、计算 mmap 对齐),或在 xargs 调用前查询 ARG_MAX 决定 -n 分块,避免"硬编码 4096"这类跨平台失效。

关系与注意:getconf 读的是 POSIX 系统限制(sysconf),与 sysctl(内核运行参数,如 vm.、net.)不同源,但部分键对应内核限制(如 OPEN_MAX 反映 RLIMIT_NOFILE 硬限制);进程实际限制以 ulimit 为准(软限制可能小于 OPEN_MAX);数值可写脚本变量进行边界判断(如比较文件数是否超 OPEN_MAX)。注意版本差异:getconf 键名大小写敏感、GNU 扩展键(_NPROCESSORS_ONLN、CLK_TCK)在不同系统支持不一,脚本应做存在性检查(getconf KEY || echo default)。用途上它比"grep /proc 猜值"更规范,比硬编码更可移植。

本题考察系统限制常量的规范获取方式。回答要点:getconf 的键与典型返回值(页大小、ARG_MAX、OPEN_MAX)、脚本自适应场景(分块、估算)、与 sysctl/ulimit 的信息源区分,体现可移植脚本的细节功力。

getconf PAGESIZE
getconf ARG_MAX
getconf OPEN_MAX
getconf _NPROCESSORS_ONLN
# 脚本自适应示例
page=$(getconf PAGESIZE); mem_pages=$(($(awk '/MemTotal/{print $2}' /proc/meminfo) * 1024 / page))
#
★★

13. shell 脚本中 mapfile、readarray 如何实现文件读入数组?

mapfile/readarray 如何把文件或命令输出读入数组?它的使用要点与常见坑是什么?

  • mapfile 语法:mapfile -t arr < file、-n/-s 控制行数
  • 数组访问:${arr[@]}、${#arr[@]}、带索引遍历
  • 坑:尾部换行、IFS 与空白保留、大文件内存

mapfile(readarray 是别名)把输入按行读入数组:mapfile -t lines < /etc/hosts 把文件每行存入 lines[0]、lines[1]...,-t 去除行尾换行符(保留其他空白);mapfile -t arr < <(cmd) 读命令输出(进程替换);-n N 只读前 N 行、-s N 跳过前 N 行、-O n 指定起始下标;读入后遍历用 for line in "${arr[@]}"(带引号防分词)、echo ${#arr[@]} 得行数、printf '%s\n' "${arr[2]}" 取特定行。相比 for line in $(cat file)(分词 + 通配符 + 空白丢失的经典坑),mapfile 保真按行存储,是处理"行数据"的正确姿势。

坑与边界:行尾 CRLF(Windows 编辑的文件)需先 tr -d '\r';超长行与超大文件全量入内存(日志文件几百 MB 时考虑流式 while read);空行会被读入为空字符串元素(-t 后仍保留空元素);IFS 不参与 mapfile 的行分割(与 read 不同),但 -t 只去换行;数组下标从 0 开始,判断空数组用 ${#arr[@]} -eq 0;函数内使用需注意 mapfile 修改的是当前 shell 的数组变量(子 shell 管道中不生效——cat file | mapfile arr 无效,必须重定向形式)。配合 readarray 可读入后二次处理(sed/awk 转换)再批量操作。

本题考察 shell 数组批量读入的规范做法。回答要点:mapfile -t 的用法与保真语义、数组访问语法、与 $(cat) 分词坑的对比、以及管道子 shell、CRLF、内存等边界,体现对行数据处理细节的掌握。

mapfile -t hosts < /etc/hosts
echo "lines: ${#hosts[@]}"
for i in "${!hosts[@]}"; do echo "$i: ${hosts[$i]}"; done
# 命令输出入数组
mapfile -t pids < <(pgrep -f myapp)
# 错误示范(管道子 shell,数组丢失)
cat /etc/hosts | mapfile -t x; echo "${#x[@]}"   # 输出 0
#
★★

14. shell 脚本中 关联数组 declare -A 如何实现 KV 结构?

bash 的关联数组(declare -A)如何实现键值对结构?使用场景与限制是什么?

  • declare -A 声明与赋值/访问语法
  • 遍历键值、存在性判断、计数
  • 限制:仅 bash 4+、键序不确定、序列化与子 shell 传递

关联数组(哈希表)用 declare -A map 声明,赋值 map[key]=value(键支持任意字符串,含空格与中文)或一次赋值 declare -A map=([k1]=v1 [k2]=v2),访问 ${map[key]},判断存在 ${map[key]+x}(空值也区分),遍历 ${!map[@]} 取键、${map[@]} 取值、${#map[@]} 计数。用途:把配置文件解析成 KV、按 IP/主机名索引状态、按服务名聚合计数、替代"逐行 grep"的高频查询。bash 4.0+ 支持(macOS 默认 bash 3 需 brew 升级或改用其他方式),脚本头部 #!/usr/bin/env bash 并检查 BASH_VERSINFO。

限制与坑:键顺序是哈希序(非插入序),需要有序时用 printf '%s\n' "${!map[@]}" | sortunset map[key] 删除;数组不能直接传递到函数/子 shell(引用传递 ${!map} 或序列化后还原——bash 5.1+ 的 declare -p 输出可 eval 恢复);${map[@]} 不带引号会分词,操作统一加引号;键含特殊字符(空格、*、[)时访问要小心引号语义;注意与索引数组(declare -a)区分(map[0] 在关联数组中键是字符串 "0")。生产脚本把 KV 解析封装成函数(read_kv file 返回关联数组全局变量),复用避免散落。

本题考察 bash 高级数据结构的使用。回答要点:声明/赋值/遍历/存在性判断的语法、bash 版本与键序/引号等限制、跨函数传递的序列化问题,体现对 KV 场景下正确语法的掌握。

declare -A svc=([web]=8080 [db]=3306 [cache]=6379)
svc[api]=9000
echo "${svc[web]}"; echo "${!svc[@]}"; echo "${#svc[@]}"
for k in "${!svc[@]}"; do echo "$k -> ${svc[$k]}"; done
unset svc[cache]
#

15. Shell/Python 脚本的命令注入防御中如何校验外部输入、避免 eval 与命令拼接并使用参数化方式执行外部命令

Shell 与 Python 脚本中如何防御命令注入?外部输入校验、避免 eval/命令拼接、参数化执行分别怎么做?

  • 命令注入原理:外部输入拼入命令字符串被解释为命令
  • Shell:引号陷阱、避免 eval、白名单校验
  • Python:subprocess 参数列表、shlex.quote、禁止 os.system/eval

命令注入发生在"外部输入被拼接进命令字符串并解释执行"时:攻击者输入 ; rm -rf /$(...)、反引号等被 shell 当作命令。防御三层:校验——对外部输入做白名单校验(正则限定字符集 ^[a-zA-Z0-9_.-]+$)与长度限制,拒绝含特殊字符(;|&$()<>!*?[]{})的输入,这是第一道闸;避免拼接与 eval——Shell 中不使用 eval、不用 cmd $input直拼(即使引号包裹仍可能被$() 展开),Python 中不用 os.system、os.popen、subprocess 的 shell=True + 拼接字符串、eval/exec(eval 只用于可信字面量);参数化执行——Python 用 subprocess.run([cmd, arg1, arg2]) 参数列表形式(不经 shell,参数原样传给 exec),Shell 中把参数放数组再展开(args=(...)"${args[@]}"`)或不得已用 shell=True 时用 shlex.quote 转义。

纵深补充:固定命令白名单(只允许预定义命令集合,参数逐一校验);环境侧防护(脚本以最小权限运行、限制可写路径、禁用危险命令路径);错误处理(参数非法直接拒绝而非继续);安全审计(脚本评审与 SAST 工具扫描 eval/拼接模式)。理解本质:命令注入是"数据被当作代码"的注入类漏洞,防御核心是"数据与代码分离"——参数化传递 + 白名单校验 + 禁止动态代码执行。

本题考察注入类漏洞的防御方法论。回答要点:注入原理(拼接即执行)、三层防御(校验、禁 eval/拼接、参数化)、以及 Shell/Python 两侧的具体写法差异,体现"数据与代码分离"的安全编程观。

# 校验(白名单)
case "$input" in *[!a-zA-Z0-9_.-]*) echo "invalid" >&2; exit 1;; esac
# 参数数组展开(不经 shell 解释)
args=(/opt/scripts/deploy.sh "$input"); "${args[@]}"
import subprocess, re
if not re.fullmatch(r"[a-zA-Z0-9_.-]+", user_input):
    raise ValueError("invalid input")
subprocess.run(["/usr/bin/git", "show", user_input], check=True)  # 参数列表
#

16. 文本处理性能选型中 awk/sed 与 Python 在流式大文件处理上的耗时与内存差异以及如何用基准测试验证

大文件流式处理中 awk/sed 与 Python 的性能与内存差异是什么?如何用基准测试验证选型?

  • awk/sed 的 C 实现与流式特性:低内存、高速
  • Python 的逐行迭代(读入行、处理、写出)与解释型开销
  • 基准方法:time、/usr/bin/time -v(内存)、同输入同输出对比

大文件(GB 级)处理的核心是"流式、低内存":awk/sed 用 C 实现,天然逐行流式(单行缓冲,内存占用几乎恒定),文本处理(字段分割、正则、替换)高度优化,吞吐通常显著高于 Python 脚本;Python(解释型 + 逐行循环)同任务耗时通常是 awk 的数倍到数十倍,且若用 read() 全量读入或列表收集结果,内存随文件线性增长。但 Python 的复杂逻辑表达力强(数据结构、正则库、回调),适合 awk 难以实现的"多步关联、跨行状态、调用库函数"场景;中间路线是 Python 按行迭代(for line in f,不读全文件)控制内存,或对超大数据用 pandas chunked 读取。

选型原则:纯文本变换(格式转换、字段抽取、替换过滤)优先 awk/sed;需要复杂状态机、关联外部数据、调用 API 用 Python(并保持逐行流式);混合模式(awk 粗过滤 + Python 细处理)可减少 Python 处理量。基准验证方法:同输入同预期输出下,用 time(或 /usr/bin/time -v 看 Maximum resident set size 内存)对比 wall time 与峰值内存;注意冷缓存(先 drop caches 或多次运行取稳定值)、输出重定向到 /dev/null 排除磁盘差异、输入分布贴近真实(行数、行长、匹配率)。结论以数据为准:小文件(MB 级)两者差异无感,优先可维护性;GB 级才需要基准驱动选型。

本题考察文本处理工具链的性能工程。回答要点:awk/sed 的流式 C 实现 vs Python 解释型逐行的性能与内存差异、选型逻辑(简单变换 vs 复杂逻辑)、以及"同输入同输出 + time/-v 内存 + 冷缓存"的基准方法,体现以测量代替猜测的工程态度。

# 基准对比(输出到 /dev/null)
/usr/bin/time -v awk '{print $2}' big.log > /dev/null
/usr/bin/time -v python3 -c "
import sys
for line in sys.stdin: print(line.split()[1], end='\n')" < big.log > /dev/null
# Python 流式写法(不读全文件)
with open("big.log") as f:
    for line in f: ...
#

17. 运维脚本的可维护性中函数拆分、shellcheck/pytest 静态与单元测试如何提升脚本质量

运维脚本的可维护性如何提升?函数拆分、shellcheck 静态检查与 pytest 单元测试分别起什么作用?

  • 函数拆分与单一职责:参数化、复用、易测
  • shellcheck:Shell 脚本静态检查的典型问题
  • pytest:对纯逻辑函数做单元测试、mock 外部命令

可维护性三件套:函数拆分——把脚本按职责拆成小函数(单一职责、参数化、命名清晰、局部变量 + 显式返回码),主流程只做编排(读取输入 → 调用函数 → 输出结果),公共逻辑(日志、重试、命令执行封装)抽成库文件(source 引入),避免"300 行面条式主流程";shellcheck——Shell 脚本静态分析工具(VS Code 集成或 CI 中 shellcheck script.sh),能抓出未加引号变量(SC2086)、无 set -e、[ vs [[、命令替换歧义、管道掩码错误等数百类问题,是 Shell 脚本的"编译期检查";pytest——对 Python 脚本中的纯逻辑(解析、计算、转换)做单元测试,用 monkeypatch/mock 模拟外部命令与文件系统(如 subprocess 调用打桩返回固定输出),对参数校验、边界条件(空输入、异常格式)覆盖用例,配合 CI 在变更时自动运行。

工程配套:代码规范(shell 用 shellcheck + bash -n 语法检查、Python 用 ruff/flake8 + mypy)、目录组织(scripts/ 下按功能分文件,common.sh 共享)、版本控制与 Review(脚本变更走 PR)、文档化(脚本头部说明用途/参数/退出码)、以及可观测(统一日志与退出码约定)。目标:脚本能被人快速读懂、能安全修改、能自动验证——把运维脚本当作软件工程的一部分管理。

本题考察运维脚本的软件工程化。回答要点:函数拆分的可测性收益、shellcheck 的静态检查价值(未加引号等典型问题)、pytest 单测与 mock 外部依赖的实践,以及规范/CI/文档等配套,体现"脚本即代码"的质量观。

# shellcheck 示例:SC2086 未加引号
path=$1
rm -rf $path        # shellcheck 报错:应为 "$path"
# pytest 示例
def parse_disk(usage_line: str) -> dict: ...
def test_parse_disk():
    assert parse_disk("/dev/sda1 100G 20G 80G 20% /")["pct"] == 20