grep
基础的用法
|
1 2 3 4 |
grep "pattern" file # 在文件中搜索包含 pattern 的行 grep "pattern" file1 file2 # 在多个文件中搜索 grep "pattern" *.cpp # 在所有 .cpp 文件中搜索 command | grep "pattern" # 过滤命令输出(管道用法) |
常用的选项
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# -r / -R:递归搜索目录(在 LLVM 源码里找东西必备) grep -r "SelectionDAG" llvm/lib/ # -r 不跟软链接,-R 跟软链接,一般用 -r 就够了 # -n:显示行号 grep -n "IRBuilder" llvm/include/llvm/IR/IRBuilder.h # 输出:42:class IRBuilder { # 行号让你能快速跳到对应位置 # -i:忽略大小写 grep -i "riscv" llvm/lib/Target/ # 同时匹配 RISCV、riscv、Riscv 等 # -l:只显示包含匹配的文件名(不显示具体内容) grep -rl "mem2reg" llvm/lib/Transforms/ # 快速找到哪些文件和 mem2reg 相关 # -L:只显示不包含匹配的文件名(-l 的反义) grep -rL "include" llvm/lib/Target/RISCV/*.cpp # 找没有 include 语句的文件(不太常用但偶尔有用) # -c:统计匹配行数 grep -rc "TODO" llvm/lib/Transforms/ # 看每个文件有多少个 TODO # -w:全词匹配 grep -rw "add" llvm/lib/IR/ # 只匹配独立的 "add",不匹配 "addUser" "ReadAdd" 等 # 没有 -w 的话搜 "add" 会匹配到大量不相关的结果 # -v:反转匹配(显示不匹配的行) ps aux | grep ninja | grep -v grep # 查找 ninja 进程,排除 grep 自身 # 这是一个经典用法,因为 grep ninja 本身也包含 "ninja" # -o:只显示匹配的部分(不显示整行) grep -o 'def [a-zA-Z]*' test.ll # 从 LLVM IR 中提取所有函数名 |
组合使用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# -rn:递归搜索 + 显示行号(最最最常用的组合) grep -rn "BasicBlock" llvm/include/llvm/IR/ # 输出: # llvm/include/llvm/IR/BasicBlock.h:42:class BasicBlock : public Value { # llvm/include/llvm/IR/Function.h:15:#include "llvm/IR/BasicBlock.h" # ... # -rni:递归 + 行号 + 忽略大小写 grep -rni "riscv" llvm/lib/Target/ # -rnw:递归 + 行号 + 全词匹配 grep -rnw "Value" llvm/include/llvm/IR/Value.h # -rn --include:递归搜索但只看特定类型的文件 grep -rn --include="*.cpp" "PassManager" llvm/lib/ grep -rn --include="*.h" "PassManager" llvm/include/ grep -rn --include="*.td" "RISCV" llvm/lib/Target/RISCV/ # 在 LLVM 开发中非常实用,因为目录里混着 .cpp .h .td .inc .def 各种文件 # --exclude:排除特定文件 grep -rn --exclude="*.txt" "TODO" llvm/ # --exclude-dir:排除特定目录 grep -rn --exclude-dir=build --exclude-dir=.git "IRBuilder" llvm-project/ # 排除构建目录和 git 目录,否则搜索会非常慢 |
上下文显示
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# -A n(After):显示匹配行之后的 n 行 grep -rn -A 5 "class BasicBlock" llvm/include/llvm/IR/BasicBlock.h # 找到类定义,看后面 5 行了解它的成员 # -B n(Before):显示匹配行之前的 n 行 grep -rn -B 3 "return nullptr" llvm/lib/Transforms/Scalar/DCE.cpp # 看 return nullptr 前面 3 行的条件判断 # -C n(Context):显示前后各 n 行 grep -rn -C 3 "phi" test.ll # 看 phi 函数及其上下文 # 实际例子:在 LLVM 源码中找一个函数的完整签名 grep -rn -A 10 "PreservedAnalyses.*run(" llvm/lib/Transforms/Scalar/DCE.cpp # 找到 run 函数,看后面 10 行了解函数体开头 # 多个匹配之间用 -- 分隔 grep -rn -C 2 "SIGTERM" llvm/lib/Support/ # 输出: # Signals.inc:42- // Register signal handlers # Signals.inc:43: signal(SIGTERM, SignalHandler); # Signals.inc:44- signal(SIGINT, SignalHandler); # -- # Signals.inc:78- if (Sig == SIGTERM) { # Signals.inc:79: // Clean up temp files # Signals.inc:80- cleanup(); |
正则表达式
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# ===== 基础正则(grep 默认模式,BRE)===== # . 匹配任意单个字符 grep "def.*add" test.ll # def 后面跟任意内容再跟 add # * 前面的字符重复 0 次或多次 grep "ab*c" file # ac, abc, abbc, abbbc, ... # [] 字符集 grep "[0-9]" test.ll # 包含数字的行 grep "[a-zA-Z_]" file # 包含字母或下划线的行 grep "[^0-9]" file # 包含非数字字符的行(^ 在 [] 内表示取反) # ^ 行首,$ 行尾 grep "^define" test.ll # 以 "define" 开头的行(LLVM IR 的函数定义) grep "ret.*$" test.ll # 以 ret 开头的行 grep "^$" file # 空行 grep -c "^$" file # 统计空行数量 # \b 单词边界(和 -w 类似但更灵活) grep "\badd\b" test.ll # 全词匹配 add # \转义 grep "\.cpp" Makefile # 搜索 .cpp(. 需要转义,否则匹配任意字符) grep "C\+\+" file # 搜索 C++(+ 需要转义) # ===== 扩展正则(grep -E 或 egrep,ERE)===== # -E 模式下不需要转义 + ? | () {},写起来更自然 # + 前面的字符重复 1 次或多次 grep -E "ab+c" file # abc, abbc, abbbc(但不匹配 ac) # ? 前面的字符重复 0 次或 1 次 grep -E "colou?r" file # 匹配 color 和 colour # | 或(交替) grep -E "SIGTERM|SIGINT|SIGKILL" llvm/lib/Support/Signals.inc # 同时搜索三个信号名 # () 分组 grep -E "(get|set)Value" llvm/include/llvm/IR/ # 匹配 getValue 和 setValue # {n,m} 重复次数 grep -E "[0-9]{1,3}\.[0-9]{1,3}" file # 匹配 IP 地址片段 # 实用的组合示例: # 搜索 LLVM IR 中所有的函数定义 grep -E "^define .+ @[a-zA-Z_]" test.ll # 搜索所有的 #include 行,区分系统头文件和本地头文件 grep -E '#include [<"]' file.cpp # 搜索 C++ 类定义 grep -rn -E "^class [A-Z][a-zA-Z]+ " llvm/include/llvm/IR/ # 搜索函数声明(返回类型 + 函数名 + 参数列表开头) grep -rn -E "[a-zA-Z_]+\s+[a-zA-Z_]+\(" llvm/lib/Transforms/Scalar/DCE.cpp # ===== Perl 正则(grep -P,功能最强)===== # 需要 grep 编译时启用了 PCRE 支持 # \d 数字,\w 单词字符,\s 空白 grep -P "\d+" test.ll # 匹配数字 grep -P "i\d+" test.ll # 匹配 LLVM IR 的整数类型:i1, i8, i32, i64 # 非贪婪匹配 grep -P "define.*?@" test.ll # .*? 尽可能少地匹配 # 前瞻和后顾 grep -P "(?<=@)\w+" test.ll # 匹配 @ 后面的函数名(不包含 @) grep -P "\w+(?=\()" file.cpp # 匹配函数调用的函数名(不包含括号) # 注意:-P 不是所有系统都支持 # macOS 的 grep 默认不支持 -P,需要装 ggrep # 如果你用 ripgrep (rg),默认就是 Perl 风格正则 |
管道组合(grep 的精髓)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# ===== 与其他命令组合 ===== # 过滤进程列表 ps aux | grep clang | grep -v grep # 先列出所有进程,再过滤出 clang 相关的,再排除 grep 自身 # 过滤编译输出,只看错误 ninja -C build 2>&1 | grep -E "error:|Error" # 2>&1 把 stderr 合并到 stdout,然后过滤错误信息 # 查看 LLVM IR 中有多少个函数定义 clang -emit-llvm -S -O2 test.c -o - | grep -c "^define" # -o - 表示输出到标准输出,直接通过管道过滤 # 统计 LLVM 源码中各种 Pass 的数量 grep -rl "PassInfoMixin" llvm/lib/Transforms/ | wc -l # 先找包含 PassInfoMixin 的文件,再统计数量 # 多级过滤 grep -rn "alloca" test.ll | grep -v "debug" | grep "i32" # 找包含 alloca 的行 → 排除调试信息相关的 → 只看 i32 类型的 # 查找 LLVM 中某个函数在哪里被调用 grep -rn "getCalledFunction" llvm/lib/Transforms/ | grep -v "^Binary" # 在优化 Pass 中搜索 getCalledFunction 的调用 # 配合 sort 和 uniq 做统计 grep -roh "SIGTERM\|SIGINT\|SIGKILL\|SIGSEGV" llvm/lib/Support/ | sort | uniq -c | sort -rn # -o 只输出匹配部分,-h 不显示文件名 # sort 排序 → uniq -c 去重并统计 → sort -rn 按数量倒排 # 输出类似: # 15 SIGINT # 12 SIGSEGV # 8 SIGTERM # 3 SIGKILL # 配合 xargs 批量操作 grep -rl "OldAPIName" llvm/lib/ | xargs sed -i 's/OldAPIName/NewAPIName/g' # 在所有匹配文件中做全局替换(重构时很有用) # 注意:这会直接修改文件,操作前确保 git 已提交 |
tmux
三层结构
|
1 2 3 |
Session(会话) └── Window(窗口,类似浏览器的 Tab) └── Pane(窗格,一个窗口里的分屏) |
前缀键
- 所有快捷键都要先按一个"前缀键",默认是
Ctrl+b - 比如"分屏"的操作是
Ctrl+b然后松开,再按%
会话(Session)管理
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# 创建一个新会话 tmux # 创建匿名会话 tmux new -s llvm # 创建名为 "llvm" 的会话(推荐取名字) tmux new -s coding # 再创建一个 "coding" 会话 # 断开会话(会话在后台继续运行!) # PREFIX d # 按 Ctrl+b,松开,按 d # 或者直接关掉终端窗口,会话也不会丢 # 查看所有会话 tmux ls # 输出类似: # llvm: 1 windows (created Mon Jan 1 10:00:00 2026) # coding: 2 windows (created Mon Jan 1 10:05:00 2026) # 重新连接到会话 tmux attach -t llvm # 连回 "llvm" 会话 tmux a -t llvm # 简写 tmux a # 连回最近的会话 # 杀掉会话 tmux kill-session -t llvm # 杀掉 "llvm" 会话 tmux kill-server # 杀掉所有会话(慎用) |
- 场景:编译
LLVM(要跑很久)
|
1 2 3 4 5 6 7 |
# 场景:编译 LLVM(要跑很久) tmux new -s build # 创建会话 ninja -C build-release -j$(nproc) # 开始编译 # 按 Ctrl+b, d 断开——编译继续跑 # 去做别的事情... # 回来看看编译好了没: tmux a -t build |
窗口(Window)管理
- 一个
Session里可以有多个Window,就像浏览器标签页:
|
1 2 3 4 5 6 7 |
PREFIX c 创建新窗口 PREFIX , 重命名当前窗口(给窗口取个有意义的名字) PREFIX n 切换到下一个窗口 PREFIX p 切换到上一个窗口 PREFIX 0-9 切换到第 0-9 号窗口(底部状态栏能看到编号) PREFIX w 列出所有窗口,用方向键选择(非常实用) PREFIX & 关闭当前窗口(会确认) |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
tmux new -s dev # 创建会话 # 现在你在窗口 0,按 PREFIX , 重命名为 "editor" # 用 vim 或 VS Code 写代码 # PREFIX c 创建新窗口,重命名为 "build" # 在这里运行编译命令 # PREFIX c 再创建一个,重命名为 "test" # 在这里运行测试 # 底部状态栏显示: # [dev] 0:editor 1:build* 2:test # 带 * 号的是当前窗口 # PREFIX 0 切回 editor 窗口 # PREFIX 1 切到 build 窗口 # PREFIX w 弹出窗口列表,上下选择 |
窗格(Pane)分屏
- 最常用的功能——在一个窗口里分出多个区域
|
1 2 3 4 5 6 7 |
PREFIX % 左右分屏(竖线分割) PREFIX " 上下分屏(横线分割) PREFIX 方向键 在窗格之间切换(←↑↓→) PREFIX z 当前窗格全屏/恢复(zoom,非常实用!) PREFIX x 关闭当前窗格(会确认) PREFIX q 显示窗格编号(快速闪现) PREFIX 空格 在预设布局之间轮换 |
- 调整窗格大小
|
1 2 3 4 5 6 7 8 9 |
PREFIX Ctrl+方向键 微调大小(每次一行/列) # 或者按住 PREFIX 不放,反复按方向键 # 更精确的方式: PREFIX : # 进入命令模式 resize-pane -D 10 # 向下扩展 10 行 resize-pane -U 5 # 向上扩展 5 行 resize-pane -L 20 # 向左扩展 20 列 resize-pane -R 20 # 向右扩展 20 列 |
- 编译器开发的经典分屏布局
|
1 2 3 4 5 6 7 8 9 10 11 |
┌─────────────────────┬──────────────────┐ │ │ │ │ 编辑代码 │ 编译/运行 │ │ (vim / code) │ (ninja / opt) │ │ │ │ │ ├──────────────────┤ │ │ │ │ │ 查看 IR/汇编 │ │ │ (cat test.ll) │ │ │ │ └─────────────────────┴──────────────────┘ |
|
1 2 3 |
# 想临时全屏看某个窗格的完整输出: PREFIX z # 全屏当前窗格 PREFIX z # 再按一次恢复分屏 |
滚动和复制
- 默认情况下你没法用鼠标滚轮看历史输出,需要进入"复制模式":
|
1 2 3 4 5 6 7 8 9 10 11 |
PREFIX [ 进入复制模式(可以滚动查看历史输出了) 用方向键 或 PgUp/PgDn 滚动 q 退出复制模式 # 在复制模式中(vi 风格键绑定): / + 关键词 向下搜索 ? + 关键词 向上搜索 n 下一个搜索结果 空格 开始选择 Enter 复制选中内容 PREFIX ] 粘贴 |
推荐配置文件
- 创建
~/.tmux.conf文件,让tmux更好用:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
cat > ~/.tmux.conf << 'EOF' # ===== 基础设置 ===== set -g default-terminal "screen-256color" # 256色支持 set -g history-limit 50000 # 增大历史记录(默认2000太少) set -g mouse on # 开启鼠标支持(滚轮、点击切换窗格、拖拽调整大小) set -g base-index 1 # 窗口编号从1开始(0离得太远不好按) setw -g pane-base-index 1 # 窗格编号也从1开始 # ===== 快捷键优化 ===== # 用 Ctrl+a 替代 Ctrl+b 作为前缀键(更好按,和 screen 一致) # 如果你习惯 Ctrl+b 可以不加这两行 # unbind C-b # set -g prefix C-a # 更直觉的分屏快捷键 bind | split-window -h -c "#{pane_current_path}" # PREFIX | 左右分屏 bind - split-window -v -c "#{pane_current_path}" # PREFIX - 上下分屏 # -c "#{pane_current_path}" 让新窗格继承当前目录 # Alt+方向键 直接切换窗格(不需要按前缀键,更快) bind -n M-Left select-pane -L bind -n M-Right select-pane -R bind -n M-Up select-pane -U bind -n M-Down select-pane -D # PREFIX r 重新加载配置文件 bind r source-file ~/.tmux.conf \; display-message "Config reloaded!" # ===== 状态栏美化 ===== set -g status-style 'bg=#333333 fg=#ffffff' set -g status-left '[#S] ' # 显示会话名 set -g status-right '%H:%M %Y-%m-%d' # 显示时间日期 # ===== 复制模式使用 vi 键绑定 ===== setw -g mode-keys vi EOF |
|
1 2 3 |
# 让配置立即生效(在 tmux 内执行): tmux source-file ~/.tmux.conf # 或者按 PREFIX : 然后输入 source-file ~/.tmux.conf |
kill
本质
kill命令的本质不是"杀死进程",而是"给进程发送信号(signal)
基础用法
|
1 2 3 |
kill <PID> # 给进程发送默认信号(SIGTERM,礼貌地请求退出) kill -9 <PID> # 给进程发送 SIGKILL(强制杀死,无法拦截) kill -s SIGSTOP <PID> # 给进程发送 SIGSTOP(暂停进程) |
目标进程的 PID
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# 方法 1:ps(最常用) ps aux | grep ninja # 输出类似: # user 12345 50.0 8.0 ... ninja -C build -j8 # user 12399 0.0 0.0 ... grep ninja ← 这是 grep 自己,忽略 # PID 是第二列:12345 # 方法 2:pgrep(按名字查 PID) pgrep ninja # 输出 PID pgrep -a ninja # 输出 PID + 完整命令行 pgrep -f "ninja -C build" # 按完整命令行匹配 # 方法 3:pidof(按精确程序名查) pidof ninja # 方法 4:top / htop(交互式查看) htop # 可以直接在界面里选中进程按 F9 发信号 # 方法 5:jobs(查看当前终端的后台任务) ninja -C build -j8 & # & 放到后台运行 jobs # 列出后台任务 # [1]+ Running ninja -C build -j8 & kill %1 # 用 %编号 杀后台任务(不需要知道 PID) |
信号
概述
- 信号是操作系统发给进程的一种异步通知
- "异步"意味着信号可以在进程执行的任意时刻到达,打断正在做的事情
- 可以把它理解为硬件中断在软件层面的对应物——硬件中断是外设通知
CPU,信号是内核通知进程
Linux有几十种信号,你需要掌握以下几个
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# 查看所有信号列表 kill -l # 输出: # 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP # 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 # 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM # 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP ... # 发信号的三种等价写法: kill -15 <PID> # 用信号编号 kill -SIGTERM <PID> # 用信号全名 kill -TERM <PID> # 用信号简名(去掉SIG前缀) |
信号的生命周期
- 一个信号从产生到被处理,经历四个阶段:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
1. 产生(Generation) 信号被某个事件触发,发送给目标进程 2. 递送前的状态(Pending) 信号已经产生但还没被目标进程处理 此时信号处于"待处理"(pending)状态 3. 递送(Delivery) 内核把信号交给目标进程 发生在进程从内核态返回用户态的时刻(比如系统调用返回、时钟中断返回) 4. 处理(Handling) 进程对信号做出响应:执行处理函数、忽略、或执行默认行为 |
- 关键理解:
- 信号不是实时递送的
- 内核只是在进程的
task_struct里设一个标志位,等进程下次从内核态切回用户态时,内核检查这个标志位,才真正递送信号 - 这就是为什么一个
CPU密集型的纯计算进程(不做系统调用)也能收到信号——时钟中断会周期性地让进程进入内核态
信号的产生来源
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
来源一:终端操作 ━━━━━━━━━━━━━━━━━━━━ Ctrl+C → SIGINT (2) 发给前台进程组 Ctrl+Z → SIGTSTP (20) 发给前台进程组 Ctrl+\ → SIGQUIT (3) 发给前台进程组 来源二:内核检测到异常 ━━━━━━━━━━━━━━━━━━━━ 除零错误 → SIGFPE (8) 空指针/越界 → SIGSEGV (11) 你调试 LLVM 时最常见的 非法指令 → SIGILL (4) 总线错误 → SIGBUS (7) 内存对齐问题 来源三:进程主动发送(kill 命令 / kill 系统调用) ━━━━━━━━━━━━━━━━━━━━ kill(pid, SIGTERM) 一个进程给另一个进程发信号 kill(0, SIGTERM) 给同进程组的所有进程发信号 kill(-pgid, SIGTERM) 给指定进程组的所有进程发信号 来源四:内核通知事件 ━━━━━━━━━━━━━━━━━━━━ 子进程退出 → SIGCHLD (17) 发给父进程 终端断开 → SIGHUP (1) 发给会话首进程(这就是 tmux 要解决的问题) 管道破裂 → SIGPIPE (13) 写入端写数据但读取端已关闭 定时器到期 → SIGALRM (14) alarm() 设置的定时器 来源五:用户自定义 ━━━━━━━━━━━━━━━━━━━━ SIGUSR1 (10) 无预定义含义,程序自己决定用途 SIGUSR2 (12) 同上 |
信号处理的三种方式
- 进程收到信号后,有且只有三种选择:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
1. 执行默认动作(SIG_DFL) ┌──────────────────────────────────────────────┐ │ 默认动作有以下几种: │ │ - Term:终止进程 │ │ - Core:终止进程 + 产生 core dump 文件 │ │ - Stop:暂停进程 │ │ - Cont:恢复已暂停的进程 │ │ - Ign:忽略 │ └──────────────────────────────────────────────┘ 各信号的默认动作: SIGTERM → Term SIGINT → Term SIGHUP → Term SIGKILL → Term SIGQUIT → Core SIGSEGV → Core SIGABRT → Core SIGFPE → Core SIGSTOP → Stop SIGCONT → Cont SIGCHLD → Ign SIGURG → Ign 2. 捕获信号,执行自定义处理函数 signal(SIGTERM, my_handler); // 收到 SIGTERM 时执行 my_handler 函数 3. 忽略信号 signal(SIGPIPE, SIG_IGN); // 收到 SIGPIPE 时什么都不做 例外:SIGKILL (9) 和 SIGSTOP (19) 无法被捕获或忽略 这是内核的硬性规定,确保系统管理员永远能杀掉或暂停任何进程 |
信号处理的底层实现
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
进程的信号相关数据结构(在 task_struct 中): ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ struct task_struct { // ... struct sigpending pending; // 待处理的信号队列 sigset_t blocked; // 信号掩码(哪些信号被阻塞) struct sighand_struct *sighand; // 信号处理函数表 // ... }; 信号掩码(Signal Mask): ━━━━━━━━━━━━━━━━━━━━━━━━ 每个进程有一个 64 位的位图(sigset_t),每一位对应一个信号。 如果某一位被设置,对应的信号就被"阻塞"——信号不会被递送, 而是保持 pending 状态,直到解除阻塞。 这和"忽略"不同: - 忽略(SIG_IGN):信号到达后被丢弃,永远不处理 - 阻塞(blocked):信号被暂时挂起,解除阻塞后会立刻递送 信号递送的时机: ━━━━━━━━━━━━━━━━━━ 不是信号一产生就立刻被处理的。内核在以下时机检查 pending 信号: 1. 系统调用返回用户态之前 2. 中断处理程序返回用户态之前 3. 进程被调度运行时 内核检查流程(伪代码): if (进程有 pending 信号 && 该信号未被阻塞) { if (处理方式 == 自定义函数) { 保存当前上下文到用户栈 设置返回地址为信号处理函数 返回用户态(此时会执行信号处理函数) 处理函数执行完后,通过 sigreturn 恢复原来的上下文 } else if (处理方式 == SIG_IGN) { 清除 pending 标志,什么都不做 } else { // SIG_DFL 执行默认动作(终止/core dump/暂停等) } } |
标准信号 vs 实时信号
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
标准信号(1-31): ━━━━━━━━━━━━━━━━ - 不排队:如果同一个信号在 pending 期间被再次发送,只记录一次 - 也就是说,连续发 5 次 SIGTERM,进程只会处理 1 次 - 这是因为 pending 信号用位图表示(每个信号就一个 bit) 演示: kill -SIGUSR1 <PID> # 发第 1 次,pending 位被设置 kill -SIGUSR1 <PID> # 发第 2 次,pending 位已经是 1 了,没变化 kill -SIGUSR1 <PID> # 发第 3 次,同上 # 进程最终只处理 1 次 SIGUSR1 实时信号(34-64,即 SIGRTMIN 到 SIGRTMAX): ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - 排队:每次发送都会入队,不会丢失 - 有序递送:按发送顺序处理 - 可以附带数据(sigqueue 发送时可以带一个 int 或 void*) - 编号越小优先级越高 演示: sigqueue(<PID>, SIGRTMIN, value1) # 入队 sigqueue(<PID>, SIGRTMIN, value2) # 入队 sigqueue(<PID>, SIGRTMIN, value3) # 入队 # 进程会依次处理 3 次,收到 value1, value2, value3 实际使用中标准信号用得更多,实时信号主要在需要可靠传递的场景使用。 |
signal vs sigaction
- 注册信号处理函数有两种
API:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// ===== signal():简单但有缺陷 ===== #include <signal.h> void handler(int sig) { // 处理信号 } signal(SIGTERM, handler); // 注册 signal(SIGPIPE, SIG_IGN); // 忽略 signal(SIGINT, SIG_DFL); // 恢复默认 // signal() 的问题: // 1. 在某些系统上,处理函数执行一次后会自动恢复为 SIG_DFL // (也就是说第二次收到信号时会执行默认行为而不是你的 handler) // 2. 无法控制信号处理期间阻塞哪些其他信号 // 3. 行为在不同 Unix 系统上不一致(Linux vs macOS vs Solaris) // 结论:了解即可,新代码别用 // ===== sigaction():推荐使用 ===== #include <signal.h> #include <string.h> void handler(int sig, siginfo_t *info, void *ucontext) { // sig:信号编号 // info:信号的详细信息(谁发的、为什么发的) // info->si_pid 发送者的 PID // info->si_uid 发送者的 UID // info->si_code 信号原因(用户发送/内核发送/定时器等) // ucontext:被中断时的 CPU 上下文(寄存器状态等) } struct sigaction sa; memset(&sa, 0, sizeof(sa)); // 处理函数(两种风格选一种) sa.sa_handler = simple_handler; // 简单版:void handler(int sig) // 或者 sa.sa_sigaction = handler; // 详细版:void handler(int, siginfo_t*, void*) sa.sa_flags = SA_SIGINFO; // 用 sa_sigaction 时必须设这个 flag // 在处理 SIGTERM 期间,额外阻塞 SIGINT // (防止处理 SIGTERM 时被 Ctrl+C 打断) sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGINT); // 其他有用的 flags sa.sa_flags |= SA_RESTART; // 被信号中断的系统调用自动重启 // (没有这个 flag,read/write 等会返回 EINTR) sa.sa_flags |= SA_NOCLDSTOP; // 子进程暂停时不发 SIGCHLD sa.sa_flags |= SA_RESETHAND; // 处理一次后恢复默认(模拟 signal() 的行为) sigaction(SIGTERM, &sa, NULL); // 注册 // 第三个参数 NULL 可以换成 &old_sa 来保存旧的处理方式 |
信号掩码操作
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <signal.h> sigset_t set, oldset; // 创建信号集 sigemptyset(&set); // 清空集合 sigfillset(&set); // 填满集合(所有信号) sigaddset(&set, SIGINT); // 添加一个信号 sigdelset(&set, SIGINT); // 移除一个信号 sigismember(&set, SIGINT); // 检查是否在集合中 // 修改进程的信号掩码 sigprocmask(SIG_BLOCK, &set, &oldset); // 阻塞 set 中的信号(追加) sigprocmask(SIG_UNBLOCK, &set, &oldset); // 解除阻塞 set 中的信号 sigprocmask(SIG_SETMASK, &set, &oldset); // 直接设置掩码为 set // oldset 保存修改前的掩码,方便之后恢复 // 查看当前 pending 的信号 sigset_t pending; sigpending(&pending); if (sigismember(&pending, SIGTERM)) { // SIGTERM 正在等待处理 } // ===== 典型用法:临界区保护 ===== // 场景:你在做一组不可中断的操作(比如写临时文件) sigset_t block, old; sigemptyset(&block); sigaddset(&block, SIGINT); sigaddset(&block, SIGTERM); sigprocmask(SIG_BLOCK, &block, &old); // 阻塞信号 // --- 临界区:不会被 SIGINT/SIGTERM 打断 --- write_temp_file(); rename_temp_to_final(); // --- 临界区结束 --- sigprocmask(SIG_SETMASK, &old, NULL); // 恢复原来的掩码 // 如果临界区期间有信号到达,它们现在会被立刻递送 |
信号处理函数中的安全问题
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
// ===== 问题:信号处理函数是异步执行的 ===== // 它可能在你程序执行到任意位置时被调用 // 如果主程序正在调用 printf,信号处理函数里也调用 printf // → 两个 printf 操作同一个 FILE 结构体 → 数据损坏 // ===== 异步信号安全(Async-Signal-Safe)函数 ===== // 信号处理函数中只能调用"异步信号安全"的函数 // POSIX 规定的安全函数列表(常用的): // write() ← 注意不是 printf/puts/fprintf // _exit() ← 注意不是 exit()(exit 会调 atexit 注册的函数) // read() // open() / close() // signal() / sigaction() // kill() / raise() // fork() // waitpid() // getpid() // // 不安全的函数(不能在信号处理函数中调用): // printf / fprintf / puts ← 最常见的坑 // malloc / free / new / delete // exit()(用 _exit() 代替) // 任何获取锁的操作(可能死锁) // ===== 正确的做法 ===== // 方法 1:信号处理函数中只设置一个标志,主循环中检查 volatile sig_atomic_t got_sigterm = 0; // volatile:防止编译器优化掉对它的读取 // sig_atomic_t:保证读写是原子的 void handler(int sig) { got_sigterm = 1; // 只做这一件事 } int main() { signal(SIGTERM, handler); while (!got_sigterm) { // 正常工作... do_work(); // 每次循环检查标志 if (got_sigterm) { // 在主流程中安全地做清理 cleanup(); break; } } return 0; } // 方法 2:用 self-pipe trick(更可靠,用于多路复用场景) int pipe_fd[2]; pipe(pipe_fd); // 创建管道 void handler(int sig) { int saved_errno = errno; // 保存 errno write(pipe_fd[1], "x", 1); // 往管道写一个字节(write 是安全的) errno = saved_errno; // 恢复 errno } // 主循环中用 epoll/select 监听这个管道 // 当管道可读时,说明收到了信号 // 方法 3:用 signalfd(Linux 特有,最现代的方式) #include <sys/signalfd.h> sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); // 阻塞这些信号(这样它们不会触发默认处理) sigprocmask(SIG_BLOCK, &mask, NULL); // 创建 signalfd int sfd = signalfd(-1, &mask, SFD_NONBLOCK); // 现在信号不再通过处理函数递送 // 而是变成了 sfd 这个文件描述符上的可读事件 // 可以用 epoll 监听,和其他 IO 事件统一处理 struct signalfd_siginfo info; read(sfd, &info, sizeof(info)); // info.ssi_signo == SIGTERM 或 SIGINT |
多线程与信号
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// ===== 多线程中的信号行为 ===== // 1. 每个线程有自己独立的信号掩码(blocked 集合) // 2. 信号处理函数是进程级别的(所有线程共享) // 3. 发给进程的信号(kill(pid, sig)),内核会选择一个没有阻塞该信号的线程来递送 // 4. 发给线程的信号(pthread_kill(thread, sig)),只递送给指定线程 // ===== 多线程程序的推荐做法 ===== // 1. 在主线程创建其他线程之前,阻塞所有要处理的信号 // 2. 创建一个专门的信号处理线程,用 sigwait 等待信号 // 3. 其他工作线程不需要关心信号 void* signal_thread(void* arg) { sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigaddset(&set, SIGTERM); while (1) { int sig; sigwait(&set, &sig); // 同步等待信号 if (sig == SIGTERM) { printf("Received SIGTERM, shutting down...\n"); // 通知其他线程退出 // ... break; } } return NULL; } int main() { // 1. 先阻塞信号(子线程会继承这个掩码) sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigaddset(&set, SIGTERM); sigprocmask(SIG_BLOCK, &set, NULL); // 2. 创建信号处理线程 pthread_t sig_thread; pthread_create(&sig_thread, NULL, signal_thread, NULL); // 3. 创建工作线程(它们继承了阻塞掩码,不会被信号打断) pthread_t worker; pthread_create(&worker, NULL, worker_function, NULL); // ... // 线程相关的信号操作 pthread_sigmask(SIG_BLOCK, &set, NULL); // 修改当前线程的掩码 pthread_kill(thread_id, SIGUSR1); // 给特定线程发信号 } |
用 /proc 观察信号状态
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 查看进程 12345 的信号状态 cat /proc/12345/status | grep Sig # 输出类似: # SigQ: 0/63432 待处理信号数量 / 上限 # SigPnd: 0000000000000000 线程级 pending 信号(十六进制位图) # ShdPnd: 0000000000000000 进程级 pending 信号 # SigBlk: 0000000000010000 被阻塞的信号 # SigIgn: 0000000000000004 被忽略的信号 # SigCgt: 0000000180004002 被捕获的信号 # 怎么读这个位图: # 每个十六进制数字代表 4 位,从右到左分别对应信号 1, 2, 3, 4... # 例如 SigIgn: 0000000000000004 # 0x4 = 二进制 0100 → 第 3 位为 1 → 信号 3 (SIGQUIT) 被忽略 |
完整速查表
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
概念 说明 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 信号 内核发给进程的异步通知 signal() 注册处理函数(简单但有缺陷,别用) sigaction() 注册处理函数(推荐) sigprocmask() 设置进程的信号掩码(阻塞/解除阻塞) sigsuspend() 原子地设置掩码并等待信号 sigwait() 同步等待信号(多线程推荐) signalfd() 把信号转成文件描述符事件(Linux 特有) kill() 给进程发信号 raise() 给自己发信号 abort() 给自己发 SIGABRT alarm() 设置定时器,到期发 SIGALRM pause() 挂起直到收到信号 volatile sig_atomic_t 在信号处理函数和主程序之间安全共享的变量类型 async-signal-safe 信号处理函数中可以安全调用的函数 pending 已产生但未递送的信号 blocked 被掩码阻塞的信号(不递送,但不丢弃) ignored 被 SIG_IGN 忽略的信号(递送后丢弃) SIGKILL / SIGSTOP 无法捕获、阻塞或忽略的两个信号 |
日常必须掌握的信号
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
信号 编号 默认行为 能否捕获 常见场景 ───────────────────────────────────────────────────────────── SIGTERM 15 终止进程 ✅ 能 礼貌请求退出(kill 默认发这个) 进程收到后可以做清理工作再退出 SIGKILL 9 强制终止 ❌ 不能 最后手段(kill -9) 进程无法捕获、无法忽略、无法阻塞 内核直接杀掉,不给进程任何清理机会 SIGINT 2 终止进程 ✅ 能 你按 Ctrl+C 时终端发的就是这个 ≈ Windows 的 Ctrl+C SIGQUIT 3 终止 + core dump ✅ 能 你按 Ctrl+\ 时终端发的 会产生 core dump 文件用于调试 SIGSTOP 19 暂停进程 ❌ 不能 暂停进程执行(冻结在原地) ≈ 调试器的 Break All SIGCONT 18 恢复进程 ✅ 能 恢复被 SIGSTOP 暂停的进程 SIGTSTP 20 暂停进程 ✅ 能 你按 Ctrl+Z 时终端发的 和 SIGSTOP 类似但可以被捕获 SIGHUP 1 终止进程 ✅ 能 终端关闭时发给所有子进程 这就是为什么关掉终端编译会中断 用 tmux/nohup 就是为了躲开这个信号 SIGSEGV 11 终止 + core dump ✅ 能 段错误(空指针/越界访问) ≈ Windows 的 Access Violation 你调试 LLVM 时可能会遇到 SIGABRT 6 终止 + core dump ✅ 能 调用 abort() 时产生 assert 失败时就是这个信号 SIGPIPE 13 终止进程 ✅ 能 写入已关闭的管道/socket 管道操作时偶尔遇到 SIGUSR1 10 终止进程 ✅ 能 用户自定义信号 SIGUSR2 12 终止进程 ✅ 能 用户自定义信号 程序可以用这两个做自定义用途 |
Ctrl+C / Ctrl+Z / Ctrl+\ 的区别
- 这三个终端快捷键本质上就是发不同的信号
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# ===== Ctrl+C:发送 SIGINT(中断)===== ninja -C build -j8 # 正在编译 # 按 Ctrl+C # → 终端给前台进程组发 SIGINT # → ninja 收到后会停止编译,清理临时文件,然后退出 # → 这是最常用的"停止当前操作"方式 # ===== Ctrl+Z:发送 SIGTSTP(暂停)===== ninja -C build -j8 # 正在编译 # 按 Ctrl+Z # → 进程被暂停(suspended),但没有被杀死 # → 终端打印:[1]+ Stopped ninja -C build -j8 # → 你回到了 shell 提示符,可以做别的事 # 恢复这个进程: fg # 把暂停的进程放回前台继续运行 bg # 让暂停的进程在后台继续运行 # 或者用 kill: kill -CONT %1 # 发 SIGCONT 恢复 # ===== Ctrl+\:发送 SIGQUIT(退出 + core dump)===== ./my_program # 正在运行 # 按 Ctrl+\ # → 发 SIGQUIT,进程终止并产生 core dump # → 用于程序卡死且 Ctrl+C 无效时 # → core dump 可以用 gdb 分析 # → 比 kill -9 好,因为至少有 core dump 可以调试 |
处理信号
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <csignal> #include <cstdio> #include <cstdlib> // 信号处理函数 void handle_sigterm(int sig) { // 收到 SIGTERM,做清理工作 printf("Received SIGTERM, cleaning up...\n"); // 删除临时文件、保存状态等 // ... exit(0); // 然后正常退出 } void handle_sigint(int sig) { // 用户按了 Ctrl+C printf("\nInterrupted by user\n"); exit(1); } int main() { // 注册信号处理函数 signal(SIGTERM, handle_sigterm); // 捕获 SIGTERM signal(SIGINT, handle_sigint); // 捕获 SIGINT (Ctrl+C) // signal(SIGKILL, ...) // 编译能过但没用——SIGKILL 无法被捕获 // 更推荐用 sigaction(功能更强、行为更可预测): struct sigaction sa; sa.sa_handler = handle_sigterm; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGTERM, &sa, nullptr); // 忽略某个信号 signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE(网络编程常见做法) // 恢复信号的默认行为 signal(SIGTERM, SIG_DFL); // 恢复 SIGTERM 的默认行为 // ... 程序主逻辑 ... while (true) { // 做事情 } return 0; } |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ x86_64汇编学习记述二08/07
- ♥ Linux 线程的同步与互斥03/31
- ♥ Linux_ 命令大全 文档编辑03/16
- ♥ Linux_命令大全 文件传输03/16
- ♥ Linux 高性能服务器编程:IP协议09/04
- ♥ Linux 进程创建&&控制&&终止03/28