惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
F
Fortinet All Blogs
Cisco Talos Blog
Cisco Talos Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Secure Thoughts
美团技术团队
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
月光博客
月光博客
T
Tor Project blog
P
Privacy & Cybersecurity Law Blog
Recorded Future
Recorded Future
I
Intezer
博客园 - 【当耐特】
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
GbyAI
GbyAI
罗磊的独立博客
V
V2EX
Google DeepMind News
Google DeepMind News
D
DataBreaches.Net
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
A
About on SuperTechFans
Scott Helme
Scott Helme
Vercel News
Vercel News
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
Recent Announcements
Recent Announcements
Hacker News: Ask HN
Hacker News: Ask HN
C
CERT Recently Published Vulnerability Notes
G
Google Developers Blog
B
Blog
博客园 - 叶小钗
WordPress大学
WordPress大学
博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Jina AI
Jina AI
IT之家
IT之家
C
Cybersecurity and Infrastructure Security Agency CISA
P
Palo Alto Networks Blog
小众软件
小众软件
博客园 - Franky
Microsoft Azure Blog
Microsoft Azure Blog
AWS News Blog
AWS News Blog

咸糖 - 自律者自由

2025 年终总结:降噪、重构与长期主义 在新加坡和新山吃过最好的食物(持续更新) 写给还在迷茫的你:我的三本大学回忆 2024 年终总结 Neovim: No Crash Incremental Selection 2022 年终总结 使用 neovim 作为 PDE(个性化开发环境) shell 是一个不错的生产力工具 使用二八法则省力地学习 awk 肉身翻墙新加坡安顿指南 使用 Docker Compose 建立你自己的开发环境 关于编写可维护的代码的一些实践与想法 我为什么使用双向链接做笔记? 关于焦虑和拖延症 Golang: 如何处理日渐膨胀的 interface 使用番茄工作法来更好的利用你的时间 Golang: 让你的零值更有用 使用 Mock 和 Interface 进行 Golang 单测 关于 Golang Slice 的一些细节 总结一些计算机常用的原则 重新学习英语语法 上班族近期小半年入门投资基金组合的学习与实践经历 疫情期间的肉身翻墙新加坡指南 About me 软技能:大厂底层员工打工指南 软技能:我是如何获取知识与信息的? 分布式的令牌桶算法的实现 实现一个AtomicInteger GC root 在哪里? 什么是 Minor GC/Major GC 漏桶算法的设计与实现 剑指offer 单例模式 TCP 针对面试学习 Actor 如何处理阻塞消息 Akka 源码解析 How to learn scala AES 需要限制 SEED 长度 Java 如何区分==与.equals()方法 2018年年度总结 Java 集合扩容
Unix 如何杀死一个进程和它的子孙进程?
xiantang · 2022-01-24 · via 咸糖 - 自律者自由

文章目录

【注意】最后更新于 December 29, 2025,文中内容可能已过时,请谨慎使用。

最近周末在维护一个开源项目,叫做 air。它是一个 Golang 的热加载代码的工具,会监听本地的文件变化,然后自动重新加载。

遇到的问题

最近遇到一个特别有意思的问题,就是使用 kill -9 pid 命令杀死进程的时候虽然会杀死它的子进程,但是它的孙子进程还是会继续存活。

背景

简而言之,就是我们的热加载组件会运行命令,然后会监听文件变化,一旦文件变化就会 kill 掉之前进程,然后重新编译代码,再执行运行的命令。

但是遇到一个用户提了这样一个问题:https://github.com/cosmtrek/air/issues/216#issuecomment-982348931 在执行命令的时候使用 dlv exec --accept-multiclient --log --headless --continue --listen :2345 --api-version 2 ./tmp/main 来运行代码与开启调试,我们的组件不会彻底的将进程杀死,而是会继续存活。导致下次一次起来的时候对应的端口会被占用。

排查问题

通过 ps -efj | grep "tmp/main" 能很清楚的看到实际上运行这条命令会起来三个进程

1
2
3
1594910868 75277 74711   0 10:09PM ttys005    0:00.14 dlv exec --accep xt       75277      0    1 S    s005
1594910868 75280 75277   0 10:09PM ttys005    0:00.02 /Library/Develop xt       75280      0    1 S+   s005
1594910868 75281 75280   0 10:09PM ttys005    0:00.01 ./tmp/main       xt       75280      0    1 SX+  s005

而且是很清晰的能看出来,进程的祖孙关系:

75277 是父进程

75280 是子进程

75281 是孙进程

如果你只是采用 kill -9 pid 来杀死进程,那么它的子进程也会被杀死,但是孙子进程还是会继续存活。

1
2
3
> kill -9 75277
> ps -ef | grep "tmp/main"
1594910868 75281     1   0 10:09PM ttys005    0:00.01 ./tmp/main

可以发现只剩下 75281 这个进程了,而且这个进程的父进程现在变成了 1,孤儿进程了。属实是孤儿了。

如果这个进程还继续占用着端口,会造成下次执行命令的时候无法正常热加载。

解决方案

查阅了各种资料之后,找到了一个很好的解决方案:使用 pgid 参数来让进程组的进程共享一个进程组号。

1
2
3
4
  PID  PPID  PGID   UID   C STIME   TTY             TIME CMD              
75837 74711 75837 1594910868   0 10:22PM ttys005    0:00.23 dlv exec --accep 
75840 75837 75840 1594910868   0 10:22PM ttys005    0:00.02 /Library/Develop 
75841 75840 75840 1594910868   0 10:22PM ttys005    0:00.01 ./tmp/main       

可以看到第三列就是对应的 pgid,虽然我们使用命令启动的 pgid 不同,但是我们可以使用 Golang 来设置进程组号,这样就可以共享进程组号了。

同时在 kill 进程的时候,也要使用这个 pgid 参数,这样就可以杀死对应的进程组了。可以参考 man kill

Negative PID values may be used to choose whole process groups;see the PGID column in ps command output。

就是对于 pid 的代表的是 PGID 也就是整个进程组,kill 的时候会将整个进程组中的进程杀死。

虽然在上面的命令中是没办法共享进程的,但是对于这个 bug 来说,我们可以使用 Setpgid 来开启 PGID,这样启动的进程就可以共享进程组号了。同时使用 syscall.Kill(-pgid, 15) 来杀死进程组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cmd := exec.Command( some_command )
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Start()

pgid, err := syscall.Getpgid(cmd.Process.Pid)
if err == nil {
syscall.Kill(-pgid, 15)  // note the minus sign
}

cmd.Wait()

总结

在单测中添加相关的单测,保证 kill 所有子进程这个行为不会因为迭代而丢失。https://github.com/cosmtrek/air/commit/1c27effe33a180f3fbbcee8f2d9ea7122d89a50b#diff-6266cec6be43e607de84d431f656ea78fac62405058d84312d9c12f3f52c7462R146

参考资料

推荐环节

最后最后和大家分享一些最近在看的好文,想过用周刊的方式发送但是因为看的比较零散,就放在每篇博文的最后,希望大家能够收获!

文章作者 xiantang

上次更新 2025-12-29 (4c152d04)

赞赏支持

微信打赏 支付宝打赏