Docker环境下带CGO的Go进程如何获取信号发送者的PID与PPID
如何在Docker环境中获取信号发送者的真实PID与PPID?
环境结构
- 包含C信号处理函数的
main.go - 用
go build编译后在Docker中运行,docker-compose.yml配置了-v /:/rootfs:ro(挂载主机根文件系统到容器内)
实验步骤
- 在Docker外部执行
kill命令向容器进程发送信号 - 执行
docker kill命令终止容器进程 - 进入Docker容器内部执行
kill命令向目标进程发送信号
实验结果
- Docker外部
kill触发:
Received signal: 15 Sent by PID: 0 Signal code: 0 User ID: 0
docker kill触发:
Received signal: 15 Sent by PID: 0 Signal code: 0 User ID: 0
- Docker内部
kill触发:
Received signal: 15 Sent by PID: 15 Signal code: 0 User ID: 230030 Name: kworker/1:0H State: S (sleeping) Tgid: 15 Ngid: 0 Pid: 15 PPid: 2 TracerPid: 0 Uid: 0 0 0 0 Gid: 0 0 0 0 FDSize: 64 Groups: NStgid: 15 NSpid: 15 NSpgid: 0 NSsid: 0 Threads: 1 SigQ: 1/7980 SigPnd: 0000000000000000 ShdPnd: 0000000000000000 SigBlk: 0000000000000000 SigIgn: ffffffffffffffff SigCgt: 0000000000000000 CapInh: 0000000000000000 CapPrm: 0000003fffffffff CapEff: 0000003fffffffff CapBnd: 0000003fffffffff CapAmb: 0000000000000000 Speculation_Store_Bypass: vulnerable Cpus_allowed: 2 Cpus_allowed_list: 1 Mems_allowed: 1 Mems_allowed_list: 0 voluntary_ctxt_switches: 5 nonvoluntary_ctxt_switches: 0
解决方案
要获取信号发送者的真实PID与PPID,需要结合Linux信号机制和Docker命名空间特性,具体步骤如下:
1. 升级信号处理函数,获取命名空间内的发送者PID
通过sigaction注册带有SA_SIGINFO标志的信号处理函数,这样就能拿到siginfo_t结构体,其中si_pid字段是发送者在当前容器PID命名空间中的ID。
示例C部分代码:
#include <signal.h> void handle_signal(int sig, siginfo_t *info, void *context); void setup_signal_handler() { struct sigaction sa; sa.sa_sigaction = handle_signal; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO; // 以SIGTERM为例,可根据需要添加其他信号 sigaction(SIGTERM, &sa, NULL); }
2. 通过挂载的主机根文件系统查询真实PID
由于容器挂载了主机/到/rootfs,可以读取主机的proc文件系统来转换命名空间PID到真实PID:
- 对于容器内获取到的命名空间PID,读取
/rootfs/proc/<ns_pid>/status文件中的NSpid字段,第二个数值就是主机上的真实PID - 再通过真实PID读取
/rootfs/proc/<real_pid>/status中的PPid字段,得到父进程ID
示例Go结合cgo的完整代码:
package main /* #include <signal.h> #include <stdio.h> extern void handle_signal(int sig, siginfo_t *info, void *context); void setup_handler() { struct sigaction sa; sa.sa_sigaction = handle_signal; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO; sigaction(SIGTERM, &sa, NULL); } */ import "C" import ( "bufio" "fmt" "os" "strings" "unsafe" ) //export handle_signal func handle_signal(sig C.int, info *C.struct_siginfo_t, context unsafe.Pointer) { nsPid := int(info.si_pid) fmt.Printf("收到信号: %d\n", sig) fmt.Printf("发送者命名空间PID: %d\n", nsPid) if nsPid == 0 { fmt.Println("注意:发送者PID为0(通常由内核或Docker守护进程触发)") return } // 读取主机proc文件获取真实PID statusPath := fmt.Sprintf("/rootfs/proc/%d/status", nsPid) file, err := os.Open(statusPath) if err != nil { fmt.Printf("打开status文件失败: %v\n", err) return } defer file.Close() scanner := bufio.NewScanner(file) var realPid int for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "NSpid:") { parts := strings.Fields(line) if len(parts) >= 2 { fmt.Sscanf(parts[1], "%d", &realPid) break } } } if realPid == 0 { fmt.Println("获取真实PID失败") return } fmt.Printf("发送者真实PID: %d\n", realPid) // 获取真实PPID realStatusPath := fmt.Sprintf("/rootfs/proc/%d/status", realPid) file, err = os.Open(realStatusPath) if err != nil { fmt.Printf("打开真实PID的status文件失败: %v\n", err) return } defer file.Close() scanner = bufio.NewScanner(file) var ppid int for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "PPid:") { parts := strings.Fields(line) if len(parts) >= 2 { fmt.Sscanf(parts[1], "%d", &ppid) break } } } fmt.Printf("发送者真实PPID: %d\n", ppid) } func main() { C.setup_handler() // 阻塞进程等待信号 select {} }
3. 处理PID为0的特殊场景
当si_pid为0时,说明信号由内核直接发送(比如docker kill是Docker守护进程请求内核发送信号),此时可以通过以下方式追踪:
- 在主机上启用
auditd服务,配置监控信号发送事件,通过审计日志找到触发信号的进程 - 直接查看主机上Docker守护进程的PID(通常是
dockerd或containerd),这类信号的最终发起方就是Docker相关进程
内容的提问来源于stack exchange,提问作者WeiAnHsieh
相关产品推荐
相关产品推荐

