Go语言中Kill子进程的正确姿势
场景
我们在编写部署系统的时候,通常需要在机器上部署一个agent,用来执行部署脚本,为了防止部署脚本写的有问题,长时间hang住,我们通常会为脚本的执行设置一个超时时间,到了时间之后就kill掉该脚本的进程。如果是Go语言实现,脑袋里应该立马浮现出
os/exec包、cmd.Process.Kill()这样的手段。但是,如果部署脚本中又调用了其他脚本,即子进程又fork出更多子进程的时候,这招就不好使了。
简单来说,就是
cmd.Process.Kill()无法杀死子进程。
知识储备
pstree -g #查看进程树和每个进程的PGID
问题验证
下面我们写段代码来简单验证一下
一般情况下:
package main
import (
"fmt"
"os/exec"
"time"
)
func main() {
cmd := exec.Command("sleep", "5") //睡眠5s
start := time.Now() //记录启动时间
time.AfterFunc(3*time.Second, func() { cmd.Process.Kill() }) //3s后将此进程杀死
err := cmd.Run() //运行该命令
fmt.Printf("pid=%d duration=%s err=%s\n", cmd.Process.Pid, time.Since(start), err) //输出进程ID、运行时间、错误
}
打开终端查看:发现进程能被kill掉
root@flight:~$ ps au
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
root 1804 0.0 0.0 4278124 996 s000 R+ 4:10下午 0:00.00 ps au
didi 1158 0.0 0.0 4296892 1284 s000 S 4:08下午 0:00.03 -bash
didi 1798 0.0 0.0 4270348 564 s001 S+ 4:10下午 0:00.00 sleep 5
root@flight:~$ ps au
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
root 1819 0.0 0.0 4268908 972 s000 R+ 4:10下午 0:00.00 ps au
didi 1158 0.0 0.0 4296892 1300 s000 S 4:08下午 0:00.03 -bash
root@flight:~$
当进程fork出子进程:
package main
import (
"fmt"
"os/exec"
"time"
)
func main() {
cmd := exec.Command("/bin/sh", "-c", "watch date > date.txt") //watch进程fork出了其他子进程
start := time.Now() //记录启动时间
time.AfterFunc(3*time.Second, func() { cmd.Process.Kill() }) //3s后将此进程杀死
err := cmd.Run() //运行该命令
fmt.Printf("pid=%d duration=%s err=%s\n", cmd.Process.Pid, time.Since(start), err) //输出进程ID、运行时间、错误
}
输出结果:发现同样运行了3s被kill
[root@localhost ~]# go run test.go
pid=16860 duration=3.001284491s err=signal: killed #同样运行了3s被kill
[root@localhost ~]#
但是查看用户进程会发现不一样:
[root@localhost ~]# ps -af
UID PID PPID C STIME TTY TIME CMD
root 2409 2277 0 17:07 pts/1 00:00:01 top
root 5118 4953 0 17:09 pts/3 00:00:00 top
root 16804 2269 14 17:14 pts/0 00:00:00 go run test.go
root 16855 16804 0 17:14 pts/0 00:00:00 /tmp/go-build276131739/b001/exe/test
root 16860 16855 0 17:14 pts/0 00:00:00 /bin/sh -c watch date > date.txt
root 16861 16860 0 17:14 pts/0 00:00:00 watch date
root 16954 4919 0 17:14 pts/2 00:00:00 ps -af
[root@localhost ~]#
[root@localhost ~]#
[root@localhost ~]# ps -af
UID PID PPID C STIME TTY TIME CMD
root 2409 2277 0 17:07 pts/1 00:00:01 top
root 5118 4953 0 17:09 pts/3 00:00:00 top
root 16861 1 0 17:14 pts/0 00:00:00 watch date
root 17169 4919 0 17:14 pts/2 00:00:00 ps -af
现象:
- 程序运行3s后
/bin/sh -c watch date > date.txt被kill,但是watch date依然存在。 watch date的父进程是1号进程。
问题原因
Go是使用kill(2)向sh进程的PID发了一个KILL信号,但没有发给watch进程,sh进程被kill之后,导致watch进程变成孤儿进程。
解决方案
kill(2)不但支持向单个PID发送信号,还可以向进程组发信号,我们只要把sh进程及其所有子进程放到一个进程组里,就可以批量Kill了。关键是PGID的设置,默认情况下,子进程会把自己的PGID设置成与父进程相同,所以,我们只要设置了sh进程的PGID,所有子进程也就相应的有了PGID。
注意:传递进程组PGID的时候要使用负数的形式。
注意:下面这种方式适合非su - <user> command执行命令的方式,否则杀死父进程后,子进程将被托管成为孤儿进程。因为他们的PGID不一样,即使设置了cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}也没有用。正确的方式是采用exec包自带的方式来指定执行用户
package main
import (
"fmt"
"os/exec"
"syscall"
"time"
)
func main() {
cmd := exec.Command("/bin/sh", "-c", "watch date > date.txt")
// Go会将PGID设置成与PID相同的值
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Setpgid=true
start := time.Now()
time.AfterFunc(3*time.Second, func() { syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) }) //想要杀死整个进程组,而不是单个进程,需要传递负数形式。
err := cmd.Run()
fmt.Printf("pid=%d duration=%s err=%s\n", cmd.Process.Pid, time.Since(start), err)
}
如果想要指定用户:
sysuser, err := user.Lookup("user1") // 通过用户名来获取用户信息
if err != nil {
fmt.Println(err)
}
uid, err := strconv.Atoi(sysuser.Uid) // 将UID的类型转换成 uint32
if err != nil {
fmt.Println(err)
}
gid, err := strconv.Atoi(sysuser.Gid) // 将GID的类型转换成 uint32
if err != nil {
fmt.Println(err)
}
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
}
参考文档
Go语言中Kill子进程的正确姿势的更多相关文章
- int转换char的正确姿势
一:背景 在一个项目中,我需要修改一个全部由数字(0~9)组成的字符串的特定位置的特定数字,我采用的方式是先将字符串转换成字符数组,然后利用数组的位置来修改对应位置的值.代码开发完成之后,发现有乱码出 ...
- C语言中,头文件和源文件的关系(转)
简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 1.预处理阶段 2.词法与语法分析阶段 3.编译阶段,首先编译成纯汇编语句, ...
- C语言中do...while(0)的妙用(转载)
转载来自:C语言中do...while(0)的妙用,感谢分享. 在linux内核代码中,经常看到do...while(0)的宏,do...while(0)有很多作用,下面举出几个: 1.避免goto语 ...
- C语言中system()函数的用法总结(转)
system()函数功能强大,很多人用却对它的原理知之甚少先看linux版system函数的源码: #include <sys/types.h> #include <sys/wait ...
- C语言中void*详解及应用
void在英文中作为名词的解释为“空虚:空间:空隙”:而在C语言中,void被翻译为“无类型”,相应的void *为“无类型指针”.void似乎只有“注释”和限制程序的作用,当然,这里的“注释”不是为 ...
- (七)C语言中的void 和void 指针类型
许多初学者对C中的void 和void 的指针类型不是很了解.因此常常在使用上出现一些错误,本文将告诉大家关于void 和void 指针类型的使用方法及技巧. 1.首先,我们来说说void 的含义: ...
- 转:C语言中的static变量和C++静态数据成员(static member)
转自:C语言中的static变量和C++静态数据成员(static member) C语言中static的变量:1).static局部变量 a.静态局部变量在函数内定义,生存期为整个程序 ...
- C语言中.h和.c文件解析(很精彩)
C语言中.h和.c文件解析(很精彩) 简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 1.预处理阶段 2.词法与语法分析 ...
- C语言中.h和.c文件解析
整理自C语言中.h和.c文件解析(很精彩) Part.1(林锐<高质量C/C++编程>) 通过头文件来调用库功能.在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的 ...
- C/C++语言中const的用法
1. const 在C和C++中的区别 C++中的const正常情况下是看成编译期的常量,编译器并不为const分配空间,只是在编译的时候将期值保存在名字表中,并在适当的时候折合在代码中. 所 ...
随机推荐
- [Contract] 一次搞懂 Solidity 的 using xx for xx
using A for *; # 把 A 的函数附给任意类型使用 using A for B; # 意思是把 A 中的方法附给 B 使用 使用上面的方式,那么在我们的合约中定义了 B 类型的变量 ...
- WPF 应用启动过程同时启动多个 UI 线程且访问 ContentPresenter 可能让多个 UI 线程互等
在应用启动过程里,除了主 UI 线程之外,如果还多启动了新的 UI 线程,且此新的 UI 线程碰到 ContentPresenter 类型,那么将可能存在让新的 UI 线程和主 UI 线程互等.这是多 ...
- RT-Thread线程同步与线程通信
一.线程同步 线程同步的使用场景 例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示,下图描述了两个线程间的数据传递: ...
- async 与 promise 的区别
async函数会引式返回一个promise,而promise的resolve值就是函数return的值 使用async和await明显节约了不少代码,不需要.then,不需要写匿名函数处理promis ...
- vue路由跳转的三种方式
目录 1.router-link [实现跳转最简单的方法] 2.this.$router.push({ path:'/user'}) 3.this.$router.replace{path:'/' } ...
- 03. x86基础指令
[说明] x86指令代码语法 制作程序时,指令数据使用代码表示,这些指令代码称为汇编代码,汇编代码由汇编器转换为对应的指令数据和数学数据. x86指令代码主要有两种语法:英特尔语法.AT&T语 ...
- mod操作符效率高吗?
编程语言中mod取余操作符%的效率不是很高,比如M = N % 10,它花费得时间本机测试是1ms,而如果使用M = N - N / 10 * 10,则只需要0.1ms. 所以平时变成得时候,可以尽量 ...
- selenium 滚动截图参考
Selenium本身并不直接支持滚动截图,但是你可以通过编程方式实现滚动截图.下面是一个Python的例子,使用Selenium和PIL库实现滚动截图: from selenium import we ...
- WPF自定义控件,如何使得xaml涉及器中的修改能立即反应到预览
这是我无意中发现的,xaml中设置的是依赖属性而不是包装器,所以我们可以直接在注册依赖属性那里设置回调,触发某个控件重绘,比如本身或父控件重绘. xaml设计器就会实时更新 1 // !!!由于xam ...
- Expander展开收缩动画
这个问题困扰了我一天,最后下了个MaterialDesign的demo,看了下他的源码,才恍然大悟,原来很简单. 我原来的设想是在expander的ControlTemplate设置触发器,在IsEx ...