在 Linux 系统中,信号是进程间通信和系统通知的重要机制。理解进程间的信号交互对于调试程序、分析系统行为至关重要。sigtrace 是一款基于 ptrace 机制的信号跟踪工具,能够捕获目标进程接收的信号并分析信号发送方的信息,为开发者提供了深入了解进程间信号交互的能力。
一、工具核心功能与价值
sigtrace 的核心功能是跟踪目标进程接收的信号,主要解决两个关键问题:
- 信号来源追踪:当进程收到信号时,识别发送信号的进程及其完整的父进程链
- 信号管控:可选择性地阻断特定信号传递到目标进程
这一工具在多个场景中具有实用价值:
- 调试程序异常退出时,快速定位是谁发送了终止信号(如 SIGTERM、SIGKILL)
- 分析进程间的信号交互模式,理解复杂系统中各组件的协作机制
- 临时阻断某些干扰性信号,以便观察进程在特定信号缺失时的行为
二、实现原理:基于 ptrace 的进程跟踪机制
sigtrace 的核心实现依赖于 Linux 提供的 ptrace 系统调用,这是一种进程间调试机制,允许一个进程(跟踪器)控制另一个进程(被跟踪者)的执行。其工作流程可分为三个阶段:
1. 建立跟踪关系
- 启动新进程:若用户指定待执行程序,sigtrace 会通过 fork 创建子进程,子进程调用 ptrace(PTRACE_TRACEME) 将自己设置为被跟踪状态,随后执行目标程序
- 附加到现有进程:若用户指定 PID,sigtrace 直接通过 ptrace(PTRACE_ATTACH) 附加到目标进程,此时目标进程会暂停执行,等待跟踪器的进一步指令
2. 信号捕获与处理
被跟踪进程每次接收信号时会暂停执行,触发跟踪器的 waitpid 调用返回。此时:
- 跟踪器通过 ptrace(PTRACE_GETSIGINFO) 获取信号详细信息,包括信号编号、发送进程 PID、发送者 UID 等
- 根据用户配置的信号过滤规则(show/block 列表),决定是否展示信号信息或阻断信号传递
- 若启用 -a 选项,跟踪器会临时附加到信号发送进程,防止其快速退出导致的信息丢失
3. 进程信息收集
为了展示信号发送方的上下文,sigtrace 通过解析 /proc
文件系统获取进程信息:
- 从
/proc/<pid>/status
读取进程名称、父进程 PID 等基本信息 - 从
/proc/<pid>/exe
读取进程可执行文件的路径(通过 readlink 系统调用) - 从
/proc/<pid>/cmdline
读取进程启动命令行参数 - 通过递归查询父进程信息,构建完整的进程族谱
三、设计思路:模块化与可扩展性
cpp
void signal_handler(int signum);
const char * const get_signal_name_from_id(const int signal);
void print_process_info(const pid_t pid);
uint64_t list_to_bitmask(char * const comma_sep_list);
char signal_is_in_bitmask(const int signal, const uint64_t bitmask);
...
int main(int argc, char *argv[], char *envp[])
{
...
const struct option option_list[] = {
{"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'v'},
{"block", required_argument, 0, 'b'},
{"show", required_argument, 0, 's'},
{"pid", required_argument, 0, 'p'},
{"quiet", no_argument, 0, 'q'},
{"attach_to_sender", no_argument, 0, 'a'},
{0, 0, 0, 0}
};
int option;
struct sigaction sa = { .sa_handler = signal_handler };
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
while ((option = getopt_long(argc, argv, "+hvb:s:p:qa", &option_list[0], NULL)) > 0) {
char stop = 0;
switch (option) {
case 'h':
print_help();
return 0;
case 'v':
printf("sigtrace version 0.1.0\n");
return 0;
case 'b':
block_signal_mask = list_to_bitmask(optarg);
break;
case 's':
show_signal_mask = list_to_bitmask(optarg);
break;
case 'q':
quiet = 1;
break;
case 'a':
attach_to_sender = 1;
break;
case 'p':
pid = atoi(optarg);
break;
default:
stop = 1;
break;
}
if (stop) {
break;
}
}
block_signal_mask |= 1 << SIGTRAP; /* 不要转发SIGTRAP信号 */
if (pid == 0) {
/* 尝试以子进程的形式执行给定的程序 */
if (optind < argc) {
if ((pid = fork()) < 0) {
fprintf(stderr, "fork failed: %s\n", strerror(pid));
return 1;
}
if (pid == 0) {
/* Child process */
if (ptrace (PTRACE_TRACEME, 0, NULL, NULL) < 0) {
fprintf(stderr, "ptrace failed: %s\n", strerror(errno));
return 1;
}
if (execve(argv[optind], &argv[optind], envp) < 0) {
fprintf(stderr, "execve failed: %s\n", strerror(errno));
return 1;
}
return 0;
}
} else {
fprintf(stderr, "Neither a PID nor a program to execute were given.\n");
return 1;
}
} else {
if (ptrace (PTRACE_ATTACH, pid, NULL, NULL) < 0) {
fprintf(stderr, "Attach failed: %s\n", strerror(errno));
return 1;
}
waitpid(pid, &status, 0);
printf("Attached to %d\n", pid);
if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) {
fprintf(stderr, "%s\n", strerror(errno));
return 1;
}
}
while(running) {
waitpid(pid, &status, 0);
/* 进程已停止,获取并处理信号 */
siginfo_t signal;
if (ptrace(PTRACE_GETSIGINFO, pid, NULL, &signal) < 0) {
if (errno != ESRCH) {
fprintf(stderr, "%s\n", strerror(errno));
return 1;
}
break;
}
if (quiet == 0 && (signal.si_pid != 0 || signal.si_uid != 0) && signal_is_in_bitmask(signal.si_signo, show_signal_mask)) {
char attached = 0;
/* 尝试停止信号发送过程,以避免信息丢失。由于此为可选操作,因此忽略错误。 */
if (attach_to_sender != 0 && ptrace(PTRACE_ATTACH, signal.si_pid, NULL, NULL) == 0) {
attached = 1;
}
printf("Got %s (%d) with uid %u\n",
get_signal_name_from_id(signal.si_signo),
signal.si_signo,
signal.si_uid);
print_process_info(signal.si_pid);
if (attached && waitpid(signal.si_pid, &status, 0) > 0) {
ptrace(PTRACE_DETACH, signal.si_pid, NULL, NULL);
}
}
if (ptrace(PTRACE_CONT, pid, NULL, signal_is_in_bitmask(signal.si_signo, block_signal_mask) ? NULL : (void *)(uintptr_t)signal.si_signo) < 0) {
if (errno != ESRCH) {
fprintf(stderr, "%s\n", strerror(errno));
return 1;
}
break;
}
}
ptrace(PTRACE_DETACH, pid, NULL, NULL);
return 0;
}
If you need the complete source code, please add the WeChat number (c17865354792)
sigtrace是一个Linux工具,它捕获发送给目标进程的信号(使用ptrace),并提供有关信号发送过程的信息。此外,还可以阻止信号传递给目标进程。
以下代码片段提供了输出示例:
cpp
$ ./sigtrace /bin/cat
Got SIGTERM (15) with uid 1000
21082 bash [/bin/bash]
/bin/bash
21075 /usr/bin/termin [/usr/bin/python2.7]
/usr/bin/python /usr/bin/terminator
866 openbox [/usr/bin/openbox]
/usr/bin/openbox --startup /usr/lib/x86_64-linux-gnu/openbox-autostart OPENBOX
858 lightdm
lightdm --session-child 13 16
817 lightdm
/usr/sbin/lightdm
1 systemd
/sbin/init
在这种情况下,从进程21082(bash)收到了SIGTERM信号,以下信息是21082的父进程。
sigtrace 可以附加到现有的进程 ID(pid)上,也可以执行给定的程序。
要执行给定的程序,只需在命令行末尾附加程序路径及其参数即可。以ls为例,命令应为sigtrace /bin/ls -ahl。由于PATH变量不会被评估,因此需要指定完整路径。当前环境变量会提供给被执行的进程。
或者,可以使用-p参数附加到已运行的进程。其语法为sigtrace -p 。sigtrace将保持附加状态,直到目标进程退出或sigtrace本身关闭。
要阻止信号,可以使用-b参数。该参数接受一个以逗号分隔的信号号列表,范围从1到63。以下示例将阻止SIGINT和SIGTERM:sigtrace -b 2,15 -p 。
还有就是,sigtrace 的代码结构体现了清晰的模块化设计思想,主要分为以下几个功能模块:
1. 信号处理模块
- 信号名称映射:通过 get_signal_name_from_id 函数将信号编号转换为可读性更强的名称(如 SIGTERM)
- 信号过滤机制:使用位掩码(bitmask)高效实现信号的包含/排除规则,通过 list_to_bitmask 函数将用户输入的逗号分隔信号列表转换为位掩码
2. 进程跟踪模块
- 跟踪关系管理:处理进程的启动、附加、脱离等生命周期操作
- 信号捕获循环:通过持续调用 waitpid 和 ptrace 系列函数,实现对目标进程状态变化的实时监控
3. 信息展示模块
- 进程信息打印:print_process_info 函数负责解析 /proc 信息并格式化输出
- 信号事件报告:将捕获的信号事件与进程信息关联,形成易于理解的跟踪日志
4. 配置解析模块
- 通过 getopt_long 处理命令行选项,支持多种运行模式(附加到进程/启动新进程)和过滤规则
四、相关领域知识点
理解 sigtrace 需要掌握以下 Linux 系统编程概念:
1. 信号机制
Linux 信号是异步事件通知机制,用于通知进程发生的特定事件(如中断、异常)。每个信号有固定编号和默认行为(如终止、暂停),进程可通过信号处理函数自定义行为。部分信号(如 SIGKILL、SIGSTOP)无法被捕获或忽略,这也是 sigtrace 的局限性之一。
2. ptrace 系统调用
ptrace 是 Linux 提供的进程调试接口,除了用于调试器实现,还广泛用于跟踪进程执行、监控系统调用等场景。其工作原理是通过修改被跟踪进程的寄存器状态和内存,实现执行控制(暂停、继续、单步执行)和状态查询。
3. /proc 文件系统
这是 Linux 提供的虚拟文件系统,通过文件接口暴露系统和进程的实时信息。sigtrace 利用其中的进程相关文件(status、exe、cmdline)获取进程元数据,这也是其依赖 Linux 特定环境的原因。
4. 进程生命周期管理
涉及 fork、execve、waitpid 等系统调用的协作:fork 创建新进程,execve 加载新程序替换进程镜像,waitpid 等待子进程状态变化,共同构成了进程创建和跟踪的基础。
五、局限性与优化方向
尽管 sigtrace 实现了核心功能,但仍存在一些局限:
- 对快速退出的信号发送进程,即使使用 -a 选项也可能无法捕获完整信息
- 依赖 /proc 文件系统,无法在不支持该机制的系统上运行
- 部分特殊信号(如 SIGKILL)无法被跟踪或阻断,受限于 Linux 内核设计
可能的优化方向包括:
- 结合 eBPF 技术实现更高效、低侵入的信号跟踪
- 增加信号发送频率统计,识别异常信号模式
- 支持信号参数解析,提供更详细的信号上下文信息
总结
sigtrace 作为一款轻量级信号跟踪工具,巧妙结合了 ptrace 调试机制、/proc 文件系统和信号处理机制,为理解 Linux 进程间信号交互提供了实用视角。其设计思路体现了 Unix 工具"做一件事并做好"的哲学,通过模块化结构实现了功能的清晰分离。对于系统管理员和开发者而言,理解 sigtrace 的工作原理不仅能更好地使用该工具,也能加深对 Linux 进程管理和信号机制的理解。
Welcome to follow WeChat official account【程序猿编码】