场景

我们在编写部署系统的时候,通常需要在机器上部署一个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子进程的正确姿势

Go语言中Kill子进程的正确姿势的更多相关文章

  1. int转换char的正确姿势

    一:背景 在一个项目中,我需要修改一个全部由数字(0~9)组成的字符串的特定位置的特定数字,我采用的方式是先将字符串转换成字符数组,然后利用数组的位置来修改对应位置的值.代码开发完成之后,发现有乱码出 ...

  2. C语言中,头文件和源文件的关系(转)

    简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 1.预处理阶段 2.词法与语法分析阶段 3.编译阶段,首先编译成纯汇编语句, ...

  3. C语言中do...while(0)的妙用(转载)

    转载来自:C语言中do...while(0)的妙用,感谢分享. 在linux内核代码中,经常看到do...while(0)的宏,do...while(0)有很多作用,下面举出几个: 1.避免goto语 ...

  4. C语言中system()函数的用法总结(转)

    system()函数功能强大,很多人用却对它的原理知之甚少先看linux版system函数的源码: #include <sys/types.h> #include <sys/wait ...

  5. C语言中void*详解及应用

    void在英文中作为名词的解释为“空虚:空间:空隙”:而在C语言中,void被翻译为“无类型”,相应的void *为“无类型指针”.void似乎只有“注释”和限制程序的作用,当然,这里的“注释”不是为 ...

  6. (七)C语言中的void 和void 指针类型

    许多初学者对C中的void 和void 的指针类型不是很了解.因此常常在使用上出现一些错误,本文将告诉大家关于void 和void 指针类型的使用方法及技巧. 1.首先,我们来说说void 的含义: ...

  7. 转:C语言中的static变量和C++静态数据成员(static member)

    转自:C语言中的static变量和C++静态数据成员(static member) C语言中static的变量:1).static局部变量        a.静态局部变量在函数内定义,生存期为整个程序 ...

  8. C语言中.h和.c文件解析(很精彩)

    C语言中.h和.c文件解析(很精彩)   简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 1.预处理阶段 2.词法与语法分析 ...

  9. C语言中.h和.c文件解析

    整理自C语言中.h和.c文件解析(很精彩) Part.1(林锐<高质量C/C++编程>) 通过头文件来调用库功能.在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的 ...

  10. C/C++语言中const的用法

    1. const 在C和C++中的区别     C++中的const正常情况下是看成编译期的常量,编译器并不为const分配空间,只是在编译的时候将期值保存在名字表中,并在适当的时候折合在代码中. 所 ...

随机推荐

  1. 学习 Avalonia 框架笔记 如何创建一个全屏置顶的 X11 应用窗口

    本文记录我从 Avalonia 框架里面学到如何创建一个全屏置顶的 X11 应用窗口的方法 开始之前,先从 Avalonia 或 CPF 里面拷贝足够的代码,这部分代码可以从本文末尾找到下载方法 设置 ...

  2. dotnet C# 基于 INotifyPropertyChanged 实现一个 CLR 属性绑定辅助类

    习惯了 WPF 或 UWP 等的依赖属性的绑定机制之后,我在写 CLR 属性时,有时也期望将两个 CLR 属性给绑定到一起.在 dotnet 里,提供了 System.ComponentModel.I ...

  3. 超级好用的 HBase GUI 工具分享

    超级好用的 HBase GUI 工具分享 你是否曾为 HBase 数据管理而苦恼?别担心,这一款超级好用的 HBase GUI (HBase Assistant)工具,让您在大数据世界中游刃有余.不再 ...

  4. 有意思!一个关于 Spring 历史的在线小游戏

    发现 Spring One 的官网上有个好玩的彩蛋,分享给大家! 进到Spring One的官网,可以看到右下角有个类似马里奥游戏中的金币图标. 点击该金币之后,会打开一个新的页面,进入下面这样一个名 ...

  5. 由初中生实现的 Windows 12 网页版!

    大家好,我是 Java陈序员. 这几天,逛 Github 的时候,看到了一个项目 win12 -- 仿 Windows12 网页版!被它实现的页面功能震撼到了,大家可以一起来感受下! 首先是登录页面. ...

  6. 11、操作系统安全加固-Windows 加固

    1.账号管理与认证授权 1.1.按用户类型分配账号 目的:根据系统要求,设定不同账户和组,管理员.数据库sa.审计用户.来宾用户等 实施方法: 打开本地用户和计算机管理器 或 打开运行,输入 lusr ...

  7. 适合IT团队的在线文档私人分享工具——showdoc部署

    转载文章: https://www.lixian.fun/3617.html

  8. vben集成keycloak

    前言 公司的项目是vben admin框架需要集成keycloak,那keycloak大家应该都不陌生了,就是统一认证的一个系统简称IDS.之前用过cas,并重构过cas的前端界面,所以对此也是比较熟 ...

  9. 原生微信小程序

    new Date 跨平台兼容性问题 在 Andriod 使用 new Date("2018-05-30 00:00:00")木有问题,但是在ios 下面识别不出来.因为 IOS 下 ...

  10. 学习c# 7.0-7.3的ref、fixed特性并在Unity下测试

    1.ref的一些运用 1.1 ref readonly 关于ref,一个主要应用是防止结构体拷贝,若返回的结构体不需要修改则用ref readonly,类似c++的const标记 : private ...